Bootstrap

Spring笔记(一)(黑马)(Ioc基础容器)

01、传统Javaweb开发的困惑

1.1 传统Javaweb开发困惑及解决方案

image.png😖问题一:层与层之间紧密耦合在了一起,接口与具体实现紧密耦合在了一起
解决思路:程序代码中不要手动new对象,第三方根据要求为程序提供需要的Bean对象。
image.png

image.png
😖问题二:通用的事务功能耦合在业务代码中,通用的日志功能耦合在业务代码中
解决思路:程序代码中不要手动new对象,第三方根据要求为程序提供需要的Bean对象的代理对象。
image.png

02、IoC、DI和AOP思想提出

2.1 三种思想的提出

image.png

  • IoC思想:

Inversion of Control,控制反转,强调的是原来在程序中创建Bean的权利反转给第三方。

  • DI思想:

Dependency Injection,依赖注入,强调的Bean之间的关系,这种由第三方负责去设置。

  • AOP思想:

Aspect Oriented Peogramming,面向切面编程,功能的横向抽取,主要的实现方式就是Proxy

2.2 框架概念的出现

框架的基本特点:

  • 框架(Framework),是基于基础技术之上,从众多业务中抽取出的通用解决方案;
  • 框架是一个半成品,使用框架规定的语法可以提高开发效率,可以用简单地代码就能完成复杂的基础业务;
  • 框架内部使用大量的设计模式、算法、底层代码操作技术,如反射、内省、xml解析、注解解析等;
  • 框架一般都具备扩展性;
  • 有了框架,我们可以将精力尽可能的投入在纯业务开发上而不用去费心技术实现以及一些辅助业务。

Java中常用的框架:
不同语言,不同领域都有属于自己的框架,使用框架开发是作为程序员的最基础的底线。Java语言中的框架,可以分为基础框架和服务框架:

  • 基础框架:完成基本业务操作的框架,如MyBatis、Spring、SpringMVC、Struts2、Hibernate等
  • 服务框架:特定领域的框架,一般还可以对外提供服务框架,如MQ、ES、Nacos等

03、Spring框架的诞生

3.1 Spring框架概述

  • Spring 框架概述

Spring是一个开源的轻量级Java开发应用框架,可以简化企业级应用开发。Spring解决了开发者在JavaEE开发 中遇到的许多常见的问题,提供了功能强大IOC、AOP及Web MVC等功能。是当前企业中Java开发几乎不能 缺少的框架之一。Spring的生态及其完善,不管是Spring哪个领域的解决方案都是依附于在Spring Framework基础框架的

  • Spring 框架概述

Spring的官网:www.spring.io

3.2 Spring框架的历史

  • Spring 框架的历史
    • Jsp 默默扛下所有;
    • MVC+三层架构分工明确,但开发成本及其高;
    • EJB 重量级框架出现,走出一个困境,有进入另一个困境;
    • Spring 春天来到,随之,SSH风生水起、称霸武林;
    • Spring 稳住江湖大哥位置,SSM开始上位;
    • Spring 本着“拿来主义”的思维快速发展,生态不断健全;
    • SpringBoot 又一里程碑崛起,把“约定大于配置“思想玩儿的炉火纯青;
    • SpringCloud 打包了微服务众多解决方案,应对互联网项目更加easy!

3.3 Spring Framework技术栈图示

image.png

3.4 BeanFactory快速入门

根据下图,分析一下Spring的BeanFactory的开发步骤:
image.png1)导入Spring的jar包或Maven坐标;

<!--Spring核心-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.7</version>
</dependency>

内部jar包:
image.png

2)定义UserService接口及其UserServiceImpl实现类;

public interface UserService {}
public class UserServiceImpl implements UserService{}

3)创建beans.xml配置文件,将UserServiceImpl的信息配置到该xml中;

<!--
    bean: 将指定类配置给Spring 让Spring创建其对象的实例
        id: 唯一标识符
        class: 实现类的全限定名
-->
<bean id="userService" class="com.mem.service.impl.UserServiceImpl"></bean>

4)编写测试代码,创建BeanFactory,加载配置文件,获取UserService实例对象。

public class BeanFactoryTest {
    public static void main(String[] args) {
        //创建BeanFactory
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        //创建读取器
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        //加载配置文件
        reader.loadBeanDefinitions("beans.xml");
        //获取Bean实例对象
        UserService userService = (UserService) beanFactory.getBean("userService");
        System.out.println(userService); 
    }
}
// com.mem.service.impl.UserServiceImpl@357246de

上面使用BeanFactory完成了IoC思想的实现,下面去实现以下DI依赖注入:

1)定义UserDao接口及其UserDaoImpl实现类;

public interface UserDao {}
public class UserDaoImpl implements UserDao {}

2)修改UserServiceImpl代码,添加一个setUserDao(UserDao userDao)用于接收注入的对象;

public class UserServiceImpl implements UserService {
    private UserDao userDao;
    // BeanFactory去调用该方法,从容器中获得userDao设置到此处
    public void setUserDao(UserDao userDao){
        System.out.println("setUserDao() 执行了");
        this.userDao = userDao;
    }
}

3)修改beans.xml配置文件,在UserDaoImpl的中嵌入配置注入;

<bean id="userService" class="com.mem.service.impl.UserServiceImpl">
    <!--注入userDao对象-->
    <!--
        property: 用于调用Bean实例中的setter方法,完成属性赋值,从而完成依赖注入
            name: 类里面属性名称
            ref: 创建userDao对象bean标签id值
    -->
    <property name="userDao" ref="userDao"></property>
</bean>
<!--配置 UserDaoImpl-->
<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl"></bean>

4)修改测试代码,获得UserService时,setUserService方法执行了注入操作。

public class Test01_BeanFactory {
    public static void main(String[] args) {
        //创建BeanFactory
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        //创建读取器
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        //加载配置文件
        reader.loadBeanDefinitions("beans.xml");
        //获取Bean实例对象
        UserService userService = (UserService) beanFactory.getBean("userService");
        System.out.println(userService);
    }
}
// 输出结果
// setUserDao() 执行了
// com.mem.service.impl.UserServiceImpl@783e6358

3.5 ApplicationContext快速入门

ApplicationContext 称为Spring容器,内部封装了BeanFactory,比BeanFactory功能更丰富更强大,使用 ApplicationContext 进行开发时,xml配置文件的名称习惯写成applicationContext.xml

public class Test02_ApplicationContext {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) applicationContext.getBean("userService");
        System.out.println(userService);
        /**
         * setUserDao() 执行了
         * com.mem.service.impl.UserServiceImpl@4e9ba398
         */
    }
}

3.6 BeanFactory与ApplicationContext的关系

BeanFactory与ApplicationContext的关系

  1. BeanFactory是Spring的早期接口,称为Spring的Bean工厂,ApplicationContext是后期更高级接口,称之为Spring容器
  2. ApplicationContext在BeanFactory基础上对功能进行了扩展,例如:监听功能、国际化功能等。BeanFactory的API更偏向底层,ApplicationContext的API大多数是对这些底层API的封装;

ApplicationContext除了继承了BeanFactory外,还继承了ApplicationEventPublisher(事件发布器)、 ResouresPatternResolver(资源解析器)、MessageSource(消息资源)等。但是ApplicationContext的核心功 能还是BeanFactory。
image.png
image.png

  1. Bean创建的主要逻辑和功能都被封装在BeanFactory中,ApplicationContext不仅继承了BeanFactory,而且ApplicationContext内部还维护着BeanFactory的引用,所以ApplicationContext与BeanFactory既有继承关系,又有组合关系。

applicationContext内部维护着beanFactory的引用,在学习过程中会查看beanFactory内部维护的属性,断点查看如下图示内容的
image.png

  1. Bean的初始化时机不同,原始BeanFactory是在首次调用getBean时才进行Bean的创建,而ApplicationContext则是配置文件加载,容器一创建就将Bean都实例化并初始化好。

验证BeanFactory和ApplicationContext对Bean的初始化时机,在UserDaoImpl的无参构造内打印一句话,验证 构造方法的执行时机
image.png
断点观察,
BeanFactory方式时,当调用getBean方法时才会把需要的Bean实例创建,即延迟加载;而 ApplicationContext是加载配置文件,容器创建时就将所有的Bean实例都创建好了,存储到一个单例池中,当调用getBean时直接从单例池中获取Bean实例返回

3.7 BeanFactory的继承体系

BeanFactory是核心接口,项目运行过程中肯定有具体实现参与,这个具体实现就是DefaultListableBeanFactory ,而ApplicationContext内部维护的Beanfactory的实现类也是它
image.png

3.8 ApplicationContext的继承体系

  • 只在Spring基础环境下,即只导入spring-context坐标时,此时ApplicationContext的继承体系

image.png

只在Spring基础环境下,常用的三个ApplicationContext作用如下:

实现类功能描述
ClassPathXmlApplicationContext加载类路径下的xml配置的ApplicationContext
FileSystemXmlApplicationContext加载磁盘路径下的xml配置的ApplicationContext
AnnotationConfigApplicationContext加载注解配置类的ApplicationContext
  • 如果Spring基础环境中加入了其他组件解决方案,如web层解决方案,即导入spring-web坐标,此时 ApplicationContext的继承体系
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.3.7</version>
</dependency>

