目录

1 Spring启示录

1.1 OCP开闭原则

1.2 依赖倒置原则DIP

1.3 控制反转IoC

2 Spring概述

2.1 Spring简介

2.2 Spring8大模块

2.3 Spring特点

2.4 本次学习使用软件版本

3 Spring入门程序

3.1 Spring下载

3.2 第一个Spring程序

3.3 第一个spring程序的细节

3.4 Spring6启用Log4j2日志框架

4 spring对IoC的实现

4.1 set注入

4.2 构造注入

4.3 set注入专题

4.3.1 注入内部bean和外部bean

4.3.2 注入简单类型

4.3.3 级联属性赋值

4.3.4 数组注入

4.3.5 集合注入

4.3.6 注入null和空字符串

4.3.7 特殊字符的注入

4.4 p命名空间注入

4.5 c命名空间注入

4.6 util命名空间

4.7 基于XML的自动装配

4.7.1 根据名称自动装配

4.7.2 根据类型自动装配

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

5 Bean的作用域

5.1 singleton

5.2 prototype

5.3 其它scope

6 GoF工厂设计模式

6.1 工厂模式三种形态

6.2 简单工厂模式

6.3 工厂方法模式

6.4 抽象工厂模式(了解)

7 Bean的实例化方式

7.1 通过构造方法实例化

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

7.3 通过factory-bean实例化

7.4 通过FactoryBean接口实例化

7.5 BeanFactory和FactoryBean的区别

7.5.1 BeanFactory

7.5.2 FactoryBean

7.6 注入自定义Date

8 Bean的声明周期

8.1 什么是Bean的生命周期

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

8.3 Bean的声明周期之五步

8.4 Bean生命周期之7步

8.5 Bean生命周期之10步

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

9 Bean的循环依赖问题

9.1 什么是Bean的循环依赖

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

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

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

10 回顾反射机制

10.1 分析方法四要素

10.2 使用反射机制调用方法

10.3 假设知道属性名


1 Spring启示录

我们之前学过mvc设计模式,这种模式可以有效的降低代码的耦合度,提高扩展力,我们再写一个这样的模式,代码如下:

在创建maven项目前我们可以先设置如下

package com.itzw.spring6.dao.impl;

import com.itzw.spring6.dao.UserDao;

public class UserDaoImplForMySQL implements UserDao {
    public void select() {
        System.out.println("正在连接数据库。。。");
    }
}
package com.itzw.spring6.service.impl;

import com.itzw.spring6.dao.UserDao;
import com.itzw.spring6.dao.impl.UserDaoImplForMySQL;
import com.itzw.spring6.service.UserService;

public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImplForMySQL();
    public void login() {
        userDao.select();
    }
}
package com.itzw.spring6.servlet;

import com.itzw.spring6.service.UserService;
import com.itzw.spring6.service.impl.UserServiceImpl;

public class UserServlet {
    private UserService userService = new UserServiceImpl();
    public void loginRequest(){
        userService.login();
    }
}

以上大概就是我们之前学的mvc架构模式,分为表示层、业务逻辑层、持久层。而其中UserServlet依赖了具体的UserServiceImpl,UserServiceImpl依赖了具体的UserDaoImplForMySQL。

假如我们不想连接mysql数据库了,我们想连接Oracle数据库,我们就要修改UserServiceImpl中的代码。

1.1 OCP开闭原则

什么是OCP?

  • OCP是软件七大开发原则当中最基本的一个原则。对扩展开放,对修改关闭
  • 其中OCP原则是最核心的最基本的,其它六个原则都是为了这个原则服务的
  • OCP开闭原则的核心是:当我们在扩展系统功能的时候,没有修改以前写好的代码那么就是符合OCP原则的,反之不符合这个原则

如上图可以很明显的看出上层是依赖下层的,下面改动上面必然改动,这样同样违背了另一个开发原则:依赖倒置原则

1.2 依赖倒置原则DIP

依赖倒置原则(Dependence Inversion Principe),倡导面向接口编程,面向抽象编程,不要面向具体编程,让上层不再依赖下层,下层改动上层不需要改动,这样大大降低耦合度,耦合度低了,扩展力就强了,同时代码复用性也会增强(软件七大开发原则都在为解耦合服务)

那我们可能有疑问,这不就是面向接口编程的吗?确实,是这样的,但是不完全是,我们虽然都是调用接口中的方法,但是我们是通过new 对象,new一个具体的接口实现类,如下:

如下才是完全面向接口,完全符合依赖倒置原则:

但是如果这样编程userDao是null,那么就会出现空指针异常,确实是这样。这也就是我们接下来要解决的问题。

1.3 控制反转IoC

控制反转(Inversion of Control),是面向对象编程的一种设计思想,可以用来降低代码的耦合度,符合依赖倒置原则。控制反转的核心是:将对象的创建权交出去,将对象和对象之间的关系管理权交出去,由第三方容器负责创建与维护

我们要学的Spring框架实现了控制反转这种思想,Spring框架可以帮我们new 对象,还可以帮我们维护对象与对象之间的关系

控制反转常用的实现方法:依赖注入(Dependency Injection,简称DI)

依赖注入DI,包括两种常见的 方式:

  • 第一种:set方法注入(执行set方法给属性赋值)
  • 第二种:构造方法注入

IoC是一种全新的设计模式,但是理论和时间成熟较晚,并没有包含在GoF中(GoF是23中设计模式)

2 Spring概述

2.1 Spring简介

来自百度百科:

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

2.2 Spring8大模块

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

Spring Core模块:这是Spring框架最基础的部分,它提供了依赖注入特征来实现容器对Bean的管理。

2.3 Spring特点

  • 轻量
  • 控制反转
  • 面向切面
  • 容器
  • 框架

