# 1.Spring 的 体系

如果我们只是使用 Spring 的框架, 则只需要引入 spring-context 这个依赖, 这个依赖又会自动的引入 CoreContainer 中其他的需要的依赖

img

img

# 2 .BeanFactory 和 ApplicationContext 的关系

  1. BeanFactory 是早期接口, ApplicationContext 继承 BeanFactory 接口, 是后期的功能更强大的接口
  2. BeanFactory 负责 Bean 对象的创建等逻辑, 而 ApplicationContext 除了继承了 BeanFactory 的能力, 还继承了别的接口, 也就是说 ApplicationContext 的能力更加强大, 比如事件发布, 资源解析接口等.
  3. ApplicationContext 接口继承了 BeanFactoryj 接口, 并且在内部还有 BeanFactory 实例对象的引用, 也就是说除了继承关系,还有融合关系
  4. BeanFactory 创建对象的时机是懒加载, 也就是当调用 getBean()方法时 才会去实例化 Bean 对象, 而 ApplicationContext, 一旦加载了配置文件就会去实例化所有的 JavaBean 对象

# 3. ApplicationContext 的 继承体系

引入 Spring-web 之前的体系

img

引入后的体系

img

##4. ApplicationContext 中, Bean 对象存在哪里

img

ApplicationContext -> BeanFactory -> singletonObjects, 保存了 Spring 容器中的所有 Bean

img

# 4. 基于 XML 配置的 Spring 容器应用

# 4.1 xml 中 常用的配置属性

img

  • id 属性 是配置 Bean 的 BeanName, 在 BeanFactory 中调用 getBean()方法时, 传入的 BeanName 参数就是这个 配置时的 "id", 会转化为 BeanName
  • class 属性: 配置这个 Bean 的 类型, 通过这个全限定名,通过反射,内省等机制 创建 SpringBean 对象
  • name 属性: 配置这个 Bean 的别名, 可以配置多个 "aa,bb,cc" 别名, 当 id 不存在时, 第一个别名转换为 BeanName
  • scope 属性: 配置 Bean 的作用范围, 单例还是多例,当引入 webmvc 依赖时,又会多出 request 和 session 的选项
  • lazy-init : 我们知道 beanFactory 是默认懒加载创建实例化 Bean 对象的, 而 ApplicationContext 是 一旦加载配置文件就会默认 实例化所有的 Bean 对象, 通过设置这个属性, 选择性的懒加载 某个 Bean, 这个只对 ApplicationContext 有效
  • destroy-method: 销毁生命周期方法, 指定 Bean 对象被销毁时执行的方法
  • init-method: 当 Bean 对象创建完毕后执行的方法(执行完构造函数之后)
  • autowire="byType": 设置 Bean 对象被自动注入时, 是通过类型来查找 Bean 还是先通过名字来查找 Bean
  • factory-methods : 如果说没有指定 factory-bean 这个属型的话, 说明这个 Class 对应的 Bean 中的某个方法是一个 工厂方法, 用于工厂方式创建某个 Bean , 如果指定了, 这个参数就是 被引用的工厂 Bean 的 工厂方法
  • factory-bean : 有这个属性的 Bean 说明是 一个采用工厂方式构建的 Bean, 这个属性就是 指定 哪一个是它的工厂, 指定这个属性后, 还需要指定 factory-methods 属性, 指定工厂中 哪一个方法是工厂方法.

# 4.2 针对属性的详细说明

# 4.2.1 Id, name, class 属性的 getBean(String beanName) 优先级问题

前三个属性可以作为,获取Bean的方式
1. 当我们配置了Id 属性时, Id优先级最大, singletonObjects的key 就是 这个Id, 作为BeanName
2. 没有配置Id, 但是配置了别名 "aa,bb,cc", 第一个别名, 会做为singletonObjects的key, 也就是BeanName
3. 前两个都没有配置, 则会默认生成一个key, 也就是这个类的全限定名 "com.snake.UserDao",作为 BeanName

情况 1:

img

img

情况 2:

img

img

img

情况 3:

img

# 4.2.2 scope 属性的详细说明

  1. 当我们的环境是一个基础的 Spring 环境时(只有 spring-context), scope 只能配置 单例或者 prototype(原型)

    singleton 和 prototype 的区别
    1. singleton 的Bean 这个容器只有一份, 并且如果使用ApplicationContext配置的, 就是容器一启动, 这个Bean就创建好了, 多次获取这个Bean都是一份, 既引用相同
    2. 如果是prototype 类型, 每次获取都是新的对象, 引用不同, 由jVM的gc 机制回收, 容器启动的时候, 并不会创建这个对象, 而是等到实际获取时才会创建一个Bean对象返回
    
  2. 当我们环境中 引入了 spring-webmvc 依赖后, 可以配置的选项新增了 request 和 session 两个选项

    也就是, 会将对应的 Bean, 放置到对应的 request'域或者 session 域中

# 4.2.3 lazy-init 属性

lazy-init 设置这个 Bean 是否懒加载, 也就是说,只有获取这个 Bean 时才会实例化这个对象, 这个属性只对 Applicationcontext 有效, 而 BeanFactory 无效, 因为这个属性就是设置 ApplicationContext 的 Bean 的,

对作用范围为 prototype 的 Bean 也无效, 因为 prototype 都是懒加载的, 而且不会放到 Spring 的单例池中.

如果某个 Bean 是 立刻加载的, 但是它被注入了一些懒加载的 Bean, 则这些懒加载的 Bean 也会被实例化, 注入到这个 Bean 中

# 4.2.4 init-method 和 init-destroy 属性

用来指定这个 Bean 的 初始化方法(稍微常用) 和 销毁方法(不常用).

  • 注意的是, 初始化方法会在 Bean 对象执行完构造方法后执行, 并且销毁方法只有显示的调用 ApplicationContext 的自类的一个 close() 方法, 才会调用, 其他的关闭都导致 Sprnig 容器没有机会去执行每个 Bean 的销毁方法.
  • 方法名可以随便取, 只要配置的时候选择就行.
  • 当 Spring 容器被关闭时, 其他的 Bean 也会被销毁, 但是只是可能没有调用对用销毁方法, Spring 容器就没了 但是 Bean 其实已经没了, 只是没有调用销毁方法.

img

img

# 扩展知识点: Spring 容器提供的 InitializingBean 接口

这个接口里面有一个 抽象方法 afterPropertiesSet() , 只要我们的 SpringBean 对象 实现了这个接口, 那么这个 Bean 在 执行完构造方法后就会 执行 afterPropertiesSet() 方法, 如果还设置了 init-method, 那么再之后执行 init-method 对应的方法.

也就是说, 想要在一个 Bean 中执行一些初始化操作, 有两种方法:

  • Bean 对象实现 InitializingBean 接口, 实现里面的 afterPropertiesSet(), 方法
  • Bean 对象 设置 init-method 属性, 执行初始化对应的方法

在依赖注入中, 只有被注入的 Bean 执行完 各种初始化方法, 才会被注入到其他的 Bean 中

设置了 init-method 和 initializingBean 接口的 Bean 的执行流程:

构造方法 -> afterPropertiesSet() -> init-method 方法

# 4.3 两种方式创建 Bean 对象 : 构造器 和 工厂 方式

我们知道, 我们都是通过 BeanFactory 或者 ApplicationContext 本质都是通过 Bean 工厂来创建 Bean 对象的, 但是实际上 Bean 工厂在创建 Bean 时, 他也可以采用 构造器的方式 或者 工厂的方式,

构造器的方式,就是 BeanFactory 通过反射, 调用构造方法来 创建对象, 之后返回. 也就是我们默认使用的方式

而工厂的方式,就是 BeanFactory 指定一个 工厂方法, 返回一个对象 .也就是 工厂套工厂

# 4.3.1 构造器方式

当我们在配置 Bean 时,默认使用的是 Bean 的无参构造方式, 但是当我们指定了 constructor-arg 子标签时, 并且没有配置成 工厂方式 那么就会去找到参数匹配的有参构造方法, 如果没有的话 即使存在无参数构造方法, 也会报错.

# 4.3.2 工厂方式 创建 Bean 对象

工厂方式创建 Bean 有三种方式 :

  • 静态工厂方式
  • 实例工厂方式
# 4.3.2.1 采用工厂方式的好处,相比构造器方式

我们采用工厂方式时:

  • 可以在工厂方法 返回 Bean 对象前, 就对这个对象做一些业务逻辑的操作
  • 针对第三方的依赖的 Jar 包, 我们可以在工厂方法里通过 获取这个对象 之后 返回, 将其加入到 Spring 容器中
# 4.3.2.2 静态工厂方式演示:

静态工厂方式是 就是说 工厂方法时静态的, 不需要将这个类实例化就可以调用 工厂方法

值得注意的是:

一旦 我们给Bean 标签 添加了 factory-method 属性, 并且没有指定 factory-Bean属性 . 那么这个Bean标签的 Class 对应的对象并不是一个 Bean, 不会添加到Spring容器, 而是作为一个工厂, 它所配置的 id 就是 工厂方式所产生的Bean 的 BeanName ,可以通过这个 BeanName来获取 工厂产生的Bean对象.

img

img

# 4.3.2.3 实例化工厂方式演示:

实例化工厂方式, 就是说方法不是静态的, 我们想要调用就必须, 先得有这个工厂, 那么我们就得在配置中, 先把工厂配置成一个 Bean, 之后在配置一个 Bean 标签, 指定它的制造工厂是谁, 以及哪个方法是 工厂方法

  • 在这种方式中, 工厂对象也在 Spring 容器中.

img

img

好处 :

  • 可以创建 Bean 前做一些业务逻辑
  • 可以将第三方的 jar 的工厂方法进行配置, 来获取 Bean
# 4.3.2.4 给工厂中的 构造 Bean 的 工厂方法 传递参数

constructor-arg 我们通过这个子标签来给 工厂方法设置参数, 这个标签的名字是 构造-参数, 它的意思是说 只要是构建 Bean 所需要的参数, 都可以用它来配置,. 并不是说只能是"构造方法"能用这个参数, 而是 能够"构造"Bean 的 方法都能用到这个参数, 所以不管是 工厂方法 还是 构造方法, 都可以用这个标签来设置参数.

# 给静态工厂的工厂方法设置参数:

img

img

# 给实例化工厂的方法设置参数:

注意,constructor-arg 标签是写在 被工厂创建的 Bean 对象里面的

img

img

# 4.3.2.5 通过实现 FactoryBean<T>接口规范, 延迟创建 Bean

如果我们使用 ApplicationContext 来获取 Bean, 那么配置的 Bean, 一旦 ApplicationContext 加载好配置文件, 就会全部的实例化, 所以所有没有设置 lazy-init 属性的 Bean, 所以在上面的工厂中, 我们获取的所有的 Bean 都是 立刻实例化的.

Spring 提供了 FactoryBean<T>接口 作为工厂 Bean 的规范, 实现了这个接口的工厂, 他们在创建 Bean 的时候采用的是 懒加载, 实现延迟创建 Bean第一次获取时, 才会去创建 Bean 对象, 并且 Bean 对象没有存储在 singletonObjects 这个 hashMap 中, 而是存储在了 factoryBeanObjectCache 这个 Map 对象

配置流程:

创建工厂类, 实现 FactoryBean<T> 接口, getObjectType() 返回对应 Bean 的接口类型

public class MyBeanFactory3 implements FactoryBean<UserDao> {
    public UserDao getObject() throws Exception {
        return new UserDaoImpl();
    }

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

配置文件中, 将工厂类设置 Bean

<!--    实现FactoryBean接口的Bean工厂-->
    <bean id="userDao2" class="com.snake.factory.MyBeanFactory3"></bean>

之后我们通过 getBean("userDao2") 来获取 UserDaoImpl 对象, 想到这里就会产生一个问题

为什么 ? getBean("userDao2") , 获取的是 UserDaoImpl 对象 , 而不是配置的 MyBeanFactory3 这个类对象呢?

因为Spring 对是实现了  FactoryBean<T>  接口的 Bean有特殊处理, Spring知道它是一个工厂, 用来产生Bean对象, 那么
配置这个工厂的 id 属性, 首先获取的不是 对应的工厂, 而是去获取 这个工厂生产的Bean对象
当第一次去访问时, factoryBeanObjectCache 是没有这个Bean对象的, 访问后就会 执行对应的工厂方法, 创建这个Bean对象,  又由于使用这个接口实现的工厂 创建的Bean都是单例的, 所以 在后续访问时, 就不会执行工厂方法, 直接从factoryBeanObjectCache 获取这个对象的引用.

singletonObjects Map 集合下的 对象是工厂:

img

factoryBeanObjectCache 是真正需要的 Bean 对象

img

# 4.4 Bean 标签下的子标签

  • property : 调用 Bean 对象的 Set 方法, 为属性赋值 , name 属性指的是 Set 方法后面的名字, value 指定的是 这个属性的值, 这个值也可以用 ref 来输入另一个 Bean 的 id, 来注入这个 Bean

    例如: 这里的 name 为 "xxx" 表明要去找 对应的 setXxx()方法 , ref 则是指向另一个 Bean

    注意实际上不要 将 set 方法取名为 Xxx ! , 而是取对应的属性名(首字母大写)

    img