image.png

新增了两个ApplicationContext

在Spring的web环境下,常用的两个ApplicationContext作用如下:

实现类功能描述
XmlWebApplicationContextweb环境下,加载类路径下的xml配置的ApplicationContext
AnnotationConfigWebApplicationContextweb环境下,加载磁盘路径下的xml配置的ApplicationContext

PS:web环境下的这两个ApplicationContext,在学习Spring集成web时在进行讲解

04、基于xml的Spring应用

4.1 Spring Bean的配置详解

Spring开发中主要是对Bean的配置,Bean的常用配置一览如下:

Xml配置方式功能描述
Bean的id和全限定名配置
通过name设置Bean的别名,通过别名也能直接获取到Bean实例
Bean的作用范围,BeanFactory作为容器时取值singleton和prototype
Bean的实例化时机,是否延迟加载。BeanFactory作为容器时无效
Bean实例化后自动执行的初始化方法,method指定方法名
Bean实例销毁前的方法,method指定方法名
设置自动注入模式,常用的有按照类型byType,按照名字byName
指定哪个工厂Bean的哪个方法完成Bean的创建

1)Bean的基础配置

例如:配置UserDaoImpl由Spring容器负责管理

<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl"/>

此时存储到Spring容器(beanFactory中的singleObjects单例池)(ConcurrentHashMap)中的Bean的beanName是userDao,值是UserDaoImpl对象,可以根据beanName获取Bean实例
image.png

applicationContext.getBean("userDao");

如果不配置id,则Spring会把当前Bean实例的全限定名作为beanName ,如下图所示
image.png

applicationContext.getBean("com.mem.dao.impl.UserDaoImpl");

2)Bean的别名配置

可以为当前Bean指定多个别名,根据别名也可以获得Bean对象

<bean id="userDao" name="aaa,bbb" class="com.mem.dao.impl.UserDaoImpl"/>

此时多个名称都可以获得UserDaoImpl实例对象 ,singleObjects中还是仍然是userDao,别名存储在beanFactory的aliasMap
image.png

applicationContext.getBean("userDao");
applicationContext.getBean("aaa");
applicationContext.getBean("bbb");

如果不配置id,则Spring会把当前name属性的第一个别名(aaa)作为beanName,如下图所示
image.png

3)Bean的范围配置

默认情况下,单纯的Spring环境Bean的作用范围有两个:Singleton和Prototype

  • singleton:单例,默认值,Spring容器创建的时候,就会进行Bean的实例化,并存储到容器内部的单例池中 ,每次getBean时都是从单例池中获取相同的Bean实例;
  • prototype:原型,Spring容器初始化时不会创建Bean实例,当调用getBean时才会实例化Bean,每次 getBean都会创建一个新的Bean实例。

当scope设置为singleton时,获得两次对象打印结果是一样的

<bean scope="singleton" id="userDao" class="com.mem.dao.impl.UserDaoImpl"></bean>
public class Test03_XmlBeanAttribute03 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        UserDao userDao2 = (UserDao) applicationContext.getBean("userDao");
        System.out.println(userDao); // com.mem.dao.impl.UserDaoImpl@1ed4004b
        System.out.println(userDao2); // com.mem.dao.impl.UserDaoImpl@1ed4004b
    }
}

通过断点调试,观察可以发现单例池中存在 userDao 实例
image.png

当scope设置为prototype时,获得两次对象打印结果是不一样的

<bean scope="prototype" id="userDao" class="com.mem.dao.impl.UserDaoImpl"/>
public class Test03_XmlBeanAttribute03 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        UserDao userDao2 = (UserDao) applicationContext.getBean("userDao");
        System.out.println(userDao); // com.mem.dao.impl.UserDaoImpl@564fabc8
        System.out.println(userDao2); // com.mem.dao.impl.UserDaoImpl@16d04d3d
    }
}

通过断点调试,观察可以发现单例池中不存在 userDao 实例,但是 userDao的信息已经被存储到 beanFactory.beanDefinitionMap中了
image.png

4)Bean的延迟加载

lazy-init设置为true时为延迟加载,也就是当Spring容器创建的时候,不会立即创建Bean实例,等待用到时在创 建Bean实例并存储到单例池中去,后续在使用该Bean直接从单例池获取即可,本质上该Bean还是单例的

<bean lazy-init="true" id="userDao" class="com.mem.dao.impl.UserDaoImpl"/>

image.png

image.png

5)Bean的初始化和销毁方法配置

Bean在被实例化后,可以执行指定的初始化方法完成一些初始化的操作,Bean在销毁之前也可以执行指定的销毁 方法完成一些操作,初始化方法名称和销毁方法名称通过

<bean init-method="init" destroy-method="destroy" id="userDao" class="com.mem.dao.impl.UserDaoImpl"/>
public class UserDaoImpl implements UserDao {
    public UserDaoImpl(){
        System.out.println("userDaoImpl 创建了");
    }
    public void init(){
        System.out.println("初始化方法。。。");
    }
    public void destroy(){
        System.out.println("销毁方法。。。");
    }
}
public class Test03_XmlBeanAttribute05 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        System.out.println(userDao);
        applicationContext.close(); // 调用close时,才有机会执行销毁方法
        /**
         * userDaoImpl 创建了
         * 初始化方法。。。
         * com.mem.dao.impl.UserDaoImpl@5702b3b1
         * 销毁方法。。。
         */
    }
}

扩展:除此之外,我们还可以通过实现 InitializingBean 接口,完成一些Bean的初始化操作,如下:

public class UserDaoImpl implements UserDao, InitializingBean {
    public UserDaoImpl(){
        System.out.println("userDaoImpl 创建了");
    }
    public void init(){
        System.out.println("初始化方法。。。");
    }
    public void destroy(){
        System.out.println("销毁方法。。。");
    }
    // 执行时机早于init() 方法
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean。。。");
    }
}
public class Test03_XmlBeanAttribute05 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        System.out.println(userDao);
        applicationContext.close(); // 调用close时,才有机会执行销毁方法
        /**
         * userDaoImpl 创建了
         * InitializingBean。。。
         * 初始化方法。。。
         * com.mem.dao.impl.UserDaoImpl@69ea3742
         * 销毁方法。。。
         */
    }
}

6)Bean的实例化配置

Spring的实例化方式主要如下两种:

  • 构造方式实例化:底层通过构造方法对Bean进行实例化
  • 工厂方式实例化:底层通过调用自定义的工厂方法对Bean进行实例化
构造方式实例化

构造方式实例化Bean又分为无参构造方法实例化和有参构造方法实例化,Spring中配置的几乎都是无参构造该方式。

同时配置了有参构造和无参构造,默认找的是无参构造,若注释了无参构造,会报错。

<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl"></bean>
public class UserDaoImpl implements UserDao {
    public UserDaoImpl(){
        System.out.println("UserDaoImpl() 执行了");
    }
    public UserDaoImpl(String name){
        System.out.println("UserDaoImpl(String name) 执行了,name="+name);
    }
}
public class Test03_XmlBeanAttribute06 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        System.out.println(userDao);
    }
}
// 无参没有注释时
UserDaoImpl() 执行了
com.mem.dao.impl.UserDaoImpl@1ed4004b
// 无参被注释时
Failed to instantiate [com.mem.dao.impl.UserDaoImpl]: No default constructor found;

所以需要显示的设置有参构造方法,下面讲解有参构造方法实例化Bean
为xml文件添加constructor-arg标签

<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl">
    <constructor-arg name="name" value="mem"/>
</bean>

运行结果

public class Test03_XmlBeanAttribute06 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        System.out.println(userDao);
    }
}
// 运行结果
UserDaoImpl(String name) 执行了,name=mem
com.mem.dao.impl.UserDaoImpl@369f73a2
工厂方式实例化

工厂方式实例化Bean,又分为如下三种:

  • 静态工厂方法实例化Bean
  • 实例工厂方法实例化Bean
  • 实现FactoryBean规范延迟实例化Bean
①静态工厂方法

静态工厂方法实例化Bean,其实就是定义一个工厂类,提供一个静态方法用于生产Bean实例,在将该工厂类及其 静态方法配置给Spring即可

<!-- 配置实例工厂Bean的哪个方法作为工厂方法 -->
<bean id="userDao" class="com.mem.factory.UserDaoFactoryBean" factory-method="getUserDaoByStatic">
    <constructor-arg name="name" value="mem"></constructor-arg>
</bean>

PS:标签不仅仅是为构造方法传递参数,只要是为了实例化对象而传递的参数都可以通过 标签完成,例如上面通过静态工厂方法实例化Bean所传递的参数也是要通过进行传递的

public class UserDaoFactoryBean {
    // 无参静态工厂方法
    public static UserDao getUserDaoByStatic(){
        // 自定义处理逻辑
        return new UserDaoImpl();
    }
    // 有参静态工厂方法
    public static UserDao getUserDaoByStatic(String name){
        // 自定义处理逻辑
        System.out.println("静态工厂方法, name:" + name);
        return new UserDaoImpl(name);
    }
}

