草庐IT

[Java]Spring6(动力节点老杜)

萤火虫的小尾巴 2023-12-23 原文

文章目录


🥽 课件&资料

老杜Spring6课件:https://www.yuque.com/docs/share/866abad4-7106-45e7-afcd-245a733b073f?# 《Spring6》
密码:mg9b
资料:https://pan.baidu.com/s/1HsmuLXtGLxifwNGr4vu1Rw?pwd=1234
提取码:1234

🥽 Spring启示录

🌊 OCP开闭原则

  • 什么是OCP?
    • OCP是软件七大开发原则当中最基本的一个原则:开闭原则
  • 对什么开?对扩展开放。
  • 对什么闭?对修改关闭。
  • OCP原则是最核心的,最基本的,其他的六个原则都是为这个原则服务的。
  • OCP开闭原则的核心是什么?
    • 只要你在扩展系统功能的时候,没有修改以前写好的代码,那么你就是符合OCP原则的。
    • 反之,如果在扩展系统功能的时候,你修改了之前的代码,那么这个设计是失败的,违背OCP原则。
  • 当进行系统功能扩展的时候,如果动了之前稳定的程序,修改了之前的程序,之前所有程序都需要进行重新测试。这是不想看到的,因为非常麻烦。

🌊 依赖倒置原则(DIP原则)

  • 什么是依赖倒置原则?
    • 面向接口编程,面向抽象编程,不要面向具体编程。
  • 依赖倒置原则的目的?
    • 降低程序的耦合度,提高扩展力。
  • 什么叫做符合依赖倒置?
    • 上 不依赖 下,就是符合。
  • 什么叫做违背依赖倒置?
    • 上 依赖 下,就是违背。
    • 只要“下”一改动,“上”就受到牵连。

🌊 控制反转

  • 当前程序的设计,显然既违背OCP,又违背DIP,怎么办?
    • 可以采用“控制反转”这种编程思想来解决这个问题。
  • 什么是控制反转?
    • 控制反转:IoC(Inversion of Control)
    • 反转是什么呢?
    • 反转的是两件事:
      • 第一件事:我不在程序中采用硬编码的方式来new对象了。(new对象我不管了,new对象的权利交出去了。)
      • 第二件事:我不在程序中采用硬编码的方式来维护对象的关系了。(对象之间关系的维护权,我也不管了,交出去了。)
  • 控制反转:是一种编程思想。或者叫做一种新型的设计模式。由于出现的比较新,没有被纳入GoF23种设计模式范围内。
  • 控制反转的核心是:将对象的创建权交出去,将对象和对象之间关系的管理权交出去,由第三方容器来负责创建与维护。

🌊 Spring框架

  • Spring框架实现了控制反转IoC这种思想
    • Spring框架可以帮你new对象。
    • Spring框架可以帮你维护对象和对象之间的关系。
  • Spring是一个实现了IoC思想的容器。

🌊 依赖注入

  • 控制反转的实现方式有多种,其中比较重要的叫做:依赖注入(Dependency Injection,简称DI)。
  • 控制反转是思想。依赖注入是这种思想的具体实现。
  • 依赖注入DI,又包括常见的两种方式:
    • 第一种:set注入(执行set方法给属性赋值)
    • 第二种:构造方法注入(执行构造方法给属性赋值)
  • 依赖注入 中 “依赖”是什么意思? “注入”是什么意思?
    • 依赖:A对象和B对象的关系。
    • 注入:是一种手段,通过这种手段,可以让A对象和B对象产生关系。
  • 依赖注入:对象A和对象B之间的关系,靠注入的手段来维护。而注入包括:set注入和构造注入。

注意术语:
OCP:开闭原则(开发原则)
DIP:依赖倒置原则(开发原则)
IoC:控制反转(一种思想,一种新型的设计模式)
DI:依赖注入(控制反转思想的具体实现方式)

🥽 Spring概述

🌊 Spring简介

  • Spring是一个开源框架,它由Rod Johnson创建。它是为了解决企业应用开发的复杂性而创建的。
  • 从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。
  • Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
  • Spring最初的出现是为了解决EJB臃肿的设计,以及难以测试等问题。
  • Spring为简化开发而生,让程序员只需关注核心业务的实现,尽可能的不再关注非业务逻辑代码(事务控制,安全日志等)。

🌊 Spring8大模块

注意:Spring5版本之后是8个模块。在Spring5中新增了WebFlux模块。

  1. Spring Core模块
    这是Spring框架最基础的部分,它提供了依赖注入(DependencyInjection)特征来实现容器对Bean的管理。核心容器的主要组件是 BeanFactory,BeanFactory是工厂模式的一个实现,是任何Spring应用的核心。它使用IoC将应用配置和依赖从实际的应用代码中分离出来。

  2. Spring Context模块
    如果说核心模块中的BeanFactory使Spring成为容器的话,那么上下文模块就是Spring成为框架的原因。
    这个模块扩展了BeanFactory,增加了对国际化(I18N)消息、事件传播、验证的支持。另外提供了许多企业服务,例如电子邮件、JNDI访问、EJB集成、远程以及时序调度(scheduling)服务。也包括了对模版框架例如Velocity和FreeMarker集成的支持

  3. Spring AOP模块
    Spring在它的AOP模块中提供了对面向切面编程的丰富支持,Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中,可以自定义拦截器、切点、日志等操作。

  4. Spring DAO模块
    提供了一个JDBC的抽象层和异常层次结构,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析,用于简化JDBC。

  5. Spring ORM模块
    Spring提供了ORM模块。Spring并不试图实现它自己的ORM解决方案,而是为几种流行的ORM框架提供了集成方案,包括Hibernate、JDO和iBATIS SQL映射,这些都遵从 Spring 的通用事务和 DAO 异常层次结构。

  6. Spring Web MVC模块
    Spring为构建Web应用提供了一个功能全面的MVC框架。虽然Spring可以很容易地与其它MVC框架集成,例如Struts,但Spring的MVC框架使用IoC对控制逻辑和业务对象提供了完全的分离。

  7. Spring WebFlux模块
    Spring Framework 中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版的后期添加的。它是完全非阻塞的,支持反应式流(Reactive Stream)背压,并在Netty,Undertow和Servlet 3.1+容器等服务器上运行。

  8. Spring Web模块
    Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文,提供了Spring和其它Web框架的集成,比如Struts、WebWork。还提供了一些面向服务支持,例如:实现文件上传的multipart请求。

🌊 Spring特点

  1. 轻量
    • 从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。
    • Spring是非侵入式的:Spring应用中的对象不依赖于Spring的特定类。
      • 非侵入式,spring框架的运行不需要依赖其他框架
  2. 控制反转
    • Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
  3. 面向切面
    • Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
  4. 容器
    • Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
  5. 框架
    • Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。
      所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。

🥽 Spring的入门程序

Spring6要求JDK最低版本是Java17

🌊 Spring5的下载

官网地址:【https://spring.io/】
官网地址(中文):【http://spring.p2hp.com/】



【https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Artifacts】




【https://repo.spring.io/artifactory/release/org/springframework/spring/5.3.9/】


🌊 Spring6的下载

官网地址:【https://spring.io/】
官网地址(中文):【http://spring.p2hp.com/】



【https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Artifacts】

将下图配置写入pom文件中

🌊 Spring的jar文件

打开libs目录,会看到很多jar包:

  • spring-core-5.3.9.jar:字节码(这个是支撑程序运行的jar包)
  • spring-core-5.3.9-javadoc.jar:代码中的注释
  • spring-core-5.3.9-sources.jar:源码

我们来看一下spring框架都有哪些jar包:

JAR文件描述
spring-aop-5.3.9.jar这个jar 文件包含在应用中使用Spring 的AOP 特性时所需的类
spring-aspects-5.3.9.jar提供对AspectJ的支持,以便可以方便的将面向切面的功能集成进IDE中
spring-beans-5.3.9.jar这个jar 文件是所有应用都要用到的,它包含访问配置文件、创建和管理bean 以及进行Inversion ofControl / Dependency Injection(IoC/DI)操作相关的所有类。如果应用只需基本的IoC/DI 支持,引入spring-core.jar 及spring-beans.jar 文件就可以了。
spring-context-5.3.9.jar这个jar 文件为Spring 核心提供了大量扩展。可以找到使用Spring ApplicationContext特性时所需的全部类,JDNI 所需的全部类,instrumentation组件以及校验Validation 方面的相关类。
spring-context-indexer-5.3.9.jar虽然类路径扫描非常快,但是Spring内部存在大量的类,添加此依赖,可以通过在编译时创建候选对象的静态列表来提高大型应用程序的启动性能。
spring-context-support-5.3.9.jar用来提供Spring上下文的一些扩展模块,例如实现邮件服务、视图解析、缓存、定时任务调度等
spring-core-5.3.9.jarSpring 框架基本的核心工具类。Spring 其它组件要都要使用到这个包里的类,是其它组件的基本核心,当然你也可以在自己的应用系统中使用这些工具类。
spring-expression-5.3.9.jarSpring表达式语言。
spring-instrument-5.3.9.jarSpring3.0对服务器的代理接口。
spring-jcl-5.3.9.jarSpring的日志模块。JCL,全称为"Jakarta Commons Logging",也可称为"Apache Commons Logging"。
spring-jdbc-5.3.9.jarSpring对JDBC的支持。
spring-jms-5.3.9.jar这个jar包提供了对JMS 1.0.2/1.1的支持类。JMS是Java消息服务。属于JavaEE规范之一。
spring-messaging-5.3.9.jar为集成messaging api和消息协议提供支持
spring-orm-5.3.9.jarSpring集成ORM框架的支持,比如集成hibernate,mybatis等。
spring-oxm-5.3.9.jar为主流O/X Mapping组件提供了统一层抽象和封装,OXM是Object Xml Mapping。对象和XML之间的相互转换。
spring-r2dbc-5.3.9.jarReactive Relational Database Connectivity (关系型数据库的响应式连接) 的缩写。这个jar文件是Spring对r2dbc的支持。
spring-test-5.3.9.jar对Junit等测试框架的简单封装。
spring-tx-5.3.9.jar为JDBC、Hibernate、JDO、JPA、Beans等提供的一致的声明式和编程式事务管理支持。
spring-web-5.3.9.jarSpring集成MVC框架的支持,比如集成Struts等。
spring-webflux-5.3.9.jarWebFlux是 Spring5 添加的新模块,用于 web 的开发,功能和 SpringMVC 类似的,Webflux 使用当前一种比较流程响应式编程出现的框架。
spring-webmvc-5.3.9.jarSpringMVC框架的类库
spring-websocket-5.3.9.jarSpring集成WebSocket框架时使用

注意:
如果你只是想用Spring的IoC功能,仅需要引入:spring-context即可。将这个jar包添加到classpath当中。

如果采用maven只需要引入context的依赖即可。

<!--Spring6的正式版发布之前,这个仓库地址是需要的-->
<repositories>
  <repository>
    <id>repository.spring.milestone</id>
    <name>Spring Milestone Repository</name>
    <url>https://repo.spring.io/milestone</url>
  </repository>
</repositories>