  • constructor-arg : 构造参数, 不管是 通过构造方法还是 工厂方式,来创建 Bean 时都需要参数, 而只要配置了这个标签, 不管是这两种的 哪种方式都可以从这些标签中来获取对应的参数值, 来构造对象 不能理解为:只能对于构造方法使用!!!

img

img

# 4.5 Bean 的依赖注入配置

我们注入属性可以使用 property , 也就是 Set 注入, 或者使用 constructor-arg 标签就是 构造 Bean 对象方法 的 参数注入.

img

img

# 4.5.1. 注入普通类型

注意, 因为我选择使用 set 方式注入, 则需要先创建 两个普通类型的 set 方法, 这里以 String 类型举例,

img

# 4.5.1 注入 List 引用类型

以 set 方式注入, 针对 List 类型的对象, 我们需要在 property 的子标签 list 下 通过 bean 或者 ref 子标签来设置注入引用类型对象, 而注入普通类型变量只需要使用 <value> "123" </value> 子标签即可

img

如果使用的是 Set 方式注入, 那么记得要 创建对应字段的 set 方法, 构造的方法注入则编写对应的构造的方法

img

构造器注入和 set 注入大同小异

<bean id="userService5" class="com.snake.service.impl.UserServiceImpl">
        <constructor-arg name="user" ref="user1"/>
        <constructor-arg name="userList">
            <list>
                <ref bean="user1"/>
                <bean id="user2" class="com.snake.pojo.User"></bean>
                <bean id="user3" class="com.snake.pojo.User"></bean>
                <bean id="user4" class="com.snake.pojo.User"></bean>
            </list>
        </constructor-arg>
    </bean>

# 4.5.2 使用 Set 注入 Map, Set , Properties 类型

<entry> 标签可以 选择 key 或者 key-ref, value 属性同理

<bean id="userService5" class="com.snake.service.impl.UserServiceImpl">
        <property name="map">
            <map>
                <entry key="u1" value-ref="user1"> </entry>
                <entry key="u2">
                    <bean class="com.snake.pojo.User"/>
                </entry>
            </map>
        </property>
        <property name="set">
            <set>
                <ref bean="user1"></ref>
                <ref bean="user2"></ref>
            </set>
        </property>
        <property name="properties">
            <props>
                <prop key="name">snake</prop>
                <prop key="age">12</prop>
                <prop key="sex"></prop>
            </props>
        </property>
    </bean>

# 4.6 设置自动装配

Bean 标签中 有一个 autowire 属性, 它的作用是 为当前的 Bean 对象 中所有的 set 方法 去寻找 一个 Bean 进入装配

相比 上面我们进行手动的转配, 比如 UserServiceImpl 中有一个 UserDaoImpl, 我们需要 手动的将 UserDaoImpl 注入到 UserServiceImpl 中, 但是使用了 autowire 它可以 选择根据 名字(id => BeanName)或者 类型, 去帮我们进行注入.

img

注意 set 方法后面的名字 要与 id 属性一致, (首字母小写后)

img

当我们设置自动装配 通过 是 byType 表明, 是使用类型来匹配, 这个时候, 如果找到了多个 匹配的 Bean 可以去注入到属性中, 就会保错, 因为不知道装配哪一个

注意 : 不管是 通过名字 byName 或者 通过类型 byType 进行注入时, set 方法都不能省略, 通过类型匹配时, 是靠的 set 方法中传入的参数类型来寻找, 只要找到对应的类型的 set 方法就可以进行通过类型的注入

img

# 4.7 Spring 命名空间标签

Spring 的标签

img

# 4.7.1 通过<beans> 指定开发环境

针对不同的环境, 我们需要有不同的配置

<?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="userDao" class="com.snake.dao.impl.UserDaoImpl"></bean>
    <bean id="userService" class="com.snake.service.impl.UserServiceImpl"></bean>
    <beans profile="dev">
        <!--        开发环境-->
        <bean id="userDao1" class="com.snake.dao.impl.UserDaoImpl"/>
    </beans>

    <beans profile="test">
        <!--    测试环境-->
        <bean id="userService1" class="com.snake.service.impl.UserServiceImpl"></bean>
    </beans>
</beans>

我们通常在代码中指定 运行时的环境, 默认环境下的配置时全都加载的.. 并且 bean 标签放在 beans 标签的上面才不会报错

img

public static void main(String[] args) {
        //指定开发环境
        System.setProperty("spring.profiles.active","dev");
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring-Env.xml");
        Object userService = applicationContext.getBean("userService");
        System.out.println(userService);
    }

# 4.7.2 通过 <import> 标签 引入其他的分散的配置文件

在一个复杂的项目中, 针对每个模块可能有不同的配置文件. 我们不可能在启动容器时, 一个一个加载太麻烦了

我们可以在主配置文件中 加载其他的配置文件, 通过 import 标签

<!--导入其他的配置文件-->
    <import resource="Spring-database.xml"/>
    <import resource="Spring-redis.xml"/>

# 4.7.3 通过 <alias> 标签为 Bean 指定别名

就是把 Bean 标签的属性 name 单独拿出来弄了一个标签, 作用是几乎一模一样的. 他们都存出在 BeanFatcory 中的 aliasMap 集合 中.

  • name 属性: 给哪个 Bean 设置 别名, 写的是这个 Bean 的 id
  • alias 属性: 具体的别名, 配置多个别名时 需要多个 <alias> 标签, 而不能像 Bean 标签的 name 属性一样,可以使用逗号隔开一次性设置多个别名
    <bean id="userDao" name="aaa,bbb,ccc" class="com.snake.dao.impl.UserDaoImpl"></bean>
    <alias name="userDao" alias="xxx"/>
    <alias name="userDao" alias="yyy"/>
    <alias name="userDao" alias="zzz,ooo"/>

img

# 4.8 引入其他的自定义命名空间

例如:自定义命名空间 context 的标签:

img

需要设置其他命名空间约束后,才能使用

img

流程: 首先得引入对应命名空间的依赖包, 不然就算引入了命名空间, 也没有对应的类帮你进行解析

例如使用 SpringMvc 的 命名空间 mvc,

首先引入依赖

 <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.22</version>
        </dependency>

img

# 4.8 总结

Spring 命名空间 一共的四个标签有用, 分别是 <beans> <bean> <import> <alias>, 还有一个 描述标签 就不说了

比较常用的是 <bean> 标签, 我们一般都是在配置 Bean 标签的属性.

# 5. Spring 容器获取 Bean 对象的三种方法

  • 只传入 beanName 的方法, 要求 Bean 的 id 是唯一的
  • 只传入 Class 类型的 方法, 要求 这种类型的 Bean 在容器中只存在一份.
  • 两个都传入的, 则是要求 Bean 的 id 是唯一的, 传入类型只是为了, 返回时不需要强制转换.

img

# 6. Spring 配置 非自定义的 Bean

思路:

  • 首先需要你要了解获取的这个 对象, 它是怎么创造出来的, 是通过构造函数, 还是通过 工厂方法
  • 这个对象是否需要设置属性
<!--    使用传统jdbc方式 -->
    <bean class="java.lang.Class" factory-method="forName">
        <constructor-arg name="className" value="com.mysql.cj.jdbc.Driver" />
    </bean>
    <bean id="connection" class="java.sql.DriverManager" factory-method="getConnection" scope="prototype">
        <constructor-arg name="url" value="jdbc:mysql://www.venomsnake.cf:3306/health?characterEncoding=utf-8"/>
        <constructor-arg name="user" value="root"/>
        <constructor-arg name="password" value="*9236"/>
    </bean>
 <!--   配置德鲁伊数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="jdbc:mysql://www.venomsnake.cf:3306/?characterEncoding=utf-8"/>
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="username" value="root"/>
        <property name="password" value="*9236"/>
    </bean>

# 7. Bean 实例化的 简化版基本流程

首先 我们在配置文件中, 编写 Bean 标签, 当 ApplicationContext 启动后, 就会读取配置文件, 将里面的每一个 Bean 标签 封装成一个 BeanDefinition 对象, 记录了这个 Bean 标签的信息, 比如全限定名, id, 是否懒加载之类的属性 将这些 BeanDefinition 对象 存放到 beanDefinitionMap 集合中, 之后遍历这个集合, 使用反射技术, 根据 BeanDefinition 提供的全选定类名来, 实例化对象 并且存储在 singletonObjects 这个 Map 集合里面

img

img

# 8. Spring 的后处理器

img

  • BeanFactoryPostProcessor: Bean 工厂后处理器, 他是在工厂解析所有的 Bean 标签 生成所有的 BeanDefinition 对象,并填充到 BeanDefinitionMap 后, 将 Map 传递给 Bean 工厂后处理器, 也就是 执行一次.

  • BeanPostProcessor: Bean 后处理器, 是在每一个 Bean 对象实例化后, 填充到单例池 singletonObjetcs 之前执行

    每一个 Bean 对象执行一次, 也就是说这个方法会执行多次

Bean 工厂后处理器, 我们只需要编写实现这个接口的类, 并且加入到 Spring 容器中, 工厂后处理器类的方法就会被调用.

我们可以在这个方法中 人为的 添加修改 BeanDefinitionMap 集合的 对象.

# 9. Spring 的 Bean 工厂后处理器 (重点)

# 9.1 在 工厂后处理器中, 修改一个还未被实例化的 Bean 对象的 BeanDefinition 对象, 使其类型发生变化

  • 创建工厂后处理器类
public class MyBeanPostProcessor implements BeanFactoryPostProcessor {
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        //1. 获取BeanDefinitionMap集合 中的某个Bean对象
        BeanDefinition userDao = configurableListableBeanFactory.getBeanDefinition("userDao");
        //2. 我们设置它的 Class 类型, 使其变成 UserServiceImpl对象
        userDao.setBeanClassName("com.snake.service.impl.UserServiceImpl");
    }
}
  • 将工厂后处理器类 添加到 Spring 容器
<!--    将工厂后处理器添加到Spring容器-->
    <bean class="com.snake.BeanFactoryProcessor.MyBeanPostProcessor"/>
    <bean id="userDao" class="com.snake.dao.impl.UserDaoImpl"></bean>
  • 通过 getBean() 方法获取 userDao 这个 Bean

     public static void main(String[] args) {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring-Beans.xml");
            Object user =  applicationContext.getBean("userDao");
            System.out.println(user);
        }
    

    可以看到控制台的输出结果, 原来是 UserDaoImpl 的 类, 被我们修改为了 UserServiceImpl 类

    img

# 9.2 不通过 配置文件, 动态的通过 Bean 工厂后处理器中 注册(添加)一个 Bean 对象

我们只需要在 工厂后置处理器方法中, 在 BeanDefinitionMap 集合中 添加一个 BeanDefinition 对象即可

  • 编写实现对应的 BeanFactoryPostProcessor 接口 的类

    public class MyBeanPostProcessor2 implements BeanFactoryPostProcessor {
        public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
            System.out.println("处理器2执行");
            //1. 创建一个 BeanDefinition接口 的实现类对象,  常用的是 RootBeanDefinition这个类
            BeanDefinition beanDefinition  = new RootBeanDefinition();
            beanDefinition.setBeanClassName("com.snake.pojo.User");
            //2. 将 configurableListableBeanFactory 接口的对象 向下强制转换为 DefaultListableBeanFactory
            DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory;
            //3. 调用子类的 注册 Bean 的方法
            defaultListableBeanFactory.registerBeanDefinition("myUser",beanDefinition);
        }
    }
    
  • 将工厂后置处理器 添加到 Spring 容器管理

<bean class="com.snake.BeanFactoryProcessor.MyBeanPostProcessor2"/>
  • 获取动态注册的 Bean 对象
public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring-Beans.xml");
        Object user =  applicationContext.getBean("myUser");
        System.out.println(user);
    }

可以看到控制台的结果, 我们没有在配置文件中添加 Bean, 但是我们获取到了.

img

# 9.3 通过实现 BeanDefinitionRegistryPostProcessor (BeanFactoryPostProcessor 的子接口 ) , 来更加方便的注册一个 bean 对象

BeanDefinitionRegistryPostProcessor 接口 是 BeanFactoryPostProcessor 的子接口 , 它提供了一个方法, 方法里面的一个参数, 是一个 提供了注册 BeanDefinition 功能方法的 对象, 可以方便我们在 BeanDefinitionMap 集合中的注册操作.而不需要强制的进行转换.

实现类代码

public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    //子接口的方法
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        System.out.println("BeanDefinitionRegistryPostProcessor接口的 postProcessBeanDefinitionRegistry的方法执行");
        // 我们通过这个接口的实现类, 来注册一个Bean
        BeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClassName("com.snake.pojo.User");
        // 注册
        beanDefinitionRegistry.registerBeanDefinition("myUser2",beanDefinition);

    }
    // 父接口的方法
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        System.out.println("BeanDefinitionRegistryPostProcessor接口的 postProcessBeanFactory 的方法执行");
    }
}

将实现类添加到 Spring 容器

<!-- BeanDefinitionRegistryPostProcessor 接口的实现类  -->
    <bean class="com.snake.BeanFactoryProcessor.MyBeanDefinitionRegistryPostProcessor"/>
<!--    将工厂后处理器添加到Spring容器-->
    <bean class="com.snake.BeanFactoryProcessor.MyBeanPostProcessor"/>

获取动态注册的 Bean

public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring-Beans.xml");
        Object user =  applicationContext.getBean("myUser2");
        System.out.println(user);
    }

可以看到这些后处理器的的方法的执行顺序

img

# 9.4 引出工厂后处理器后, Spring 容器的处理流程

img

# 9.5 案例实战 : 使用 Spring 提供的 工厂后处理器扩展点, 实现 注解扫描, 配置 Bean

img

首先, 我们要自定义一个 注解 @MyComponent , 最终实现的功能, 就是 添加了这个注解的类自动被添加到 Spring 容器