测试代码,直接通过ApplicationContext获得userDao即可

public class Test03_XmlBeanAttribute06 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        System.out.println(userDao);
    }
}
// 测试有参
静态工厂方法, name:mem
UserDaoImpl(String name) 执行了,name=mem
com.mem.dao.impl.UserDaoImpl@1d16f93d

断点调试:
发现单例池中没有UserDaoFactoryBean对象,有userDao对象
image.png

使用静态工厂方法的优点:

  • 在bean创建前后,可以添加自定义的逻辑代码
  • 如果想要引用第三方的工具类,并交给Spring管理时,可以使用这种方式
②实例工厂方法

实例工厂方法,也就是非静态工厂方法产生Bean实例,与静态工厂方式比较,该方式需要先有工厂对象,在用工厂 对象去调用非静态方法,所以在进行配置时,要先配置工厂Bean,在配置目标Bean

<!-- 配置实例工厂Bean的哪个方法作为工厂方法 -->
<bean id="userDao" factory-bean="userDaoFactoryBean" factory-method="getUserDao">
    <constructor-arg name="name" value="mem"></constructor-arg>
</bean>
<!-- 配置实例工厂Bean -->
<bean id="userDaoFactoryBean" class="com.mem.factory.UserDaoFactoryBean"></bean>
public class UserDaoFactoryBean {

    // 无参非静态工厂方法
    public UserDao getUserDao(){
        // 自定义处理逻辑
        return new UserDaoImpl();
    }
    // 有参非静态工厂方法
    public UserDao getUserDao(String name){
        // 自定义处理逻辑
        System.out.println("非静态工厂方法, name:" + name);
        return new UserDaoImpl(name);
    }
}
// 测试有参
非静态工厂方法, name:mem
UserDaoImpl(String name) 执行了,name=mem
com.mem.dao.impl.UserDaoImpl@65e2dbf3
public class Test03_XmlBeanAttribute06 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        System.out.println(userDao);
    }
}

断点调试:
发现单例池中不仅有userDao对象,而且还有UserDaoFactoryBean对象
image.png

使用实例工厂方法的优点:

  • 在bean创建前后,可以添加自定义的逻辑代码
  • 如果想要引用第三方的工具类,并交给Spring管理时,可以使用这种方式
③实现FactoryBean

上面不管是静态工厂方式还是非静态工厂方式,都是自定义的工厂方法,Spring提供了FactoryBean的接口规范, FactoryBean接口定义如下:

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

PS: @Nullable 注解 TODO

首先: 定义工厂实现FactoryBean

public class UserDaoFactoryBean2 implements FactoryBean<UserDao> {

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

    @Override
    public Class<?> getObjectType() {
        return UserDao.class;
    }
}

其次: 配置FactoryBean交由Spring管理即可

<!--
    通过实现FactoryBean的方式创建Bean
-->
<bean id="userDao2" class="com.mem.factory.UserDaoFactoryBean2"></bean>

最后: 通过Spring容器根据beanName可以正常获得UserDaoImpl

public class Test03_XmlBeanAttribute06 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao2");
        System.out.println(userDao);
    }
}
// 打印结果
UserDaoImpl() 执行了
com.mem.dao.impl.UserDaoImpl@4bec1f0c

断点调试:发现单例池中的对象并不是UserDaoImpl了,而是UserDaoFactoryBean2
image.png

用第三种方式实现,具有延迟功能;
通过断点观察发现Spring容器创建时,FactoryBean被实例化了,并存储到了单例池singletonObjects中,但是 getObject()方法尚未被执行,UserDaoImpl也没被实例化,当首次用到UserDaoImpl时,才调用getObject(), 此工厂方式产生的Bean实例不会存储到单例池singletonObjects中,会存储到 factoryBeanObjectCache 缓存池 中,并且后期每次使用到userDao都从该缓存池中返回的是同一个userDao实例
image.png

第一次调用时,写入缓存,第二次调用时,直接去缓存中查找。
好处:可以临时把需要产生某些bean的需求存起来,等什么时候用到,再产生。

7)Bean的依赖注入配置

Bean的依赖注入有两种方式:
image.png

其中,ref 是 reference 的缩写形式,翻译为:涉及,参考的意思,用于引用其他Bean的id。value 用于注入普通 属性值

依赖注入的数据类型有如下三种:

  • 普通数据类型,例如:String、int、boolean等,通过value属性指定。
  • 引用数据类型,例如:UserDaoImpl、DataSource等,通过ref属性指定。
  • 集合数据类型,例如:List、Map、Properties等

普通数据类型、引用数据类型略

集合数据类型如下所示
①List:

注入 List 集合 – 普通数据
image.png
注入 List 集合 – 引用数据
image.png
注入 List 集合 – 引用数据 也可以直接引用容器中存在的Bean

也可以直接引用容器中存在的Bean

image.png

②注入 Set 集合

image.png

③注入 Map<k,v> 集合

image.png
④注入 Properties 键值对
image.png

扩展:自动装配方式
如果被注入的属性类型是Bean引用的话,那么可以在 标签中使用 autowire 属性去配置自动注入方式,属 性值有两个:

  • byName:通过属性名自动装配,即去匹配 setXxx 与 id=“xxx”(name=“xxx”)是否一致
  • byType:通过Bean的类型从容器中匹配,匹配出多个相同Bean类型时,报错。

image.png

通过类型装配,如果有多个userDao的bean,则会报错:No qualifying bean of type ‘com.mem.dao.UserDao’ available: expected single matching bean but found 2: userDao,userDao2

8)Spring的其他配置标签

Spring 的 xml 标签大体上分为两类,一种是默认标签,一种是自定义标签

  • 默认标签:就是不用额外导入其他命名空间约束的标签,例如 标签
  • 自定义标签:就是需要额外引入其他命名空间约束,并通过前缀引用的标签,例如 context:property-placeholder/ 标签
默认标签

Spring的默认标签用到的是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">
</beans>

该命名空间约束下的默认标签如下:
image.png
标签,除了经常用的做为根标签外,还可以嵌套在根标签内,使用profile属性切换开发环境
image.png
可以使用以下两种方式指定被激活的环境:

  • 使用命令行动态参数,虚拟机参数位置加载 -Dspring.profiles.active=test
  • 使用代码的方式设置环境变量 System.setProperty("spring.profiles.active","test")

标签,用于导入其他配置文件,项目变大后,就会导致一个配置文件内容过多,可以将一个配置文件根据业务某块进行拆分,拆分后,最终通过标签导入到一个主配置文件中,项目加载主配置文件就连同导入的文件一并加载了
image.png

标签是为某个Bean添加别名,与在标签上使用name属性添加别名的方式一样,我们为 UserServiceImpl指定四个别名:aaa、bbb、xxx、yyy
image.png
断点调试,在beanFactory中维护着一个名为aliasMap的Map<String,String>集合,存储别名和beanName 之间的映射关系
image.png

自定义标签

Spring的自定义标签需要引入外部的命名空间,并为外部的命名空间指定前缀,使用 <前缀:标签> 形式的标签,称之为自定义标签,自定义标签的解析流程也是 Spring xml扩展点方式之一,在《Spring整合其他框架》章节进行 详细介绍
image.png

4.2 Spring的get方法

image.png

//根据beanName获取容器中的Bean实例,需要手动强转
UserService userService = (UserService) applicationContext.getBean("userService");
//根据Bean类型去容器中匹配对应的Bean实例,如存在多个匹配Bean则报错
UserService userService2 = applicationContext.getBean(UserService.class);
//根据beanName获取容器中的Bean实例,指定Bean的Type类型
UserService userService3 = applicationContext.getBean("userService", UserService.class);

4.3 Spring配置非自定义Bean

以上在 xml 中配置的Bean都是自己定义的,例如:UserDaoImpl,UserServiceImpl。但是,在实际开发中有些功能类并不是我们自己定义的,而是使用的第三方jar包中的,那么,这些Bean要想让Spring进行管理,也需要对其进行配置

配置非自定义的Bean需要考虑如下两个问题:

  • 被配置的Bean的实例化方式是什么?无参构造、有参构造、静态工厂方式还是实例工厂方式;
  • 被配置的Bean是否需要注入必要属性

1)配置 Druid 数据源交由Spring管理

导入Druid坐标

<!-- druid数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.17</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>

配置 DruidDataSource
原来的方式
image.png
现在的方式

<!-- 配置数据源信息 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
    <property name="url" value="jdbc:mysql://localhost:3306/mybatis"></property>
    <property name="username" value="root"></property>
    <property name="password" value="root"></property>
</bean>

2)配置Connection交由Spring管理 (静态工厂方法案例)

Connection 的产生是通过DriverManager的静态方法getConnection获取的,所以我们要用静态工厂方式配置
原来的方式
image.png
现在的方式

<!-- 配置Connection -->
<bean id="clazz" class="java.lang.Class" factory-method="forName">
    <constructor-arg name="className" value="com.mysql.jdbc.Driver"></constructor-arg>
</bean>
<bean id="connection" class="java.sql.DriverManager" factory-method="getConnection" scope="prototype">
    <constructor-arg name="url" value="jdbc:mysql://localhost:3306/mybatis"></constructor-arg>
    <constructor-arg name="user" value="root"></constructor-arg>
    <constructor-arg name="password" value="root"></constructor-arg>