2.4 本次学习使用软件版本

  • IDEA:2021.2.3
  • JDK:java17(Spring6要求JDK最低版本是java17)
  • Maven:3.3.9
  • Spring:6.0.0-M2
  • Junit:4.13.2

3 Spring入门程序

3.1 Spring下载

官网地址:Spring | Home

官网地址(中文):Spring 中文网 官网

以上两个地址都可以下载,不过我们可以直接使用maven下载依赖,就像mybatis一样

3.2 第一个Spring程序

前期准备:在idea中创建一个maven模块,这个我们早已设置好,直接用即可

第一步:添加spring context依赖

    <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依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.0-M2</version>
        </dependency>
    </dependencies>

注意:打包方式为jar

当加入spring context依赖之后,会关联引入其它依赖

  • spring aop:面向切面编程
  • spring beans:IoC核心
  • spring core:spring核心工具包
  • spring jcl:spring的日志包
  • spring expression:spring表达式

第二步:添加junit依赖

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

第三步:定义bean:User

package com.itzw.spring6.bean;

/**
 * 封装用户信息
 */
public class User {
}

第四步:编写spring配置文件spring.xml,放在类的根目录下也就是resources目录

我们直接右击resources就可以创建idea提供的文件模板:

在配置文件中进行bean配置:

<?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="userBean" class="com.itzw.spring6.bean.User"></bean>

</beans>

需要注意的是:这个文件最好放在类路径下,方便移植

bean标签的两个重要属性:id:这是bean的身份证,不能重复,是唯一标识;class:必须填写类的全路径,全限定类名。

第五步:编写测试程序

package com.itzw.spring6.test;

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

public class Spring6Test {
    @Test
    public void testFirst(){
        //第一步:获取spring容器对象
        //ApplicationContext是一个接口,接口下有很多实现类,其中有一个叫做:ClassPathXmlApplicationContext
        //ClassPathXmlApplicationContext 专门从类路径下加载spring配置文件的一个spring上下文对象
        //运行这行代码就相当于启动了spring容器,解析spring.xml文件,并且实例化所有的bean对象,放到spring容器当中
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

        //第二步:根据bean的id从spring容器中获取这个对象
        Object userBean = applicationContext.getBean("userBean");
        System.out.println(userBean);
    }

}

测试结果:

3.3 第一个spring程序的细节

(1)bean的id不能重复:

    <bean id="userBean" class="com.itzw.spring6.bean.User"></bean>
    <bean id="userBean" class="com.itzw.spring6.bean.Vip"></bean>

(2)底层是怎样创建对象的:

我们在User类中写上无参构造:

public class User {
    public User(){
        System.out.println("这是User类的无参构造");
    }
}

测试:

如果只有有参构造没有无参呢?

public class User {
/*    public User(){
        System.out.println("这是User类的无参构造");
    }*/

    public User(String name){
        System.out.println("这是User类的有参构造");
    }
}

经过测试:spring是通过调用类的无参构造来创建对象的,所以要想让spring给你创建对象,必须保证无参构造方法是存在的

spring是通过反射机制调用无参构造方法创建对象

(3)创建好的对象存储在map集合中

(4)Spring配置文件的名字可以随意改

(5)spring配置文件可以创建多个

我们再创建一个spring配置文件,配置bean的信息

我们直接在ClassPathXmlApplicationContext构造方法参数上传递路径即可,不需要再new一个ClassPathXmlApplicationContext对象,为什么呢?通过源码查看是可以传多个的 

(6)配置文件中的类必须是自定义的吗

不是,可以是jdk自带的类,如下:

<bean id="dateBean" class="java.util.Date"></bean>
        Object dateBean = applicationContext.getBean("dateBean");
        System.out.println(dateBean);

可以直接输出当前日期

经测试,spring配置文件中的bean可以是任意类,只要它不是抽象的并且有无参构造

(7)执行getBean方法时传入的参数不存在会报异常

(8)getBean方法返回类型问题

默认返回类型是Object,当然我们可以强转,但是有没有别的办法,

User userBean1 = applicationContext.getBean("userBean", User.class);

可以通过第二个参数返回bean的类型

3.4 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>

这样我们的输出信息就有日志信息了: 