//设置元注解, 配置这个注解的基本东西
@Target(ElementType.TYPE) // 这个注解只能加载类上
@Retention(RetentionPolicy.RUNTIME) // 这个注解作用在 运行时刻
public @interface MyComponent {
    String value();
}

在一些类上加上自定义的注解

@MyComponent("car")
public class Car {
}

@MyComponent("order")
public class Order {
}

@MyComponent("otherBean")
public class OtherBean {
}

导入扫描注解工具类

package com.snake.utils;

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.snake");
        System.out.println(stringClassMap);
    }
}

编写 BeanDefinitionRegistryPostProcessor 接口的 实现类, 在里面进行 注解的扫描, 并且依次注册成 Bean

public class BeanScanFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        //1. 扫描指定的包. 获取对应的Map集合
        Map<String, Class> stringClassMap = BaseClassScanUtils.scanMyComponentAnnotation("com.snake");
        //2. 遍历集合, 依次注册为Bean
        stringClassMap.forEach((beanName,beanClazz)->{
            BeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClassName(beanClazz.getName());
            beanDefinitionRegistry.registerBeanDefinition(beanName,beanDefinition);
        });
    }
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

    }
}

获取 这些添加了注解, 但是没有在配置文件中进行配置的 Bean

public class ApplicationContextTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring-Beans.xml");
        Object otherBean =  applicationContext.getBean("otherBean");
        Object order =  applicationContext.getBean("order");
        Object car =  applicationContext.getBean("car");
        System.out.println(otherBean);
        System.out.println(car);
        System.out.println(order);
    }
}

查看运行结果:

img

# 10. Spring 的 Bean 后处理器 (重点)

img

如果存在实现了 BeanPostProcessor 的接口的类, 并且这个类交给了 Spring 容器处理, 那么 每一个 Bean 在实例化后 , 就会执行 后处理器的 方法, 我们可以在这些加入 SingletonObjects 集合前, 在后处理器中 针对某些 Bean 进行一些操作.

许多框架技术, Spring 都是通过这个扩展点, 来整合第三方框架技术。

# 10.1 Bean 后处理器案例

案例 : 针对一个 userServiceImpl 的 Bean, 不在配置文件中 为其注入 userDao 实现类, 而是在 后处理器中进行添加

  • 编写实现 BeanPostProcessor 的 接口的 实现类
public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        // 判断这个类是否 是 UserServiceImpl 类型
        if (bean instanceof UserServiceImpl){
            System.out.println("BeanPostProcessor 的 postProcessBeforeInitialization方法");
            //我们给他设置一个属性
            UserServiceImpl userService = (UserServiceImpl)  bean;
            userService.setUserDao(new UserDaoImpl());
         }
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof UserServiceImpl){
            System.out.println("BeanPostProcessor 的  postProcessAfterInitialization 方法");
        }
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}
  • 将 BeanPostProcessor 接口实现类, 配置到 Spring 容器中

        <bean class="com.snake.BeanPostProcessor.MyBeanPostProcessor"/>
    <bean id="userService" class="com.snake.service.impl.UserServiceImpl"></bean>
    
  • 获取 userService 这个 Bean, 并打印里面设置的 UserDao 的 实现类

    public class ApplicationContextTest {
        public static void main(String[] args) {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring-Beans.xml");
            UserServiceImpl userService = (UserServiceImpl) applicationContext.getBean("userService");
            System.out.println(userService.getUserDao());
        }
    }
    
  • 查看输出结果

    img

# 10.2 引入 后处理器之后, Bean 在 加入单例池前执行哪些方法

  • 首先在 Bean 对象 需要执行 构造器方法, 将自己实例化出来,
  • 之后 如果存在 Bean 后处理器, 则会执行 Bean 后处理器的 前置方法 (postProcessBeforeInitialization) ,
  • 之后执行各种初始化方法, Bean 的 初始化方法有两个, 第一个是实现了 InitiazileBean 接口 后的 一个 afterPropertiesSet 方法() , 这个方法先执行
  • 之后执行 Bean 配置中的 Init-method 属性指定的 初始化方法,
  • 再之后就是执行 Bean 后处理器的 后置方法(postProcessAfterInitialization)

img

# 10.3 Bean 后处理器案例

img

使用 Bean 后处理器 结合 java 动态代理技术, 对 每一个 Bean 进行功能的增强, 原理就是 通过代理, 将原来的 Bean 进行包装, 返回一个增强的代理对象 Bean, 将这个 Bean 返回给 Spring 的 容器

# 案例流程

  • 编写 Bean 后处理器接口的实现类, 我们想要在 Bean 进行初始化后, 再进行我们的增强操作, 所以 我们实现 后置的处理器方法

所以, 一些初始化的方法, 是被代理之前就被执行的,像是 通过 init-method 或者 AfterPropertiesSet() 方法, 我们的代理对象不会为我们进行增强. 但要是 在 Bean 后置处理器的前置方法中就将 Bean 增强为了代理对象, 那么后面的初始化方法也会被增强

public class ProxyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        //使用动态代理技术, 增强原有的Bean, 在每一个Bean 原有功能的基础上, 增强 一个统计完成时间的功能
        Object proxyBean = Proxy.newProxyInstance(
                bean.getClass().getClassLoader(),  //类加载器
                bean.getClass().getInterfaces(),    //这个Bean所有实现的接口
                (proxy, method, args) -> {          //lambda 表达式
                    //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;
                });
        //返回的是我们的代理Bean对象, 将我们的代理对象添加到Spring容器
        return BeanPostProcessor.super.postProcessAfterInitialization(proxyBean, beanName);
    }
}
  • 将 Bean 后置处理器 和 配置一个 Bean 到容器中

    <bean class="com.snake.BeanPostProcessor.ProxyBeanPostProcessor"/>
        <bean id="orderService" class="com.snake.service.impl.OrderServiceImpl"/>
    
  • 获取 Bean , 并调用接口中的方法

    public class ApplicationContextTest {
        public static void main(String[] args) {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring-Beans.xml");
            OrderService orderService = (OrderService) applicationContext.getBean("orderService");
            orderService.show();
        }
    }
    
  • 我们可以看到我们的方法已经被增强, 说明我们的 Bean 已经被包装成了一个代理对象

img

# 11. 引入 Bean 后处理器后的 Bean 实例化流程 (重点)

img

  1. 读取并解析 XMl 文件, 将里面的每一个 Bean 标签 封装成一个 BeanDefinition 对象
  2. 将 BeanDefinition 对象 填充到 BeanDefinitionMap 集合
  3. 查找 Spring 容器中 所有实现了 BeanDefinitionRegistryPostProcessor 接口的 实现类, 调用里面的 接口方法 先调用自己接口中的一个 处理方法, 再调用父接口的 处理方法, 每一个类的这两个方法只执行一次
  4. 查找 Spring 容器中 所有实现类 BeanFactoryPostProcessor 接口的 实现类, 调用里面的 一个处理方法. 执行一次
  5. 使用 构造方法, 或者是 工厂的 制造方法 将 Bean 进行实例化,
  6. 查找 Spring 容器中,. 所有实现了 BeanPostProcessor 接口的 实现类的 前置方法, 针对每一个 Bean 执行一次
  7. 如果这个 Bean 实现类 InitializingBean 接口, 就执行 接口定义的 AfterPropertiesSet() 方法, 对 Bean 进行的一些初始化操作
  8. 如果这个 Bean 指定了 init-method 属性, 那么就去执行对应的 init-method 方法.
  9. 查找 Spring 容器中,. 所有实现了 BeanPostProcessor 接口的 实现类的  后置方法, 针对每一个 Bean 执行一次
  10. 将 Bean 对象添加到 单例池中 (SingletonObjects)

# 12 Bean 的生命周期 (重点)

img

一个完整的周期是从 对象的 创建到 销毁, 但是我们本次不研究销毁方法, 我们从 对象被构造开始研究,

Bean 后置处理器及其下面的流程已经学习过, 所以我们学习 Bean 实例的属性填充和 Aware 接口的 属性注入, 但是 Aware 并不是很重要, 我们注意学习 Aware 的思想.

img

# 12.1 Bean 实例的 属性注入.

当我们给 Bean 标签配置了 一个 property 属性时, 则这个 Bean 需要被设置一些属性, 而 BeanDefinition 对象, 也会记录 这个 Bean 对象所需要设置的 参数.

<bean id="userDao" class="com.snake.dao.impl.UserDaoImpl"/>
    <bean id="userService" class="com.snake.service.impl.UserServiceImpl">
        <property name="username" value="snake"/>
        <property name="userDao" ref="userDao"/>
    </bean>

当容器启动时, 将这个 Bean 标签 封装成一个 BeanDefinition 对象, 这个对象 同样封装了 这个 Bean 所需要注入设置的值

img

# 12.2 Spring 属性注入的三种情况

我们在配置 Bean 标签时, 有些 Bean 需要给它设置属性, 设置属性 会有三种情况

  • 注入普通属性, 例如 String, Int 或者存储基本类型的集合时, 直接通过 Set 方法的反射设置进去
  • 注入 单向对象引用属性时(例如一个 UserService 中 有一个 UserDao 对象的 引用, 而 UserDao 没有对 UserService 的引用), 从容器获取被引用的 Bean. 之后通过 Set 方法反射设置进去, 如果 此时的容器还没有这个 Bean 对象. 则先创建这个被注入对象 Bean 的实例(完成它的整个生命周期), 再进行注入操作.
  • 注入双向对象引用属性时, 就有些复杂了,. 会出现 循环引用(依赖) 的问题. 就是两个 Bean 对象 互相依赖. 下面的三级缓存来具体说明这个问题

# 12.3 Spring 的三级缓存

在上面我们引出了循环依赖的问题, 比如参考下面这张图:

假设我们现在只有一个 单例池 SingletonObject ,

img img img

# 12.4 Aware 接口

我们在一个 Bean 对象中没办法直接获取 框架中封装的一些对象. 但是提供了 Aware 接口. 我们就可以让 Spring 帮我们注入.

img

案例, 在一个普通的 Bean 对象中, 获取 BeanFactory 对象, ApplicationContext 对象, 以及自己在配置时才会生成的 BeanName, 然后就是获取 ServletContext 对象, 而这个对象必须在 Web 环境运行中,才能获取..

  • 编写一个 Bean 对象. 实现对应的 Aware 接口
package com.snake.service.impl;

import com.snake.service.FileService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.context.ServletContextAware;
import javax.servlet.ServletContext;

public class FileServiceImpl implements FileService , BeanNameAware, ApplicationContextAware, BeanFactoryAware, ServletContextAware {
    @Override
    public void setServletContext(ServletContext servletContext) {
        System.out.println(servletContext);
    }

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

    @Override
    public void setBeanName(String s) {
        System.out.println(s);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println(applicationContext);
    }
}
  • 获取 FileService 这个 Bean

    public class ApplicationContext {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring-Beans.xml");
            Object fileService = applicationContext.getBean("fileService");
        }
    }
    

    查看控制台结果

    img

# 13. 精简版的 最终的 Spring 的 Bean 的生命周期

img

img

img

# 14. Spring xml 方式整合第三方框架

# 14.1 Spring 整合方案

Spring xm 方式整合第三方的框架, 主要有两种方案:

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

# 14.2 Spring 整合 Mybatis

# 14.2.1 流程

  • 导入依赖

    <!--        mysql连接驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.30</version>
            </dependency>
    <!--德鲁伊数据库连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.2.11</version>
            </dependency>
    <!--        mybatis 基本-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.10</version>
            </dependency>
    <!--        mybatis 整合Spring的依赖-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis-spring</artifactId>
                <version>2.0.7</version>
            </dependency>
    <!--        Spring的jdbc包, 里面还依赖的了 Spring-tx 事务包, 所以我们不需要引入tx了-->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>5.3.22</version>
            </dependency>
    
  • 创建 spring 的 Dao 层配置文件

        <!--   配置德鲁伊数据库连接池 -->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="url" value="jdbc:mysql://www.venomsnake.cf:3306/Mybatis?characterEncoding=utf-8"/>
            <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
            <property name="username" value="root"/>
            <property name="password" value="*9236"/>
        </bean>
    <!--
        配置Mybatis的SqlSessionFactoryBean, 这是一个工厂类, 实现了BeanFactory<T> 接口
        帮我们将SQLSessionFactory加入Spring容器,
        需要给它设置一个 数据源, 通过set方式
    -->
        <bean class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    <!--
        配置Mybatis的 Mapper扫描器类  MapperScannerConfigurer,
        这个类它实现了 Bean工厂后处理器接口 BeanDefinitionRegistryPostProcessor ,
        在接口方法中, 帮我们将Mapper的实现类 添加到Spring容器. 所以我们需要将这个工厂后处理添加到Spring容器
        这个Bean需要一个 属性, 就是扫描哪一个包
    -->
        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <property name="basePackage" value="com.snake.mapper"/>
        </bean>
    
  • 编写 Mapper 和 Mapper.xml 文件

    Mapper 接口

    public interface UserMapper {
        List<User> findAll();
    }
    

    Mapper.xml 文件

    <?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.snake.mapper.UserMapper">
        <select id="findAll" resultType="com.snake.pojo.User">
            select * from t_user ;
        </select>
    </mapper>
    
  • 在 UserServiceImpl 中注入 UserMapper, 进行测试

    <!--
    userMapper这个Bean 已经通过 Mapper扫描里,帮我们在接口方法中创建并注册了
    对应的BeanDefinition对象, 所以UserMapper会动态的产生, 放在Spring容器中
    不需要我们进行Bean标签的配置
    -->
    <bean id="userService" class="com.snake.service.impl.UserServiceImpl">
            <property name="userMapper" ref="userMapper"/>
        </bean>
    
    public class UserServiceImpl implements UserService{
        private UserMapper userMapper;
    
        public void setUserMapper(UserMapper userMapper) {
            this.userMapper = userMapper;
        }
    
        public void show() {
            List<User> all = userMapper.findAll();
            for (User user1 : all) {
                System.out.println(user1);
            }
        }
    }
    

    # 14.2.2 原理讲解