</bean>

3)配置日期对象交由Spring管理 (非静态工厂方法案例)

原来的方式
image.png
现在的方式
可以看成是实例工厂方式,使用Spring配置方式产生Date实例

<bean id="simpleDateFormat" class="java.text.SimpleDateFormat">
    <constructor-arg name="pattern" value="yyyy-MM-dd HH:mm:ss"></constructor-arg>
</bean>
<bean id="date" factory-bean="simpleDateFormat" factory-method="parse">
    <constructor-arg name="source" value="2023-08-27 12:00:00"></constructor-arg>
</bean>

4)配置MyBatis的SqlSessionFactory交由Spring管理

导入MyBatis的相关坐标:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>

原来的方式
image.png
现在的方式

<!--静态工厂方式产生Bean实例-->
<bean id="in" class="org.apache.ibatis.io.Resources" factory-method="getResourceAsFile">
    <constructor-arg name="resource" value="mybatis-config.xml"></constructor-arg>
</bean>
<!--无参构造方式产生Bean实例-->
<bean id="builder" class="org.apache.ibatis.session.SqlSessionFactoryBuilder"></bean>
<!--实例工厂方式产生Bean实例-->
<bean id="sqlSessionFactory" factory-bean="builder" factory-method="build">
    <constructor-arg name="inputStream" ref="in"></constructor-arg>
</bean>

4.4 Bean实例化的基本流程

Spring容器在进行初始化时,会将xml配置的的信息封装成一个BeanDefinition对象,所有的 BeanDefinition存储到一个名为beanDefinitionMap的Map集合中去,Spring框架在对该Map进行遍历,使用反射创建Bean实例对象,创建好的Bean对象存储在一个名为singletonObjects的Map集合中,当调用getBean方法 时则最终从该Map集合中取出Bean实例对象返回

image.png
DefaultListableBeanFactory对象内部维护着一个Map用于存储封装好的BeanDefinitionMap
image.png
Spring框架会取出beanDefinitionMap中的每个BeanDefinition信息,反射构造方法或调用指定的工厂方法生成Bean实例对象,所以只要将BeanDefinition注册到beanDefinitionMap这个Map中,Spring就会进行对应的Bean的实例化操作

singletonObjects这个map在DefaultSingletonBeanRegistry类中声明

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
}

总结
基本步骤:

  • 加载xml配置文件,解析获取配置中的每个的信息,封装成一个个的BeanDefinition对象;
  • 将BeanDefinition存储在一个名为beanDefinitionMap的Map中;
  • ApplicationContext底层遍历beanDefinitionMap,创建Bean实例对象;
  • 创建好的Bean实例对象,被存储到一个名为singletonObjects的Map中;
  • 当执行applicationContext.getBean(beanName)时,从singletonObjects去匹配Bean实例返回

image.png

4.5 Spring的后处理器

Spring的后处理器是Spring对外开发的重要扩展点,允许我们介入到Bean的整个实例化流程中来,以达到动态注册 BeanDefinition动态修改BeanDefinition,以及动态修改Bean的作用
Spring主要有两种后处理器:

  • BeanFactoryPostProcessor:Bean工厂后处理器,在BeanDefinitionMap填充完毕,Bean实例化之前执行; (执行一次)
  • BeanPostProcessor:Bean后处理器,一般在Bean实例化之后,填充到单例池singletonObjects之前执行。 (执行大于一次)

1) Bean工厂后处理器 – BeanFactoryPostProcessor - 针对BeanDefinition

BeanFactoryPostProcessor是一个接口规范,实现了该接口的类只要交由Spring容器管理的话,那么Spring就会回调该接口的方法,用于对BeanDefinition注册和修改的功能
BeanFactoryPostProcessor 定义如下:

@FunctionalInterface
public interface BeanFactoryPostProcessor {
    void postProcessBeanFactory(ConfigurableListableBeanFactory var1) throws BeansException;
}

修改BeanDefinition
编写BeanFactoryPostProcessor

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println("MyBeanFactoryPostProcessor的postProcessBeanFactory方法执行了");
        // 修改xml中的bean <bean id="userService" class="com.mem.service.impl.UserServiceImpl2"></bean>
        BeanDefinition userServiceBD = beanFactory.getBeanDefinition("userService"); //获得UserDao定义对象
        userServiceBD.setBeanClassName("com.mem.dao.impl.UserDaoImpl");  //修改class
        //userServiceBD.setInitMethodName(methodName); //修改初始化方法
        //userServiceBD.setLazyInit(true); //修改是否懒加载
        //... 省略其他的设置方式 ...
    }
}

配置BeanFactoryPostProcessor

<bean id="userService" class="com.mem.service.impl.UserServiceImpl2"></bean>
<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl"></bean>
<bean class="com.mem.processor.MyBeanFactoryPostProcessor"/>

测试:发现原来的xml中userService 被修改成了UserDaoImpl对象

public class Test06_BeanFactoryPostProcessor01 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext6.xml");
        Object userService =  applicationContext.getBean("userService");
        System.out.println(userService);
    }
}
// 结果
MyBeanFactoryPostProcessor的postProcessBeanFactory方法执行了
com.mem.dao.impl.UserDaoImpl@192b07fd

注册新的BeanDefinition
MyBeanFactoryPostProcessor类中添加相关的代码

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println("MyBeanFactoryPostProcessor的postProcessBeanFactory方法执行了");
        // 注册一个新的UserServiceImpl2对象
        // 强转成子类DefaultListableBeanFactory
        if (beanFactory instanceof DefaultListableBeanFactory){
            DefaultListableBeanFactory beanFactory1 = (DefaultListableBeanFactory) beanFactory;
            BeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClassName("com.mem.service.impl.UserServiceImpl2");
            // 进行注册操作
            beanFactory1.registerBeanDefinition("userService2",beanDefinition);
        }
    }
}

测试:userService2作为beanName,去查找。

public class Test05_PostProcessor {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext6.xml");
        Object userService2 =  applicationContext.getBean("userService2");
        System.out.println(userService2);
    }
}
// 结果
MyBeanFactoryPostProcessor的postProcessBeanFactory方法执行了
com.mem.service.impl.UserServiceImpl2@192b07fd

**简化:**Spring 提供了一个BeanFactoryPostProcessor的子接口BeanDefinitionRegistryPostProcessor专门用于注册 BeanDefinition操作 如下所示:

public class MyBeanFactoryPostProcessor2 implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        System.out.println("MyBeanFactoryPostProcessor2的postProcessBeanDefinitionRegistry方法执行了");
        BeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClassName("com.mem.service.impl.UserServiceImpl2");
        beanDefinitionRegistry.registerBeanDefinition("userService2",beanDefinition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        System.out.println("MyBeanFactoryPostProcessor2的postProcessBeanFactory方法执行了");
    }
}

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println("MyBeanFactoryPostProcessor的postProcessBeanFactory方法执行了");
    }
}

测试:优先级顺序

public class Test06_BeanFactoryPostProcessor01 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext6.xml");
        Object userService2 =  applicationContext.getBean("userService2");
        System.out.println(userService2);
    }
}
// 结果
MyBeanFactoryPostProcessor2的postProcessBeanDefinitionRegistry方法执行了
MyBeanFactoryPostProcessor2的postProcessBeanFactory方法执行了
MyBeanFactoryPostProcessor的postProcessBeanFactory方法执行了
com.mem.service.impl.UserServiceImpl2@55d56113

如上结果所示:MyBeanFactoryPostProcessor2类中的postProcessBeanDefinitionRegistry先执行,其次是postProcessBeanFactory,最后才是MyBeanFactoryPostProcessor类中的postProcessBeanFactory方法。
流程图完善:
image.png

案例, 使用Spring的BeanFactoryPostProcessor扩展点完成自定义注解扫描

要求如下:

  • 自定义@MyComponent注解,使用在类上;
  • 使用资料中提供好的包扫描器工具BaseClassScanUtils 完成指定包的类扫描;
  • 自定义BeanFactoryPostProcessor完成注解@MyComponent的解析,解析后最终被Spring管理。

实现:
自定义@MyComponent注解,使用在类上

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
    // 显示的指定的beanName
    String value() default "";
}

在类上使用@MyComponent

@MyComponent("firstBean")
public class FirstBeanByAnno {
}

自定义BeanFactoryPostProcessor完成注解解析

public class MyBeanFactoryPostProcessor3 implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        System.out.println("MyBeanFactoryPostProcessor3的postProcessBeanDefinitionRegistry方法执行了");
        //指定要扫描的包
        String basePackage = "com.mem";
        //调用扫描工具扫描指定包及其子包下的@MyComponent
        Map<String, Class> scanMyComponentAnnotation = BaseClassScanUtils.scanMyComponentAnnotation(basePackage);
        //遍历Map集合,创建BeanDefinition对象进行注册
        scanMyComponentAnnotation.forEach((beanName,clazz)->{
            BeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClassName(clazz.getName());
            beanDefinitionRegistry.registerBeanDefinition(beanName,beanDefinition);
        });
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        System.out.println("MyBeanFactoryPostProcessor3的postProcessBeanFactory方法执行了");
    }
}

