目录
容器与 bean
1) 容器接口
演示1 - BeanFactory 与 ApplicationContext 的区别
代码参考
收获💡
演示2 - 国际化
2) 容器实现
演示1 - DefaultListableBeanFactory
代码参考
收获💡
演示2 - 常见 ApplicationContext 实现
代码参考
收获💡
3) Bean 的生命周期
演示1 - bean 生命周期
代码参考
收获💡
演示2 - 模板方法设计模式
关键代码
演示3 - bean 后处理器排序
代码参考
收获💡
4) Bean 后处理器
演示1 - 后处理器作用
代码参考
收获💡
演示2 - @Autowired bean 后处理器运行分析
代码参考
收获💡
5) BeanFactory 后处理器
演示1 - BeanFactory 后处理器的作用
代码参考
收获💡
演示2 - 模拟解析 @ComponentScan
代码参考
收获💡
演示3 - 模拟解析 @Bean
代码参考
收获💡
演示4 - 模拟解析 Mapper 接口
代码参考
收获💡
6) Aware 接口
演示 - Aware 接口及 InitializingBean 接口
代码参考
收获💡
配置类 @Autowired 失效分析
7) 初始化与销毁
演示 - 初始化销毁顺序
代码参考
收获💡
8) Scope
演示1 - request, session, application 作用域
代码参考
收获💡
分析 - singleton 注入其它 scope 失效
演示2 - 4种解决方法
代码参考
收获💡
AOP
9) AOP 实现之 ajc 编译器
收获💡
10) AOP 实现之 agent 类加载
收获💡
11) AOP 实现之 proxy
演示1 - jdk 动态代理
收获💡
演示2 - cglib 代理
收获💡
12) jdk 动态代理进阶
演示1 - 模拟 jdk 动态代理
收获💡
演示2 - 方法反射优化
代码参考
收获💡
13) cglib 代理进阶
演示 - 模拟 cglib 代理
代码参考
收获💡
14) cglib 避免反射调用
演示 - cglib 如何避免反射
代码参考
收获💡
15) jdk 和 cglib 在 Spring 中的统一
演示 - 底层切点、通知、切面
代码参考
收获💡
16) 切点匹配
演示 - 切点匹配
代码参考
收获💡
17) 从 @Aspect 到 Advisor
演示1 - 代理创建器
代码参考
收获💡
演示2 - 代理创建时机
代码参考
收获💡
演示3 - @Before 对应的低级通知
代码参考
收获💡
18) 静态通知调用
演示1 - 通知调用过程
代码参考
收获💡
演示2 - 模拟 MethodInvocation
代码参考
收获💡
19) 动态通知调用
演示 - 带参数绑定的通知方法调用
代码参考
收获💡
WEB
20) RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
演示1 - DispatcherServlet 初始化
代码参考
收获💡
演示2 - 自定义参数与返回值处理器
代码参考
收获💡
21) 参数解析器
演示 - 常见参数解析器
代码参考
收获💡
22) 参数名解析
演示 - 两种方法获取参数名
代码参考
收获💡
23) 对象绑定与类型转换
底层第一套转换接口与实现
底层第二套转换接口
高层接口与实现
演示1 - 类型转换与数据绑定
代码参考
收获💡
演示2 - 数据绑定工厂
代码参考
收获💡
演示3 - 获取泛型参数
代码参考
收获💡
24) @ControllerAdvice 之 @InitBinder
演示 - 准备 @InitBinder
收获💡
25) 控制器方法执行流程
图1
图2
图3
26) @ControllerAdvice 之 @ModelAttribute
演示 - 准备 @ModelAttribute
代码参考
收获💡
27) 返回值处理器
演示 - 常见返回值处理器
代码参考
收获💡
28) MessageConverter
演示 - MessageConverter 的作用
代码参考
收获💡
29) @ControllerAdvice 之 ResponseBodyAdvice
演示 - ResponseBodyAdvice 增强
代码参考
收获💡
30) 异常解析器
演示 - ExceptionHandlerExceptionResolver
代码参考
收获💡
31) @ControllerAdvice 之 @ExceptionHandler
演示 - 准备 @ExceptionHandler
代码参考
收获💡
32) Tomcat 异常处理
演示1 - 错误页处理
关键代码
收获💡
演示2 - BasicErrorController
关键代码
收获💡
33) BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
演示 - 本组映射器和适配器
关键代码
收获💡
34) RouterFunctionMapping 与 HandlerFunctionAdapter
演示 - 本组映射器和适配器
关键代码
收获💡
35) SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
演示1 - 本组映射器和适配器
代码参考
关键代码
收获💡
演示2 - 静态资源解析优化
关键代码
收获💡
演示3 - 欢迎页
关键代码
收获💡
映射器与适配器小结
36) mvc 处理流程
Boot
37) Boot 骨架项目
38) Boot War项目
测试
启示
39) Boot 启动过程
演示 - 启动过程
收获💡
40) Tomcat 内嵌容器
演示1 - Tomcat 内嵌容器
关键代码
演示2 - 集成 Spring 容器
关键代码
41) Boot 自动配置
AopAutoConfiguration
DataSourceAutoConfiguration
MybatisAutoConfiguration
TransactionAutoConfiguration
ServletWebServerFactoryAutoConfiguration
DispatcherServletAutoConfiguration
WebMvcAutoConfiguration
ErrorMvcAutoConfiguration
MultipartAutoConfiguration
HttpEncodingAutoConfiguration
演示 - 自动配置类原理
关键代码
收获💡
42) 条件装配底层
收获💡
其它
43) FactoryBean
演示 - FactoryBean
代码参考
收获💡
44) @Indexed 原理
演示 - @Indexed
代码参考
收获💡
45) 代理进一步理解
演示 - 代理
代码参考
收获💡
46) @Value 装配底层
按类型装配的步骤
演示 - @Value 装配过程
代码参考
收获💡
47) @Autowired 装配底层
演示 - @Autowired 装配过程
代码参考
收获💡
48) 事件监听器
演示 - 事件监听器
代码参考
收获💡
49) 事件发布器
演示 - 事件发布器
代码参考
收获💡
容器与 bean
1) 容器接口
-
BeanFactory 接口,典型功能有:
-
getBean
-
-
ApplicationContext 接口,是 BeanFactory 的子接口。它扩展了 BeanFactory 接口的功能,如:
-
国际化
-
通配符方式获取一组 Resource 资源
-
整合 Environment 环境(能通过它获取各种来源的配置信息)
-
事件发布与监听,实现组件之间的解耦
-
可以看到,我们课上讲的,都是 BeanFactory 提供的基本功能,ApplicationContext 中的扩展功能都没有用到。
演示1 - BeanFactory 与 ApplicationContext 的区别
代码参考
com.a.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 - 国际化
public class TestMessageSource {public static void main(String[] args) {GenericApplicationContext context = new GenericApplicationContext();context.registerBean("messageSource", MessageSource.class, () -> {ResourceBundleMessageSource ms = new ResourceBundleMessageSource();ms.setDefaultEncoding("utf-8");ms.setBasename("messages");return ms;});context.refresh();System.out.println(context.getMessage("hi", null, Locale.ENGLISH));System.out.println(context.getMessage("hi", null, Locale.CHINESE));System.out.println(context.getMessage("hi", null, Locale.JAPANESE));}}
国际化文件均在 src/resources 目录下
messages.properties(空)
messages_en.properties
hi=Hello
messages_ja.properties
hi=こんにちは
messages_zh.properties
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.a.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.a.a02.A02
收获💡
-
常见的 ApplicationContext 容器实现
-
内嵌容器、DispatcherServlet 的创建方法、作用
3) Bean 的生命周期
一个受 Spring 管理的 bean,生命周期主要阶段有
-
创建:根据 bean 的构造方法或者工厂方法来创建 bean 实例对象
-
依赖注入:根据 @Autowired,@Value 或其它一些手段,为 bean 的成员变量填充值、建立关系
-
初始化:回调各种 Aware 接口,调用对象的各种初始化方法
-
销毁:在容器关闭时,会销毁所有单例对象(即调用它们的销毁方法)
-
prototype 对象也能够销毁,不过需要容器这边主动调用
-
一些资料会提到,生命周期中还有一类 bean 后处理器:BeanPostProcessor,会在 bean 的初始化的前后,提供一些扩展逻辑。但这种说法是不完整的,见下面的演示1
演示1 - bean 生命周期
代码参考
a.a.03 包
创建
依赖注入
初始化
可用
销毁
创建前后的增强
-
postProcessBeforeInstantiation
-
这里返回的对象若不为 null 会替换掉原本的 bean,并且仅会走 postProcessAfterInitialization 流程
-
-
postProcessAfterInstantiation
-
这里如果返回 false 会跳过依赖注入阶段
-
依赖注入前的增强
-
postProcessProperties
-
如 @Autowired、@Value、@Resource
-
初始化前后的增强
-
postProcessBeforeInitialization
-
这里返回的对象会替换掉原本的 bean
-
如 @PostConstruct、@ConfigurationProperties
-
-
postProcessAfterInitialization
-
这里返回的对象会替换掉原本的 bean
-
如代理增强
-
销毁之前的增强
-
postProcessBeforeDestruction
-
如 @PreDestroy
-
收获💡
-
Spring bean 生命周期各个阶段
-
模板设计模式, 指大流程已经固定好了, 通过接口回调(bean 后处理器)在一些关键点前后提供扩展
演示2 - 模板方法设计模式
关键代码
public class TestMethodTemplate {public static void main(String[] args) {MyBeanFactory beanFactory = new MyBeanFactory();beanFactory.addBeanPostProcessor(bean -> System.out.println("解析 @Autowired"));beanFactory.addBeanPostProcessor(bean -> System.out.println("解析 @Resource"));beanFactory.getBean();}// 模板方法 Template Method Patternstatic class MyBeanFactory {public Object getBean() {Object bean = new Object();System.out.println("构造 " + bean);System.out.println("依赖注入 " + bean); // @Autowired, @Resourcefor (BeanPostProcessor processor : processors) {processor.inject(bean);}System.out.println("初始化 " + bean);return bean;}private List<BeanPostProcessor> processors = new ArrayList<>();public void addBeanPostProcessor(BeanPostProcessor processor) {processors.add(processor);}}static interface BeanPostProcessor {public void inject(Object bean); // 对依赖注入阶段的扩展}}
演示3 - bean 后处理器排序
代码参考
com.a.a03.TestProcessOrder
收获💡
-
实现了 PriorityOrdered 接口的优先级最高
-
实现了 Ordered 接口与加了 @Order 注解的平级, 按数字升序
-
其它的排在最后
4) Bean 后处理器
演示1 - 后处理器作用
代码参考
com.a.a04 包
收获💡
-
@Autowired 等注解的解析属于 bean 生命周期阶段(依赖注入, 初始化)的扩展功能,这些扩展功能由 bean 后处理器来完成
-
每个后处理器各自增强什么功能
-
AutowiredAnnotationBeanPostProcessor 解析 @Autowired 与 @Value
-
CommonAnnotationBeanPostProcessor 解析 @Resource、@PostConstruct、@PreDestroy
-
ConfigurationPropertiesBindingPostProcessor 解析 @ConfigurationProperties
-
-
另外 ContextAnnotationAutowireCandidateResolver 负责获取 @Value 的值,解析 @Qualifier、泛型、@Lazy 等
演示2 - @Autowired bean 后处理器运行分析
代码参考
com.a.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.a.a05.ComponentScanPostProcessor
收获💡
-
Spring 操作元数据的工具类 CachingMetadataReaderFactory
-
通过注解元数据(AnnotationMetadata)获取直接或间接标注的注解信息
-
通过类元数据(ClassMetadata)获取类名,AnnotationBeanNameGenerator 生成 bean 名
-
解析元数据是基于 ASM 技术
演示3 - 模拟解析 @Bean
代码参考
com.a.a05.AtBeanPostProcessor
收获💡
-
进一步熟悉注解元数据(AnnotationMetadata)获取方法上注解信息
演示4 - 模拟解析 Mapper 接口
代码参考
com.a.a05.MapperPostProcessor
收获💡
-
Mapper 接口被 Spring 管理的本质:实际是被作为 MapperFactoryBean 注册到容器中
-
Spring 的诡异做法,根据接口生成的 BeanDefinition 仅为根据接口名生成 bean 名
6) Aware 接口
演示 - Aware 接口及 InitializingBean 接口
代码参考
com.a.a06 包
收获💡
-
Aware 接口提供了一种【内置】 的注入手段,例如
-
BeanNameAware 注入 bean 的名字
-
BeanFactoryAware 注入 BeanFactory 容器
-
ApplicationContextAware 注入 ApplicationContext 容器
-
EmbeddedValueResolverAware 注入 ${} 解析器
-
-
InitializingBean 接口提供了一种【内置】的初始化手段
-
对比
-
内置的注入和初始化不受扩展功能的影响,总会被执行
-
而扩展功能受某些情况影响可能会失效
-
因此 Spring 框架内部的类常用内置注入和初始化
-
配置类 @Autowired 失效分析
Java 配置类不包含 BeanFactoryPostProcessor 的情况
ApplicationContextBeanFactoryPostProcessorBeanPostProcessorJava配置类1. 执行 BeanFactoryPostProcessor2. 注册 BeanPostProcessor3. 创建和初始化3.1 依赖注入扩展(如 @Value 和 @Autowired)3.2 初始化扩展(如 @PostConstruct)3.3 执行 Aware 及 InitializingBean3.4 创建成功ApplicationContextBeanFactoryPostProcessorBeanPostProcessorJava配置类
Java 配置类包含 BeanFactoryPostProcessor 的情况,因此要创建其中的 BeanFactoryPostProcessor 必须提前创建 Java 配置类,而此时的 BeanPostProcessor 还未准备好,导致 @Autowired 等注解失效
ApplicationContextBeanFactoryPostProcessorBeanPostProcessorJava配置类3. 创建和初始化3.1 执行 Aware 及 InitializingBean3.2 创建成功1. 执行 BeanFactoryPostProcessor2. 注册 BeanPostProcessorApplicationContextBeanFactoryPostProcessorBeanPostProcessorJava配置类
对应代码
@Configurationpublic class MyConfig1 {private static final Logger log = LoggerFactory.getLogger(MyConfig1.class);@Autowiredpublic void setApplicationContext(ApplicationContext applicationContext) {log.debug("注入 ApplicationContext");}@PostConstructpublic void init() {log.debug("初始化");}@Bean // ⬅️ 注释或添加 beanFactory 后处理器对应上方两种情况public BeanFactoryPostProcessor processor1() {return beanFactory -> {log.debug("执行 processor1");};}}
注意
解决方法:
用内置依赖注入和初始化取代扩展依赖注入和初始化
用静态工厂方法代替实例工厂方法,避免工厂对象提前被创建
7) 初始化与销毁
演示 - 初始化销毁顺序
代码参考
com.a.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.a.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
@Componentpublic class E {private static final Logger log = LoggerFactory.getLogger(E.class);private F f;public E() {log.info("E()");}@Autowiredpublic void setF(F f) {this.f = f;log.info("setF(F f) {}", f.getClass());}public F getF() {return f;}}
要注入的对象 F 期望是多例
@Component@Scope("prototype")public class F {private static final Logger log = LoggerFactory.getLogger(F.class);public F() {log.info("F()");}}
测试
E e = context.getBean(E.class); F f1 = e.getF(); F f2 = e.getF(); System.out.println(f1); System.out.println(f2);
输出
com.itheima.demo.cycle.F@6622fc65 com.itheima.demo.cycle.F@6622fc65
发现它们是同一个对象,而不是期望的多例对象
对于单例对象来讲,依赖注入仅发生了一次,后续再没有用到多例的 F,因此 E 用的始终是第一次依赖注入的 F
[Mermaid]Parse error on line 8: ...)f1(f 创建)e1-->f1-->e2 -------------------^ Expecting 'SEMI', 'NEWLINE', 'EOF', got 'ARROW_POINT'
解决
仍然使用 @Lazy 生成代理
代理对象虽然还是同一个,但当每次使用代理对象的任意方法时,由代理创建新的 f 对象
使用f方法
使用f方法
使用f方法
e 创建
e set 注入 f代理
f 创建
@Component public class E {@Autowired@Lazypublic void setF(F f) {this.f = f;log.info("setF(F f) {}", f.getClass());}// ... }
注意
@Lazy 加在也可以加在成员变量上,但加在 set 方法上的目的是可以观察输出,加在成员变量上就不行了
@Autowired 加在 set 方法的目的类似
输出
E: setF(F f) class com.itheima.demo.cycle.F$$EnhancerBySpringCGLIB$$8b54f2bc F: F() com.itheima.demo.cycle.F@3a6f2de3 F: F() com.itheima.demo.cycle.F@56303b57
从输出日志可以看到调用 setF 方法时,f 对象的类型是代理类型
演示2 - 4种解决方法
代码参考
com.a.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 动态代理
public class JdkProxyDemo {interface Foo {void foo();}static class Target implements Foo {public void foo() {System.out.println("target foo");}}public static void main(String[] param) {// 目标对象Target target = new Target();// 代理对象Foo proxy = (Foo) Proxy.newProxyInstance(Target.class.getClassLoader(), new Class[]{Foo.class},(p, method, args) -> {System.out.println("proxy before...");Object result = method.invoke(target, args);System.out.println("proxy after...");return result;});// 调用代理proxy.foo();}
}
运行结果
proxy before... target foo proxy after...
收获💡
-
jdk 动态代理要求目标必须实现接口,生成的代理类实现相同接口,因此代理与目标之间是平级兄弟关系
演示2 - cglib 代理
public class CglibProxyDemo {static class Target {public void foo() {System.out.println("target foo");}}public static void main(String[] param) {// 目标对象Target target = new Target();// 代理对象Target proxy = (Target) Enhancer.create(Target.class, (MethodInterceptor) (p, method, args, methodProxy) -> {System.out.println("proxy before...");Object result = methodProxy.invoke(target, args);// 另一种调用方法,不需要目标对象实例
// Object result = methodProxy.invokeSuper(p, args);System.out.println("proxy after...");return result;});// 调用代理proxy.foo();}
}
运行结果与 jdk 动态代理相同
收获💡
-
cglib 不要求目标实现接口,它生成的代理类是目标的子类,因此代理与目标之间是子父关系
-
限制⛔:根据上述分析 final 类无法被 cglib 增强
12) jdk 动态代理进阶
演示1 - 模拟 jdk 动态代理
public class A12 {interface Foo {void foo();int bar();}static class Target implements Foo {public void foo() {System.out.println("target foo");}public int bar() {System.out.println("target bar");return 100;}}public static void main(String[] param) {// ⬇️1. 创建代理,这时传入 InvocationHandlerFoo proxy = new $Proxy0(new InvocationHandler() { // ⬇️5. 进入 InvocationHandlerpublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable{// ⬇️6. 功能增强System.out.println("before...");// ⬇️7. 反射调用目标方法return method.invoke(new Target(), args);}});// ⬇️2. 调用代理方法proxy.foo();proxy.bar();}
}
模拟代理实现
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException;
// ⬇️这就是 jdk 代理类的源码, 秘密都在里面
public class $Proxy0 extends Proxy implements A12.Foo {public $Proxy0(InvocationHandler h) {super(h);}// ⬇️3. 进入代理方法public void foo() {try {// ⬇️4. 回调 InvocationHandlerh.invoke(this, foo, new Object[0]);} catch (RuntimeException | Error e) {throw e;} catch (Throwable e) {throw new UndeclaredThrowableException(e);}}@Overridepublic int bar() {try {Object result = h.invoke(this, bar, new Object[0]);return (int) result;} catch (RuntimeException | Error e) {throw e;} catch (Throwable e) {throw new UndeclaredThrowableException(e);}}static Method foo;static Method bar;static {try {foo = A12.Foo.class.getMethod("foo");bar = A12.Foo.class.getMethod("bar");} catch (NoSuchMethodException e) {throw new NoSuchMethodError(e.getMessage());}}
}
收获💡
代理一点都不难,无非就是利用了多态、反射的知识
-
方法重写可以增强逻辑,只不过这【增强逻辑】千变万化,不能写死在代理内部
-
通过接口回调将【增强逻辑】置于代理类之外
-
配合接口方法反射(是多态调用),就可以再联动调用目标方法
-
会用 arthas 的 jad 工具反编译代理类
-
限制⛔:代理增强是借助多态来实现,因此成员变量、静态方法、final 方法均不能通过代理实现
演示2 - 方法反射优化
代码参考
com.a.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.a.a13 包
收获💡
和 jdk 动态代理原理查不多
-
回调的接口换了一下,InvocationHandler 改成了 MethodInterceptor
-
调用目标时有所改进,见下面代码片段
-
method.invoke 是反射调用,必须调用到足够次数才会进行优化
-
methodProxy.invoke 是不反射调用,它会正常(间接)调用目标对象的方法(Spring 采用)
-
methodProxy.invokeSuper 也是不反射调用,它会正常(间接)调用代理对象的方法,可以省略目标对象
-
public class A14Application {public static void main(String[] args) throws InvocationTargetException {Target target = new Target();Proxy proxy = new Proxy();proxy.setCallbacks(new Callback[]{(MethodInterceptor) (p, m, a, mp) -> {System.out.println("proxy before..." + mp.getSignature());// ⬇️调用目标方法(三种)
// Object result = m.invoke(target, a); // ⬅️反射调用
// Object result = mp.invoke(target, a); // ⬅️非反射调用, 结合目标用Object result = mp.invokeSuper(p, a); // ⬅️非反射调用, 结合代理用System.out.println("proxy after..." + mp.getSignature());return result;}});// ⬇️调用代理方法proxy.save();}
}
注意
调用 Object 的方法, 后两种在 jdk >= 9 时都有问题, 需要 --add-opens java.base/java.lang=ALL-UNNAMED
14) cglib 避免反射调用
演示 - cglib 如何避免反射
代码参考
com.a.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
[Mermaid]Parse error on line 14: ...sor o-- "一" Advice<<interface>> Advice ---------------------^ Expecting 'CLASS', 'MEMBER', 'SEPARATOR', 'UNICODE_TEXT', 'NUM', 'ALPHA', got 'DEPENDENCY'
代理相关类图
-
AopProxyFactory 根据 proxyTargetClass 等设置选择 AopProxy 实现
-
AopProxy 通过 getProxy 创建代理对象
-
图中 Proxy 都实现了 Advised 接口,能够获得关联的切面集合与目标(其实是从 ProxyFactory 取得)
-
调用代理方法时,会借助 ProxyFactory 将通知统一转为环绕通知:MethodInterceptor
[Mermaid]Parse error on line 32: ...d : ProxyFactory}<<interface>> Advise --------------------^ Expecting 'CLASS', 'MEMBER', 'SEPARATOR', 'UNICODE_TEXT', 'NUM', 'ALPHA', got 'DEPENDENCY'
演示 - 底层切点、通知、切面
代码参考
com.a.a15.A15
收获💡
-
底层的切点实现
-
底层的通知实现
-
底层的切面实现
-
ProxyFactory 用来创建代理
-
如果指定了接口,且 proxyTargetClass = false,使用 JdkDynamicAopProxy
-
如果没有指定接口,或者 proxyTargetClass = true,使用 ObjenesisCglibAopProxy
-
例外:如果目标是接口类型或已经是 Jdk 代理,使用 JdkDynamicAopProxy
-
-
注意
要区分本章节提到的 MethodInterceptor,它与之前 cglib 中用的的 MethodInterceptor 是不同的接口
16) 切点匹配
演示 - 切点匹配
代码参考
com.a.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返回最终的结果
图中不同颜色对应一次环绕通知或目标的调用起始至终结
[Mermaid]Parse error on line 17: ... -ih : rect rgb(200, 223, 255)ih ->> + ----------------------^ Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', got ','
演示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.a.a20 包
收获💡
-
DispatcherServlet 是在第一次被访问时执行初始化, 也可以通过配置修改为 Tomcat 启动后就初始化
-
在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化
-
RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,其中
-
key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
-
value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
-
有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet
-
-
RequestMappingHandlerAdapter 初始化时,会准备 HandlerMethod 调用时需要的各个组件,如:
-
HandlerMethodArgumentResolver 解析控制器方法参数
-
HandlerMethodReturnValueHandler 处理控制器方法返回值
-
演示2 - 自定义参数与返回值处理器
代码参考
com.a.a20.TokenArgumentResolver ,com.itheima.a20.YmlReturnValueHandler
收获💡
-
体会参数解析器的作用
-
体会返回值处理器的作用
21) 参数解析器
演示 - 常见参数解析器
代码参考
com.a.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.a.a22.A22
收获💡
-
如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名
-
如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
-
普通类, 会包含局部变量表, 用 asm 可以拿到参数名
-
接口, 不会包含局部变量表, 无法获得参数名
-
这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
-
-
23) 对象绑定与类型转换
底层第一套转换接口与实现
[Mermaid]Parse error on line 24: ...er3 --> Converters<<interface>> Format ---------------------^ Expecting 'CLASS', 'MEMBER', 'SEPARATOR', 'UNICODE_TEXT', 'NUM', 'ALPHA', got 'DEPENDENCY'
-
Printer 把其它类型转为 String
-
Parser 把 String 转为其它类型
-
Formatter 综合 Printer 与 Parser 功能
-
Converter 把类型 S 转为类型 T
-
Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
-
FormattingConversionService 利用其它们实现转换
底层第二套转换接口
[Mermaid]Parse error on line 5: ..."多" PropertyEditor<<interface>> Proper ---------------------^ Expecting 'CLASS', 'MEMBER', 'SEPARATOR', 'UNICODE_TEXT', 'NUM', 'ALPHA', got 'DEPENDENCY'
-
PropertyEditor 把 String 与其它类型相互转换
-
PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
-
与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配
高层接口与实现
[Mermaid]Parse error on line 15: ...ertyEditorRegistry<<interface>> TypeCo ---------------------^ Expecting 'CLASS', 'MEMBER', 'SEPARATOR', 'UNICODE_TEXT', 'NUM', 'ALPHA', got 'DEPENDENCY'
-
它们都实现了 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.a.a23 包
收获💡
基本的类型转换与数据绑定用法
-
SimpleTypeConverter
-
BeanWrapperImpl
-
DirectFieldAccessor
-
ServletRequestDataBinder
演示2 - 数据绑定工厂
代码参考
com.a.a23.TestServletDataBinderFactory
收获💡
ServletRequestDataBinderFactory 的用法和扩展点
-
可以解析控制器的 @InitBinder 标注方法作为扩展点,添加自定义转换器
-
控制器私有范围
-
-
可以通过 ConfigurableWebBindingInitializer 配置 ConversionService 作为扩展点,添加自定义转换器
-
公共范围
-
-
同时加了 @InitBinder 和 ConversionService 的转换优先级
-
优先采用 @InitBinder 的转换器
-
其次使用 ConversionService 的转换器
-
使用默认转换器
-
特殊处理(例如有参构造)
-
演示3 - 获取泛型参数
代码参考
com.a.a23.sub 包
收获💡
-
java api 获取泛型参数
-
spring api 获取泛型参数
24) @ControllerAdvice 之 @InitBinder
演示 - 准备 @InitBinder
准备 @InitBinder 在整个 HandlerAdapter 调用过程中所处的位置
[Mermaid]Parse error on line 9: ...ntainerrect rgb(200, 150, 255)adapter ----------------------^ Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', got ','
-
RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
-
HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
-
HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers
收获💡
-
RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法
-
RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @InitBinder 方法
-
以上两种 @InitBinder 的解析结果都会缓存来避免重复解析
-
控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂
25) 控制器方法执行流程
图1
HandlerMethodServletInvocableHandlerMethodbeanmethodWebDataBinderFactoryParameterNameDiscovererHandlerMethodArgumentResolverCompositeHandlerMethodReturnValueHandlerComposite
HandlerMethod 需要
-
bean 即是哪个 Controller
-
method 即是 Controller 中的哪个方法
ServletInvocableHandlerMethod 需要
-
WebDataBinderFactory 负责对象绑定、类型转换
-
ParameterNameDiscoverer 负责参数名解析
-
HandlerMethodArgumentResolverComposite 负责解析参数
-
HandlerMethodReturnValueHandlerComposite 负责处理返回值
图2
RequestMappingHandlerAdapterWebDataBinderFactoryModelFactoryModelAndViewContainer准备 @InitBinder准备 @ModelAttribute添加Model数据RequestMappingHandlerAdapterWebDataBinderFactoryModelFactoryModelAndViewContainer
图3
RequestMappingHandlerAdapterServletInvocableHandlerMethodArgumentResolversReturnValueHandlersModelAndViewContainerinvokeAndHandle获取 args有的解析器涉及 RequestBodyAdvice有的解析器涉及数据绑定生成模型数据argsmethod.invoke(bean,args) 得到 returnValue处理 returnValue有的处理器涉及 ResponseBodyAdvice添加Model数据,处理视图名,是否渲染等获取 ModelAndViewRequestMappingHandlerAdapterServletInvocableHandlerMethodArgumentResolversReturnValueHandlersModelAndViewContainer
26) @ControllerAdvice 之 @ModelAttribute
演示 - 准备 @ModelAttribute
代码参考
com.a.a26 包
准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置
[Mermaid]Parse error on line 12: ...apter: rect rgb(200, 150, 255)adapter ----------------------^ Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', got ','
收获💡
-
RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @ModelAttribute 方法
-
RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @ModelAttribute 方法
-
以上两种 @ModelAttribute 的解析结果都会缓存来避免重复解析
-
控制器方法调用时,会综合利用本类的 @ModelAttribute 方法和 @ControllerAdvice 中的 @ModelAttribute 方法创建模型工厂
27) 返回值处理器
演示 - 常见返回值处理器
代码参考
com.a.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.a.a28.A28
收获💡
-
MessageConverter 的作用
-
@ResponseBody 是返回值处理器解析的
-
但具体转换工作是 MessageConverter 做的
-
-
如何选择 MediaType
-
首先看 @RequestMapping 上有没有指定
-
其次看 request 的 Accept 头有没有指定
-
最后按 MessageConverter 的顺序, 谁能谁先转换
-
29) @ControllerAdvice 之 ResponseBodyAdvice
演示 - ResponseBodyAdvice 增强
代码参考
com.a.a29 包
ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置
[Mermaid]Parse error on line 23: ...rnValuerect rgb(200, 150, 255)rh ->> r ----------------------^ Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', got ','
收获💡
-
ResponseBodyAdvice 返回响应体前包装
30) 异常解析器
演示 - ExceptionHandlerExceptionResolver
代码参考
com.a.a30.A30
收获💡
-
它能够重用参数解析器、返回值处理器,实现组件重用
-
它能够支持嵌套异常
31) @ControllerAdvice 之 @ExceptionHandler
演示 - 准备 @ExceptionHandler
代码参考
com.a.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 - 错误页处理
关键代码
@Bean // ⬅️修改了 Tomcat 服务器默认错误地址, 出错时使用请求转发方式跳转
public ErrorPageRegistrar errorPageRegistrar() {return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}@Bean // ⬅️TomcatServletWebServerFactory 初始化前用它增强, 注册所有 ErrorPageRegistrar
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {return new ErrorPageRegistrarBeanPostProcessor();
}
收获💡
-
Tomcat 的错误页处理手段
演示2 - BasicErrorController
关键代码
@Bean // ⬅️ErrorProperties 封装环境键值, ErrorAttributes 控制有哪些错误信息
public BasicErrorController basicErrorController() {ErrorProperties errorProperties = new ErrorProperties();errorProperties.setIncludeException(true);return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}@Bean // ⬅️名称为 error 的视图, 作为 BasicErrorController 的 text/html 响应结果
public View error() {return new View() {@Overridepublic void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {System.out.println(model);response.setContentType("text/html;charset=utf-8");response.getWriter().print("""<h3>服务器内部错误</h3>""");}};
}@Bean // ⬅️收集容器中所有 View 对象, bean 的名字作为视图名
public ViewResolver viewResolver() {return new BeanNameViewResolver();
}
收获💡
-
Spring Boot 中 BasicErrorController 如何工作
33) BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
演示 - 本组映射器和适配器
关键代码
@Bean
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {return new BeanNameUrlHandlerMapping();
}@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {return new SimpleControllerHandlerAdapter();
}@Bean("/c3")
public Controller controller3() {return (request, response) -> {response.getWriter().print("this is c3");return null;};
}
收获💡
-
BeanNameUrlHandlerMapping,以 / 开头的 bean 的名字会被当作映射路径
-
这些 bean 本身当作 handler,要求实现 Controller 接口
-
SimpleControllerHandlerAdapter,调用 handler
-
模拟实现这组映射器和适配器
34) RouterFunctionMapping 与 HandlerFunctionAdapter
演示 - 本组映射器和适配器
关键代码
@Bean
public RouterFunctionMapping routerFunctionMapping() {return new RouterFunctionMapping();
}@Bean
public HandlerFunctionAdapter handlerFunctionAdapter() {return new HandlerFunctionAdapter();
}@Bean
public RouterFunction<ServerResponse> r1() {// ⬇️映射条件 ⬇️handlerreturn route(GET("/r1"), request -> ok().body("this is r1"));
}
收获💡
-
RouterFunctionMapping, 通过 RequestPredicate 条件映射
-
handler 要实现 HandlerFunction 接口
-
HandlerFunctionAdapter, 调用 handler
35) SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
演示1 - 本组映射器和适配器
代码参考
org.springframework.boot.autoconfigure.web.servlet.A35
关键代码
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();Map<String, ResourceHttpRequestHandler> map = context.getBeansOfType(ResourceHttpRequestHandler.class);handlerMapping.setUrlMap(map);return handlerMapping;
}@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {return new HttpRequestHandlerAdapter();
}@Bean("/**")
public ResourceHttpRequestHandler handler1() {ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();handler.setLocations(List.of(new ClassPathResource("static/")));return handler;
}@Bean("/img/**")
public ResourceHttpRequestHandler handler2() {ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();handler.setLocations(List.of(new ClassPathResource("images/")));return handler;
}
收获💡
-
SimpleUrlHandlerMapping 不会在初始化时收集映射信息,需要手动收集
-
SimpleUrlHandlerMapping 映射路径
-
ResourceHttpRequestHandler 作为静态资源 handler
-
HttpRequestHandlerAdapter, 调用此 handler
演示2 - 静态资源解析优化
关键代码
@Bean("/**")
public ResourceHttpRequestHandler handler1() {ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();handler.setLocations(List.of(new ClassPathResource("static/")));handler.setResourceResolvers(List.of(// ⬇️缓存优化new CachingResourceResolver(new ConcurrentMapCache("cache1")),// ⬇️压缩优化new EncodedResourceResolver(),// ⬇️原始资源解析new PathResourceResolver()));return handler;
}
收获💡
-
责任链模式体现
-
压缩文件需要手动生成
演示3 - 欢迎页
关键代码
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {Resource resource = context.getResource("classpath:static/index.html");return new WelcomePageHandlerMapping(null, context, resource, "/**");
}@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {return new SimpleControllerHandlerAdapter();
}
收获💡
-
欢迎页支持静态欢迎页与动态欢迎页
-
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 接口 (静态资源处理)
-
这也是典型适配器模式体现
-
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 步走视图解析及渲染流程
-
-
-
调用拦截器的 postHandle 方法
-
处理异常或视图渲染
-
如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
-
@ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
-
-
正常,走视图解析及渲染流程
-
-
调用拦截器的 afterCompletion 方法
-
Boot
37) Boot 骨架项目
如果是 linux 环境,用以下命令即可获取 spring boot 的骨架 pom.xml
curl -G https://start.spring.io/pom.xml -d dependencies=web,mysql,mybatis -o pom.xml
也可以使用 Postman 等工具实现
若想获取更多用法,请参考
curl https://start.spring.io
38) Boot War项目
步骤1:创建模块,区别在于打包方式选择 war
接下来勾选 Spring Web 支持
步骤2:编写控制器
@Controller public class MyController {@RequestMapping("/hello")public String abc() {System.out.println("进入了控制器");return "hello";} }
步骤3:编写 jsp 视图,新建 webapp 目录和一个 hello.jsp 文件,注意文件名与控制器方法返回的视图逻辑名一致
src|- main|- java|- resources|- webapp|- hello.jsp
步骤4:配置视图路径,打开 application.properties 文件
spring.mvc.view.prefix=/ spring.mvc.view.suffix=.jsp
将来 prefix + 控制器方法返回值 + suffix 即为视图完整路径
测试
如果用 mvn 插件 mvn spring-boot:run
或 main 方法测试
-
必须添加如下依赖,因为此时用的还是内嵌 tomcat,而内嵌 tomcat 默认不带 jasper(用来解析 jsp)
<dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId><scope>provided</scope> </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.a.a39.A39_1 对应 SpringApplication 构造
com.a.a39.A39_2 对应第1步,并演示 7 个事件
com.a.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 基本结构
Server └───Service├───Connector (协议, 端口)└───Engine└───Host(虚拟主机 localhost)├───Context1 (应用1, 可以设置虚拟路径, / 即 url 起始路径; 项目磁盘路径, 即 docBase )│ │ index.html│ └───WEB-INF│ │ web.xml (servlet, filter, listener) 3.0│ ├───classes (servlet, controller, service ...)│ ├───jsp│ └───lib (第三方 jar 包)└───Context2 (应用2)│ index.html└───WEB-INFweb.xml
演示1 - Tomcat 内嵌容器
关键代码
public static void main(String[] args) throws LifecycleException, IOException {// 1.创建 Tomcat 对象Tomcat tomcat = new Tomcat();tomcat.setBaseDir("tomcat");// 2.创建项目文件夹, 即 docBase 文件夹File docBase = Files.createTempDirectory("boot.").toFile();docBase.deleteOnExit();// 3.创建 Tomcat 项目, 在 Tomcat 中称为 ContextContext context = tomcat.addContext("", docBase.getAbsolutePath());// 4.编程添加 Servletcontext.addServletContainerInitializer(new ServletContainerInitializer() {@Overridepublic void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {HelloServlet helloServlet = new HelloServlet();ctx.addServlet("aaa", helloServlet).addMapping("/hello");}}, Collections.emptySet());// 5.启动 Tomcattomcat.start();// 6.创建连接器, 设置监听端口Connector connector = new Connector(new Http11Nio2Protocol());connector.setPort(8080);tomcat.setConnector(connector);
}
演示2 - 集成 Spring 容器
关键代码
WebApplicationContext springContext = getApplicationContext();// 4.编程添加 Servlet
context.addServletContainerInitializer(new ServletContainerInitializer() {@Overridepublic void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {// ⬇️通过 ServletRegistrationBean 添加 DispatcherServlet 等for (ServletRegistrationBean registrationBean : springContext.getBeansOfType(ServletRegistrationBean.class).values()) {registrationBean.onStartup(ctx);}}
}, Collections.emptySet());
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 格式的数据
演示 - 自动配置类原理
关键代码
假设已有第三方的两个自动配置类@Configuration // ⬅️第三方的配置类
static class AutoConfiguration1 {@Beanpublic Bean1 bean1() {return new Bean1();}
}@Configuration // ⬅️第三方的配置类
static class AutoConfiguration2 {@Beanpublic Bean2 bean2() {return new Bean2();}
}
提供一个配置文件 META-INF/spring.factories,key 为导入器类名,值为多个自动配置类名,用逗号分隔
MyImportSelector=\ AutoConfiguration1,\ AutoConfiguration2
注意
上述配置文件中 MyImportSelector 与 AutoConfiguration1,AutoConfiguration2 为简洁均省略了包名,自己测试时请将包名根据情况补全
引入自动配置
@Configuration // ⬅️本项目的配置类
@Import(MyImportSelector.class)
static class Config { }static class MyImportSelector implements DeferredImportSelector {// ⬇️该方法从 META-INF/spring.factories 读取自动配置类名,返回的 String[] 即为要导入的配置类public String[] selectImports(AnnotationMetadata importingClassMetadata) {return SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null).toArray(new String[0]);}
}
收获💡
-
自动配置类本质上就是一个配置类而已,只是用 META-INF/spring.factories 管理,与应用配置类解耦
-
@Enable 打头的注解本质是利用了 @Import
-
@Import 配合 DeferredImportSelector 即可实现导入,selectImports 方法的返回值即为要导入的配置类名
-
DeferredImportSelector 的导入会在最后执行,为的是让其它配置优先解析
42) 条件装配底层
条件装配的底层是本质上是 @Conditional 与 Condition,这两个注解。引入自动配置类时,期望满足一定条件才能被 Spring 管理,不满足则不管理,怎么做呢?
比如条件是【类路径下必须有 dataSource】这个 bean ,怎么做呢?
首先编写条件判断类,它实现 Condition 接口,编写条件判断逻辑
static class MyCondition1 implements Condition { // ⬇️如果存在 Druid 依赖,条件成立public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {return ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource", null);} }
其次,在要导入的自动配置类上添加 @Conditional(MyCondition1.class)
,将来此类被导入时就会做条件检查
@Configuration // 第三方的配置类 @Conditional(MyCondition1.class) // ⬅️加入条件 static class AutoConfiguration1 {@Beanpublic Bean1 bean1() {return new Bean1();} }
分别测试加入和去除 druid 依赖,观察 bean1 是否存在于容器
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.17</version> </dependency>
收获💡
-
学习一种特殊的 if - else
其它
43) FactoryBean
演示 - FactoryBean
代码参考
com.a.a43 包
收获💡
-
它的作用是用制造创建过程较为复杂的产品, 如 SqlSessionFactory, 但 @Bean 已具备等价功能
-
使用上较为古怪, 一不留神就会用错
-
被 FactoryBean 创建的产品
-
会认为创建、依赖注入、Aware 接口回调、前初始化这些都是 FactoryBean 的职责, 这些流程都不会走
-
唯有后初始化的流程会走, 也就是产品可以被代理增强
-
单例的产品不会存储于 BeanFactory 的 singletonObjects 成员中, 而是另一个 factoryBeanObjectCache 成员中
-
-
按名字去获取时, 拿到的是产品对象, 名字前面加 & 获取的是工厂对象
-
44) @Indexed 原理
真实项目中,只需要加入以下依赖即可
<dependency><groupId>org.springframework</groupId><artifactId>spring-context-indexer</artifactId><optional>true</optional> </dependency>
演示 - @Indexed
代码参考
com.a.a44 包
收获💡
-
在编译时就根据 @Indexed 生成 META-INF/spring.components 文件
-
扫描时
-
如果发现 META-INF/spring.components 存在, 以它为准加载 bean definition
-
否则, 会遍历包下所有 class 资源 (包括 jar 内的)
-
-
解决的问题,在编译期就找到 @Component 组件,节省运行期间扫描 @Component 的时间
45) 代理进一步理解
演示 - 代理
代码参考
com.a.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.a.a46 包
收获💡
-
ContextAnnotationAutowireCandidateResolver 作用之一,获取 @Value 的值
-
了解 ${ } 对应的解析器
-
了解 #{ } 对应的解析器
-
TypeConvert 的一项体现
47) @Autowired 装配底层
演示 - @Autowired 装配过程
代码参考
com.a.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.a.a48 包
收获💡
事件监听器的两种方式
-
实现 ApplicationListener 接口
-
根据接口泛型确定事件类型
-
-
@EventListener 标注监听方法
-
根据监听器方法参数确定事件类型
-
解析时机:在 SmartInitializingSingleton(所有单例初始化完成后),解析每个单例 bean
-
49) 事件发布器
演示 - 事件发布器
代码参考
com.a.a49 包
收获💡
事件发布器模拟实现
-
addApplicationListenerBean 负责收集容器中的监听器
-
监听器会统一转换为 GenericApplicationListener 对象,以支持判断事件类型
-
-
multicastEvent 遍历监听器集合,发布事件
-
发布前先通过 GenericApplicationListener.supportsEventType 判断支持该事件类型才发事件
-
可以利用线程池进行异步发事件优化
-
-
如果发送的事件对象不是 ApplicationEvent 类型,Spring 会把它包装为 PayloadApplicationEvent 并用泛型技术解析事件对象的原始类型
-
视频中未讲解
-