img

# 14.3 Spring 整合 需要使用第三方命名空间的 标签的 技术

有许多的第三方的技术, 如何想要整合到 Spring 中, 可能会需要用到他们的自定义的标签, 例如 dubbo 技术, 而且 Spring 的 Context 的组件(spring-context)有些功能 也需要引入自定义标签从 才能使用.

# 14.3.1 使用 context 命名空间 加载配置文件, 并且使用 SpringEl 表达式,引用配置文件的值

  • 使用 context 标签, 你需要能够解析它的组件, 那么首先需要 引入 spring-context 的依赖, 不过都是 Spring 项目了 这个依赖就是必定引入的了

    <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.3.24</version>
            </dependency>
    
  • 在 Spring 配置文件, 引入 对应的命名空间, 配合 Spring 表达式使用

    img

  • 编写 需要加载的 properties 配置文件

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://www.venomsnake.cf:3306/Mybatis?characterEncoding=utf-8
jdbc.username=root
jdbc.password=*9236

# 14.3.2 源码解析后, 总流程总结

具体细节看视频吧...

img

# 14.3.3 案例:模拟一个框架开发者,在 Spring 容器中 整合自己的框架 对象, 作为一个 Bean

img

注意创建的是 META-INF 目录

  • 确定自己的命名空间名词, schema 虚拟路径. 标签的名称

    1. 命名空间设置为: snake
    2. schema 的虚拟路径设置为 : http://www.snake.com/schema/snake
    3. 标签的名字是 annotation-driven
    
  • 编写 schema 约束文件 snake-annotation.xsd (名字随意,但是需要是 xsd 文件), 在里面定义自己的约束

    记得把 xmlns 和 targetNamespace  替换成自己的 schema 虚拟路径!!

    <?xml version="1.0" encoding="UTF-8"?>
    
    <xsd:schema xmlns="http://www.snake.com/schema/snake"
                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                targetNamespace="http://www.snake.com/schema/snake">
    
        <xsd:element name="annotation-driven"></xsd:element>
    
    </xsd:schema>
    

    之后将它放到 Resources 的某个目录下

    img

  • 编写自己的命名空间处理器, 需要继承 NamespaceHandlerSupport 类, 我们需要重载里面的 init()方法

    其父类的 parser 方法会帮我们解析 xml 文件时, 根据对应的标签, 帮我们调用对应的 BeanDefinitionParser 的 parse() 方法

    /***
     * 自定义的命名空间处理器
     */
    public class SnakeNameSpaceHandler extends NamespaceHandlerSupport {
        @Override
        public void init() {
            //1. 针对自己定义的命名空间的每一个标签注册一个标签的解析器
            //由于我的命名空间只有一个annotation-driven标签就添加一个
            this.registerBeanDefinitionParser("annotation-driven", new SnakeBeanDefinitionParser());
        }
    }
    
  • 根据自己命名空间的每一个 标签 写一个对应的 BeanDefinitionParser 接口的 实现类 (SnakeBeanDefinitionParser), 实现里面的 parse()方法

    parse() 里面就是这个 自定义的命名空间的标签的主要处理逻辑了, 我们将这个标签的功能定义为, 帮我们 在 Spring 容器注册一个 实现了 Bean 后处理器接口的类

    public class SnakeBeanDefinitionParser implements BeanDefinitionParser {
        @Override
        public BeanDefinition parse(Element element, ParserContext parserContext) {
            //1. 这个解析器 , 对应的是解析 snake命名空间下的 annotation-driven 标签
            // 专门用于将自己创建的一个Bean后处理器 注册到Spring容器中
            BeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClassName("com.snake.BeanPostProcessor.SnakeBeanPostProcessor");
           //注册这个Bean, 参数为 beanName 和 BeanDefinition对象
          parserContext.getRegistry().registerBeanDefinition("snakeBeanPostProcessor",beanDefinition);
            return beanDefinition;
        }
    }
    
  • 编写一个 Bean 后处理器类, 我们的标签的功能是将它注册到 Spring 容器

    public class SnakeBeanPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("自定义的标签, 成功的将我的Bean后处理配置到了容器!");
            return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
        }
    }
    
  • 在 Resources 目录下 创建 META-INF 目录, 在里面创建 spring-handlers 和 spring-schemas 文件

    spring-handlers 文件

    schema 虚拟路径, 对应我们自定义的 命名空间处理器, 加上 \ 是为了转义: 

    http\://www.snake.com/schema/snake=com.snake.handles.SnakeNameSpaceHandler
    

    spring-schemas 文件

    指定命令空间的约束文件.

    http\://www.snake.com/schema/snake/snake-annotation.xsd=com/snake/annotation/config/snake-annotation.xsd
    
  • 在 Spring 的配置文件中, 引入我们的自定义命名空间, 并使用里面的标签

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:snake="http://www.snake.com/schema/snake"
           xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.snake.com/schema/snake
           http://www.snake.com/schema/snake/snake-annotation.xsd">
    <!--自定义的命名空间 snake, 以及自定义的标签 annotation-driven-->
        <snake:annotation-driven></snake:annotation-driven>
    </beans>
    
  • 启动 Spring 容器, 查看它是否帮我们在解析 xml 中的 自定义的标签, 可以看到执行结果

img

# 15. 基于注解的 Spring 应用

# 15.1 Spring 注解的 版本历史

img

# 15.2 Spring 的 注解说明

# 15.2.1 @Component 注解

这个注解的作用是, 替代了 xml 中的<bean id="" class> 标签. 只要加上这个注解的类自动就会被添加到 Spring 容器

如果没有给@component 设置 Value 属性的话, 那么 BeanName 就是这个类的类名的 首字母小写, 例如 UserDaoImpl 类 加上@Component 注解, 那么对应的 BeanName 就是 userDaoImpl

说明:

1. @component注解的作用就是, 标识这是一个Bean, 拥有这个注解的类, 如果被注解扫描器扫描到就会产生对应的Bean对象
2. @component 有一个 Value 属性, 这个属性就是配置 这个Bean的 BeanName的
值得注意的时, 相比XML方式, 我们给bean 标签设置 id 属性时 , id就是对应的BeanName,
如果没有id, 就是别名, 如果也没有别名, 那么就是这个类的 全限定类名.

如果没有给@component 设置Value属性的话, 那么BeanName 就是这个类的类名的 首字母小写

img

基本使用流程

  • 在 Xml 配置文件中, 配置 @Component 的注解扫描标签

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           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
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
    <!--
    		配置@Component注解的扫描, 扫描包及其子包
    		本质底层通过Bean工厂后处理扫描添加注解的类,
            创建BeanDefinition对象,添加到Map集合
    -->
        <context:component-scan base-package="com.snake"/>
    </beans>
    
  • 给类上加注解 @Component

    @Component("userDao")
    public class UserDaoImpl implements UserDao {
    }
    
  • 测试

    public class AnnotationConfigTest {
        public static void main(String[] args) {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            Object userDao = applicationContext.getBean("userDao");
            System.out.println(userDao);
        }
    }
    
  • 结果 : 我们通过注解成功的将类添加到了容器

    img

# 15.2.2 @Scope @Lazy @PostConstruct @PreDestory 注解

img

@Component() //BeanName 是 类名首字母的小写 : userDaoImpl
//@Component("userDao") //设置类名是 userDao
@Scope("singleton")
@Lazy(true) //默认为true
public class UserDaoImpl implements UserDao {
    public UserDaoImpl() {
        System.out.println("构造方法...");
    }
    @PostConstruct //表明这个是初始化方法
    public void init(){
        System.out.println("init方法...");
    }
    @PreDestroy //销毁方法, 需要显示调用的 容器的 close() 方法, 才有效.
    public void destroy(){
        System.out.println("destroy方法...");
    }
}

# 15.2.3 @Component 衍生出来的三个语义化注解

img

JavaEE 分层开发, 针对不同层的功能不同的 Bean 使用不同的注解, 提高代码的可阅读性, 而针对哪些不属于任何层的 Bean 则继续使用 @Component 注解.

# 15.2.4 依赖注入的 四个注解

img

# 1. @Value 注解

@Value 注解用于, 给类的字段或者方法中的参数注入 普通类型数据, 例如 字符串, 布尔类型. 数字等

但是, 如果将待注入的值,写到@value 注解里的话, 还不如直接给这些字段参数, 直接赋值, 所以, 我们一般都是搭配 context 命名空间提供的配置文件加载标签. 使用 Spring 的 El 表达式来使用

>Spring 只会给 一个 Bean 对象 进行属性的注入, 所以这些注入注解使用的前提 是这个类是一个 SpringBean, 你如果是 new 出来的对象, 是不会进行属性的设置的!

**@Value 对属性的注入是不需要写 set 方法的, 底层是通过暴力反射, 直接就设置了值 **

演示例子:

因为我们的 Sping 表达式使用了外置的配置文件的键值对, 所以需要配置 context 的配置文件加载器的标签

<!--    加载外置配置文件的键值对 到Spring容器-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

java 代码

@Component
public class DataSourceConfig {

    @Value("snake")  //这么写没有什么意义, 因为我们可以直接 name  = "snake"
    private String name;

    @Value("男性")  //在方法上给参数设置值,写死一个值, 没有什么意义
    public void setSex(String sex){
        System.out.println(sex);
    }

    @Value("24") // 会给每一个参数设置值为 24
    public void sum ( Integer n1, Integer n2){
        System.out.println(n1 + n2);
    }
    //我们一般都是搭配 Spring表达式加载外置属性的值
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;

    public void show(){
        System.out.println(name);
        System.out.println(url);
        System.out.println(driver);
        System.out.println(username);
        System.out.println(password);
    }
}
# 2. @Autowired 注解
  • @Autowired 注解 用于给 Spring 的 Bean 注入一个引用类型的 Bean, 前提是被注入的 Bean 在 Spring 容器中存在.

  • 可以加在字段或者 方法上.

  • @Autowired 注解   通过类型匹配注入, 如果存在多个相同类型的 Bean,则以字段的名字作为 BeanName 去按照名字匹配一个 Bean, 如果找不到则报错.

    报错:
    No qualifying bean of type 'com.snake.dao.UserDao' available: expected single matching bean but found 2: userDaoImpl,userDaoImpl2
    

演示:

@Service
public class UserServiceImpl implements UserService {
    @Autowired //通过类型匹配注入, 如果存在多个相同类型的Bean,则以字段的名作为BeanName去按照名字匹配一个Bean, 如果找不到则报错
    private UserDao userDao; //字段名是userDao, 如果类型有多个则以 "userDao"作为BeanName去匹配

    @Override
    public void show() {
        System.out.println(userDao);
    }
}
# 3.@Qualifier 注解
  • 用在字段或者方法上, (方法参数旁边也可以)

  • @Qualifier 注解 经常与@ Autowired 注解搭配使用, 因为 Autowired 是按照类型匹配的, 除了人为的修改字段的名字的方式 , 在存在多个同类型的 Bean 时无法,指定一个 Bean 进行注入. 而@Qualifier 注解则弥补了这个功能. 它可以设置 value 值, 那么 @Autowired 和 @Qualifier 就会去寻找类型匹配和 BeanName 匹配 value 值的的 Bean

演示:

@Service("userService")
public class UserServiceImpl implements UserService {
    @Autowired //通过类型匹配注入
    @Qualifier("userDaoImpl") //指定注入的BeanName是userDaoImpl
    //@Qualifier("userDaoImpl2")
    private UserDao userDao;

    @Override
    public void show() {
        System.out.println(userDao);
    }
}
# 4. @Resource 注解
  • 这个注解并不是在 Spring 的包提供的, 而是 在 javax.annotation 包下的, 是 Jdk 提供的, 而 Spring 也会去扫描这个注解作为一个 Bean
  • @Resource 注解没有 value 属性, 是通过它的 name 属性来指定被注入的 Bean 的 BeanName, 这个注解是通过名字来注入 Bean 的
  • 加在字段或者 方法上面, 不能加在方法参数旁边

演示:

@Service("userService")
public class UserServiceImpl implements UserService {
    @Resource(name = "userDaoImpl2")
    private UserDao userDao2;
    @Override
    public void show() {
    	System.out.println(userDao2);
    }
}
# 5. @Autowired 注解的 扩展功能

@Autowired 不光是可以将 Bean 对象注入到对应类型的字段,或者参数中, 还可以将 Bean 对象 添加到集合中

经过测试, 发现如果 Resource 不写 name 属性的话也能达到同样的功能

在这个时候, 按照类型匹配找到的多个同类型的 Bean 对象并不会报错, 而是将这些 Bean 对象注入到一个集合中

@Service("userService")
public class UserServiceImpl implements UserService {
    @Resource(name = "userDaoImpl2")
    private UserDao userDao2;

    @Autowired
    private void list(List<UserDao> userDaoList){
        System.out.println("Bean对象被注入到一个集合: "+userDaoList);
    }
    @Override
    public void show() {
    }
}

img

# 15.3 基于注解的 非自定义 Bean