配置自定义BeanFactoryPostProcessor到xml中

<bean class="com.mem.processor.MyBeanFactoryPostProcessor3"/>

测试类

public class Test06_BeanFactoryPostProcessor02 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext6.xml");
        Object firstBean =  applicationContext.getBean("firstBean");
        System.out.println(firstBean);
    }
}

包扫描 工具类

public class BaseClassScanUtils {

    //设置资源规则
    private static final String RESOURCE_PATTERN = "/**/*.class";

    public static Map<String, Class> scanMyComponentAnnotation(String basePackage) {

        //创建容器存储使用了指定注解的Bean字节码对象
        Map<String, Class> annotationClassMap = new HashMap<String, Class>();

        //spring工具类,可以获取指定路径下的全部类
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        try {
            String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            ClassUtils.convertClassNameToResourcePath(basePackage) + RESOURCE_PATTERN;
            Resource[] resources = resourcePatternResolver.getResources(pattern);
            //MetadataReader 的工厂类
            MetadataReaderFactory refractory = new CachingMetadataReaderFactory(resourcePatternResolver);
            for (Resource resource : resources) {
                //用于读取类信息
                MetadataReader reader = refractory.getMetadataReader(resource);
                //扫描到的class
                String classname = reader.getClassMetadata().getClassName();
                Class<?> clazz = Class.forName(classname);
                //判断是否属于指定的注解类型
                if(clazz.isAnnotationPresent(MyComponent.class)){
                    //获得注解对象
                    MyComponent annotation = clazz.getAnnotation(MyComponent.class);
                    //获得属value属性值
                    String beanName = annotation.value();
                    //判断是否为""
                    if(beanName!=null&&!beanName.equals("")){
                        //存储到Map中去
                        annotationClassMap.put(beanName,clazz);
                        continue;
                    }

                    //如果没有为"",那就把当前类的类名作为beanName
                    annotationClassMap.put(clazz.getSimpleName(),clazz);

                }
            }
        } catch (Exception exception) {
        }

        return annotationClassMap;
    }

    public static void main(String[] args) {
        Map<String, Class> stringClassMap = scanMyComponentAnnotation("com.mem");
        System.out.println(stringClassMap);
    }
}

2) Bean后处理器 – BeanPostProcessor - 针对Bean

Bean被实例化后,到最终缓存到名为singletonObjects单例池之前,中间会经过Bean的初始化过程,例如:属性的 填充、初始方法init的执行等,其中有一个对外进行扩展的点BeanPostProcessor,我们称为Bean后处理。跟上面的 Bean工厂后处理器相似,它也是一个接口,实现了该接口并被容器管理的BeanPostProcessor,会在流程节点上被 Spring自动调用。

BeanPostProcessor的接口定义如下:

public interface BeanPostProcessor {
    @Nullable
    //在属性注入完毕,init初始化方法执行之前被回调
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    //在初始化方法执行之后,被添加到单例池singletonObjects之前被回调
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

自定义MyBeanPostProcessor,完成快速入门测试

public class MyBeanPostProcessor implements BeanPostProcessor {
    // 初始化之前执行
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(beanName+"-BeanPostProcessor的before方法...");
        return bean;
    }
    // 初始化之后执行
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(beanName+"-BeanPostProcessor的after方法...");
        return bean;
    }
}
public class UserDaoImpl implements UserDao, InitializingBean {
    public UserDaoImpl(){
        System.out.println("UserDaoImpl创建了...");
    }
    public UserDaoImpl(String name){
        //        System.out.println("UserDaoImpl(String name) 执行了,name="+name);
    }
    public void init(){
        System.out.println("UserDaoImpl初始化方法执行...");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("UserDaoImpl属性填充...");
    }
}

配置MyBeanPostProcessor

<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl" init-method="init"></bean>
<bean class="com.mem.processor.MyBeanPostProcessor"/>

测试类:

public class Test07_BeanPostProcessor01 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext7.xml");
        Object userDao =  applicationContext.getBean("userDao");
        System.out.println(userDao);
    }
}
// 结果
UserDaoImpl创建了...
userDao-BeanPostProcessor的before方法...
UserDaoImpl属性填充...
UserDaoImpl初始化方法执行...
userDao-BeanPostProcessor的after方法...
com.mem.dao.impl.UserDaoImpl@6adbc9d
案例, 对Bean方法进行执行时间日志增强

要求如下:

  • Bean的方法执行之前控制台打印当前时间;
  • Bean的方法执行之后控制台打印当前时间。

分析:

  • 对方法进行增强主要就是代理设计模式和包装设计模式;
  • 由于Bean方法不确定,所以使用动态代理在运行期间执行增强操作;
  • 在Bean实例创建完毕后,进入到单例池之前,使用Proxy代替真是的目标Bean

实现:
编写BeanPostProcessor,增强逻辑编写在 after方法中

public class MyBeanPostProcessor2 implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 使用动态代理对目标bean进行增强,返回proxy对象,进而存储到单例池中
        Object proxyInstance = Proxy.newProxyInstance(
            bean.getClass().getClassLoader(),
            bean.getClass().getInterfaces(),
            (proxy,method,args)->{
                // 1. 输出开始时间
                System.out.println("方法:"+method.getName()+"-开始时间:"+new Date());
                // 2. 执行目标方法
                Object result = method.invoke(bean, args);
                // 3. 输出结束时间
                System.out.println("方法:"+method.getName()+"-结束时间:"+new Date());
                return result;
            }
        );
        return proxyInstance;
    }
}
public class UserDaoImpl implements UserDao {
    public UserDaoImpl(){
        //        System.out.println("UserDaoImpl创建了...");
    }

    @Override
    public void show() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("show()....");
    }
}

配置BeanPostProcessor

<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl"></bean>
<bean class="com.mem.processor.MyBeanPostProcessor2"/>

测试类:

public class Test07_BeanPostProcessor02 {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext7.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        userDao.show();
    }
}
// 结果
方法:show-开始时间:Sat Jun 24 18:11:07 CST 2023
show()....
方法:show-结束时间:Sat Jun 24 18:11:10 CST 2023

流程图完善:
image.png

4.6 Spring Bean的生命周期

Spring Bean的生命周期是从 Bean 实例化之后,即通过反射创建出对象之后,到Bean成为一个完整对象,最终存储到单例池中,这个过程被称为Spring Bean的生命周期。
Spring Bean的生命周期大体上分为三个阶段:

  • Bean的实例化阶段:Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的, 是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化;
  • Bean的初始化阶段:Bean创建之后还仅仅是个"半成品",还需要对Bean实例的属性进行填充、执行一些Aware 接口方法、执行BeanPostProcessor方法、执行InitializingBean接口的初始化方法、执行自定义初始化init方法 等。该阶段是Spring最具技术含量和复杂度的阶段,Aop增强功能,后面要学习的Spring的注解功能等、 spring高频面试题Bean的循环引用问题都是在这个阶段体现的;
  • Bean的完成阶段:经过初始化阶段,Bean就成为了一个完整的Spring Bean,被存储到单例池 singletonObjects中去了,即完成了Spring Bean的整个生命周期。

由于Bean的初始化阶段的步骤比较复杂,所以着重研究Bean的初始化阶段

初始化阶段

Spring Bean的初始化过程涉及如下几个过程:

  • Bean实例的属性填充
  • Aware接口属性注入
  • BeanPostProcessor的before()方法回调
  • InitializingBean接口的初始化方法回调
  • 自定义初始化方法init回调
  • BeanPostProcessor的after()方法回调

PS:通过代码验证上述初始化顺序… …

Bean实例属性填充

BeanDefinition中有对当前Bean实体的注入信息通过属性propertyValues进行了存储,例如UserService的属性信息如下:
image.png

Spring在进行属性注入时,会分为如下几种情况:

  • 注入普通属性,String、int或存储基本类型的集合时,直接通过set方法的反射设置进去;
  • 注入单向对象引用属性时,从容器中getBean获取后通过set方法反射设置进去,如果容器中没有,则先创建被 注入对象Bean实例(完成整个生命周期)后,在进行注入操作;
  • 注入双向对象引用属性时,就比较复杂了,涉及了循环引用(循环依赖)问题,下面会详细阐述解决方案。

PS:通过代码验证上述第二第三种属性填充… …

①略
②针对单向对象引用属性时,执行的顺序问题:

public class UserServiceImpl implements UserService {
    public UserServiceImpl() {
        System.out.println("UserServiceImpl创建了...");
    }

    private UserDao userDao;
    public void setUserDao(UserDao userDao){
        System.out.println("setUserDao()执行了,注入成功...");
        this.userDao = userDao;
    }
}
public class UserDaoImpl implements UserDao {
    public UserDaoImpl(){
        System.out.println("UserDaoImpl创建了...");
    }
}

配置xml文件:

<!--配置 UserServiceImpl-->
<bean id="userService" class="com.mem.service.impl.UserServiceImpl">
  <!--注入userDao对象-->
  <property name="userDao" ref="userDao"></property>
</bean>
<!--配置 UserDaoImpl-->
<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl"></bean>