但是我们自己怎么使用日志信息呢?

    @Test
    public void testLog(){
        //第一步:创建日志记录对象
        //获取Spring6Test类的日志记录对象,也就是说只要是Spring6Test类中的代码记录日志的话,就输出日志信息
        Logger logger = LoggerFactory.getLogger(Spring6Test.class);

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

4 spring对IoC的实现

前面我们讲过我们可以使用依赖注入实现控制反转。控制反转的思想是将对象的创建权交出去,交给第三方容器。

实现依赖注入主要有两个方式:set注入和构造注入

4.1 set注入

我们创建一个新的模块,我们先像之前那样创建一个dao文件和一个service文件,不过这次在service目录下我们不new新的dao对象,如下:

package com.itzw.spring6.dao;

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

public class UserDao {
    private static final Logger logger = LoggerFactory.getLogger(UserDao.class);
    public void insert(){
        logger.info("正在插入信息。。");
    }
}
package com.itzw.spring6.service;

import com.itzw.spring6.dao.UserDao;

public class UserService {

    UserDao userDao;

    public void saveUser(){
        userDao.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="userDaoBean" class="com.itzw.spring6.dao.UserDao"></bean>
    <bean id="userServiceBean" class="com.itzw.spring6.service.UserService"></bean>
</beans>
public class SpringTest {
    @Test
    public void testInjectBySet(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        UserService userServiceBean = applicationContext.getBean("userServiceBean", UserService.class);
        userServiceBean.saveUser();
    }
}

但是这样测试结果显然是出错的,dao对象是null的

我们使用set注入的方式来实现控制反转:

我们需要提供一个set方法,spring会调用这个set方法给userDao属性赋值,我们直接使用idea工具生成这个set方法即可

我们现在需要service调用set方法,这就要借助spring,我们在bean标签配置property标签,其中name属性就是用来传set方法的,name属性名指定格式为set方法的方法名去掉set,然后剩下的单词首字母小写。这就实现了UserService类调用set方法,但是我们还需要传值,传一个UserDao对象的值,因为上面我们配置类userDao的bean标签,我们直接把它的id值传给property中的ref属性即可这就完成了传值。如下:

配置好后再次测试,测试成功。

4.2 构造注入

我们多建一个dao文件,在service文件中直接用idea自动生成构造方法

package com.itzw.spring6.service;

import com.itzw.spring6.dao.UserDao;
import com.itzw.spring6.dao.VipDao;

public class AccountService {
    UserDao userDao;
    VipDao vipDao;

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

    public void test(){
        userDao.insert();
        vipDao.delete();
    }
}

在spring配置文件中配置如下,和set方法差不多

    <bean id="userDaoBean" class="com.itzw.spring6.dao.UserDao"></bean>
    <bean id="vipDaoBean" class="com.itzw.spring6.dao.VipDao"></bean>

    <!--构造注入-->
    <bean id="accountServiceBean" class="com.itzw.spring6.service.AccountService">
        <!--index属性指定参数下标,第一个参数是0,第二个参数是1,第三个参数是3,依次。。
            ref指定注入的bean的id-->
        <!--<constructor-arg index="0" ref="userDaoBean"/>
        <constructor-arg index="1" ref="vipDaoBean"/>-->
        <!--我们还有别的方法进行构造注入-->
        <constructor-arg name="userDao" ref="userDaoBean"/>
        <constructor-arg name="vipDao" ref="vipDaoBean"/>
    </bean>

测试即可。

4.3 set注入专题

以为set注入使用的较多,我们使用set注入来学习下面的内容

4.3.1 注入内部bean和外部bean

我们之前使用的set注入就是注入外部bean,那什么是注入内部bean呢?

在property中嵌套bean标签就是内部bean。这样麻烦了一点,我们一般不用这种方式,我们还是用之前的方式 

    <bean id="userDaoBean" class="com.itzw.spring6.dao.UserDao"/>
    <bean id="orderDaoBean" class="com.itzw.spring6.service.OrderDao">
        <property name="userDao">
            <bean class="com.itzw.spring6.dao.UserDao"/>
        </property>
    </bean>

4.3.2 注入简单类型

我们之前注入的数据都不是简单类型,对象属性都是一个对象,我们现在注入属性是一个数据的。

我们写一个类,写上属性,创建set方法和toString方法:

package com.itzw.spring6.beans;

public class User {
    private String name;
    private int age;

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

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

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

编写spring配置文件:

    <!--简单类型注入-->
    <bean id="userBean" class="com.itzw.spring6.beans.User">
        <property name="age" value="24"/>
        <property name="name" value="张麻子"/>
    </bean>

在这里name属性依然是set方法,但是我们给set方法参数传值就只要传简单的数据就可以了,所以我们使用value来赋值。

测试:结果如下:

那么简单类型包括哪些呢?

	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));
	}

我们查看源码分析:BeanUtils类,得知简单类型有:

  • 基本数据类型
  • 基本数据类型对应包装类
  • String或其他的CharSequence子类
  • Number子类
  • Date子类
  • Enum子类
  • URI
  • URL
  • Temporal子类
  • Locale
  • Class
  • 另外还包括以上简单值类型对应的数据类型

简单类型注入的经典应用:

给数据源的属性赋值,比如我们经常使用的数据库的连接:

    private String driver;
    private String url;
    private String username;
    private String password;

    @Override
    public String toString() {
        return "MyDataSource{" +
                "driver='" + driver + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + 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;
    }
    <!--简单类型注入的应用-->
    <bean id="dataSourceBean" class="com.itzw.spring6.jdbc.MyDataSource">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://172.0.0.1:3306/spring"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </bean>
    @Test
    public void testSimple2(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
        Object dataSourceBean = applicationContext.getBean("dataSourceBean");
        System.out.println(dataSourceBean);
    }

你们可以把简单类型都测试一遍,那我不测了,我不打扰我走了哈哈。

4.3.3 级联属性赋值

也就是我们熟悉的套娃赋值,就是一个类的属性有另一个类。我们先用我们学过的方式赋值:

package com.itzw.spring6.dao;

public class Clazz {
    private String name;

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

    @Override
    public String toString() {
        return "Clazz{" +
                "name='" + name + '\'' +
                '}';
    }
}
package com.itzw.spring6.dao;

public class Student {
    private String name;
    private int age;
    private Clazz clazz;

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

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

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

    public void setAge(int age) {
        this.age = age;
    }
}
    <!--级联属性赋值-->
    <bean class="com.itzw.spring6.dao.Student" id="studentBean">
        <property name="name" value="张麻子"/>
        <property name="age" value="34"/>
        <property name="clazz" ref="clazzBean"/>
    </bean>
    <bean class="com.itzw.spring6.dao.Clazz" id="clazzBean">
        <property name="name" value="高三一班"/>
    </bean>
    @Test
    public void testCascade(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
        Student studentBean = applicationContext.getBean("studentBean", Student.class);
        System.out.println(studentBean);
    }

以上是我们学过的方式,测试结果:

我们使用级联属性赋值:

    <!--级联属性赋值-->
    <bean class="com.itzw.spring6.dao.Student" id="studentBean">
        <property name="name" value="张麻子"/>
        <property name="age" value="34"/>
        <property name="clazz" ref="clazzBean"/>
        <!--级联属性赋值-->
        <property name="clazz.name" value="高三二班"/>
    </bean>
    <bean class="com.itzw.spring6.dao.Clazz" id="clazzBean"></bean>

使用这种方式我们需要给clazz属性构造get方法,这种方式显得很麻烦,还不如之前的方法。

4.3.4 数组注入

首先简单类型的数组注入:

package com.itzw.spring6.dao;

import java.util.Arrays;

public class Huang {
    private String[] hobbies;

    public void setHobbies(String[] hobbies) {
        this.hobbies = hobbies;
    }

    @Override
    public String toString() {
        return "Huang{" +
                "hobbies=" + Arrays.toString(hobbies) +
                '}';
    }
}
    <!--数组注入-->
    <bean id="huang" class="com.itzw.spring6.dao.Huang">
        <property name="hobbies">
            <array>
                <value>抽烟</value>
                <value>喝酒</value>
                <value>烫头</value>
            </array>
        </property>
    </bean>
    @Test
    public void testArray(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
        Huang huang = applicationContext.getBean("huang", Huang.class);
        System.out.println(huang);
    }

如果数组元素是非简单类型呢?

    <bean class="com.itzw.spring6.dao.Woman" id="w1">
        <property name="name" value="小花"/>
    </bean>
    <bean class="com.itzw.spring6.dao.Woman" id="w2">
        <property name="name" value="小美"/>
    </bean>
    <bean class="com.itzw.spring6.dao.Woman" id="w3">
        <property name="name" value="小丽"/>
    </bean>
    <!--数组注入-->
    <bean id="huang" class="com.itzw.spring6.dao.Huang">
        <property name="hobbies">
            <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>

4.3.5 集合注入

注意的是:list集合有序和重复,set集合无序不重复

package com.itzw.spring6.dao;

import java.util.List;
import java.util.Set;

public class Person {
    List names;
    Set addrs;

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

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

    @Override
    public String toString() {
        return "Person{" +
                "names=" + names +
                ", addrs=" + addrs +
                '}';
    }
}
    <!--集合注入set和list-->
    <bean class="com.itzw.spring6.dao.Person" id="person">
        <property name="names">
            <list>
                <value>张三</value>
                <value>李四</value>
                <value>张麻子</value>
                <value>黄四郎</value>
                <value>张三</value>
            </list>
        </property>
        <property name="addrs">
            <set>
                <value>徐州市铜山区</value>
                <value>徐州市云龙区</value>
                <value>徐州市铜山区</value>
                <value>徐州市铜山区</value>
            </set>
        </property>
    </bean>
    @Test
    public void testListAndSet(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
        Person person = applicationContext.getBean("person", Person.class);
        System.out.println(person);
    }

map集合,有键值对:

        <property name="tel">
            <map>
                <!--一个entry就表示一个键值对-->
                <entry key="tel1" value="110"/>
                <entry key="tel2" value="119"/>
                <entry key="tel3" value="120"/>
            </map>
        </property>

properties集合本质上也是map集合,但是它的注入方式不一样

        <property name="properties">
            <props>
                <prop key="username">root</prop>
                <prop key="password">1234</prop>
            </props>
        </property>

4.3.6 注入null和空字符串

    <!--注入null和空字符串-->
    <bean class="com.itzw.spring6.dao.Man" id="man">
        <!--<property name="name" value="张麻子"></property>-->
        <property name="name">
            <!--手动赋值为null-->
            <null></null>
        </property>
        <!--赋值为空字符串-->
        <!--第一种方式-->
        <!--<property name="addr" value=""/>-->
        <!--第二种方式-->
        <property name="addr">
            <value/>
        </property>
    </bean>

4.3.7 特殊字符的注入

XML中有5个特殊字符,分别是:<、>、'、"、&。

这些字符直接出现在xml当中会报错:

解决方式有两种:

  • 第一种:使用转义字符代替
  • 第二种:将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。

特殊字符对应的转移字符如下:

特殊字符

转义字符

>

&gt;

<

&lt;

'

&apos;

"

&quot;

&

&amp;

    <!--特殊字符的注入-->
    <bean class="com.itzw.spring6.dao.Zhou" id="zhou">
        <!--第一种方式-->
        <property name="name" value="1 &lt; 2"/>
        <!--第二种方式-->
        <property name="addr">
            <value><![CDATA[2<3]]></value>
        </property>
    </bean>

4.4 p命名空间注入

使用p命名空间注入可以简化配置,使用前提是:

其实p命名空间注入就是代替set注入的

<bean class="com.itzw.spring6.dao.Dog" id="dog" p:name="小花" p:age="2"/>

4.5 c命名空间注入

c命名空间注入是用来简化构造注入的,那么使用前提是:

  • 需要在xml配置文件头部添加信息:xmlns:c="http://www.springframework.org/schema/c"
  • 需要提供构造方法。 
<bean class="com.itzw.spring6.dao.Cat" id="cat" c:name="小猫" c:age="2"/>

注意:不管是p命名注入还是c命名注入都可以注入非简单类型

4.6 util命名空间

使用util命名空间可以让配置复用,使用前提是:在spring配置文件头部添加如下信息:

比如我想给多个java文件都传输jdbc连接的信息,它们的信息都是一样的,这时我们就可以将这段信息使用util命名空间方式

<?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:properties id="prop">
        <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
        <prop key="url">jdbc:mysql://localhost:3306/spring</prop>
        <prop key="username">root</prop>
        <prop key="password">123</prop>
    </util:properties>

    <bean class="com.itzw.spring6.jdbc.MyDataSource2" id="ds2">
        <property name="properties" ref="prop"/>
    </bean>
    <bean class="com.itzw.spring6.jdbc.MyDataSource3" id="ds3">
        <property name="properties" ref="prop"/>
    </bean>
</beans>

4.7 基于XML的自动装配

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

4.7.1 根据名称自动装配

回忆之前的业务逻辑层和持久层之间的连接,其中spring配置信息如下:

    <bean class="com.itzw.spring6.dao.UserDao" id="userDaoBean">
    </bean>
    <bean class="com.itzw.spring6.service.OrderDao" id="orderDao">
        <property name="userDao" ref="userDaoBean"/>
    </bean>

我们使用自动装配:

    <bean class="com.itzw.spring6.dao.UserDao" id="userDao"/>
    <bean class="com.itzw.spring6.service.OrderDao" id="orderDao" autowire="byName"/>

在orderDao的bean中添加autowire,值设为byName表示通过名称进行自动装配

OrderDao类中有一个UserDao属性,set方法为setUserDao,而UserDao的bean的id为userDao,恰好和OrderDao中的set方法对应。满足这些才能自动装配

也就是说需要set方法名和想要注入的类的bean的id值对应上才行

4.7.2 根据类型自动装配

    <bean class="com.itzw.spring6.dao.UserDao"/>
    <bean class="com.itzw.spring6.service.OrderDao" id="orderDao" autowire="byType"/>

这样连id值都不需要传了,直接就能识别自己想要的类。但是也有缺陷,不能出现多个同一个类的bean,这样它就识别不出哪个是自己需要的了。

值得注意的是:不管是根据name自动装配还是类型自动装配都是基于set注入实现的,也就是都需要有set方法,否则不行。

从这自动装配我们可以看出来,尤其是根据类型自动装配可读性 很差而且有限制,不如我们用原始方法,没有方便多少反而看的 蛋疼。

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

我们连接数据库的时候需要配置一些信息,我们能像之前学习一样把这些配置信息放在一个文件中然后引入到xml文件中吗?当然可以。

第一步:写一个数据源类 提供相关属性:

package com.itzw.spring6.jdbc;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class MyDataSource implements DataSource {

    private String driver;
    private String url;
    private String username;
    private String password;

    @Override
    public String toString() {
        return "MyDataSource{" +
                "driver='" + driver + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + 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 Connection getConnection() throws SQLException {
        return null;
    }

    //...

}

第二步:在类路径下建立jdbc.properties文件并配置信息:

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=123

第三步:在spring配置文件中引入context命名空间

<?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">

</beans>

第四步:在spring配置文件中使用:

使用context标签引入jdbc配置文件

    <context:property-placeholder location="jdbc.properties"/>
    <bean class="com.itzw.spring6.jdbc.MyDataSource" id="ds">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
    </bean>

但需要注意的是,${}中的值会默认先去系统找对应的值,比如username会去系统找,极可能输出的结果是系统也就是Windows的usernam。所以我们在配置名称的时候最好前面加上jdbc.

5 Bean的作用域

5.1 singleton

默认情况下,Spring的IoC容器创建的Bean对象是单例的。

    @Test
    public void testBean(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-bean.xml");
        Customer customer1 = applicationContext.getBean("customer", Customer.class);
        System.out.println(customer1);
        Customer customer2 = applicationContext.getBean("customer", Customer.class);
        System.out.println(customer2);
        Customer customer3 = applicationContext.getBean("customer", Customer.class);
        System.out.println(customer3);
    }

如上我们调用三次getBean,返回的是同一个对象

那么这个对象在什么时候创建的呢,我们写上无参构造,把getBean方法都删除,执行程序,发现无参构造执行了,我们得知默认情况下,Bean对象的创建是在初始化Spring上下文的时候就完成的。

5.2 prototype

如果想让spring的bean对象以多例的形式存在,可以在bean标签中指定scope属性的值为:prototype,这样spring会在每一次执行getBean的时候都创建bean对象,调用几次就创建几次

我们再执行上段代码: 

这时如果不调用getBean方法,那么无参构造就不会只执行

5.3 其它scope

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:很少使用。

我们可以自己定义,但是没必要,再见。

6 GoF工厂设计模式

设计模式:一种可以被重复利用的解决方案。GoF(Gang of Four),中文名——四人组。

GoF包括了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。

不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。

GoF23种设计模式可分为三大类:

创建型(5个):解决对象创建问题。

  • 单例模式
  • 原型模式
  • 建造者模式
  • 抽象工厂模式
  • 工厂方法模式

结构型(7个):一些类或对象组合在一起的经典结构。

  • 代理模式
  • 桥接模式
  • 外观模式
  • 享元模式
  • 组合模式
  • 适配器模式
  • 装饰模式

行为型(11个):解决类或对象之间的交互问题。

  • 策略模式
  • 解释器模式
  • 中介者模式
  • 访问者模式
  • 状态模式
  • 备忘录模式
  • 命令模式
  • 迭代子模式
  • 观察者模式
  • 责任链模式
  • 模板方法模式

工厂模式是解决对象创建问题的,所以工厂模式属于创建型设计模式。这里为什么学习工厂模式呢?这是因为Spring框架底层使用了大量的工厂模式

6.1 工厂模式三种形态

工厂模式通常有三种形态:

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

6.2 简单工厂模式

简单工厂模式的角色包括三个:

  • 抽象产品 角色
  • 具体产品 角色
  • 工厂类 角色

简单工厂模式的代码如下:

抽象产品角色:

package com.itzw.factory;

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

具体产品角色:

package com.itzw.factory;

public class Gun extends Weapon{

    @Override
    public void attack() {
        System.out.println("机枪正在发射...");
    }
}
package com.itzw.factory;

public class Plane extends Weapon{
    @Override
    public void attack() {
        System.out.println("飞机正在扔小男孩...");
    }
}
package com.itzw.factory;

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

工厂类角色:

package com.itzw.factory;

public class WeaponFactory {
    public static Weapon get(String WeaponType){
        if ("GUN".equals(WeaponType)){
            return new Gun();
        }else if ("PLANE".equals(WeaponType)){
            return new Plane();
        }else if ("TANK".equals(WeaponType)){
            return new Tank();
        }else {
            throw new RuntimeException("不支持该武器");
        }
    }
}

测试:

package com.itzw.factory;

public class Test {
    public static void main(String[] args) {
        Weapon tank = WeaponFactory.get("TANK");
        tank.attack();
        Weapon plane = WeaponFactory.get("PLANE");
        plane.attack();
        Weapon gun = WeaponFactory.get("GUN");
        gun.attack();
    }
}

这种模式就是简单工厂模式,它的优点:客户端程序,也就是我们这里的 测试程序不需要关系对象的创建细节,需要哪个对象只需要向工厂索要,初步实现了责任的分离。客户端只负责消费,工厂只负责生产。但 它也有缺点:工厂类中集中了所有产品的创造逻辑,一旦出问题整个系统会瘫痪;还有就是比较明显的,不符合OCP开闭原则,我们想扩展系统时也就是比如需要扩展一个新的武器需要修改工厂类。

6.3 工厂方法模式

工厂方法模式的角色包括:

  • 抽象工厂角色
  • 具体工厂角色
  • 抽象产品角色
  • 具体产品角色

抽象产品角色和具体产品角色和上面的简单工厂模式一样:

package com.itzw.factory2;

public abstract class Weapon {
    public abstract void attack();
}
package com.itzw.factory2;

public class Gun extends Weapon {

    @Override
    public void attack() {
        System.out.println("机枪正在发射...");
    }
}
package com.itzw.factory2;

public class Plane extends Weapon {
    @Override
    public void attack() {
        System.out.println("飞机正在扔小男孩...");
    }
}

抽象工厂角色:创建一个方法能让它返回一个抽象产品角色

package com.itzw.factory2;

public interface WeaponFactory {
    Weapon get();
}

具体工厂角色:

package com.itzw.factory2;

public class GunFactory implements WeaponFactory{
    @Override
    public Weapon get() {
        return new Gun();
    }
}
package com.itzw.factory2;

public class PlaneFactory implements WeaponFactory{
    @Override
    public Weapon get() {
        return new Plane();
    }
}

测试:

package com.itzw.factory2;

public class Test {
    public static void main(String[] args) {
        WeaponFactory gun = new GunFactory();
        gun.get().attack();
        WeaponFactory plane = new PlaneFactory();
        plane.get().attack();
    }
}

这时我们再想加入新的武器就不需要修改代码了,直接创建一个具体类继承抽象类然后创建具体工厂角色继承抽象工厂即可。

显然这种模式符合OCP开闭原则,但是每次新增一个产品就需要创建两个类,在一定程度上增加了系统的复杂度

6.4 抽象工厂模式(了解)

抽象工厂模式可以看做上面两种模式的结合。

7 Bean的实例化方式

或者可以说是bean的获取方式

Spring为Bean提供了多种实例化方式,通常包括4种方式。(也就是说在Spring中为Bean对象的创建准备了多种方案,目的是:更加灵活)

  • 第一种:通过构造方法实例化
  • 第二种:通过简单工厂模式实例化
  • 第三种:通过factory-bean实例化
  • 第四种:通过FactoryBean接口实例化

7.1 通过构造方法实例化

之前我们学习的就是通过构造方法实例化:

package com.itzw.constructor;

public class User {
    public User(){
        System.out.println("User的无参构造执行了");
    }
}

spring配置文件:

<bean id="user" class="com.itzw.constructor.User"/>

测试:

    @Test
    public void testConstructor(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Object user = applicationContext.getBean("user", User.class);
        System.out.println(user);
    }

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

bean:

package com.itzw.bean;

public class Vip {
    public Vip(){
        System.out.println("vip的无参构造执行了");
    }
}

编写简单工厂模式中的工厂类:这里的方法是静态方法

package com.itzw.bean;

public class VipFactory {
    public static Vip get(){
        return new Vip();
    }
}

spring.xml:

我们需要在这指定调用哪个类的哪个方法获取Bean。factory-method属性指定的是工厂类当中的静态方法,也就是告诉spring框架调用这个方法可以获取bean

    <!--通过简单工厂模式实例化-->
    <bean class="com.itzw.bean.VipFactory" id="vip" factory-method="get"/>

7.3 通过factory-bean实例化

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

和上个方法的区别就是spring配置文件的配置不同,把一个bean标签分解成两个,并且具体工厂类的方法是实例方法如下:

package com.itzw.bean;

public class Order {
    public Order(){
        System.out.println("Order的无参构造执行了");
    }
}

注意这里的方法是实例方法

package com.itzw.bean;

public class OrderFactory {
    public Order get(){
        return new Order();
    }
}

spring:factory-bean告诉框架调用哪个对象,factory-method告诉框架调用哪个方法。

    <!--通过工厂方法模式-->
    <bean class="com.itzw.bean.OrderFactory" id="orderFactory"/>
    <bean factory-bean="orderFactory" factory-method="get" id="order" />

7.4 通过FactoryBean接口实例化

以上三种方法,需要我们自定义factory-bean或者factory-method。在我们编写类实现FactoryBean接口后这俩属性就不需要指定了,factory-bean会自动指向实现FactoryBean接口的类,factory-method会自动指向getObject方法

package com.itzw.bean;

public class Animal {
    public Animal(){
        System.out.println("Animal构造方法执行了");
    }
}

具体工厂类实现FactoryBean接口

package com.itzw.bean;

import org.springframework.beans.factory.FactoryBean;

public class AnimalFactory implements FactoryBean<Animal> {

    @Override
    public Animal getObject() throws Exception {
        return new Animal();
    }

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

    @Override
    public boolean isSingleton() {
        return FactoryBean.super.isSingleton();
    }
}
    <!--通过FactoryBean接口-->
    <bean class="com.itzw.bean.Animal" id="animal"/>

7.5 BeanFactory和FactoryBean的区别

7.5.1 BeanFactory

Spring IoC容器的顶级对象,BeanFactory被翻译为“Bean工厂”,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象。

BeanFactory是工厂。

7.5.2 FactoryBean

FactoryBean:它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean。

在Spring中,Bean可以分为两类:

  • 第一类:普通Bean
  • 第二类:工厂Bean(记住:工厂Bean也是一种Bean,只不过这种Bean比较特殊,它可以辅助Spring实例化其它Bean对象。)

7.6 注入自定义Date

我们在前面就讲到过Date类型是简单类型可以直接使用value属性赋值,但是对格式要求非常严格,只能是这样类型的:Mon Oct 10 14:30:26 CST 2022,而这种类型不符合我们常见的格式。

package com.itzw.bean;

import java.util.Date;

public class Student {
    private Date date;

    public void setDate(Date date) {
        this.date = date;
    }

    @Override
    public String toString() {
        return "Student{" +
                "date=" + date +
                '}';
    }
}
    <!--Date类型的注入-->
    <bean class="com.itzw.bean.Student" id="student">
        <property name="date" value="Mon Oct 10 14:30:26 CST 2022"/>
    </bean>

以上就是我们对Date类型当做简单类型的注入。

我们可以把Date当做非简单类型注入:

还是那个Student类,我们创建Date工厂实现FactoryBean,并且定义属性和构造方法接收spring配置文件传来的日期,我们使用SimpleDateFormat定义日期格式接收传来的日期(比如2020-8-8),然后会返回一个日期

package com.itzw.bean;

import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.factory.FactoryBean;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateFactoryBean implements FactoryBean<Date> {

    //定义属性接收日期
    private String date;

    //通过构造方法给日期赋值
    public DateFactoryBean(String date) {
        this.date = date;
    }

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

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

传给date工厂一个日期,会返回一个Date默认格式的日期,然后我们再通过Student的Bean将这个日期传给Student。

    <!--Date类型的注入(非简单类型)-->
    <bean id="dateFactoryBean" class="com.itzw.bean.DateFactoryBean">
        <constructor-arg name="date" value="2000-9-25"/>
    </bean>
    <bean class="com.itzw.bean.Student" id="student2">
        <property name="date" ref="dateFactoryBean"/>
    </bean>

8 Bean的声明周期

8.1 什么是Bean的生命周期

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

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

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

8.3 Bean的声明周期之五步

Bean生命周期可以粗略的划分为五大步:

  • 第一步:实例化Bean
  • 第二步:Bean属性赋值
  • 第三步:初始化Bean
  • 第四步:使用Bean
  • 第五步:销毁Bean

这五步的位置如下:

注意:我们需要自己写初始化和销毁方法,并且最后我们要手动关闭销毁方法。我们还要在spring配置文件中指定初始化方法和销毁方法。

package com.itzw.spring6.lifecycle;

public class User {
    private String name;

    public User(){
        System.out.println("1.实例化bean");
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("2.给bean属性赋值");
    }

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

    public void initBean(){
        System.out.println("3.初始化bean");
    }

    public void destroyBean(){
        System.out.println("4.销毁bean");
    }
}
    <!--我们自己创建的初始化bean和销毁bean方法都需要在这里指定,因为是我们自己创建的不可能自动识别-->
    <bean id="user" class="com.itzw.spring6.lifecycle.User"
          init-method="initBean" destroy-method="destroyBean">
        <property name="name" value="张麻子"/>
    </bean>
    @Test
    public void testLifeCycle(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("user", User.class);
        //System.out.println(user);
        System.out.println("4.使用bean");
        //我们需要手动关闭spring容器才能执行销毁方法,我们还需要将applicationContext强转为ClassPathXmlApplicationContext
        ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
        context.close();
    }

8.4 Bean生命周期之7步

在以上的5步的基础上,在第3步初始化Bean前后可以各加一步,可以加入我们想加入的代码,这一共就是七步了,可以加入“Bean后处理器”。

编写一个类实现BeanPostProcessor类,并且重写before和after方法:

package com.itzw.spring6.lifecycle;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class LogBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean后处理器的before方法执行,即将开始初始化");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean后处理器的before方法执行,已经完成初始化");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}

我们需要在spring配置文件中配置bean后处理器,相当于引入这个类。

    <!--配置Bean后处理器。这个后处理器将作用于当前配置文件中所有的bean。-->
    <bean class="com.itzw.spring6.lifecycle.LogBeanPostProcessor"/>

8.5 Bean生命周期之10步

在上面七步的基础上,在Bean后处理器before执行之前检查bean是否实现Aware的相关接口,并设置相关依赖,在Bean后处理器before执行之后检查bean是否实现了InitialzingBean接口,并调用接口方法,在销毁bean之前检查bean是否实现了DisposableBean接口,并调用接口方法。

以上三个检查接口并执行相关方法或依赖一共是三步,加上前面的七步一共是十步。

那这些接口是什么意思呢?

Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware

当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean

当Bean实现了BeanClassLoaderAware,Spring会加载该Bean的类加载器传递给bean

当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给bean 

我们实现这些接口感受一下:

package com.itzw.spring6.lifecycle;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;

public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean,DisposableBean {
    private String name;

    public User(){
        System.out.println("1.实例化bean");
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("2.给bean属性赋值");
    }

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

    public void initBean(){
        System.out.println("4.初始化bean");
    }

    public void destroyBean(){
        System.out.println("7.销毁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接口在后处理器的before方法执行之后执行了");
    }


    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean接口在销毁bean前执行了");
    }
}

