Spring高级49讲原笔记
Spring高级49讲原笔记
Pei容器与 bean
1) 容器接口
BeanFactory 接口,典型功能有:
- getBean
ApplicationContext 接口,是 BeanFactory 的子接口。它扩展了 BeanFactory 接口的功能,如:
- 国际化
- 通配符方式获取一组 Resource 资源
- 整合 Environment 环境(能通过它获取各种来源的配置信息)
- 事件发布与监听,实现组件之间的解耦
可以看到,我们课上讲的,都是 BeanFactory 提供的基本功能,ApplicationContext 中的扩展功能都没有用到。
演示1 - BeanFactory 与 ApplicationContext 的区别
代码参考
com.itheima.a01 包
收获💡
通过这个示例结合 debug 查看 ApplicationContext 对象的内部结构,学到:
到底什么是 BeanFactory
- 它是 ApplicationContext 的父接口
- 它才是 Spring 的核心容器, 主要的 ApplicationContext 实现都【组合】了它的功能,【组合】是指 ApplicationContext 的一个重要成员变量就是 BeanFactory
BeanFactory 能干点啥
- 表面上只有 getBean
- 实际上控制反转、基本的依赖注入、直至 Bean 的生命周期的各种功能,都由它的实现类提供
- 例子中通过反射查看了它的成员变量 singletonObjects,内部包含了所有的单例 bean
ApplicationContext 比 BeanFactory 多点啥
- ApplicationContext 组合并扩展了 BeanFactory 的功能
- 国际化、通配符方式获取一组 Resource 资源、整合 Environment 环境、事件发布与监听
- 新学一种代码之间解耦途径,事件解耦
建议练习:完成用户注册与发送短信之间的解耦,用事件方式、和 AOP 方式分别实现
注意
- 如果 jdk > 8, 运行时请添加 –add-opens java.base/java.lang=ALL-UNNAMED,这是因为这些版本的 jdk 默认不允许跨 module 反射
- 事件发布还可以异步,这个视频中没有展示,请自行查阅 @EnableAsync,@Async 的用法
演示2 - 国际化
1 | public class TestMessageSource { |
国际化文件均在 src/resources 目录下
messages.properties(空)
messages_en.properties
1 | hi=Hello |
messages_ja.properties
1 | hi=こんにちは |
messages_zh.properties
1 | hi=你好 |
注意
- ApplicationContext 中 MessageSource bean 的名字固定为 messageSource
- 使用 SpringBoot 时,国际化文件名固定为 messages
- 空的 messages.properties 也必须存在
2) 容器实现
Spring 的发展历史较为悠久,因此很多资料还在讲解它较旧的实现,这里出于怀旧的原因,把它们都列出来,供大家参考
- DefaultListableBeanFactory,是 BeanFactory 最重要的实现,像控制反转和依赖注入功能,都是它来实现
- ClassPathXmlApplicationContext,从类路径查找 XML 配置文件,创建容器(旧)
- FileSystemXmlApplicationContext,从磁盘路径查找 XML 配置文件,创建容器(旧)
- XmlWebApplicationContext,传统 SSM 整合时,基于 XML 配置文件的容器(旧)
- AnnotationConfigWebApplicationContext,传统 SSM 整合时,基于 java 配置类的容器(旧)
- AnnotationConfigApplicationContext,Spring boot 中非 web 环境容器(新)
- AnnotationConfigServletWebServerApplicationContext,Spring boot 中 servlet web 环境容器(新)
- AnnotationConfigReactiveWebServerApplicationContext,Spring boot 中 reactive web 环境容器(新)
另外要注意的是,后面这些带有 ApplicationContext 的类都是 ApplicationContext 接口的实现,但它们是组合了 DefaultListableBeanFactory 的功能,并非继承而来
演示1 - DefaultListableBeanFactory
代码参考
com.itheima.a02.TestBeanFactory
收获💡
- beanFactory 可以通过 registerBeanDefinition 注册一个 bean definition 对象
- 我们平时使用的配置类、xml、组件扫描等方式都是生成 bean definition 对象注册到 beanFactory 当中
- bean definition 描述了这个 bean 的创建蓝图:scope 是什么、用构造还是工厂创建、初始化销毁方法是什么,等等
- beanFactory 需要手动调用 beanFactory 后处理器对它做增强
- 例如通过解析 @Bean、@ComponentScan 等注解,来补充一些 bean definition
- beanFactory 需要手动添加 bean 后处理器,以便对后续 bean 的创建过程提供增强
- 例如 @Autowired,@Resource 等注解的解析都是 bean 后处理器完成的
- bean 后处理的添加顺序会对解析结果有影响,见视频中同时加 @Autowired,@Resource 的例子
- beanFactory 需要手动调用方法来初始化单例
- beanFactory 需要额外设置才能解析 ${} 与 #{}
演示2 - 常见 ApplicationContext 实现
代码参考
com.itheima.a02.A02
收获💡
- 常见的 ApplicationContext 容器实现
- 内嵌容器、DispatcherServlet 的创建方法、作用
3) Bean 的生命周期
一个受 Spring 管理的 bean,生命周期主要阶段有
- 创建:根据 bean 的构造方法或者工厂方法来创建 bean 实例对象
- 依赖注入:根据 @Autowired,@Value 或其它一些手段,为 bean 的成员变量填充值、建立关系
- 初始化:回调各种 Aware 接口,调用对象的各种初始化方法
- 销毁:在容器关闭时,会销毁所有单例对象(即调用它们的销毁方法)
- prototype 对象也能够销毁,不过需要容器这边主动调用
一些资料会提到,生命周期中还有一类 bean 后处理器:BeanPostProcessor,会在 bean 的初始化的前后,提供一些扩展逻辑。但这种说法是不完整的,见下面的演示1
演示1 - bean 生命周期
代码参考
com.itheima.a03 包
graph LR 创建 --> 依赖注入 依赖注入 --> 初始化 初始化 --> 可用 可用 --> 销毁
创建前后的增强
- postProcessBeforeInstantiation
- 这里返回的对象若不为 null 会替换掉原本的 bean,并且仅会走 postProcessAfterInitialization 流程
- postProcessAfterInstantiation
- 这里如果返回 false 会跳过依赖注入阶段
依赖注入前的增强
- postProcessProperties
- 如 @Autowired、@Value、@Resource
初始化前后的增强
- postProcessBeforeInitialization
- 这里返回的对象会替换掉原本的 bean
- 如 @PostConstruct、@ConfigurationProperties
- postProcessAfterInitialization
- 这里返回的对象会替换掉原本的 bean
- 如代理增强
销毁之前的增强
- postProcessBeforeDestruction
- 如 @PreDestroy
收获💡
- Spring bean 生命周期各个阶段
- 模板设计模式, 指大流程已经固定好了, 通过接口回调(bean 后处理器)在一些关键点前后提供扩展
演示2 - 模板方法设计模式
关键代码
1 | public class TestMethodTemplate { |
演示3 - bean 后处理器排序
代码参考
com.itheima.a03.TestProcessOrder
收获💡
- 实现了 PriorityOrdered 接口的优先级最高
- 实现了 Ordered 接口与加了 @Order 注解的平级, 按数字升序
- 其它的排在最后
4) Bean 后处理器
演示1 - 后处理器作用
代码参考
com.itheima.a04 包
收获💡
- @Autowired 等注解的解析属于 bean 生命周期阶段(依赖注入, 初始化)的扩展功能,这些扩展功能由 bean 后处理器来完成
- 每个后处理器各自增强什么功能
- AutowiredAnnotationBeanPostProcessor 解析 @Autowired 与 @Value
- CommonAnnotationBeanPostProcessor 解析 @Resource、@PostConstruct、@PreDestroy
- ConfigurationPropertiesBindingPostProcessor 解析 @ConfigurationProperties
- 另外 ContextAnnotationAutowireCandidateResolver 负责获取 @Value 的值,解析 @Qualifier、泛型、@Lazy 等
演示2 - @Autowired bean 后处理器运行分析
代码参考
com.itheima.a04.DigInAutowired
收获💡
- AutowiredAnnotationBeanPostProcessor.findAutowiringMetadata 用来获取某个 bean 上加了 @Value @Autowired 的成员变量,方法参数的信息,表示为 InjectionMetadata
- InjectionMetadata 可以完成依赖注入
- InjectionMetadata 内部根据成员变量,方法参数封装为 DependencyDescriptor 类型
- 有了 DependencyDescriptor,就可以利用 beanFactory.doResolveDependency 方法进行基于类型的查找
5) BeanFactory 后处理器
演示1 - BeanFactory 后处理器的作用
代码参考
com.itheima.a05 包
- ConfigurationClassPostProcessor 可以解析
- @ComponentScan
- @Bean
- @Import
- @ImportResource
- MapperScannerConfigurer 可以解析
- Mapper 接口
收获💡
- @ComponentScan, @Bean, @Mapper 等注解的解析属于核心容器(即 BeanFactory)的扩展功能
- 这些扩展功能由不同的 BeanFactory 后处理器来完成,其实主要就是补充了一些 bean 定义
演示2 - 模拟解析 @ComponentScan
代码参考
com.itheima.a05.ComponentScanPostProcessor
收获💡
- Spring 操作元数据的工具类 CachingMetadataReaderFactory
- 通过注解元数据(AnnotationMetadata)获取直接或间接标注的注解信息
- 通过类元数据(ClassMetadata)获取类名,AnnotationBeanNameGenerator 生成 bean 名
- 解析元数据是基于 ASM 技术
演示3 - 模拟解析 @Bean
代码参考
com.itheima.a05.AtBeanPostProcessor
收获💡
- 进一步熟悉注解元数据(AnnotationMetadata)获取方法上注解信息
演示4 - 模拟解析 Mapper 接口
代码参考
com.itheima.a05.MapperPostProcessor
收获💡
- Mapper 接口被 Spring 管理的本质:实际是被作为 MapperFactoryBean 注册到容器中
- Spring 的诡异做法,根据接口生成的 BeanDefinition 仅为根据接口名生成 bean 名
6) Aware 接口
演示 - Aware 接口及 InitializingBean 接口
代码参考
com.itheima.a06 包
收获💡
- Aware 接口提供了一种【内置】 的注入手段,例如
- BeanNameAware 注入 bean 的名字
- BeanFactoryAware 注入 BeanFactory 容器
- ApplicationContextAware 注入 ApplicationContext 容器
- EmbeddedValueResolverAware 注入 ${} 解析器
- InitializingBean 接口提供了一种【内置】的初始化手段
- 对比
- 内置的注入和初始化不受扩展功能的影响,总会被执行
- 而扩展功能受某些情况影响可能会失效
- 因此 Spring 框架内部的类常用内置注入和初始化
配置类 @Autowired 失效分析
Java 配置类不包含 BeanFactoryPostProcessor 的情况
sequenceDiagram participant ac as ApplicationContext participant bfpp as BeanFactoryPostProcessor participant bpp as BeanPostProcessor participant config as Java配置类 ac ->> bfpp : 1. 执行 BeanFactoryPostProcessor ac ->> bpp : 2. 注册 BeanPostProcessor ac ->> +config : 3. 创建和初始化 bpp ->> config : 3.1 依赖注入扩展(如 @Value 和 @Autowired) bpp ->> config : 3.2 初始化扩展(如 @PostConstruct) ac ->> config : 3.3 执行 Aware 及 InitializingBean config -->> -ac : 3.4 创建成功
Java 配置类包含 BeanFactoryPostProcessor 的情况,因此要创建其中的 BeanFactoryPostProcessor 必须提前创建 Java 配置类,而此时的 BeanPostProcessor 还未准备好,导致 @Autowired 等注解失效
sequenceDiagram participant ac as ApplicationContext participant bfpp as BeanFactoryPostProcessor participant bpp as BeanPostProcessor participant config as Java配置类 ac ->> +config : 3. 创建和初始化 ac ->> config : 3.1 执行 Aware 及 InitializingBean config -->> -ac : 3.2 创建成功 ac ->> bfpp : 1. 执行 BeanFactoryPostProcessor ac ->> bpp : 2. 注册 BeanPostProcessor
对应代码
1 |
|
注意
解决方法:
- 用内置依赖注入和初始化取代扩展依赖注入和初始化
- 用静态工厂方法代替实例工厂方法,避免工厂对象提前被创建
7) 初始化与销毁
演示 - 初始化销毁顺序
代码参考
com.itheima.a07 包
收获💡
Spring 提供了多种初始化手段,除了课堂上讲的 @PostConstruct,@Bean(initMethod) 之外,还可以实现 InitializingBean 接口来进行初始化,如果同一个 bean 用了以上手段声明了 3 个初始化方法,那么它们的执行顺序是
- @PostConstruct 标注的初始化方法
- InitializingBean 接口的初始化方法
- @Bean(initMethod) 指定的初始化方法
与初始化类似,Spring 也提供了多种销毁手段,执行顺序为
- @PreDestroy 标注的销毁方法
- DisposableBean 接口的销毁方法
- @Bean(destroyMethod) 指定的销毁方法
8) Scope
在当前版本的 Spring 和 Spring Boot 程序中,支持五种 Scope
- singleton,容器启动时创建(未设置延迟),容器关闭时销毁
- prototype,每次使用时创建,不会自动销毁,需要调用 DefaultListableBeanFactory.destroyBean(bean) 销毁
- request,每次请求用到此 bean 时创建,请求结束时销毁
- session,每个会话用到此 bean 时创建,会话结束时销毁
- application,web 容器用到此 bean 时创建,容器停止时销毁
有些文章提到有 globalSession 这一 Scope,也是陈旧的说法,目前 Spring 中已废弃
但要注意,如果在 singleton 注入其它 scope 都会有问题,解决方法有
- @Lazy
- @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
- ObjectFactory
- ApplicationContext.getBean
演示1 - request, session, application 作用域
代码参考
com.itheima.a08 包
- 打开不同的浏览器, 刷新 http://localhost:8080/test 即可查看效果
- 如果 jdk > 8, 运行时请添加 –add-opens java.base/java.lang=ALL-UNNAMED
收获💡
- 有几种 scope
- 在 singleton 中使用其它几种 scope 的方法
- 其它 scope 的销毁时机
- 可以将通过 server.servlet.session.timeout=30s 观察 session bean 的销毁
- ServletContextScope 销毁机制疑似实现有误
分析 - singleton 注入其它 scope 失效
以单例注入多例为例
有一个单例对象 E
1 |
|
要注入的对象 F 期望是多例
1 |
|
测试
1 | E e = context.getBean(E.class); |
输出
1 | com.itheima.demo.cycle.F@6622fc65 |
发现它们是同一个对象,而不是期望的多例对象
对于单例对象来讲,依赖注入仅发生了一次,后续再没有用到多例的 F,因此 E 用的始终是第一次依赖注入的 F
graph LR e1(e 创建) e2(e set 注入 f) f1(f 创建) e1-->f1-->e2
解决
- 仍然使用 @Lazy 生成代理
- 代理对象虽然还是同一个,但当每次使用代理对象的任意方法时,由代理创建新的 f 对象
graph LR e1(e 创建) e2(e set 注入 f代理) f1(f 创建) f2(f 创建) f3(f 创建) e1-->e2 e2--使用f方法-->f1 e2--使用f方法-->f2 e2--使用f方法-->f3
1 |
|
注意
- @Lazy 加在也可以加在成员变量上,但加在 set 方法上的目的是可以观察输出,加在成员变量上就不行了
- @Autowired 加在 set 方法的目的类似
输出
1 | E: setF(F f) class com.itheima.demo.cycle.F$$EnhancerBySpringCGLIB$$8b54f2bc |
从输出日志可以看到调用 setF 方法时,f 对象的类型是代理类型
演示2 - 4种解决方法
代码参考
com.itheima.a08.sub 包
- 如果 jdk > 8, 运行时请添加 –add-opens java.base/java.lang=ALL-UNNAMED
收获💡
- 单例注入其它 scope 的四种解决方法
- @Lazy
- @Scope(value = “prototype”, proxyMode = ScopedProxyMode.TARGET_CLASS)
- ObjectFactory
- ApplicationContext
- 解决方法虽然不同,但理念上殊途同归: 都是推迟其它 scope bean 的获取
AOP
AOP 底层实现方式之一是代理,由代理结合通知和目标,提供增强功能
除此以外,aspectj 提供了两种另外的 AOP 底层实现:
- 第一种是通过 ajc 编译器在编译 class 类文件时,就把通知的增强功能,织入到目标类的字节码中
- 第二种是通过 agent 在加载目标类时,修改目标类的字节码,织入增强功能
- 作为对比,之前学习的代理是运行时生成新的字节码
简单比较的话:
- aspectj 在编译和加载时,修改目标字节码,性能较高
- aspectj 因为不用代理,能突破一些技术上的限制,例如对构造、对静态方法、对 final 也能增强
- 但 aspectj 侵入性较强,且需要学习新的 aspectj 特有语法,因此没有广泛流行
9) AOP 实现之 ajc 编译器
代码参考项目 demo6_advanced_aspectj_01
收获💡
- 编译器也能修改 class 实现增强
- 编译器增强能突破代理仅能通过方法重写增强的限制:可以对构造方法、静态方法等实现增强
注意
- 版本选择了 java 8, 因为目前的 aspectj-maven-plugin 1.14.0 最高只支持到 java 16
- 一定要用 maven 的 compile 来编译, idea 不会调用 ajc 编译器
10) AOP 实现之 agent 类加载
代码参考项目 demo6_advanced_aspectj_02
收获💡
- 类加载时可以通过 agent 修改 class 实现增强
11) AOP 实现之 proxy
演示1 - jdk 动态代理
1 | public class JdkProxyDemo { |
运行结果
1 | proxy before... |
收获💡
- jdk 动态代理要求目标必须实现接口,生成的代理类实现相同接口,因此代理与目标之间是平级兄弟关系
演示2 - cglib 代理
1 | public class CglibProxyDemo { |
运行结果与 jdk 动态代理相同
收获💡
- cglib 不要求目标实现接口,它生成的代理类是目标的子类,因此代理与目标之间是子父关系
- 限制⛔:根据上述分析 final 类无法被 cglib 增强
12) jdk 动态代理进阶
演示1 - 模拟 jdk 动态代理
1 | public class A12 { |
模拟代理实现
1 | import java.lang.reflect.InvocationHandler; |
收获💡
代理一点都不难,无非就是利用了多态、反射的知识
- 方法重写可以增强逻辑,只不过这【增强逻辑】千变万化,不能写死在代理内部
- 通过接口回调将【增强逻辑】置于代理类之外
- 配合接口方法反射(是多态调用),就可以再联动调用目标方法
- 会用 arthas 的 jad 工具反编译代理类
- 限制⛔:代理增强是借助多态来实现,因此成员变量、静态方法、final 方法均不能通过代理实现
演示2 - 方法反射优化
代码参考
com.itheima.a12.TestMethodInvoke
收获💡
- 前 16 次反射性能较低
- 第 17 次调用会生成代理类,优化为非反射调用
- 会用 arthas 的 jad 工具反编译第 17 次调用生成的代理类
注意
运行时请添加 –add-opens java.base/java.lang.reflect=ALL-UNNAMED –add-opens java.base/jdk.internal.reflect=ALL-UNNAMED
13) cglib 代理进阶
演示 - 模拟 cglib 代理
代码参考
com.itheima.a13 包
收获💡
和 jdk 动态代理原理查不多
- 回调的接口换了一下,InvocationHandler 改成了 MethodInterceptor
- 调用目标时有所改进,见下面代码片段
- method.invoke 是反射调用,必须调用到足够次数才会进行优化
- methodProxy.invoke 是不反射调用,它会正常(间接)调用目标对象的方法(Spring 采用)
- methodProxy.invokeSuper 也是不反射调用,它会正常(间接)调用代理对象的方法,可以省略目标对象
1 | public class A14Application { |
注意
- 调用 Object 的方法, 后两种在 jdk >= 9 时都有问题, 需要 –add-opens java.base/java.lang=ALL-UNNAMED
14) cglib 避免反射调用
演示 - cglib 如何避免反射
代码参考
com.itheima.a13.ProxyFastClass,com.itheima.a13.TargetFastClass
收获💡
- 当调用 MethodProxy 的 invoke 或 invokeSuper 方法时, 会动态生成两个类
- ProxyFastClass 配合代理对象一起使用, 避免反射
- TargetFastClass 配合目标对象一起使用, 避免反射 (Spring 用的这种)
- TargetFastClass 记录了 Target 中方法与编号的对应关系
- save(long) 编号 2
- save(int) 编号 1
- save() 编号 0
- 首先根据方法名和参数个数、类型, 用 switch 和 if 找到这些方法编号
- 然后再根据编号去调用目标方法, 又用了一大堆 switch 和 if, 但避免了反射
- ProxyFastClass 记录了 Proxy 中方法与编号的对应关系,不过 Proxy 额外提供了下面几个方法
- saveSuper(long) 编号 2,不增强,仅是调用 super.save(long)
- saveSuper(int) 编号 1,不增强, 仅是调用 super.save(int)
- saveSuper() 编号 0,不增强, 仅是调用 super.save()
- 查找方式与 TargetFastClass 类似
- 为什么有这么麻烦的一套东西呢?
- 避免反射, 提高性能, 代价是一个代理类配两个 FastClass 类, 代理类中还得增加仅调用 super 的一堆方法
- 用编号处理方法对应关系比较省内存, 另外, 最初获得方法顺序是不确定的, 这个过程没法固定死
15) jdk 和 cglib 在 Spring 中的统一
Spring 中对切点、通知、切面的抽象如下
- 切点:接口 Pointcut,典型实现 AspectJExpressionPointcut
- 通知:典型接口为 MethodInterceptor 代表环绕通知
- 切面:Advisor,包含一个 Advice 通知,PointcutAdvisor 包含一个 Advice 通知和一个 Pointcut
classDiagram class Advice class MethodInterceptor class Advisor class PointcutAdvisor Pointcut <|-- AspectJExpressionPointcut Advice <|-- MethodInterceptor Advisor <|-- PointcutAdvisor PointcutAdvisor o-- "一" Pointcut PointcutAdvisor o-- "一" Advice <<interface>> Advice <<interface>> MethodInterceptor <<interface>> Pointcut <<interface>> Advisor <<interface>> PointcutAdvisor
代理相关类图
- AopProxyFactory 根据 proxyTargetClass 等设置选择 AopProxy 实现
- AopProxy 通过 getProxy 创建代理对象
- 图中 Proxy 都实现了 Advised 接口,能够获得关联的切面集合与目标(其实是从 ProxyFactory 取得)
- 调用代理方法时,会借助 ProxyFactory 将通知统一转为环绕通知:MethodInterceptor
classDiagram Advised <|-- ProxyFactory ProxyFactory o-- Target ProxyFactory o-- "多" Advisor ProxyFactory --> AopProxyFactory : 使用 AopProxyFactory --> AopProxy Advised <|-- 基于CGLIB的Proxy 基于CGLIB的Proxy <-- ObjenesisCglibAopProxy : 创建 AopProxy <|-- ObjenesisCglibAopProxy AopProxy <|-- JdkDynamicAopProxy 基于JDK的Proxy <-- JdkDynamicAopProxy : 创建 Advised <|-- 基于JDK的Proxy class AopProxy { +getProxy() Object } class ProxyFactory { proxyTargetClass : boolean } class ObjenesisCglibAopProxy { advised : ProxyFactory } class JdkDynamicAopProxy { advised : ProxyFactory } <<interface>> Advised <<interface>> AopProxyFactory <<interface>> AopProxy
演示 - 底层切点、通知、切面
代码参考
com.itheima.a15.A15
收获💡
- 底层的切点实现
- 底层的通知实现
- 底层的切面实现
- ProxyFactory 用来创建代理
- 如果指定了接口,且 proxyTargetClass = false,使用 JdkDynamicAopProxy
- 如果没有指定接口,或者 proxyTargetClass = true,使用 ObjenesisCglibAopProxy
- 例外:如果目标是接口类型或已经是 Jdk 代理,使用 JdkDynamicAopProxy
注意
- 要区分本章节提到的 MethodInterceptor,它与之前 cglib 中用的的 MethodInterceptor 是不同的接口
16) 切点匹配
演示 - 切点匹配
代码参考
com.itheima.a16.A16
收获💡
- 常见 aspectj 切点用法
- aspectj 切点的局限性,实际的 @Transactional 切点实现
17) 从 @Aspect 到 Advisor
演示1 - 代理创建器
代码参考
org.springframework.aop.framework.autoproxy 包
收获💡
- AnnotationAwareAspectJAutoProxyCreator 的作用
- 将高级 @Aspect 切面统一为低级 Advisor 切面
- 在合适的时机创建代理
- findEligibleAdvisors 找到有【资格】的 Advisors
- 有【资格】的 Advisor 一部分是低级的, 可以由自己编写, 如本例 A17 中的 advisor3
- 有【资格】的 Advisor 另一部分是高级的, 由解析 @Aspect 后获得
- wrapIfNecessary
- 它内部调用 findEligibleAdvisors, 只要返回集合不空, 则表示需要创建代理
- 它的调用时机通常在原始对象初始化后执行, 但碰到循环依赖会提前至依赖注入之前执行
演示2 - 代理创建时机
代码参考
org.springframework.aop.framework.autoproxy.A17_1
收获💡
- 代理的创建时机
- 初始化之后 (无循环依赖时)
- 实例创建后, 依赖注入前 (有循环依赖时), 并暂存于二级缓存
- 依赖注入与初始化不应该被增强, 仍应被施加于原始对象
演示3 - @Before 对应的低级通知
代码参考
org.springframework.aop.framework.autoproxy.A17_2
收获💡
- @Before 前置通知会被转换为原始的 AspectJMethodBeforeAdvice 形式, 该对象包含了如下信息
- 通知代码从哪儿来
- 切点是什么(这里为啥要切点, 后面解释)
- 通知对象如何创建, 本例共用同一个 Aspect 对象
- 类似的还有
- AspectJAroundAdvice (环绕通知)
- AspectJAfterReturningAdvice
- AspectJAfterThrowingAdvice (环绕通知)
- AspectJAfterAdvice (环绕通知)
18) 静态通知调用
代理对象调用流程如下(以 JDK 动态代理实现为例)
- 从 ProxyFactory 获得 Target 和环绕通知链,根据他俩创建 MethodInvocation,简称 mi
- 首次执行 mi.proceed() 发现有下一个环绕通知,调用它的 invoke(mi)
- 进入环绕通知1,执行前增强,再次调用 mi.proceed() 发现有下一个环绕通知,调用它的 invoke(mi)
- 进入环绕通知2,执行前增强,调用 mi.proceed() 发现没有环绕通知,调用 mi.invokeJoinPoint() 执行目标方法
- 目标方法执行结束,将结果返回给环绕通知2,执行环绕通知2 的后增强
- 环绕通知2继续将结果返回给环绕通知1,执行环绕通知1 的后增强
- 环绕通知1返回最终的结果
图中不同颜色对应一次环绕通知或目标的调用起始至终结
sequenceDiagram participant Proxy participant ih as InvocationHandler participant mi as MethodInvocation participant Factory as ProxyFactory participant mi1 as MethodInterceptor1 participant mi2 as MethodInterceptor2 participant Target Proxy ->> +ih : invoke() ih ->> +Factory : 获得 Target Factory -->> -ih : ih ->> +Factory : 获得 MethodInterceptor 链 Factory -->> -ih : ih ->> +mi : 创建 mi mi -->> -ih : rect rgb(200, 223, 255) ih ->> +mi : mi.proceed() mi ->> +mi1 : invoke(mi) mi1 ->> mi1 : 前增强 rect rgb(200, 190, 255) mi1 ->> mi : mi.proceed() mi ->> +mi2 : invoke(mi) mi2 ->> mi2 : 前增强 rect rgb(150, 190, 155) mi2 ->> mi : mi.proceed() mi ->> +Target : mi.invokeJoinPoint() Target ->> Target : Target -->> -mi2 : 结果 end mi2 ->> mi2 : 后增强 mi2 -->> -mi1 : 结果 end mi1 ->> mi1 : 后增强 mi1 -->> -mi : 结果 mi -->> -ih : end ih -->> -Proxy :
演示1 - 通知调用过程
代码参考
org.springframework.aop.framework.A18
收获💡
代理方法执行时会做如下工作
- 通过 proxyFactory 的 getInterceptorsAndDynamicInterceptionAdvice() 将其他通知统一转换为 MethodInterceptor 环绕通知
- MethodBeforeAdviceAdapter 将 @Before AspectJMethodBeforeAdvice 适配为 MethodBeforeAdviceInterceptor
- AfterReturningAdviceAdapter 将 @AfterReturning AspectJAfterReturningAdvice 适配为 AfterReturningAdviceInterceptor
- 这体现的是适配器设计模式
- 所谓静态通知,体现在上面方法的 Interceptors 部分,这些通知调用时无需再次检查切点,直接调用即可
- 结合目标与环绕通知链,创建 MethodInvocation 对象,通过它完成整个调用
演示2 - 模拟 MethodInvocation
代码参考
org.springframework.aop.framework.A18_1
收获💡
- proceed() 方法调用链中下一个环绕通知
- 每个环绕通知内部继续调用 proceed()
- 调用到没有更多通知了, 就调用目标方法
MethodInvocation 的编程技巧在实现拦截器、过滤器时能用上
19) 动态通知调用
演示 - 带参数绑定的通知方法调用
代码参考
org.springframework.aop.framework.autoproxy.A19
收获💡
- 通过 proxyFactory 的 getInterceptorsAndDynamicInterceptionAdvice() 将其他通知统一转换为 MethodInterceptor 环绕通知
- 所谓动态通知,体现在上面方法的 DynamicInterceptionAdvice 部分,这些通知调用时因为要为通知方法绑定参数,还需再次利用切点表达式
- 动态通知调用复杂程度高,性能较低
WEB
20) RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter 俩是一对,分别用来
- 处理 @RequestMapping 映射
- 调用控制器方法、并处理方法参数与方法返回值
演示1 - DispatcherServlet 初始化
代码参考
com.itheima.a20 包
收获💡
- DispatcherServlet 是在第一次被访问时执行初始化, 也可以通过配置修改为 Tomcat 启动后就初始化
- 在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化
- RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,其中
- key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
- value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
- 有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet
- RequestMappingHandlerAdapter 初始化时,会准备 HandlerMethod 调用时需要的各个组件,如:
- HandlerMethodArgumentResolver 解析控制器方法参数
- HandlerMethodReturnValueHandler 处理控制器方法返回值
演示2 - 自定义参数与返回值处理器
代码参考
com.itheima.a20.TokenArgumentResolver ,com.itheima.a20.YmlReturnValueHandler
收获💡
- 体会参数解析器的作用
- 体会返回值处理器的作用
21) 参数解析器
演示 - 常见参数解析器
代码参考
com.itheima.a21 包
收获💡
- 初步了解 RequestMappingHandlerAdapter 的调用过程
- 控制器方法被封装为 HandlerMethod
- 准备对象绑定与类型转换
- 准备 ModelAndViewContainer 用来存储中间 Model 结果
- 解析每个参数值
- 解析参数依赖的就是各种参数解析器,它们都有两个重要方法
- supportsParameter 判断是否支持方法参数
- resolveArgument 解析方法参数
- 常见参数的解析
- @RequestParam
- 省略 @RequestParam
- @RequestParam(defaultValue)
- MultipartFile
- @PathVariable
- @RequestHeader
- @CookieValue
- @Value
- HttpServletRequest 等
- @ModelAttribute
- 省略 @ModelAttribute
- @RequestBody
- 组合模式在 Spring 中的体现
- @RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取
22) 参数名解析
演示 - 两种方法获取参数名
代码参考
com.itheima.a22.A22
收获💡
- 如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名
- 如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
- 普通类, 会包含局部变量表, 用 asm 可以拿到参数名
- 接口, 不会包含局部变量表, 无法获得参数名
- 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
23) 对象绑定与类型转换
底层第一套转换接口与实现
classDiagram Formatter --|> Printer Formatter --|> Parser class Converters { Set~GenericConverter~ } class Converter class ConversionService class FormattingConversionService ConversionService <|-- FormattingConversionService FormattingConversionService o-- Converters Printer --> Adapter1 Adapter1 --> Converters Parser --> Adapter2 Adapter2 --> Converters Converter --> Adapter3 Adapter3 --> Converters <<interface>> Formatter <<interface>> Printer <<interface>> Parser <<interface>> Converter <<interface>> ConversionService
- Printer 把其它类型转为 String
- Parser 把 String 转为其它类型
- Formatter 综合 Printer 与 Parser 功能
- Converter 把类型 S 转为类型 T
- Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
- FormattingConversionService 利用其它们实现转换
底层第二套转换接口
classDiagram PropertyEditorRegistry o-- "多" PropertyEditor <<interface>> PropertyEditorRegistry <<interface>> PropertyEditor
- PropertyEditor 把 String 与其它类型相互转换
- PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
- 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配
高层接口与实现
classDiagram TypeConverter <|-- SimpleTypeConverter TypeConverter <|-- BeanWrapperImpl TypeConverter <|-- DirectFieldAccessor TypeConverter <|-- ServletRequestDataBinder SimpleTypeConverter --> TypeConverterDelegate BeanWrapperImpl --> TypeConverterDelegate DirectFieldAccessor --> TypeConverterDelegate ServletRequestDataBinder --> TypeConverterDelegate TypeConverterDelegate --> ConversionService TypeConverterDelegate --> PropertyEditorRegistry <<interface>> TypeConverter <<interface>> ConversionService <<interface>> PropertyEditorRegistry
- 它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)
- 首先看是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
- 再看有没有 ConversionService 转换
- 再利用默认的 PropertyEditor 转换
- 最后有一些特殊处理
- SimpleTypeConverter 仅做类型转换
- BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property
- DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field
- ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,具备校验与获取校验结果功能
演示1 - 类型转换与数据绑定
代码参考
com.itheima.a23 包
收获💡
基本的类型转换与数据绑定用法
- SimpleTypeConverter
- BeanWrapperImpl
- DirectFieldAccessor
- ServletRequestDataBinder
演示2 - 数据绑定工厂
代码参考
com.itheima.a23.TestServletDataBinderFactory
收获💡
ServletRequestDataBinderFactory 的用法和扩展点
- 可以解析控制器的 @InitBinder 标注方法作为扩展点,添加自定义转换器
- 控制器私有范围
- 可以通过 ConfigurableWebBindingInitializer 配置 ConversionService 作为扩展点,添加自定义转换器
- 公共范围
- 同时加了 @InitBinder 和 ConversionService 的转换优先级
- 优先采用 @InitBinder 的转换器
- 其次使用 ConversionService 的转换器
- 使用默认转换器
- 特殊处理(例如有参构造)
演示3 - 获取泛型参数
代码参考
com.itheima.a23.sub 包
收获💡
- java api 获取泛型参数
- spring api 获取泛型参数
24) @ControllerAdvice 之 @InitBinder
演示 - 准备 @InitBinder
准备 @InitBinder 在整个 HandlerAdapter 调用过程中所处的位置
sequenceDiagram participant adapter as HandlerAdapter participant bf as WebDataBinderFactory participant mf as ModelFactory participant ihm as ServletInvocableHandlerMethod participant ar as ArgumentResolvers participant rh as ReturnValueHandlers participant container as ModelAndViewContainer rect rgb(200, 150, 255) adapter ->> +bf: 准备 @InitBinder bf -->> -adapter: end adapter ->> +mf: 准备 @ModelAttribute mf ->> +container: 添加Model数据 container -->> -mf: mf -->> -adapter: adapter ->> +ihm: invokeAndHandle ihm ->> +ar: 获取 args ar ->> ar: 有的解析器涉及 RequestBodyAdvice ar ->> container: 有的解析器涉及数据绑定生成Model数据 ar -->> -ihm: args ihm ->> ihm: method.invoke(bean,args) 得到 returnValue ihm ->> +rh: 处理 returnValue rh ->> rh: 有的处理器涉及 ResponseBodyAdvice rh ->> +container: 添加Model数据,处理视图名,是否渲染等 container -->> -rh: rh -->> -ihm: ihm -->> -adapter: adapter ->> +container: 获取 ModelAndView container -->> -adapter:
- RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
- HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
- HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers
收获💡
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法
- RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @InitBinder 方法
- 以上两种 @InitBinder 的解析结果都会缓存来避免重复解析
- 控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂
25) 控制器方法执行流程
图1
classDiagram class ServletInvocableHandlerMethod { +invokeAndHandle(ServletWebRequest,ModelAndViewContainer) } HandlerMethod <|-- ServletInvocableHandlerMethod HandlerMethod o-- bean HandlerMethod o-- method ServletInvocableHandlerMethod o-- WebDataBinderFactory ServletInvocableHandlerMethod o-- ParameterNameDiscoverer ServletInvocableHandlerMethod o-- HandlerMethodArgumentResolverComposite ServletInvocableHandlerMethod o-- HandlerMethodReturnValueHandlerComposite
HandlerMethod 需要
- bean 即是哪个 Controller
- method 即是 Controller 中的哪个方法
ServletInvocableHandlerMethod 需要
- WebDataBinderFactory 负责对象绑定、类型转换
- ParameterNameDiscoverer 负责参数名解析
- HandlerMethodArgumentResolverComposite 负责解析参数
- HandlerMethodReturnValueHandlerComposite 负责处理返回值
图2
sequenceDiagram participant adapter as RequestMappingHandlerAdapter participant bf as WebDataBinderFactory participant mf as ModelFactory participant container as ModelAndViewContainer adapter ->> +bf: 准备 @InitBinder bf -->> -adapter: adapter ->> +mf: 准备 @ModelAttribute mf ->> +container: 添加Model数据 container -->> -mf: mf -->> -adapter:
图3
sequenceDiagram participant adapter as RequestMappingHandlerAdapter participant ihm as ServletInvocableHandlerMethod participant ar as ArgumentResolvers participant rh as ReturnValueHandlers participant container as ModelAndViewContainer adapter ->> +ihm: invokeAndHandle ihm ->> +ar: 获取 args ar ->> ar: 有的解析器涉及 RequestBodyAdvice ar ->> container: 有的解析器涉及数据绑定生成模型数据 container -->> ar: ar -->> -ihm: args ihm ->> ihm: method.invoke(bean,args) 得到 returnValue ihm ->> +rh: 处理 returnValue rh ->> rh: 有的处理器涉及 ResponseBodyAdvice rh ->> +container: 添加Model数据,处理视图名,是否渲染等 container -->> -rh: rh -->> -ihm: ihm -->> -adapter: adapter ->> +container: 获取 ModelAndView container -->> -adapter:
26) @ControllerAdvice 之 @ModelAttribute
演示 - 准备 @ModelAttribute
代码参考
com.itheima.a26 包
准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置
sequenceDiagram participant adapter as HandlerAdapter participant bf as WebDataBinderFactory participant mf as ModelFactory participant ihm as ServletInvocableHandlerMethod participant ar as ArgumentResolvers participant rh as ReturnValueHandlers participant container as ModelAndViewContainer adapter ->> +bf: 准备 @InitBinder bf -->> -adapter: rect rgb(200, 150, 255) adapter ->> +mf: 准备 @ModelAttribute mf ->> +container: 添加Model数据 container -->> -mf: mf -->> -adapter: end adapter ->> +ihm: invokeAndHandle ihm ->> +ar: 获取 args ar ->> ar: 有的解析器涉及 RequestBodyAdvice ar ->> container: 有的解析器涉及数据绑定生成Model数据 ar -->> -ihm: args ihm ->> ihm: method.invoke(bean,args) 得到 returnValue ihm ->> +rh: 处理 returnValue rh ->> rh: 有的处理器涉及 ResponseBodyAdvice rh ->> +container: 添加Model数据,处理视图名,是否渲染等 container -->> -rh: rh -->> -ihm: ihm -->> -adapter: adapter ->> +container: 获取 ModelAndView container -->> -adapter:
收获💡
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @ModelAttribute 方法
- RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @ModelAttribute 方法
- 以上两种 @ModelAttribute 的解析结果都会缓存来避免重复解析
- 控制器方法调用时,会综合利用本类的 @ModelAttribute 方法和 @ControllerAdvice 中的 @ModelAttribute 方法创建模型工厂
27) 返回值处理器
演示 - 常见返回值处理器
代码参考
com.itheima.a27 包
收获💡
- 常见的返回值处理器
- ModelAndView,分别获取其模型和视图名,放入 ModelAndViewContainer
- 返回值类型为 String 时,把它当做视图名,放入 ModelAndViewContainer
- 返回值添加了 @ModelAttribute 注解时,将返回值作为模型,放入 ModelAndViewContainer
- 此时需找到默认视图名
- 返回值省略 @ModelAttribute 注解且返回非简单类型时,将返回值作为模型,放入 ModelAndViewContainer
- 此时需找到默认视图名
- 返回值类型为 ResponseEntity 时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
- 返回值类型为 HttpHeaders 时
- 会设置 ModelAndViewContainer.requestHandled 为 true
- 返回值添加了 @ResponseBody 注解时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
- 组合模式在 Spring 中的体现 + 1
28) MessageConverter
演示 - MessageConverter 的作用
代码参考
com.itheima.a28.A28
收获💡
- MessageConverter 的作用
- @ResponseBody 是返回值处理器解析的
- 但具体转换工作是 MessageConverter 做的
- 如何选择 MediaType
- 首先看 @RequestMapping 上有没有指定
- 其次看 request 的 Accept 头有没有指定
- 最后按 MessageConverter 的顺序, 谁能谁先转换
29) @ControllerAdvice 之 ResponseBodyAdvice
演示 - ResponseBodyAdvice 增强
代码参考
com.itheima.a29 包
ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置
sequenceDiagram participant adapter as HandlerAdapter participant bf as WebDataBinderFactory participant mf as ModelFactory participant ihm as ServletInvocableHandlerMethod participant ar as ArgumentResolvers participant rh as ReturnValueHandlers participant container as ModelAndViewContainer adapter ->> +bf: 准备 @InitBinder bf -->> -adapter: adapter ->> +mf: 准备 @ModelAttribute mf ->> +container: 添加Model数据 container -->> -mf: mf -->> -adapter: adapter ->> +ihm: invokeAndHandle ihm ->> +ar: 获取 args ar ->> ar: 有的解析器涉及 RequestBodyAdvice ar ->> container: 有的解析器涉及数据绑定生成Model数据 ar -->> -ihm: args ihm ->> ihm: method.invoke(bean,args) 得到 returnValue ihm ->> +rh: 处理 returnValue rect rgb(200, 150, 255) rh ->> rh: 有的处理器涉及 ResponseBodyAdvice end rh ->> +container: 添加Model数据,处理视图名,是否渲染等 container -->> -rh: rh -->> -ihm: ihm -->> -adapter: adapter ->> +container: 获取 ModelAndView container -->> -adapter:
收获💡
- ResponseBodyAdvice 返回响应体前包装
30) 异常解析器
演示 - ExceptionHandlerExceptionResolver
代码参考
com.itheima.a30.A30
收获💡
- 它能够重用参数解析器、返回值处理器,实现组件重用
- 它能够支持嵌套异常
31) @ControllerAdvice 之 @ExceptionHandler
演示 - 准备 @ExceptionHandler
代码参考
com.itheima.a31 包
收获💡
- ExceptionHandlerExceptionResolver 初始化时会解析 @ControllerAdvice 中的 @ExceptionHandler 方法
- ExceptionHandlerExceptionResolver 会以类为单位,在该类首次处理异常时,解析此类的 @ExceptionHandler 方法
- 以上两种 @ExceptionHandler 的解析结果都会缓存来避免重复解析
32) Tomcat 异常处理
我们知道 @ExceptionHandler 只能处理发生在 mvc 流程中的异常,例如控制器内、拦截器内,那么如果是 Filter 出现了异常,如何进行处理呢?
在 Spring Boot 中,是这么实现的:
- 因为内嵌了 Tomcat 容器,因此可以配置 Tomcat 的错误页面,Filter 与 错误页面之间是通过请求转发跳转的,可以在这里做手脚
- 先通过 ErrorPageRegistrarBeanPostProcessor 这个后处理器配置错误页面地址,默认为
/error
也可以通过${server.error.path}
进行配置 - 当 Filter 发生异常时,不会走 Spring 流程,但会走 Tomcat 的错误处理,于是就希望转发至
/error
这个地址- 当然,如果没有 @ExceptionHandler,那么最终也会走到 Tomcat 的错误处理
- Spring Boot 又提供了一个 BasicErrorController,它就是一个标准 @Controller,@RequestMapping 配置为
/error
,所以处理异常的职责就又回到了 Spring - 异常信息由于会被 Tomcat 放入 request 作用域,因此 BasicErrorController 里也能获取到
- 具体异常信息会由 DefaultErrorAttributes 封装好
- BasicErrorController 通过 Accept 头判断需要生成哪种 MediaType 的响应
- 如果要的不是 text/html,走 MessageConverter 流程
- 如果需要 text/html,走 mvc 流程,此时又分两种情况
- 配置了 ErrorViewResolver,根据状态码去找 View
- 没配置或没找到,用 BeanNameViewResolver 根据一个固定为 error 的名字找到 View,即所谓的 WhitelabelErrorView
评价
- 一个错误处理搞得这么复杂,就问恶心不?
演示1 - 错误页处理
关键代码
1 | // ⬅️修改了 Tomcat 服务器默认错误地址, 出错时使用请求转发方式跳转 |
收获💡
- Tomcat 的错误页处理手段
演示2 - BasicErrorController
关键代码
1 | // ⬅️ErrorProperties 封装环境键值, ErrorAttributes 控制有哪些错误信息 |
收获💡
- Spring Boot 中 BasicErrorController 如何工作
33) BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
演示 - 本组映射器和适配器
关键代码
1 |
|
收获💡
- BeanNameUrlHandlerMapping,以 / 开头的 bean 的名字会被当作映射路径
- 这些 bean 本身当作 handler,要求实现 Controller 接口
- SimpleControllerHandlerAdapter,调用 handler
- 模拟实现这组映射器和适配器
34) RouterFunctionMapping 与 HandlerFunctionAdapter
演示 - 本组映射器和适配器
关键代码
1 |
|
收获💡
- RouterFunctionMapping, 通过 RequestPredicate 条件映射
- handler 要实现 HandlerFunction 接口
- HandlerFunctionAdapter, 调用 handler
35) SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
演示1 - 本组映射器和适配器
代码参考
org.springframework.boot.autoconfigure.web.servlet.A35
关键代码
1 |
|
收获💡
- SimpleUrlHandlerMapping 不会在初始化时收集映射信息,需要手动收集
- SimpleUrlHandlerMapping 映射路径
- ResourceHttpRequestHandler 作为静态资源 handler
- HttpRequestHandlerAdapter, 调用此 handler
演示2 - 静态资源解析优化
关键代码
1 |
|
收获💡
- 责任链模式体现
- 压缩文件需要手动生成
演示3 - 欢迎页
关键代码
1 |
|
收获💡
- 欢迎页支持静态欢迎页与动态欢迎页
- WelcomePageHandlerMapping 映射欢迎页(即只映射 ‘/‘)
- 它内置的 handler ParameterizableViewController 作用是不执行逻辑,仅根据视图名找视图
- 视图名固定为 forward:index.html
- SimpleControllerHandlerAdapter, 调用 handler
- 转发至 /index.html
- 处理 /index.html 又会走上面的静态资源处理流程
映射器与适配器小结
- HandlerMapping 负责建立请求与控制器之间的映射关系
- RequestMappingHandlerMapping (与 @RequestMapping 匹配)
- WelcomePageHandlerMapping (/)
- BeanNameUrlHandlerMapping (与 bean 的名字匹配 以 / 开头)
- RouterFunctionMapping (函数式 RequestPredicate, HandlerFunction)
- SimpleUrlHandlerMapping (静态资源 通配符 /** /img/**)
- 之间也会有顺序问题, boot 中默认顺序如上
- HandlerAdapter 负责实现对各种各样的 handler 的适配调用
- RequestMappingHandlerAdapter 处理:@RequestMapping 方法
- 参数解析器、返回值处理器体现了组合模式
- SimpleControllerHandlerAdapter 处理:Controller 接口
- HandlerFunctionAdapter 处理:HandlerFunction 函数式接口
- HttpRequestHandlerAdapter 处理:HttpRequestHandler 接口 (静态资源处理)
- 这也是典型适配器模式体现
- RequestMappingHandlerAdapter 处理:@RequestMapping 方法
36) mvc 处理流程
当浏览器发送一个请求 http://localhost:8080/hello
后,请求到达服务器,其处理流程是:
服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
- 路径:默认映射路径为
/
,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器- jsp 不会匹配到 DispatcherServlet
- 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
- 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean
- 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
- HandlerMapping,初始化时记录映射关系
- HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
- HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
- ViewResolver
- 路径:默认映射路径为
DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法
- 例如根据 /hello 路径找到 @RequestMapping(“/hello”) 对应的控制器方法
- 控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
- HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
DispatcherServlet 接下来会:
- 调用拦截器的 preHandle 方法
- RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
- @ControllerAdvice 全局增强点1️⃣:补充模型数据
- @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器
- 使用 HandlerMethodArgumentResolver 准备参数
- @ControllerAdvice 全局增强点3️⃣:RequestBody 增强
- 调用 ServletInvocableHandlerMethod
- 使用 HandlerMethodReturnValueHandler 处理返回值
- @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强
- 根据 ModelAndViewContainer 获取 ModelAndView
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 例如,有的返回值处理器调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
- 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 调用拦截器的 postHandle 方法
- 处理异常或视图渲染
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
- 正常,走视图解析及渲染流程
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- 调用拦截器的 afterCompletion 方法
Boot
37) Boot 骨架项目
如果是 linux 环境,用以下命令即可获取 spring boot 的骨架 pom.xml
1 | curl -G https://start.spring.io/pom.xml -d dependencies=web,mysql,mybatis -o pom.xml |
也可以使用 Postman 等工具实现
若想获取更多用法,请参考
1 | curl https://start.spring.io |
38) Boot War项目
步骤1:创建模块,区别在于打包方式选择 war
接下来勾选 Spring Web 支持
步骤2:编写控制器
1 |
|
步骤3:编写 jsp 视图,新建 webapp 目录和一个 hello.jsp 文件,注意文件名与控制器方法返回的视图逻辑名一致
1 | src |
步骤4:配置视图路径,打开 application.properties 文件
1 | spring.mvc.view.prefix=/ |
将来 prefix + 控制器方法返回值 + suffix 即为视图完整路径
测试
如果用 mvn 插件 mvn spring-boot:run
或 main 方法测试
- 必须添加如下依赖,因为此时用的还是内嵌 tomcat,而内嵌 tomcat 默认不带 jasper(用来解析 jsp)
1 | <dependency> |
也可以使用 Idea 配置 tomcat 来测试,此时用的是外置 tomcat
- 骨架生成的代码中,多了一个 ServletInitializer,它的作用就是配置外置 Tomcat 使用的,在外置 Tomcat 启动后,去调用它创建和运行 SpringApplication
启示
对于 jar 项目,若要支持 jsp,也可以在加入 jasper 依赖的前提下,把 jsp 文件置入 META-INF/resources
39) Boot 启动过程
阶段一:SpringApplication 构造
- 记录 BeanDefinition 源
- 推断应用类型
- 记录 ApplicationContext 初始化器
- 记录监听器
- 推断主启动类
阶段二:执行 run 方法
得到 SpringApplicationRunListeners,名字取得不好,实际是事件发布器
- 发布 application starting 事件1️⃣
封装启动 args
准备 Environment 添加命令行参数(*)
ConfigurationPropertySources 处理(*)
- 发布 application environment 已准备事件2️⃣
通过 EnvironmentPostProcessorApplicationListener 进行 env 后处理(*)
- application.properties,由 StandardConfigDataLocationResolver 解析
- spring.application.json
绑定 spring.main 到 SpringApplication 对象(*)
打印 banner(*)
创建容器
准备容器
- 发布 application context 已初始化事件3️⃣
加载 bean 定义
- 发布 application prepared 事件4️⃣
refresh 容器
- 发布 application started 事件5️⃣
执行 runner
- 发布 application ready 事件6️⃣
- 这其中有异常,发布 application failed 事件7️⃣
带 * 的有独立的示例
演示 - 启动过程
com.itheima.a39.A39_1 对应 SpringApplication 构造
com.itheima.a39.A39_2 对应第1步,并演示 7 个事件
com.itheima.a39.A39_3 对应第2、8到12步
org.springframework.boot.Step3
org.springframework.boot.Step4
org.springframework.boot.Step5
org.springframework.boot.Step6
org.springframework.boot.Step7
收获💡
- SpringApplication 构造方法中所做的操作
- 可以有多种源用来加载 bean 定义
- 应用类型推断
- 添加容器初始化器
- 添加监听器
- 演示主类推断
- 如何读取 spring.factories 中的配置
- 从配置中获取重要的事件发布器:SpringApplicationRunListeners
- 容器的创建、初始化器增强、加载 bean 定义等
- CommandLineRunner、ApplicationRunner 的作用
- 环境对象
- 命令行 PropertySource
- ConfigurationPropertySources 规范环境键名称
- EnvironmentPostProcessor 后处理增强
- 由 EventPublishingRunListener 通过监听事件2️⃣来调用
- 绑定 spring.main 前缀的 key value 至 SpringApplication
- Banner
40) Tomcat 内嵌容器
Tomcat 基本结构
1 | Server |
演示1 - Tomcat 内嵌容器
关键代码
1 | public static void main(String[] args) throws LifecycleException, IOException { |
演示2 - 集成 Spring 容器
关键代码
1 | WebApplicationContext springContext = getApplicationContext(); |
41) Boot 自动配置
AopAutoConfiguration
Spring Boot 是利用了自动配置类来简化了 aop 相关配置
- AOP 自动配置类为
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
- 可以通过
spring.aop.auto=false
禁用 aop 自动配置 - AOP 自动配置的本质是通过
@EnableAspectJAutoProxy
来开启了自动代理,如果在引导类上自己添加了@EnableAspectJAutoProxy
那么以自己添加的为准 @EnableAspectJAutoProxy
的本质是向容器中添加了AnnotationAwareAspectJAutoProxyCreator
这个 bean 后处理器,它能够找到容器中所有切面,并为匹配切点的目标类创建代理,创建代理的工作一般是在 bean 的初始化阶段完成的
DataSourceAutoConfiguration
- 对应的自动配置类为:org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- 它内部采用了条件装配,通过检查容器的 bean,以及类路径下的 class,来决定该 @Bean 是否生效
简单说明一下,Spring Boot 支持两大类数据源:
- EmbeddedDatabase - 内嵌数据库连接池
- PooledDataSource - 非内嵌数据库连接池
PooledDataSource 又支持如下数据源
- hikari 提供的 HikariDataSource
- tomcat-jdbc 提供的 DataSource
- dbcp2 提供的 BasicDataSource
- oracle 提供的 PoolDataSourceImpl
如果知道数据源的实现类类型,即指定了 spring.datasource.type
,理论上可以支持所有数据源,但这样做的一个最大问题是无法订制每种数据源的详细配置(如最大、最小连接数等)
MybatisAutoConfiguration
- MyBatis 自动配置类为
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
- 它主要配置了两个 bean
- SqlSessionFactory - MyBatis 核心对象,用来创建 SqlSession
- SqlSessionTemplate - SqlSession 的实现,此实现会与当前线程绑定
- 用 ImportBeanDefinitionRegistrar 的方式扫描所有标注了 @Mapper 注解的接口
- 用 AutoConfigurationPackages 来确定扫描的包
- 还有一个相关的 bean:MybatisProperties,它会读取配置文件中带
mybatis.
前缀的配置项进行定制配置
@MapperScan 注解的作用与 MybatisAutoConfiguration 类似,会注册 MapperScannerConfigurer 有如下区别
- @MapperScan 扫描具体包(当然也可以配置关注哪个注解)
- @MapperScan 如果不指定扫描具体包,则会把引导类范围内,所有接口当做 Mapper 接口
- MybatisAutoConfiguration 关注的是所有标注 @Mapper 注解的接口,会忽略掉非 @Mapper 标注的接口
这里有同学有疑问,之前介绍的都是将具体类交给 Spring 管理,怎么到了 MyBatis 这儿,接口就可以被管理呢?
- 其实并非将接口交给 Spring 管理,而是每个接口会对应一个 MapperFactoryBean,是后者被 Spring 所管理,接口只是作为 MapperFactoryBean 的一个属性来配置
TransactionAutoConfiguration
事务自动配置类有两个:
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
前者配置了 DataSourceTransactionManager 用来执行事务的提交、回滚操作
后者功能上对标 @EnableTransactionManagement,包含以下三个 bean
- BeanFactoryTransactionAttributeSourceAdvisor 事务切面类,包含通知和切点
- TransactionInterceptor 事务通知类,由它在目标方法调用前后加入事务操作
- AnnotationTransactionAttributeSource 会解析 @Transactional 及事务属性,也包含了切点功能
如果自己配置了 DataSourceTransactionManager 或是在引导类加了 @EnableTransactionManagement,则以自己配置的为准
ServletWebServerFactoryAutoConfiguration
- 提供 ServletWebServerFactory
DispatcherServletAutoConfiguration
- 提供 DispatcherServlet
- 提供 DispatcherServletRegistrationBean
WebMvcAutoConfiguration
- 配置 DispatcherServlet 的各项组件,提供的 bean 见过的有
- 多项 HandlerMapping
- 多项 HandlerAdapter
- HandlerExceptionResolver
ErrorMvcAutoConfiguration
- 提供的 bean 有 BasicErrorController
MultipartAutoConfiguration
- 它提供了 org.springframework.web.multipart.support.StandardServletMultipartResolver
- 该 bean 用来解析 multipart/form-data 格式的数据
HttpEncodingAutoConfiguration
- POST 请求参数如果有中文,无需特殊设置,这是因为 Spring Boot 已经配置了 org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter
- 对应配置 server.servlet.encoding.charset=UTF-8,默认就是 UTF-8
- 当然,它只影响非 json 格式的数据
演示 - 自动配置类原理
关键代码
假设已有第三方的两个自动配置类
1 | // ⬅️第三方的配置类 |
提供一个配置文件 META-INF/spring.factories,key 为导入器类名,值为多个自动配置类名,用逗号分隔
1 | MyImportSelector=\ |
注意
- 上述配置文件中 MyImportSelector 与 AutoConfiguration1,AutoConfiguration2 为简洁均省略了包名,自己测试时请将包名根据情况补全
引入自动配置
1 | // ⬅️本项目的配置类 |
收获💡
- 自动配置类本质上就是一个配置类而已,只是用 META-INF/spring.factories 管理,与应用配置类解耦
- @Enable 打头的注解本质是利用了 @Import
- @Import 配合 DeferredImportSelector 即可实现导入,selectImports 方法的返回值即为要导入的配置类名
- DeferredImportSelector 的导入会在最后执行,为的是让其它配置优先解析
42) 条件装配底层
条件装配的底层是本质上是 @Conditional 与 Condition,这两个注解。引入自动配置类时,期望满足一定条件才能被 Spring 管理,不满足则不管理,怎么做呢?
比如条件是【类路径下必须有 dataSource】这个 bean ,怎么做呢?
首先编写条件判断类,它实现 Condition 接口,编写条件判断逻辑
1 | static class MyCondition1 implements Condition { |
其次,在要导入的自动配置类上添加 @Conditional(MyCondition1.class)
,将来此类被导入时就会做条件检查
1 | // 第三方的配置类 |
分别测试加入和去除 druid 依赖,观察 bean1 是否存在于容器
1 | <dependency> |
收获💡
- 学习一种特殊的 if - else
其它
43) FactoryBean
演示 - FactoryBean
代码参考
com.itheima.a43 包
收获💡
- 它的作用是用制造创建过程较为复杂的产品, 如 SqlSessionFactory, 但 @Bean 已具备等价功能
- 使用上较为古怪, 一不留神就会用错
- 被 FactoryBean 创建的产品
- 会认为创建、依赖注入、Aware 接口回调、前初始化这些都是 FactoryBean 的职责, 这些流程都不会走
- 唯有后初始化的流程会走, 也就是产品可以被代理增强
- 单例的产品不会存储于 BeanFactory 的 singletonObjects 成员中, 而是另一个 factoryBeanObjectCache 成员中
- 按名字去获取时, 拿到的是产品对象, 名字前面加 & 获取的是工厂对象
- 被 FactoryBean 创建的产品
44) @Indexed 原理
真实项目中,只需要加入以下依赖即可
1 | <dependency> |
演示 - @Indexed
代码参考
com.itheima.a44 包
收获💡
- 在编译时就根据 @Indexed 生成 META-INF/spring.components 文件
- 扫描时
- 如果发现 META-INF/spring.components 存在, 以它为准加载 bean definition
- 否则, 会遍历包下所有 class 资源 (包括 jar 内的)
- 解决的问题,在编译期就找到 @Component 组件,节省运行期间扫描 @Component 的时间
45) 代理进一步理解
演示 - 代理
代码参考
com.itheima.a45 包
收获💡
spring 代理的设计特点
依赖注入和初始化影响的是原始对象
- 因此 cglib 不能用 MethodProxy.invokeSuper()
代理与目标是两个对象,二者成员变量并不共用数据
static 方法、final 方法、private 方法均无法增强
- 进一步理解代理增强基于方法重写
46) @Value 装配底层
按类型装配的步骤
- 查看需要的类型是否为 Optional,是,则进行封装(非延迟),否则向下走
- 查看需要的类型是否为 ObjectFactory 或 ObjectProvider,是,则进行封装(延迟),否则向下走
- 查看需要的类型(成员或参数)上是否用 @Lazy 修饰,是,则返回代理,否则向下走
- 解析 @Value 的值
- 如果需要的值是字符串,先解析 ${ },再解析 #{ }
- 不是字符串,需要用 TypeConverter 转换
- 看需要的类型是否为 Stream、Array、Collection、Map,是,则按集合处理,否则向下走
- 在 BeanFactory 的 resolvableDependencies 中找有没有类型合适的对象注入,没有向下走
- 在 BeanFactory 及父工厂中找类型匹配的 bean 进行筛选,筛选时会考虑 @Qualifier 及泛型
- 结果个数为 0 抛出 NoSuchBeanDefinitionException 异常
- 如果结果 > 1,再根据 @Primary 进行筛选
- 如果结果仍 > 1,再根据成员名或变量名进行筛选
- 结果仍 > 1,抛出 NoUniqueBeanDefinitionException 异常
演示 - @Value 装配过程
代码参考
com.itheima.a46 包
收获💡
- ContextAnnotationAutowireCandidateResolver 作用之一,获取 @Value 的值
- 了解 ${ } 对应的解析器
- 了解 #{ } 对应的解析器
- TypeConvert 的一项体现
47) @Autowired 装配底层
演示 - @Autowired 装配过程
代码参考
com.itheima.a47 包
收获💡
- @Autowired 本质上是根据成员变量或方法参数的类型进行装配
- 如果待装配类型是 Optional,需要根据 Optional 泛型找到 bean,再封装为 Optional 对象装配
- 如果待装配的类型是 ObjectFactory,需要根据 ObjectFactory 泛型创建 ObjectFactory 对象装配
- 此方法可以延迟 bean 的获取
- 如果待装配的成员变量或方法参数上用 @Lazy 标注,会创建代理对象装配
- 此方法可以延迟真实 bean 的获取
- 被装配的代理不作为 bean
- 如果待装配类型是数组,需要获取数组元素类型,根据此类型找到多个 bean 进行装配
- 如果待装配类型是 Collection 或其子接口,需要获取 Collection 泛型,根据此类型找到多个 bean
- 如果待装配类型是 ApplicationContext 等特殊类型
- 会在 BeanFactory 的 resolvableDependencies 成员按类型查找装配
- resolvableDependencies 是 map 集合,key 是特殊类型,value 是其对应对象
- 不能直接根据 key 进行查找,而是用 isAssignableFrom 逐一尝试右边类型是否可以被赋值给左边的 key 类型
- 如果待装配类型有泛型参数
- 需要利用 ContextAnnotationAutowireCandidateResolver 按泛型参数类型筛选
- 如果待装配类型有 @Qualifier
- 需要利用 ContextAnnotationAutowireCandidateResolver 按注解提供的 bean 名称筛选
- 有 @Primary 标注的 @Component 或 @Bean 的处理
- 与成员变量名或方法参数名同名 bean 的处理
48) 事件监听器
演示 - 事件监听器
代码参考
com.itheima.a48 包
收获💡
事件监听器的两种方式
- 实现 ApplicationListener 接口
- 根据接口泛型确定事件类型
- @EventListener 标注监听方法
- 根据监听器方法参数确定事件类型
- 解析时机:在 SmartInitializingSingleton(所有单例初始化完成后),解析每个单例 bean
49) 事件发布器
演示 - 事件发布器
代码参考
com.itheima.a49 包
收获💡
事件发布器模拟实现
- addApplicationListenerBean 负责收集容器中的监听器
- 监听器会统一转换为 GenericApplicationListener 对象,以支持判断事件类型
- multicastEvent 遍历监听器集合,发布事件
- 发布前先通过 GenericApplicationListener.supportsEventType 判断支持该事件类型才发事件
- 可以利用线程池进行异步发事件优化
- 如果发送的事件对象不是 ApplicationEvent 类型,Spring 会把它包装为 PayloadApplicationEvent 并用泛型技术解析事件对象的原始类型
- 视频中未讲解