测试顺序:

public class Test04_AttributeInput {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) applicationContext.getBean("userService");
        System.out.println(userService);
    }
}
//情况1(调整xml中userDao的位置,位于userService后面):
UserServiceImpl创建了...
UserDaoImpl创建了...
setUserDao()执行了,注入成功...
//情况2(调整xml中userDao的位置,位于userService前面):
UserDaoImpl创建了...
UserServiceImpl创建了...
setUserDao()执行了,注入成功...

③针对双向对象引用属性时,产生的循环引用问题
多个实体之间相互依赖并形成闭环的情况就叫做"循环依赖",也叫做"循环引用"
问题描述:
image.png
如下图所示:

  1. 调用getBean(“userService”)时,实例化userService,并在内存开辟空间 X001 用于存储;
  2. userService初始化(属性填充userDao对象);
  3. 去单例池中查找userDao,发现没有;
  4. 实例化userDao对象,并在内存开辟空间 X002 用于存储;
  5. userDao初始化(属性填充userService对象);
  6. 去单例池中查找userService,发现没有;
  7. 再次循环

image.png

Spring 解决方案: 三级缓存
Spring提供了三级缓存存储 完整Bean实例半成品Bean实例 ,用于解决循环引用问题在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供如下三个Map:

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
	//1、最终存储单例Bean成品的容器,即实例化和初始化都完成的Bean,称之为"一级缓存"
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
    //2、早期Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用了,称之为"二级缓存"
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
    //3、单例Bean的工厂池,缓存半成品对象,对象未被引用,使用时在通过工厂创建Bean,称之为"三级缓存"
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
}
@FunctionalInterface
public interface ObjectFactory<T> {
    T getObject() throws BeansException;
}

image.png
结合之前的图讲解,

  • UserService创建完毕之后,把UserService的引用存储到第三级缓存中(存储的并不是Bean本身,而是ObjectFactory,等用到的时候,在调用getObject()方法,进行真正创建)
  • UserService 属性注入,需要UserDao,从缓存中获取,没有UserDao;
  • UserDao实例化对象,但尚未初始化,将UserDao存储到到三级缓存;
  • UserDao属性注入,需要UserService,从三级缓存获取UserService,UserService从三级缓存移入二级缓存;
  • UserDao执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存;
  • UserService 注入UserDao;
  • UserService执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存。

二级缓存和三级缓存的区别:

二级缓存存的是被引用的Bean
三级缓存存的是未被引用的Bean

代码验证后,分析出UserService与UserDao实例化与初始化的顺序如下:
image.png

三级缓存源码剖析流程.pdf

常用的Aware接口

Aware接口是一种框架辅助属性注入的一种思想,其他框架中也可以看到类似的接口。框架具备高度封装性,我们接 触到的一般都是业务代码,一个底层功能API不能轻易的获取到,但是这不意味着永远用不到这些对象,如果用到了 ,就可以使用框架提供的类似Aware的接口,让框架给我们注入该对象。
image.png

public class UserServiceImpl implements UserService, ServletContextAware,
ApplicationContextAware, BeanFactoryAware, BeanNameAware {
    private UserDao userDao;

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

    @Override
    public void setServletContext(ServletContext servletContext) {
        System.out.println(servletContext);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println(applicationContext);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println(beanFactory);
    }

    @Override
    public void setBeanName(String s) {
        System.out.println(s);
    }
}
<bean id="userDao" class="com.mem.dao.impl.UserDaoImpl"></bean>
<bean id="userService" class="com.mem.service.impl.UserServiceImpl">
  <property name="userDao" ref="userDao"></property>
</bean>

测试类:

public class Test09_Aware {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext9.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        UserService userService = (UserService) applicationContext.getBean("userService");
    }
}
// 结果
// beanName
Service 
// BeanFactory
org.springframework.beans.factory.support.DefaultListableBeanFactory@6fb554cc: defining beans [userDao,userService]; root of factory hierarchy
// ApplicationContext
org.springframework.context.support.ClassPathXmlApplicationContext@65b3120a, started on Sun Jun 25 22:31:27 CST 2023

4.7 Spring IoC整体流程总结

总流程图:
image.png
简易版:
image.png

详解:

  1. bean标签首先通过Reader读取,封装成BeanDefinition对象;
  2. 把每个bean标签都封装成BeanDefinition对象,存入beanDefinitionMap中;
  3. Spring循环beanDefinitionMap,取出每个beanDefinition对象,通过反射创建真正的实例对象;
  4. 将所有创建的对象存入singletonObjects单例池Map中;
  5. 当我们调用getBean方法,其实就是从单例池中获取对象。

扩展1:加入bean工厂后处理器(BeanFactoryPostProcessor)
image.png

详解:

  1. 将BD存入beanDefinitionMap中后,经过Bean工厂后处理器及其子类可以动态的添加一些BD到beanDefinitionMap

由于BeanFactoryPostProcessor的注册bean功能实现较为复杂,添加其子类BeanDefinitionRegistryPostProcessor来处理注册bean功能

扩展2:加入BeanPostProcessor
image.png

详解:

  1. 在Obj初始化对象之前,执行before()方法,在Obj初始化之后执行after()方法

可以利用这两方法对Obj做增强,修改属性,添加属性,动态代理之类的

汇总版:
image.png

4.8 Spring xml方式整合第三方框架

xml整合第三方框架有两种整合方案:

  • 不需要自定义名空间,不需要使用Spring的配置文件配置第三方框架本身内容,例如:MyBatis;
  • 需要引入第三方框架命名空间,需要使用Spring的配置文件配置第三方框架本身内容,例如:Dubbo。

不需要自定义名空间

Spring整合MyBatis

之前已经在Spring中简单的配置了SqlSessionFactory,但是这不是正规的整合方式, MyBatis提供了mybatis-spring.jar专门用于两大框架的整合。

📄原来的MyBatis的步骤如下:
  • 编写配置文件mybatis-config.xml
  • 编写Mapper接口及Mapper配置xml
  • 编写测试代码

编写配置文件mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <package name="com.mem.mapper"/>
    </mappers>
</configuration>

编写Mapper接口及Mapper配置xml

package com.mem.mapper;
import com.mem.pojo.User;
import java.util.List;

public interface UserMapper {
    List<User> findAll();
}
public class User {
    private Integer id;
    private String username;
    private String password;
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mem.mapper.UserMapper">
    <select id="findAll" resultType="com.mem.pojo.User">
        select * from user
    </select>
</mapper>

编写测试代码

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = factoryBuilder.build(in);
        SqlSession session = sqlSessionFactory.openSession();
        UserMapper userMapper = session.getMapper(UserMapper.class);
        List<User> all = userMapper.findAll();
        all.forEach(System.out::println);
    }
}
// 结果
User{id=1, username='张三', password='123456'}
User{id=2, username='李四', password='654321'}
📄Spring整合MyBatis的步骤如下:
  • 导入MyBatis整合Spring的相关坐标;(请见资料中的pom.xml)
  • 编写Mapper和Mapper.xml;
  • 配置SqlSessionFactoryBean和MapperScannerConfigurer;
  • 编写测试代码

导入MyBatis整合Spring的相关坐标

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.6</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.12</version>
</dependency>

配置SqlSessionFactoryBean和MapperScannerConfigurer:

<!--整合Mybatis的配置文件-->
<!-- 配置数据源信息 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
    <property name="url" value="jdbc:mysql://192.168.91.135:3306/mybatis?useSSL=false"></property>
    <property name="username" value="root"></property>
    <property name="password" value="123456"></property>
</bean>

<!-- 配置SqlSessionFactoryBean,作用将SqlSessionFactory存储到Spring容器中-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!--MapperScannerConfigurer,作用扫描指定的包,产生Mapper对象,存储到Spring容器中-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.mem.mapper"/>
</bean>

<bean id="userService" class="com.mem.service.impl.UserServiceImpl">
    <property name="userMapper" ref="userMapper"></property>
</bean>

编写Mapper和Mapper.xml

public interface UserMapper {
    List<User> findAll();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mem.mapper.UserMapper">
    <select id="findAll" resultType="com.mem.pojo.User">
        select * from user
    </select>
</mapper>

编写测试代码

public class Test10_Mybatis {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext10.xml");
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.show();
    }
}
// 结果
User{id=1, username='张三', password='123456'}
User{id=2, username='李四', password='654321'}
Spring整合MyBatis的原理剖析

整合包里提供了一个SqlSessionFactoryBean和一个扫描Mapper的配置对象,SqlSessionFactoryBean一旦被实例化,就开始扫描Mapper并通过动态代理产生Mapper的实现类存储到Spring容器中。相关的有如下四个类:

  • SqlSessionFactoryBean:需要进行配置,用于提供SqlSessionFactory
  • MapperScannerConfigurer:需要进行配置,用于扫描指定mapper注册BeanDefinition
  • MapperFactoryBean:Mapper的FactoryBean,获得指定Mapper时调用getObject方法;
  • ClassPathMapperScanner:definition.setAutowireMode(2) 修改了自动注入状态,所以 MapperFactoryBean中的setSqlSessionFactory会自动注入进去。