对于生命周期我们掌握七种就可以

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

Spring根据bean的作用域来选择管理方式

  • 对于singleton作用域的bean,Spring能够精确的知道该bean何时被创建,以及何时被销毁
  • 而对于prototype作用域的bean,spring只负责创建

我们把之前User类的spring.xml文件中的片配置scope设置为prototype:

只执行了前八步

9 Bean的循环依赖问题

9.1 什么是Bean的循环依赖

A对象有B属性,B对象有A属性。比如Husband对象有Wife属性,Wife对象有Husband属性,如下:

package com.itzw.spring6.bean;

public class Husband {
    private String name;
    private Wife wife;
}
package com.itzw.spring6.bean;

public class Wife {
    private String name;
    private Husband husband;
}

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

package com.itzw.spring6.bean;

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

    public String getName() {
        return name;
    }

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

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

    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}
package com.itzw.spring6.bean;

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

    public String getName() {
        return name;
    }

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

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

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

注意这里的toString方法里面不能直接输出husband和wife对象,因为这样就递归了,我们指定输出对应的get方法即可

    <bean class="com.itzw.spring6.bean.Husband" id="husband" scope="singleton">
        <property name="name" value="张麻子"/>
        <property name="wife" ref="wife"/>
    </bean>

    <bean class="com.itzw.spring6.bean.Wife" id="wife" scope="singleton">
        <property name="name" value="马邦德"/>
        <property name="husband" ref="husband"/>
    </bean>
    @Test
    public void testDC(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Husband husband = applicationContext.getBean("husband", Husband.class);
        System.out.println(husband);

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

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

我们简单分析一下原理:因为使用的是singleton是单例模式,对象只会实例化一次,所以在spring容器加载的时候实例化bean,只要进行实例化后就会立刻“曝光”【不等属性值赋值就曝光了】。比如在给Husband对象赋值的时候需要得到WIfe对象才能完成赋值,而这时Wife对象已经实例化已经曝光可以得到。同样的Wife对象在赋值的时候需要Husband对象,此时Husband对象也已经实例化结束已经曝光,所以不会出现问题。

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

scope改成prototype

这样会出错:

提示我们:请求的 bean 目前正在创建中:是否有无法解析的循环引用?

那为什么会出错呢?因为使用的是prototype。在我们给Husband赋值的时候需要注入WifeBean,这就需要new一个新的Wife对象给Husband,而new出的Wife对象需要Husband对象才行,这时又会new一个新的Husband对象...会成死循环。

但如果有一个Bean的scope是singleton就不会出错了。比如WIfe是singleton

当我们给Husband赋值的时候,需要Wife对象,因为Wife对象是单例的只有一个固定的,会曝光,Husband对象就能得到了。当我们给Wife对象赋值的时候,需要Husband,Husband会new一个新的对象,此时Husband需要Wife对象,因为WIfe对象是单例的,直接就可以得到了。

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

package com.itzw.spring6.bean2;

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

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + wife.getName() +
                '}';
    }
}
package com.itzw.spring6.bean2;

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

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + husband.getName() +
                '}';
    }
}
    <bean id="husband" class="com.itzw.spring6.bean2.Husband" scope="singleton">
        <constructor-arg name="name" value="张麻子"/>
        <constructor-arg ref="wife"/>
    </bean>
    <bean id="wife" class="com.itzw.spring6.bean2.Wife" scope="singleton">
        <constructor-arg name="name" value="马邦德"/>
        <constructor-arg name="husband" ref="husband"/>
    </bean>