<dependencies>
  <!--spring context依赖:使用的是6.0.0-M2里程碑版-->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.0.0-M2</version>
  </dependency>
</dependencies>

🌊 第一个Spring程序


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cw.spring</groupId>
    <artifactId>spring-002</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 打包方式 -->
    <packaging>jar</packaging>

    <!-- 配置多个仓库 -->
    <repositories>
        <!-- spring里程碑版本的仓库 -->
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    
    <!-- 依赖 -->
    <dependencies>
        <!--spring context依赖:使用的是6.0.0-M2里程碑版-->
        <!--引入spring context依赖,标识引入spring基础依赖-->
        <!--如果需要使用spring的其他功能还需要引入相应的依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.0-M2</version>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        
    </dependencies>
    
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>

spring框架中管理的每个对象都叫bean

定义bean:User

package cw.spring.bean;

public class User {

}

编写spring的配置文件,该文件放在类的根路径下。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- spring的配置文件 -->
    <!-- 该文件放在类的根路径下方便后期的移植 -->
    <!-- 放在resources目录下,相当于放在类的根路径下 -->
    <!-- 配置bean,spring才能帮助我们管理这个对象 -->
    <!-- 
        id: bean的唯一标识
        class: 类的全路径
     -->
    <bean id="userBean" class="cw.spring.bean.User" />
</beans>

编写测试程序

package cw.spring.test;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTest {
    @Test
    public void springTest() {
        // 第一步:获取Spring容器对象。
        // ApplicationContext 翻译为:应用上下文。其实就是Spring容器。
        // ApplicationContext 是一个接口。
        // ApplicationContext 接口下有很多实现类。其中有一个实现类叫做:ClassPathXmlApplicationContext
        // ClassPathXmlApplicationContext 专门从类路径当中加载spring配置文件的一个Spring上下文对象。
        // 这行代码只要执行:就相当于启动了Spring容器,解析spring.xml文件,并且实例化所有的bean对象,放到spring容器当中。
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
    
        // 第二步:根据bean的id从Spring容器中获取这个对象。
        Object userBean = applicationContext.getBean("userBean");
        System.out.println(userBean);
    }
}

🌊 第一个Spring程序剖析

  • 在spring的配置文件中id是不能重名
  • spring是通过反射机制调用类的无参数构造方法来创建对象的,所以要想让spring给你创建对象,必须保证无参数构造方法是存在的。
  • spring把创建好的对象存储到一个Map当中
  • spring配置文件名字是我们负责提供的,spring配置文件的名字是随意的。
  • spring的配置文件可以有多个,在ClassPathXmlApplicationContext构造方法的参数上传递文件路径即可。且配置文件的文件名、文件路径都是任意的
    //ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
    //ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml", "beans.xml");
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml", "beans.xml", "xml/beans.xml");
    
    // 源码:
    public ClassPathXmlApplicationContext(String... configLocations) throws BeansException {
        this(configLocations, true, (ApplicationContext)null);
    }
    
  • 在spring配置文件中配置的bean可以任意类,只要这个类不是抽象的,并且提供了无参数构造方法。
  • getBean()方法调用时,id不存在的时候,会出现异常
  • getBean()方法返回的类型是Object,如果访问子类的特有属性和方法时,还需要向下转型,有其它办法可以解决这个问题吗?
    //Object nowTime = applicationContext.getBean("nowTime");
    //Date nowTime = (Date) applicationContext.getBean("nowTime");
    // 不想强制类型转换,可以使用以下代码(通过第二个参数来指定返回的bean的类型。)
    Date nowTime = applicationContext.getBean("nowTime", Date.class);
    
  • ClassPathXmlApplicationContext是从类路径中加载配置文件,如果没有在类路径当中,又应该如何加载配置文件呢?
    • 没有在类路径中的话,需要使用FileSystemXmlApplicationContext类进行加载配置文件。
    • 这种方式较少用。一般都是将配置文件放到类路径当中,这样可移植性更强。
      ApplicationContext applicationContext2 = new FileSystemXmlApplicationContext("d:/spring6.xml");
      
  • ApplicationContext的超级父接口BeanFactory。
    • BeanFactory是Spring容器的超级接口。ApplicationContext是BeanFactory的子接口。
      //ApplicationContext接口的超级父接口是:BeanFactory(翻译为Bean工厂,就是能够生产Bean对象的一个工厂对象。)
      //BeanFactory是IoC容器的顶级接口。
      //Spring的IoC容器底层实际上使用了:工厂模式。
      //Spring底层的IoC是怎么实现的?XML解析 + 工厂模式 + 反射机制
      //ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
      BeanFactory applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
      User user = applicationContext.getBean("userBean", User.class);
      System.out.println(user);
      
  • Spring底层的IoC容器是通过:XML解析+工厂模式+反射机制实现的。
  • spring不是在调用getBean()方法的时候创建对象,执行new ClassPathXmlApplicationContext("spring6.xml");的时候,就会实例化对象。

🌊 Spring6启用Log4j2日志框架

从Spring5之后,Spring框架支持集成的日志框架是Log4j2

第一步:引入Log4j2的依赖

<!--log4j2的依赖-->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.19.0</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-slf4j2-impl</artifactId>
  <version>2.19.0</version>
</dependency>

第二步:在类的根路径下提供log4j2.xml配置文件(文件名固定为:log4j2.xml,文件必须放到类根路径下。)

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    
    <loggers>
        <!--
            level指定日志级别,从低到高的优先级:
                ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
            达到相应的级别才会输出日志信息
            级别越低输出的日志信息越多
        -->
        <root level="DEBUG">
            <appender-ref ref="spring6log"/>
        </root>
    </loggers>
    
    <appenders>
        <!--输出日志信息到控制台-->
        <console name="spring6log" target="SYSTEM_OUT">
            <!--控制日志输出的格式-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
        </console>
    </appenders>

</configuration>

第三步:使用日志框架

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


// 你自己怎么去使用log4j2记录日志信息呢?
// 第一步:创建日志记录器对象
// 获取FirstSpringTest类的日志记录器对象,也就是说只要是FirstSpringTest类中的代码执行记录日志的话,就输出相关的日志信息。
Logger logger = LoggerFactory.getLogger(FirstSpringTest.class);

// 第二步:记录日志,根据不同的级别来输出日志
logger.info("我是一条消息");
logger.debug("我是一条调试信息");
logger.error("我是一条错误信息");

🥽 Spring对IoC的实现

🌊 IoC 控制反转

  • 控制反转是一种思想。
  • 控制反转是为了降低程序耦合度,提高程序扩展力,达到OCP原则,达到DIP原则。
  • 控制反转,反转的是什么?
    • 将对象的创建权利交出去,交给第三方容器负责。
    • 将对象和对象之间关系的维护权交出去,交给第三方容器负责。
  • 控制反转这种思想如何实现呢?
    • DI(Dependency Injection):依赖注入

🌊 依赖注入

  • 依赖注入实现了控制反转的思想。
  • Spring通过依赖注入的方式来完成Bean管理的。
  • Bean管理说的是:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。
  • 依赖注入:
    • 依赖指的是对象和对象之间的关联关系。
    • 注入指的是一种数据传递行为,通过注入行为来让对象和对象产生关系。
  • 依赖注入常见的实现方式包括两种:
    • 第一种:set注入
    • 第二种:构造注入

💦 set注入

set注入,基于set方法实现的,底层会通过反射机制调用属性对应的set方法然后给属性赋值。这种方式要求属性必须对外提供set方法。

public class UserDao {

    private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

    public void insert(){
        //System.out.println("数据库正在保存用户信息。");
        // 使用一下log4j2日志框架
        logger.info("数据库正在保存用户信息。");
    }
}
public class VipDao {

    private static final Logger logger = LoggerFactory.getLogger(VipDao.class);

    public void insert(){
        logger.info("正在保存Vip信息!!!!");
    }
}
public class UserService {

    private UserDao userDao;
    private VipDao vipDao;

    public void setAbc(VipDao vipDao){
        this.vipDao = vipDao;
    }

    // set注入的话,必须提供一个set方法。
    // Spring容器会调用这个set方法,来给userDao属性赋值。
    // 我自己写一个set方法,不使用IDEA工具生成的。不符合javabean规范。
    // 至少这个方法是以set单词开始的。前三个字母不能随便写,必须是“set"
    /*public void setMySQLUserDao(UserDao xyz){
        this.userDao = xyz;
    }*/


    // 这个set方法是IDEA工具生成的,符合javabean规范。
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void saveUser(){
        // 保存用户信息到数据库
        userDao.insert();
        vipDao.insert();
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--配置dao-->
    <bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>

    <!--配置service-->
    <bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
        <!-- 想让Spring调用对应的set方法,需要配置property标签 -->
        <!-- name属性怎么指定值:set方法的方法名,去掉set,然后把剩下的单词首字母变小写,写到这里。-->
        <!-- ref翻译为引用。英语单词:references。ref后面指定的是要注入的bean的id。-->
        <!--<property name="mySQLUserDao" ref="userDaoBean"/>-->

        <!--set方法起名的时候,不要为难自己,按照规范来。所以一般情况下name位置写属性名就行了。-->
        <property name="userDao" ref="userDaoBean"/>

        <!--<property name="vipDao" ref="vipDaoBean"/>-->
        <property name="abc" ref="vipDaoBean"/>

    </bean>

    <bean id="vipDaoBean" class="com.powernode.spring6.dao.VipDao"/>

</beans>
  • 实现原理:

    • 通过property标签获取到属性名:userDao
    • 通过属性名推断出set方法名:setUserDao
    • 通过反射机制调用setUserDao()方法给属性赋值
    • property标签的name是属性名。
    • property标签的ref是要注入的bean对象的id。(通过ref属性来完成bean的装配,这是bean最简单的一种装配方式。装配指的是:创建系统组件之间关联的动作)
  • property标签的name是:setUserDao()方法名演变得到的。演变的规律是:

    • setUsername() 演变为 username
    • setPassword() 演变为 password
    • setUserDao() 演变为 userDao
    • setUserService() 演变为 userService
  • 另外,对于property标签来说,ref属性也可以采用标签的方式,但使用ref属性是多余的:

    <bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
      <property name="userDao">
        <ref bean="userDaoBean"/>
      </property>
    </bean>
    
  • 总结:set注入的核心实现原理:通过反射机制调用set方法来给属性赋值,让两个对象之间产生关系。

💦 构造注入

核心原理:通过调用构造方法来给属性赋值。

与set注入相比,构造注入是在创建对象的同时进行注入,进行属性的赋值,而set注入是在对象创建之后。

public class CustomerService {

    private UserDao userDao;
    private VipDao vipDao;

    public CustomerService(UserDao userDao, VipDao vipDao) {
        this.userDao = userDao;
        this.vipDao = vipDao;
    }

    public void save(){
        userDao.insert();
        vipDao.insert();
    }

}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="xxxx" class="com.powernode.spring6.dao.UserDao"/>

    <bean id="yyyy" class="com.powernode.spring6.dao.VipDao"/>

    <bean id="csBean3" class="com.powernode.spring6.service.CustomerService">
        <!--不指定下标,也不指定参数名,让spring自己做类型匹配吧。-->
        <!--这种方式实际上是根据类型进行注入的。spring会自动根据类型来判断把ref注入给哪个参数。-->
        <constructor-arg ref="yyyy"/>
        <constructor-arg ref="xxxx"/>
    </bean>

    <bean id="csBean2" class="com.powernode.spring6.service.CustomerService">
        <!--根据构造方法参数的名字进行注入。-->
        <constructor-arg name="vipDao" ref="yyyy"/>
        <constructor-arg name="userDao" ref="xxxx"/>
    </bean>

    <bean id="csBean" class="com.powernode.spring6.service.CustomerService">
        <!--构造注入-->
        <!--
            index属性指定参数下标,第一个参数是0,第二个参数是1,第三个参数是2,以此类推。
            ref属性用来指定注入的bean的id
        -->
        <!--指定构造方法的第一个参数,下标是0-->
        <constructor-arg index="0" ref="xxxx"/>
        <!--指定构造方法的第二个参数,下标是1-->
        <constructor-arg index="1" ref="yyyy"/>
    </bean>

</beans>
  • 通过测试得知,通过构造方法注入的时候:
    • 可以通过下标
    • 可以通过参数名
    • 也可以不指定下标和参数名,可以类型自动推断。
  • Spring在装配方面做的还是比较健壮的。

🌊 set注入专题

💦 注入外部Bean

外部Bean的特点:bean定义到外面,在property标签中使用ref属性进行注入。通常这种方式是常用。

    <!--声明/定义Bean-->
    <bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"></bean>

    <bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
        <!--使用ref属性来引入。这就是注入外部Bean-->
        <property name="orderDao" ref="orderDaoBean"/>
    </bean>

💦 注入内部Bean

内部Bean的方式:在bean标签中嵌套bean标签。

    <bean id="orderServiceBean2" class="com.powernode.spring6.service.OrderService">
        <property name="orderDao">
            <!--在property标签中使用嵌套的bean标签,这就是内部Bean-->
            <bean class="com.powernode.spring6.dao.OrderDao"></bean>
        </property>
    </bean>

💦 注入简单类型

public class User {
    private String username; // String是简单类型
    private String password;
    private int age; // int是简单类型

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }
}
    <!--注入简单类型-->
    <bean id="userBean" class="com.powernode.spring6.bean.User">
        <!--重点:如果是给简单类型赋值,就不能使用ref了。就需要使用value了。-->
        <property name="username" value="张三"/>
        <property name="password" value="123"/>
        <property name="age" value="20"/>
    </bean>