SqlSessionFactoryBean
配置SqlSessionFactoryBean作用是向容器中提供SqlSessionFactory,SqlSessionFactoryBean实现了 FactoryBean和InitializingBean两个接口,所以会自动执行getObject() 和afterPropertiesSet()方法

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
	// FactoryBean中重写的方法,用于延迟返回 sqlSessionFactory
    public SqlSessionFactory getObject() throws Exception {
        if (this.sqlSessionFactory == null) {
            this.afterPropertiesSet();
        }
        return this.sqlSessionFactory;
    }
    // InitializingBean中重写的方法,用于创建 sqlSessionFactory
    public void afterPropertiesSet() throws Exception {
        // ~~~
        this.sqlSessionFactory = this.buildSqlSessionFactory();
    }
    protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
        // 相当于原来mybatis中的SqlSessionFactory sqlSessionFactory = factoryBuilder.build(in)
    	return this.sqlSessionFactoryBuilder.build(targetConfiguration);
    }
}

MapperScannerConfigurer
配置MapperScannerConfigurer作用是扫描Mapper,向容器中注册Mapper对应的MapperFactoryBean, MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor和InitializingBean两个接口,会在 postProcessBeanDefinitionRegistry方法中向容器中注册MapperFactoryBean

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    	// 类加载路径下的扫描器
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        // 1. 调用父类(ClassPathBeanDefinitionScanner)中的scan方法
        scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n"));
    }
}

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
	public int scan(String... basePackages) {
        int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
        // 2. 调用的是子类(ClassPathMapperScanner)中的doScan方法
        this.doScan(basePackages);
    }
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 4. 注册BeanDefinition到BeanDefinitionMap中,
        // definitionHolder中封装着BeanDefinition, 此时这个BD的beanClass: UserMapper(接口) 
        this.registerBeanDefinition(definitionHolder, this.registry);
        return beanDefinitions;
    }
}

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
	public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 3. 调用父类(ClassPathBeanDefinitionScanner)中的doScan方法,返回beanDefinitions集合
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        // 5. 调用自身的processBeanDefinitions方法
        this.processBeanDefinitions(beanDefinitions);
        return beanDefinitions;
    }
    // 用于修改definition的beanClass
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        BeanDefinitionHolder holder = (BeanDefinitionHolder)var4.next();
        AbstractBeanDefinition definition = (AbstractBeanDefinition)holder.getBeanDefinition();
        // 6. UserMapper -> MapperFactoryBean 不然没法创建对象
        definition.setBeanClass(this.mapperFactoryBeanClass);
        // 8. 按照类型去注入MapperFactoryBean的getObject()方法所需的sqlSessionFactory对象
        definition.setAutowireMode(2); // PS:autowireMode取值:1是根据名称自动装配,2是根据类型自动装配  
    }
}

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
	// 7. 当调用getBean()方法时,底层调用getObject(),用于动态返回bean
    public T getObject() throws Exception {
        // 相当于原来的
        // SqlSession session = sqlSessionFactory.openSession();
        // UserMapper userMapper = session.getMapper(UserMapper.class);
        return this.getSqlSession().getMapper(this.mapperInterface);
    }
}

总结:

  1. SqlSessionFactoryBean,负责产生SqlSessionFactory,并把SqlSessionFactory扔到Spring容器中,谁用谁按类型或名称注入;
  2. MapperScannerConfigurer,作为扫描的入口,用于加载配置文件,扫描包;
  3. 在扫描时,内部调用ClassPathMapperScanner的scan方法进行相应的扫描;
  4. 把扫描好的BeanDefinition扔到BDMap中;
  5. 再通过ClassPathMapperScanner去修改BeaClass的路径,改为MapperFactoryBean的全路径;
  6. 当调用getBean(UserMapper.class)时,通过MapperFactoryBean中的getObject()方法,去获取Mapper对象;

需要引入第三方框架命名空间

Spring 整合其他组件时就不像MyBatis这么简单了,例如Dubbo框架在于Spring进行整合时,要使用Dubbo提供的 命名空间的扩展方式,自定义了一些Dubbo的标签
image.png

为了降低我们此处的学习成本,不在引入Dubbo第三方框架了,以Spring的 context 命名空间去进行讲解,该方式 也是命名空间扩展方式。

context命名空间

需求:加载外部properties文件,将键值对存储在Spring容器中

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useSSL=false
jdbc.username=root
jdbc.password=123456

引入context命名空间,在使用context命名空间的标签,使用SpEL表达式在xml或注解中根据key获得value

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

    <!--整合Mybatis的配置文件- 测试context命名空间-->
    <!-- 加载properties文件-->
    <context:property-placeholder location="jdbc.properties"/>

    <!-- 配置数据源信息 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!-- 配置SqlSessionFactoryBean,作用将SqlSessionFactory存储到Spring容器中-->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--MapperScannerConfigurer,作用扫描指定的包,产生Mapper对象,存储到Spring容器中-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.mem.mapper"/>
    </bean>
</beans>

其实,加载的properties文件中的属性最终通过Spring解析后会被存储到了Spring容器的environment中去,不仅 自己定义的属性会进行存储,Spring也会把环境相关的一些属性进行存储
image.png

原理剖析解析过程:

只能从源头ClassPathXmlApplicationContext入手,经历复杂的源码追踪,找到如下两个点:
1)在创建DefaultNamespaceHandlerResolver时,为处理器映射地址handlerMappingsLocation属性赋值,并加载命名空间处理器到Map handlerMappings 中去
image.png
第一点完成后,Map集合handlerMappings就被填充了很多XxxNamespaceHandler,继续往下追代码
2)在DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法中,发现如下逻辑:
image.png
如果是默认命名空间,则执行parseDefaultElement方法
image.png
如果是自定义命名空间,则执行parseCustomElement方法
image.png
在执行resovle方法时,就是从这个名为handlerMappings的Map中根据命名空间名称获得对应的处理器对象,此处是ContextNamespaceHandler,最终执行NamespaceHandler的parse方法

ContextNamespaceHandler源码如下,间接实现了NamespaceHandler接口,初始化方法init会被自动调用。由于 context命名空间下有多个标签,所以每个标签又单独注册了对应的解析器,注册到了其父类 NamespaceHandlerSupport的Map parsers中去了
image.png
通过上述分析,我们清楚的了解了外部命名空间标签的执行流程,如下:

  • 将自定义标签的约束 与 物理约束文件与网络约束名称的约束 以键值对形式存储到一个spring.schemas文件里 ,该文件存储在类加载路径的 META-INF里,Spring会自动加载到;
  • 将自定义命名空间的名称 与 自定义命名空间的处理器映射关系 以键值对形式存在到一个叫spring.handlers文 件里,该文件存储在类加载路径的 META-INF里,Spring会自动加载到;
  • 准备好NamespaceHandler,如果命名空间只有一个标签,那么直接在parse方法中进行解析即可,一般解析结果就是注册该标签对应的BeanDefinition。如果命名空间里有多个标签,那么可以在init方法中为每个标签都注册一个BeanDefinitionParser,在执行NamespaceHandler的parse方法时在分流给不同的 BeanDefinitionParser进行解析(重写doParse方法即可)。
案例:自定义命名空间和标签

设想自己是一名架构师,进行某一个框架与Spring的集成开发,效果是通过一个指示标签,向Spring容器中自动注入一个BeanPostProcessor
image.png

步骤分析:

  1. 确定命名空间名称、schema虚拟路径、标签名称;
  2. 编写schema约束文件haohao-annotation.xsd
  3. 在类加载路径下创建META目录,编写约束映射文件spring.schemas和处理器映射文件spring.handlers
  4. 编写命名空间处理器 HaohaoNamespaceHandler,在init方法中注册HaohaoBeanDefinitionParser
  5. 编写标签的解析器 HaohaoBeanDefinitionParser,在parse方法中注册HaohaoBeanPostProcessor
  6. 编写HaohaoBeanPostProcessor
    以上六步是框架开发者写的,以下是框架使用者写的
  7. 在applicationContext.xml配置文件中引入命名空间
  8. 在applicationContext.xml配置文件中使用自定义的标签

编写schema约束文件haohao-annotation.xsd
image.png
在类加载路径下创建META目录,编写约束映射文件spring.schemas和处理器映射文件spring.handlers
image.png
编写命名空间处理器 HaohaoNamespaceHandler,在init方法中注册HaohaoBeanDefinitionParser
image.png
编写标签的解析器 HaohaoBeanDefinitionParser,在parse方法中注册HaohaoBeanPostProcessor
image.png
编写HaohaoBeanPostProcessor
image.png

05、基于注解的Spring应用

5.1 Bean基本注解开发

Spring除了xml配置文件进行配置之外,还可以使用注解方式进行配置,注解方式慢慢成为xml配置的替代方案。我们有了xml开发的经验,学习注解开发就方便了许多,注解开发更加快捷方便。
Spring提供的注解有三个版本:

  • 2.0时代,Spring开始出现注解
  • 2.5时代,Spring的Bean配置可以使用注解完成
  • 3.0时代,Spring其他配置也可以使用注解完成,我们进入全注解时代