测试结果是失败的,错误和上面的错一样,为什么呢?

因为使用构造注入,这要求我们在实例化bean的时候就要给属性值赋值,但是经过上面那些分析我们就应该已经明白了,我们是做不到实例化就立刻给属性赋值的,这会造成死循环。

小小总结一下:经过上面测试我们发现只有在使用set方法注入并且scope的值为singleton的时候才能避免循环依赖。

根本原因在于,这种方式可以做到“实例化Bean”和“给Bean属性赋值”这两个动作分开完成。实例化bean的时候调用无参构造来完成,此时可以先不给属性赋值,可以提前将该bean“曝光”给外界。

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

10 回顾反射机制

10.1 分析方法四要素

我们随便创建一个类:

package com.itzw.reflect;

public class Student {
    private String name;
    private int age;

    public void doSome(){
        System.out.println("doSome无参方法执行");
    }

    public String doSome(String name){
        System.out.println("doSome返回姓名的方法执行");
        return name;
    }
}

我们调用这个类的方法:

package com.itzw.reflect;

public class Test {
    public static void main(String[] args) {
        Student student = new Student();
        student.doSome();
        String doSome = student.doSome("王德发");
        System.out.println(doSome);
    }
}

我们发现调用一个类的方法需要四要素:类的对象;方法名;方法参数;返回值