img

img

注意点:

  • 我们通过@Bean 注解, 添加到方法上, 那么这个方法返回的对象就是被添加到 Spring 容器的 Bean 如果没有指定@Bean 的值, 那么这个 Bean 的 BeanName 就是方法名

  • Spring 会自动通过类型帮我们注入这个方法中所有需要的参数(如果是 Bean 的话)

    • 如果是普通类型的值, 但是没有写@Value 注解的话就会报错

    • 如果没有使用@Qualifier 注解来指定 BeanName 来注入某个 Bean,那么就会以类型进行注入,但是如果发现了多个类型匹配的 Bean, 就会以参数名做为 BeanName 来寻找一个 Bean,如果找不到就保错

    • 在这个地方的 @Qualifier 注解 可以单独使用, 不需要搭配@Autowired(也就是说可以省略) @Qualifier 注解 可以通过 BeanName 我们指定我们想要注入的 Bean 具体类型

    • 这个@Bean 所在的配置类 一定得被加入到 Spring 容器中, 也就是说得是一个 Bean

@Component //不能少
public class DataSourceConfig {
   @Bean("dataSource")
   public DataSource dataSource(
           @Value("${jdbc.driver}") String driver,
            @Qualifier ("userDaoImpl2") UserDao userDao,
            UserService userService){
        System.out.println(driver);
        System.out.println(userDao);
        System.out.println(userService);
        DruidDataSource dataSource = new DruidDataSource();
        return dataSource;
   }
}

# 15.4 使用 Spring 的配置类, 完全代替配置文件

与配置类相关的注解:

# 15.4.1 @Configuration

  • 添加这个注解的类作为一个配置类, 并添加到 Spring 容器(拥有@component 的功能)

# 15.4.2 @ComponentScan

  • 代替了 xml 中 context 命名空间的 注解扫描标签

    配置类上

    @ComponentScan("com.snake")
    

    XML 里

    <context:component-scan base-package="com.snake"/>
    
  • 使用静态数组, 来一次扫描多个包

    //@ComponentScan({"com.snake","com.saber"}) //可以传入静态数组的方式, 设置多个基本包
    
    <context:component-scan base-package="com.snake,com.saber"/>
    

# 15.4.3 @PropertySource

添加这个注解代替 context 命名空间的 property-placeholder 标签

xml 文件加载外置配置文件

<context:property-placeholder location="classpath:jdbc.properties,myApp.properties"/>

配置类上

@PropertySource("classpath:jdbc.properties")
-----------配置多个---------------
@PropertySource({"classpath:jdbc.properties","classpath:myApp.properties"})

# 15.4.4 @Import

导入其他类的配置, 并不要求这个类是一个 Bean 对象 , 代替了 xml 配置文件的 import 标签, 里面的配置相关的标签可以生效

而在原来使用 XML 的应用容器时, 这个使用了@Bean 标签的类, 则必须添加@Component 等注解成为一个 Bean 才行.

@Import(DataSourceConfig.class)//导入其他的 类的class, 作为一个配置类, 但是被导入的类,不需要写@Configuration 注解,或者 @Component注解

# 15. 4. 5 使用 AnnotationConfigApplicationContext 注解配置应用上下文类, 作为启动的容器

相比 XML 配置开发时. 使用的 ClassPathXmlApplicationContext 类. 我们在使用注解开发时需要使用 AnnotationConfigApplicationContext 类, 构造的参数则是配置类的 Class

 public static void main(String[] args) {
        //1. 我们使用全注解开发时, 没有配置文件, 而且我们需要注解开发专用的容器, 传入配置类的Class
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        Object dataSource = applicationContext.getBean("dataSource");
        System.out.println(dataSource);
    }

# 15.4.6 Spring 的其他的相关注解

  1. @Primary 设置 Bean 被注入的优先级

如果设置了@Primary 这个注解, 那么通过类型注入时, 如果存在多个相同类型的 Bean, 则优先注入@Primary 的 Bean

img

  1. @Profile 设置环境的配置类

    设置这个 Bean 的环境, 只有在对应的环境才回去加载这个 Bean

img

# 15.5 注解的解析原理

注解方式是通过, 注册一个后处理器, 后处理器中调用 扫描器的 doScan 方法() 将每一个扫描出来的@Component 注解,生成 BeanDefinition 对象, 进行注册

xml 方式, 则是通过对应注解扫描的标签. 在解析这个标签时, 通过对应的标签解析器, 调用对应的 parse() 方法, 这个方法在 使用 扫描器的 doScan() 方法,扫描每一个@Component 的对象, 生成 BeanDefinition 对象, 进行注册

img

# 15.6 使用注解方式整合第三方框架

# 15.6.1 注解方式整合 Mybatis

就是把 Xml 中的 三个标签, 转换为 Bean 的方式

千万注意 @MapperScan 注解的扫描的包一定是具体的某个 mapper 包, 不要直接扫描上一级的包,  因为这个注解会将所有的接口都当做一个 Mapper 来处理,添加到 Spring 容器, 对应的 BeanName 是 类名

@Configuration
@ComponentScan("com.snake")
@PropertySource("classpath:jdbc.properties")
@MapperScan("com.snake.mapper") //Mybatis的注解扫描, 注意一定要是mapper包下, 他会扫描所有的接口,将接口的类名作为BeanName 生成一个Bean, 不要扫描mapper之外的包!
public class SpringConfig {


    /**
     * 配置数据源
     * @param driver 驱动的全限定名
     * @param url 连接地址
     * @param username 用户名
     * @param password 密码
     * @return  数据源
     */
    @Bean
    public DataSource dataSource(
            @Value("${jdbc.driver}") String driver,
            @Value("${jdbc.url}") String url,
            @Value("${jdbc.username}") String username,
            @Value("${jdbc.password}") String password
    ){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    /**
     * 配置SqlSessionFactoryBean到Spring 容器, 这个Bean是一个FactoryBean接口的实现类, 作用是 产生SqlSessionFactory 到Spring容器.
     * @param dataSource 数据源
     * @return SqlSessionFactoryBean 对象
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }
}

# 15.6.2 源码解析

看视频吧

第三方框架可以通过注解来将自己整合到 Spring 中, 自己定义的注解中使用 @Import 注解, 而@import 设置的类会作为一个 Bean 添加到 Spring 容器中, 通过添加实现了指定接口的类到 spring 容器, ;例如 ImportSelector 或者 ImportBeanDefinitionRegistrar 接口.

ImportSelector 接口中的方法 返回一个 字符串数组, 装的是想要被添加到 Spring 容器的类的 全限定名.

ImportBeanDefinitionRegistrar 接口, 则是提供了一个方法. 方法的参数提供了注册 Bean 的 Registry 类. 可以通过它来很方便的添加类到 Spring 容器.

img

# 16. AOP 面向切面编程

# 16.1 简介

img

# 16.2 实现方案

img

# 16.3 模拟 AOP 的基础代码

  • 编写目标被增强类

    /***
     * 目标类
     */
    @Service("userService")
    public class UserServiceImpl implements UserService {
        @Override
        public void show1() {
            System.out.println("show1...");
        }
    
        @Override
        public void show2() {
            System.out.println("show2...");
        }
    }
    
  • 编写提供增强方法的增强类

    @Component
    public class MyAdvice {
        public void before(){
            System.out.println("前置增强...");
        }
        public void after(){
            System.out.println("后置增强...");
        }
    }
    
  • 通过后处理器类,使用代理技术, 对目标类增强

    注意,在这个 Bean 后处理器类中, 我们不能通过自动注入的方式来获取 MyAdvice 对象, 因为 MyAdvice 类也被加入到了容器, 它也会被后处理器执行, 而此时 MyAdvice 这个对象还没有成为一个 Bean, 这只是我的理解, 可能有问题...

    我们采用的是 ApplicationContextAware 接口来获取容器对象,之后在获取 bean 对象

    @Component
    public class MyBeanPostProcessor implements BeanPostProcessor , ApplicationContextAware {
        private ApplicationContext applicationContext;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            //过滤, 根据包名过滤, 只对com.snake.service.impl包下的类进行增强, 如果想要包含子包,就用字符串的以啥..开头的Api
            if(bean.getClass().getPackage().getName().equals("com.snake.service.impl"))
            {
                MyAdvice myAdvice = (MyAdvice) applicationContext.getBean("myAdvice");
                Object beanProxy = Proxy.newProxyInstance(
                        bean.getClass().getClassLoader(),
                        bean.getClass().getInterfaces(),
                        ((proxy, method, args) -> {
                            //前置增强
                            myAdvice.before();
                            Object invoke = method.invoke(bean, args);
                            //后置增强
                            myAdvice.after();
                            return invoke;
                        })
                );
                return BeanPostProcessor.super.postProcessAfterInitialization(beanProxy, beanName);
            }
            return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
        }
    }
    
    • 编写配置类, 将扫描上述类的包. 将其添加到 spring 容器中

    • 进行编码测试

      public class MyMockAOPTest {
          public static void main(String[] args) {
              ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
              UserService userService = (UserService) applicationContext.getBean("userService");
              userService.show1();
      
          }
      }
      

# 16.4 AOP 相关名词概念

img

img

# 16.5 Spring 的 AOP

# 16.5.1 XML 方式快速入门流程

img

  • 导入相关坐标

    AOP 相关的坐标有两个, 一个是 Spring-aop, 这个包被 Spring-context 提供了,

    而 aspectjweaver 包实际上并不是 spring 提供的, 是一个第三方的 AOP 的框架

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.24</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.8</version>
    </dependency>
    
  • 准备目标类 和 增强类, 配置 Bean

    <!--    配置目标类-->
        <bean id="userService" class="com.snake.service.impl.UserServiceImpl"/>
    <!--    配置通知类(增强类)-->
        <bean id="myAdvice" class="com.snake.advice.MyAdvice"/>
    
  • 配置切点表达式(既, 定义哪些方法将要被增强) 注意引入 AOP 名名空间

  • 配置 "织入" (就是配置切点被哪些通知方法增强,是前置增强还是后置增强)

<!--
引入aop命名空间, 配置aop
一个config可以配置多个切点表达式, 可以配置多个切面, 在切面标签中使用ref引用 Spring容器中的 对应的通知类, 子标签配置"织入"
-->
    <aop:config>
<!--        配置切点表达式-->
        <aop:pointcut id="myPointCut" expression="execution(void com.snake.service.impl.UserServiceImpl.show1())"/>
<!--        配置切面, ref引用的是spring容器中的的通知类-->
        <aop:aspect id="serviceAspect" ref="myAdvice">
            <aop:before method="before" pointcut-ref="myPointCut"/>
            <aop:after method="after" pointcut-ref="myPointCut"/>
        </aop:aspect>
    </aop:config>

# 16.5.2 切点表达式的配置

img

img

# 16.5.3 AspectJ 的 五种通知类型

img

img

img

# 16.5.4 使用 xml 时 配置 AOP 时, 针对一个方法, 各种通知的执行顺序

普通前置 -> 环绕前 -> 目标方法 -> 最终方法 -> 环绕后 -> 普通后置

    <aop:config>
<!--        配置切点表达式-->
        <aop:pointcut id="myPointCut" expression="execution(void com.snake.service.impl.*.*(..))"/>
<!--        配置切面, ref引用的是spring容器中的的通知类-->
        <aop:aspect id="serviceAspect" ref="myAdvice">
            <aop:before method="before" pointcut-ref="myPointCut"/>
            <aop:after-returning method="afterRunning" pointcut-ref="myPointCut"/>
            <aop:around method="around" pointcut-ref="myPointCut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="myPointCut" throwing="e"/>
            <aop:after method="after" pointcut-ref="myPointCut"/>
        </aop:aspect>
    </aop:config>

img

# 16.5. 5 AOP 两种 XML 配置方式的 <advisor> 方式

img

通过给一个通知类 实现 Advice 接口的 子接口, 例如 前置通知的接口, 实现前置通知的方法等

  • 前置通知接口 : MethodBeforeAdvice
  • 后置通知接口: AfterReturningAdvice
  • 环绕通知接口: MethodInterceptor
  • ......

经测试 环绕通知接口和 前置或者后置接口同时实现时, 只有环绕有效

xml

    <aop:config>
<!--        配置切点表达式-->
        <aop:pointcut id="myPointCut" expression="execution(void com.snake.service.impl.*.*(..))"/>
<!--        myAdvice2 实现了各种通知接口的方法-->
        <aop:advisor advice-ref="myAdvice2" pointcut-ref="myPointCut"/>
    </aop:config>

# 16.5.6 两种方式的区别

img

# 16.6 spring 的 xml 方式的 aop 原理

总结下 xml 配置 aop 的原理:

首先我们在 xml 配置文件中, 引入自定义命名空间 aop, 因此该命名空间对应的解析器的 init() 方法会执行, 在 init() 为 aop 命名空间的标签注册一个解析器, 而 aop:config 的解析器 中调用对应的 parser 方法, 在这个方法中注册了一个 间接实现的 BeanPostProcessor 的类:

img

在它的 After 方法中 将一个 Bean 对象进行生成代理, 并返回

# 16.7 Spring AOP 产生动态代理对象的两种方式

img

img

img

# 16.8 AOP 注解开发

# 16.8.1 注解开发快速入门

img

  • 引入 AOP 相关依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.24</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.8</version>
    </dependency>
    
  • 半注解, 在 xml 配置 AOP 相关注解的解析器

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:aop="http://www.springframework.org/schema/aop"
           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/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">
    <!--    Component扫描-->
        <context:component-scan base-package="com.snake"/>
    <!--    开启AOP自动代理, 解析我们的aop相关注解-->
        <aop:aspectj-autoproxy/>
    </beans>
    