基本Bean注解,主要是使用注解的方式替代原有xml的 标签及其标签属性的配置
image.png
使用@Component 注解替代标签
image.png
可以通过@Component注解的value属性指定当前Bean实例的beanName,也可以省略不写,不写的情况下为当前类名首字母小写
image.png
使用注解对需要被Spring实例化的Bean进行标注,但是需要告诉Spring去哪找这些Bean,要配置组件扫描路径
image.png

image.png
使用上述注解完成UserDaoImpl的基本配置
image.png

由于JavaEE开发是分层的,为了每层Bean标识的注解语义化更加明确,@Component又衍生出如下三个注解:
image.png
image.png

5.2 Bean依赖注入注解开发

Bean依赖注入的注解,主要是使用注解的方式替代xml的 标签完成属性的注入操作
image.png
Spring主要提供如下注解,用于在Bean内部进行属性注入的:
image.png

@Value

通过@Value 直接注入普通属性
image.png
通过@Value 注入properties文件中的属性
image.png
加载properties文件
image.png

@Autowired

@Autowired注解,用于根据类型进行注入
image.png
当容器中同一类型的Bean实例有多个时,会尝试自动根据名字进行匹配:
image.png

当容器中同一类型的Bean实例有多个时,且名字与被注入Bean名称不匹配时会报错

@Qualifier

@Qualifier配合@Autowired可以完成根据名称注入Bean实例,使用@Qualifier指定名称
image.png

@Resource

@Resource注解既可以根据类型注入,也可以根据名称注入,无参就是根据类型注入,有参数就是根据名称注入
image.png

PS:@Resource注解存在与 javax.annotation 包中,Spring对其进行了解析

/**
 * 扩展@Autowired 按照类型注入,不一定只能在set方法上
 */
@Component
public class UserServiceImpl implements UserService {

    @Autowired
    public void xxx(UserDao userDao){
        System.out.println("xxx:"+userDao); 
        // xxx:com.mem.dao.impl.UserDaoImpl@35fc6dc4
    }

    @Autowired
    public void yyy(List<UserDao> userDaoList){
        System.out.println("yyy:"+userDaoList); 
        // yyy:[com.mem.dao.impl.UserDaoImpl@35fc6dc4, com.mem.dao.impl.UserDaoImpl2@7fe8ea47]
    }
}

5.3 非自定义Bean注解开发

非自定义Bean不能像自定义Bean一样使用@Component进行管理,非自定义Bean要通过工厂的方式进行实例化, 使用@Bean标注方法即可,@Bean的属性为beanName,如不指定为当前工厂方法名称
image.png

PS:工厂方法所在类必须要被Spring管理

如果@Bean工厂方法需要参数的话,则有如下几种注入方式:

  • 使用@Autowired 根据类型自动进行Bean的匹配,@Autowired可以省略
  • 使用@Qualifier 根据名称进行Bean的匹配;
  • 使用@Value 根据名称进行普通数据类型匹配。

image.png


5.4 Bean配置类的注解开发

@Component等注解替代了标签,但是像、 context:componentScan等非标签怎样去使用注解替代呢?

<!-- 加载properties文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 组件扫描 -->
<context:component-scan base-package="com.itheima"/>
<!-- 引入其他xml文件 -->
<import resource="classpath:beans.xml"/>

定义一个配置类替代原有的xml配置文件,标签以外的标签,一般都是在配置类上使用注解完成的

@Configuration注解标识的类为配置类,替代原有xml配置文件,该注解第一个作用是标识该类是一个配置类,第二个作用是具备@Component作用

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";

    boolean proxyBeanMethods() default true;
}

@ComponentScan 组件扫描配置,替代原有xml文件中的<context:component-scan base-package=“”/>配置

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};
    // ~~~~
}

base-package的配置方式:

  • 指定一个或多个包名:扫描指定包及其子包下使用注解的类
  • 不配置包名:扫描当前@componentScan注解配置类所在包及其子包下的类

@PropertySource 注解用于加载外部properties资源配置,替代原有xml中的 <context:property-placeholder location=“”/>配置

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {
    String name() default "";

    String[] value();

    boolean ignoreResourceNotFound() default false;

    String encoding() default "";

    Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}

@Import 用于加载其他配置类,替代原有xml中的 配置

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    Class<?>[] value();
}

使用方法:

@Configuration
// <context:component-scan base-package="com.mem"/>
@ComponentScan("com.mem")
// <context:property-placeholder location="classpath:jdbc.properties"/>
@PropertySource("classpath:jdbc.properties")
// <import resource="classpath:beans.xml"/>
@Import(value = NoCustomBean.class)
public class MyConfig {
}

5.5 Spring配置其他注解

扩展:@Primary注解用于标注相同类型的Bean优先被使用权,@Primary 是Spring3.0引入的,与@Component 和@Bean一起使用,标注该Bean的优先级更高,则在通过类型获取Bean或通过@Autowired根据类型进行注入时, 会选用优先级更高的

@Repository("userDao")
public class UserDaoImpl implements UserDao{}

@Repository("userDao2")
@Primary
public class UserDaoImpl2 implements UserDao{}
@Bean
public UserDao userDao01(){return new UserDaoImpl();}

@Bean
@Primary
public UserDao userDao02(){return new UserDaoImpl2();}

扩展:@Profile 注解的作用同于xml配置时学习profile属性,是进行环境切换使用的

<beans profile="test">

注解 @Profile 标注在类或方法上,标注当前产生的Bean从属于哪个环境,只有激活了当前环境,被标注的Bean才 能被注册到Spring容器里,不指定环境的Bean,任何环境下都能注册到Spring容器里

@Repository("userDao")
@Profile("test")
public class UserDaoImpl implements UserDao{}

@Repository("userDao2")
public class UserDaoImpl2 implements UserDao{}

可以使用以下两种方式指定被激活的环境:

  • 使用命令行动态参数,虚拟机参数位置加载 -Dspring.profiles.active=test
  • 使用代码的方式设置环境变量 System.setProperty(“spring.profiles.active”,“test”);

5.6 Spring注解的解析原理

image.png
使用@Component等注解配置完毕后,要配置组件扫描才能使注解生效

  • xml配置组件扫描:
<context:component-scan base-package="com.itheima"/>
  • 配置类配置组件扫描:
@Configuration
@ComponentScan("com.itheima")
public class AppConfig {
}

使用xml方式配置组件扫描,而component-scan是一个context命名空间下的自定义标签,所以要找到对应的命名空间处理器NamespaceHandler 和 解析器,查看spring-context包下的spring.handlers文件
image.png
查看 ContextNamespaceHandler 类

public void init() {
	this.registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
}

将ComponentScanBeanDefinitionParser进行了注册,对其源码进行跟踪,最终将标注的@Component的类,生成对应的BeanDefiition进行了注册
//todo 时序图

使用配置类配置组件扫描,使用AnnotationConfigApplicationContext容器在进行创建时,内部调用了如下代码, 该工具注册了几个Bean后处理器:

AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);

image.png
//todo 时序图
对比:
image.png


5.7 Spring注解方式整合第三方框架

第三方框架整合,依然使用MyBatis作为整合对象,之前我们已经使用xml方式整合了MyBatis,现在使用注解方式 无非就是将xml标签替换为注解,将xml配置文件替换为配置类而已,原有xml方式整合配置如下:
image.png
使用@Bean将DataSource和SqlSessionFactoryBean存储到Spring容器中,而MapperScannerConfigurer使用注 解@MapperScan进行指明需要扫描的Mapper在哪个包下,使用注解整合MyBatis配置方式如下:
image.png

Spring整合MyBatis的原理

注解方式,Spring整合MyBatis的原理,关键在于@MapperScan,@MapperScan不是Spring提供的注解,是 MyBatis为了整合Spring,在整合包org.mybatis.spring.annotation中提供的注解,源码如下:
image.png
重点关注一下@Import({MapperScannerRegistrar.class}),当@MapperScan被扫描加载时,会解析@Import注解,从而加载指定的类,此处就是加载了**MapperScannerRegistrar **

MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,Spring会自动调用 registerBeanDefinitions方法,该方法中又注册MapperScannerConfigurer类,而MapperScannerConfigurer类作用是扫描Mapper,向容器中注册Mapper对应的MapperFactoryBean,前面讲过,此处不在赘述了:
image.png
//TODO 时序图

Spring与MyBatis注解方式整合有个重要的技术点就是@Import,第三方框架与Spring整合xml方式很多是凭借自 定义标签完成的,而第三方框架与Spring整合注解方式很多是靠@Import注解完成的。
@Import可以导入如下三种类:

  • 普通的配置类
  • 实现ImportSelector接口的类
  • 实现ImportBeanDefinitionRegistrar接口的类
实现ImportSelector接口的类

@Import导入实现了ImportSelector接口的类
image.png
ImportSelector接口selectImports方法的参数AnnotationMetadata代表注解的媒体数据,可以获得当前注解修饰的类的元信息,例如:获得组件扫描的包名
image.png
//TODO 时序图

实现ImportBeanDefinitionRegistrar接口的类

@Import导入实现ImportBeanDefinitionRegistrar接口的类,实现了该接口的类的registerBeanDefinitions方法会被自动调用,在该方法内可以注册BeanDefinition
image.png
//TODO 时序图

;