10.2 使用反射机制调用方法

还是那个类,不过我们使用反射机制的方式调用方法

package com.itzw.reflect;

import java.lang.reflect.Method;

public class Test2 {
    public static void main(String[] args) throws Exception {
        //获取类
        Class<?> clazz = Class.forName("com.itzw.reflect.Student");
        //获取方法
        Method doSome = clazz.getDeclaredMethod("doSome", String.class);
        //获取类的对象
        Object obj = clazz.newInstance();
        //调用方法
        Object retValue = doSome.invoke(obj, "张麻子");
        //输出返回值
        System.out.println(retValue);
    }
}

10.3 假设知道属性名

package com.itzw.reflect;

public class User {
    private String name;
    private int age;

    public User(){}

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

假设有如上的一个类,你知道的信息是:

  • 类名是:com.itzw.reflect.User
  • 该类中有String类型的name属性和int类型的age属性
  • 另外你知道该类的设计符合javabean规范

知道以上信息如何给属性赋值呢?

我们这里给age属性赋值:

package com.itzw.reflect;

import java.lang.reflect.Method;

public class Test3 {
    public static void main(String[] args) throws Exception{
        String className = "com.itzw.reflect.User";
        String propertyName = "age";
        String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
        //获取类
        Class<?> clazz = Class.forName(className);
        //获取方法
        Method setMethod = clazz.getDeclaredMethod(setMethodName,int.class);
        //获取对象
        Object obj = clazz.newInstance();
        //调用方法
        setMethod.invoke(obj,12);
        //输出类
        System.out.println(obj);
    }
}

之所以回顾反射机制是为了下面手写spring框架

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