  • 编写增强类 和 目标类, 将两个类加入到 spring 容器, 在 advice 类上加上 AOP 相关注解

    增强类(通知类):

    @Component
    @Aspect //切面配置
    public class AnnoAdvice {
    
        /**
         * service 包下 所有的类的所有方法被增强
         */
        @Before("execution(* com.snake.service.impl.*.*(..))")
        public void before(){
            System.out.println("前置增强...");
        }
    }
    

    目标类

    @Service("userService")
    public class UserServiceImpl implements UserService {
        @Override
        public void show1() {
            System.out.println("代码...");
    //        int i = 1 / 0;
        }
    
        @Override
        public void show2() {
            System.out.println("show2...");
        }
    }
    
  • 测试

# 16.8.2 注解配置 aop 详解

img

全注解开发配置:

  • 引入相关依赖

  • 编写目标类和 增强类(通知类)

    @Component
    @Aspect
    public class AnnoAdvice {
        /**
         * service 包下 所有的类的所有方法被增强
         *  @Pointcut 抽取切点表达式
         */
        @Pointcut("execution(* com.snake.service.impl.*.*(..))")
        public void myPointCut(){
    
        }
        @Before("myPointCut()") //引用被抽取的切点表达式
        public void before(){
            System.out.println("前置增强...");
        }
    
        @AfterReturning("AnnoAdvice.myPointCut()")
        public void afterReturning(){
            System.out.println("后置增强");
        }
    
        @Around("myPointCut()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("环绕前");
            Object proceed = joinPoint.proceed();
            System.out.println("环绕后");
            return proceed;
        }
    
        @AfterThrowing(pointcut = "myPointCut()", throwing = "e")
        public void afterThrowing(Throwable e){
            System.out.println("异常通知");
            e.printStackTrace();
        }
    
        @After("myPointCut()")
        public void after(){
            System.out.println("最终通知");
        }
    }
    
  • 编写 aop 的配置类

    @Configuration
    @ComponentScan("com.snake")
    @EnableAspectJAutoProxy //开启AOP自动代理
    public class SpringConfig {
    
    }
    
  • 测试

    public static void main(String[] args) {
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
            UserService userService = (UserService) applicationContext.getBean("userService");
            userService.show1();
        }
    

    img

注意执行顺序的不同, xml 方式与 注解方式不不同, 而好像从某个结点开始 aop 的版本, 执行顺序也不同...

# 16.8 原理

img

三种方式都是为了, 将 AspectJAwareAdvisorAutoProxyCreator 添加到 Spring 容器, 而 AspectJAwareAdvisorAutoProxyCreator 的父类实现了 BeanPostProcessor 接口, 在这个方法中对每一个 Bean 进行过滤, 产生代理对象

# 17. 基于 AOP 的声明式事务控制

# 17.1 了解一下编程式事务控制

img

img

# 17.2 基于 XML 的事务配置

本质上事务控制是通过 aop 实现的, 既对原有的业务方法进行增强, 在执行业务方法之前, 将事务开启, 在业务方法执行完毕后, 提交事务.

所以事务配置 除了 Spring 提供的 tx 包还需要 aop 相关的包.

Spring 为我们提供了, 专门用于数据库的 通知类(增强类 advice) ,这个类在 aop 中, 这个类是通过实现 aop 相关的方法接口来使用 aop 的, 所以我们在配置时需要使用 advisor 方式进行配置.

事务配置流程:

  • 导入相关依赖包

    springContex 容器. springJdbc 中 间接依赖了 Spring-tx 包, 所以我们不用显示的去引入, 我们还需要引入 aop 相关的包, 以及数据库等相关的包

    <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.3.24</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>5.3.24</version>
            </dependency>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.9.8</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>5.3.24</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis-spring</artifactId>
                <version>2.0.7</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.10</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.30</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.2.11</version>
            </dependency>
    
  • 在 xml 中使用 aop 命名空间, 在里面使用子标签 advisor 来配置事务

    1. 由于spring提供的事务advice类的功能还可以进行事务相关的配置, 没有像之前使用Bean 标签来讲advice类加入到spring容器, 而是使用tx命名空间的专用的advice标签来设置一个advice类,并添加到spring容器.
    2. 而advice类因为进行了事务等操作, 需要使用相关的 平台事务管理器接口(PlatformTransactionManager)的实现类, 例如底层是jdbc的方式需要使用 DataSourceTransactionManager 这个管理器实现类,mybatis底层也是jdbc 也是用这个.
    如果是使用的是hibernate数据库框架, 就需要使用它的事务管理器.
    

    xml 配置

    在配置时, 推荐先 按照 aop-> tx:advice -> bean 的顺序 可以联想出接下来的步骤

     <!--    配置advice使用的事务管理器 它需要数据源-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--    使用tx命名空间提供的配置一个数据库advice的标签-->
        <tx:advice id="myAdvice" transaction-manager="transactionManager">
            <tx:attributes>
                <tx:method name="*"/>
            </tx:attributes>
        </tx:advice>
    
        <!--   使用aop配置事务增强-->
        <aop:config>
            <aop:pointcut id="myServicePointcut" expression="execution(* com.snake.service.impl.*.*(..))"/>
            <aop:advisor advice-ref="myAdvice" pointcut-ref="myServicePointcut"/>
        </aop:config>
    
  • 进行代码测试

# 17.3 事务属性 详细配置说明

我们可以在 tx:advice 标签中, 通过 tx:method 标签详细的配置对应每一个切点(方法) 的事务属性.

比如事务的隔离级别, 超时时间, 是否只读. 事务的传播级别.

# 17.3.1 事务的隔离级别

Mysql 默认的 事务隔离级别是 REPEATABLE_READ

img

# 17.3.2 事务的传播行为

img

事务的传播行为就是 一个 业务方法 调用另一个业务方法时, 被调用的业务方法的 事务采取什么样的行为,

在图片中, 挂起 就是不使用的意思.

常用的有 REQUIRED (默认) 和 SUPPORTS

# 17.3.3 事务属性配置案例

    <!--    使用tx命名空间提供的配置一个数据库advice的标签-->
    <tx:advice id="myAdvice" transaction-manager="transactionManager">
        <tx:attributes>
<!--
    1.  method 标签的 name 属性指定的是切点的哪些方法,应用这个事务. 可以使用通配符*.
        例如添加相关的方法 addUser, addOrder 等业务方法,可以采用相同的业务属性 配置name="add*" 即可
    2. isolation 事务的隔离级别, default指的是采用数据库的设置的级别 , 而mysql 默认是 REPEATABLE_READ,
    3. read-only  对于数据库的操作只能是查询, 默认为false, 如果方法中对数据库进行更新等操作则报错
    4. timeout 超时时间, 单位为秒, 超时时,就取消这次操作. 默认是-1, 既永不超时.
        但是除了事务的超时, 数据库也有设置超时时间, 那个如果超时了, 也会出错
    5. propagation 事务的传播级别, 默认为 REQUIRED
-->
            <tx:method name="transAccount" isolation="REPEATABLE_READ" read-only="false" timeout="3" propagation="REQUIRED"/>
            <tx:method name="add*" isolation="REPEATABLE_READ" read-only="false" timeout="3" propagation="REQUIRED"/>
            <tx:method name="delete*"/>
            <tx:method name="update*"/>
            <tx:method name="select*"/>
<!--            按照从上到下的顺序依次查找匹配的切点方法的方法名,
                最后这个* 可以理解为默认, 对于没有匹配的方法名, 都采用这个事务.
                但是这个事务属性全都没有没有配置则,都是默认的
-->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

# 17.3.4 切点表达式 与 tx:method 标签中的 name 属性的区别

img

# 17.4 基于注解的 事务配置

主要是多了两个注解,

@Transactional 注解代替了 xml 中的 <tx:advice><aop:config> 的配置, 可以加在类上或者 方法上.

  • 当加在方法上时, 只是为这一个方法设置事务, 并且可以针对它设置 对应的事务属性, 比如 传播级别, 隔离级别. 超时时间,是否只读,等等.

  • 当加在类上时, 则说明这个类的所有方法都 被增加了事务,并且 如果这个类的方法没有加 @Transactional 注解则使用类上的事务属性, 但是如果 方法上也加了这个注解, 则采用方法自己上面的@Transactional 注解,

即采用就近原则, 来选择@Transactional

@Override
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
    public void transAccount(String incrAccountName, String descAccountName, BigDecimal money) {
        accountMapper.decreaseMoney(descAccountName,money);
        int i= 1/0;
        accountMapper.increaseMoney(incrAccountName,money);
    }

当我们使用了 @Transactional 注解时, 我们就需要使用 对应这个注解的 解析器, 如果是 xml 的 方式, 则添加对应的注解驱动

    <!--    配置advice使用的事务管理器 它需要数据源-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
<!-- 事务注解的解析器(注解驱动), 默认会在spring容器中 寻找 BeanName 为 transactionManager 的事务管理平台-->
    <tx:annotation-driven/>

当我们使用 xml 的 时候, 可以在 <tx:annotation-driven/> 中的 transaction-manager 属性来指定 平台事务管理的器的 BeanName.

<!-- 事务注解的解析器(注解驱动), 默认会在spring容器中 寻找 BeanName 为 transactionManager 的事务管理平台-->
    <tx:annotation-driven/>
<!--指定事务管理器的BeanName-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

如果是 全注解, 则在配置类上加上 @EnableTransactionManagement 注解, 并且 将 平台事务管理器配置成 Bean 添加到 spring 容器, 使用全注解来配置时, 可以将方法名 不设置为 transactionManager. 

@Configuration
@ComponentScan("com.snake")
@MapperScan("com.snake.mapper")
@PropertySource("classpath:jdbc.properties")
@ImportResource("classpath:spring-database.xml") //在配置类中导入xml配置文件
@EnableTransactionManagement //事务管理器的注解驱动.
public class SpringConfig {

    @Bean
    public DataSource dataSource(
            @Value("${jdbc.driverClassName}") String driverClassName,
            @Value("${jdbc.url}") String url,
            @Value("${jdbc.username}") String username,
            @Value("${jdbc.password}") String password
    ){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return  dataSource;
    }
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }

    /**
     * 将平台事务管理器添加到spring容器中
     * @param dataSource
     * @return
     */
    @Bean
    public JdbcTransactionManager transactionManager2(DataSource dataSource){
        return new JdbcTransactionManager(dataSource);
    }
}

但是 一旦是 xml 与 注解混合,比如 xml 配置了 事务的注解驱动那么在 配置类中 配置事务管理器的平台时,需要将方法名设置为  transactionManager, 除非使用 transaction-manager 属性指定了事务处理器的名字.

# 18. Spring 整合 Web 环境

# 18.1 复习 JavaWeb 的三大组件

img

# 18.2 Spring 手动整合 JavaWeb 环境.

如果我们想要在一个 JavaWeb 项目中整合 Spring, 我们需要怎么做?

首先 web 项目是分层开发的, 我们可以将 service 和 dao 层配置成 Bean, 之后在 Web 层(Servlet) 来获取. 但是如果, 我们针对每一个 Servlet 都书写, 创建 ApplicationContext 的代码, 之后通过 getBean() 方法获取 Bean. 的代码的话, 多个请求,多个 Servlet 会导致创建多个 Spring 容器.

针对以上问题. 我们需求是:

  • Spring 容器 (ApplicationContext) 只会创建一次.
  • 我们在任何 Servlet 中都可以获取.

我们知道 web 的 三大组件, Servlet , filter ,listener 都有他们的 生命周期

  • Servlet 默认是在第一次访问时,创建, 创建完成后加入到 Web 的容器中,
  • filter 则是 在 web 容器总启动时就创建. 在对象创建完毕后执行 init() 方法.
  • listener 则是监听四大域对象的变化来执行对应的方法.

解决方案 :

我们可以使用listener来监听 ServletContext 域, 这个域是整个web应用. 当Web应用启动时, 我们在里面创建Spring容器, 并将spring容器applicationContext加入到 ServletContext域 中, 则我们可以在Servlet中通过request来获取

配置 web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

<!--    配置全局参数. 可以被ServletContext获取-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
<!--    配置一个监听器-->
    <listener>
        <listener-class>com.snake.listener.ContextLoaderListener</listener-class>
    </listener>
</web-app>

监听器:

public class ContextLoaderListener implements ServletContextListener {
    private String CONTEXT_CONFIG_LOCATION="contextConfigLocation";
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext servletContext = sce.getServletContext();
        String initParameter = servletContext.getInitParameter(CONTEXT_CONFIG_LOCATION);
        String springConfig = initParameter.substring("classpath:".length());
        System.out.println(springConfig);
        //创建spring容器
        ApplicationContext app = new ClassPathXmlApplicationContext(springConfig);
        //将spring容器加入到 ServletContext域中
        servletContext.setAttribute("applicationContext",app);
    }
}

servlet

@WebServlet("/account")
public class AccountServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取spring容器
        ServletContext servletContext = req.getServletContext();
        ApplicationContext applicationContext = (ApplicationContext) servletContext.getAttribute("applicationContext");
        AccountService accountService = (AccountService) applicationContext.getBean("accountService");
        accountService.transAccount("saber","snake",new BigDecimal("500"));
    }
}

# 18.3 使用 Spring-web 包整合 web 环境

# 18.3.1 使用的是 xml 的 spring 配置文件