  • 需要特别注意:如果给简单类型赋值,使用value属性或value标签。而不是ref。

  • 通过源码分析得知,BeanUtils类,简单类型包括:

    • 基本数据类型
    • 基本数据类型对应的包装类
    • String或其他的CharSequence子类
    • Number子类
    • Date子类,java.util.Date是简单类型
    • Enum子类
    • URI
    • URL
    • Temporal子类,Temporal是Java8提供的时间和时区类型
    • Locale,Locale是语言类,也是简单类型。
    • Class
    • 另外还包括以上简单值类型对应的数组类型。
    public class BeanUtils{
        
        //.......
        
        /**
    	 * Check if the given type represents a "simple" property: a simple value
    	 * type or an array of simple value types.
    	 * <p>See {@link #isSimpleValueType(Class)} for the definition of <em>simple
    	 * value type</em>.
    	 * <p>Used to determine properties to check for a "simple" dependency-check.
    	 * @param type the type to check
    	 * @return whether the given type represents a "simple" property
    	 * @see org.springframework.beans.factory.support.RootBeanDefinition#DEPENDENCY_CHECK_SIMPLE
    	 * @see org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#checkDependencies
    	 * @see #isSimpleValueType(Class)
    	 */
    	public static boolean isSimpleProperty(Class<?> type) {
    		Assert.notNull(type, "'type' must not be null");
    		return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));
    	}
    
    	/**
    	 * Check if the given type represents a "simple" value type: a primitive or
    	 * primitive wrapper, an enum, a String or other CharSequence, a Number, a
    	 * Date, a Temporal, a URI, a URL, a Locale, or a Class.
    	 * <p>{@code Void} and {@code void} are not considered simple value types.
    	 * @param type the type to check
    	 * @return whether the given type represents a "simple" value type
    	 * @see #isSimpleProperty(Class)
    	 */
    	public static boolean isSimpleValueType(Class<?> type) {
    		return (Void.class != type && void.class != type &&
    				(ClassUtils.isPrimitiveOrWrapper(type) ||
    				Enum.class.isAssignableFrom(type) ||
    				CharSequence.class.isAssignableFrom(type) ||
    				Number.class.isAssignableFrom(type) ||
    				Date.class.isAssignableFrom(type) ||
    				Temporal.class.isAssignableFrom(type) ||
    				URI.class == type ||
    				URL.class == type ||
    				Locale.class == type ||
    				Class.class == type));
    	}
        
        //........
    }
    
  • 编写一个程序,把所有的简单类型全部测试一遍:

    package com.powernode.spring6.beans;
    
    import java.net.URI;
    import java.net.URL;
    import java.time.LocalDate;
    import java.util.Date;
    import java.util.Locale;
    
    public class A {
        private byte b;
        private short s;
        private int i;
        private long l;
        private float f;
        private double d;
        private boolean flag;
        private char c;
    
        private Byte b1;
        private Short s1;
        private Integer i1;
        private Long l1;
        private Float f1;
        private Double d1;
        private Boolean flag1;
        private Character c1;
    
        private String str;
    
        private Date date;
    
        private Season season;
    
        private URI uri;
    
        private URL url;
    
        private LocalDate localDate;
    
        private Locale locale;
    
        private Class clazz;
        
        // 生成setter方法
        // 生成toString方法
    }
    
    enum Season {
        SPRING, SUMMER, AUTUMN, WINTER
    }
    
    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="a" class="com.powernode.spring6.beans.A">
            <property name="b" value="1"/>
            <property name="s" value="1"/>
            <property name="i" value="1"/>
            <property name="l" value="1"/>
            <property name="f" value="1"/>
            <property name="d" value="1"/>
            <property name="flag" value="false"/>
    
            <property name="c" value="a"/>
            <property name="b1" value="2"/>
            <property name="s1" value="2"/>
            <property name="i1" value="2"/>
            <property name="l1" value="2"/>
            <property name="f1" value="2"/>
            <property name="d1" value="2"/>
            <property name="flag1" value="true"/>
            <property name="c1" value="a"/>
    
            <property name="str" value="zhangsan"/>
            <!--注意:value后面的日期字符串格式不能随便写,必须是Date对象toString()方法执行的结果。-->
            <!--报错了,说1970-10-11这个字符串无法转换成java.util.Date类型。-->
            <!--<property name="birth" value="1970-10-11"/>-->
            <!--如果你硬要把Date当做简单类型的话,使用value赋值的话,这个日期字符串格式有要求-->
            <!--在实际开发中,我们一般不会把Date当做简单类型,虽然它是简单类型。一般会采用ref给Date类型的属性赋值。-->
            <!--如果想使用其他格式的日期字符串,就需要进行特殊处理了。具体怎么处理,可以看后面的课程!!!!-->
            <property name="date" value="Fri Sep 30 15:26:38 CST 2022"/>
            <property name="season" value="WINTER"/>
            <property name="uri" value="/save.do"/>
            <!--spring6之后,会自动检查url是否有效,如果无效会报错。-->
            <property name="url" value="http://www.baidu.com"/>
            <property name="localDate" value="EPOCH"/>
            <!--java.util.Locale 主要在软件的本地化时使用。它本身没有什么功能,更多的是作为一个参数辅助其他方法完成输出的本地化。-->
            <property name="locale" value="CHINESE"/>
            <property name="clazz" value="java.lang.String"/>
        </bean>
    </beans>
    
  • 需要注意的是:

    • 如果把Date当做简单类型的话,日期字符串格式不能随便写。格式必须符合Date的toString()方法格式。显然这就比较鸡肋了。如果我们提供一个这样的日期字符串:2010-10-11,在这里是无法赋值给Date类型的属性的。
    • spring6之后,当注入的是URL,那么这个url字符串是会进行有效性检测的。如果是一个存在的url,那就没问题。如果不存在则报错。
  • 经典案例:给数据源的属性注入值:

    • 假设我们现在要自己手写一个数据源,我们都知道所有的数据源都要实现javax.sql.DataSource接口,并且数据源中应该有连接数据库的信息,例如:driver、url、username、password等。
    /**
     * 所有的数据源都要实现java规范:javax.sql.DataSource
     * 什么是数据源:能够给你提供Connection对象的,都是数据源。
     **/
    public class MyDataSource implements DataSource { // 可以把数据源交给Spring容器来管理。
    
        private String driver;
        private String url;
        private String username;
        private String password;
    
        public void setDriver(String driver) {
            this.driver = driver;
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public String toString() {
            return "MyDataSource{" +
                    "driver='" + driver + '\'' +
                    ", url='" + url + '\'' +
                    ", username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    
        @Override
        public Connection getConnection() throws SQLException {
            // 获取数据库连接对象的时候需要4个信息:driver url username password
            return null;
        }
    
        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return null;
        }
    
        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }
    
        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {
    
        }
    
        @Override
        public void setLoginTimeout(int seconds) throws SQLException {
    
        }
    
        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }
    
        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    
        @Override
        public <T> T unwrap(Class<T> iface) throws SQLException {
            return null;
        }
    
        @Override
        public boolean isWrapperFor(Class<?> iface) throws SQLException {
            return false;
        }
    }
    
        <!--让spring来管理我们的数据源-->
        <bean id="myDataSource" class="com.powernode.spring6.jdbc.MyDataSource">
            <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </bean>
    
    @Test
    public void testDataSource(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-datasource.xml");
        MyDataSource dataSource = applicationContext.getBean("dataSource", MyDataSource.class);
        System.out.println(dataSource);
    }
    

💦 级联属性赋值