在上面通过手动的方式去整合 web 环境时, 我们需要手动书写一个监听器(服务器启动时创建 spring 容器), 以及一个 util 类(获取 spring 容器). 这些类 可以通过引入 spring 中引入 spring-web 包, 来使用 spring 提供的.

  • 引入 spring-web 依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.3.22</version>
    </dependency>
    
  • 书写一个 全局配置文件的参数, 和 配置一个 listener

    <!--    配置全局参数. 可以被ServletContext获取-->
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </context-param>
    <!--    配置spring-web提供的监听器-->
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>
    
  • 测试使用

@WebServlet("/account")
public class AccountServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取spring容器
        ServletContext servletContext = req.getServletContext();
        //使用spring-web包提供的工具类 WebApplicationContextUtils 获取 spring容器
        WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        AccountService accountService = (AccountService) webApplicationContext.getBean("accountService");
        accountService.transAccount("saber","snake",new BigDecimal("500"));
    }
}

# 18.3.2 使用的是注解配置类

我们之前在 web.xml 文件中通过 全局参数, 配置了 xml 的 spring 的配置文件位置, 但是实际上当我们是使用全注解开发时, 就需要加载的是配置类,而不是一个 配置文件. spring -web 也提供了这种方式, 我们需要 自定义一个 webApplicationContext , 在这个对象初始化时, 将配置文件类, 注册到 spring 容器.

  • 编写 web.xml , 指定全局参数, contextClass 的值

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
    
    <!--    加载自定义的applicationContext时, 全局参数-->
        <context-param>
            <param-name>contextClass</param-name>
            <!--自定义的一个AnnotationConfigWebApplicationContext的自类, 这个类中注册配置类 -->
            <param-value>com.snake.config.MyAnnotationConfigWebApplicationContext</param-value>
        </context-param>
    
    <!--    配置全局参数. 可以被ServletContext获取-->
    <!--    <context-param>-->
    <!--        <param-name>contextConfigLocation</param-name>-->
    <!--        <param-value>classpath:applicationContext.xml</param-value>-->
    <!--    </context-param>-->
    <!--    配置spring-web提供的监听器-->
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>
    </web-app>
    
  • 自定义 web 容器

    public class MyAnnotationConfigWebApplicationContext extends AnnotationConfigWebApplicationContext{
        public MyAnnotationConfigWebApplicationContext() {
            super();
            //将配置类注册到spring容器
            this.register(SpringConfig.class);
        }
    }
    

    # 18.4 根据上面引出 的问题. 和 Spring 实际整合 webmvc 框架的思想

    img

    img

    img

# 19. SpringMVC

# 19.1 概述

Spring mvc 提供了一个 前端控制器, 它本质是一个 Servlet , 它将我们的所有请求进行汇集, 帮我们做了一些 原本大量 Servlet 共有的行为, 比如获取请求参数, 将参数转为为 javaBean 对象, 将数据进行返回前,转换为 Json 格式, 等等, 这些东西都是 所有的 Servlet 共有的, 于是 前端控制器 将这些共有的行为抽取出来.

我们只需要关注每一个 Servlet 不同的行为, 既 专门的业务处理逻辑, 将这些逻辑代码封装成一个个的方法, 作为一个 Controller, 前端控制器获取请求后将对应的参数等东西转发给 一个个 处理业务逻辑的 Bean 对象

img

# 19.2 快速入门