// 班级类
public class Clazz {
    // 班级名称
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Clazz{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class Student {
    private String name;

    // 学生属于哪个班级
    private Clazz clazz;

    public void setClazz(Clazz clazz) {
        this.clazz = clazz;
    }

    // 使用级联属性赋值,这个需要这个get方法。
    // 因为级联给学生所在的班级属性赋值会调用学生的getClazz()方法
    public Clazz getClazz() {
        return clazz;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", clazz=" + clazz +
                '}';
    }
}

原先的注入方法

	<bean id="studentBean" class="com.powernode.spring6.bean.Student">
        <!--简单类型,使用value-->
        <property name="name" value="张三"/>
        <!--这不是简单类型,使用ref-->
        <property name="clazz" ref="clazzBean"/>
    </bean>

    <bean id="clazzBean" class="com.powernode.spring6.bean.Clazz">
        <property name="name" value="高三一班"/>
    </bean>

级联注入:

    <!--使用级联属性赋值需要注意两点:
            1. 配置的顺序不能颠倒,必须如下顺序。
            2. clazz属性必须提供getter方法。
    -->
    <bean id="studentBean" class="com.powernode.spring6.bean.Student">
        <!--简单类型,使用value-->
        <property name="name" value="张三"/>
        <!--这不是简单类型,使用ref-->
        <property name="clazz" ref="clazzBean"/>
        <!--级联属性赋值-->
        <!-- name="clazz.name" 会调用学生的getClazz()方法,所以学生类必须提供getClazz()方法,然后调用set方法给班级的name赋值 -->
        <property name="clazz.name" value="高三二班"/>
    </bean>

	<!-- 这里不进行注入 -->
    <bean id="clazzBean" class="com.powernode.spring6.bean.Clazz"></bean>

💦 注入数组

当数组中的元素是简单类型:

public class QianDaYe {
    private String[] aiHaos;

    public void setAiHaos(String[] aiHaos) {
        this.aiHaos = aiHaos;
    }

    @Override
    public String toString() {
        return "QianDaYe{" +
                "aiHaos=" + Arrays.toString(aiHaos) +
                '}';
    }
}

    <bean id="yuQian" class="com.powernode.spring6.bean.QianDaYe">

        <!-- 这个数组属性当中的元素类型是String简单类型 -->
        <property name="aiHaos">
            <array>
                <value>抽烟</value>
                <value>喝酒</value>
                <value>烫头</value>
            </array>
        </property>

    </bean>

当数组中的元素是非简单类型:

public class Woman {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Woman{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class QianDaYe {
    private String[] aiHaos;

    // 多个女性朋友
    private Woman[] womens;

    public void setWomens(Woman[] womens) {
        this.womens = womens;
    }

    public void setAiHaos(String[] aiHaos) {
        this.aiHaos = aiHaos;
    }

    @Override
    public String toString() {
        return "QianDaYe{" +
                "aiHaos=" + Arrays.toString(aiHaos) +
                ", womens=" + Arrays.toString(womens) +
                '}';
    }
}

    <bean id="w1" class="com.powernode.spring6.bean.Woman">
        <property name="name" value="小花"/>
    </bean>

    <bean id="w2" class="com.powernode.spring6.bean.Woman">
        <property name="name" value="小亮"/>
    </bean>

    <bean id="w3" class="com.powernode.spring6.bean.Woman">
        <property name="name" value="小明"/>
    </bean>

    <bean id="yuQian" class="com.powernode.spring6.bean.QianDaYe">

        <!-- 这个数组属性当中的元素类型是String简单类型 -->
        <property name="aiHaos">
            <array>
                <value>抽烟</value>
                <value>喝酒</value>
                <value>烫头</value>
            </array>
        </property>

        <!-- 这个数组当中的类型就不是简单类型了-->
        <property name="womens">
            <array>
                <ref bean="w1"/>
                <ref bean="w2"/>
                <ref bean="w3"/>
            </array>
        </property>

    </bean>
  • 要点:
    • 如果数组中是简单类型,使用value标签。
    • 如果数组中是非简单类型,使用ref标签。

💦 注入List集合与Set集合

  • List集合:有序可重复
    • 注意:注入List集合的时候使用list标签,如果List集合中是简单类型使用value标签,反之使用ref标签。
  • Set集合:无序不可重复
    • 要点:
      • 使用<set>标签
      • set集合中元素是简单类型的使用value标签,反之使用ref标签。
public class Person {

    // 注入List集合
    private List<String> names;

    // 注入Set集合
    private Set<String> addrs;

    public void setNames(List<String> names) {
        this.names = names;
    }

    public void setAddrs(Set<String> addrs) {
        this.addrs = addrs;
    }

    @Override
    public String toString() {
        return "Person{" +
                "names=" + names +
                ", addrs=" + addrs +
                '}';
    }
}
    <bean id="personBean" class="com.powernode.spring6.bean.Person">

        <property name="names">
            <!--list集合有序可重复-->
            <list>
                <value>张三</value>
                <value>李四</value>
                <value>王五</value>
                <value>张三</value>
                <value>张三</value>
                <value>张三</value>
                <value>张三</value>
            </list>
        </property>

        <property name="addrs">
            <!--set集合无序不可重复-->
            <set>
                <value>北京大兴区</value>
                <value>北京大兴区</value>
                <value>北京海淀区</value>
                <value>北京海淀区</value>
                <value>北京大兴区</value>
            </set>
        </property>
    </bean>

💦 注入Map集合与注入Properties

  • Map集合:
    • 使用<map>标签
    • 如果key是简单类型,使用 key 属性,反之使用 key-ref 属性。
    • 如果value是简单类型,使用 value 属性,反之使用 value-ref 属性。
  • java.util.Properties继承java.util.Hashtable,所以Properties也是一个Map集合。
    • Properties使用<props>标签嵌套<prop>标签完成。
public class Person {

    // 注入Map集合
    // 多个电话
    private Map<Integer, String> phones;

    // 注入属性类对象
    // Properties本质上也是一个Map集合。
    // Properties的父类Hashtable,Hashtable实现了Map接口。
    // 虽然这个也是一个Map集合,但是和Map的注入方式有点像,但是不同。
    // Properties的key和value只能是String类型。
    private Properties properties;

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    public void setPhones(Map<Integer, String> phones) {
        this.phones = phones;
    }

    @Override
    public String toString() {
        return "Person{" +
                "phones=" + phones +
                ", properties=" + properties +
                '}';
    }
}
    <bean id="personBean" class="com.powernode.spring6.bean.Person">
        
        <property name="properties">
            <!--注入Properties属性类对象-->
            <props>
                <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
                <prop key="url">jdbc:mysql://localhost:3306/spring6</prop>
                <prop key="username">root</prop>
                <prop key="password">123456</prop>
            </props>
        </property>

        <property name="phones">
            <!--注入Map集合-->
            <map>
                <!--如果key和value不是简单类型就用这个配置。-->
                <!--<entry key-ref="" value-ref=""/>-->
                <!--如果是简单类型就是key和value-->
                <entry key="1" value="110"/>
                <entry key="2" value="120"/>
                <entry key="3" value="119"/>
            </map>
        </property>
    </bean>

💦 注入null和空字符串

  • 注入空字符串使用:<value/> 或者 value=“”
  • 注入null使用:<null/> 或者 不为该属性赋值
public class Cat {
    private String name;
    private int age;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

    <bean id="catBean" class="com.powernode.spring6.bean.Cat">
        <!--不给属性注入,属性的默认值就是null-->
        <!--<property name="name" value="tom"></property>-->
        <!-- 这不是注入null,这只是注入了一个"null"字符串-->
        <!--<property name="name" value="null"/>-->
        <!--这种方式是手动注入null-->
        <!--<property name="name">
            <null/>
        </property>-->

        <!--注入空字符串第一种方式-->
        <!--<property name="name" value=""/>-->
        <!--注入空字符串第二种方式-->
        <property name="name">
            <value/>
        </property>

        <property name="age" value="3"></property>
    </bean>

💦 注入的值中含有特殊符号

  • XML中有5个特殊字符,分别是:<、>、'、"、&
  • 以上5个特殊符号在XML中会被特殊对待,会被当做XML语法的一部分进行解析,如果这些特殊符号直接出现在注入的字符串当中,会报错。
  • 解决方案包括两种:
    • 第一种:特殊符号使用转义字符代替。
    • 第二种:将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。使用CDATA时,不能使用value属性,只能使用value标签。
  • 5个特殊字符对应的转义字符分别是:
public class MathBean {
    private String result;

    public void setResult(String result) {
        this.result = result;
    }

    @Override
    public String toString() {
        return "MathBean{" +
                "result='" + result + '\'' +
                '}';
    }
}
    <bean id="mathBean" class="com.powernode.spring6.bean.MathBean">
        <!--第一种方案:使用实体符号代替特殊符号-->
        <!--<property name="result" value="2 &lt; 3" />-->

        <!--第二种方案:使用<![CDATA[]]>-->
        <property name="result">
            <!--只能使用value标签-->
            <value><![CDATA[2 < 3]]></value>
        </property>

    </bean>

🌊 p命名空间注入

  • 目的:简化配置。
  • p命名空间实际上是对set注入的简化。
  • 使用p命名空间注入的前提条件包括两个:
    • 第一:在XML头部信息中添加p命名空间的配置信息:xmlns:p=“http://www.springframework.org/schema/p”
    • 第二:p命名空间注入是基于setter方法的,所以需要对应的属性提供setter方法。
public class Dog {
    // 简单类型
    private String name;
    private int age;
    // 非简单类型
    private Date birth;

    // p命名空间注入底层还是set注入,只不过p命名空间注入可以让spring配置变的更加简单。
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birth=" + birth +
                '}';
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--
        第一步:在spring的配置文件头部添加p命名空间。xmlns:p="http://www.springframework.org/schema/p"
        第二步:使用 p:属性名 = "属性值"
    -->
    <bean id="dogBean" class="com.powernode.spring6.bean.Dog" p:name="小花" p:age="3" p:birth-ref="birthBean"/>

    <!--这里获取的是当前系统时间。-->
    <bean id="birthBean" class="java.util.Date"/>

</beans>

🌊 c命名空间注入

  • c命名空间是简化构造方法注入的。
  • 使用c命名空间的两个前提条件:
    • 第一:需要在xml配置文件头部添加信息:xmlns:c=“http://www.springframework.org/schema/c”
    • 第二:需要提供构造方法。
  • c命名空间是依靠构造方法的。
  • 注意:不管是p命名空间还是c命名空间,注入的时候都可以注入简单类型以及非简单类型。
public class People {
    private String name;
    private int age;
    private boolean sex;

    // c命名空间是简化构造注入的。
    // c命名空间注入办法是基于构造方法的。
    public People(String name, int age, boolean sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                '}';
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--
        第一步:在spring的配置文件头部添加: xmlns:c="http://www.springframework.org/schema/c"
        第二步:使用
            c:_0 下标方式
            c:name 参数名方式
    -->
    <!--<bean id="peopleBean" class="com.powernode.spring6.bean.People" c:_0="zhangsan" c:_1="30" c:_2="true"></bean>-->

    <bean id="peopleBean" class="com.powernode.spring6.bean.People" c:name="jack" c:age="30" c:sex="true"></bean>

</beans>

🌊 util命名空间

  • 使用util命名空间可以让配置复用。

  • 使用util命名空间的前提是:在spring配置文件头部添加配置信息。如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:util="http://www.springframework.org/schema/util"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
    
package com.powernode.spring6.beans;

import java.util.Properties;

public class MyDataSource1 {
	// Properties属性类对象,这是一个Map集合,key和value都是String类型。
    private Properties properties;

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public String toString() {
        return "MyDataSource1{" +
                "properties=" + properties +
                '}';
    }
}

package com.powernode.spring6.beans;

import java.util.Properties;

public class MyDataSource2 {
    private Properties properties;

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public String toString() {
        return "MyDataSource2{" +
                "properties=" + properties +
                '}';
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <!--引入util命名空间
        在spring的配置文件头部添加:
        xmlns:util="http://www.springframework.org/schema/util"

        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
    -->
    <util:properties id="prop">
        <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
        <prop key="url">jdbc:mysql://localhost:3306/spring6</prop>
        <prop key="username">root</prop>
        <prop key="password">123</prop>
    </util:properties>

    <!--数据源1-->
    <bean id="ds1" class="com.powernode.spring6.jdbc.MyDataSource1">
        <property name="properties" ref="prop"/>
    </bean>

    <!--数据源2-->
    <bean id="ds2" class="com.powernode.spring6.jdbc.MyDataSource2">
        <property name="properties" ref="prop"/>
    </bean>

</beans>

🌊 基于XML的自动装配

Spring还可以完成自动化的注入,自动化注入又被称为自动装配。它可以根据名字进行自动装配,也可以根据类型进行自动装配。

💦 根据名称自动装配

如果根据名称装配(byName),底层会调用set方法进行注入。
例如:setAge() 对应的名字是age,setPassword()对应的名字是password,setEmail()对应的名字是email。

public class OrderDao {

    private static final Logger logger = LoggerFactory.getLogger(OrderDao.class);

    public void insert(){
        logger.info("订单正在生成....");
    }
}
public class OrderService {

    private OrderDao orderDao;

    // 通过set方法给属性赋值。
    public void setOrderDao(OrderDao orderDao) {
        this.orderDao = orderDao;
    }

    /**
     * 生成订单的业务方法。。。
     */
    public void generate(){
        orderDao.insert();
    }
}
    <!--根据名字进行自动装配-->
    <!--注意:自动装配也是基于set方式实现的。-->
    <bean id="orderService" class="com.powernode.spring6.service.OrderService" autowire="byName"></bean>

    <!--id一般也叫作bean的名称。-->
    <!--根据名字进行自动装配的时候,被注入的对象的bean的id不能随便写,怎么写?set方法的方法名去掉set,剩下单词首字母小写。-->
    <bean id="orderDao" class="com.powernode.spring6.dao.OrderDao"/>

    <!--<bean id="orderService" class="com.powernode.spring6.service.OrderService">
        <property name="orderDao" ref="fdsafdsafdsa"/>
    </bean>

    <bean id="fdsafdsafdsa" class="com.powernode.spring6.dao.OrderDao"/>-->
  • 这个配置起到关键作用:
  • OrderService Bean中需要添加autowire=“byName”,表示通过名称进行装配。
  • OrderService 类中有一个OrderDao属性,而OrderDao属性的名字是orderDao,对应的set方法是setOrderDao(),正好和OrderDao Bean的id是一样的。这就是根据名称自动装配。

💦 根据类型自动装配

  • 无论是byName还是byType,在装配的时候都是基于set方法的。所以set方法是必须要提供的。提供构造方法是不行的.
  • 如果byType,根据类型装配时,如果配置文件中有两个类型一样的bean会报错
  • 当byType进行自动装配的时候,配置文件中某种类型的Bean必须是唯一的,不能出现多个。
public class CustomerService {

    private UserDao userDao;
    private VipDao vipDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void setVipDao(VipDao vipDao) {
        this.vipDao = vipDao;
    }

    public void save(){
        userDao.insert();
        vipDao.insert();
    }

}
    <!--根据类型进行自动装配-->
    <!--自动装配是基于set方法的-->
    <!--根据类型进行自动装配的时候,在有效的配置文件当中,某种类型的实例只能有一个。-->
    <bean class="com.powernode.spring6.dao.VipDao"></bean>
    <bean id="x" class="com.powernode.spring6.dao.UserDao"></bean>
    <!--如果byType,根据类型装配时,如果配置文件中有两个类型一样的bean会报错-->
    <!--<bean id="y" class="com.powernode.spring6.dao.UserDao"></bean>-->
    <bean id="cs" class="com.powernode.spring6.service.CustomerService" autowire="byType"></bean>

🌊 Spring引入外部属性配置文件

我们都知道编写数据源的时候是需要连接数据库的信息的,例如:driver url username password等信息。这些信息可以单独写到一个属性配置文件中吗,这样用户修改起来会更加的方便。当然可以。

jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring6
jdbc.username=root
jdbc.password=123
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!--
        引入外部的properties文件
            第一步:引入context命名空间。
            	xmlns:context="http://www.springframework.org/schema/context"
            	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
            第二步:使用标签context:property-placeholder的location属性来指定属性配置文件的路径。
                    location默认从类的根路径下开始加载资源。
    -->
    <context:property-placeholder location="jdbc.properties"/>

    <!--配置数据源-->
    <bean id="ds" class="com.powernode.spring6.jdbc.MyDataSource">
        <!--怎么取值呢?第三步:${key}-->
        <property name="driver" value="${jdbc.driverClass}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <!--加前缀是由于spring加载变量是优先从系统变量中进行加载,username系统变量已经存在-->
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

</beans>

🥽 Bean的作用域

  1. Spring默认情况下是如何管理这个Bean的:
    • 默认情况下Bean是单例的。(单例:singleton)
    • 在Spring上下文初始化的时候实例化。
    • 每一次调用getBean()方法的时候,都返回那个单例的对象。
  2. 当将bean的scope属性设置为prototype:
    • bean是多例的。
    • spring上下文初始化的时候,并不会初始化这些prototype的bean。
    • 每一次调用getBean()方法的时候,实例化该bean对象。
    • prototype翻译为:原型。
    <!--
        目前来说:scope属性有两个值
            第一个值:singleton 单例(默认情况下就是单例的。)
            第二个值:prototype 原型/多例
    -->
    <bean id="sb" class="com.powernode.spring6.bean.SpringBean" scope="threadScope"></bean>
    <!--
        目前来说:scope属性有两个值
            第一个值:singleton 单例(默认情况下就是单例的。)
            第二个值:prototype 原型/多例

        其实scope属性有多个值:
            例如:request session
            但是request session要求项目必须是一个web应用。当你引入springmvc框架的时候,这两个值就可以使用了。
            request:一次请求当中一个bean
            session:一次会话中只有一个bean
    -->
    <bean id="sb" class="com.powernode.spring6.bean.SpringBean" scope="session"></bean>
<!--引入web的框架,例如springmvc-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>6.0.0-M2</version>
</dependency>
  • scope属性的值一共包括8个选项:
    • singleton:默认的,单例。
    • prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
    • request:一个请求对应一个Bean。仅限于在WEB应用中使用。
    • session:一个会话对应一个Bean。仅限于在WEB应用中使用。
    • global session:portlet应用中专用的。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
    • application:一个应用对应一个Bean。仅限于在WEB应用中使用。
    • websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。
    • 自定义scope:很少使用。

接下来咱们自定义一个Scope,线程级别的Scope,在同一个线程中,获取的Bean都是同一个。跨线程则是不同的对象:

  • 第一步:自定义Scope。(实现Scope接口)
    • spring内置了线程范围的类:org.springframework.context.support.SimpleThreadScope,可以直接用,我们不需要额外自己实现一个Scope。
  • 第二步:将自定义的Scope注册到Spring容器中。
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!--配置我们自定义的作用域-->
        <!-- CustomScopeConfigurer用户自定义的范围配置 -->
        <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
            <property name="scopes">
                <map>
                    <!-- key为使用自定义范围时的名 -->
                    <entry key="threadScope">
                        <!--这个Scope接口的实现类使用的是Spring框架内置的。也可以自定义。-->
                        <bean class="org.springframework.context.support.SimpleThreadScope"/>
                    </entry>
                </map>
            </property>
        </bean>
        
        <!-- 第三步:使用Scope。 -->
        <bean id="sb" class="com.powernode.spring6.bean.SpringBean" scope="threadScope"></bean>
    
    </beans>
    
  • 第三步:使用Scope。

🥽 GoF之工厂模式

  • 设计模式:一种可以被重复利用的解决方案。
  • GoF(Gang of Four),中文名——四人组。
  • 《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为"四人组(Gang of Four)"。
  • 该书中描述了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。
  • 不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。
  • GoF23种设计模式可分为三大类:
  • 创建型(5个):解决对象创建问题。
    • 单例模式
      • 工厂方法模式
      • 抽象工厂模式
      • 建造者模式
      • 原型模式
    • 结构型(7个):一些类或对象组合在一起的经典结构。
      • 代理模式
      • 装饰模式
      • 适配器模式
      • 组合模式
      • 享元模式
      • 外观模式
      • 桥接模式
    • 行为型(11个):解决类或对象之间的交互问题。
      • 策略模式
      • 模板方法模式
      • 责任链模式
      • 观察者模式
      • 迭代子模式
      • 命令模式
      • 备忘录模式
      • 状态模式
      • 访问者模式
      • 中介者模式
      • 解释器模式
  • 工厂模式是解决对象创建问题的,所以工厂模式属于创建型设计模式。这里为什么学习工厂模式呢?这是因为Spring框架底层使用了大量的工厂模式。

🌊 工厂模式的三种形态

  • 工厂模式通常有三种形态:
    • 第一种:简单工厂模式(Simple Factory):不属于23种设计模式之一。简单工厂模式又叫做:静态 工厂方法模式。简单工厂模式是工厂方法模式的一种特殊实现。
    • 第二种:工厂方法模式(Factory Method):是23种设计模式之一。
    • 第三种:抽象工厂模式(Abstract Factory):是23种设计模式之一。

🌊简单工厂模式

  • 简单工厂模式的角色包括三个:
    • 抽象产品 角色
    • 具体产品 角色
    • 工厂类 角色

抽象产品 角色

public abstract class Weapon {

    /**
     * 所有的武器都可以攻击。
     */
    public abstract void attack();

}

具体产品 角色

public class Tank extends Weapon{
    @Override
    public void attack() {
        System.out.println("坦克开炮!!!");
    }
}

public class Fighter extends Weapon{
    @Override
    public void attack() {
        System.out.println("战斗机抛下小男孩!!!!");
    }
}

public class Dagger extends Weapon{
    @Override
    public void attack() {
        System.out.println("砍丫的!!!");
    }
}

工厂类 角色(生产者)

public class WeaponFactory {

    /**
     * 静态方法。要获取什么产品?就看你传什么参数,传TANK获取坦克,传DAGGER获取匕首,传FIGHTER获取战斗机
     * 简单工厂模式中有一个静态方法,所以被称为:静态工厂方法模式。
     * @param weaponType
     * @return
     */
    public static Weapon get(String weaponType){
        if ("TANK".equals(weaponType)) {
            return new Tank();
        } else if ("DAGGER".equals(weaponType)) {
            return new Dagger();
        } else if ("FIGHTER".equals(weaponType)) {
            return new Fighter();
        } else {
            throw new RuntimeException("不支持该武器的生产");
        }
    }

}

客户端程序(消费者)

public class Test {
    public static void main(String[] args) {

        // 需要坦克
        // 对于我客户端来说,坦克的生产细节,我不需要关心,我只需要向工厂索要即可。
        // 简单工厂模式达到了什么呢?职责分离。客户端不需要关心产品的生产细节。
        // 客户端只负责消费。工厂类负责生产。一个负责生产,一个负责消费。生产者和消费者分离了。这就是简单工厂模式的作用。
        Weapon tank = WeaponFactory.get("TANK");
        tank.attack();
        // 需要匕首
        Weapon dagger = WeaponFactory.get("DAGGER");
        dagger.attack();
        // 需要战斗机
        Weapon fighter = WeaponFactory.get("FIGHTER");
        fighter.attack();
    }
}
  • 简单工厂模式解决什么问题呢?
    • 优点:客户端程序不需要关心对象的创建细节,需要哪个对象时,只需要向工厂索要即可,初步实现了责任的分离。客户端只负责“消费”,工厂负责“生产”。生产和消费分离。
  • 简单工厂模式的缺点?
    • 缺点一:假设现在需要扩展一个新的产品,WeaponFactory工厂类的代码是需要修改的,显然违背了OCP原则。
    • 缺点二:工厂类的责任比较重大,不能出现任何问题,因为这个工厂类负责所有产品的生产,称为全能类,或者有人把它叫做上帝类。这个工厂类一旦出问题,整个系统必然全部瘫痪。(不要把所有鸡蛋放到一个篮子里面哦。)
  • Spring中的BeanFactory就使用了简单工厂模式。

🌊 工厂方法模式

  • 工厂方法模式既保留了简单工厂模式的优点,同时又解决了简单工厂模式的缺点。
  • 工厂方法模式的角色包括:
    • 抽象工厂角色
    • 具体工厂角色
    • 抽象产品角色
    • 具体产品角色
  • 工厂方法模式可以解决简单工厂模式当中的OCP问题。
    • 怎么解决的?一个工厂对应生产一种产品。
    • 这样工厂就不是全能类了,不是上帝类了。
    • 另外,也可以符合OCP原则。

抽象产品角色 Weapon

abstract public class Weapon {

    public abstract void attack();
}

具体产品角色 Dagger Gun

public class Dagger extends Weapon{

    @Override
    public void attack() {
        System.out.println("砍丫的!!!");
    }
}

public class Gun extends Weapon{
    @Override
    public void attack() {
        System.out.println("开枪射击!!!");
    }
}

抽象工厂角色 WeaponFactory

abstract public class WeaponFactory {

    /**
     * 这个方法不是静态的。是实例方法。
     * @return
     */
    public abstract Weapon get();

}

具体工厂角色 DaggerFactory GunFactory

public class DaggerFactory extends WeaponFactory{
    @Override
    public Weapon get() {
        return new Dagger();
    }
}

public class GunFactory extends WeaponFactory{
    @Override
    public Weapon get() {
        return new Gun();
    }
}
  • 工厂方法模式的优点:
    • 当你扩展一个产品的时候,符合OCP原则,因为只需要添加两个类,一个类是具体产品类,一个类是具体工厂类。都是添加类,没有修改之前的代码,所以符合OCP。
    • 一个调用者想创建一个对象,只要知道其名称就可以了。
    • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
    • 屏蔽产品的具体实现,调用者只关心产品的接口。
  • 工厂方法模式的缺点:
    • 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
    • 如果要实现不用每次实例化,还需要创建一个用于创建工厂对象的工厂类(有绕回去了简单工厂模式),或者采用反射实现

🌊 抽象工厂模式(了解)

  • 抽象工厂模式相对于工厂方法模式来说,就是工厂方法模式是针对一个产品系列的,而抽象工厂模式是针对多个产品系列的,即工厂方法模式是一个产品系列一个工厂类,而抽象工厂模式是多个产品系列一个工厂类。
  • 抽象工厂模式特点:抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。它有多个抽象产品类,每个抽象产品类可以派生出多个具体产品类,一个抽象工厂类,可以派生出多个具体工厂类,每个具体工厂类可以创建多个具体产品类的实例。每一个模式都是针对一定问题的解决方案,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式针对的是多个产品等级结果。
  • 抽象工厂中包含4个角色:
    • 抽象工厂角色
    • 具体工厂角色
    • 抽象产品角色
    • 具体产品角色
  • 抽象工厂模式的类图如下:

武器产品族

public abstract class Weapon {
    public abstract void attack();
}

public class Gun extends Weapon{
    @Override
    public void attack() {
        System.out.println("开枪射击!");
    }
}

public class Dagger extends Weapon{
    @Override
    public void attack() {
        System.out.println("砍丫的!");
    }
}

水果产品族

public abstract class Fruit {
    /**
     * 所有果实都有一个成熟周期。
     */
    public abstract void ripeCycle();
}

public class Orange extends Fruit{
    @Override
    public void ripeCycle() {
        System.out.println("橘子的成熟周期是10个月");
    }
}

public class Apple extends Fruit{
    @Override
    public void ripeCycle() {
        System.out.println("苹果的成熟周期是8个月");
    }
}

抽象工厂类

public abstract class AbstractFactory {
    public abstract Weapon getWeapon(String type);
    public abstract Fruit getFruit(String type);
}

具体工厂类

public class WeaponFactory extends AbstractFactory{

    public Weapon getWeapon(String type){
        if (type == null || type.trim().length() == 0) {
            return null;
        }
        if ("Gun".equals(type)) {
            return new Gun();
        } else if ("Dagger".equals(type)) {
            return new Dagger();
        } else {
            throw new RuntimeException("无法生产该武器");
        }
    }

    @Override
    public Fruit getFruit(String type) {
        return null;
    }
}

public class FruitFactory extends AbstractFactory{
    @Override
    public Weapon getWeapon(String type) {
        return null;
    }

    public Fruit getFruit(String type){
        if (type == null || type.trim().length() == 0) {
            return null;
        }
        if ("Orange".equals(type)) {
            return new Orange();
        } else if ("Apple".equals(type)) {
            return new Apple();
        } else {
            throw new RuntimeException("我家果园不产这种水果");
        }
    }
}

客户端程序

public class Client {
    public static void main(String[] args) {
        // 客户端调用方法时只面向AbstractFactory调用方法。
        AbstractFactory factory = new WeaponFactory(); // 注意:这里的new WeaponFactory()可以采用 简单工厂模式 进行隐藏。
        Weapon gun = factory.getWeapon("Gun");
        Weapon dagger = factory.getWeapon("Dagger");

        gun.attack();
        dagger.attack();

        AbstractFactory factory1 = new FruitFactory(); // 注意:这里的new FruitFactory()可以采用 简单工厂模式 进行隐藏。
        Fruit orange = factory1.getFruit("Orange");
        Fruit apple = factory1.getFruit("Apple");

        orange.ripeCycle();
        apple.ripeCycle();
    }
}

  • 抽象工厂模式的优缺点:
    • 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
    • 缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在AbstractFactory里加代码,又要在具体的里面加代码。

🥽 Bean的实例化方式

  • Spring为Bean提供了多种实例化方式,通常包括4种方式。(也就是说在Spring中为Bean对象的创建准备了多种方案,目的是:更加灵活)
    • 第一种:通过构造方法实例化
    • 第二种:通过简单工厂模式实例化
    • 第三种:通过factory-bean实例化
    • 第四种:通过FactoryBean接口实例化

🌊 通过构造方法实例化

public class SpringBean {

    public SpringBean() {
        System.out.println("Spring Bean的无参数构造方法执行。");
    }
}
<!--Spring提供的实例化方式,第一种:在spring配置文件中直接配置类全路径,Spring会自动调用该类的无参数构造方法来实例化Bean-->
<bean id="sb" class="com.powernode.spring6.bean.SpringBean"/>

🌊 通过简单工厂模式实例化

定义一个Bean

public class Star {

    public Star() {
        System.out.println("Star的无参数构造方法执行。");
    }

}

编写简单工厂模式当中的工厂类

public class StarFactory {

    // 工厂类中有一个静态方法。
    // 简单工厂模式又叫做:静态工厂方法模式。
    public static Star get(){
        // 这个Star对象最终实际上创建的时候还是我们负责new的对象。
        return new Star();
    }

}

在Spring配置文件中指定创建该Bean的方法(使用factory-method属性指定)

<!--Spring提供的实例化方式,第二种:通过简单工厂模式。你需要在Spring配置文件中告诉Spring框架,调用哪个类的哪个方法获取Bean-->
<!--factory-method 属性指定的是工厂类当中的静态方法。也就是告诉Spring框架,调用这个方法可以获取Bean。-->
<!-- 创建出来的为Star对象,不为工厂对象,工厂类中的方法为静态方法,通过类名直接调用创建Star对象 -->
<bean id="star" class="com.powernode.spring6.bean.StarFactory" factory-method="get"/>

🌊 通过factory-bean实例化

这种方式本质上是:通过工厂方法模式进行实例化。

定义一个Bean

/**
 * 工厂方法模式当中的:具体产品角色。
 **/
public class Gun {
    public Gun() {
        System.out.println("Gun的无参数构造方法执行。");
    }
}

定义具体工厂类,工厂类中定义实例方法

/**
 * 工厂方法模式当中的:具体工厂角色。
 **/
public class GunFactory {

    // 工厂方法模式中的具体工厂角色中的方法是:实例方法。
    public Gun get(){
        // 实际上new这个对象还是我们程序员自己new的。
        return new Gun();
    }
}

在Spring配置文件中指定factory-bean以及factory-method

<!--Spring提供的实例化方式,第三种:通过工厂方法模式。通过 factory-bean属性 + factory-method属性来共同完成。-->
<!--告诉Spring框架,调用哪个对象的哪个方法来获取Bean。-->
<!-- 由于工厂类用于创建对象的方法为示例方法,需要先创建工厂对象,所以工厂对象也需要被spring管理 -->
<bean id="gunFactory" class="com.powernode.spring6.bean.GunFactory"/><!--GunFactory实际上就是一个FactoryBean-->
<!--以下的配置很关键,factory-bean属性告诉Spring调用哪个对象。factory-method告诉Spring调用该对象的哪个方法。-->
<!-- 该配置用于指定用哪个工厂对象的哪个方法创建对象 -->
<bean id="gun" factory-bean="gunFactory" factory-method="get"/>

🌊 通过FactoryBean接口实例化

  • 以上的第三种方式中,factory-bean是我们自定义的,factory-method也是我们自己定义的。
  • 在Spring中,当你编写的类直接实现FactoryBean接口之后,factory-bean不需要指定了,factory-method也不需要指定了。
  • factory-bean会自动指向实现FactoryBean接口的类,factory-method会自动指向getObject()方法。
  • FactoryBean在Spring中是一个接口。被称为“工厂Bean”。“工厂Bean”是一种特殊的Bean。所有的“工厂Bean”都是用来协助Spring框架来创建其他Bean对象的。

定义一个Bean

public class Person { // 普通的Bean

    public Person() {
        System.out.println("Person的无参数构造方法执行。");
    }
}

编写一个类实现FactoryBean接口

public class PersonFactoryBean implements FactoryBean<Person> {

    // PersonFactoryBean也是一个Bean。只不过这个Bean比较特殊。叫做工厂Bean。
    // 通过工厂Bean这个特殊的Bean可以获取一个普通的Bean。

    @Override
    public Person getObject() throws Exception {
        // 最终这个Bean的创建还是程序员自己new的。
        return new Person();
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }

    // 这个方法在接口中有默认实现。
    // 默认返回true,表示单例的。
    // 如果想多例,直接将这个方法修改为return false;即可。
    @Override
    public boolean isSingleton() {
        return true;
    }
}

在Spring配置文件中配置FactoryBean

<!--Spring提供的实例化方式,第四种:通过FactoryBean接口来实现。-->
<!--这种方式实际上就是第三种方式的简化。-->
<!--由于你编写的类实现了FactoryBean接口,所以这个类是一个特殊的类,不需要你再手动指定:factory-bean、factory-method-->
<!--通过一个特殊的Bean:工厂Bean。来返回一个普通的Bean Person对象。-->
<!--通过FactoryBean这个工厂Bean主要是想对普通Bean进行加工处理。-->
<bean id="person" class="com.powernode.spring6.bean.PersonFactoryBean"/>

🌊 BeanFactory和FactoryBean的区别

  • BeanFactory
    • Spring IoC容器的顶级对象,BeanFactory被翻译为“Bean工厂”,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象
    • BeanFactory是工厂。
  • FactoryBean
    • FactoryBean:它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean
    • 在Spring中,Bean可以分为两类:
      • 第一类:普通Bean
      • 第二类:工厂Bean(记住:工厂Bean也是一种Bean,只不过这种Bean比较特殊,它可以辅助Spring实例化其它Bean对象。)

🌊 注入自定义Date

我们前面说过,java.util.Date在Spring中被当做简单类型,简单类型在注入的时候可以直接使用value属性或value标签来完成。但我们之前已经测试过了,对于Date类型来说,采用value属性或value标签赋值的时候,对日期字符串的格式要求非常严格,必须是这种格式的:Mon Oct 10 14:30:26 CST 2022。其他格式是不会被识别的。

未使用FactoryBean来完成

public class Student {

    // java.util.Date 在Spring当中被当做简单类型。 但是简单类型的话,注入的日期字符串格式有要求。
    // java.util.Date 在Spring当中也可以被当做非简单类型。
    private Date birth;

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    @Override
    public String toString() {
        return "Student{" +
                "birth=" + birth +
                '}';
    }
}
<!--这种方式只能获取系统当前时间,这种时间不能作为生日。-->
<bean id="nowTime" class="java.util.Date"/>

<bean id="student" class="com.powernode.spring6.bean.Student">
    <!--把日期类型当做简单类型。-->
    <!--<property name="birth" value="Mon Oct 10 14:30:26 CST 2022"/>-->
    <!--把日期类型当做非简单类型。-->
    <property name="birth" ref="nowTime"/>
</bean>

使用FactoryBean来完成

public class DateFactoryBean implements FactoryBean<Date> {

    // DateFactoryBean这个工厂Bean协助我们Spring创建这个普通的Bean:Date。

    private String strDate;

    public DateFactoryBean(String strDate) {
        this.strDate = strDate;
    }

    @Override
    public Date getObject() throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date = sdf.parse(strDate);
        return date;
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }
}
<!--通过工厂Bean:DateFactoryBean 来返回普通Bean:java.util.Date -->
<!--先调用工厂bean的构造方法创建工厂对象,然后调用工厂对象的工厂方法创建date对象-->
<bean id="date" class="com.powernode.spring6.bean.DateFactoryBean">
    <constructor-arg index="0" value="2008-10-11"/>
</bean>

<bean id="studentBean" class="com.powernode.spring6.bean.Student">
    <property name="birth" ref="date"/>
</bean>

🥽 Bean的生命周期

🌊 什么是Bean的生命周期

  • Spring其实就是一个管理Bean对象的工厂。它负责对象的创建,对象的销毁等。
  • 所谓的生命周期就是:对象从创建开始到最终销毁的整个过程。
    • 什么时候创建Bean对象?
    • 创建Bean对象的前后会调用什么方法?
    • Bean对象什么时候销毁?
    • Bean对象的销毁前后调用什么方法?

🌊 为什么要知道Bean的生命周期

  • 其实生命周期的本质是:在哪个时间节点上调用了哪个类的哪个方法。
  • 我们需要充分的了解在这个生命线上,都有哪些特殊的时间节点。
  • 只有我们知道了特殊的时间节点都在哪,到时我们才可以确定代码写到哪。
  • 我们可能需要在某个特殊的时间点上执行一段特定的代码,这段代码就可以放到这个节点上。当生命线走到这里的时候,自然会被调用。

🌊 Bean的生命周期之5步

  • Bean生命周期的管理,可以参考Spring的源码:AbstractAutowireCapableBeanFactory类的doCreateBean()方法。
  • Bean生命周期可以粗略的划分为五大步:
    • 第一步:实例化Bean
    • 第二步:Bean属性赋值
    • 第三步:初始化Bean
    • 第四步:使用Bean
    • 第五步:销毁Bean

/**
 *
 * Bean的生命周期按照粗略的五步的话:
 * 第一步:实例化Bean(调用无参数构造方法。)
 * 第二步:给Bean属性赋值(调用set方法。)
 * 第三步:初始化Bean(会调用Bean的init方法。注意:这个init方法需要自己写,自己配。)
 * 第四步:使用Bean
 * 第五步:销毁Bean(会调用Bean的destroy方法。注意:这个destroy方法需要自己写,自己配。)
 **/
public class User{

    private String name;

    public User() {
        System.out.println("第一步:无参数构造方法执行。");
    }

    public void setName(String name) {
        System.out.println("第二步:给对象的属性赋值。");
        this.name = name;
    }

    // 这个方法需要自己写,自己配。方法名随意。
    public void initBean(){
        System.out.println("第三步:初始化Bean。");
    }

    // 这个方法需要自己写,自己配。方法名随意。
    public void destroyBean(){
        System.out.println("第五步:销毁Bean。");
    }


}
    @Test
    public void testBeanLifecycleFive(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("user", User.class);
        System.out.println("第四步:使用Bean:" + user);

        // 注意:必须手动关闭Spring容器,这样Spring容器才会销毁Bean.
        ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
        context.close();
    }
<!--需要手动指定初始化方法,和销毁方法。-->
<!-- init-method="initBean" 配置对象的初始化方法 -->
<!-- destroy-method="destroyBean" 配置对象的销毁方法 -->
<bean id="user" class="com.powernode.spring6.bean.User"
      init-method="initBean" destroy-method="destroyBean" >
    <property name="name" value="zhangsan"/>
</bean>
  • 需要注意的:
    • 第一:只有正常关闭spring容器,bean的销毁方法才会被调用。
    • 第二:ClassPathXmlApplicationContext类才有close()方法。
    • 第三:配置文件中的init-method指定初始化方法。destroy-method指定销毁方法。

🌊 Bean生命周期之7步

  • 在以上的5步中,第3步是初始化Bean,如果你还想在初始化前和初始化后添加代码,可以加入“Bean后处理器”。
  • “Bean后处理器”编写一个类实现BeanPostProcessor类,并且重写before和after方法
  • Bean生命周期七步:比五步添加的那两步在哪里?在初始化Bean的前和后。
    • 第一步:实例化Bean
    • 第二步:Bean属性赋值
    • 第三步:执行“Bean后处理器”的before方法。
    • 第四步:初始化Bean
    • 第五步:执行“Bean后处理器”的after方法。
    • 第六步:使用Bean
    • 第七步:销毁Bean

/**
 * 日志Bean后处理器。
 **/
public class LogBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第三步:执行Bean后处理器的before方法。");
        // return 代码不用修改,不用动
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    // 方法有两个参数:
    // 第一个参数:刚创建的bean对象。
    // 第二个参数:bean的名字。
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第五步:执行Bean后处理器的after方法。");
        // return 代码不用修改,不用动
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}
    <!--配置Bean后处理器。-->
    <!--注意:这个Bean后处理器将作用于整个配置文件中所有的bean。-->
    <!-- 这个Bean后处理器也只在当前配置文件起作用 -->
    <bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>

    <!--需要手动指定初始化方法,和销毁方法。-->
    <!-- init-method="initBean" 配置对象的初始化方法 -->
    <!-- destroy-method="destroyBean" 配置对象的销毁方法 -->
    <bean id="user" class="com.powernode.spring6.bean.User"
          init-method="initBean" destroy-method="destroyBean">
        <property name="name" value="zhangsan"/>
    </bean>
public class User{

    private String name;

    public User() {
        System.out.println("第一步:无参数构造方法执行。");
    }

    public void setName(String name) {
        System.out.println("第二步:给对象的属性赋值。");
        this.name = name;
    }

    // 这个方法需要自己写,自己配。方法名随意。
    public void initBean(){
        System.out.println("第四步:初始化Bean。");
    }

    // 这个方法需要自己写,自己配。方法名随意。
    public void destroyBean(){
        System.out.println("第七步:销毁Bean。");
    }
}
    @Test
    public void testBeanLifecycleFive(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("user", User.class);
        System.out.println("第六步:使用Bean:" + user);

        // 注意:必须手动关闭Spring容器,这样Spring容器才会销毁Bean.
        ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
        context.close();
    }
  • 一定要注意:在spring.xml文件中配置的Bean后处理器将作用于当前配置文件中所有的Bean。

🌊 Bean生命周期之10步

  • Bean生命周期十步:比七步添加的那三步在哪里?
    • 点位1:在“Bean后处理器”before方法之前
      • 干了什么事儿?
        • 检查Bean是否实现了Aware相关的接口,如果实现了接口则调用这些接口中的方法。
        • 然后调用这些方法的目的是为了给你传递一些数据,让你更加方便使用。
    • 点位2:在“Bean后处理器”before方法之后
      • 干了什么事儿?
        • 检查Bean是否实现了InitializingBean接口,如果实现了,则调用接口中的方法。
    • 点位3:使用Bean之后,或者说销毁Bean之前
      • 干了什么事儿?
        • 检查Bean是否实现了DisposableBean接口,如果实现了,则调用接口中的方法。
    • 添加的这三个点位的特点:都是在检查你这个Bean是否实现了某些特定的接口,如果实现了这些接口,则Spring容器会调用这个接口中的方法。

  • 上图中检查Bean是否实现了Aware的相关接口是什么意思?
  • Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
    • 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
    • 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
    • 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
  • 测试以上10步,可以让User类实现5个接口,并实现所有方法:
    • BeanNameAware
    • BeanClassLoaderAware
    • BeanFactoryAware
    • InitializingBean
    • DisposableBean
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {

    private String name;

    public User() {
        System.out.println("第一步:无参数构造方法执行。");
    }

    public void setName(String name) {
        System.out.println("第二步:给对象的属性赋值。");
        this.name = name;
    }

    // 这个方法需要自己写,自己配。方法名随意。
    public void initBean(){
        System.out.println("第四步:初始化Bean。");
    }

    // 这个方法需要自己写,自己配。方法名随意。
    public void destroyBean(){
        System.out.println("第七步:销毁Bean。");
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println("Bean这个类的加载器:" + classLoader);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("生产这个Bean的工厂对象是:" + beanFactory);
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("这个Bean的名字是:" + name);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean's afterPropertiesSet执行。");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean's destroy方法执行");
    }
}

public class LogBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第三步:执行Bean后处理器的before方法。");
        // return 代码不用修改,不用动
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    // 方法有两个参数:
    // 第一个参数:刚创建的bean对象。
    // 第二个参数:bean的名字。
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第五步:执行Bean后处理器的after方法。");
        // return 代码不用修改,不用动
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}

    <!--配置Bean后处理器。-->
    <!--注意:这个Bean后处理器将作用于整个配置文件中所有的bean。-->
    <!-- 这个Bean后处理器也只在当前配置文件起作用 -->
    <bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>

    <!--需要手动指定初始化方法,和销毁方法。-->
    <!-- init-method="initBean" 配置对象的初始化方法 -->
    <!-- destroy-method="destroyBean" 配置对象的销毁方法 -->
    <bean id="user" class="com.powernode.spring6.bean.User"
          init-method="initBean" destroy-method="destroyBean">
        <property name="name" value="zhangsan"/>
    </bean>
    @Test
    public void testBeanLifecycleFive(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("user", User.class);
        System.out.println("第六步:使用Bean:" + user);

        // 注意:必须手动关闭Spring容器,这样Spring容器才会销毁Bean.
        ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
        context.close();
    }

  • 通过测试可以看出来:
    • InitializingBean的方法早于init-method的执行。
    • DisposableBean的方法早于destroy-method的执行。

🌊 Bean的作用域不同,管理方式不同

  • Spring 根据Bean的作用域来选择管理方式。
    • 对于singleton作用域的Bean,Spring 能够精确地知道该Bean何时被创建,何时初始化完成,以及何时被销毁;Spring容器只对singleton的Bean进行完整的生命周期管理。
    • 而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。
    <!--配置Bean后处理器。-->
    <!--注意:这个Bean后处理器将作用于整个配置文件中所有的bean。-->
    <!-- 这个Bean后处理器也只在当前配置文件起作用 -->
    <bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>

    <!--需要手动指定初始化方法,和销毁方法。-->
    <!-- init-method="initBean" 配置对象的初始化方法 -->
    <!-- destroy-method="destroyBean" 配置对象的销毁方法 -->
    <bean id="user" class="com.powernode.spring6.bean.User"
          init-method="initBean" destroy-method="destroyBean" scope="prototype">
        <property name="name" value="zhangsan"/>
    </bean>
    /**
     * Spring容器只对singleton的Bean进行完整的生命周期管理。
     * 如果是prototype作用域的Bean,Spring容器只负责将该Bean初始化完毕。等客户端程序一旦获取到该Bean之后,Spring容器就不再管理该对象的生命周期了。
     */
    @Test
    public void testBeanLifecycleFive(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("user", User.class);
        System.out.println("第六步:使用Bean:" + user);

        // 注意:必须手动关闭Spring容器,这样Spring容器才会销毁Bean.
        ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
        context.close();
    }

🌊 自己new的对象如何让Spring管理

有些时候可能会遇到这样的需求,某个java对象是我们自己new的,然后我们希望这个对象被Spring容器管理,怎么实现?

    @Test
    public void testRegisterBean(){
        // 自己new的对象
        Student student = new Student();
        System.out.println(student);

        // 将以上自己new的这个对象纳入Spring容器来管理。半路上交给Spring来管理。
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        // 自己new的对象注册到spring容器中
        factory.registerSingleton("studentBean", student);

        // 从spring容器中获取
        Object studentBean = factory.getBean("studentBean");
        System.out.println(studentBean);
    }

🥽 Bean的循环依赖问题

🌊 什么是Bean的循环依赖

A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。

比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。

🌊 singleton下的set注入产生的循环依赖

在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。

public class Husband {
    private String name;
    private Wife wife;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setWife(Wife wife) {
        this.wife = wife;
    }

    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}
public class Wife {
    private String name;
    private Husband husband;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setHusband(Husband husband) {
        this.husband = husband;
    }

    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband.getName() +
                '}';
    }
}
<!--singleton + setter模式下的循环依赖是没有任何问题的。-->
<!--singleton表示在整个Spring容器当中是单例的,独一无二的对象。-->
<!--
    在singleton + setter模式下,为什么循环依赖不会出现问题,Spring是如何应对的?
        主要的原因是,在这种模式下Spring对Bean的管理主要分为清晰的两个阶段:
            第一个阶段:在Spring容器加载的时候,实例化Bean,只要其中任意一个Bean实例化之后,马上进行 “曝光”【不等属性赋值就曝光】(因为单例下就只会创建该类型对象一个)
            第二个阶段:Bean“曝光”之后,再进行属性的赋值(调用set方法。)。

        核心解决方案是:实例化对象和对象的属性赋值分为两个阶段来完成的。

    注意:只有在scope是singleton的情况下,Bean才会采取提前“曝光”的措施。
-->
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
    <property name="name" value="张三"/>
    <property name="wife" ref="wifeBean"/>
</bean>

<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
    <property name="name" value="小花"/>
    <property name="husband" ref="husbandBean"/>
</bean>
    @Test
    public void testCD(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

        Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
        System.out.println(husbandBean);

        Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
        System.out.println(wifeBean);
    }

🌊 prototype下的set注入产生的循环依赖

当循环依赖的所有Bean的scope="prototype"的时候,产生的循环依赖,Spring是无法解决的,会出现BeanCurrentlyInCreationException异常。

public class Husband {
    private String name;
    private Wife wife;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setWife(Wife wife) {
        this.wife = wife;
    }

    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}
public class Wife {
    private String name;
    private Husband husband;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setHusband(Husband husband) {
        this.husband = husband;
    }

    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband.getName() +
                '}';
    }
}
<!--在prototype + setter模式下的循环依赖,存在问题,会出现异常!-->
<!--BeanCurrentlyInCreationException 当前的Bean正在处于创建中异常。。。-->
<!-- 注意:当两个bean的scope都是prototype的时候,才会出现异常。如果其中任意一个是singleton的,就不会出现异常。-->
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype">
    <property name="name" value="张三"/>
    <property name="wife" ref="wifeBean"/>
</bean>

<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype">
    <property name="name" value="小花"/>
    <property name="husband" ref="husbandBean"/>
</bean>
    @Test
    public void testCD(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

        Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
        System.out.println(husbandBean);

        Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
        System.out.println(wifeBean);
    }

如果其中一个是singleton,另一个是prototype,是没有问题的。singleton在解析配置文件时就创建对象,当需要prototype时可以马上创建一个prototype对象赋值给singleton,而prototype对象所需的singleton只有唯一的一个,可以让依赖停止下来,所以没有问题。

🌊 singleton下的构造注入产生的循环依赖

public class Husband {
    private String name;
    private Wife wife;

    public Husband(String name, Wife wife) {
        this.name = name;
        this.wife = wife;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}
public class Wife {
    private String name;
    private Husband husband;

    public Wife(String name, Husband husband) {
        this.name = name;
        this.husband = husband;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband.getName() +
                '}';
    }
}
<!--构造注入,这种循环依赖有没有问题?-->
<!--注意:基于构造注入的方式下产生的循环依赖也是无法解决的,所以编写代码时一定要注意。-->
<!--使用构造注入无法创建完对象马上进行曝光,因为创建对象需要另一个对象作为属性,无法完成对象的创建(类似死锁)-->
<bean id="h" scope="singleton" class="com.powernode.spring6.bean2.Husband">
    <constructor-arg index="0" value="张三"></constructor-arg>
    <constructor-arg index="1" ref="w"></constructor-arg>
</bean>

<bean id="w" scope="singleton" class="com.powernode.spring6.bean2.Wife">
    <constructor-arg index="0" value="小花"></constructor-arg>
    <constructor-arg index="1" ref="h"></constructor-arg>
</bean>
    @Test
    public void testCD2(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");

        Husband husbandBean = applicationContext.getBean("h", Husband.class);
        System.out.println(husbandBean);

        Wife wifeBean = applicationContext.getBean("w", Wife.class);
        System.out.println(wifeBean);
    }

执行结果:发生了异常,信息如下:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'hBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
	... 56 more
  • 和上一个测试结果相同,都是提示产生了循环依赖,并且Spring是无法解决这种循环依赖的。
  • 为什么呢?
  • 主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。

🌊 Spring解决循环依赖的机理

Spring解决循环依赖的机理:https://www.yuque.com/dujubin/ltckqu/kipzgd#wt8oL

  • Spring为什么可以解决set + singleton模式下循环依赖?

  • 根本的原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。

  • 实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。

  • 给Bean属性赋值的时候:调用setter方法来完成。

  • 两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。

  • 也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。

  • 在Spring框架底层源码级别上是如何实现的呢?

    源码分析:
    DefaultSingletonBeanRegistry类中有三个比较重要的缓存:
        private final Map<String, Object> singletonObjects                  一级缓存
        private final Map<String, Object> earlySingletonObjects             二级缓存
        private final Map<String, ObjectFactory<?>> singletonFactories      三级缓存
    
        这三个缓存都是Map集合。
        Map集合的key存储的都是bean的name(bean id)。
    
        一级缓存存储的是:单例Bean对象。完整的单例Bean对象,也就是说这个缓存中的Bean对象的属性都已经赋值了。是一个完整的Bean对象。
        二级缓存存储的是:早期的单例Bean对象。这个缓存中的单例Bean对象的属性没有赋值。只是一个早期的实例对象。
        三级缓存存储的是:单例工厂对象。这个里面存储了大量的“工厂对象”,每一个单例Bean对象都会对应一个单例工厂对象。
                        这个集合中存储的是,创建该单例对象时对应的那个单例工厂对象。
    

    在该类中有这样一个方法addSingletonFactory(),这个方法的作用是:将创建Bean对象的ObjectFactory对象提前曝光。


    从源码中可以看到,spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。

  • Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。

有关[Java]Spring6(动力节点老杜)的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. ruby-on-rails - 带 Spring 锁的 Rails 4 控制台 - 2

    我正在使用Ruby2.1.1和Rails4.1.0.rc1。当执行railsc时,它被锁定了。使用Ctrl-C停止,我得到以下错误日志:~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`gets':Interruptfrom~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`verify_server_version'from~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.

  3. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  4. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  5. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  6. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  7. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  8. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  9. spring.profiles.active和spring.profiles.include的使用及区别说明 - 2

    转自:spring.profiles.active和spring.profiles.include的使用及区别说明下文笔者讲述spring.profiles.active和spring.profiles.include的区别简介说明,如下所示我们都知道,在日常开发中,开发|测试|生产环境都拥有不同的配置信息如:jdbc地址、ip、端口等此时为了避免每次都修改全部信息,我们则可以采用以上的属性处理此类异常spring.profiles.active属性例:配置文件,可使用以下方式定义application-${profile}.properties开发环境配置文件:application-dev

  10. java - 为什么 ruby​​ modulo 与 java/other lang 不同? - 2

    我基本上来自Java背景并且努力理解Ruby中的模运算。(5%3)(-5%3)(5%-3)(-5%-3)Java中的上述操作产生,2个-22个-2但在Ruby中,相同的表达式会产生21个-1-2.Ruby在逻辑上有多擅长这个?模块操作在Ruby中是如何实现的?如果将同一个操作定义为一个web服务,两个服务如何匹配逻辑。 最佳答案 在Java中,模运算的结果与被除数的符号相同。在Ruby中,它与除数的符号相同。remainder()在Ruby中与被除数的符号相同。您可能还想引用modulooperation.

随机推荐