img

  • 导入 spring-webmvc 的坐标

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.22</version>
    </dependency>
    

    img

  • 将前端控制器 Servlet 在 web.xml 中进行配置. 因为本质上他是一个 Servlet 对象. 并且 访问路径可以设置为 /, 或者 /*

    前段控制器这个 Servlet 在创建完成时, 会执行 初始化的方法, 而在这个方法中会创建 spring 容器. 因此我们需要传入配置文件

    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
    		http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" id="WebApp_ID" version="4.0">
    <!--    配置前端控制器 这个Servlet-->
        <servlet>
            <servlet-name>DispatcherServlet</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--        在前端控制器初始化时, 创建spring容器-->
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:spring-mvc.xml</param-value>
            </init-param>
    <!--        服务器启动时, 创建Servlet-->
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>DispatcherServlet</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    </web-app>
    
  • 编写 Controller 并配置映射路径

@Controller
public class AccountController {
    @RequestMapping("/show")
    public String show(){
        System.out.println("show...");
//        在传统的mvc模式开发中, 我们需要返回视图给用户.
//        现在当前端使用了 html + ajax 时, 我们就不需要了返回视图了
        return "/index.jsp";
    }
}

# 19.3 Springmvc 提供的 组件

img

img

# 19.4 前端控制器 初始化 各个组件的一些细节

前端控制器 DispatcherServlet , 在这个类中有一些集合属性, 每个集合都保存了一种组件的 对应接口的实现类.

img

当前端控制器创建完毕后, 就会执行对应组件的初始化方法.

在这个方法中对各组件进行初始化

protected void initStrategies(ApplicationContext context) {
    this.initMultipartResolver(context);
    this.initLocaleResolver(context);
    this.initThemeResolver(context);
    this.initHandlerMappings(context);
    this.initHandlerAdapters(context);
    this.initHandlerExceptionResolvers(context);
    this.initRequestToViewNameTranslator(context);
    this.initViewResolvers(context);
    this.initFlashMapManager(context);
}

# 19.4.1 处理器映射器 在初始化的一些细节

首先寻找 spring 容器中是否含有 实现 HandlerMapping 接口的 Bean ,没有的话就采用默认的策略 , 既加载 webmvc 包下 DispatcherServlet.properties 文件 中定义的

各个组件的 对应接口的实现类, 封装到对应组件的集合中

img

DispatcherServlet.properties 文件的 采用的组件实现类

## Default implementation classes for DispatcherServlet's strategy interfaces.
## Used as fallback when no matching beans are found in the DispatcherServlet context.
## Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
	org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
	org.springframework.web.servlet.function.support.HandlerFunctionAdapter


org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

# 19.5 注解

  • @RequestMapping :设置访问路径

    1. value  属性 是一个字符串数组, 可以绑定多个请求路径
    2. method 属性 则用来限制访问的请求方式, 是一个枚举类型
    
  • @GetMapping , @PostMapping 等 方法映射注解

    相当于 @RequestMapping注解, 但是指定了对应的请求方式
    
  • @RequestParam : 对参数做一些操作

    1. 可以指定 请求参数与方法参数的映射, 但请求参数与方法参数不一致时使用
    2. 可以设置参数是否是必要的, 以及设置默认值, 只要写了这个注解,说明这个参数就是必要的.
    3. 在接收List 或者 Map 类型的数据时,需要使用这个注解才能正确的接收
    	List 类型, 不使用这个注解, 则报错
    	Map 类型,不使用这个注解,则接收不到值, size 为0
    
    @RequestParam(value = "username", required = true , defaultValue = "snake") String name
    
  • @RequestBody 注解: 获取所有的请求体数据, 并且封装到方法中的一个参数里面

    1. 注意 @RequestBody, 只是当我们想要原封不动的获取全部请求体数据时才会使用. 并且只能对一个方法参数使用.如果一个方法中使用了 多次 @RequestBody 注解, 那么只有第一次能够获取到请求体的数据, 后面几次无法获取到, 并且前端会报错400, 因为输入流已经关闭?
    但是 如果是在上传多个文件时(其实也可以直接一个文件数组), 针对每一个文件参数可以使用一个@RequestBody, 并不会报错... 这是个特例?
    2. @RequestBody注解, 我们通常用来传递JSON格式数据, 将JSON格式数据, 通过 Jackson 或者 FastJson 工具, 转换为 Bean对象. 因为前端传递JSON格式数据时, 一般都是一个对象的数据. 我们后端只需要一个方法参数来接受
    
    @PostMapping("/upload3")
        public String upload2(@RequestBody MultipartFile myFile1,@RequestBody MultipartFile myFile2) throws IOException {
            System.out.println(myFile1);
            System.out.println(myFile2);
            String location = System.getProperty("user.dir");
            myFile1.transferTo(new File(location+"/"+myFile1.getOriginalFilename()));
            myFile2.transferTo(new File(location+"/"+myFile2.getOriginalFilename()));
            return "/index.jsp";
        }
    
  • @PathVariable 注解 : 在使用 Restfull 风格时, 通过此注解对路径的参数进行映射

    //路径: localhost/user/10/snake
        @GetMapping("/{id}/{name}")
        public String param1(@PathVariable("id") Integer id,@PathVariable("name") String name){
            System.out.println("id:"+id);
            System.out.println("name:"+name);
            return "/index.jsp";
        }
    
  • @RequestHeader 获取请求头,封装到参数, 当缺少 value 属性时则获取所有请求头,需要使用 map 集合接收

     //获取所有请求头
        @RequestMapping("/allHeader")
        public String head(@RequestHeader Map<String,String> map){
            map.forEach((k,v)->{
                System.out.println(k + " ==> "+ v);
            });
            return "/index.jsp";
        }
        //获取某个请求头
        @RequestMapping("/head1")
        public String head(@RequestHeader("Accept-Encoding") String accountEncoding){
            System.out.println(accountEncoding);
            return "/index.jsp";
        }
    
  • @CookieValue:获取某个 Cookie 值

     @RequestMapping("/cookie")
        public String cookie(@CookieValue(value = "JSESSIONID",defaultValue="") String jsessionid){
            System.out.println(jsessionid);
    
            return "/index.jsp";
        }
    
  • @RequestAttribute 和 @SessionAttribute 注解 : 从域中获取数据, 并给参数赋值, 相比先获取域对象, 再获取值方便些

    //往请求域中设置数据
        @RequestMapping("/set")
        public String set(HttpServletRequest request, HttpSession session){
            User user = new User();
            request.setAttribute("id",123);
            session.setAttribute("user",user);
            return "get"; //跳转到下面的请求
        }
    
        //从请求域中获取数据
        @RequestMapping("/get")
        public void requestAttr(@RequestAttribute("id") Integer id, @SessionAttribute("user") User user){
            System.out.println(id);
            System.out.println(user);
        }
    
  • @CrossOrigin 运行跨域, 需要设置 value 属性,来指定允许的来源, 会在响应头中添加一个 响应头

    img

# 19.6 使用 SpringMVC 接受参数的一些细节

  • 接受数组类型时, 可以使用 java 的数组或者 List 来接受, 但是 List 参数 需要使用@RequestParam 注解 ,除了 GET 方式, 使用@RequestBody 方式也可以接收

    // 请求路径 http://localhost/quick/param4?hobby=game&hobby=music&hobby=animate
        // 接受数组类型数据. 使用List集合接收
        // 指的注意的是: springmvc 会把 List 当成一个Bean 对象, 就像是一个 User对象一样.
        // 他会尝试去将List创建出来, 但是这是一个接口, 所以会报错:
        //  java.lang.IllegalStateException: No primary or single unique constructor found for interface java.util.List
        // 我们需要使用 @RequestParam注解告诉它, 只需要帮我们封装就行, 注意参数名一致
    	// 除了GET方式, 使用@RequestBody方式即可
        @GetMapping("/param4")
        public String param4(@RequestParam List<String> hobby){
            for (String s : hobby) {
                System.out.println(s);
            }
            return "/index.jsp";
        }
    
  • 接收 Map 集合时, 也需要使用 @RequestParam 注解,不使用的话不报错,但是无法接收到值.既 size 大小为 0

    // 请求路径 http://localhost/quick/param5?id=1&username=snake&age=20
        // 使用Map 结合 接收多个参数 , 请求参数名作为key, 请求值作为值
        @GetMapping("/param5")
        public String param5(@RequestParam Map<String,String> map){
            System.out.println("size:"+map.size());
            map.forEach((k,v)->{
                System.out.println(k +" ==> " + v);
            });
            return "/index.jsp";
        }
    
  • 接受 基本数据类型时, 例如 int ,参数必须传递, 因为如果不传递的话只就是 null, 而基本类型数据接受 null 值 , 就会报错, 所以我们基本上使用, 基本类型的包装类来接收这些数据

  • 接收 引用类型数据时, 例如 User , 等, 只要参数名匹配, 就可以自动的将参数封装到 对象的属性里面, 当 对象里面还嵌套着对象时,

    请求参数, 需要使用 符号 "." 来, 绑定.

    注意 对象需要重写 get 和 set 方法

    // 请求路径 http://localhost/quick/param6?id=1&username=snake&age=20&address.city=henan&address.street=yuxinlu
        // 接受 对象类型, 例如User, 他会根据属性名字进行匹配, 将数据进行封装
        // 如果对象中的属性 还有对象, 那么前端传递参数时, 需要使用 "." , 例如 address.city
        // 要求是 对象拥有对应属性的set方法.
        @GetMapping("/param6")
        public String param6(User user){
            System.out.println(user);
            return "/index.jsp";
        }
    
  • 使用 Post 方式接收 x-www-form-urlencoded 格式 的 普通类型数据时, 只需要注意 参数名一致, 不要加上 @RequestBody 注解.

# 19.7 将 Jackson 工具集成到 SpringMVC, 实现自动转换 JSON 格式数据为对象.

当前端传递 json 格式数据时, 我们需要使用@RequestBody 注解获取完成的请求体数据, 并将数据手动转换为 对应的封装对象, 比如 User,List 等等, 有些麻烦.

我们可以将 JackSon 或者 FastJson 这两个 json 转换工具集成到 springMvc 中, 我们知道 SpringMVC 的 组件中 有一个 处理器适配器, 当我们没有在 Spring 容器中配置它等等实现类时, 就会去采用默认的策略 使用默认的实现类, 但是我们现在想要使用他的另一个实现类 RequestMappingHandlerAdapter, 并将 Jackson 工具的转换器, 添加到处理器适配器中的 消息转换器集合中, 帮我们转换 Http 请求的数据.

在后面的学习中, 我们可以通过一个注解驱动,自动帮我们做这件事情..

  • 导入 Jackson 工具的依赖

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.1</version>
    </dependency>
    
  • 将处理器适配器配置成 Bean, 并在消息转换器集合中添加 Jackson 提供的消息转换器

    <!--    使用自定义的处理器适配器. 并在里面集成jackson的json转换工具-->
        <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
            </list>
        </property>
    
  • 测试使用..

     //集成Jackson的消息转换器后, 自动将json格式数据封装为Bean对象
     //注意@RequestBody注解不能省略
        @PostMapping("/param3")
        public String param3(@RequestBody User user){
            System.out.println(user);
            return "/index.jsp";
        }
    

    成功的自动封装:

    img

# 19.8 接收上传文件

前端要想上传文件的三个条件

  1. 方式为 Post, 因为 Get 方式不能在 url 位置中传递大量的数据(浏览器或者服务器的限制, 而不是 HTTP 协议的规定限制)
  2. 表单数据格式 为 MultPart/From-data
  3. 上传时有文件名

后端如果使用的是 SpringMVC 框架, 默认是没有上传文件的解析器的, 需要我们手动配置, 后面可以通过配置一个注解驱动帮我们完成这件事

img

  • 在 spring 配置一个上传文件解析器的 Bean, 注意 id 必须为 multipartResolver

        <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
     <!--     文件名的编码格式. 默认是iso8859-1-->
            <property name="defaultEncoding" value="UTF-8"/>
    <!--        单次上传的每个文件上传的最大大小,单位为字节 5M-->
            <property name="maxUploadSizePerFile" value="5242880"/>
    <!--        单次最大的上传多个文件总大小,单位为字节 8M-->
            <property name="maxUploadSize" value="8388608"/>
        </bean>
    
  • 因为 CommonsMultipartResolver 底层用的是 Apache 的 commons-fileupload 包, 所以我们需要因入依赖

    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>
    
  • 编写测试代码

    注意上传表单的 name, 属性和参数名一致

    @PostMapping("/upload")
        public String upload(@RequestBody MultipartFile myFile) throws IOException {
            //获取当前路径, 也就是 java命令的执行路径
            String location = System.getProperty("user.dir");
            System.out.println(location);
            //将文件转储到一个位置
            myFile.transferTo(new File(location+"/demo.png"));
            System.out.println(myFile);
            return "/index.jsp";
        }
    
  • 上传测试

img

# 19.9 静态资源访问

使用 SpringMVC 后, 无法直接访问静态资源的原因:

在传统的javaWeb开发中, 直接访问静态资源(jsp,不算是静态资源文件)是没有事的. 但是当我们使用SpringMVC框架之后, 我们配置的访问路径是 缺省的, "/ "
导致覆盖了 Tomcat原有的DefaultServlet, 它的访问路径也是 "/", 并且原有的这个DefaultServlet 是拥有寻找静态资源并返回的能力的. 但是当我们替换为SpringMVC的 前端控制器后, 它是没有访问静态资源的能力的.

访问静态资源的三种方式

# 方式一 : 重新激活 DefaultServlet 的 访问能力

通过设置更加精确的路径, 来让默认 Servlet 来处理

img

# 方式二 和三:

注意第二种方式, 中 mapping 是映射路径, 是虚拟路径, 当 url 中匹配时, 再去后面的实际路径去寻找

img

# SpringMVC 的 DispatchServlet 的初始化流程

# 容器创建

父子容器? 整合了 Spring 环境, 和 SpringMVC 环境的项目, 存在两个容器. Spring 容器和 SpringMVC 容器.

在 Spring 容器中 getBean() 能否获取到 SpringMVC 容器的 Bean?

不能, 因为 SpringMVC 是子容器, 而 SpringMVC 能否获取 Spring 容器的 Bean, 可以, 因为 Spring 容器是 SpringMVC 的父容器, 当子容器不存在这个 Bean 时, 回向父容器中尝试获取.

# 初始化 9 大组件

当 SpringMVC 容器创建后, 会执行容器相关的配置和刷新. configureAndRefreshWebApplicationContext(cwac) 在里面, 将会进行一些 Bean 工厂后处理器, 和 Bean 后处理器.

然后回完成后, 就会触发一个 上下文刷新完毕的事件. img 之后有一个上下文监听器, 监听到这个事件后开始执行一个函数onRefresh 在这个函数中, 使用了模版方法设计模式, 调用了另一个初始化函数, 而这个函数, FrameworkServlet 并没有写代码.

而是被子类 DispatchServlet覆盖, 在这个方法中又调用了initStrategies方法. img

在该方法中, 初始化,注册 9 大组件.

# 以 HandlerMapping 处理器映射器举例

它的初始化, 会判断用户, 是否向容器中注入了 自己的处理器映射器的 实现.

如果没有, 用户如果通过配置文件(xml 方式), 开启了注解驱动<mvc:annotation-driven>, 他会帮我们给 SpringMVC 的一些组件, 提供一些实现, 放入到 Spring 容器. 而注解方式@EnableWebMVC的作用与 注解驱动的作用相同.

他们对于处理器映射器组件, 会提供四个实现的 Bean, 分别有

image

如果没有使用@Enable注解, 没有使用注解驱动标签, 也没有使用自己想 Spring 容器中添加这个处理器适配器的 Bean.

就会采用默认的策略方法, img 这个方法会去 找一个DispatchServlet.properties的文件, 这个文件中, 对于处理器适配器则 提供了 三个实现的 Bean.

img

TIP

因此我们可以自定义 9 大组件, 然后将对应的实现添加到 Spring 容器中, DispatchServlet 在加载完成后, 初始化 9 大组件的事件触发后, 就可以使用我们自己自定义的组件.

# SpringMVC 的请求处理流程

img

# 请求是如何交到DispatcherServlet手中的

我们知道, 在我们学习 javaWeb 时, 所有的请求都会被Servletservice(ServletRequest,ServletResponse)方法处理.

而 SpringMVC 中 使用了DispatcherServlet来充当这个Servlet, DispatchServlet 间接实现了 Servlet这个接口, Servlet接口中, 有如下几个方法: img DispatcherServlet的初始化流程就是从 init() 开始的, 而DispatcherServlet处理请求的开始也是从service开始的.

  1. Servlet(接口)#service (javax.servlet)

  2. GenericServlet(抽象类)#service (javax.servlet)

  3. HttpServlet(抽象类)#Service (javax.servlet.http) img

    • 这个方法主要是将 reqres 分别转化为了 HttpServletRequestHttpServletResponse 类型 然后调用了受保护的 protected Service(HttpServletRequest req , HttpServletResponse rep)方法
    protected void service(HttpServletRequest req, HttpServletResponse resp)
         throws ServletException, IOException
     {
         String method = req.getMethod();
    
         if (method.equals(METHOD_GET)) {
             long lastModified = getLastModified(req);
             if (lastModified == -1) {
                 // servlet doesn't support if-modified-since, no reason
                 // to go through further expensive logic
                 doGet(req, resp);
             } else {
                 long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                 if (ifModifiedSince < lastModified) {
                     // If the servlet mod time is later, call doGet()
                     // Round down to the nearest second for a proper compare
                     // A ifModifiedSince of -1 will always be less
                     maybeSetLastModified(resp, lastModified);
                     doGet(req, resp);
                 } else {
                     resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                 }
             }
    
         } else if (method.equals(METHOD_HEAD)) {
             long lastModified = getLastModified(req);
             maybeSetLastModified(resp, lastModified);
             doHead(req, resp);
    
         } else if (method.equals(METHOD_POST)) {
             doPost(req, resp);
    
         } else if (method.equals(METHOD_PUT)) {
             doPut(req, resp);
    
         } else if (method.equals(METHOD_DELETE)) {
             doDelete(req, resp);
    
         } else if (method.equals(METHOD_OPTIONS)) {
             doOptions(req,resp);
    
         } else if (method.equals(METHOD_TRACE)) {
             doTrace(req,resp);
    
         } else {
             //
             // Note that this means NO servlet supports whatever
             // method was requested, anywhere on this server.
             //
    
             String errMsg = lStrings.getString("http.method_not_implemented");
             Object[] errArgs = new Object[1];
             errArgs[0] = method;
             errMsg = MessageFormat.format(errMsg, errArgs);
    
             resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
         }
     }
    

    这个方法主要是将 请求按照 请求方式分类, 分发到对应的doGet, doPost等方法去.

  4. doGet等方法被调用后, 会被它的实现类FrameworkServlet(抽象类)(org.springframework.web.servlet)覆盖的doGet,doPost等方法, 转发到processResuest方法 img 相当于把各种请求方式的请求重新集中起来, 统一到processResult处理, 与第三步中的做法感觉正是相反, 因为这已经来到了 SpringMVC 框架的逻辑了.

  5. 而在processResult方法中 调用了doService方法, 在FrameworkServlet类中, 这是一个抽象类方法.

  6. 此时就最终来到了FrameworkServlet的实现类DispatcherServlet, 调用了它的doService方法, 而在这个方法中, 最核心的就是调用了当前类的第 925 行的doDispatch(request,response)方法 开始进行派发逻辑.

# doDispatch方法的处理逻辑

# 核心方法getHandler, 获取处理器执行链(HandlerExecutionChain)

     //将request 变量换一个名字
     HttpServletRequest processedRequest = request;
     //提前准备一个 处理器执行链类型 变量.
		HandlerExecutionChain mappedHandler = null;
     // 决定为当前请求, 使用哪一个处理器链, 也就是获取处理器链
		mappedHandler = getHandler(processedRequest);

getHandler的方法逻辑:

	/**
	 * 为这个请求返回一个处理执行链
	 * <p>Tries all handler mappings in order.
	 * @param request current HTTP request
	 * @return the HandlerExecutionChain, or {@code null} if no handler could be found
	 */
	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
            //遍历所有的处理器映射器(也就是9大组件之一), 当我们开启注解驱动时, 默认会有4个处理器映射器.
			for (HandlerMapping hm : this.handlerMappings) {
				if (logger.isTraceEnabled()) {
					logger.trace(
							"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
				}
                //只要有一个处理器适配器能处理这个请求, 就返回这个处理器执行链
				HandlerExecutionChain handler = hm.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

HandlerMapping是一个接口, 它的抽象类AbstractHandlerMapping, 实现了getHandler这个方法, 这个方法主要调用了两个核心的方法:

Object handler = getHandlerInternal(request);
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
  1. getHandlerInternal 是获取最终的代码逻辑处理的 抽象(Controler 里面的编写的方法)
  2. getHandlerExecutionChain 是将当前请求所匹配的拦截器, 添加到 HandlerExecutionChain 的这个对象中, HandlerExecutionChain 有一个拦截器的集合, 记录当前请求所需要经过的拦截器. 里面也保存了, 我们最终要处理这个请求的 处理器handler对象(Controler 里面的编写的方法的抽象) 最终返回处理器执行器链.

处理器执行器链 类的 定义 img

# 调用getHandlerAdapter, 获取能够使用 handler 的适配器

	// 确定当前请求的处理程序适配器。
	HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

虽然我们得到了处理适配器链, 知道了正确的执行顺序, 但是我们并不知道怎样去执行处理器(handler), 因为它仅仅是一个抽象. 我们需要一个能够知道它如何执行的适配器即处理器适配器HandlerAdapter.

getHandlerAdapter 方法要求传入 handler 返回一个 HandlerAdapter. img

# 处理器适配器执行handler, 以及HandlerExecutionChain执行拦截器的前置,后置,最终方法.

img

# 用自己的话描述 SpringMVC的DispatcherServlet的执行流程.

因为DispatcherServlet的父类实现了Servlet这个接口, 当一个请求到来时,在执行父类相关的的方法后, 最后会调用doService()方法 这是一个保护修饰的方法, 由DispatcherServlet这个FrameworkServlet的processRequest使用模版方法设计模式调研doService()这个方法. 而doService()这个方法中到右DispatcherServlet的核心方法: doDispatch(), 这个方法主要做了如下几件事:

  1. 根据请求获取相应的 HandlerExecutionChain(处理器执行链)

    TIP

    如果我们自己配置HanderMapping类型的Bean的话, 默认就会从DispatchServlet.properties这个文件中, 加载默认的几个HandlerMapping, 我们的请求主要是由RequestHandlerMapping 这个类来帮我们获取处理器执行链

  2. 根据处理器执行链里面的 handler获取能够执行的HandlerAdapter(处理器适配器)

    TIP

    处理器适配器也是会从默认的DispatchServlet.properties文件中加载, handler就是执行逻辑的抽象, 起始就是我们在Controller中写的一个个的方法. 捕获handler只是方法的抽象信息, 比如这个方法有哪些参数, 它所在的Controller的Bean的类型等信息. 实际上去执行这个处理器还需要一个合适的处理器适配器.

    我们的http请求处理的适配器一般是RequestMappinghandlerAdapter

  3. 执行处理器执行链中拦截器的前置方法
  4. 使用处理器适配器执行handler所对应的方法.(如果是JSON数据的话,直接这里就返回给前端了)
  5. 执行处理器执行链中拦截器的后置方法
  6. processDispatchResult()方法执行, 执行视图和模型相关的操作, 在里面调用执行处理器执行链中拦截器的最终方法.
更新时间: 2024年5月26日星期日下午4点47分