1. 容器接口

以 SpringBoot 的启动类为例:

1
2
3
4
5
6
7
@Slf4j
@SpringBootApplication
public class A01Application {
public static void main(String[] args) {
SpringApplication.run(A01Application.class, args);
}
}

其中的 run() 方法是有返回值的:

1
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);

在 IDEA 中使用快捷键 Ctrl + Alt + U 查看 ConfigurableApplicationContext类的类图:

ConfigurableApplicationContext接口继承了 ApplicationContext接口,而 ApplicationContext接口又间接地继承了 BeanFactory接口,除此之外还继承了其他很多接口,相当于对 BeanFactory进行了拓展。

1.1 什么是 BeanFactory

  • 它是 ApplicationContext的父接口
  • 它才是 Spring 的核心容器,主要的 ApplicationContext实现 组合 了它的功能,也就是说,BeanFactoryApplicationContext中的一个成员变量。

常用的context.getBean("xxx") 方法,其实是调用了 BeanFactorygetBean() 方法。

1.2 BeanFactory 能做什么

进入BeanFactory接口,在 IDEA 中使用快捷键Ctrl+F12 查看这个接口中的所有方法定义:

通过这些方法定义可知,BeanFactory表面上只有getBean()方法,实际上控制反转、基本的依赖注入、乃至 Bean 的生命周期的各种功能,都由它的实现类提供。

查看 DefaultListableBeanFactory 类的类图:

DefaultListableBeanFactory 实现了 BeanFactory接口,它能管理 Spring 中所有的 Bean,当然也包含 Spring 容器中的那些单例对象。

DefaultListableBeanFactory 还继承了 DefaultSingletonBeanRegistry 类,这个类就是用来管理 Spring 容器中的单例对象。

在 IDEA 提供的类图中选中 DefaultSingletonBeanRegistry,然后按下F4进入这个类。它有一个Map类型的成员变量singletonObjects:

1
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

Map的 key 就是 Bean 的名字,而 value 是对应的 Bean,即单例对象。

创建两个类

1
2
3
4
5
6
7
8
@Component
public class ComponentA {
}

@Component
public class ComponentB {
}

查看singletonObjects中是否存在两个 bean 的信息。

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
public class A01Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
Field singletonObjects = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects");
singletonObjects.setAccessible(true);
Map<String, Object> map = (Map<String, Object>) singletonObjects.get(context.getBeanFactory());
map.entrySet().stream()
.filter(entry -> entry.getKey().startsWith("component"))
.forEach(entry -> System.out.println(entry.getKey() + " ==> " + entry.getValue()));
}
}

运行 main()方法后,控制台打印出:

componentA  ==>  com.itheima.a01.ComponentA@10c72a6f
componentB  ==>  com.itheima.a01.ComponentB@70e94ecb

1.3 ApplicationContext 的功能

回顾ApplicationContext的类图:

它的拓展功能主要体现在继承的 4 个父接口上

  • MessageSource:使其具备处理国际化资源的能力
  • ResourcePatternResolver:使其具备使用通配符进行资源匹配的能力
  • EnvironmentCapable:使其具备读取 Spring 环境信息、配置文件信息的能力
  • ApplicationEventPublisher:使其具备发布事件的能力

MessageSource

读取一个 Key 翻译后的结果

在 SpringBoot 项目的 resources 目录下创建 messages.properties、messages_en.properties、messages_zh_CN.properties 三个国际化文件

测试 MessageSource 接口中 getMessage() 方法的使用:

1
2
3
4
5
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
System.out.println(context.getMessage("thanks", null, Locale.ENGLISH));
System.out.println(context.getMessage("thanks", null, Locale.SIMPLIFIED_CHINESE));
}

运行 main() 方法后,控制台打印出:

Thank you
谢谢

ResourcePatternResolver

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class A01Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
Resource[] resource = context.getResources("classpath*:META-INF/spring.factories");
for (Resource r : resource) {
System.out.println(r);
}
}
}

运行 main() 方法后,控制台打印出:

URL [jar:file:/C:/Users/Administrator/.m2/repository/org/springframework/boot/spring-boot/2.5.5/spring-boot-2.5.5.jar!/META-INF/spring.factories]
URL [jar:file:/C:/Users/Administrator/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.5/spring-boot-autoconfigure-2.5.5.jar!/META-INF/spring.factories]
URL [jar:file:/C:/Users/Administrator/.m2/repository/org/springframework/spring-beans/5.3.10/spring-beans-5.3.10.jar!/META-INF/spring.factories]  

EnvironmentCapable

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class A01Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
//来自系统变量
System.out.println(context.getEnvironment().getProperty("java_home"));
//来自properties文件
System.out.println(context.getEnvironment().getProperty("server.port"));
}
}

运行 main() 方法后,控制台打印出:

C:\Program Files\Java\jdk1.8.0_60
8080

ApplicationEventPublisher

定义事件类 RegisteredEvent

1
2
3
4
5
6
public class RegisteredEvent extends ApplicationEvent {

public RegisteredEvent(Object source) {
super(source);
}
}

ComponentA 作为发送事件的 Bean:

1
2
3
4
5
6
7
8
9
10
11
@Component
@Slf4j
public class ComponentA {
@Autowired
private ApplicationEventPublisher context;

public void register() {
log.info("ComponentA 发布注册事件");
context.publishEvent(new RegisteredEvent(this));
}
}

ComponentB 作为事件监听器:

1
2
3
4
5
6
7
8
9
@Component
@Slf4j
public class ComponentB {
@EventListener
public void hander(RegisteredEvent event) {
log.info("{}",event);
log.info("ComponentB接收到事件,发送短信");
}
}

在 main() 方法中使用ComponentA发送事件:

1
2
3
4
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
context.getBean(ComponentA.class).register();
}

运行 main()方法后,控制台打印出:

com.itheima.a01.ComponentA              : ComponentA 发布注册事件
com.itheima.a01.ComponentB              : com.itheima.a01.RegisteredEvent[source=com.itheima.a01.ComponentA@33425811]
com.itheima.a01.ComponentB              : ComponentB接收到事件,发送短信

2. 容器实现

2.1 BeanFactory 的实现

现有如下测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class TestBeanFactory {

@Configuration
static class Config {
@Bean
public Bean1 bean1() {
return new Bean1();
}

@Bean
public Bean2 bean2() {
return new Bean2();
}
}

@Slf4j
static class Bean1 {
public Bean1() {
log.debug("构造 Bean1()");
}

@Autowired
private Bean2 bean2;

public Bean2 getBean2() {
return bean2;
}
}

@Slf4j
static class Bean2 {
public Bean2() {
log.debug("构造 Bean2()");
}
}

}

使用DefaultListableBeanFactory作为beanFactory的实现类,来进行演示。

有了 Bean 工厂,还需要定义 Bean,之后再把定义的 Bean 注册到工厂即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
//beanFactory的一个重要实现
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
//刚创建好时,内部是没有任何的bean的,我们需要给beanFactory中添加一些bean的定义(class、scope、init、destroy)
//并不是添加bean对象,bean对象是由beanFactory控制反转帮我们创建出来的

//创建beanDefinition
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(Config.class)
.setScope("singleton")
.getBeanDefinition();
//加入到beanFactory中
beanFactory.registerBeanDefinition("config", beanDefinition);
//查看beanFactory中有哪些beanDefinition,把它们的名字打印出来
for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}

运行 main()方法后,控制台打印出:

config

现在 Bean 工厂中 有且仅有一个 名为 config 的 Bean。

解析配置类

但我们的配置类明明加了@Configuration@Bean 两个注解,那么 Bean 工厂中应该还存在 bean1bean2啊,那为什么现在没有呢?

很明显是现在的 BeanFactory 缺少了解析 @Configuration@Bean 两个注解的能力。

1
2
3
4
5
public static void main(String[] args) {
// 给 BeanFactory 添加一些常用的后置处理器
AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);
Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
}

运行 main()方法后,控制台打印出:

config
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory

观察控制台输出,我们可以看到有一个名为:

org.springframework.context.annotation.internalConfigurationAnnotationProcessor的 Bean,根据其所含的 ConfigurationAnnotationProcessor 字样,可以知道这个 Bean 就是用来处理 @Configuration@Bean 注解的,将配置类中定义的 Bean 信息补充到 BeanFactory 中。

但现在已经加入 Bean 工厂中了,为啥依旧没有 bean1bean2 呢?

现在仅仅是将处理器添加到了 Bean 工厂,还没有使用处理器。

internalConfigurationAnnotationProcessor 这样的 Bean,都有一个共同的类型,名为 BeanFactoryPostProcessor,因此可以我们拿到所有的后置处理器,逐一执行。

1
2
3
4
5
6
7
8
 beanFactory.getBeansOfType(BeanFactoryPostProcessor.class)
.values()
.stream()
.forEach(beanFactoryPostProcessor -> beanFactoryPostProcessor.postProcessBeanFactory(beanFactory));
//再次打印
for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}

运行 main()方法后,控制台打印出:

config
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
bean1
bean2

由此可见,beanFactory的原始功能并没有那么丰富,它的一些拓展功能是由一些后置处理器来完成的。

依赖注入

bean1bean2已经被补充到 bean 工厂中了,那我们可以使用吗?

1
2
//观察bean1中是不是成功依赖注入了bean2
System.out.println(beanFactory.getBean(Bean1.class).getBean2());
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean1'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
com.itheima.a02.TestBeanFactory$Bean1 - 构造 Bean1()
null

控制台输出为 null,bean2没有注入到bean1

beanFactory依赖注入的功能也没有…

我们刚才用到的是其中一种后置处理器,是针对beanFactory的后置处理器,作用是补充beanDefination,去解析这些@Bean@Configuration,但如果要依赖注入的话,还有一些叫bean的后置处理器,由它来去解析@Autowired

在先前添加到 BeanFactory 中的后置处理器里,有名为 internalAutowiredAnnotationProcessorinternalCommonAnnotationProcessor 的两个后置处理器。前者用于解析 @Autowired 注解,后者用于解析 @Resource 注解,这种我们称之为bean的后置处理器,它们会针对 bean 生命周期的各个阶段,提供一些拓展功能,它们都有一个共同的类型BeanPostProcessor,因此可以:

1
2
3
4
5
6
public static void main(String[] args) {
System.out.println("---------------------------------------------");
// Bean 后置处理器,对 Bean 的生命周期的各个阶段提供拓展,例如 @AutoWired @Resource...
beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor);
System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean1'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
com.atguigu.demo.TestBeanFactory$Bean1 - 构造 Bean1()
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean2'
com.itheima.a02.TestBeanFactory$Bean2 - 构造 Bean2()
com.itheima.a02.TestBeanFactory$Bean2@31304f14   

建立BeanPostProcessorBeanFactory的关系后,bean2 被成功注入到 bean1 中了。

1
2
3
4
5
6
7
//只是将后置处理器添加到beanFactory中(后置处理器只是存在于beanFactory中的一个bean而已)
AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);
//调用(beanFactory::addBeanPostProcessor)建立 BeanPostProcessor 和 BeanFactory 的联系,
//将来我这个beanFactory中每个bean创建时需要哪些后置处理器
beanFactory.getBeansOfType(BeanPostProcessor.class)
.values()
.forEach(beanFactory::addBeanPostProcessor);

我们发现:刚开始 beanFactory 中都是一些BeanDefination,并没有去实例化、初始化,仅仅是保存了 bean 的描述信息在 bean 工厂中,当我们第一次调用getBean()时,才会去创建 bean,即延时创建。

那有没有什么办法预先就初始化好单例对象呢?

1
2
3
4
5
6
7
8
public static void main(String[] args) {

//预先初始化单例对象(完成依赖注入和初始化流程)
beanFactory.preInstantiateSingletons();
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor);
System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'config'
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean1'
com.itheima.a02.TestBeanFactory$Bean1 - 构造 Bean1()
org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'bean2'
com.itheima.a02.TestBeanFactory$Bean2 - 构造 Bean2()
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
com.itheima.a02.TestBeanFactory$Bean2@76505305    

目前可以知道,BeanFactory不会 :

  • 主动调用 BeanFactory后置处理器;
  • 主动添加 Bean 后置处理器
  • 主动初始化单例对象;
  • 解析 ${}#{}

bean 后置处理器的顺序

在最初给出的类信息中进行补充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class TestBeanFactory {
@Configuration
static class Config {
@Bean
public Bean1 bean1() {
return new Bean1();
}

@Bean
public Bean2 bean2() {
return new Bean2();
}

@Bean
public Bean3 bean3() {
return new Bean3();
}

@Bean
public Bean4 bean4() {
return new Bean4();
}
}

interface Inter {

}

@Slf4j
static class Bean1 {
public Bean1() {
log.debug("构造 Bean1()");
}

@Autowired
private Bean2 bean2;

@Autowired //根据类型注入,同类型的bean有多个,则通过成员变量名和bean的名称匹配,注入bean3
@Resource(name = "bean4")//根据名称注入bean4
private Inter bean3;

private Inter getInter() {
return bean3;
}

public Bean2 getBean2() {
return bean2;
}
}

@Slf4j
static class Bean2 {
public Bean2() {
log.debug("构造 Bean2()");
}
}

@Slf4j
static class Bean3 implements Inter {
public Bean3() {
log.debug("构造 Bean3()");
}
}

@Slf4j
static class Bean4 implements Inter {
public Bean4() {
log.debug("构造 Bean4()");
}
}
}

向 Bean 工厂中添加了 bean3bean4,并且在 bean1 中注入 Inter 类型的 Bean。

现在 Bean 工厂中Inter 类型的 Bean 有两个,分别是 bean3bean4,那么会注入哪一个呢?

如果只使用 @Autowired,首先会按照类型注入,如果同类型的 Bean 有多个,再按照变量名称注入,如果再注入失败,就报错;如果只使用 @Resource,首先会按照 name 进行注入,同名的 Bean 有多个,再按照类型注入,否则就报错。

那如果即使用 @Autowired 又使用 @Resource(name = "bean4") 呢?

1
2
3
public static void main(String[] args) {
System.out.println(beanFactory.getBean(Bean1.class).getInter());
}
com.itheima.a02.TestBeanFactory$Bean3@175c2241    

根据打印的结果可知,@Autowired 生效了,这是因为internalAutowiredAnnotationProcessor 排在 internalCommonAnnotationProcessor 之前。我们可以打印出它们的顺序:

1
2
3
4
5
6
beanFactory.getBeansOfType(BeanPostProcessor.class)
.values()
.forEach(beanPostProcessor -> {
System.out.println(">>>>>>>>" + beanPostProcessor);
beanFactory.addBeanPostProcessor(beanPostProcessor);
});
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@2e005c4b
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@4567f35d  

然后我们改变它们的顺序

1
2
3
4
5
6
7
8
beanFactory.getBeansOfType(BeanPostProcessor.class)
.values()
.stream()
.sorted(beanFactory.getDependencyComparator())
.forEach(beanPostProcessor -> {
System.out.println(">>>>>>>>" + beanPostProcessor);
beanFactory.addBeanPostProcessor(beanPostProcessor);
});
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@4f18837a
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@359f7cdf	
com.itheima.a02.TestBeanFactory$Bean4@62379589    

结果注入的是bean4

为什么使用beanFactory.getDependencyComparator() 后就改变了 BeanPostProcessor 的先后顺序呢?

我们在使用AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory)工具类时,翻看源码可以发现

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {
DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
if (beanFactory != null) {
if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
//设置比较器对象
beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
}

if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
}
}
}

比较器是根据后置处理器的getOrder()返回值,从小到大进行排序。

AutowiredAnnotationBeanPostProcessor 设置的 order 是:

1
private int order = Ordered.LOWEST_PRECEDENCE - 2;

CommonAnnotationBeanPostProcessor 设置的 order 是:

1
2
3
public CommonAnnotationBeanPostProcessor() {
setOrder(Ordered.LOWEST_PRECEDENCE - 3);
}

因此当设置了AnnotationAwareOrderComparator 比较器后,CommonAnnotationBeanPostProcessor 排在更前面,@Resource 就先生效。

总结

  • 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.2 ApplicationContext 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class A02Application {

static class Bean1 {

}

static class Bean2 {
private Bean1 bean1;

public void setBean1(Bean1 bean1) {
this.bean1 = bean1;
}

public Bean1 getBean1() {
return bean1;
}
}

}

ClassPathXmlApplicationContext

较为经典的容器,基于 classpath 下的 xml 格式的配置文件 来创建

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


<bean id="bean1" class="com.itheima.a02.A02Application.Bean1"></bean>


<bean id="bean2" class="com.itheima.a02.A02Application.Bean2">
<property name="bean1" ref="bean1"></property>
</bean>
</beans>
1
2
3
4
5
6
7
private static void testClassPathXmlApplicationContext() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
for (String beanDefinitionName : context.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
System.out.println(context.getBean(Bean2.class).getBean1());
}
bean1
bean2
com.itheima.a02.A02Application$Bean1@6f1de4c7   

FileSystemXmlApplicationContext

基于磁盘路径下 xml 格式的配置文件来创建。

1
2
3
4
5
6
7
8
private static void testFileSystemXmlApplicationContext() {
//使用相对路径时,以模块为起点(IDEA 中需要设置 Working directory),也支持绝对路径
FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("D:\\EdgeDownload\\framwork\\demo\\src\\main\\resources\\bean1.xml");
for (String beanDefinitionName : context.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
System.out.println(context.getBean(Bean2.class).getBean1());
}
bean1
bean2
com.itheima.a02.A02Application$Bean1@128d2484   

ClassPathXmlApplicationContextFileSystemXmlApplicationContext 都依赖于从 XML 文件中读取 Bean 的信息,而这都利用了 XmlBeanDefinitionReader 进行读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 public static void main(String[] args) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
System.out.println("读取之前");
for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
System.out.println("读取之后");
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
xmlBeanDefinitionReader.loadBeanDefinitions(new ClassPathResource("bean1.xml"));
// xmlBeanDefinitionReader.loadBeanDefinitions(new FileSystemResource("D:\\EdgeDownload\\framwork\\demo\\src\\main\\resources\\bean1.xml"));
for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}
读取之前
读取之后
org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [bean1.xml]
bean1
bean2   

AnnotationConfigApplicationContext

基于 java 配置类来创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void testAnnotationConfigApplicationContext() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Config.class);
for (String beanDefinitionName : context.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
System.out.println(context.getBean(Bean2.class).getBean1());
}

@Configuration
static class Config {
@Bean
public Bean1 bean1() {
return new Bean1();
}

@Bean
public Bean2 bean2(Bean1 bean1) {
Bean2 bean2 = new Bean2();
bean2.setBean1(bean1);
return bean2;
}
}
1
2
3
4
public class A02Application {
public static void main(String[] args) {
testAnnotationConfigApplicationContext();
}
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
a02Application.Config
bean1
bean2
com.itheima.a02.A02Application$Bean1@7748410a   

与前面两种基于 Xml 创建ApplicationContext的方式相比,AnnotationConfigApplicationContext自动帮我们添加了一些常用的后置处理器。

如果想在基于 Xml 的方式中也添加上这些 Bean,可以在 Xml 中进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">


<bean id="bean1" class="com.itheima.a02.A02Application.Bean1"></bean>


<bean id="bean2" class="com.itheima.a02.A02Application.Bean2">
<property name="bean1" ref="bean1"></property>
</bean>
<!-- 添加后置处理器-->
<context:annotation-config/>
</beans>

AnnotationConfigServletWebServerApplicationContext

基于 Java 配置类来创建,用于 web 环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
static class WebConfig {
@Bean
public ServletWebServerFactory servletWebServerFactory() {
// 提供内嵌的 Web 容器
return new TomcatServletWebServerFactory();
}

@Bean
public DispatcherServlet dispatcherServlet() {
// 添加前端控制器
return new DispatcherServlet();
}

@Bean
public DispatcherServletRegistrationBean registrationBean(DispatcherServlet dispatcherServlet) {
// 将 DispatcherServlet 注册到 Tomcat 服务器
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}

// 如果 bean 以 '/' 开头,将 '/' 后的 bean 的名称作为访问路径
@Bean("/hello")
public Controller controller1() {
// 添加控制器,用于展示
return (request, response) -> {
response.getWriter().println("hello");
return null;
};
}
}

1
2
3
4
5
private static void testAnnotationConfigServletWebServerApplicationContext() {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}

运行代码,在浏览器中访问 http://localhost:8080/hello 路径则会显示出 hello 字样

2.3 【补充】BeanFactory 接口体系

BeanFactory 其实就是 Spring IOC 容器,它本身是一个接口,提供了一系列获取 Bean 的方式。

基于它也有众多子接口:

  • ListableBeanFactory:提供获取 Bean 集合的能力,比如一个接口可能有多个实现,通过该接口下的方法就能获取某种类型的所有 Bean;

  • HierarchicalBeanFactoryHierarchical 意为“层次化”,通常表示一种具有层级结构的概念或组织方式,这种层次化结构可以通过父子关系来表示对象之间的关联,比如树、图、文件系统、组织架构等。根据该接口下的方法可知,能够获取到父容器,说明 BeanFactory 有父子容器概念;

  • AutowireCapableBeanFactory:提供了创建 Bean、自动装配 Bean、属性填充、Bean 初始化、依赖注入等能力,比如 @Autowired 注解的底层实现就依赖于该接口的 resolveDependency() 方法;

  • ConfigurableBeanFactory:该接口并未直接继承至 BeanFactory,而是继承了 HierarchicalBeanFactoryConfigurable 意为“可配置的”,就是说该接口用于对 BeanFactory 进行一些配置,比如设置类型转换器。

2.4 【补充】读取 BeanDefinition

BeanDefinition 也是一个接口,它封装了 Bean 的定义,Spring 根据 Bean 的定义,就能创建出符合要求的 Bean。

读取 BeanDefinition 可以通过下列两种类完成:

  • BeanDefinitionReader
  • ClassPathBeanDefinitionScanner

BeanDefinitionReader

该接口中对 loadBeanDefinitions() 方法进行了多种重载,支持传入一个或多个 Resource 对象、资源位置来加载 BeanDefinition

它有一系列相关实现,比如:

  • XmlBeanDefinitionReader:通过读取 XML 文件来加载;
  • PropertiesBeanDefinitionReader:通过读取 properties 文件来加载,此类已经被 @Deprecated 注解标记;

除此之外,还有一个 AnnotatedBeanDefinitionReader,尽管它并不是 BeanDefinition 的子类,但它们俩长得很像,根据其类注释可知:它能够通过编程的方式对 Bean 进行注册,是 ClassPathBeanDefinitionScanner 的替代方案,能读取通过注解定义的 Bean。

ClassPathBeanDefinitionScanner

通过扫描指定包路径下的 @Component 及其派生注解来注册 Bean,是 @ComponentScan 注解的底层实现。

比如 MyBatis 通过继承 ClassPathBeanDefinitionScanner 实现通过 @MapperScan注解来扫描指定包下的 Mapper 接口。

BeanDefinitionRegistry

AnnotatedBeanDefinitionReaderClassPathBeanDefinitionScanner中都有一个 BeanDefinitionRegistry类型的成员变量,它是一个接口,提供了 BeanDefinition 的增加、删除和查找功能。

注册与获取 Bean

根据前面的补充,现在可以这样注册并获取 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DefaultListableBeanFactoryTest {
static class MyBean {
}

public static void main(String[] args) {
// 既实现了 BeanFactory,又实现了 BeanDefinitionRegistry
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// ClassPathBeanDefinitionScanner 的一种替代,编程式显式注册 bean
AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(beanFactory);
reader.registerBean(MyBean.class);
MyBean bean = beanFactory.getBean(MyBean.class);
System.out.println(bean);
}
}

2.5 【补充】ApplicationContext 接口体系

1
2
3
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
// --snip--
}

ApplicationContext 接口继承了许多接口,可谓是接口的集大成者,其中:

  • EnvironmentCapable:提供获取 Environment 的能力
  • ListableBeanFactory:提供了获取某种类型的 Bean 集合的能力
  • HierarchicalBeanFactory:提供了获取父容器的能力
  • MessageSource:提供了对国际化信息进行处理的能力
  • ApplicationEventPublisher:提供了事件发布能力
  • ResourcePatternResolver:提供了通过通配符获取多个资源的能力

虽然 ApplicationContext 继承了很多接口,但这些能力的实现是通过一种委派(Delegate)的方式实现的,这种方式也被叫做委派模式,但它并不属于 GoF 的 23 种设计模式中的一种,是一种面向对象的设计模式。什么是委派呢?

1
2
3
4
5
6
7
8
9
10
11
12
public class MyApplicationContext implements ApplicationContext {

private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

@Override
public Resource[] getResources(String locationPattern) throws IOException {
return resourcePatternResolver.getResources(locationPattern);
}

// --snip--

}

实现获取资源的方式并不是由实现类自身完成,而是交给其内部的一个成员变量完成,这样的方式就是委派(这和对象适配器模式很相似)。

在日常编码遇到这样的实现逻辑时,类名可以以 Delegate 结尾。

ConfigurableApplicationContext

ApplicationContext 有一个子接口 ConfigurableApplicationContext,从类名就可以看出,它提供了对 ApplicationContext 进行配置的能力,浏览其内部方法可知,提供了诸如设置父容器、设置 Environment等能力。

AbstractApplicationContext

ApplicationContext 有一个非常重要的抽象实现 AbstractApplicationContext,其他具体实现都会继承这个抽象实现,在其内部通过委派的方式实现了一些接口的能力,除此之外还有一个与 Spring Bean 的生命周期息息相关的方法:refresh()

3. Bean 的生命周期

1
2
3
4
5
6
7
8
@SpringBootApplication
public class A03Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A03Application.class, args);
// 调用 close 方法,显示生命周期的销毁阶段
context.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@Component
public class LifeCycleBean {

public LifeCycleBean() {
log.info("构造");
}

@Autowired
public void autowire(@Value("${JAVA_HOME}") String home) {
log.info("依赖注入: {}", home);
}

@PostConstruct
public void init() {
log.info("初始化");
}

@PreDestroy
public void destroy() {
log.info("销毁");
}
}

运行主启动类,查看控制台的日志信息(只列举主要信息):

com.itheima.a03.LifeCycleBean            : 构造
com.itheima.a03.LifeCycleBean            : 依赖注入: C:\Program Files\Java\jdk1.8.0_60
com.itheima.a03.LifeCycleBean            : 初始化
com.itheima.a03.LifeCycleBean            : 销毁    

除此之外,Spring 还提供了一些对 Bean 生命周期的各个阶段进行拓展的BeanPostProcessor,比如

InstantiationAwareBeanPostProcessorDestructionAwareBeanPostProcessor

实现这两个接口,并使用 @Component 注解标记实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Slf4j
@Component
public class MyBeanPostProcessor implements InstantiationAwareBeanPostProcessor, DestructionAwareBeanPostProcessor {

@Override
public void postProcessBeforeDestruction(Object o, String beanName) throws BeansException {
if ("lifeCycleBean".equals(beanName)) {
log.info("<<<<<<<<<< 销毁执行之前,如 @PreDestroy");
}
}

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
if ("lifeCycleBean".equals(beanName)) {
log.info("<<<<<<<<<< 实例化之前执行,这里返回的对象会替换掉原本的 bean");
}
return null;
}

@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
if ("lifeCycleBean".equals(beanName)) {
log.info("<<<<<<<<<< 实例化之后执行,如果返回 false 会跳过依赖注入节点");
// return false;
}
return true;
}

@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
if ("lifeCycleBean".equals(beanName)) {
log.info("<<<<<<<<<< 依赖注入阶段执行,如 @Autowired、@Value、@Resource");
}
return pvs;
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if ("lifeCycleBean".equals(beanName)) {
log.info("<<<<<<<<<< 初始化执行之前,这里返回的对象会替换掉原本的 bean,如 @PostConstruct、@ConfigurationProperties");
}
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("lifeCycleBean".equals(beanName)) {
log.info("<<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的 bean,如代理增强");
}
return bean;
}
}

再运行主启动类,查看控制台的日志信息(只列举主要信息):

com.itheima.a03.MyBeanPostProcessor     : <<<<<<<<<< 实例化之前执行,这里返回的对象会替换掉原本的 bean
com.itheima.a03.LifeCycleBean                     : 构造
com.itheima.a03.MyBeanPostProcessor      : <<<<<<<<<< 实例化之后执行,如果返回 false 会跳过依赖注入节点
com.itheima.a03.MyBeanPostProcessor      : <<<<<<<<<< 依赖注入阶段执行,如 @Autowired、@Value、@Resource
com.itheima.a03.LifeCycleBean            		 : 依赖注入: C:\Program Files\Java\jdk1.8.0_60
com.itheima.a03.MyBeanPostProcessor      : <<<<初始化执行之前,这里返回的对象会替换掉原本的 bean,如 @PostConstruct、@ConfigurationProperties
com.itheima.a03.LifeCycleBean           		  : 初始化
com.itheima.a03.MyBeanPostProcessor      : <<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的 bean,如代理增强
com.itheima.a03.MyBeanPostProcessor      : <<<<<<<<<< 销毁执行之前,如 @PreDestroy
com.itheima.a03.LifeCycleBean           		  : 销毁    

为什么实现了 BeanPostProcessor 接口后就能够在 Bean 生命周期的各个阶段进行拓展呢?

这使用了模板方法设计模式。

现有如下代码,模拟 BeanFactory 构造 Bean:

1
2
3
4
5
6
7
8
9
static class MyBeanFactory {
public Object getBean() {
Object bean = new Object();
System.out.println("构造 " + bean);
System.out.println("依赖注入 " + bean);
System.out.println("初始化 " + bean);
return bean;
}
}

如果现在需要在依赖注入之后,初始化之前进行其他的操作,那首先能想到的就是直接改写 getBean()相关操作的代码,显然不是一种好方式。

可以定义一个接口:

1
2
3
interface BeanPostProcessor {
void inject(Object bean);
}

然后对 MyBeanFactory 进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class MyBeanFactory {
private List<BeanPostProcessor> processors = new ArrayList<>();

public void addProcessor(BeanPostProcessor processor) {
processors.add(processor);
}

public Object getBean() {
Object bean = new Object();
System.out.println("构造 " + bean);
System.out.println("依赖注入 " + bean);
for (BeanPostProcessor processor : processors) {
processor.inject(bean);
}
System.out.println("初始化 " + bean);
return bean;
}
}

之后如果需要拓展,调用 MyBeanFactory 实例的 addProcessor() 方法添加拓展逻辑即可:

1
2
3
4
5
6
public static void main(String[] args) {
MyBeanFactory beanFactory = new MyBeanFactory();
beanFactory.addProcessor(bean -> System.out.println("解析 @Autowired"));
beanFactory.addProcessor(bean -> System.out.println("解析 @Resource"));
beanFactory.getBean();
}
构造 java.lang.Object@49097b5d
依赖注入 java.lang.Object@49097b5d
解析 @Autowired
解析 @Resource
初始化 java.lang.Object@49097b5d    

4. Bean 的后置处理器

4.1 常见的 Bean 后置处理器

现有如下三个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
public class Bean1 {
private Bean2 bean2;

@Autowired
public void setBean2(Bean2 bean2) {
log.info("@Autowired 生效: {}", bean2);
this.bean2 = bean2;
}

private Bean3 bean3;

@Resource
public void setBean3(Bean3 bean3) {
log.info("@Resource 生效: {}", bean3);
this.bean3 = bean3;
}

private String home;

@Autowired
public void setHome(@Value("${JAVA_HOME}") String home) {
log.info("@Value 生效: {}", home);
this.home = home;
}

@PostConstruct
public void init() {
log.info("@PostConstruct 生效");
}

@PreDestroy
public void destroy() {
log.info("@PreDestroy 生效");
}
}
1
2
3
4
5
public class Bean2 {
}

public class Bean3 {
}

Bean2Bean3 很简单,而在 Bean1 中使用了多个注解以实现 Bean 注入和值注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class A04Application {
public static void main(String[] args) {
// GenericApplicationContext 是一个干净的容器
GenericApplicationContext context = new GenericApplicationContext();
// 用原始方式注册三个 bean
context.registerBean("bean1", Bean1.class);
context.registerBean("bean2", Bean2.class);
context.registerBean("bean3", Bean3.class);

// 初始化容器。执行 beanFactory 后置处理器,添加 bean 后置处理器,初始化所有单例
context.refresh();

// 销毁容器
context.close();
}
}

运行上述方法后,控制台中只打印了与 Spring 相关的日志信息,也就是说 Bean1 中使用的注解并没有生效。

GenericApplicationContext 添加一些 Bean 后置处理器,使得 Bean1 中使用的注解能够生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
//GenericApplicationContext是一个干净的容器
GenericApplicationContext context = new GenericApplicationContext();

context.registerBean("bean1", Bean1.class);
context.registerBean("bean2", Bean2.class);
context.registerBean("bean3", Bean3.class);
//解析 @Value 的值
context.getDefaultListableBeanFactory().setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
//解析@Autowired、@Value注解
context.registerBean(AutowiredAnnotationBeanPostProcessor.class);
//解析@Resource、@PostConstruct、@PreDestroy注解
context.registerBean(CommonAnnotationBeanPostProcessor.class);

//初始化容器。执行 beanFactory 后置处理器,添加 bean 后置处理器,初始化所有单例
context.refresh();

//销毁容器
context.close();
}
com.itheima.a04.Bean1 - @Resource 生效: com.atguigu.a04.Bean3@1649b0e6
com.itheima.a04.Bean1 - @Autowired 生效: com.atguigu.a04.Bean2@34f7cfd9
com.itheima.a04.Bean1 - @Value 生效: C:\Program Files\Java\jdk1.8.0_60
com.itheima.a04.Bean1 - @PostConstruct 生效
com.itheima.a04.Bean1 - @PreDestroy 生效    

解析 @ConfigurationProperties

1
2
3
4
5
6
7
8
@Getter
@Setter
@ToString
@ConfigurationProperties(prefix = "java")
public class Bean4 {
private String home;
private String version;
}

上述代码用于获取环境变量中 java.home 和 java.version 的信息。

对先前的 main()方法进行补充:

1
2
3
4
5
6
7
8
public static void main(String[] args) {

context.registerBean("bean4", Bean4.class);

System.out.println(context.getBean(Bean4.class));

}

Bean4(home=null, version=null)    

Bean4 成功添加到容器中,但值注入失败了,显然是缺少解析 @ConfigurationProperties 注解的后置处理器。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
context.registerBean("bean4", Bean4.class);

// 解析@ConfigurationProperties
ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory());

System.out.println(context.getBean(Bean4.class));
}

Bean4(home=C:\Users\Administrator.jdks\corretto-17.0.11, version=17.0.11)

4.2 AutowiredAnnotationBeanPostProcessor

AutowiredAnnotationBeanPostProcessor 可用于解析@Autowired@Value注解,下面我们对它的运行过程进行深入分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DigInAutowired {
public static void main(String[] args) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
//注册成品的bean,不再进行 bean的创建过程、依赖注入、初始化....
beanFactory.registerSingleton("bean2", new Bean2());
beanFactory.registerSingleton("bean3", new Bean3());
//解析 @Value 的值
beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
//${} 的解析器
beanFactory.addEmbeddedValueResolver(new StandardEnvironment()::resolvePlaceholders);

//之前AutowiredAnnotationBeanPostProcessor 是在beanFactory内部被调用,执行到 依赖注入阶段,获取到后置处理器,执行对应方法
//查看哪些属性、方法加了 @Autowired,这称之为 InjectionMetadata
AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
processor.setBeanFactory(beanFactory);

//不会依赖注入
Bean1 bean1 = new Bean1();
System.out.println(bean1);

//执行依赖注入 解析@Autowired @Value
processor.postProcessProperties(null, bean1, "bean1");
System.out.println(bean1);
}
}
Bean1(bean2=null, bean3=null, home=null)
com.itheima.a04.Bean1 - @Autowired 生效: com.itheima.a04.Bean2@de3a06f
com.itheima.a04.Bean1 - @Value 生效: C:\Program Files\Java\jdk1.8.0_60
Bean1(bean2=com.itheima.a04.Bean2@de3a06f, bean3=null, home=C:\Program Files\Java\jdk1.8.0_60)    

在未调用AutowiredAnnotationBeanPostProcessor#postProcessProperties() 方法时,bean1中的bean2bean3home都没有注入,而在调用之后,成功注入了bean2home,由于bean3是通过@Resource标注,所以不会进行注入。

postProcessProperties()源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
}
return pvs;
}

其中 findAutowiringMetadata() 用于查找哪些属性方法参数上标注@Autowired@Value ,将这些信息封装到InjectionMetadata 中,然后调用inject()反射完成注入。

findAutowiringMetadata()是一个私有方法,利用反射进行调用并打断点查看InjectionMetadata中的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class DigInAutowired {
public static void main(String[] args) throws Exception {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
//注册成品的bean,不再进行 bean的创建过程、依赖注入、初始化....
beanFactory.registerSingleton("bean2", new Bean2());
beanFactory.registerSingleton("bean3", new Bean3());
//解析 @Value 的值
beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
//${} 的解析器
beanFactory.addEmbeddedValueResolver(new StandardEnvironment()::resolvePlaceholders);

//之前AutowiredAnnotationBeanPostProcessor 是在beanFactor内部被调用,执行到 依赖注入阶段,获取到后置处理器,执行对应方法
// 查看哪些属性、方法加了 @Autowired,这称之为 InjectionMetadata
AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
processor.setBeanFactory(beanFactory);

//不会依赖注入
Bean1 bean1 = new Bean1();
Method findAutowiringMetadata =
AutowiredAnnotationBeanPostProcessor.class.getDeclaredMethod("findAutowiringMetadata", String.class, Class.class, PropertyValues.class);
findAutowiringMetadata.setAccessible(true);
//获取 bean1类中 添加了@Autowired 或 @Value 的成员变量,方法参数信息
InjectionMetadata metadata = (InjectionMetadata) findAutowiringMetadata.invoke(processor, "bean1", Bean1.class, null);
//此处断点
System.out.println(metadata);

}
}

image-20241219224813677

InjectionMetadata 中有一个名为injectedElements的集合类型成员变量,其中存储了添加了@Autowired@Value 的成员变量,方法参数信息

然后调用InjectionMetadata#inject()注入

1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
public static void main(String[] args) {
// -------

// 获取 Bean1 上加了 @Value、@Autowired 注解的成员变量、方法参数信息
InjectionMetadata metadata = (InjectionMetadata) method.invoke(postProcessor, "bean1", Bean1.class, null);

// 调用 InjectionMetadata 来进行依赖注入,注入时按类型查找值
metadata.inject(bean1, "bean1", null);
System.out.println(bean1);
}
com.itheima.a04.Bean1 - @Autowired 生效: com.itheima.a04.Bean2@1e4a7dd4
com.itheima.a04.Bean1 - @Value 生效: C:\Program Files\Java\jdk1.8.0_60
Bean1(bean2=com.itheima.a04.Bean2@1e4a7dd4, bean3=null, home=C:\Program Files\Java\jdk1.8.0_60)    

既然InjectionMetadata中保存了 @Value@Autowired 注解的成员变量、方法参数信息,那么inject()中又如何处理呢?

通过以下代码简单概括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SneakyThrows
public static void main(String[] args) {
// ------

// 如何按类型查找值
Field bean3 = Bean1.class.getDeclaredField("bean3");
//成员变量信息 被封装为 DependencyDescriptor 对象
DependencyDescriptor dd1 = new DependencyDescriptor(bean3, false);
//要注入谁
Object o1 = beanFactory.doResolveDependency(dd1, null, null, null);
System.out.println(o1);

Method setBean2 = Bean1.class.getDeclaredMethod("setBean2", Bean2.class);
// MethodParameter 构造方法的第二个参数表示需要解析的方法中参数的索引
DependencyDescriptor dd2 = new DependencyDescriptor(new MethodParameter(setBean2, 0), false);
Object o2 = beanFactory.doResolveDependency(dd2, null, null, null);
System.out.println(o2);

Method setHome = Bean1.class.getDeclaredMethod("setHome", String.class);
DependencyDescriptor dd3 = new DependencyDescriptor(new MethodParameter(setHome, 0), true);
Object o3 = beanFactory.doResolveDependency(dd3, null, null, null);
System.out.println(o3);
}
com.itheima.a04.Bean3@31206beb
com.itheima.a04.Bean2@50de0926
C:\Program Files\Java\jdk1.8.0_60    

5. BeanFactory 后置处理器

5.1 常见的工厂后置处理器

引入所需依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.15</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

需要用到的类信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@ComponentScan("com.itheima.a05.component")
public class Config {

@Bean
public Bean1 bean1() {
return new Bean1();
}

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}

@Bean(initMethod = "init")
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/advanced_spring");
dataSource.setName("root");
dataSource.setPassword("123456");
return dataSource;
}
}
1
2
3
4
5
6
@Slf4j
public class Bean1 {
public Bean1() {
System.out.println("我被 Spring 管理啦");
}
}
1
2
3
4
5
6
7
@Slf4j
@Component
public class Bean2 {
public Bean2() {
log.info("我被 Spring 管理啦");
}
}

使用GenericApplicationContext 作为容器,向容器中注册config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class A05Application {
public static void main(String[] args) {
//GenericApplicationContext 是一个 【干净】的容器
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);

//初始化容器
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}

//销毁容器
context.close();
}
}
config

并没有打印出config 外的 Bean 信息,@Configuration@Bean 并没有被解析。

向容器中注册ConfigurationClassPostProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class A05Application {
public static void main(String[] args) {
//GenericApplicationContext 是一个 【干净】的容器
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
// @ComponentScan @Bean @Import @ImportResource
context.registerBean(ConfigurationClassPostProcessor.class);

//初始化容器
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}

//销毁容器
context.close();
}
}

com.itheima.a05.component.Bean2 - 我被 Spring 管理啦
com.itheima.a05.Bean1 - 我被 Spring 管理啦
com.alibaba.druid.pool.DruidDataSource - {dataSource-1,root} inited
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
bean2
bean1
sqlSessionFactoryBean
dataSource    

整合MyBatis时,@Mapper注解的解析,也需要特定的BeanFactory后置处理器

1
2
3
4
5
6
7
8
@Mapper
public interface Mapper1 {
}

@Mapper
public interface Mapper2 {
}

向容器中注册MapperScannerConfigurer

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
// @ComponentScan @Bean @Import @ImportResource
context.registerBean(ConfigurationClassPostProcessor.class);
context.registerBean(MapperScannerConfigurer.class,
i -> i.getPropertyValues().add("basePackage", "com.itheima.a05.mapper"));

}
mapper1
mapper2    

除此之外,还有一些常用的后置处理器并没有在上述信息中体现。

5.2 工厂后置处理器模拟实现

移除容器中添加的 ConfigurationClassPostProcessorMapperScannerConfigurer 两个后置处理器,编码模拟对应功能的实现。

组件扫描 @ComponentScan

Bean2所在包路径下,新增两个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
@Controller
public class Bean3 {
public Bean3() {
log.info("我被 Spring 管理啦");
}
}

@Slf4j
public class Bean4 {
public Bean4() {
log.info("我被 Spring 管理啦");
}
}

编写 ComponentScanPostProcessor,实现 @ComponentScan 注解的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class ComponentScanPostProcessor implements BeanFactoryPostProcessor {
/**
* 调用 context.refresh() 方法时回调
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
try {
// 查找Config类上的ComponentScan注解
ComponentScan componentScan = AnnotationUtils.findAnnotation(Config.class, ComponentScan.class);
// 如果找到了ComponentScan注解
if (componentScan != null) {
// 遍历ComponentScan注解中的basePackages属性
for (String packageName : componentScan.basePackages()) {
// 打印包名
System.out.println(packageName);
// 创建一个CachingMetadataReaderFactory实例
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
// 构建资源路径
String path = "classpath*:" + packageName.replace(".", "/") + "/**/*.class";
// 获取指定路径下的所有资源
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(path);
// 创建一个AnnotationBeanNameGenerator实例
AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();
// 遍历所有资源
for (Resource resource : resources) {
// 获取资源的MetadataReader
MetadataReader reader = factory.getMetadataReader(resource);
// 获取注解元数据
AnnotationMetadata annotationMetadata = reader.getAnnotationMetadata();
//System.out.println("类信息 => " + reader.getClassMetadata().getClassName());
//System.out.println("是否包含@Component注解 => " + reader.getAnnotationMetadata().hasAnnotation(Component.class.getName()));
//System.out.println("是否包含@Component的派生注解 => " + reader.getAnnotationMetadata().hasMetaAnnotation(Component.class.getName()));
// 如果类上有@Component注解或其派生注解
if (annotationMetadata.hasAnnotation(Component.class.getName())
|| annotationMetadata.hasMetaAnnotation(Component.class.getName())) {
// 创建一个AbstractBeanDefinition实例
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(reader.getClassMetadata().getClassName())
.getBeanDefinition();
// 如果configurableListableBeanFactory是DefaultListableBeanFactory的实例
if (configurableListableBeanFactory instanceof DefaultListableBeanFactory beanFactory) {
// 生成bean名称
String beanName = generator.generateBeanName(beanDefinition, beanFactory);
// 注册bean定义
beanFactory.registerBeanDefinition(beanName, beanDefinition);
}
}
}
}
}
} catch (IOException e) {
// 打印异常堆栈信息
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
context.registerBean(ComponentScanPostProcessor.class);
//初始化容器
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}
//销毁容器
context.close();
}
com.itheima.a05.component
com.itheima.a05.component.Bean2 - 我被 Spring 管理啦
com.itheima.a05.component.Bean3 - 我被 Spring 管理啦
config
com.itheima.a05.ComponentScanPostProcessor
bean2
bean3    

@Bean 的解析

Config类中新增一个方法作为对比项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
@ComponentScan("com.atguigu.a05.component")
public class Config {

//新增方法,用于对比
public Bean2 bean2() {
return new Bean2();
}

@Bean
public Bean1 bean1() {
return new Bean1();
}

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}

@Bean(initMethod = "init")
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/advanced_spring");
dataSource.setName("root");
dataSource.setPassword("123456");
return dataSource;
}
}

编写 AtBeanPostProcessor 实现 @Bean 注解的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class AtBeanPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
try {
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
MetadataReader reader = factory.getMetadataReader(new ClassPathResource("com/itheima/a05/Config.class"));

Set<MethodMetadata> methods =
reader.getAnnotationMetadata().getAnnotatedMethods(Bean.class.getName());

for (MethodMetadata method : methods) {
String initMethod = method.getAnnotationAttributes(Bean.class.getName()).get("initMethod").toString();
System.out.println(method);
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
builder.setFactoryMethodOnBean(method.getMethodName(), "config");
// 工厂方法、构造方法的参数 自动装配选择的模式为AUTOWIRE_CONSTRUCTOR
builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
if (initMethod.length() > 0) {
builder.setInitMethodName(initMethod);
}
if (configurableListableBeanFactory instanceof DefaultListableBeanFactory beanFactory) {
beanFactory.registerBeanDefinition(method.getMethodName(), beanDefinition);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

sqlSessionFactoryBean 需要一个DataSource类型的参数,所以必须指定 自动装配模式

1
2
3
4
5
6
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(AtBeanPostProcessor.class);

context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}

context.close();
}
com.itheima.a05.Config.bean1()
com.itheima.a05.Config.sqlSessionFactoryBean(javax.sql.DataSource)
com.itheima.a05.Config.dataSource()
com.itheima.a05.Bean1 - 我被 Spring 管理啦
com.alibaba.druid.pool.DruidDataSource - {dataSource-1,root} inited
config
com.itheima.a05.AtBeanPostProcessor
bean1
sqlSessionFactoryBean
dataSource

@Mapper 的解析

@Mapper注解是标注在接口上的,Spring 并不能真的管理一个接口,最终管理的还是一个个的对象。

而这些接口依赖于 @MapperFactoryBean 将接口转换为对象。

在 Config 添加注册 Mapper1Mapper2 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
public MapperFactoryBean<Mapper1> mapper1(SqlSessionFactory sqlSessionFactory) {
MapperFactoryBean<Mapper1> factory = new MapperFactoryBean<>(Mapper1.class);
factory.setSqlSessionFactory(sqlSessionFactory);
return factory;
}

@Bean
public MapperFactoryBean<Mapper2> mapper2(SqlSessionFactory sqlSessionFactory) {
MapperFactoryBean<Mapper2> factory = new MapperFactoryBean<>(Mapper2.class);
factory.setSqlSessionFactory(sqlSessionFactory);
return factory;
}

运行后,容器中 包含 名为 mapper1mapper2 的 Bean

这种方式虽然可以完成 Mapper 接口的注册,但每次只能单个注册,不能批量注册。

移除 Config 类中的 mapper1()mapper2() 方法,编写 MapperPostProcessor 实现@Mapper 注解的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MapperPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

}

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanFactory) throws BeansException {

try {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
//扫描mapper包下的资源
Resource[] resources = resolver.getResources("classpath:com/atguigu/a05/mapper/**/*.class");
//beanName生成器
AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();
//读取类的元数据信息
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
for (Resource resource : resources) {
MetadataReader reader = factory.getMetadataReader(resource);
ClassMetadata classMetadata = reader.getClassMetadata();
if (classMetadata.isInterface()) {
//根据接口生成
AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition(MapperFactoryBean.class)
.addConstructorArgValue(classMetadata.getClassName())
.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
.getBeanDefinition();

//根据Mapper接口生成BeanDefinition
AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition(classMetadata.getClassName())
.getBeanDefinition();

String beanName = generator.generateBeanName(beanDefinition2, beanFactory);
beanFactory.registerBeanDefinition(beanName, beanDefinition1);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);

context.registerBean(AtBeanPostProcessor.class);
context.registerBean(MapperPostProcessor.class);

context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}

context.close();
}
com.itheima.a05.Config.bean1()
com.itheima.a05.Config.sqlSessionFactoryBean(javax.sql.DataSource)
com.itheima.a05.Config.dataSource()
22:24:18.940 [main] INFO com.alibaba.druid.pool.DruidDataSource - {dataSource-1,root} inited
22:24:19.042 [main] INFO com.itheima.a05.Bean1 - 我被 Spring 管理啦
config
com.itheima.a05.AtBeanPostProcessor
com.itheima.a05.MapperPostProcessor
mapper1
mapper2
bean1
sqlSessionFactoryBean
dataSource

容器中存在 mapper1mapper2 两个 Bean。

5.3 【补充】注册创建完成的 Bean

如果要将 Bean 添加到 Spring 容器中,需要先根据配置文件或注解信息为每一个 Bean 生成一个 BeanDefinition,然后将这些 BeanDefinition 添加到 BeanDefinitionRegistry 中,当创建 Bean 对象时,直接从 BeanDefinitionRegistry 中获取 BeanDefinition 来生成 Bean。

如果生成的 Bean 是单例的,Spring 会将它们保存到 SingletonBeanRegistry 中,后续需要时从这里面寻找,避免重复创建。

那么向 Spring 容器中添加单例 Bean 时,可以跳过注册 BeanDefinition,直接向 SingletonBeanRegistry 中添加创建完成的 Bean。既然添加的是创建完成的 Bean,所以 这个 Bean 不会经过 Spring 的生命周期。

SingletonBeanRegistry 是一个接口,它有一个子接口名为 ConfigurableListableBeanFactory,而这恰好是 BeanFactoryPostProcessor 接口中抽象方法的参数:

1
2
3
4
@FunctionalInterface
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}

尝试使用 BeanFactoryPostProcessor 注册创建完成的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
public class TestBeanFactoryPostProcessor {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean("bean2", Bean2.class);
context.registerBean(MyBeanFactoryPostProcessor.class);
context.refresh();

Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println(">>>>>>>>>>>>>>>>>>");
System.out.println(context.getBean(Bean1.class));
}

static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Bean1 bean1 = new Bean1();
bean1.setName("mofan");
beanFactory.registerSingleton("bean1", bean1);
}
}

@Getter
@ToString
static class Bean1 {
@Setter
private String name;
private Bean2 bean2;

@Autowired
private void setBean2(Bean2 bean2) {
log.debug("依赖注入 bean2");
this.bean2 = bean2;
}

@PostConstruct
public void init() {
log.debug("初始化...");
}
}

static class Bean2 {
}
}
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
bean2
testBeanFactoryPostProcessor.MyBeanFactoryPostProcessor
>>>>>>>>>>>>>>>>>>
TestBeanFactoryPostProcessor.Bean1(name=mofan, bean2=null)  

BeanDefinition 的名称数组中不包含 bean1,也没有输出任何与经过 Spring 生命周期相关的日志信息,容器中 bean1 里注入的 bean2 也是 null。这表明通过这种方式注册的 Bean 不会注册 BeanDefinition,也不会经过 Spring 生命周期。

6. Aware 接口

6.1 Aware 接口

Aware 接口用于注入一些与容器相关的信息,例如

  1. BeanNameAware 注入 bean 的名字
  2. BeanFactoryAware 注入 BeanFactory 容器
  3. ApplicationContextAware 注入 ApplicationContext 容器
  4. EmbeddedValueResolverAware ${}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyBean implements BeanNameAware, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(MyBean.class);

@Override
public void setBeanName(String name) {
logger.debug("\n当前bean =>" + this + " \n名称 => " + name);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
logger.debug("\n当前bean =>" + this + " \nApplicationContext => " + applicationContext);
}
}

1
2
3
4
5
6
7
8
9
10
@Slf4j
public class A06Application {
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("myBean", MyBean.class);

context.refresh();
context.close();
}
}
当前bean =>com.itheima.a06.MyBean@1a3869f4 
名称 => myBean
当前bean =>com.itheima.a06.MyBean@1a3869f4 
ApplicationContext => org.springframework.context.support.GenericApplicationContext@75bd9247, started on Mon Dec 23 23:08:39 CST 2024  

6.2 InitializingBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyBean implements BeanNameAware, ApplicationContextAware,InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(MyBean.class);

@Override
public void setBeanName(String name) {
logger.debug("\n当前bean =>" + this + " \n名称 => " + name);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
logger.debug("\n当前bean =>" + this + " \nApplicationContext => " + applicationContext);
}

@Override
public void afterPropertiesSet() throws Exception {
logger.debug("\n当前bean =>" + this + " 初始化");
}

}
当前bean =>com.itheima.a06.MyBean@63440df3 
名称 => myBean
com.itheima.a06.MyBean - 当前bean =>com.itheima.a06.MyBean@63440df3 
ApplicationContext => org.springframework.context.support.GenericApplicationContext@75bd9247, started on Mon Dec 23 23:10:58 CST 2024
com.itheima.a06.MyBean - 当前bean =>com.itheima.a06.MyBean@63440df3 初始化    

BeanFactoryAwareApplicationContextAwareEmbeddedValueResolverAware 三个接口的功能可以使用 @Autowired 注解实现,InitializingBean 接口的功能也可以使用 @PostConstruct注解实现,为什么还要使用接口呢?

@Autowired@PostConstruct 注解的解析需要使用 Bean 后置处理器,属于拓展功能,而Aware接口属于内置功能,不加任何拓展 Spring 就能识别。

在某些情况下,拓展功能会失效,而内置功能不会失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyBean implements BeanNameAware, ApplicationContextAware,InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(MyBean.class);

@Override
public void setBeanName(String name) {
logger.debug("\n当前bean =>" + this + " \n名称 => " + name);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
logger.debug("\n当前bean =>" + this + " \nApplicationContext => " + applicationContext);
}

@Override
public void afterPropertiesSet() throws Exception {
logger.debug("\n当前bean =>" + this + " 初始化");
}

@Autowired
public void method(ApplicationContext applicationContext) {
logger.debug("\n当前bean =>" + this + " \n使用@AutoWired注入,ApplicationContext => " + applicationContext);
}

@PostConstruct
public void init() {
logger.debug("\n当前bean =>" + this + " 使用@PostConstruct 初始化");
}
}
com.itheima.a06.MyBean - 
当前bean =>com.itheima.a06.MyBean@63440df3 
名称 => myBean
com.itheima.a06.MyBean - 
当前bean =>com.itheima.a06.MyBean@63440df3 
ApplicationContext => org.springframework.context.support.GenericApplicationContext@75bd9247, started on Mon Dec 23 23:23:43 CST 2024
com.itheima.a06.MyBean - 
当前bean =>com.itheima.a06.MyBean@63440df3 初始化    

运行 main()方法会发现使用的注解没有被成功解析,原因很简单,GenericApplicationContext 是一个干净的容器,其内部没有用于解析这些注解的bean后置处理器。如果想要这些注解生效,则需要添加必要的 bean 后置处理器:

1
2
context.registerBean(AutowiredAnnotationBeanPostProcessor.class);
context.registerBean(CommonAnnotationBeanPostProcessor.class);

6.3 失效的@Autowired 注解

在某些情况下,尽管容器中存在必要的后置处理器,但@Autowired@PostConstruct等注解也会失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MyConfig1 {
private static final Logger logger = LoggerFactory.getLogger(MyConfig1.class);

@Autowired
private void setApplicationContext(ApplicationContext applicationContext) {
logger.info("注入 ApplicationContext");
}

@PostConstruct
public void init() {
logger.info("执行 MyConfig1 的 init 方法");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("myConfig1", MyConfig1.class);
// 解析 @Autowired 注解的Bean后处理器
context.registerBean(AutowiredAnnotationBeanPostProcessor.class);
// 解析 @PostConstruct 注解的Bean后处理器
context.registerBean(CommonAnnotationBeanPostProcessor.class);
// 解析@ComponentScan、@Bean、@Import、@ImportResource注解的后处理器
context.registerBean(ConfigurationClassPostProcessor.class);

context.refresh();//1、beanFactory后置处理器 2、添加bean后置处理器 3、初始化单例
context.close();
}

com.itheima.a06.MyConfig1 - 注入 ApplicationContext
com.itheima.a06.MyConfig1 - 执行 MyConfig1 的 init 方法    

@Autowired@PostConstruct 注解解析成功。

然后我们向Config1 中添加BeanFactoryPostProcessor

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MyConfig1 {
private static final Logger logger = LoggerFactory.getLogger(MyConfig1.class);
// --snip--

@Bean //beanFactory后置处理器
public BeanFactoryPostProcessor processor1() {
return beanFactory -> logger.info("执行 processor1");
}
}

Config1 中添加了一个被 @Bean 注解标记的 processor1()方法,用于向容器中添加 BeanFactoryPostProcessor

再次运行

com.itheima.a06.MyConfig1 - 执行 processor1    

用传统接口方式的注入和初始化依然成功,processor1() 方法成功生效,但@Autowired@PostConstruct 的注入和初始化失败。

那是什么原因导致的呢?

配置类 @Autowired 注解失效分析

  • Java 配置类不包含 BeanFactoryPostProcessor 的情况
1
2
3
4
5
6
7
8
9
10
11
12
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 创建成功

image-20250206212342130

  • Java 配置类包含 BeanFactoryPostProcessor 的情况

    • 当 Java 配置类中定义了BeanFactoryPostProcessor 时,如果要创建其中的 BeanFactoryPostProcessor 必须提前创建 Java 配置类,而此时的 BeanPostProcessor 还未准备好,导致 @Autowired 等注解失效
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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

    image-20250206212814752

可以使用 Spring 内置的接口,避免类似问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyConfig2 implements InitializingBean, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(MyConfig2.class);

@Override
@Autowired
public void setApplicationContext(ApplicationContext applicationContext) {
logger.info("注入 ApplicationContext");
}


@Override
public void afterPropertiesSet() throws Exception {
logger.info("初始化");
}
@Bean //beanFactory后置处理器
public BeanFactoryPostProcessor processor2() {
return beanFactory -> logger.info("执行 processor2");
}

}
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("myConfig2", MyConfig2.class);
context.registerBean(AutowiredAnnotationBeanPostProcessor.class);
context.registerBean(CommonAnnotationBeanPostProcessor.class);
context.registerBean(ConfigurationClassPostProcessor.class);

context.refresh();
context.close();
}

com.itheima.a06.MyConfig2 - 注入 ApplicationContext
com.itheima.a06.MyConfig2 - 初始化
com.itheima.a06.MyConfig2 - 执行 processor2    

总结

  • Aware 接口提供了一种内置的注入手段,可以注入BeanFactory ,ApplicationContext
  • InitializingBean接口提供了一种内置的初始化手段
  • 内置的注入和初始化不受扩展功能的影响,总会执行,因此 Spring 框架内部的类常用它们

7. 初始化与销毁

初始化和销毁 Bean 的实现有三种:

  1. 依赖于后置处理器提供的拓展功能

  2. 相关接口的功能

  3. 使用 @Bean 注解中的属性进行指定

三种初始化方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//3种初始化方法
@Slf4j
public class Bean1 implements InitializingBean {
@PostConstruct
public void init() {
log.info("@PostConstruct 初始化");
}

@Override
public void afterPropertiesSet() throws Exception {
log.info("InitializingBean 初始化");
}

public void init3() {
log.info("@Bean 初始化");
}
}

三种销毁方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
public class Bean2 implements DisposableBean {
@PreDestroy
public void destroy1() {
log.info("@PreDestroy 销毁");
}

@Override
public void destroy() throws Exception {
log.info("DisposableBean 销毁");
}

public void destroy3() {
log.info("@Bean 销毁");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootApplication
public class A07Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A07Application.class, args);
context.close();
}

@Bean(initMethod = "init3")
public Bean1 bean1() {
return new Bean1();
}

@Bean(destroyMethod = "destroy3")
public Bean2 bean2() {
return new Bean2();
}
}

执行顺序

com.itheima.a07.Bean1                    : @PostConstruct 初始化
com.itheima.a07.Bean1                    : InitializingBean 初始化
com.itheima.a07.Bean1                    : @Bean 初始化
com.itheima.a07.Bean2                    : @PreDestroy 销毁
com.itheima.a07.Bean2                    : DisposableBean 销毁
com.itheima.a07.Bean2                    : @Bean 销毁   

8. Scope

8.1 Scope 的类型与销毁

Scope 用于指定 Bean 的作用范围,有如下五个取值:

  • singleton:单例(默认值)。容器启动时创建(未设置延迟),容器关闭时销毁
  • prototype:多例。每次使用时创建,不会自动销毁,需要调用 DefaultListableBeanFactory#destroyBean() 进行销毁
  • request:作用于 Web 应用的请求范围。每次请求用到此 Bean 时创建,请求结束时销毁
  • session:作用于 Web 应用的会话范围。每个会话用到此 Bean 时创建,会话结束时销毁
  • application:作用于 Web 应用的 ServletContext。Web 容器用到此 Bean 时创建,容器关闭时销毁
    前两个取值不再赘述,重点看下后三个取值。
1
2
3
4
5
6
7
8
9
@Slf4j
@Component
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class BeanForRequest {
@PreDestroy
public void destroy() {
log.info("destroy");
}
}
1
2
3
4
5
6
7
8
9
@Slf4j
@Component
@Scope(WebApplicationContext.SCOPE_SESSION)
public class BeanForSession {
@PreDestroy
public void destroy() {
log.info("destroy");
}
}
1
2
3
4
5
6
7
8
9
@Slf4j
@Component
@Scope(WebApplicationContext.SCOPE_APPLICATION)
public class BeanForApplication {
@PreDestroy
public void destroy() {
log.info("destroy");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
public class MyController {
@Lazy
@Autowired
private BeanForRequest beanForRequest;

@Lazy
@Autowired
private BeanForSession beanForSession;

@Lazy
@Autowired
private BeanForApplication beanForApplication;

@GetMapping(value = "/test", produces = "text/html")
public String test(HttpServletRequest request, HttpSession session) {
// 设置 session 过期时间为 10 秒
session.setMaxInactiveInterval(10);
// ServletContext sc = request.getServletContext();
return "<ul>" +
"<li>request scope: " + beanForRequest + "</li>" +
"<li>session scope: " + beanForSession + "</li>" +
"<li>application scope: " + beanForApplication + "</li>" +
"</ul>";
}
}

主启动类

1
2
3
4
5
6
7
@SpringBootApplication
public class A08Application {

public static void main(String[] args) {
SpringApplication.run(A08Application.class, args);
}
}

如果使用的 JDK 版本大于 8,需要要启动参数中添加如下信息避免报错:

1
--add-opens java.base/java.lang=ALL-UNNAMED

但更建议在 pom.xml 中添加以下配置,一劳永逸:

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
--add-opens java.base/java.lang=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
</build>

运行主启动类,在浏览器中访问 http://localhost:8080/test,页面上显示:

request scope: com.itheima.a08.BeanForRequest@2b6ecfd4
session scope: com.itheima.a08.BeanForSession@54ecf133
application scope:com.itheima.a08.BeanForApplication@29b1126e

刷新页面,页面上的信息变化为:

request scope: com.itheima.a08.BeanForRequest@3ef32346
session scope: com.itheima.a08.BeanForSession@54ecf133
application scope:com.itheima.a08.BeanForApplication@29b1126e    

可以看到 request scope 发生了变化,session scopeapplication scope 没有变化。

换一个浏览器访问 http://localhost:8080/test,两个浏览器中的会话肯定不是同一个,此时 session scope 应该会发生变化:

request scope: com.itheima.a08.BeanForRequest@49575379
session scope: com.itheima.a08.BeanForSession@86a5942
application scope:com.itheima.a08.BeanForApplication@29b1126e   

application 的作用范围是 ServletContext,要想 application scope 发生变化可以重启程序。

当刷新页面后, request scope 的值发生变化,request 作用范围的 Bean 执行了销毁方法。

com.itheima.a08.BeanForRequest           : 销毁    

如果想看到 session 作用范围的 Bean 执行销毁方法,可以等 session 过期时在控制台上看到对应的信息。默认情况下,session 的过期时间是 30 分钟,为了更好地测试,可以在配置文件中添加:

1
2
# 修改 session 过期时间为 10s
server.servlet.session.timeout=10s

这个配置是全局的,如果只想针对某个请求进行配置,则可以:

1
2
3
4
5
6
7
@GetMapping(value = "/test", produces = "text/html")
public String test(HttpServletRequest request, HttpSession session) {
// 设置 session 过期时间为 10 秒
session.setMaxInactiveInterval(10);

// --snip--
}

设置 session 过期时间为 10 秒后,并不表示不进行任何操作 10 秒后就能在控制台上看到执行销毁方法的信息,经过测试,大概会等 1 分钟,静静等待 1 分钟左右,控制台上显示:

com.itheima.a08.BeanForSession        : 销毁

很遗憾没有办法看到application 作用范围的 Bean 执行销毁方法,因为 Spring 似乎并没有对 application 作用范围的 Bean 进行正确的销毁处理,因此在 Servlet 容器销毁时看不到 application 作用范围的 Bean 执行销毁方法。

8.2 Scope 失效分析

现有两个类

1
2
3
4
@Scope("prototype")
@Component
public class F1 {
}
1
2
3
4
5
6
@Component
@Getter
public class E {
@Autowired
private F1 f1;
}

执行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@ComponentScan("com.atguigu.a09")
public class A09Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A09Application.class);

E e = context.getBean(E.class);
log.info("{}", e.getF1());
log.info("{}", e.getF1());
log.info("{}", e.getF1());

context.close();
}
}

F1Scopeprototype,向 E 中注入后,log.info("{}", e.getF1()) 打印出的应该是不同的对象,可结果却相同

com.itheima.a09.A09Application - com.itheima.a09.F1@55183b20
com.itheima.a09.A09Application - com.itheima.a09.F1@55183b20
com.itheima.a09.A09Application - com.itheima.a09.F1@55183b20    

获取到的f1居然都是同一个,也就是说向单例对象中注入多例对象失败了。

对于单例对象来说,依赖注入仅发生了一次,后续不会再注入其他的 f1,因此 e 始终使用的是第一次注入的 f1

1
2
3
4
5
6
7
8
graph LR

e1(e 创建)
e2(e set 注入 f)

f1(f 创建)

e1-->f1-->e2

image-20250206214110018

可以使用 @Lazy 生成代理对象,虽然代理对象依旧是同一个,但每次使用代理对象中的方法时,会由代理对象创建新的目标对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

image-20250206214145420

方案一

1
2
3
4
5
6
7
@Component
@Getter
public class E {
@Lazy
@Autowired
private F1 f1;
}

执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@ComponentScan("com.atguigu.a09")
public class A09Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A09Application.class);

E e = context.getBean(E.class);
log.info("{}", e.getF1().getClass());
log.info("{}", e.getF1());
log.info("{}", e.getF1());
log.info("{}", e.getF1());

context.close();
}
}
com.itheima.a09.A09Application - class com.itheima.a09.F1EnhancerBySpringCGLIBEnhancerBySpringCGLIBf49b54ad
com.itheima.a09.A09Application - com.itheima.a09.F1@429bffaa
com.itheima.a09.A09Application - com.itheima.a09.F1@483f6d77
com.itheima.a09.A09Application - com.itheima.a09.F1@63a12c68    

使用@Lazy注解后,注入的是代理对象,每次获取到的f1不再是同一个。

方案二

除了使用 @Lazy 注解外,可以使用 @Scope 注解的 proxyMode 属性指定代理模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class F2 {
}


@Getter
@Component
public class E {

@Autowired
private F2 f2;
}

执行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@ComponentScan("com.atguigu.a09")
public class A09Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(A09Application.class);

E e = context.getBean(E.class);

log.info("{}", e.getF2().getClass());
log.info("{}", e.getF2());
log.info("{}", e.getF2());
log.info("{}", e.getF2());

context.close();
}
}
com.itheima.a09.A09Application - class com.itheima.a09.F2$$EnhancerBySpringCGLIB$$fbdf70d3
com.itheima.a09.A09Application - com.itheima.a09.F2@6d4d66d2
com.itheima.a09.A09Application - com.itheima.a09.F2@2a265ea9
com.itheima.a09.A09Application - com.itheima.a09.F2@11392934    

方案三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Scope(value = "prototype")
public class F3 {
}

@Component
public class E {

@Autowired
private ObjectFactory<F3> f3;

public F3 getF3() {
return f3.getObject();
}
}

执行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@ComponentScan("com.atguigu.a09")
public class A09Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A09Application.class);

E e = context.getBean(E.class);
log.info("{}", e.getF3().getClass());
log.info("{}", e.getF3());
log.info("{}", e.getF3());
log.info("{}", e.getF3());

context.close();
}
}
com.itheima.a09.A09Application - class com.itheima.a09.F3
com.itheima.a09.A09Application - com.itheima.a09.F3@25ce9dc4
com.itheima.a09.A09Application - com.itheima.a09.F3@17f62e33
com.itheima.a09.A09Application - com.itheima.a09.F3@27406a17    

方案四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Scope(value = "prototype")
public class F4 {
}

@Component
public class E {

@Autowired
private ApplicationContext applicationContext;

public F4 getF4() {
return applicationContext.getBean(F4.class);
}
}

执行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
@ComponentScan("com.atguigu.a09")
public class A09Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(A09Application.class);

E e = context.getBean(E.class);

log.info("{}", e.getF4());
log.info("{}", e.getF4());

context.close();
}
}
com.itheima.a09.A09Application - com.itheima.a09.F4@54422e18
com.itheima.a09.A09Application - com.itheima.a09.F4@117159c0    

如果对性能要求较高,则推荐使用后两种方式,前两种使用代理会有一定的性能损耗;如果不在乎那点性能损耗,则可以使用第一种方式,这种方式最简单。

四种解决方式虽然不同,但在理念上殊途同归,都是推迟了其他 Scope Bean 的获取,或者说按需加载。

9. AspectJ 编译器增强

新建一个 SpringBoot 项目,引入 Aop 相关依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

新建 Service 类

1
2
3
4
5
6
7
8
@Service
public class MyService {
private static final Logger log = LoggerFactory.getLogger(MyService.class);

public void foo() {
log.info("foo()");
}
}

新建切面类,当前切面类没有被 Spring 管理

1
2
3
4
5
6
7
8
9
@Aspect //切面类并没有被Spring管理
public class MyAspect {
private static final Logger logger = LoggerFactory.getLogger(MyAspect.class);

@Before("execution(* com.itheima.service.MyService.foo())")
public void before() {
logger.info("before()");
}
}

主启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
public class A10Application {
private static final Logger log = LoggerFactory.getLogger(A10Application.class);

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A10Application.class, args);
MyService service = context.getBean(MyService.class);

log.info("service class: {}", service.getClass());
service.foo();

context.close();
}
}
com.itheima.A10Application                : service class: class com.itheima.service.MyService
com.itheima.aop.MyAspect                  : before()
com.itheima.service.MyService             : foo()    

service.getClass()打印出的是原始类的 Class 信息,而非代理类 Class 信息。

实际并没有通过代理进行增强,而是用 AspectJ 编译器进行增强,原理是 通过改写目标类源文件来增强

在 pom 中引入插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>8</source>
<target>8</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<goals>
<!-- use this goal to weave all your main classes -->
<goal>compile</goal>
<!-- use this goal to weave all your test classes -->
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

使用 Maven 进行编译,查看编译后生成的 target 文件夹下的 MyService.class 文件

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class MyService {
private static final Logger log = LoggerFactory.getLogger(MyService.class);

public MyService() {
}

public void foo() {
MyAspect.aspectOf().before();
log.info("foo()");
}
}

编译后的代码中新增加了一行代码,MyAspect.aspectOf().before(),编译时增强

既然如此,那么不依赖 Spring 容器也能实现方法的增强

1
2
3
4
5
6
7
8
9
public class A10Application {
private static final Logger log = LoggerFactory.getLogger(A10Application.class);

public static void main(String[] args) {
MyService service = new MyService();
log.info("service class: {}", service.getClass());
service.foo();
}
}
com.itheima.A10Application                : service class: class com.itheima.service.MyService
com.itheima.aop.MyAspect                  : before()
com.itheima.service.MyService            : foo()   

这种增强方式,可以突破一些代理的限制,代理本质上是通过方法重写来实现,如果目标是 static 的,那么代理是不能增强的

10. Agent 增强

Service 类

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@Service
public class MyService {
final public void foo() {
log.info("foo()");
bar();
}

public void bar(){
log.info("bar()");
}
}

切面类

1
2
3
4
5
6
7
8
9
@Slf4j
@Aspect
public class MyAspect {

@Before("execution(* com.itheima.service.MyService.*())")
public void before() {
log.info("before()");
}
}

主启动类

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@SpringBootApplication
public class A11Application {

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A11Application.class, args);
MyService service = context.getBean(MyService.class);
log.info("service class: {}", service.getClass());
service.foo();
}

}
com.itheima.A11Application                  : service class: class com.itheima.service.MyService
com.itheima.aop.MyAspect                    : before()
com.itheima.service.MyService               : foo()
com.itheima.aop.MyAspect                    : before()
com.itheima.service.MyService               : bar()    

前提需要在resource目录下新建 METE-INF 文件夹,并在 目录下新建 aop.xml,其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
<aspectj>
<aspects>
<!-- 切面类全限定类名 -->
<aspect name="com.itheima.aop.MyAspect"/>
<weaver options="-verbose -showWeaveInfo">
<!-- 被增强方法所在类的全限定类名 -->
<include within="com.itheima.service.MyService"/>
<!-- 切面类全限定类名 -->
<include within="com.itheima.aop.MyAspect"/>
</weaver>
</aspects>
</aspectj>

运行时添加 VM options

1
-javaagent:D:\environment\Maven\3.6.3-repository\.m2\repository\org\aspectj\aspectjweaver\1.9.7\aspectjweaver-1.9.7.jar

其中的 D:\environment\Maven\3.6.3-repository.m2 指本地 Maven 仓库地址,还需要确保本地仓库中存在 1.9.7 版本的 aspectjweaver,否则修改至对应版本。

从输出的内容可以看到 service.getClass() 打印出的信息也是原始类的 Class 信息,而非代理类的 Class 信息。因此不依赖 Spring 容器,直接 new 一个 MyService 实例并调用其 foo() 方法也能达到增强的目的。

如果查看 MyService 对应的 class 文件,会发现其内容并没有被修改,可以断定不是编译时增强,这里是在类加载时增强。

利用 Arthas 反编译类文件

可以借助阿里巴巴的 Arthas 来反编译加载的类文件,下载地址:下载 | arthas

在使用 Arthas 前,需要确保对应 Java 进程的存在,因此在上述代码中调用 service.foo()方法后并没有关闭 Spring 容器。

解压下载的压缩包,进入 arthas-boot.jar 文件的同级目录,使用终端工具执行 java -jar .\arthas-boot.jar

运行arthas-boot的jar包

运行之后会列举出存在的 Java 进程,找到需要连接的进程,之后输入目标进程对应的序号。当界面上成功显示 Arthas 的 Banner 时,证明连接成功:

选择A11Application的Java进程

输入 jad indi.mofan.service.MyService 表示需要反编译 MyService

反编译MyService文件

可以看到 foo() 和 bar() 方法的第一行都被增加了一行代码,也就是这行代码对这两个方法实现了增强。

不仅如此,如果使用代理实现增强,被调用的 bar() 方法不会被成功增强,因为调用时默认使用了 this 关键词,表示调用的是原类中的方法,而不是代理类中的方法

11. 动态代理

11.1 JDK 动态代理

JDK 动态代理 只能 针对接口进行代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class JDKProxyDemo {
interface Foo {
void foo();
}

static class Target implements Foo {
@Override
public void foo() {
System.out.println("Target foo");
}
}


public static void main(String[] param) {
//目标对象
Target target = new Target();

Foo proxy = (Foo) Proxy.newProxyInstance(JDKProxyDemo.class.getClassLoader(), //加载在运行期间动态生成的字节码文件
new Class[]{Foo.class},
(p, method, args) -> {
System.out.println("before");
Object result = method.invoke(target, args);//让代理返回目标方法执行的结果
System.out.println("after");
return result;
});
proxy.foo();
}
}
before
Target foo
after    

总结

代理对象和目标对象类似兄弟关系,都实现了相同接口,因此不能将代理对象强转成目标对象类型

代理类与目标类之间没有继承关系,因此目标类可以被final修饰

11.2 CGLib 动态代理

CGLib 动态代理与 JDK 动态代理不一样,无需目标类实现某个特定的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CglibProxyDemo {
static class Target {
public void foo() {
System.out.println("foo");
}
}

public static void main(String[] param) {

Target t = (Target) Enhancer.create(Target.class, (MethodInterceptor) (proxy, method, args, methodProxy) -> {
System.out.println("before");
// Object result = method.invoke(new Target(), args); //方法反射调用目标
//可以避免反射调用
// Object result = methodProxy.invoke(new Target(), args); //内部没有用反射,需要目标(Spring)
Object result = methodProxy.invokeSuper(proxy, args);//内部没有用反射,需要代理
System.out.println("after");

return result;
});

t.foo();
}
}
before
foo
after    

调用目标方法的方式有三种

1
2
3
4
Object result = method.invoke(new Target(), args); //方法反射调用目标
//methodProxy 可以避免反射调用
Object result = methodProxy.invoke(new Target(), args); //内部没有用反射,需要目标(Spring)
Object result = methodProxy.invokeSuper(proxy, args);//内部没有用反射,需要代理

总结

  • 与 JDK 动态代理相比,CGLib 动态代理无需实现接口

  • 代理对象和目标对象是父子关系,也就是说代理类继承了目标类

  • 由于代理类继承了目标类,因此目标类不能被 final 修饰,否则将报异常信息

  • 代理类继承目标类后,通过重写目标类中的方法进行增强,因此方法不能被 final 修饰,否则将无法被增强,但不会抛出异常

12. JDK 动态代理原理

12.1 模拟 JDK 动态代理的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class A13 {
interface Foo {
void foo();
}

static class Target implements Foo {
@Override
public void foo() {
System.out.println("Target.foo");
}
}

public static void main(String[] args) {
new $Proxy0().foo();
}
}

代理类

1
2
3
4
5
6
7
8
9
public class $Proxy0 implements Foo {
@Override
public void foo() {
//1.功能增强
System.out.println("before");
//2.调用目标方法
new Target().foo();
}
}
before
Target.foo    

代码看起来太简单了,但如果是 JDK 中的实现

  • 功能增强 的代码实现会直接硬编码吗???
  • 调用目标方法 一定要调用吗???存不存在满足条件才调用的场景???

也就是说,"功能增强"和"调用目标"这两部分的代码都是不确定的。

针对这种"不确定"的实现,可以提供一个抽象方法,等到用户具体使用时提供抽象的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class A13 {
interface Foo {
void foo();
}

interface InvocationHandler {
void invoke();
}

static class Target implements Foo {
@Override
public void foo() {
System.out.println("Target.foo");
}
}

public static void main(String[] args) {
Foo proxy = new $Proxy0(new InvocationHandler() {
@Override
public void invoke() {
//1.功能增强
System.out.println("before");
//2.调用目标方法
new Target().foo();
}
});
proxy.foo();
}
}

代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class $Proxy0 implements Foo {

private InvocationHandler h;

public $Proxy0(InvocationHandler h) {
this.h = h;
}

@Override
public void foo() {
h.invoke();
}
}
before
Target.foo    

多个抽象方法的接口

这样的实现依旧有问题,如果接口中提供了两个抽象方法呢?比如:

1
2
3
4
5
interface Foo {
void foo();

void bar();
}

此时无论是目标类,还是代理类都要重写这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class A13 {
interface Foo {
void foo();

void bar();
}

interface InvocationHandler {
void invoke();
}

static class Target implements Foo {
@Override
public void foo() {
System.out.println("Target.foo");
}

@Override
public void bar() {
System.out.println("Target.bar");
}
}

public static void main(String[] args) {
Foo proxy = new $Proxy0(new InvocationHandler() {
@Override
public void invoke() {
//1.功能增强
System.out.println("before");
//2.调用目标方法
new Target().foo();
}
});
proxy.foo();
proxy.bar();
}
}

代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class $Proxy0 implements Foo {

private InvocationHandler h;

public $Proxy0(InvocationHandler h) {
this.h = h;
}

@Override
public void foo() {
h.invoke();
}

@Override
public void bar() {
h.invoke();
}
}
before
Target.foo
before
Target.foo    

proxy.foo()proxy.bar()输出的都是Target.foo,因为实现 InvocationHandlerinvoke() 方法时,依旧只调用了目标类的 foo() 方法,而不是 bar()方法。

也就是说,在调用代理对象中的某个方法时,增强的应该是目标对象中对应的方法,希望在调用目标方法时能够动态编码。

那么可以在 invoke() 方法中添加两个入参,分别表示需要调用的目标方法和目标方法的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class A13 {
interface Foo {
void foo();

void bar();
}

interface InvocationHandler {
void invoke(Method method, Object... args) throws Throwable;
}

static class Target implements Foo {
@Override
public void foo() {
System.out.println("Target.foo");
}

@Override
public void bar() {
System.out.println("Target.bar");
}
}

public static void main(String[] args) {
Foo proxy = new $Proxy0(new InvocationHandler() {
@Override
public void invoke(Method method, Object... args) throws Throwable {
//1.功能增强
System.out.println("before");
//2.调用目标方法
//new Target().foo();
method.invoke(new Target(), args);
}
});
proxy.foo();
proxy.bar();
}
}

代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class $Proxy0 implements Foo {

private InvocationHandler h;

public $Proxy0(InvocationHandler h) {
this.h = h;
}

@Override
public void foo() {
try {
Method foo = Foo.class.getMethod("foo");
h.invoke(foo, new Object[0]);
} catch (Throwable e) {
e.printStackTrace();
}
}

@Override
public void bar() {
try {
Method bar = Foo.class.getMethod("bar");
h.invoke(bar, new Object[0]);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
before
Target.foo
before
Target.bar    

有返回值的抽象方法

优化还在继续,如果抽象方法有返回值呢?比如:

1
2
3
4
5
interface Foo {
void foo();

int bar();
}

实现了这个接口的目标类和代理类重写的方法都需要有具体的返回值:

1
2
3
4
5
6
7
8
9
10
11
12
static class Target implements Foo {
@Override
public void foo() {
System.out.println("target foo");
}

@Override
public int bar() {
System.out.println("target bar");
return 1;
}
}

目标类可以直接返回,那代理类返回什么?

InvocationHandlerinvoke() 方法是对"功能增强"和"调用目标"的抽象,因此可以使 invoke() 方法也返回一个值,返回的值即为目标方法的返回值,这样就可以使得代理类中的方法有值可返。

1
2
3
interface InvocationHandler {
Object invoke(Method method, Object[] params) throws Throwable;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class $Proxy0 implements Foo {

private final InvocationHandler h;

public $Proxy0(InvocationHandler h) {
this.h = h;
}

@Override
public void foo() {
try {
Method foo = Foo.class.getMethod("foo");
h.invoke(foo, new Object[0]);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

@Override
public int bar() {
try {
Method bar = Foo.class.getMethod("bar");
return (int) h.invoke(bar, new Object[0]);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
}

修改 main()方法,打印 bar() 方法的返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
$Proxy0 proxy = new $Proxy0(new InvocationHandler() {
@Override
public Object invoke(Method method, Object[] params) throws Throwable {
// 1. 功能增强
System.out.println("before...");
// 2. 调用目标
return method.invoke(new Target(), params);
}
});
proxy.foo();
// 调用另一个方法
System.out.println(proxy.bar());
}
before...
target foo
before...
target bar
1

在静态代码块里创建 Method 实例

每调用一次代理对象中的方法都会创建一个 Method 实例,这些实例是可以复用的,因此可以将这些实例的创建移动到静态代码块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class $Proxy0 implements Foo {

private InvocationHandler h;

static Method foo;

static Method bar;

static {
try {
foo = Foo.class.getMethod("foo");
bar = Foo.class.getMethod("bar");
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
}
}

public $Proxy0(InvocationHandler h) {
this.h = h;
}

@Override
public void foo() {
try {
h.invoke(foo, new Object[0]);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

@Override
public int bar() {
try {
Object result = h.invoke(bar, new Object[0]);
return (int) result;
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
}

同时对异常进行处理,运行时异常直接抛出,编译时异常转换为运行时异常抛出。

invoke()方法增加代理对象作为参数

在 JDK 提供的 InvocationHandler接口的 invoke() 方法还将代理对象作为方法的参数,以便用户根据实际情况使用。继续修改自定义的 InvocationHandler接口:

1
2
3
interface InvocationHandler {
Object invoke(Object proxy, Method method, Object[] params) throws Throwable;
}

修改代理类中对 invoke() 方法的调用,第一个参数为当前类的实例,即 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class $Proxy0 implements Foo {

// --snip--

@Override
public void foo() {
try {
h.invoke(this, foo, new Object[0]);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

@Override
public int bar() {
try {
return (int) h.invoke(this, bar, new Object[0]);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

// --snip--
}

main()方法重写的 invoke()方法也要增加 proxy参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
$Proxy0 proxy = new $Proxy0(new InvocationHandler() {
@Override
public Object invoke(Object proxy,Method method, Object[] params) throws Throwable {
// 1. 功能增强
System.out.println("before...");
// 2. 调用目标
return method.invoke(new Target(), params);
}
});
proxy.foo();
System.out.println(proxy.bar());
}

运行 main()方法,结果不发生变化。

对照 JDK

到目前为止,我们自定义的InvocationHandler接口,和 JDK 提供的接口基本无异,注释自定义的InvocationHandler接口,替换为 JDK 提供的。

在 JDK 提供的 InvocationHandler 接口的注释中有一句:See Also: Proxy,在 Proxy 类的代码中有:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Proxy implements java.io.Serializable {

// --snip--

protected InvocationHandler h;

protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}

// --snip--
}

Proxy 类中有一个 InvocationHandler 对象的成员变量。

因此还可以使代理类 $Proxy0继承 Proxy 来进一步优化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class $Proxy0 extends Proxy implements Foo {

// private InvocationHandler h;

static Method foo;

static Method bar;

static {
try {
foo = Foo.class.getMethod("foo");
bar = Foo.class.getMethod("bar");
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
}
}

public $Proxy0(InvocationHandler h) {
super(h);
}

@Override
public void foo() {
try {
h.invoke(this, foo, new Object[0]);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

@Override
public 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);
}
}
}

12.2 JDK 代理源码

JDK 动态代理生成的代理类是以字节码的形式存在的

利用 Arthas 反编译代理类字节码文件

要使用 Arthas 的反编译功能需要满足两个条件:

  1. 知道被反编译文件的全限定类名
  2. 程序不能中断,需要存在 Java 进程

为了满足这个条件,可以在控制台打印出生成的代理类的全限定类名,然后利用阻塞 IO 使程序不中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class JDKProxyDemo {
interface Foo {
void foo();
}

static class Target implements Foo {
@Override
public void foo() {
System.out.println("Target foo");
}
}


public static void main(String[] param) throws Exception {
//目标对象
Target target = new Target();

Foo proxy = (Foo) Proxy.newProxyInstance(JDKProxyDemo.class.getClassLoader(), //加载在运行期间动态生成的字节码文件
new Class[]{Foo.class},
(p, method, args) -> {
System.out.println("before");
Object result = method.invoke(target, args);//让代理返回目标方法执行的结果
System.out.println("after");
return result;
});
//打印全限定类名
System.out.println(proxy.getClass());
proxy.foo();
//阻塞进程
System.in.read();
}
}
class com.itheima.a12.$Proxy0
before
Target foo
after    

运行 main()方法,在当前项目目录下生成 com.itheima.a12.$Proxy0.class 文件,查看其内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.itheima.a12;

import com.itheima.a12.JDKProxyDemo;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

final class $Proxy0
extends Proxy
implements JDKProxyDemo.Foo {
private static final Method m0;
private static final Method m1;
private static final Method m2;
private static final Method m3;

private static MethodHandles.Lookup proxyClassLookup(MethodHandles.Lookup lookup) throws IllegalAccessException {
if (lookup.lookupClass() == Proxy.class && lookup.hasFullPrivilegeAccess()) {
return MethodHandles.lookup();
}
throw new IllegalAccessException(lookup.toString());
}

public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}

static {
try {
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("com.atguigu.a12.JDKProxyDemo$Foo").getMethod("foo", new Class[0]);
return;
}
catch (NoSuchMethodException noSuchMethodException) {
throw new NoSuchMethodError(noSuchMethodException.getMessage());
}
catch (ClassNotFoundException classNotFoundException) {
throw new NoClassDefFoundError(classNotFoundException.getMessage());
}
}

public final boolean equals(Object object) {
try {
return (Boolean)this.h.invoke(this, m1, new Object[]{object});
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}

public final String toString() {
try {
return (String)this.h.invoke(this, m2, null);
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}

public final int hashCode() {
try {
return (Integer)this.h.invoke(this, m0, null);
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}

public final void foo() {
try {
this.h.invoke(this, m3, null);
return;
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
}

其内容与自定义的 $Proxy0 几乎无异,只不过 JDK 生成的代理类信息还生成 equals()toString()hashCode() 三个方法对应的 Method对象,并对它们也进行了相同的增强。

12.3 JDK 代理类字节码生成

JDK 在生成代理类时,没有经历源码、编译阶段,而是直接采用字节码,使用了 ASM 来完成。

ASM 的学习成本较高,在此不做过多介绍,本节将采用一直“曲线求国”的方式,使用 IDEA 的 Byte Code Analyzer插件将 Java 源码转换成使用 ASM 编写的代码,建议在 Java8 环境下使用。

编写接口和代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public interface Foo {
void foo();
}


public class $Proxy0 extends Proxy implements Foo {
private static Method method;

static {
try {
method = Foo.class.getMethod("foo");
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
}
}

public $Proxy0(InvocationHandler h) {
super(h);
}

@Override
public void foo() {
try {
this.h.invoke(this, method, null);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
}

编译后,右键 Analyze Byte Code,查看 ASM,拷贝其内容,复制到$Proxy0Dump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package com.itheima;

import org.springframework.asm.*;


public class $Proxy0Dump implements Opcodes {

public static byte[] dump() throws Exception {

ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
RecordComponentVisitor recordComponentVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;

classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/itheima/$Proxy0", null, "java/lang/reflect/Proxy", new String[]{"com/itheima/Foo"});

classWriter.visitSource("$Proxy0.java", null);

{
fieldVisitor = classWriter.visitField(ACC_PRIVATE | ACC_STATIC, "method", "Ljava/lang/reflect/Method;", null, null);
fieldVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/reflect/InvocationHandler;)V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(25, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitVarInsn(ALOAD, 1);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V", false);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(26, label1);
methodVisitor.visitInsn(RETURN);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLocalVariable("this", "Lcom/itheima/$Proxy0;", null, label0, label2, 0);
methodVisitor.visitLocalVariable("h", "Ljava/lang/reflect/InvocationHandler;", null, label0, label2, 1);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "foo", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
Label label1 = new Label();
Label label2 = new Label();
methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/Throwable");
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(31, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitFieldInsn(GETFIELD, "com/itheima/$Proxy0", "h", "Ljava/lang/reflect/InvocationHandler;");
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitFieldInsn(GETSTATIC, "com/itheima/$Proxy0", "method", "Ljava/lang/reflect/Method;");
methodVisitor.visitInsn(ACONST_NULL);
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/lang/reflect/InvocationHandler", "invoke", "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;", true);
methodVisitor.visitInsn(POP);
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(34, label1);
Label label3 = new Label();
methodVisitor.visitJumpInsn(GOTO, label3);
methodVisitor.visitLabel(label2);
methodVisitor.visitLineNumber(32, label2);
methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Throwable"});
methodVisitor.visitVarInsn(ASTORE, 1);
Label label4 = new Label();
methodVisitor.visitLabel(label4);
methodVisitor.visitLineNumber(33, label4);
methodVisitor.visitTypeInsn(NEW, "java/lang/reflect/UndeclaredThrowableException");
methodVisitor.visitInsn(DUP);
methodVisitor.visitVarInsn(ALOAD, 1);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/UndeclaredThrowableException", "<init>", "(Ljava/lang/Throwable;)V", false);
methodVisitor.visitInsn(ATHROW);
methodVisitor.visitLabel(label3);
methodVisitor.visitLineNumber(35, label3);
methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
methodVisitor.visitInsn(RETURN);
Label label5 = new Label();
methodVisitor.visitLabel(label5);
methodVisitor.visitLocalVariable("e", "Ljava/lang/Throwable;", null, label4, label3, 1);
methodVisitor.visitLocalVariable("this", "Lcom/itheima/$Proxy0;", null, label0, label5, 0);
methodVisitor.visitMaxs(4, 2);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
Label label1 = new Label();
Label label2 = new Label();
methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/NoSuchMethodException");
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(18, label0);
methodVisitor.visitLdcInsn(Type.getType("Lcom/itheima/Foo;"));
methodVisitor.visitLdcInsn("foo");
methodVisitor.visitInsn(ICONST_0);
methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/Class");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;", false);
methodVisitor.visitFieldInsn(PUTSTATIC, "com/itheima/$Proxy0", "method", "Ljava/lang/reflect/Method;");
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(21, label1);
Label label3 = new Label();
methodVisitor.visitJumpInsn(GOTO, label3);
methodVisitor.visitLabel(label2);
methodVisitor.visitLineNumber(19, label2);
methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/NoSuchMethodException"});
methodVisitor.visitVarInsn(ASTORE, 0);
Label label4 = new Label();
methodVisitor.visitLabel(label4);
methodVisitor.visitLineNumber(20, label4);
methodVisitor.visitTypeInsn(NEW, "java/lang/NoSuchMethodError");
methodVisitor.visitInsn(DUP);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/NoSuchMethodException", "getMessage", "()Ljava/lang/String;", false);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/NoSuchMethodError", "<init>", "(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(ATHROW);
methodVisitor.visitLabel(label3);
methodVisitor.visitLineNumber(22, label3);
methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitLocalVariable("e", "Ljava/lang/NoSuchMethodException;", null, label4, label3, 0);
methodVisitor.visitMaxs(3, 1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();

return classWriter.toByteArray();
}
}

编写测试方法使用 $Proxy0Dump 生成 $Proxy0 的 class 文件:

1
2
3
4
5
6
7
8
9
public class TestProxy {
public static void main(String[] args) throws Exception {
byte[] dump = $Proxy0Dump.dump();

FileOutputStream os = new FileOutputStream("$Proxy0.class");
os.write(dump, 0, dump.length);
os.close();
}
}

反编译后的内容与手动编写的 $Proxy0.java 文件的内容无异。

实际使用时并不需要使用 $Proxy0Dump 生成 $Proxy.class 文件,而是利用 ClassLoader 直接加载类信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TestProxy {
public static void main(String[] args) throws Exception {
byte[] dump = $Proxy0Dump.dump();
// FileOutputStream outputStream = new FileOutputStream("$Proxy0.class");
// outputStream.write(dump);
// outputStream.close();
ClassLoader classLoader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return super.defineClass(name, dump, 0, dump.length);
}
};
Class<?> proxyClass = classLoader.loadClass("com.itheima.$Proxy0");
Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
Foo proxy = (Foo) constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
System.out.println("调用目标");
System.out.println("after");
return null;
}
});

proxy.foo();
}
}
before
调用目标
after    

12.4 JDK 反射优化

使用 JDK 的动态代理时,会使用反射调用方法:

1
Object result = method.invoke(target, params);

相比于正常调用方法,利用反射的性能要稍微低一些,JDK 对反射进行了优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TestMethodProxy {

public static void main(String[] args) throws Exception {
Method foo = TestMethodProxy.class.getMethod("foo", int.class);
for (int i = 1; i <= 17; i++) {
show(i, foo);
foo.invoke(null, i);
}
System.in.read();
}

// 方法反射调用时,底层使用了 MethodAccessor 的实现类
private static void show(int i, Method foo) throws Exception {
Method getMethodAccessor = Method.class.getDeclaredMethod("getMethodAccessor");
getMethodAccessor.setAccessible(true);
Object invoke = getMethodAccessor.invoke(foo);
if (invoke == null) {
System.out.println(i + ":" + null);
return;
}
// DelegatingMethodAccessorImpl 的全限定类名(不同版本的 JDK 存在差异)
Field delegate = Class.forName("sun.reflect.DelegatingMethodAccessorImpl").getDeclaredField("delegate");
delegate.setAccessible(true);
System.out.println(i + ": " + delegate.get(invoke));
}

public static void foo(int i) {
System.out.println(i + ": foo");
}
}
1: null
1: foo
2: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
2: foo
3: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
3: foo
4: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
4: foo
5: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
5: foo
6: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
6: foo
7: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
7: foo
8: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
8: foo
9: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
9: foo
10: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
10: foo
11: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
11: foo
12: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
12: foo
13: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
13: foo
14: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
14: foo
15: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
15: foo
16: sun.reflect.NativeMethodAccessorImpl@1be6f5c3
16: foo
17: sun.reflect.GeneratedMethodAccessor2@5b2133b1
17: foo    

从上述信息可知,第一次调用时没有使用 MethodAccessor 对象,从第二次到第十六次,使用了 NativeMethodAccessorImpl 对象,而在第十七次使用了 GeneratedMethodAccessor2 对象。

NativeMethodAccessorImpl 基于 Java 本地 API 实现,性能较低,第十七次调用换成 GeneratedMethodAccessor2 后,性能得到一定的提升。

使用 Arthas 反编译查看 GeneratedMethodAccessor2 类中的信息,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class GeneratedMethodAccessor2 extends MethodAccessorImpl {
/*
* Loose catch block
*/
public Object invoke(Object object, Object[] objectArray) throws InvocationTargetException {
// --snip--
try {
// 正常调用方法
TestMethodProxy.foo((int)c);
return null;
}
catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}
catch (ClassCastException | NullPointerException runtimeException) {
throw new IllegalArgumentException(super.toString());
}
}
}

反编译得到的代码中,不再是通过反射调用方法,而是直接正常调用方法,即:

1
TestMethodProxy.foo((int)c);

性能得到了提升,但这样的提升也是有一定代价的:为优化 一个 方法的反射调用,生成了一个 GeneratedMethodAccessor2 代理类。

13. CGLib 动态代理原理

13.1 CGLib 动态代理的模拟

目标类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Target {
public void save() {
System.out.println("save()");
}

public void save(int i) {
System.out.println("save(int)");
}

public void save(long i) {
System.out.println("save(long)");
}
}

CGLib 动态代理生成的代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Proxy extends Target {

private MethodInterceptor methodInterceptor;

public void setMethodInterceptor(MethodInterceptor methodInterceptor) {
this.methodInterceptor = methodInterceptor;
}

static Method save0;
static Method save1;
static Method save2;

static {
try {
save0 = Target.class.getMethod("save");
save1 = Target.class.getMethod("save", int.class);
save2 = Target.class.getMethod("save", long.class);
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
}
}

@Override
public void save() {
try {
methodInterceptor.intercept(this, save0, new Object[0], null);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

@Override
public void save(int i) {
try {
methodInterceptor.intercept(this, save1, new Object[]{i}, null);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

@Override
public void save(long i) {
try {
methodInterceptor.intercept(this, save2, new Object[]{i}, null);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
Target target = new Target();

Proxy proxy = new Proxy();
proxy.setMethodInterceptor(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("before");
// 反射调用
return method.invoke(target, args);
}
});

proxy.save();
proxy.save(1);
proxy.save(2L);
}
before
save()
before
save(int)
before
save(long)    

13.2 MethodProxy

在上述 Proxy 类中,重写了父类中的方法,并在重写的方法中调用了 intercept()方法,重写的这些方法相当于是带增强功能的方法。

在 JDK 的动态代理中,使用反射对方法进行调用,而在 CGLib 动态代理中,可以使用 intercept() 方法中 MethodProxy 类型的参数实现不经过反射来调用方法。

接收的 MethodProxy 类型的参数可以像 Method 类型的参数一样,在静态代码块中被实例化。

可以通过静态方法 MethodProxy.create() 来创建 MethodProxy 对象:

1
2
3
4
5
6
7
public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
MethodProxy proxy = new MethodProxy();
proxy.sig1 = new Signature(name1, desc);
proxy.sig2 = new Signature(name2, desc);
proxy.createInfo = new CreateInfo(c1, c2);
return proxy;
}
  • 参数 c1指目标类(或者说原始类)的 Class对象;
  • 参数 c2指代理类的 Class对象;
  • 参数 desc指方法描述符;
  • 参数 name1指带 增强 功能的方法名称;
  • 参数 name2指带 原始 功能的方法名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class Proxy extends Target {

private MethodInterceptor methodInterceptor;

public void setMethodInterceptor(MethodInterceptor methodInterceptor) {
this.methodInterceptor = methodInterceptor;
}

static Method save0;
static Method save1;
static Method save2;
static MethodProxy save0Proxy;
static MethodProxy save1Proxy;
static MethodProxy save2Proxy;

static {
try {
save0 = Target.class.getMethod("save");
save1 = Target.class.getMethod("save", int.class);
save2 = Target.class.getMethod("save", long.class);

save0Proxy = MethodProxy.create(Target.class, Proxy.class, "()V", "save", "saveSuper");
save1Proxy = MethodProxy.create(Target.class, Proxy.class, "(I)V", "save", "saveSuper");
save2Proxy = MethodProxy.create(Target.class, Proxy.class, "(J)V", "save", "saveSuper");
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
}
}

// >>>>>>>>>>>>>>>>>>>>>>>> 带原始功能的方法
public void saveSuper() {
super.save();
}

public void saveSuper(int i) {
super.save(i);
}

public void saveSuper(long i) {
super.save(i);
}


// >>>>>>>>>>>>>>>>>>>>>>>> 带增强功能的方法
@Override
public void save() {
try {
methodInterceptor.intercept(this, save0, new Object[0], save0Proxy);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

@Override
public void save(int i) {
try {
methodInterceptor.intercept(this, save1, new Object[]{i}, save1Proxy);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

@Override
public void save(long i) {
try {
methodInterceptor.intercept(this, save2, new Object[]{i}, save2Proxy);
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestProxy {
public static void main(String[] args) {
Proxy proxy = new Proxy();
proxy.setMethodInterceptor(new MethodInterceptor() {
@Override
public Object intercept(Object p, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("before");
// return method.invoke(new Target(), args);//反射调用
// return methodProxy.invoke(new Target(), args);//内部无反射,结合目标用
return methodProxy.invokeSuper(p, args);//内部无反射,结合代理用
}
});
proxy.save();
proxy.save(1);
proxy.save(2L);
}
}

14. MethodProxy 原理

调用 methodProxy.invoke() 方法时,会额外创建一个代理类,该代理类配合目标对象使用。

调用 methodProxy.invokeSuper() 方法时,也会额外创建一个代理类,该代理类配合代理对象使用。

当调用 MethodProxy 对象的 invoke() 方法或 invokeSuper() 方法时,就会生成这两个代理类,它们都继承至 FastClass

FastClass 是一个抽象类,其内部有多个抽象方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class FastClass {
public abstract int getIndex(String var1, Class[] var2);

public abstract int getIndex(Class[] var1);

public abstract Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException;

public abstract Object newInstance(int var1, Object[] var2) throws InvocationTargetException;

public abstract int getIndex(Signature signature);

public abstract int getMaxIndex();
}

重点关注 invoke() 方法与 getIndex(Signature signature) 方法。

模拟生成的与目标类相关的代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class TargetFastClass {

static Signature s0 = new Signature("save", "()V");
static Signature s1 = new Signature("save", "(I)V");
static Signature s2 = new Signature("save", "(J)V");

/**
* 获取目标方法的编号
* Target 目标类中的方法:
* save() 0
* save(int) 1
* save(long) 2
*
* @param signature 包含方法名称、参数返回值
* @return 方法编号
*/
public int getIndex(Signature signature) {
if (s0.equals(signature)) {
return 0;
} else if (s1.equals(signature)) {
return 1;
} else if (s2.equals(signature)) {
return 2;
}
return -1;
}

/**
* 根据 getIndex() 方法返回的方法编号正常调用目标对象方法
*
* @param index 方法编号
* @param target 目标对象
* @param args 调用目标对象方法需要的参数
* @return 方法返回结果
*/
public Object invoke(int index, Object target, Object[] args) {
if (index == 0) {
((Target) target).save();
return null;
} else if (index == 1) {
((Target) target).save((int) args[0]);
return null;
} else if (index == 2) {
((Target) target).save((long) args[0]);
return null;
} else {
throw new RuntimeException("无此异常");
}
}

public static void main(String[] args) {
TargetFastClass fastClass = new TargetFastClass();
int index = fastClass.getIndex(new Signature("save", "()V"));
fastClass.invoke(index, new Target(), new Object[0]);

index = fastClass.getIndex(new Signature("save", "(J)V"));
fastClass.invoke(index, new Target(), new Object[]{2L});
}
}
save()
save(long)    

模拟生成的与代理类相关的代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ProxyFastClass {
static Signature s0 = new Signature("saveSuper", "()V");
static Signature s1 = new Signature("saveSuper", "(I)V");
static Signature s2 = new Signature("saveSuper", "(J)V");

/**
* 获取代理方法的编号
* Proxy 代理类中的方法:
* saveSuper() 0
* saveSuper(int) 1
* saveSuper(long) 2
*
* @param signature 包含方法名称、参数返回值
* @return 方法编号
*/
public int getIndex(Signature signature) {
if (s0.equals(signature)) {
return 0;
} else if (s1.equals(signature)) {
return 1;
} else if (s2.equals(signature)) {
return 2;
}
return -1;
}

/**
* 根据 getIndex() 方法返回的方法编号正常调用代理对象中带原始功能的方法
*
* @param index 方法编号
* @param proxy 代理对象
* @param args 调用方法需要的参数
* @return 方法返回结果
*/
public Object invoke(int index, Object proxy, Object[] args) {
if (index == 0) {
((Proxy) proxy).save();
return null;
} else if (index == 1) {
((Proxy) proxy).save((int) args[0]);
return null;
} else if (index == 2) {
((Proxy) proxy).save((long) args[0]);
return null;
} else {
throw new RuntimeException("无此异常");
}
}

public static void main(String[] args) {
ProxyFastClass proxyFastClass = new ProxyFastClass();
int index = proxyFastClass.getIndex(new Signature("saveSuper", "()V"));
proxyFastClass.invoke(index, new Proxy(), new Object[0]);
}
}
save()  

总结

  • 调用 MethodProxy.create() 方法创建 MethodProxy 对象时,要求传递带增强功能的方法名、带原始功能的方法名以及方法描述符。
  • 根据方法名和方法描述符可以在调用生成的两个代理类中的 getIndex()方法时获取方法的编号,之后:
  • 调用 methodProxy.invoke() 方法时,就相当于调用 TargetFastClass 中的 invoke() 方法,并在这个 invoke() 方法中正常调用目标对象方法(Spring 底层的选择)。
  • 调用 methodProxy.invokeSuper() 方法时,就相当于调用 ProxyFastClass 中的 invoke() 方法,并在这个 invoke() 方法中正常调用代理对象中带原始功能的方法。

与 JDK 中优化反射调用方法的对比

  • 在 JDK 中需要反射调用 16 次方法后才会生成优化反射调用的代理类,而在 CGLib 中,当调用 MethodProxy.create() 方法时就会生成优化反射调用的代理类;
  • 在 JDK 中一个方法的反射调用优化就要生成一个代理类,而在 CGLib 中,一个代理类生成两个 FastClass代理类,每个FastClass可以匹配到多个方法。

15. JDK 和 CGLib 的统一

15.1 advisor

切面有 aspectadvisor 两个概念,aspect 是多组通知(advice)和切点(pointcut)的组合,也是实际编码时使用的,advisor 则是更细粒度的切面,仅包含一个通知和切点,aspect 在生效之前会被拆解成多个 advisor

Spring 中对切点、通知、切面的抽象如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

image-20250207161716147

  • 切点:即 Pointcut,其典型实现是 AspectJExpressionPointcut

  • 通知:即 Advice,其典型子类接口为 MethodInterceptor,表示环绕通知

  • 切面:即 Advisor,仅包含一个切点和通知

本节将重点介绍 advisor 切面。

15.2 切面与代理对象的创建

在 Spring 中,切点通过接口 org.springframework.aop.Pointcut 来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface Pointcut {

/**
* 根据类型过滤
*/
ClassFilter getClassFilter();

/**
* 根据方法匹配
*/
MethodMatcher getMethodMatcher();


/**
* Canonical Pointcut instance that always matches.
*/
Pointcut TRUE = TruePointcut.INSTANCE;

}

Pointcut 接口有很多实现类,比如:

  • AnnotationMatchingPointcut:通过注解进行匹配
  • AspectJExpressionPointcut:通过 AspectJ 表达式进行匹配(本节的选择)

在 Spring 中,通知的表示也有很多接口,在此介绍最基本、最重要的接口 org.aopalliance.intercept.MethodInterceptor,这个接口实现的通知属于环绕通知。

在 Spring 中,切面的实现也有很多,在此选择 DefaultPointcutAdvisor,创建这种切面时,传递一个切点和通知。

最后创建代理对象时,无需显式实现 JDK 动态代理或 CGLib 动态代理,Spring 提供了名为 ProxyFactory的工厂,其内部通过不同的情况选择不同的代理实现,更方便地创建代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
interface I1 {
void foo();

void bar();
}

static class Target1 implements I1 {

@Override
public void foo() {
System.out.println("target1 foo");
}

@Override
public void bar() {
System.out.println("target1 bar");
}
}

static class Target2 {
public void foo() {
System.out.println("target2 foo");
}

public void bar() {
System.out.println("target2 bar");
}
}

public static void main(String[] args) {
/*
* 两个切面概念:
* aspect =
* 通知 1 (advice) + 切点 1(pointcut)
* 通知 2 (advice) + 切点 2(pointcut)
* 通知 3 (advice) + 切点 3(pointcut)
* ...
*
* advisor = 更细粒度的切面,包含一个通知和切点
* */

// 1. 备好切点(根据 AspectJ 表达式进行匹配)
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* foo())");
// 2. 备好通知
MethodInterceptor advice = invocation -> {
System.out.println("before...");
Object result = invocation.proceed();
System.out.println("after...");
return result;
};
// 3. 备好切面
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
// 4. 创建代理
Target1 target = new Target1();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.addAdvisor(advisor);

I1 proxy = (I1) factory.getProxy();
System.out.println(proxy.getClass());
proxy.foo();
proxy.bar();
}
class com.itheima.a15.A15$Target1$$EnhancerBySpringCGLIB$$933570ca
before...
target1 foo
after...
target1 bar   

foo() 方法被增强,但 bar() 并没有,并且选择了 CGLib 动态代理作为代理的实现。

Spring 是根据什么信息来选择不同的动态代理实现呢?

ProxyFactory 的父类 ProxyConfig 中有个名为 proxyTargetClass 的布尔类型成员变量:

  • proxyTargetClass == false,并且目标对象所在类实现了接口时,将选择 JDK 动态代理;
  • proxyTargetClass == false,但目标对象所在类未实现接口时,将选择 CGLib 动态代理;
  • proxyTargetClass == true,总是选择 CGLib 动态代理。

上文中的 target对象的所在类 Targer1 实现了 I1 接口,最终为什么依旧选择了 CGLib 动态代理作为代理类的创建方式呢?

这是因为并没有显式这是 target 对象的实现类,Spring 认为其并未实现接口。

设置 factory 对象的 interfaces 信息:

1
factory.setInterfaces(target.getClass().getInterfaces());
class com.itheima.a15.$Proxy0
before
target1 foo
after
target1 bar    

此时选择的动态代理实现方式是 JDK 动态代理。

再设置 factory 对象的 proxyTargetClasstrue

1
factory.setProxyTargetClass(true);
class com.itheima.a15.A15$Target1$$EnhancerBySpringCGLIB$$933570ca
before...
target1 foo
after...
target1 bar    

此时选择的动态代理实现方式是 CGLib 动态代理。

再将 proxyTargetClass 的值修改回 false,并修改目标对象的所在类为 Target2Target2并未实现任何接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
// --snip--

// 4. 创建代理
Target2 target = new Target2();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.addAdvisor(advisor);
factory.setInterfaces(target.getClass().getInterfaces());
factory.setProxyTargetClass(false);

Target2 proxy = (Target2) factory.getProxy();
System.out.println(proxy.getClass());
proxy.foo();
proxy.bar();
}
class com.itheima.a15.A15$Target1$$EnhancerBySpringCGLIB$$933570ca
before...
target1 foo
after...
target1 bar    

此时选择的动态代理实现方式是 CGLib 动态代理。

ProxyFactory 是用来创建代理的核心实现,使用 AopProxyFactory 选择具体的代理实现:

  • JdkDynamicAopProxy
  • ObjenesisCglibAopProxy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

image-20250207162052604

AopProxyFactory 根据 proxyTargetClass 等设置选择 AopProxy 实现,AopProxy 通过 getProxy() 方法创建代理对象。

上述类图中的类与接口都实现了 Advised 接口,能够获得关联的切面集合与目标(实际上是从 ProxyFactory 中获取的)。

调用代理方法时,会借助 ProxyFactory 统一将通知转换为环绕通知 MethodInterceptor

16. 切点匹配

上一节中,选择 AspectJExpressionPointcut作为切点的实现,判断编写的 AspectJ 表达式是否与某一方法匹配可以使用其 matches() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws NoSuchMethodException {
AspectJExpressionPointcut pt1 = new AspectJExpressionPointcut();
pt1.setExpression("execution(* bar())");
System.out.println(pt1.matches(T1.class.getMethod("foo"), T1.class));
System.out.println(pt1.matches(T1.class.getMethod("bar"), T1.class));

AspectJExpressionPointcut pt2 = new AspectJExpressionPointcut();
pt2.setExpression("@annotation(org.springframework.transaction.annotation.Transactional)");
System.out.println(pt2.matches(T1.class.getMethod("foo"), T1.class));
System.out.println(pt2.matches(T1.class.getMethod("bar"), T1.class));
}

static class T1 {
@Transactional
public void foo() {

}

public void bar() {

}
}
false
true
true
false    

@Transactional 是 Spring 中使用频率非常高的注解,那它底层是通过 AspectJExpressionPointcut@annotation() 切点表达式相结合对目标方法进行匹配的吗?

答案是否定的。@Transactional 注解除了可以作用在方法上,还可以作用在类(或接口)上。

在底层 @Transactional注解的匹配使用到了 StaticMethodMatcherPointcut,在此模拟一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public static void main(String[] args) throws NoSuchMethodException {

StaticMethodMatcherPointcut pt3 = new StaticMethodMatcherPointcut() {
@Override
public boolean matches(Method method, Class<?> targetClass) {
// 检查方法上是否添加了 @Transactional 注解
MergedAnnotations annotations = MergedAnnotations.from(method);
if (annotations.isPresent(Transactional.class)) {
return true;
}
// 检查类上或所实现的接口是否添加了 @Transactional 注解
annotations = MergedAnnotations.from(targetClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
return annotations.isPresent(Transactional.class);
}
};

System.out.println(pt3.matches(T1.class.getMethod("foo"), T1.class));
System.out.println(pt3.matches(T1.class.getMethod("bar"), T1.class));
System.out.println(pt3.matches(T2.class.getMethod("foo"), T2.class));
System.out.println(pt3.matches(T3.class.getMethod("foo"), T3.class));
}

static class T1 {
@Transactional
public void foo() {

}

public void bar() {

}
}

@Transactional
static class T2 {
public void foo() {

}
}

@Transactional
interface I3 {
void foo();
}

static class T3 implements I3 {
@Override
public void foo() {

}
}
true
false
true
true    

无论是 AspectJExpressionPointcut 还是 StaticMethodMatcherPointcut,它们都实现了 MethodMatcher 接口,用来执行方法的匹配。

17. 从@Aspect 到 Adivisor

17.1 AnnotationAwareAspectJAutoProxyCreator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static class Target1 {
public void foo() {
System.out.println("target1 foo");
}
}

static class Target2 {
public void bar() {
System.out.println("target2 bar");
}
}

/**
* 高级切面
*/
@Aspect
static class Aspect1 {
@Before("execution(* foo())")
public void before() {
System.out.println("aspect1 before...");
}

@After("execution(* foo())")
public void after() {
System.out.println("aspect1 after...");
}
}

@Configuration
static class Config {
/**
* 低级切面,由一个切点和一个通知组成
*/
@Bean
public Advisor advisor3(MethodInterceptor advice3) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* foo())");
return new DefaultPointcutAdvisor(pointcut, advice3);
}

@Bean
public MethodInterceptor advices() {
return invocation -> {
System.out.println("advice3 before...");
Object result = invocation.proceed();
System.out.println("advice3 after...");
return result;
};
}
}

编写 main()方法创建 Spring 容器,并添加必要的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("aspect1", Aspect1.class);
context.registerBean("config", Config.class);
context.registerBean(ConfigurationClassPostProcessor.class);

context.refresh();
for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}
context.close();
}
aspect1
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
advisor3
advices    

Spring 中存在一个名为 AnnotationAwareAspectJAutoProxyCreator 的 Bean 后置处理器,尽管它的名称中没有 BeanPostProcessor 的字样,但它确实是实现了 BeanPostProcessor 接口的。

AnnotationAwareAspectJAutoProxyCreator 有两个主要作用:

  1. 找到容器中所有的切面,针对高级切面,将其转换为低级切面;
  2. 根据切面信息,利用 ProxyFactory 创建代理对象。

AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor,可以在 Bean 生命周期中的一些阶段对 Bean 进行拓展。AnnotationAwareAspectJAutoProxyCreator 可以在 Bean 进行 依赖注入之前Bean 初始化之后 对 Bean 进行拓展。

重点介绍 AnnotationAwareAspectJAutoProxyCreator 中的两个方法:

  • findEligibleAdvisors():位于父类 AbstractAdvisorAutoProxyCreator 中,从容器中寻找相匹配的切面。低级切面直接添加,高级切面转换为低级切面再添加。
  • wrapIfNecessary():位于父类 AbstractAutoProxyCreator 中,用于将有资格被代理的 Bean 进行包装,即创建代理对象。

findEligibleAdvisors() 方法

findEligibleAdvisors() 方法接收两个参数:

  • beanClass:配合切面使用的目标类 Class 信息
  • beanName:当前被代理的 Bean 的名称

修改 main() 方法,向容器中添加 AnnotationAwareAspectJAutoProxyCreator 后置处理器,测试 findEligibleAdvisors() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("aspect1", Aspect1.class);
context.registerBean("config", Config.class);
context.registerBean(ConfigurationClassPostProcessor.class);

context.registerBean(AnnotationAwareAspectJAutoProxyCreator.class);

context.refresh();

// 测试 findEligibleAdvisors 方法
AnnotationAwareAspectJAutoProxyCreator creator = context.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
// 获取能够配合 Target1 使用的切面
List<Advisor> advisors = creator.findEligibleAdvisors(Target1.class, "target1");
advisors.forEach(System.out::println);

context.close();
}
org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.framework.autoproxy.A17$Config$$Lambda$105/0x000001bf4b0fe948@626abbd0]
InstantiationModelAwarePointcutAdvisor: expression [execution(* foo())]; advice method [public void org.springframework.aop.framework.autoproxy.A17$Aspect1.before()]; perClauseKind=SINGLETON
InstantiationModelAwarePointcutAdvisor: expression [execution(* foo())]; advice method [public void org.springframework.aop.framework.autoproxy.A17$Aspect1.after()]; perClauseKind=SINGLETON   

打印出 4 个能配合 Target1 使用的切面信息,其中:

  • 第一个切面 ExposeInvocationInterceptor.ADVISOR 是 Spring 为每个代理对象都会添加的切面;
  • 第二个切面 DefaultPointcutAdvisor 是自行编写的低级切面;
  • 第三个和第四个切面 InstantiationModelAwarePointcutAdvisor 是由高级切面转换得到的两个低级切面。

若按照 creator.findEligibleAdvisors(Target2.class, "target2") 的方式进行调用,控制台不会打印出任何信息,因为没有任何切面能够配合 Target2 使用。

wrapIfNecessary() 方法

wrapIfNecessary() 方法内部调用了 findEligibleAdvisors() 方法,若 findEligibleAdvisors() 方法返回的集合不为空,则表示需要创建代理对象。

如果需要创建对象,wrapIfNecessary()方法返回的是代理对象,否则仍然是原对象。

wrapIfNecessary()方法接收三个参数:

  • bean:原始 Bean 实例
  • beanName:Bean 的名称
  • cacheKey:用于元数据访问的缓存 key
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// --snip--

Object o1 = creator.wrapIfNecessary(new Target1(), "target1", "target1");
System.out.println(o1.getClass());
Object o2 = creator.wrapIfNecessary(new Target2(), "target2", "target2");
System.out.println(o2.getClass());

context.close();
}
class org.springframework.aop.framework.autoproxy.A17$Target1$$EnhancerBySpringCGLIB$$634976f6
class org.springframework.aop.framework.autoproxy.A17$Target2    

Target1 对象是被代理的,而 Target2 依旧是原对象。

如果将 o1 转换为 Target1,并调用 foo() 方法,foo() 方法将被增强:

1
2
3
4
5
6
7
public static void main(String[] args) {
// --snip--

((Target1) o1).foo();

context.close();
}
advice3 before...
aspect1 before...
target1 foo
aspect1 after...
advice3 after...    

切面的顺序控制

根据上述打印的信息可知,低级切面相比于高级切面先一步被执行,这个执行顺序是可以被控制的。

针对高级切面来说,可以在类上使用 @Order 注解,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Aspect
@Order(1)
static class Aspect1 {
@Before("execution(* foo())")
public void before() {
System.out.println("aspect1 before...");
}

@After("execution(* foo())")
public void after() {
System.out.println("aspect1 after...");
}
}

在高级切面中,@Order 只有放在类上才生效,放在方法上不会生效。比如高级切面中有多个前置通知,这些前置通知对应的方法上使用 @Order 注解是无法生效的。

针对低级切面,需要设置 advisororder 值,而不是向高级切面那样使用 @Order 注解,使用 @Order 注解设置在 advisor3() 方法上是无用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
static class Config {
/**
* 低级切面,由一个切点和一个通知组成
*/
@Bean
public Advisor advisor3(MethodInterceptor advice3) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* foo())");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice3);
// 设置切面执行顺序
advisor.setOrder(2);
return advisor;
}

// --snip--
}

设置完成后,高级切面的执行优先级高于低级切面。执行 main() 方法验证执行顺序是否改变:

aspect1 before...
advice3 before...
target1 foo
advice3 after...
aspect1 after...    

17.2 代理对象创建时机

使用 AnnotationAwareAspectJAutoProxyCreator Bean 后置处理器创建代理对象的时机有以下两个选择:

  • Bean 的依赖注入之前
  • Bean 初始化完成之后

这两个时机二选一,不会重复创建代理对象。

以下述代码为例,查看代理对象的创建时机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package org.springframework.aop.framework.autoproxy;


public class A17_1 {
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(ConfigurationClassPostProcessor.class);
context.registerBean(Config.class);
context.refresh();
context.close();
// 创建 -> (*) 依赖注入 -> 初始化 (*)

}

@Configuration
static class Config {
/**
* 解析 @AspectJ 注解,产生代理
*/
@Bean
public AnnotationAwareAspectJAutoProxyCreator annotationAwareAspectJAutoProxyCreator() {
return new AnnotationAwareAspectJAutoProxyCreator();
}

/**
* 解析 @Autowired
*/
@Bean
public AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor() {
return new AutowiredAnnotationBeanPostProcessor();
}

/**
* 解析 @PostConstruct
*/
@Bean
public CommonAnnotationBeanPostProcessor commonAnnotationBeanPostProcessor() {
return new CommonAnnotationBeanPostProcessor();
}

@Bean
public Advisor advisor(MethodInterceptor advice) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* foo())");
return new DefaultPointcutAdvisor(pointcut, advice);
}

@Bean
public MethodInterceptor advice() {
return invocation -> {
System.out.println("before...");
return invocation.proceed();
};
}

@Bean
public Bean1 bean1() {
return new Bean1();
}

@Bean
public Bean2 bean2() {
return new Bean2();
}
}

static class Bean1 {
public void foo() {}
public Bean1() {
System.out.println("Bean1()");
}
@PostConstruct
public void init() {
System.out.println("Bean1 init()");
}
}

static class Bean2 {
public Bean2() {
System.out.println("Bean2()");
}
@Autowired
public void setBean1(Bean1 bean1) {
System.out.println("Bean2 setBean1(bean1) class is: " + bean1.getClass());
}
@PostConstruct
public void init() {
System.out.println("Bean2 init()");
}
}
}

bean2 中注入了 bean1

Bean1()
Bean1 init()
Creating implicit proxy for bean 'bean1' with 0 common interceptors and 2 specific interceptors 
Bean2()
Bean2 setBean1(bean1) class is: class org.springframework.aop.framework.autoproxy.A17_1$Bean1$$EnhancerBySpringCGLIB$$b7d6405
Bean2 init()    

bean1 初始化完成后,额外打印了一句日志信息:

1
Creating implicit proxy for bean 'bean1' with 0 common interceptors and 2 specific interceptors

表示为 bean1 创建了隐式代理。

此时代理对象在 Bean 初始化完成之后创建。

之后为 bean2 进行依赖注入时,注入的 bean1 是代理对象。

Bean1 类中添加 setBean2() 方法,表示向 bean1 中注入 bean2,此时 bean1 依赖 bean2,而 bean2 原本就依赖了 bean1,出现循环依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class Bean1 {
public void foo() {}
public Bean1() {
System.out.println("Bean1()");
}
@Autowired
public void setBean2(Bean2 bean2) {
System.out.println("Bean1 setBean2(bean2) class is: " + bean2.getClass());
}
@PostConstruct
public void init() {
System.out.println("Bean1 init()");
}
}

再次运行 main() 方法,查看 bean1 的代理对象的生成时机:

Bean1()
Bean2()
Creating implicit proxy for bean 'bean1' with 0 common interceptors and 2 specific interceptors 
Bean2 setBean1(bean1) class is: class org.springframework.aop.framework.autoproxy.A17_1$Bean1$$EnhancerBySpringCGLIB$$5cff48bf
Bean2 init()
Bean1 setBean2(bean2) class is: class org.springframework.aop.framework.autoproxy.A17_1$Bean2
Bean1 init()    

首先进行 bean1 的实例化,然后进行 bean1 的依赖注入,但此时容器中并没有 bean2,因此需要进行 bean2 的实例化。

接下来进行 bean2 的依赖注入,向 bean2 中注入 bean1,注入的 bean1 应该是被增强的,即它的代理对象,因此创建 bean1 的代理对象后再完成 bean2 的依赖注入。

接着继续 bean2 的生命周期,完成 bean2 的初始化阶段,最后回到 bean1 的依赖注入阶段,向 bean1 中注入 bean2,最后完成 bean1 的初始化阶段。

总结

代理对象的创建时机:

  • 无循环依赖时,在 Bean 初始化阶段之后创建;
  • 有循环依赖时,在 Bean 实例化后、依赖注入之前创建,并将代理对象暂存于二级缓存。

Bean 的依赖注入阶段和初始化阶段不应该被增强,仍应被施加于原始对象。

17.3 高级切面转低级切面

调用 AnnotationAwareAspectJAutoProxyCreator 对象的 findEligibleAdvisors() 方法时,获取能配合目标 Class 使用的切面,最终返回 Advisor列表。在搜索过程中,如果遇到高级切面,则会将其转换成低级切面。

现有切面类与目标类信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static class Aspect {
@Before("execution(* foo())")
public void before1() {
System.out.println("before1");
}

@Before("execution(* foo())")
public void before2() {
System.out.println("before2");
}

public void after() {
System.out.println("after");
}

public void afterReturning() {
System.out.println("afterReturning");
}

public void afterThrowing() {
System.out.println("afterThrowing");
}

public Object around(ProceedingJoinPoint pjp) throws Throwable {
try {
System.out.println("around...before");
return pjp.proceed();
} finally {
System.out.println("around...after");
}
}
}

static class Target {
public void foo() {
System.out.println("target foo");
}
}

高级切面中与通知类型相关的常用注解有 5 个:

  • @Before:前置通知
  • @AfterReturning:后置通知
  • @AfterThrowing:异常通知
  • @After:最终通知
  • @Around:环绕通知

以解析 @Before 注解为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Throwable {
// 切面对象实例工厂,用于后续反射调用切面中的方法
AspectInstanceFactory factory = new SingletonAspectInstanceFactory(new Aspect());
// 高级切面转低级切面类
List<Advisor> list = new ArrayList<>();
for (Method method : Aspect.class.getDeclaredMethods()) {
if (method.isAnnotationPresent(Before.class)) {
// 解析切点
String expression = method.getAnnotation(Before.class).value();
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(expression);
// 通知类。前置通知对应的通知类是 AspectJMethodBeforeAdvice
AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(method, pointcut, factory);
// 切面
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
list.add(advisor);
}
}
for (Advisor advisor : list) {
System.out.println(advisor);
}
}
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void org.springframework.aop.framework.autoproxy.A17_2$Aspect.before2()]; aspect name '']
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void org.springframework.aop.framework.autoproxy.A17_2$Aspect.before1()]; aspect name '']    

@Before 标记的前置通知会被转换成原始的 AspectJMethodBeforeAdvice 形式,该对象包含了以下信息:

  • 通知对应的方法信息
  • 切点信息
  • 通知对象如何创建,本例公用一个 Aspect 对象

通知相关注解与原始通知类对应关系如下:

注解 对应的原始通知类
@Before AspectJMethodBeforeAdvice
@AfterReturning AspectJAfterReturningAdvice
@AfterThrowing AspectJAfterThrowingAdvice
@After AspectJAfterAdvice
@Around AspectJAroundAdvice

18. 静态通知调用

18.1 统一转换成环绕通知

通知相关注解都对应一个原始通知类,在 Spring 底层会将这些通知转换成环绕通知 MethodInterceptor。如果原始通知类本就实现了 MethodInterceptor 接口,则无需转换。

原始通知类 是否需要转换成 MethodInterceptor
AspectJMethodBeforeAdvice
AspectJAfterReturningAdvice
AspectJAfterThrowingAdvice ×
AspectJAfterAdvice ×
AspectJAroundAdvice ×

使用 ProxyFactory 无论基于哪种方式创建代理对象,最终调用 advice(通知,或者说通知对应的方法)的都是 MethodInvocation 对象。

项目中存在的 advisor(原本的低级切面和由高级切面转换得到的低级切面)往往不止一个,它们一个套一个地被调用,因此需要一个调用链对象,即 MethodInvocation

MethodInvocation 需要知道 advice 有哪些,还需要知道目标对象是哪个。调用次序如下:

通知调用次序

由上图可知,环绕 通知最适合作为 advice,而 BeforeAfterReturning 都应该转换成环绕通知。

统一转换成环绕通知的形式,体现了设计模式中的适配器模式:

  • 对外更方便使用和区分各种通知类型
  • 对内统一都是环绕通知,统一使用 MethodInterceptor表示

通过 ProxyFactory 对象的 getInterceptorsAndDynamicInterceptionAdvice() 方法将其他通知统一转换为 MethodInterceptor 环绕通知:

注解 原始通知类 适配器 拦截器
@Before AspectJMethodBeforeAdvice MethodBeforeAdviceAdapter MethodBeforeAdviceInterceptor
@AfterReturning AspectJAfterReturningAdvice AspectJAfterReturningAdvice AfterReturningAdviceInterceptor

转换得到的通知都是静态通知,体现在 getInterceptorsAndDynamicInterceptionAdvice() 方法中的 Interceptors 部分,这些通知在被调用时无需再次检查切点,直接调用即可。

代码测试

切面类与目标类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static class Aspect {
@Before("execution(* foo())")
public void before1() {
System.out.println("before1");
}

@Before("execution(* foo())")
public void before2() {
System.out.println("before2");
}

public void after() {
System.out.println("after");
}

@AfterReturning("execution(* foo())")
public void afterReturning() {
System.out.println("afterReturning");
}

@AfterThrowing("execution(* foo())")
public void afterThrowing(Exception e) {
System.out.println("afterThrowing " + e.getMessage());
}

@Around("execution(* foo())")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
try {
System.out.println("around...before");
return pjp.proceed();
} finally {
System.out.println("around...after");
}
}
}

static class Target {
public void foo() {
System.out.println("target foo");
}
}

将高级切面转换成低级切面,并将通知统一转换成环绕通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@SuppressWarnings("all")
public static void main(String[] args) throws Throwable {

AspectInstanceFactory factory = new SingletonAspectInstanceFactory(new Aspect());
// 1. 高级切面转低级切面类
List<Advisor> list = new ArrayList<>();
for (Method method : Aspect.class.getDeclaredMethods()) {
if (method.isAnnotationPresent(Before.class)) {
// 解析切点
String expression = method.getAnnotation(Before.class).value();
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(expression);
// 通知类
AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(method, pointcut, factory);
// 切面
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
list.add(advisor);
} else if (method.isAnnotationPresent(AfterReturning.class)) {
// 解析切点
String expression = method.getAnnotation(AfterReturning.class).value();
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(expression);
// 通知类
AspectJAfterReturningAdvice advice = new AspectJAfterReturningAdvice(method, pointcut, factory);
// 切面
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
list.add(advisor);
} else if (method.isAnnotationPresent(Around.class)) {
// 解析切点
String expression = method.getAnnotation(Around.class).value();
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(expression);
// 通知类
AspectJAroundAdvice advice = new AspectJAroundAdvice(method, pointcut, factory);
// 切面
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
list.add(advisor);
}
}
for (Advisor advisor : list) {
System.out.println(advisor);
}

// 2. 通知统一转换为环绕通知 MethodInterceptor
Target target = new Target();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisors(list);

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
List<Object> methodInterceptorList = proxyFactory.getInterceptorsAndDynamicInterceptionAdvice(Target.class.getMethod("foo"), Target.class);
for (Object o : methodInterceptorList) {
System.out.println(o);
}
}
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void org.springframework.aop.framework.A18$Aspect.before2()]; aspect name '']
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJAroundAdvice: advice method [public java.lang.Object org.springframework.aop.framework.A18$Aspect.around(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable]; aspect name '']
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJMethodBeforeAdvice: advice method [public void org.springframework.aop.framework.A18$Aspect.before1()]; aspect name '']
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.aspectj.AspectJAfterReturningAdvice: advice method [public void org.springframework.aop.framework.A18$Aspect.afterReturning()]; aspect name '']
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@7ce6a65d
org.springframework.aop.aspectj.AspectJAroundAdvice: advice method [public java.lang.Object org.springframework.aop.framework.A18$Aspect.around(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable]; aspect name ''
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@1500955a
org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor@e874448 

根据打印信息可知:

  • 前置通知 AspectJMethodBeforeAdvice 被转换成 MethodBeforeAdviceInterceptor
  • 环绕通知 AspectJAroundAdvice 保持不变
  • 后置通知 AspectJAfterReturningAdvice 被转换成 AfterReturningAdviceInterceptor

18.2 调用链执行

高级切面成功转换成低级切面,切面中的通知也全部转换成环绕通知 MethodInterceptor,最后还要调用这些通知和目标方法。

这个调用交由调用链对象 MethodInvocation 来完成,在调用链对象中存放了所有经过转换得到的环绕通知和目标方法。

MethodInvocation 是一个接口,其最根本的实现是 ReflectiveMethodInvocation

构建 ReflectiveMethodInvocation对象需要 6 个参数:

  • proxy:代理对象
  • target:目标对象
  • method:目标对象中的方法对象
  • arguments:调用目标对象中的方法需要的参数
  • targetClass:目标对象的 Class 对象
  • interceptorsAndDynamicMethodMatchers:转换得到的环绕通知列表
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Throwable {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
// 3. 创建并执行调用链 (环绕通知s + 目标)
MethodInvocation methodInvocation = new ReflectiveMethodInvocation(
null, target, Target.class.getMethod("foo"), new Object[0], Target.class, methodInterceptorList
);
methodInvocation.proceed();
}

运行 main() 方法后会抛出异常:

Exception in thread "main" java.lang.IllegalStateException: No MethodInvocation found:   

调用链对象明明已经创建好了呀!

这是因为调用链在执行过程会调用到很多通知,而某些通知内部可能需要使用调用链对象。因此需要将调用链对象存放在某一位置,使所有通知都能获取到调用链对象。

这个“位置”就是 当前线程

可以在所有通知的最外层再添加一个环绕通知,将调用链对象放入当前线程。

将MethodInvocation放入当前线程

这里我们使用 Spring 提供的 ExposeInvocationInterceptor 作为最外层的环绕通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws Throwable {
// --snip--

// 2. 通知统一转换为环绕通知 MethodInterceptor
Target target = new Target();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
// 在最外层添加环绕通知,把 MethodInvocation 放入当前线程
proxyFactory.addAdvice(ExposeInvocationInterceptor.INSTANCE);
proxyFactory.addAdvisors(list);

// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
// 3. 创建并执行调用链 (环绕通知s + 目标)
MethodInvocation methodInvocation = new ReflectiveMethodInvocation(
null, target, Target.class.getMethod("foo"), new Object[0], Target.class, methodInterceptorList
);
methodInvocation.proceed();
}
before2
before1
around...before
target foo
around...after
afterReturning    

18.3 模拟实现调用链

调用链执行过程是一个递归过程。执行 proceed() 方法将调用调用链中下一个通知或目标方法。当调用链中没有通知时,就调用目标方法,反之调用下一个通知。

这体现了设计模式中的责任链模式。

目标类 Target

1
2
3
4
5
static class Target {
public void foo() {
System.out.println("Target foo()");
}
}

实现 MethodInterceptor 接口,编写两个环绕通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static class Advice1 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Advice1.before()");
Object result = invocation.proceed();
System.out.println("Advice1.after()");
return result;
}
}

static class Advice2 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Advice2.before()");
Object result = invocation.proceed();
System.out.println("Advice2.after()");
return result;
}
}

实现 MethodInvocation 接口,实现自己的调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static class MyInvocation implements MethodInvocation {

private final Object target;
private final Method method;
private final Object[] args;
private final List<MethodInterceptor> methodInterceptorList;
private int count = 1;

public MyInvocation(Object target, Method method, Object[] args, List<MethodInterceptor> methodInterceptorList) {
this.target = target;
this.method = method;
this.args = args;
this.methodInterceptorList = methodInterceptorList;
}

@Override
public Method getMethod() {
return this.method;
}

@Override
public Object[] getArguments() {
return this.args;
}

@Override
public Object proceed() throws Throwable {
if (count > methodInterceptorList.size()) {
// 调用目标,结束递归并返回
return method.invoke(target, args);
}
// 逐一调用通知
MethodInterceptor interceptor = methodInterceptorList.get(count++ - 1);
// 递归操作交给通知类
return interceptor.invoke(this);
}

@Override
public Object getThis() {
return this.target;
}

@Override
public AccessibleObject getStaticPart() {
return method;
}
}
1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Throwable {
Target target = new Target();
List<MethodInterceptor> list = new ArrayList<>(Arrays.asList(
new Advice1(),
new Advice2()
));
MyInvocation invocation = new MyInvocation(target, Target.class.getMethod("foo"), new Object[0], list);
invocation.proceed();
}
Advice1.before()
Advice2.before()
Target foo()
Advice2.after()
Advice1.after()    

18.4 代理对象调用流程

以 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 返回最终的结果

下图中不同颜色对应一次环绕通知或目标的调用起始至终结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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(0, 100, 0)
ih ->> +mi : mi.proceed()
mi ->> +mi1 : invoke(mi)
mi1 ->> mi1 : 前增强
rect rgb(125, 120, 25)
mi1 ->> mi : mi.proceed()
mi ->> +mi2 : invoke(mi)
mi2 ->> mi2 : 前增强
rect rgb(10, 90, 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 :

image-20250207163334482

19. 动态通知调用

前文的示例都是静态通知调用,无需参数绑定,执行时无需切点信息,性能较高。

相应地就有动态通知调用,它需要参数绑定,执行时还需要切点信息,性能较低。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Aspect
static class MyAspect {
/**
* 静态通知调用,无需参数绑定,性能较高
* 执行时无需切点信息
*/
@Before("execution(* foo(..))")
public void before1() {
System.out.println("before1");
}

/**
* 动态通知调用,需要参数绑定,性能较低
* 执行时还需要切点信息
*/
@Before("execution(* foo(..)) && args(x)")
public void before2(int x) {
System.out.printf("before(%d)\n", x);
}
}

目标类 Target

1
2
3
4
5
static class Target {
public void foo(int x) {
System.out.printf("target foo(%d)\n", x);
}
}

配置类 MyConfig

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
static class MyConfig {
@Bean
public AnnotationAwareAspectJAutoProxyCreator proxyCreator() {
return new AnnotationAwareAspectJAutoProxyCreator();
}

@Bean
public MyAspect myAspect() {
return new MyAspect();
}
}

编写 main()方法,新建 Spring 容器,查找符合条件的切面,将所有通知转换成环绕通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws Throwable {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(ConfigurationClassPostProcessor.class);
context.registerBean(MyConfig.class);
context.refresh();

AnnotationAwareAspectJAutoProxyCreator creator = context.getBean(AnnotationAwareAspectJAutoProxyCreator.class);
List<Advisor> list = creator.findEligibleAdvisors(Target.class, "target");

Target target = new Target();
ProxyFactory factory = new ProxyFactory();
factory.setTarget(target);
factory.addAdvisors(list);

List<Object> interceptorList = factory.getInterceptorsAndDynamicInterceptionAdvice(Target.class.getMethod("foo", int.class), Target.class);
for (Object o : interceptorList) {
System.out.println(o);
}
}
org.springframework.aop.interceptor.ExposeInvocationInterceptor@12591ac8
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@5a7fe64f
org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher@38145825    

第一个 ExposeInvocationInterceptor 对象是 Spring 添加的环绕通知,第二个 MethodBeforeAdviceInterceptor 对象是前置通知转换得到的环绕通知,那 InterceptorAndDynamicMethodMatcher 对象是什么呢?

1
2
3
4
5
6
7
8
9
10
11
class InterceptorAndDynamicMethodMatcher {

final MethodInterceptor interceptor;

final MethodMatcher methodMatcher;

public InterceptorAndDynamicMethodMatcher(MethodInterceptor interceptor, MethodMatcher methodMatcher) {
this.interceptor = interceptor;
this.methodMatcher = methodMatcher;
}
}

InterceptorAndDynamicMethodMatcher 并没有实现 MethodInterceptor 接口,它 不是一个环绕通知,对应了动态通知调用。

因此 ProxyFactory 对象的 getInterceptorsAndDynamicInterceptionAdvice() 方法返回的不仅是转换得到的环绕通知,还有对应动态通知调用的 InterceptorAndDynamicMethodMatcher 对象。

InterceptorAndDynamicMethodMatcher 对象中包含了环绕通知 interceptor 对象和切点信息 methodMatcher(前文使用过的 AspectJExpressionPointcut 也实现了 MethodMatcher 接口)。

尝试查看 InterceptorAndDynamicMethodMatcher 对象中包含的信息,但该类并未声明成 public,其成员变量也未被 public 修饰,也没提供获取的方式,但可以使用反射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) throws Throwable {
// --snip--

for (Object o : interceptorList) {
showDetail(o);
}
}

public static void showDetail(Object o) {
try {
Class<?> clazz = Class.forName("org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher");
if (clazz.isInstance(o)) {
Field methodMatcher = clazz.getDeclaredField("methodMatcher");
methodMatcher.setAccessible(true);
Field methodInterceptor = clazz.getDeclaredField("interceptor");
methodInterceptor.setAccessible(true);
System.out.println("环绕通知和切点:" + o);
System.out.println("\t切点为:" + methodMatcher.get(o));
System.out.println("\t通知为:" + methodInterceptor.get(o));
} else {
System.out.println("普通环绕通知:" + o);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
普通环绕通知:org.springframework.aop.interceptor.ExposeInvocationInterceptor@5a7fe64f
普通环绕通知:org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@38145825
环绕通知和切点:org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher@41330d4f
	切点为:AspectJExpressionPointcut: (int x) execution(* foo(..)) && args(x)
	通知为:org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@24c1b2d2    

根据打印的切点信息可知,InterceptorAndDynamicMethodMatcher 对象的确对应了动态通知调用。

最后创建调用链对象,执行通知和原始方法:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Throwable {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>");
Object proxy = factory.getProxy();
MethodInvocation methodInvocation = new ReflectiveMethodInvocation(
proxy, target, Target.class.getMethod("foo", int.class), new Object[]{100}, Target.class, interceptorList
) {
};

methodInvocation.proceed();
}
before1
before(100)
target foo(100)       

动态通知调用需要切点信息,需要对参数进行匹配和绑定,复杂程度高,性能比静态通知调用低。

20. RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter

20.1 DispatcherServlet 的初始化

选择内嵌 Tomcat 的 Spring 容器

1
2
3
4
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}

WebConfig 作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂DispatcherServletDispatcherServlet 注册对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
@ComponentScan
public class WebConfig {
/**
* 内嵌 web 容器工厂
* @return
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
return new TomcatServletWebServerFactory();
}

/**
* 创建 DispatcherServlet
* @return
*/
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

/**
* 注册 DispatcherServlet,SpringMVC的入口
* @return
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean =
new DispatcherServletRegistrationBean(dispatcherServlet, "/");
return registrationBean;
}
}

运行 main()方法,控制台打印出:

Tomcat initialized with port(s): 8080 (http)
Root WebApplicationContext: initialization completed in 1222 ms  

Tomcat 容器初始化成功,Spring 容器初始化成功,但 DispatcherServlet 还未被初始化。

当 Tomcat 服务器 首次 使用到 DispatcherServlet 时,才会由 Tomcat 服务器初始化 DispatcherServlet

清空控制台信息,使用浏览器访问 localhost:8080,控制台打印出:

信息: Initializing Spring DispatcherServlet 'dispatcherServlet'
[INFO ] Initializing Servlet 'dispatcherServlet' 
[TRACE] No MultipartResolver 'multipartResolver' declared 
[TRACE] No LocaleResolver 'localeResolver': using default [AcceptHeaderLocaleResolver] 
[TRACE] No ThemeResolver 'themeResolver': using default [FixedThemeResolver] 
[TRACE] No HandlerMappings declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No HandlerAdapters declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No HandlerExceptionResolvers declared in servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No RequestToViewNameTranslator 'viewNameTranslator': using default [DefaultRequestToViewNameTranslator] 
[TRACE] No ViewResolvers declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties 
[TRACE] No FlashMapManager 'flashMapManager': using default [SessionFlashMapManager] 
[INFO] Completed initialization in 482 ms   

完成 DispatcherServlet 的初始化。

Debug 断点查看DispatcherServlet的初始化时机

断点 DispatcherServletonRefresh() 方法中 this.initStrategies(context) 所在行:

1
2
3
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);//断点位置
}

以 DEBUG 方式重启程序,此时程序尚未执行到断点处。

再次在浏览器中访问 localhost:8080,程序执行到断点处。

查看调用栈可知,是从 GenericServletinit() 方法执行到 onRefresh() 方法的:

1
2
3
4
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}

因此 DispatcherServlet 的初始化流程走的是 Servlet 的初始化流程。

使 DispatcherServlet 在 Tomcat 服务器启动时被初始化

修改添加到 Spring 容器的 DispatcherServlet 注册 Bean:

1
2
3
4
5
6
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}

设置 loadOnStartup 为一个正数。

当存在多个 DispatcherServlet 需要被注册时,设置的 loadOnStartup 越大,优先级越低。

再次重启程序,根据控制台输出的内容可知,不仅完成 Tomcat 和 Spring 容器的初始化,DispatcherServlet 也初始化成功。

抽取配置信息到配置文件中

使用 @PropertySource 注解设置配置类需要读取的配置文件,以便后续读取配置文件中的内容。

要读取配置文件中的内容,可以使用 @Value 注解,但该注解一次仅仅能够读取一个值,现实是往往需要从配置文件中读取多个值。

可以使用 @EnableConfigurationProperties 注解完成配置文件信息与对象的绑定,后续使用时作为 @Bean 注解标记的方法的参数直接在方法中使用即可:

1
2
server.port=9090
spring.mvc.servlet.load-on-startup=1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.itheima.a20;

import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.DispatcherServlet;

/**
* @author LXDa
* @date 2025/1/14 21:55
*/

@Configuration
@ComponentScan
@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
/**
* 内嵌 web 容器工厂
* @return
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties) {
return new TomcatServletWebServerFactory(serverProperties.getPort());
}

/**
* 创建 DispatcherServlet
* @return
*/
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

/**
* 注册 DispatcherServlet,SpringMVC的入口
* @return
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(
DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties) {
DispatcherServletRegistrationBean registrationBean =
new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
return registrationBean;
}
}

再次重启程序,根据控制台输出的内容可知,Tomcat 此时监听的端口是 9090DispatcherServlet 也在 Tomcat 启动时被初始化。

DispatcherServlet 初始化时执行的操作

回到 DispatcherServletonRefresh() 方法,它又调用了 initStrategies() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}

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

在这个方法中初始化了一系列组件,重点介绍:

  • initHandlerMappings():初始化处理器映射器
  • initHandlerAdapters():初始化处理器适配器
  • initHandlerExceptionResolvers():初始化异常处理器

在所有的初始化方法中都有一个相似的逻辑,首先使用一个布尔值判断是否检测 所有 目标组件。

Spring 支持父子容器嵌套,如果判断的布尔值为 true,那么 Spring 不仅会在当前容器中获取目标组件,还会在其所有父级容器中寻找。

initHandlerMappings()为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) { // 是否需要检测所有处理器映射器
// --snip--
} else {
// 无需检测所有处理器映射器时,获取当前容器中的处理器映射器
// --snip--
}

if (this.handlerMappings == null) {
// 当前容器中没有处理器映射器时,设置默认的处理器映射器
this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class);
// --snip--
}

// --snip--
}

20.2 RequestMappingHandlerMapping

HandlerMapping 即处理器映射器,用于建立请求路径和控制器方法的映射关系

RequestMappingHandlerMappingHandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。

当 Spring 容器中没有 HandlerMapping 的实现时,尽管DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。

在配置类中将 RequestMappingHandlerMapping 添加到 Spring 容器:

1
2
3
4
5
6
//如果用 DispatcherServlet 初始化时默认添加的组件,并不会放到容器中作为bean,给测试带来困扰
//1、加入 RequestMappingHandlerMapping
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}

自定义控制器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j
@Controller
public class Controller1 {
@GetMapping("/test1")
public ModelAndView test1() throws Exception {
log.debug("test1()");
return null;
}

@PostMapping("/test2")
public ModelAndView test2(@RequestParam("name") String name) {
log.debug("test2({})", name);
return null;
}

@PutMapping("/test3")
public ModelAndView test3(String token) {
log.debug("test3({})", token);
return null;
}

@RequestMapping("/test4")
public User test4() {
log.debug("test4");
return new User("张三", 18);
}
}

编写 main()方法,从 Spring 容器中获取 RequestMappingHandlerMapping,再获取请求路径与映射器方法的映射关系,并根据给定请求获取控制器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws Exception {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);

//解析 @RequestMapping 及其派生注解,生成路径与控制器方法的映射关系,初始化时建立
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);

//获取映射结果
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((info, method) -> {
log.info("{} ---> {}", info, method);
});
//接收请求,获取控制器方法,返回处理器执行链对象
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test1")
MockHttpServletResponse response = new MockHttpServletResponse();

//处理器执行器链
HandlerExecutionChain executionChain = handlerMapping.getHandler(request);
System.out.println(executionChain);
}
{POST [/test2]} ---> com.itheima.a20.Controller1#test2(String)
{PUT [/test3]} ---> com.itheima.a20.Controller1#test3(String)
{GET [/test1]} ---> com.itheima.a20.Controller1#test1()
{ [/test4]} ---> com.itheima.a20.Controller1#test4()
HandlerExecutionChain with [com.itheima.a20.Controller1#test1()] and 0 interceptors  

getHandler() 方法返回的对象是处理器执行链,不仅包含映射器方法,还包含需要执行的拦截器信息。

MockHttpServletRequest 的使用

需要导入以下依赖

1
2
3
4
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>

20.3 RequestMappingHandlerAdapter

RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。

同样需要在配置类中将 RequestMappingHandlerAdapter 添加到 Spring 容器,但该类中需要测试的方法被 protected 修饰,无法直接使用,因此创建一个子类,将子类添加到 Spring 容器中:

1
2
3
4
5
6
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
return super.invokeHandlerMethod(request, response, handlerMethod);
}
}
1
2
3
4
5
//2、继续加入 RequestMappingHandlerAdapter ,会替换掉 DispatcherServlet 默认的4个 HandlerAdapter
@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}

在 main()方法中测试 RequestMappingHandlerAdapterinvokeHandlerMethod() 方法:

1
2
3
4
5
@PostMapping("/test2")
public ModelAndView test2(@RequestParam("name") String name) {
log.info("test2({})", name);
return null;
}
1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
// --snip--

MockHttpServletRequest test2Req = new MockHttpServletRequest("POST", "/test2");
request.setParameter("name", "lxd");
MockHttpServletResponse test2Resp = new MockHttpServletResponse();
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(test2Req, test2Resp, ((HandlerMethod) handlerMapping.getHandler(request).getHandler()));
}
com.itheima.a20.Controller1 - test2(lxd)  

实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?

需要解析@RequestParam 注解

Spring 支持多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 MyRequestMappingHandlerAdaptergetArgumentResolvers() 方法获取所有参数解析器。

Spring 也支持许多种类的控制器方法返回值类型,使用 MyRequestMappingHandlerAdaptergetReturnValueHandlers() 方法获取所有返回值处理器。

1
2
3
4
5
6
7
8
System.out.println("【参数解析器】");
for (HandlerMethodArgumentResolver resolver : handlerAdapter.getArgumentResolvers()) {
System.out.println(resolver);
}
System.out.println("【返回值解析器】");
for (HandlerMethodReturnValueHandler resolver : handlerAdapter.getReturnValueHandlers()) {
System.out.println(resolver);
}
【参数解析器】
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@2ab2710
org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver@253b380a
org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver@29c2c826
org.springframework.web.servlet.mvc.method.annotation.PathVariableMapMethodArgumentResolver@3350ebdd
org.springframework.web.servlet.mvc.method.annotation.MatrixVariableMethodArgumentResolver@6818d900
org.springframework.web.servlet.mvc.method.annotation.MatrixVariableMapMethodArgumentResolver@149f5761
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@2ba33e2c
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@1f193686
org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver@31e72cbc
org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver@5fad41be
org.springframework.web.method.annotation.RequestHeaderMapMethodArgumentResolver@6dcd5639
org.springframework.web.servlet.mvc.method.annotation.ServletCookieValueMethodArgumentResolver@3b36e000
org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver@333cb916
org.springframework.web.servlet.mvc.method.annotation.SessionAttributeMethodArgumentResolver@629ae7e
org.springframework.web.servlet.mvc.method.annotation.RequestAttributeMethodArgumentResolver@1d25c1c
org.springframework.web.servlet.mvc.method.annotation.ServletRequestMethodArgumentResolver@de88ac6
org.springframework.web.servlet.mvc.method.annotation.ServletResponseMethodArgumentResolver@5bca7664
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@105b693d
org.springframework.web.servlet.mvc.method.annotation.RedirectAttributesMethodArgumentResolver@3fae596
org.springframework.web.method.annotation.ModelMethodProcessor@4a0df195
org.springframework.web.method.annotation.MapMethodProcessor@42fcc7e6
org.springframework.web.method.annotation.ErrorsMethodArgumentResolver@9255c05
org.springframework.web.method.annotation.SessionStatusMethodArgumentResolver@5da7cee2
org.springframework.web.servlet.mvc.method.annotation.UriComponentsBuilderMethodArgumentResolver@78830d9a
com.itheima.a20.TokenArgumentResolver@5ce4369b
org.springframework.web.servlet.mvc.method.annotation.PrincipalMethodArgumentResolver@7f829c76
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver@1cb19dba
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@7c3ebc6b
【返回值解析器】
org.springframework.web.servlet.mvc.method.annotation.ModelAndViewMethodReturnValueHandler@1931d99
org.springframework.web.method.annotation.ModelMethodProcessor@6a9950f1
org.springframework.web.servlet.mvc.method.annotation.ViewMethodReturnValueHandler@7ad54c55
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler@73017a80
org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler@6ae7deac
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor@4a5905d9
org.springframework.web.servlet.mvc.method.annotation.HttpHeadersReturnValueHandler@1a3e5f23
org.springframework.web.servlet.mvc.method.annotation.CallableMethodReturnValueHandler@6293e39e
org.springframework.web.servlet.mvc.method.annotation.DeferredResultMethodReturnValueHandler@365553de
org.springframework.web.servlet.mvc.method.annotation.AsyncTaskMethodReturnValueHandler@34a0ef00
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@5c0f79f0
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor@21fdfefc
org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler@3daa82be
org.springframework.web.method.annotation.MapMethodProcessor@ec1b2e4
com.itheima.a20.YmlReturnValueHander@29a69a35
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor@67e28be3  

自定义参数解析器

假如经常需要使用到请求头中的 Token 信息,自定义 @Token 注解,使用该注解标记由控制器方法的哪个参数来获取 Token 信息:

1
2
3
4
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}

使 test3() 控制器方法参数被 @Token 标记:

1
2
3
4
5
@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
log.info("test3({})", token);
return null;
}

自定义参数解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
@Override
// 是否支持某个参数
public boolean supportsParameter(MethodParameter parameter) {
Token token = parameter.getParameterAnnotation(Token.class);
return token != null;
}

@Override
// 解析参数
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return webRequest.getHeader("token");
}
}

将参数解析器添加到 HandlerAdapter

1
2
3
4
5
6
7
@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));
return handlerAdapter;
}

测试执行 test3() 控制器方法:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
// --snip--
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
MockHttpServletRequest test3Req = new MockHttpServletRequest("PUT", "/test3");
request.addHeader("token", "token info");
MockHttpServletResponse test3Resp = new MockHttpServletResponse();
HandlerExecutionChain executionChain = handlerMapping.getHandler(test3Req);
handlerAdapter.invokeHandlerMethod(test3Req, test3Resp, (HandlerMethod) executionChain.getHandler());
}
com.itheima.a20.Controller1 - test3(token info)  

自定义返回值处理器

@ResponseBody 标记了控制器方法时,方法的返回值会转换成 JSON 写入响应体中。

自定义 @Yml 注解,被 @Yml 注解标记的控制器方法的返回值会转换成 YAML 写入响应体中。

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Yml {
}

使 test4() 控制器方法被 @Yml 注解标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/test4")
@Yml
public User test4() {
log.info("test4");
return new User("张三", 18);
}

@Data
@AllArgsConstructor
static class User {
private String name;
private int age;
}

自定义返回值处理器将返回值转换成 YAML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class YmlReturnValueHander implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Yml yml = returnType.getMethodAnnotation(Yml.class);
return yml != null;
}

@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
//1、转换返回结果为Yaml
String str = new Yaml().dump(returnValue);
//2、将yaml 字符串写入响应
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setContentType("text/plain;charset=utf-8");
response.getWriter().print(str);
//3、设置请求处理完毕
mavContainer.setRequestHandled(true);
}
}

将返回值处理器添加到 HandlerAdapter 中:

1
2
3
4
5
6
7
8
9
10
@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
YmlReturnValueHander ymlReturnValueHander = new YmlReturnValueHander();
MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));
//添加自定义返回值处理器
handlerAdapter.setCustomReturnValueHandlers(List.of(ymlReturnValueHander));
return handlerAdapter;
}

测试执行 test4() 控制器方法:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
// --snip--

MockHttpServletRequest test4Req = new MockHttpServletRequest("GET", "/test4");
MockHttpServletResponse test4Resp = new MockHttpServletResponse();
handlerAdapter.invokeHandlerMethod(test4Req, test4Resp, ((HandlerMethod) handlerMapping.getHandler(test4Req).getHandler()));
//检验响应
System.out.println(test4Resp.getContentAsString());
}
com.itheima.a20.Controller1 - test4
!!com.itheima.a20.Controller1$User {age: 18, name: 张三}  

21. 参数解析器

Spring 提供了许多种类的控制器方法参数解析器,定义一个包含多个不同种类参数的控制器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static class Controller {
public void test(
@RequestParam("name1") String name1, // name1=张三
String name2,// name2=李四
@RequestParam("age") int age,// age=18 (参数类型转换)
@RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据
@RequestParam("file") MultipartFile file, // 上传文件
@PathVariable("id") int id, // /test/124 /test/{id}
@RequestHeader("Content-Type") String header,
@CookieValue("token") String token,
@Value("${JAVA_HOME}") String home2, // spring 获取数据 ${} #{}
HttpServletRequest request, // request, response, session ...
@ModelAttribute("abc") User user1, // name=zhang&age=18
User user2, // name=zhang&age=18
@RequestBody User user3 // json
) {
}
}

@Data
@ToString
static class User {
private String name;
private int age;
}

将控制方法封装成HandlerMethod 并打印参数信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Exception {
// 控制器方法封装成 HandlerMethod
Method method = Controller.class.getMethod("test", String.class, String.class,
int.class, String.class, MultipartFile.class,
int.class, String.class, String.class,
String.class, HttpServletRequest.class, User.class,
User.class, User.class);
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), method);

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
String annotations = Arrays.stream(parameter.getParameterAnnotations())
.map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotations.length() > 0 ? "@" + annotations + " " : "";
// 设置参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
System.out.println("【" + parameter.getParameterIndex() + "】"
+ str + " "
+ parameter.getParameterType().getSimpleName() + " "
+ parameter.getParameterName());
}
}
【0】@RequestParam String  name1
【1】String  name2
【2】@RequestParam int  age
【3】@RequestParam String  home1
【4】@RequestParam MultipartFile  file
【5】@PathVariable int  id
【6】@RequestHeader String  header
【7】@CookieValue String  token
【8】@Value String  home2
【9】HttpServletRequest  request
【10】@ModelAttribute User  user1
【11】User  user2
【12】@RequestBody User  user3  

mock 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static HttpServletRequest mockRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name1", "zhangsan");
request.setParameter("name2", "lisi");
request.addPart(new MockPart("file", "hello", "hello".getBytes(StandardCharsets.UTF_8)));
Map<String, String> uriTemplateVariables = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123");
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
request.setContentType("application/json");
request.setCookies(new Cookie("token", "123456"));
request.setParameter("name", "张三");
request.setParameter("age", "18");
request.setContent("""
{
"name":"李四",
"age":18
}
""".getBytes(StandardCharsets.UTF_8));
return new StandardServletMultipartResolver().resolveMultipart(request);
}

21.2 @RequestParam

解析@RequestParam 需要使用RequestParamMethodArgumentResolver,构造需要两个参数:

  • beanFactory:Bean 工厂对象。需要解析 ${}时,就需要指定 Bean 工厂对象
  • useDefaultResolution:布尔类型参数。
    • false 表示只解析添加了 @RequestParam 注解的参数,
    • true 表示省略 @RequestParam 注解的参数也能进行解析。

RequestParamMethodArgumentResolver 通过 resolveArgument()解析,方法所需四个参数:

  • parameter:参数对象
  • mavContainerModelAndView 容器,用来存储中间的 Model 结果
  • webRequest:由 ServletWebRequest 封装后的请求对象
  • binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 "18"转换成整型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
HttpServletRequest request = mockRequest();

// 控制器方法封装成 HandlerMethod
Method method = Controller.class.getMethod("test", String.class, String.class,
int.class, String.class, MultipartFile.class,
int.class, String.class, String.class,
String.class, HttpServletRequest.class, User.class,
User.class, User.class);
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), method);

// 准备对象绑定与类型转换
ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory(null, null);

// 准备 ModelAndViewContainer 用来存储中间的 Model 结果
ModelAndViewContainer container = new ModelAndViewContainer();

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory, true);

String annotations = Arrays.stream(parameter.getParameterAnnotations())
.map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotations.length() > 0 ? "@" + annotations + " " : "";
// 设置参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());

if (resolver.supportsParameter(parameter)) {
Object value = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println("【" + parameter.getParameterIndex() + "】"
+ str
+ parameter.getParameterType().getSimpleName() + " "
+ parameter.getParameterName()
+ " --> " + value);
} else {
System.out.println("【" + parameter.getParameterIndex() + "】"
+ str
+ parameter.getParameterType().getSimpleName() + " "
+ parameter.getParameterName());
}
}
}
【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18   【String ---> Integer  类型转换】
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@28975c28
Exception in thread "main" java.lang.IllegalStateException: Optional int parameter 'id' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type 

前 5 个参数正确解析,在解析第 6 个参数时产生了异常

这是因为在构造RequestParamMethodArgumentResolver 时,将 useDefaultResolution 设置为 true,针对未添加@RequestParam注解的参数都用该参数解析器进行解析,第 6 个参数id,由当前解析器解析结果为null,无法将null值赋给基本类型int,第 6 个参数应交由其他参数解析器进行解析

多个参数解析器组合使用

不同类型的参数需要使用不同参数解析器进行解析,当前解析器不支持解析当前类型参数时,就需要换到下个解析器尝试解析。

可以将所有参数解析器添加到一个集合中,然后遍历这个集合,实现上述需求。

Spring 提供了名为 HandlerMethodArgumentResolverComposite 的类,对上述逻辑进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, true)
);

// --snip--

if (composite.supportsParameter(parameter)) {
Object value = composite.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println("【" + parameter.getParameterIndex() + "】"
+ str
+ parameter.getParameterType().getSimpleName() + " "
+ parameter.getParameterName()
+ " --> " + value);
if (!(container.getModel()).isEmpty()) {
System.out.println("模型数据为: " + container.getModel());
}
} else {
System.out.println("【" + parameter.getParameterIndex() + "】"
+ str + " "
+ parameter.getParameterType().getSimpleName() + " "
+ parameter.getParameterName());
}

}
}

21.2 @PathVariable

@PathVariable 需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。

使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver()
);

// --snip--
}
}

修改 RequestParamMethodArgumentResolver 参数解析器的构造,将 useDefaultResolution 设置为 false,让程序暂时不抛出异常。

【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int  id  -->  123  

21.3 @RequestHeader

@RequestHeader 需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个 Bean 工厂对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory)
);

// --snip--
}
}
【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int  id  -->  123
【6】@RequestHeader String  header  -->  application/json  

21.4 @CookieValue

@CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个 Bean 工厂对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory)
);

// --snip--
}
}
【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int  id  -->  123
【6】@RequestHeader String  header  -->  application/json
【7】@CookieValue String  token  -->  123456  

21.5 @Value

@Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个 Bean 工厂对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory)
);

// --snip--
}
}
【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int  id  -->  123
【6】@RequestHeader String  header  -->  application/json
【7】@CookieValue String  token  -->  123456
【8】@Value String  home2  -->  C:\Program Files\Java\jdk1.8.0_60  

21.6 HttpServletRequest

HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver()
);

// --snip--
}
}
【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int  id  -->  123
【6】@RequestHeader String  header  -->  application/json
【7】@CookieValue String  token  -->  123456
【8】@Value String  home2  -->  C:\Program Files\Java\jdk1.8.0_60
【9】HttpServletRequest  request  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c45d770

ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}

21.7 @ModelAttribute

@ModelAttribute 需要使用到 ServletModelAttributeMethodProcessor 参数解析器。构造时需要传入一个布尔类型的值。为 false 时,表示 @ModelAttribute 是必须的。

针对 @ModelAttribute("abc") User user1User user2 两种参数来说,尽管后者没有使用 @ModelAttribute 注解,但它们使用的是同一种解析器。

添加两个 ServletModelAttributeMethodProcessor 参数解析器,先解析带 @ModelAttribute 注解的参数,再解析不带 @ModelAttribute 注解的参数。

通过 ServletModelAttributeMethodProcessor 解析得到的数据还会被存入 ModelAndViewContainer 中。存储的数据结构是一个 Map,其 key 为 @ModelAttribute 注解指定的 value 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。

1
2
3
4
5
6
7
8
9
static class Controller {
public void test(
// 指定 value
@ModelAttribute("abc") User user1, // name=zhang&age=18
User user2, // name=zhang&age=18
@RequestBody User user3 // json
) {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new ServletModelAttributeMethodProcessor(true)
);

// --snip--

if (composite.supportsParameter(parameter)) {
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(paramInfo + " -> " + v);
// 打印模型数据
ModelMap modelMap = container.getModel();
if (MapUtils.isNotEmpty(modelMap)) {
System.out.println("模型数据: " + modelMap);
}
} else {
System.out.println(paramInfo);
}
}
}

【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int  id  -->  123
【6】@RequestHeader String  header  -->  application/json
【7】@CookieValue String  token  -->  123456
【8】@Value String  home2  -->  C:\Program Files\Java\jdk1.8.0_60
【9】HttpServletRequest  request  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c45d770
【10】@ModelAttribute User  user1  -->  A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
【11】User  user2  -->  A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
22:58:09.903 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Read "application/json" to [A21.User(name=李四, age=18)]
【12】@RequestBody User  user3  -->  A21.User(name=李四, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}  

@RequestBody User user3 参数也被 ServletModelAttributeMethodProcessor 解析了,如果想使其数据通过 JSON 数据转换而来,则需要使用另一个参数解析器。

21.8 @RequestBody

@RequestBody 注解的解析需要使用到 RequestResponseBodyMethodProcessor 参数解析器。构造时需要传入一个消息转换器列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)
);

// --snip--
}
}
【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int  id  -->  123
【6】@RequestHeader String  header  -->  application/json
【7】@CookieValue String  token  -->  123456
【8】@Value String  home2  -->  C:\Program Files\Java\jdk1.8.0_60
【9】HttpServletRequest  request  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c45d770
【10】@ModelAttribute User  user1  -->  A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
【11】User  user2  -->  A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
23:10:33.282 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Read "application/json" to [A21.User(name=李四, age=18)]
【12】@RequestBody User  user3  -->  A21.User(name=李四, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}  

@RequestBody User user3 参数数据通过 JSON 数据得到,与上一节的解析进行区分。

除此之外,添加的参数解析器的顺序也影响着解析结果

1
2
3
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)

先添加解析 @ModelAttribute 的解析器,再添加解析 @RequestBody 的解析器,最后添加解析省略了 @ModelAttribute 的解析器。如果更换最后两个解析器的顺序,那么 @RequestBody User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor

因此 String name2 参数也能通过添加同种参数但不同构造参数的解析器进行解析,注意添加的解析器的顺序,先处理对象,再处理单个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String[] args) throws Exception {
// --snip--

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(beanFactory, true)
);

// --snip--
}
}
【0】@RequestParam String  name1  -->  zhangsan
【1】String  name2  -->  lisi
【2】@RequestParam int  age  -->  18
【3】@RequestParam String  home1  -->  C:\Program Files\Java\jdk1.8.0_60
【4】@RequestParam MultipartFile  file  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@327af41b
【5】@PathVariable int  id  -->  123
【6】@RequestHeader String  header  -->  application/json
【7】@CookieValue String  token  -->  123456
【8】@Value String  home2  -->  C:\Program Files\Java\jdk1.8.0_60
【9】HttpServletRequest  request  -->  org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c45d770
【10】@ModelAttribute User  user1  -->  A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
【11】User  user2  -->  A21.User(name=张三, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
23:10:33.282 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Read "application/json" to [A21.User(name=李四, age=18)]
【12】@RequestBody User  user3  -->  A21.User(name=李四, age=18)
模型数据为: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}  

22. 获取参数名

在项目的src目录外创建Bean2.java文件,使其不会被 IDEA 自动编译

1
2
3
4
5
public class Bean2 {
public void foo(String name, int age) {

}
}

将命令行切换到 Bean2.java 文件所在目录的位置,执行 javac .\Bean2.java 命令手动编译 Bean2.java。查看 Bean2.class 文件的内容:

1
2
3
4
5
6
7
public class Bean2 {
public Bean2() {
}

public void foo(String var1, int var2) {
}
}

编译生成的 class 文件中的 foo() 方法的参数名称不再是 nameage,也就是说直接使用 javac 命令进行编译得到的字节码文件不会保存方法的参数名称。

执行javac -parameters .\Bean2.java 再次编译 Bean2.java,并查看得到的 Bean2.class 文件内容:

1
2
3
4
5
6
7
public class Bean2 {
public Bean2() {
}

public void foo(String name, int age) {
}
}

foo()方法的参数名称得以保留。

使用 javap -c -v .\Bean2.class 命令反编译Bean2.classfoo()方法的反编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
  public void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 4: 0
MethodParameters:
Name Flags
name
age
}

foo()方法的参数信息被保存在 MethodParameters 中,可以使用 反射 获取:

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
// 反射获取参数名
Method foo = Bean2.class.getMethod("foo", String.class, int.class);
for (Parameter parameter : foo.getParameters()) {
System.out.println(parameter.getName());
}
}
name
age  

使用javac -g .\Bean2.java命令进行编译也会保留方法的参数信息。再次使用javap 反编译 Bean2.classfoo()方法的反编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  public void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LBean2;
0 1 1 name Ljava/lang/String;
0 1 2 age I
}

foo() 方法的参数信息被保存在LocalVariableTable 中,不能使用反射获取,但可以使用 ASM 获取,使用 Spring 封装的解析工具:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
// 反射获取参数名
Method foo = Bean2.class.getMethod("foo", String.class, int.class);

// 基于 LocalVariableTable 本地变量表获取
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(foo);
System.out.println(Arrays.toString(parameterNames));
}
[name, age]  

在【21. 参数解析器】中并没有使用 LocalVariableTableParameterNameDiscoverer,而是使用的是 DefaultParameterNameDiscovererDefaultParameterNameDiscoverer 将两种实现进行了统一:

1
2
3
4
5
6
7
8
9
10
11
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {

public DefaultParameterNameDiscoverer() {
if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}

}

javac -g 的局限性

假设有这样一个接口:

1
2
3
public interface Bean1 {
public void foo(String name, int age);
}

如果使用 javac -g .\Bean1.java 命令进行编译后,再利用javap 查看 foo()方法的反编译结果:

1
2
3
public abstract void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC, ACC_ABSTRACT

并没有记录抽象方法 foo() 的参数信息。

使用 javac -parameters .\Bean1.java

1
2
3
4
5
6
7
public abstract void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC, ACC_ABSTRACT
MethodParameters:
Name Flags
name
age

参数信息得以保留。

23. 对象绑定与类型转换

23.1 三种转换接口

底层第一套转换接口与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

image-20250217215251806

  • Printer 把其它类型转为 String
  • ParserString 转为其它类型
  • Formatter 综合 PrinterParser 功能
  • Converter 把任意类型S 转为任意类型T
  • PrinterParserConverter 经过适配转换成 GenericConverter放入Converters 集合
  • FormattingConversionService利用其他接口实现转换

底层第二套转换接口

由 JDK 提供,而不是 Spring。

1
2
3
4
5
6
classDiagram

PropertyEditorRegistry o-- "多" PropertyEditor

<<interface>> PropertyEditorRegistry
<<interface>> PropertyEditor

image-20250217215400549

  • PropertyEditorString 与其它类型相互转换
  • PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
  • 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配

高层接口与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

image-20250217215442036

  • 它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到TypeConverterDelegate委派ConversionServicePropertyEditorRegistry真正执行转换(Facade 门面模式)
    • 首先查看是否存在实现了 PropertyEditorRegistry 的自定义转换器,@InitBinder 注解实现的就是自定义转换器 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
    • 再看有没有 ConversionService转换
    • 再利用默认的 PropertyEditor转换
    • 最后有一些特殊处理
  • SimpleTypeConverter 仅做类型转换
  • BeanWrapperImpl 利用 Property,即 Getter/Setter,为 Bean 的属性赋值,必要时进行类型转换
  • DirectFieldAccessor 利用Field,即字段(成员变量),为 Bean 的字段赋值,必要时进行类型转换
  • ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property还是 Field,具备校验与获取校验结果功能

23.2 使用示例

SimpleTypeConverter

1
2
3
4
5
6
7
8
9
public class TestSimpleConverter {
public static void main(String[] args) {
SimpleTypeConverter converter = new SimpleTypeConverter();
Integer number = converter.convertIfNecessary("13", int.class);
System.out.println(number);
Date date = converter.convertIfNecessary("1999/03/04", Date.class);
System.out.println(date);
}
}
13
Thu Mar 04 00:00:00 CST 1999  

BeanWrapperImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestBeanWrapper {
public static void main(String[] args) {
// 利用反射为 bean 的属性赋值
MyBean bean = new MyBean();
BeanWrapperImpl wrapper = new BeanWrapperImpl(bean);
wrapper.setPropertyValue("a", "10");
wrapper.setPropertyValue("b", "hello");
wrapper.setPropertyValue("c", "1999/03/04");
System.out.println(bean);
}

@Getter
@Setter
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}

}
TestBeanWrapper.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)  

DirectFieldAccessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestFieldAccessor {
public static void main(String[] args) {
// 利用反射为 bean 的字段赋值
MyBean bean = new MyBean();
DirectFieldAccessor accessor = new DirectFieldAccessor(bean);
accessor.setPropertyValue("a", "10");
accessor.setPropertyValue("b", "hello");
accessor.setPropertyValue("c", "1999/03/04");
System.out.println(bean);
}

@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}

}
TestFieldAccessor.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)  

DataBinder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestDataBinder {
public static void main(String[] args) {
// 执行数据绑定
MyBean bean = new MyBean();
DataBinder binder = new DataBinder(bean);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("a", "10");
pvs.add("b", "hello");
pvs.add("c", "1999/03/04");
binder.bind(pvs);
System.out.println(bean);
}

@Getter
@Setter
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}

}
TestDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)  

如果 MyBean 没有提供 Getter/Setter方法,可以调用 DataBinderinitDirectFieldAccess()方法使数据绑定逻辑走字段赋值,而不是属性赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestDataBinder {
public static void main(String[] args) {
// 执行数据绑定
MyBean bean = new MyBean();
DataBinder binder = new DataBinder(bean);
binder.initDirectFieldAccess();
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("a", "10");
pvs.add("b", "hello");
pvs.add("c", "1999/03/04");
binder.bind(pvs);
System.out.println(bean);
}

@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
}
TestDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)  

Web 环境下的数据绑定

Web 环境下的数据绑定需要使用 DataBinder 的子类 ServletRequestDataBinder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
// web 环境下的数据绑定
MyBean bean = new MyBean();
DataBinder dataBinder = new ServletRequestDataBinder(bean);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("a", "10");
request.setParameter("b", "hello");
request.setParameter("c", "1999/03/04");

dataBinder.bind(new ServletRequestParameterPropertyValues(request));

System.out.println(bean);
}

@Getter
@Setter
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestServletDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)  

23.3 绑定器工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
@Setter
@ToString
public static class User {
private Date birthday;
private Address address;
}

@Getter
@Setter
@ToString
public static class Address {
private String name;
}

在 Web 环境下进行数据绑定:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "北京");

User user = new User();
ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(user);

dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);
}
TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=北京))  

birthday 绑定失败,要想使其绑定成功,需要自定义转换器,有两种方式:

  • 使用 Spring 提供的 ConversionService
  • 使用 JDK 提供的 PropertyEditorRegistry

创建 DataBinder 的职责交由DataBinderFactory 完成,以便添加各种选项,拓展不同的自定义转换器。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args)throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "北京");

User user = new User();
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), user, "user");

dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);
}

运行 main() 方法后,控制台输出的结果不变。

利用 @InitBinder 自定义转换器

声明一个 Controller 类,其中包含一个被 @InitBinder 注解标记的方法:

1
2
3
4
5
6
7
static class MyController {
@InitBinder
public void myMethod(WebDataBinder dataBinder) {
// 拓展 dataBinder 的转换器
dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 进行拓展"));
}
}

WebDataBinder 作为方法参数,在方法体中添加自定义转换器MyDateFormatter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
public class MyDateFormatter implements Formatter<Date> {
private final String desc;

public MyDateFormatter(String desc) {
// 仅做测试
this.desc = desc;
}

@Override
public String print(Date date, Locale locale) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
return sdf.format(date);
}

@Override
public Date parse(String text, Locale locale) throws ParseException {
log.debug(">>>>>> 进入了: {}", desc);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
return sdf.parse(text);
}
}

在构造DataBinderFactory时传入InvocableHandlerMethod列表,列表中包含根据 Controller 对象、Controller 类中被 @InitBinder 注解标记的方法对象构造的InvocableHandlerMethod对象,DataBinderFactory就知道有哪些InitBinder方法,在createBinder时,回调每一个InitBinder,传递dataBinder,自由进行拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TestServletDataBinderFactory {
public static void main(String[] args) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "北京");

User user = new User();
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("myMethod", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null);

WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), user, "user");

dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);
}

@Getter
@Setter
@ToString
public static class User {
private Date birthday;
private Address address;
}

@Getter
@Setter
@ToString
public static class Address {
private String name;
}

static class MyController {
@InitBinder
public void myMethod(WebDataBinder dataBinder) {
// 拓展 dataBinder 的转换器
dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 进行拓展"));
}
}
}

再次执行main(),birthday 被成功绑定:

com.itheima.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=北京))  

这种方式使用了 JDK 提供的PropertyEditorRegistry,证据就在 WebDataBinderaddCustomFormatter() 方法中:

1
2
3
4
public void addCustomFormatter(Formatter<?> formatter) {
FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter);
getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter);
}

ConversionService 拓展

选择 FormattingConversionService 作为 ConversionService 的实现,向其中添加自定义转换器MyDateFormatter

构造 DataBinderFactory 时传入 WebBindingInitializer 的实现,因此将FormattingConversionService 封装成 ConfigurableWebBindingInitializer传入 DataBinderFactory 的构造方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "北京");

User user = new User();
//“用 ConversionService 转换”
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式拓展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);

WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), user, "user");

dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);
}
com.itheima.a23.MyDateFormatter - >>>>>> 进入了: 用 ConversionService 方式拓展转换功能
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=北京))  

如果同时存在 @InitBinderConversionService,将以 @InitBinder 为主,@InitBinder 实现的转换器属于自定义转换器,自定义转换器的优先级更高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "北京");

User user = new User();
//“用 @InitBinder 转换”
InvocableHandlerMethod method =
new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("myMethod", WebDataBinder.class));
//“用 ConversionService 转换”
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式拓展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory =
new ServletRequestDataBinderFactory(List.of(method), initializer);

WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), user, "user");

dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);
}
com.itheima.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=北京))  

默认的 ConversionService

ConversionService 有一个默认实现 DefaultFormattingConversionService,它还是 FormattingConversionService 的子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "北京");

User user = new User();

//"使用 默认ConversionService 转换"
DefaultFormattingConversionService service = new DefaultFormattingConversionService();
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory =
new ServletRequestDataBinderFactory(null, initializer);

WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), user, "user");

dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);
}
TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=北京))  

birthday绑定失败,默认的 ConversionService需要搭配注解 @DateTimeFormat 使用。在目标类的字段上使用该注解标记,并指定被转换的日期格式:

1
2
3
4
5
6
7
8
@Getter
@Setter
@ToString
public static class User {
@DateTimeFormat(pattern = "yyyy|MM|dd")
private Date birthday;
private Address address;
}
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=北京))  

SpringBoot 中还提供了 ApplicationConversionService,它也是 FormattingConversionService的子类,上述代码将 DefaultFormattingConversionService换成 ApplicationConversionService也能达到相同效果。

23.4 Spring 的泛型操作技巧

有一基类 BaseDao,接收一个泛型参数:

1
2
3
4
5
public class BaseDao<T> {
T findOne() {
return null;
}
}

围绕 BaseDao 有如下五个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EmployeeDao extends BaseDao {
}

public class Student {
}

public class StudentDao extends BaseDao<Student> {
}

public class Teacher {
}

public class TeacherDao extends BaseDao<Teacher> {
}

尝试获取 BaseDao子类泛型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) {
// 1. java api
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
// 带有泛型信息的父类信息
Type teacherDaoType = TeacherDao.class.getGenericSuperclass();
System.out.println("TeacherDao type: " + teacherDaoType);
System.out.println("TeacherDao type class: " + teacherDaoType.getClass());

Type employeeDaoType = EmployeeDao.class.getGenericSuperclass();
System.out.println("EmployeeDao type: " + employeeDaoType);
System.out.println("EmployeeDao type class: " + employeeDaoType.getClass());

// 有泛型参数的 Type 对象才是 ParameterizedType 类型
if (teacherDaoType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) teacherDaoType;
System.out.println(parameterizedType.getActualTypeArguments()[0]);
}

// 2. spring api 1
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class);
System.out.println(t);

// 3. spring api 2
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
System.out.println(ResolvableType.forClass(StudentDao.class).getSuperType().getGeneric().resolve());
}
TeacherDao type: com.itheima.a23.BaseDao
TeacherDao type class: class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
EmployeeDao type: class com.itheima.a23.BaseDao
EmployeeDao type class: class java.lang.Class
class com.itheima.a23.Teacher
>>>>>>>>>>>>>>>>>>>>>>>
class com.itheima.a23.Teacher
>>>>>>>>>>>>>>>>>>>>>>>
class com.itheima.a23.Student  

24. ControllerAdivice 之 @InitBinder

准备 @InitBinder在整个 HandlerAdapter 调用过程中所处的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:

image-20250218220052516

  • RequestMappingHandlerAdapter在图中缩写为 HandlerAdapter
  • HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
  • HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers

功能与使用

@InitBinder 注解只能作用在方法上,通常搭配@ControllerAdvice@Controller 以及他们的衍生注解使用。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@InitBinder
public void binder3(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
}
}

@Controller
static class Controller1 {
@InitBinder
public void binder1(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
}

public void foo() {
}
}

@Controller
static class Controller2 {
@InitBinder
public void binder21(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder21 转换器"));
}

@InitBinder
public void binder22(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder22 转换器"));
}

public void bar() {
}
}
}

@InitBinder 加在被 @ControllerAdvice 标记的类中,是对 所有 控制器都生效的自定义类型转换器。当加在被@Controller 标记的类中,是只对当前控制器生效的自定义类型转换器。

@InitBinder 的来源有两个:

  1. @ControllerAdvice标记的类中@InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录
  2. @Controller 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在控制器方法首次执行时解析并记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();

handlerAdapter.setApplicationContext(context);
handlerAdapter.afterPropertiesSet();

log.debug("1. 刚开始...");
showBindMethods(handlerAdapter);

context.close();
}

@SuppressWarnings("all")
private static void showBindMethods(RequestMappingHandlerAdapter handlerAdapter) throws NoSuchFieldException, IllegalAccessException {
Field initBinderAdviceCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderAdviceCache");
initBinderAdviceCache.setAccessible(true);
Map<ControllerAdviceBean, Set<Method>> globalMap = (Map<ControllerAdviceBean, Set<Method>>) initBinderAdviceCache.get(handlerAdapter);
log.debug("全局的 @InitBinder 方法 {}",
globalMap.values().stream()
.flatMap(ms -> ms.stream().map(m -> m.getName()))
.collect(Collectors.toList())
);

Field initBinderCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderCache");
initBinderCache.setAccessible(true);
Map<Class<?>, Set<Method>> controllerMap = (Map<Class<?>, Set<Method>>) initBinderCache.get(handlerAdapter);
log.debug("控制器的 @InitBinder 方法 {}",
controllerMap.entrySet().stream()
.flatMap(e -> e.getValue().stream().map(v -> e.getKey().getSimpleName() + "." + v.getName()))
.collect(Collectors.toList())
);
}
com.itheima.a24.A24 - 1. 刚开始...
com.itheima.a24.A24 - 全局的 @InitBinder 方法 [binder3]
com.itheima.a24.A24 - 控制器的 @InitBinder 方法 []  

全局的@InitBinder 方法被解析并记录,但控制器中被 @InitBinder 标记的方法并没有被解析记录。

模拟调用控制器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();

handlerAdapter.setApplicationContext(context);
handlerAdapter.afterPropertiesSet();

log.info("1. 刚开始...");
showBindMethods(handlerAdapter);

Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
getDataBinderFactory.setAccessible(true);
log.info("2. 模拟调用 Controller1 的 foo 方法...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo")));
showBindMethods(handlerAdapter);

log.info("3. 模拟调用 Controller2 的 bar 方法时...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller2(), WebConfig.Controller2.class.getMethod("bar")));
showBindMethods(handlerAdapter);

context.close();
}
com.itheima.a24.A24 - 1. 刚开始...
com.itheima.a24.A24 - 全局的 @InitBinder 方法 [binder3]
com.itheima.a24.A24 - 控制器的 @InitBinder 方法 []
com.itheima.a24.A24 - 2. 模拟调用 Controller1 的 foo 方法...
com.itheima.a24.A24 - 全局的 @InitBinder 方法 [binder3]
com.itheima.a24.A24 - 控制器的 @InitBinder 方法 [Controller1.binder1]
com.itheima.a24.A24 - 3. 模拟调用 Controller2 的 bar 方法时...
com.itheima.a24.A24 - 全局的 @InitBinder 方法 [binder3]
com.itheima.a24.A24 - 控制器的 @InitBinder 方法 [Controller2.binder22, Controller2.binder21, Controller1.binder1]  

首次调用控制器中的方法时,控制器中被 @InitBinder 标记方法被解析记录。

25. 控制器方法执行流程

ServletInvocableHandlerMethod 的组成

1
2
3
4
5
6
7
8
9
10
11
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

image-20250218220643424

HandlerMethod需要:

  • bean 即是哪个 Controller
  • method 即是 Controller 中的哪个方法

ServletInvocableHandlerMethod 需要:

  • WebDataBinderFactory 负责对象绑定、类型转换
  • ParameterNameDiscoverer 负责参数名解析
  • HandlerMethodArgumentResolverComposite 负责解析参数
  • HandlerMethodReturnValueHandlerComposite 负责处理返回值

控制器方法执行流程

RequestMappingHandlerAdapter 为起点,创建 WebDataBinderFactory,添加自定义类型转换器,再创建 ModelFactory,添加Model 数据:

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
participant adapter as RequestMappingHandlerAdapter
participant bf as WebDataBinderFactory
participant mf as ModelFactory
participant container as ModelAndViewContainer
adapter ->> +bf: 初始化 advice: 解析@InitBinder
bf -->> -adapter: 添加自定义类型转换器
adapter ->> +mf: 初始化 advice: 解析@ModelAttribute
mf ->> +container: 添加 Model 数据
container -->> -mf:
mf -->> -adapter:

image-20250218220717334

接下来调用 ServletInvocableHandlerMethod,主要完成三件事:

  1. 准备参数
  2. 反射调用控制器方法
  3. 处理返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sequenceDiagram
participant adapter as RequestMappingHandlerAdapter
participant ihm as ServletInvocableHandlerMethod
participant ar as HandlerMethodArgumentResolverComposite
participant rh as HandlerMethodReturnValueHandlerComposite
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:

image-20250218220752158

代码演示

提供配置类 WebConfig,其中包含一个控制器 Controller1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class WebConfig {
@Controller
static class Controller1 {
@ResponseStatus(HttpStatus.OK)
public ModelAndView foo(User user) {
// 使用 @ResponseStatus 注解,暂不考虑返回值的处理
System.out.println("foo");
return null;
}
}

@Getter
@Setter
@ToString
static class User {
private String name;
}
}

创建 Spring 容器,Mock 请求,创建 HandlerMethod 对象指定需要执行的控制器方法,创建 DataBinderFactory数据绑定工厂。向创建的 HandlerMethod对象中添加数据绑定工厂、参数名称解析器、参数解析器(暂不考虑返回值的处理),最后创建模型视图容器,调用 HandlerMethodinvokeAndHandle 方法执行控制器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);

MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "lxd");

ServletInvocableHandlerMethod handlerMethod =
new ServletInvocableHandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo", WebConfig.User.class));
ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory(null, null);

handlerMethod.setDataBinderFactory(binderFactory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
// 暂不考虑返回值的处理

ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());

context.close();
}
//组装一系列参数解析器
public static HandlerMethodArgumentResolverComposite getArgumentResolvers(AnnotationConfigApplicationContext context) {
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
new PathVariableMethodArgumentResolver(),
new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ServletRequestMethodArgumentResolver(),
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
);
return composite;
}

foo
{user=WebConfig.User(name=lxd), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}  

26. ControllerAdvice 之 @ModelAttribute

准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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(100, 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:

image-20250218220847784

功能与使用

@ModelAttribute可以作用在参数上和方法上。

当其作用在参数上时,由 ServletModelAttributeMethodProcessor 解析。由解析器调用对象的构造方法,创建空对象,调用数据绑定工厂,将空对象与请求中的请求参数绑定,得到的结果作为模型数据添加到ModelAndViewContainer中,当未指定 @ModelAttribute 的 value 时,添加到 ModelAndViewContainer 中的 key 是对象类型首字母小写对应的字符串。

当其作用在方法上时:

  • 如果该方法加在由 @Controller 注解标记的类中,会针对当前控制器中每个方法调用时补充模型数据。@ModelAttribute 标记的方法,如果该方法有返回值,自动将返回值添加到 ModelAndViewContainer 中。当未指定 @ModelAttributevalue 时,添加到 ModelAndViewContainer 中的 key 是返回值类型首字母小写对应的字符串。
  • 如果该方法在被 @ControllerAdvice 注解标记的类中,所有控制器方法调用时都会加入的模型数据。
    作用在方法上的 @ModelAttribute 注解由 RequestMappingHandlerAdapter 解析。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
static class Controller1 {
@ResponseStatus(HttpStatus.OK)
public ModelAndView foo(@ModelAttribute("u") User user) {
// 使用 @ResponseStatus 注解,暂不考虑返回值的处理
System.out.println("foo");
return null;
}
}

@ControllerAdvice
static class MyControllerAdvice {
@ModelAttribute("a")
public String aa() {
return "aa";
}
}

先不使用 RequestMappingHandlerAdapter 对作用在方法上的 @ModelAttribute 注解进行解析,沿用【25. 控制器方法执行流程】中的 main() 方法:

1
2
foo
{u=WebConfig.User(name=lxd), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

再解析方法上的 @ModelAttribute 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);

RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();

MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "lxd");
/*
现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起, 并完成控制器方法的调用, 如下
*/
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
new Controller1(), Controller1.class.getMethod("foo", User.class));

ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);

handlerMethod.setDataBinderFactory(factory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
// 暂不考虑返回值的处理

ModelAndViewContainer container = new ModelAndViewContainer();

// 获取模型工厂方法
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);

// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);

handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);

System.out.println(container.getModel());

context.close();
}
foo
{a=aa, user=WebConfig.User(name=lxd), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}  

27. 返回值处理器

含有多种返回值的控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  @Slf4j
static class Controller {
public ModelAndView test1() {
log.info("test1()");
ModelAndView mav = new ModelAndView("view1");
mav.addObject("name", "张三");
return mav;
}

public String test2() {
log.info("test2()");
return "view2";
}

@ModelAttribute
// @RequestMapping("/test3")
public User test3() {
log.info("test3()");
return new User("李四", 20);
}

public User test4() {
log.info("test4()");
return new User("王五", 30);
}

public HttpEntity<User> test5() {
log.info("test5()");
return new HttpEntity<>(new User("赵六", 40));
}

public HttpHeaders test6() {
log.info("test6()");
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/html");
return headers;
}

@ResponseBody
public User test7() {
log.info("test7()");
return new User("钱七", 50);
}
}
@Getter
@Setter
@ToString
@AllArgsConstructor
public static class User {
private String name;
private int age;
}

测试渲染视图需要用到的配置

为测试对视图的渲染,采用 Freemarker 进行测试,先导入 Freemarker 依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

Freemarker 配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Configuration
public class WebConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setDefaultEncoding("utf-8");
configurer.setTemplateLoaderPath("classpath:templates");
return configurer;
}

/**
* FreeMarkerView 在借助 Spring 初始化时,会要求在 web 环境才会走 setConfiguration, 这里想办法去掉了 web 环境的约束
*/
@Bean
public FreeMarkerViewResolver viewResolver(FreeMarkerConfigurer configurer) {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver() {
@Override
protected AbstractUrlBasedView instantiateView() {
FreeMarkerView view = new FreeMarkerView() {
@Override
protected boolean isContextRequired() {
return false;
}
};
view.setConfiguration(configurer.getConfiguration());
return view;
}
};
resolver.setContentType("text/html;charset=utf-8");
resolver.setPrefix("/");
resolver.setSuffix(".ftl");
resolver.setExposeSpringMacroHelpers(false);
return resolver;
}
}

渲染视图使用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
@SuppressWarnings("all")
private static void renderView(ApplicationContext context, ModelAndViewContainer container,
ServletWebRequest webRequest) throws Exception {
log.debug(">>>>>> 渲染视图");
FreeMarkerViewResolver resolver = context.getBean(FreeMarkerViewResolver.class);
String viewName = container.getViewName() != null ? container.getViewName() : new DefaultRequestToViewNameTranslator().getViewName(webRequest.getRequest());
log.debug("没有获取到视图名, 采用默认视图名: {}", viewName);
// 每次渲染时, 会产生新的视图对象, 它并非被 Spring 所管理, 但确实借助了 Spring 容器来执行初始化
View view = resolver.resolveViewName(viewName, Locale.getDefault());
view.render(container.getModel(), webRequest.getRequest(), webRequest.getResponse());
System.out.println(new String(((MockHttpServletResponse) webRequest.getResponse()).getContentAsByteArray(), StandardCharsets.UTF_8));
}

提供构造 HandlerMethodReturnValueHandlerComposite 对象的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public static HandlerMethodReturnValueHandlerComposite getReturnValueHandler() {
HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
composite.addHandlers(Arrays.asList(
new ModelAndViewMethodReturnValueHandler(),
new ViewNameMethodReturnValueHandler(),
new ServletModelAttributeMethodProcessor(false),
new HttpEntityMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new HttpHeadersReturnValueHandler(),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)
));
return composite;
}

测试返回值处理器的方法 testReturnValueProcessor()

利用两个函数式接口 Comsumer,对 Mock 的请求进行补充,或者在请求处理完毕后,输出 Mock 的响应信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@SneakyThrows
private static void testReturnValueProcessor(ApplicationContext context, String methodName,
Consumer<MockHttpServletRequest> requestConsumer,
Consumer<MockHttpServletResponse> responseConsumer) {
Method method = Controller.class.getMethod(methodName);
Controller controller = new Controller();
Object returnValue = method.invoke(controller);

HandlerMethod handlerMethod = new HandlerMethod(context, method);
ModelAndViewContainer container = new ModelAndViewContainer();

HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
MethodParameter returnType = handlerMethod.getReturnType();
MockHttpServletRequest request = new MockHttpServletRequest();
Optional.ofNullable(requestConsumer).ifPresent(i -> i.accept(request));
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
if (composite.supportsReturnType(returnType)) {
composite.handleReturnValue(returnValue, returnType, container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
if (!container.isRequestHandled()) {
// 渲染视图
renderView(context, container, webRequest);
} else {
Optional.ofNullable(responseConsumer).ifPresent(i -> i.accept(response));
}
}
}

27.1 ModelAndView

ModelAndView类型的返回值由 ModelAndViewMethodReturnValueHandler处理,构造时无需传入任何参数。

解析 ModelAndView 时,将其中的视图和模型数据分别提取出来,放入 ModelAndViewContainer中,之后根据视图信息找到对应的模板页面,再将模型数据填充到模板页面中,完成视图的渲染。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
test1(context);
}

private static void test1(ApplicationContext context) {
testReturnValueProcessor(context, "test1", null, null);
}

对应的模板页面 view1.ftl

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>view1</title>
</head>
<body>
<h1>Hello! ${name}</h1>
</body>
</html>
20:40:56.969 [main] INFO com.itheima.a27.A27\$Controller - test1()
{name=张三}
view1
20:40:57.212 [main] INFO com.itheima.a27.A27 - \>>>>>> 渲染视图
20:40:57.212 [main] INFO com.itheima.a27.A27 - 没有获取到视图名, 采用默认视图名: view1
20:40:57.275 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - View name 'view1', model {name=张三}
20:40:57.280 [main] DEBUG com.itheima.a27.WebConfig\$1$1 - Rendering [/view1.ftl]  
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>view1</title>
</head>
<body>
<h1>Hello! 张三</h1>
</body>
</html>

27.2 字符串类型

控制器方法的返回值是字符串类型时,返回的字符串即为视图的名称。与 ModelAndView类型的返回值相比,不包含模型数据。

此种类型的返回值由 ViewNameMethodReturnValueHandler处理,构造时无需传入任何参数

1
2
3
private static void test2(ApplicationContext context) {
testReturnValueProcessor(context, "test2", null, null);
}

对应的模板页面 view2.ftl

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>view2</title>
</head>
<body>
<h1>Hello!</h1>
</body>
</html>
20:45:09.540 [main] INFO com.itheima.a27.A27\$Controller - test2()
{}
view2
20:45:09.797 [main] INFO com.itheima.a27.A27 - \>>>>>> 渲染视图
20:45:09.798 [main] INFO com.itheima.a27.A27 - 没有获取到视图名, 采用默认视图名: view2
20:45:09.856 [main] DEBUG com.itheima.a27.WebConfig\$1$1 - View name 'view2', model {}
20:45:09.861 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - Rendering [/view2.ftl]  
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>view2</title>
</head>
<body>
<h1>Hello!</h1>
</body>
</html>

27.3 @ModelAttribute

@ModelAttribute 的用法在【26. ControllerAdvice 之 @ModelAttribute】中已经介绍过,简单来说,当 @ModelAttribute 注解作用在方法上时,会将方法的返回值作为模型数据添加到 ModelAndViewContainer中。

@ModelAttribute 标记的方法的返回值由 ServletModelAttributeMethodProcessor解析,构造时需要传入一个布尔类型数据 annotationNotRequired,表示 @ModelAttribute 注解是否不是必须的。

模型数据已经有了,但视图名称又是什么呢?

在实际开发场景中,控制器方法需要被 @RequestMapping 标记,并指定请求地址,比如:

1
2
3
4
5
6
@ModelAttribute
@RequestMapping("/test3")
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}

当未找到视图名称时,默认以请求路径作为视图名称。

但在本节测试中省略了路径映射这一步,因此需要通过编程的方式将请求路径解析后的结果放入 request 作用域中。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static Consumer<MockHttpServletRequest> mockHttpServletRequestConsumer(String methodName) {
return req -> {
req.setRequestURI("/" + methodName);
UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(req);
};
}

private static void test3(ApplicationContext context) {
String methodName = "test3";
testReturnValueProcessor(context, methodName,
mockHttpServletRequestConsumer(methodName), null);
}

对应的模板页面test3.ftl

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>test3</title>
</head>
<body>
<h1>Hello! ${user.name} ${user.age}</h1>
</body>
</html>
20:48:56.651 [main] INFO com.itheima.a27.A27\$Controller - test3()
{user=A27.User(name=李四, age=20)}
null
20:48:56.908 [main] INFO com.itheima.a27.A27 - \>>>>>> 渲染视图
20:48:56.911 [main] INFO com.itheima.a27.A27 - 没有获取到视图名, 采用默认视图名: test3
20:48:56.967 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - View name 'test3', model {user=A27.User(name=李四, age=20)}
20:48:56.973 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - Rendering [/test3.ftl]  
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>test3</title>
</head>
<body>
<h1>Hello! 李四 20</h1>
</body>
</html>

针对控制器方法 test4() 也可以按照相同方式测试:

1
2
3
4
private static void test4(ApplicationContext context) {
String methodName = "test4";
testReturnValueProcessor(context, methodName, mockHttpServletRequestConsumer(methodName), null);
}

对应的模板页面 test4.ftl

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>test4</title>
</head>
<body>
<h1>Hello! ${user.name} ${user.age}</h1>
</body>
</html>
20:50:20.654 [main] INFO com.itheima.a27.A27\$Controller - test4()
{user=A27.User(name=王五, age=30)}
null
20:50:20.907 [main] INFO com.itheima.a27.A27 - \>>>>>> 渲染视图
20:50:20.909 [main] INFO com.itheima.a27.A27 - 没有获取到视图名, 采用默认视图名: test4
20:50:20.976 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - View name 'test4', model {user=A27.User(name=王五, age=30)}
20:50:20.982 [main] DEBUG com.itheima.a27.WebConfig\$1\$1 - Rendering [/test4.ftl]  
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>test4</title>
</head>
<body>
<h1>Hello! 王五 30</h1>
</body>
</html>

与解析参数类似,返回值处理器的执行顺序也有严格要求。

27.4 HttpEntity

HttpEntity类型的返回值由 HttpEntityMethodProcessor处理,构造时需要传入一个消息转换器列表。

这种类型的返回值表示响应完成,无需经过视图的解析、渲染流程再生成响应。可在处理器的handleReturnValue()方法中得以论证:

1
2
3
4
5
6
7
8
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 一进入方法就设置请求处理完毕
mavContainer.setRequestHandled(true);

// --snip--
}

HttpEntity中包含了状态码、响应体信息和响应头信息。

尝试在请求处理完毕后,输出响应体信息:

1
2
3
4
5
6
7
8
9
10
private static final Consumer<MockHttpServletResponse> RESPONSE_CONSUMER = resp -> {
for (String name : resp.getHeaderNames()) {
System.out.println(name + " = " + resp.getHeader(name));
}
System.out.println(new String(resp.getContentAsByteArray(), StandardCharsets.UTF_8));
};

private static void test5(ApplicationContext context) {
testReturnValueProcessor(context, "test5", null, RESPONSE_CONSUMER);
}
com.itheima.a27.A27\$Controller - test5()
{}
null
Content-Type = application/json
{"name":"赵六","age":40}  

27.5 HttpHeaders

HttpEntity相比,HttpHeaders只包含响应头信息,HttpHeaders类型的返回值由 HttpHeadersReturnValueHandler处理,构造时无需传入任何参数。

HttpEntity一样,这种类型的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。

1
2
3
private static void test6(ApplicationContext context) {
testReturnValueProcessor(context, "test6", null, RESPONSE_CONSUMER);
}
20:53:54.546 [main] INFO com.itheima.a27.A27$Controller - test6()
{}
null
Content-Type:text/html  

27.6 @ResponseBody

@ResponseBody 标记的方法的返回值由 RequestResponseBodyMethodProcessor处理,构造时需要传入一个消息转换器列表。

这样的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue()方法中得以论证(省略源码)。

1
2
3
private static void test7(ApplicationContext context) {
testReturnValueProcessor(context, "test7", null, RESPONSE_CONSUMER);
}
com.itheima.a27.A27$Controller - test7()
{}
null
Content-Type:application/json
{"name":"钱七","age":50}  

28. 消息转换器

在构造参数解析器 RequestResponseBodyMethodProcessor、返回值解析器 HttpEntityMethodProcessorRequestResponseBodyMethodProcessor时,都需要传入消息转换器列表。

消息转换器的基类是 HttpMessageConverter

介绍两个常见的消息转换器的实现:

  • MappingJackson2HttpMessageConverter
  • MappingJackson2XmlHttpMessageConverter

定义User

1
2
3
4
5
6
7
8
9
10
11
12
13
@Getter
@Setter
@ToString
public static class User {
private String name;
private int age;

@JsonCreator
public User(@JsonProperty("name") String name, @JsonProperty("age") int age) {
this.name = name;
this.age = age;
}
}

User对象转换成 JSON 格式的数据:

1
2
3
4
5
6
7
8
9
10
@SneakyThrows
public static void test1() {
MockHttpOutputMessage message = new MockHttpOutputMessage();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 判断能否将对象转换成目标消息格式
if (converter.canWrite(User.class, MediaType.APPLICATION_JSON)) {
converter.write(new User("张三", 18), MediaType.APPLICATION_JSON, message);
System.out.println(message.getBodyAsString());
}
}

{“name”:“张三”,“age”:18}

User对象转换成 XML格式的数据:

1
2
3
4
5
6
7
8
9
@SneakyThrows
public static void test2() {
MockHttpOutputMessage message = new MockHttpOutputMessage();
MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
if (converter.canWrite(User.class, MediaType.APPLICATION_XML)) {
converter.write(new User("李四", 20), MediaType.APPLICATION_XML, message);
System.out.println(message.getBodyAsString());
}
}

使用 MappingJackson2XmlHttpMessageConverter时,需要额外导入依赖:

1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
1
<User><name>李四</name><age>20</age></User>

JSON格式的数据转换成 User对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void test3() {
//language=JSON
String json = "{\n" +
" \"name\": \"李四\",\n" +
" \"age\": 20\n" +
"}";
MockHttpInputMessage message = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
if (converter.canRead(User.class, MediaType.APPLICATION_JSON)) {
Object read = converter.read(User.class, message);
System.out.println(read);
}
}
A28.User(name=李四, age=20)  

如果存在多个消息转换器呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@SneakyThrows
public static void test4() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);

//request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE);
//response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE);

RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2HttpMessageConverter(),
new MappingJackson2XmlHttpMessageConverter()
));
processor.handleReturnValue(
new User("张三", 18),
new MethodParameter(A28.class.getMethod("user"), -1),
new ModelAndViewContainer(),
webRequest
);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

@ResponseBody
public User user () {
return null;
}

将以添加的消息转换器顺序为主,比如此处会将 User对象转换成 JSON格式的数据:

1
{ "name": "张三", "age": 18 }

调换添加的消息转换器顺序:

1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
public static void test4() {
// --snip--

RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2XmlHttpMessageConverter(),
new MappingJackson2HttpMessageConverter()
));

// --snip--
}

这下会将 User对象转换成 XML格式的数据:

1
<User><name>张三</name><age>18</age></User>

再将添加的消息转换器顺序还原,在请求头中添加 Accept信息,指定数据格式为 XML

1
2
3
4
5
6
7
8
9
10
11
12
@SneakyThrows
public static void test4() {
// --snip--

request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE);
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2HttpMessageConverter(),
new MappingJackson2XmlHttpMessageConverter()
));

// --snip--
}

尽管转换成 JSON 的转换器在前,但会以请求头中指定的 Accept信息为主:

1
<User><name>张三</name><age>18</age></User>

在上文基础上,指定响应的 Content-Typeapplication/json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void test4() {
// --snip--

request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE);
response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE);

RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2HttpMessageConverter(),
new MappingJackson2XmlHttpMessageConverter()
));

// --snip--
}

此时又会以 Content-Type 的信息为主:

1
{ "name": "张三", "age": 18 }

总结

@ResponseBody 注解由 RequestResponseBodyMethodProcessor解析,但涉及到的数据格式转换由返回值处理器调用消息转换器完成。

当存在多个消息转换器时,如果选择 MediaType

  • 首先看 @RequestMapping注解的 produces为主,相当于设置了响应的 Content-Type,比如:
1
2
3
4
5
@ResponseBody
@RequestMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
public User user () {
return null;
}
  • 再看请求头中的 Accept 是否指定了目标格式
  • 最后按照消息转换器的添加顺序进行转换

29. ControllerAdvice 之 ResponseBodyAdvice

ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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(20, 130, 25)
rh ->> rh: 有的处理器涉及 ResponseBodyAdvice
end
rh ->> +container: 添加 Model 数据,处理视图名,是否渲染等
container -->> -rh:
rh -->> -ihm:
ihm -->> -adapter:
adapter ->> +container: 获取 ModelAndView
container -->> -adapter:

image-20250220222146269

ResponseBodyAdvice是一个接口,对于实现了这个接口并被 @ControllerAdvice 标记的类来说,能够在调用每个控制器方法返回结果前,调用重写的 ResponseBodyAdvice接口中的 beforeBodyWrite() 方法对返回值进行增强。

现有一个控制器类与内部使用到的 User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public static class MyController {
@ResponseBody
public User user() {
return new User("王五", 18);
}
}

@Getter
@Setter
@ToString
@AllArgsConstructor
public static class User {
private String name;
private int age;
}

调用控制器方法,并输出响应数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class A29 {

// {"name":"王五","age":18}
// {"code":xx, "msg":xx, data: {"name":"王五","age":18} }
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);

ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
context.getBean(WebConfig.MyController.class),
WebConfig.MyController.class.getMethod("user")
);
handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory(Collections.emptyList(), null));
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
handlerMethod.setHandlerMethodReturnValueHandlers(getReturnValueHandlers(context));

MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request, response), container);

System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));

}

public static HandlerMethodArgumentResolverComposite getArgumentResolvers(AnnotationConfigApplicationContext context) {
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
new PathVariableMethodArgumentResolver(),
new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
new ServletRequestMethodArgumentResolver(),
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
);
return composite;
}

public static HandlerMethodReturnValueHandlerComposite getReturnValueHandlers(AnnotationConfigApplicationContext context) {
HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
// 省略其他返回值处理器的添加
composite.addHandler(new RequestResponseBodyMethodProcessor(
Collections.singletonList(new MappingJackson2HttpMessageConverter())
));
composite.addHandler(new ServletModelAttributeMethodProcessor(true));
return composite;
}

}

{“name”:“王五”,“age”:18}

在实际开发场景中常常需要对返回的数据类型进行统一,比如都返回 Result类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
private int code;
private String msg;
private Object data;

@JsonCreator
private Result(@JsonProperty("code") int code, @JsonProperty("data") Object data) {
this.code = code;
this.data = data;
}

private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}

public static Result ok() {
return new Result(200, null);
}

public static Result ok(Object data) {
return new Result(200, data);
}

public static Result error(String msg) {
return new Result(500, "服务器内部错误:" + msg);
}
}

除了直接让控制器方法返回 Result外,还可以使用 ResponseBodyAdvice进行增强:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
public class WebConfig {

@ControllerAdvice
static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
/*
* 满足条件才转换
* 1. 控制器方法被 @ResponseBody 注解标记
* 2. 控制器方法所在类被 @ResponseBody 注解或被包含 @ResponseBody 注解的注解标记
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
// returnType.getContainingClass().isAnnotationPresent(ResponseBody.class)) {
return true;
}
return false;
}

// 将 User 或其它类型统一为 Result 类型
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.ok(body);
}
}
}

进行上述增强后,再运行 main() 方法, 输出结果不变, 这是因为没有将实现的 ResponseBodyAdvice 添加到返回值处理器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static HandlerMethodReturnValueHandlerComposite getReturnValueHandlers(AnnotationConfigApplicationContext context) {
// 添加 advice
List<ControllerAdviceBean> annotatedBeans = ControllerAdviceBean.findAnnotatedBeans(context);
List<Object> responseBodyAdviceList = annotatedBeans.stream()
.filter(b -> b.getBeanType() != null
&& ResponseBodyAdvice.class.isAssignableFrom(b.getBeanType()))
.collect(Collectors.toList());

HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
// 省略其他返回值处理器的添加
composite.addHandler(new RequestResponseBodyMethodProcessor(
Collections.singletonList(new MappingJackson2HttpMessageConverter()),
responseBodyAdviceList
));
composite.addHandler(new ServletModelAttributeMethodProcessor(true));
return composite;
}

再次运行 main() 方法,控制台输出:

{“code”:200,“data”:{“name”:“王五”,“age”:18}}

如果将控制器方法修改成以下形式,也能输出相同的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
@ResponseBody
public static class MyController {
public User user() {
return new User("王五", 18);
}
}
// 或者
@RestController
public static class MyController {
public User user() {
return new User("王五", 18);
}
}

30. 异常处理

DispatcherServlet中对异常处理的核心方法是 processHandlerException(),在这个方法中会对所有异常解析器进行遍历,然后使用每个异常解析器对异常信息进行处理。

存放异常解析器的是 DispatcherServlet中泛型为 HandlerExceptionResolver、名为 handlerExceptionResolvers的列表成员变量。

HandlerExceptionResolver 是一个接口,本节讲解解析 @ExceptionHandler 注解的异常解析器 ExceptionHandlerExceptionResolver

四个控制器类,测试异常处理方法被 @ResponseBody 注解标记、异常处理方法返回 ModelAndView、嵌套异常和对异常处理方法的参数处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static class Controller1 {
public void foo() {
}

@ResponseBody
@ExceptionHandler
public Map<String, Object> handle(ArithmeticException e) {
return Collections.singletonMap("error", e.getMessage());
}
}

static class Controller2 {
public void foo() {
}

@ExceptionHandler
public ModelAndView handler(ArithmeticException e) {
return new ModelAndView("test2", Collections.singletonMap("error", e.getMessage()));
}
}

static class Controller3 {
public void foo() {
}

@ResponseBody
@ExceptionHandler
public Map<String, Object> handle(IOException e) {
return Collections.singletonMap("error", e.getMessage());
}
}

static class Controller4 {
public void foo() {}

@ExceptionHandler
@ResponseBody
public Map<String, Object> handle(Exception e, HttpServletRequest request) {
System.out.println(request);
return Collections.singletonMap("error", e.getMessage());
}
}

根据handlerMethod得知异常是在哪个类中触发,在类中是否存在标注@ExceptionHandler的方法,将方法参数类型与实际异常类型匹配,匹配成功,则进行异常处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void main(String[] args) throws Exception {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
//添加默认的参数解析器和返回值处理器
resolver.afterPropertiesSet();
//测试Json
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception e = new ArithmeticException("/ zero");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
System.out.println("-----------------------------------------------------------");
//测试mav
handlerMethod = new HandlerMethod(new Controller2(), Controller2.class.getMethod("foo"));
ModelAndView modelAndView = resolver.resolveException(request, response, handlerMethod, e);
System.out.println(modelAndView.getModel());
System.out.println(modelAndView.getViewName());
System.out.println("-----------------------------------------------------------");
//测试嵌套异常
e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
System.out.println("-----------------------------------------------------------");
//异常处理方法参数解析
e = new Exception("e1");
handlerMethod = new HandlerMethod(new Controller4(), Controller4.class.getMethod("foo"));
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
{"error":"/ zero"}
-----------------------------------------------------------
{error=/ zero}
test2
-----------------------------------------------------------
{"error":"/ zero"}{"error":"e3"}
-----------------------------------------------------------
org.springframework.mock.web.MockHttpServletRequest@298a5e20
{"error":"/ zero"}{"error":"e3"}{"error":"e1"}  

31. ControllerAdvice 之 @ExceptionHandler

控制器中被 @ExceptionHandler 标记的异常处理方法只会在当前控制器中生效,如果想要某个异常处理方法全局生效,则需要将异常处理方法编写在被 @ControllerAdvice 注解标记的类中。

一个“朴素”的控制器类:

1
2
3
4
static class Controller1 {
public void foo() {
}
}

当不存在任何异常处理方法时,调用控制器中的foo() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void main(String[] args) {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();

ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(Collections.singletonList(new MappingJackson2XmlHttpMessageConverter()));
resolver.afterPropertiesSet();

HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception exception = new Exception("e1");
resolver.resolveException(request, response, handlerMethod, exception);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

main() 方法运行后,控制台不输出任何信息。

编写配置类,向 Spring 容器中添加 ExceptionHandlerExceptionResolver,并声明全局异常处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@ExceptionHandler
@ResponseBody
public Map<String, Object> handle(Exception e) {
return Collections.singletonMap("error", e.getMessage());
}
}

@Bean
public ExceptionHandlerExceptionResolver resolver() {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
// 无需调用 resolver.afterPropertiesSet()方法,这是 Spring 的提供的内置拓展,会在 Spring 生命周期中自动执行
return resolver;
}
}

ExceptionHandlerExceptionResolver不再直接 new ,而是从 Spring 容器中获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SneakyThrows
public static void main(String[] args) {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
ExceptionHandlerExceptionResolver resolver = context.getBean(ExceptionHandlerExceptionResolver.class);

HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception exception = new Exception("e1");
resolver.resolveException(request, response, handlerMethod, exception);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
{"error":"e1"}  

ExceptionHandlerExceptionResolver 触发时机

ExceptionHandlerExceptionResolver是在什么时机下找到ControllerAdvice中的异常处理方法呢?

ExceptionHandlerExceptionResolverafterPropertiesSet方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void afterPropertiesSet() {
this.initExceptionHandlerAdviceCache();
this.initMessageConverters();
List handlers;
if (this.argumentResolvers == null) {
//获取默认的参数解析器
handlers = this.getDefaultArgumentResolvers();
this.argumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
}

if (this.returnValueHandlers == null) {
//获取默认的返回值处理器
handlers = this.getDefaultReturnValueHandlers();
this.returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite()).addHandlers(handlers);
}

}

进入initExceptionHandlerAdviceCache()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void initExceptionHandlerAdviceCache() {
if (this.getApplicationContext() != null) {
//获取到整个容器中所有ControllerAdvice相关bean
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this.getApplicationContext());
Iterator var2 = adviceBeans.iterator();
//遍历处理每个ControllerAdvice相关bean
while(var2.hasNext()) {
ControllerAdviceBean adviceBean = (ControllerAdviceBean)var2.next();
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
//处理过程中记录ExceptionHandler标注的方法,如果对应Controller没有处理异常的方法,可从exceptionHandlerAdviceCache获取
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
//针对响应体增强
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}

if (this.logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
this.logger.debug("ControllerAdvice beans: none");
} else {
this.logger.debug("ControllerAdvice beans: " + handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
}
}

}
}

ExceptionHandler类似的还有前面介绍的RequestMappingHandlerAdapter

RequestMappingHandlerAdapterafterPropertiesSet()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void afterPropertiesSet() {
this.initControllerAdviceCache();
this.initMessageConverters();
List handlers;
if (this.argumentResolvers == null) {
handlers = this.getDefaultArgumentResolvers();
this.argumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
}

if (this.initBinderArgumentResolvers == null) {
handlers = this.getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
}

if (this.returnValueHandlers == null) {
handlers = this.getDefaultReturnValueHandlers();
this.returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite()).addHandlers(handlers);
}

}

进入initControllerAdviceCache()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private void initControllerAdviceCache() {
if (this.getApplicationContext() != null) {
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(this.getApplicationContext());
List<Object> requestResponseBodyAdviceBeans = new ArrayList();
Iterator var3 = adviceBeans.iterator();
//同上
while(var3.hasNext()) {
ControllerAdviceBean adviceBean = (ControllerAdviceBean)var3.next();
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}

Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}

Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(adviceBean, binderMethods);
}

if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}

if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}

if (this.logger.isDebugEnabled()) {
int modelSize = this.modelAttributeAdviceCache.size();
int binderSize = this.initBinderAdviceCache.size();
int reqCount = this.getBodyAdviceCount(RequestBodyAdvice.class);
int resCount = this.getBodyAdviceCount(ResponseBodyAdvice.class);
if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {
this.logger.debug("ControllerAdvice beans: none");
} else {
this.logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize + " @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");
}
}

}
}

32. Tomcat 异常处理

可以使用@ExceptionHandler@ControllerAdvice全局对控制器方法中抛出的异常进行处理,但针对诸如 filter等,非控制器方法中的异常就变得无能为力了。

因此需要一个更上层的“异常处理者”,这个“异常处理者”就是 Tomcat 服务器。

32.1 Tomcat 的错误页处理

首先将“老三样”利用配置类添加到 Spring 容器中,还要将 RequestMappingHandlerMappingRequestMappingHandlerAdapter也添加到 Spring 容器中。

必要的控制器也不能少,控制器方法手动制造异常,但不提供使用 @ExceptionHandler实现的异常处理方法,将产生的异常交由 Tomcat 处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}

@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}

@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
// 解析 @RequestMapping
return new RequestMappingHandlerMapping();
}

@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
// 注意默认的 RequestMappingHandlerAdapter 不会带 jackson 转换器
handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
return handlerAdapter;
}

@Controller
public static class MyController {
@RequestMapping("test")
public ModelAndView test() {
int i = 1 / 0;
return null;
}
}
}

利用 AnnotationConfigServletWebServerApplicationContext创建 Spring Web 容器,并输出所有的路径映射信息:

1
2
3
4
5
6
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
handlerMapping.getHandlerMethods().forEach((k, v) -> System.out.println("映射路径: " + k + "\t方法信息: " + v));
}
映射路径: { [/test]}	方法信息: com.itheima.a32.WebConfig$MyController#test()  

在浏览器中访问 http://localhost:8080/test地址:

Tomcat错误处理页

显示 Tomcat 的错误处理页,并在页面中输出了错误信息。

Tomcat 默认提供的错误处理方式返回的是 HTML 格式的数据,但需要返回 JSON 格式的数据又该怎么自定义呢?

修改 Tomcat 默认的错误处理路径,并添加后置处理器进行注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 修改了 Tomcat 服务器默认错误地址
*/
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
/*
* ErrorPageRegistrar 由 SpringBoot 提供,TomcatServletWebServerFactory 也实现了该接口
* 出现错误,会使用请求转发 forward 跳转到 error 地址
*/
return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}

@Bean
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
/*
* 在 TomcatServletWebServerFactory 初始化完成前,获取容器中所有的 ErrorPageRegistrar
* 并将这些 ErrorPageRegistrar 进行注册
*/
return new ErrorPageRegistrarBeanPostProcessor();
}

重启程序,再次在浏览器中访问 http://localhost:8080/test,此时页面上不再显示 Tomcat 的默认错误处理页,而是产生了 404错误。

这是因为整个程序中并没有名称为 error的页面,或者为/error 的请求路径。在控制器中添加请求路径为 /error的控制器方法,该方法被 @ResponseBody 标记,最终返回 JSON 格式的数据:

1
2
3
4
5
6
7
@RequestMapping("/error")
@ResponseBody
public Map<String, Object> error(HttpServletRequest request) {
// tomcat 会将异常对象存储到 request 作用域中,可以直接获取
Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
return Collections.singletonMap("error", e.getMessage());
}

再次重启程序,控制台输出的路径映射信息多了一条:

映射路径: { [/error]}	方法信息: com.itheima.a32.WebConfig\$MyController#error(HttpServletRequest)
映射路径: { [/test]}	方法信息: com.itheima.a32.WebConfig$MyController#test()  

在浏览器中访问 http://localhost:8080/test

image-20250222102655295

32.2 BasicErrorController

BasicErrorController是由 SpringBoot 提供的类,它也是一个控制器:

1
2
3
4
5
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// --snip--
}

它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /error

向容器中添加 BasicErrorController,构造 BasicErrorController时需要传递两个参数:

  • errorAttributes:需要显示的错误信息对象
  • errorProperties:读取配置文件中的键值信息,定制处理
1
2
3
4
@Bean
public BasicErrorController basicErrorController() {
return new BasicErrorController(new DefaultErrorAttributes(), new ErrorProperties());
}

移除前文添加的error()控制器方法。

再次重启程序,控制台输出的路径映射信息为:

映射路径: { [/error]}	方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
映射路径: { [/test]}	方法信息: com.itheima.a32.WebConfig$MyController#test()
映射路径: { [/error], produces [text/html]}	方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)  

路径映射信息多了两条,它们的请求路径一样,但根据不同的请求来源返回不同格式的数据。

使用接口测试工具访问

如果采用 Postman 等接口测试工具访问 http://localhost:8080/test 路径时,将返回 JSON 格式的数据,比如

1
2
3
4
5
6
{
"timestamp": 1737641294401,
"status": 500,
"error": "Internal Server Error",
"path": "/test"
}

timestampstatus等响应内容就是错误属性 errorAttributes的中包含的内容。

返回的数据中并没有显示异常信息,可以通过配置文件进行配置:

1
server.error.include-exception=true

也可以在添加 BasicErrorController到 Spring 容器中时,设置错误属性 errorProperties

1
2
3
4
5
6
@Bean
public BasicErrorController basicErrorController() {
ErrorProperties errorProperties = new ErrorProperties();
errorProperties.setIncludeException(true);
return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}

重启程序,再次使用接口测试工具访问 http://localhost:8080/test

1
2
3
4
5
6
7
{
"timestamp": 1737641411625,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.ArithmeticException",
"path": "/test"
}

使用浏览器访问

如果使用浏览器访问 http://localhost:8080/test,又会回到“解放前”,显示与 Tomcat 的默认错误处理页相同的内容。

这是因为使用浏览器访问时,将调用 BasicErrorController中的 errorHtml() 控制器方法:

1
2
3
4
5
6
7
8
9
10
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

该方法返回 ModelAndView,并且在没有添加新的错误视图的情况下,尝试寻找视图名称为 error的视图。

这里既没有添加新的错误视图,也没有名称为 error 的视图,因此最终又会交由 Tomcat 进行处理。

尝试向 Spring 容器中添加一个 View视图,Bean 的名字 必须error

1
2
3
4
5
6
7
8
@Bean
public View error() {
return (model, request, response) -> {
System.out.println(model);
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("<h3>服务器内部错误</h3>");
};
}

为了能够在查找指定名称的视图时,按照 View 类型的 Bean 的名称进行匹配,还需要添加一个解析器:

1
2
3
4
5
@Bean
public ViewResolver viewResolver() {
// View 类型的 Bean 的名称即为视图名称
return new BeanNameViewResolver();
}

重启程序,使用浏览器访问 http://localhost:8080/test

image-20250222102924231

控制台还打印出:

{timestamp=Thu Jan 23 22:14:25 CST 2025, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, path=/test}  

33. BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter

33.1 功能与使用

BeanNameUrlHandlerMappingRequestMappingHandlerMapping类似,也是用于解析请求路径,只不过 BeanNameUrlHandlerMapping将根据请求路径在 Spring 容器中寻找同名的 Bean,对请求进行处理,这个 Bean 必须 以/开头。比如:请求路径为 /c1,寻找的 Bean 的名称也是/c1

SimpleControllerHandlerAdapterRequestMappingHandlerAdapter也类似,也是用于调用控制器方法,但要求控制器类必须实现 org.springframework.web.servlet.mvc.Controller 接口。

现有三个控制器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component("/c1")
public static class Controller1 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c1");
return null;
}
}

@Component("/c2")
public static class Controller2 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c2");
return null;
}
}

@Bean("/c3")
public Controller controller3() {
return (request, response) -> {
response.getWriter().print("this is c3");
return null;
};
}

提供配置类 WebConfig,添加 Web 换件下必要的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
public class WebConfig {

//内嵌 web 容器工厂
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}

//创建 DispatcherServlet
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

//注册 DispatcherServlet,SpringMVC 的入口
@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}

@Bean
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
return new BeanNameUrlHandlerMapping();
}

@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
}
1
2
3
4
5
6
7
public class A33 {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
}

运行 main()方法后,在浏览器中访问 http://localhost:8080/c1,页面上显示 this is c1。更换请求路径为 c2c3后,也会出现类似的信息。

33.2 自定义实现

在配置类 WebConfig中移除 Spring 提供的 BeanNameUrlHandlerMappingSimpleControllerHandlerAdapter,手动编码实现它们的功能。

为了与前文的测试形成对比,将 Controller2的 Bean 名称设置为 c2,而不是 /c2,使其不能被解析到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Component
static class MyHandlerMapping implements HandlerMapping {

@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
String key = request.getRequestURI();
Controller controller = controllerMap.get(key);
if (controller == null) {
return null;
}
return new HandlerExecutionChain(controller);
}

@Autowired
private ApplicationContext context;

private Map<String, Controller> controllerMap;

@PostConstruct
public void init() {
controllerMap = context.getBeansOfType(Controller.class).entrySet().stream()
.filter(i -> i.getKey().startsWith("/"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}

@Component
static class MyHandlerAdapter implements HandlerAdapter {

@Override
public boolean supports(Object handler) {
return handler instanceof Controller;
}

@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof Controller) {
((Controller) handler).handleRequest(request, response);
}
return null;
}

@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
}

运行 main() 方法后,在浏览器中访问:

http://localhost:8080/c1,页面上显示 this is c1
http://localhost:8080/c2,页面上显示 404
http://localhost:8080/c3,页面上显示 this is c3

34. RouterFunctionMapping 与 HandlerFunctionAdapter

RouterFunctionMapping在初始化时,在 Spring 容器中收集所有 RouterFunctionRouterFunction包括两部分:

  1. RequestPredicate:设置映射条件
  2. HandlerFunction:处理逻辑

当请求到达时,根据映射条件找到 HandlerFunction,即 handler,然后使用 HandlerFunctionAdapter调用 handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}

@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}

@Bean
public RouterFunctionMapping routerFunctionMapping() {
return new RouterFunctionMapping();
}

@Bean
public HandlerFunctionAdapter handlerFunctionAdapter() {
return new HandlerFunctionAdapter();
}

@Bean
public RouterFunction<ServerResponse> r1() {
return route(GET("/r1"), req -> ok().body("this is r1"));
}

@Bean
public RouterFunction<ServerResponse> r2() {
return route(GET("/r2"), req -> ok().body("this is r2"));
}
}
1
2
3
4
5
6
public class A34 {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
}

运行 main() 方法后,在浏览器中访问 http://localhost:8080/r1,页面上显示 this is r1,访问 r2 时也类似。

35. SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter

35.1 功能与使用

概括一下,这两个主要用于静态资源处理,SimpleUrlHandlerMapping用于静态资源映射,而静态资源处理器是 ResourceHttpRequestHandlerHttpRequestHandlerAdapter用于处理器调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}

@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}

@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
// 设置静态资源处理器,得到所有映射关系
Map<String, ResourceHttpRequestHandler> map = context.getBeansOfType(ResourceHttpRequestHandler.class);
mapping.setUrlMap(map);
return mapping;
}

@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
return new HttpRequestHandlerAdapter();
}

@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
// 以 / 结尾表示目录,否则认为是文件
handler.setLocations(Collections.singletonList(new ClassPathResource("static/")));
return handler;
}

@Bean("/img/**")
public ResourceHttpRequestHandler handler2() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(Collections.singletonList(new ClassPathResource("images/")));
return handler;
}
}

添加的两个 ResourceHttpRequestHandler类型的 Bean,分别设置了它们处理 ClassPath 路径下哪个目录下的静态资源,那如何将请求路径与静态资源访问路径进行映射呢?

也就是说,当要访问 ClassPath 路径下的 static目录下的静态资源时,应该通过哪个请求路径呢?

可以利用通配符设置添加的 ResourceHttpRequestHandler类型的 Bean 的名称。

比如设置 Bean 的名称为 /**,那么在访问 localhost:8080/r1.html时,就会尝试访问 ClassPath 路径下 static目录中名为 r1.html 的静态资源;又比如设置 Bean 的名称为 /img/**,那么在访问 localhost:8080/img/1.jpg 时, 就会尝试访问 ClassPath 路径下 images目录中名为 1.jpg的静态资源。

35.2 资源解析器

ResourceHttpRequestHandler用于对静态资源进行处理,但静态资源解析的功能是由 ResourceResolver完成的。

ResourceHttpRequestHandler实现了 InitializingBean接口,查看重写的 afterPropertiesSet()

1
2
3
4
5
6
7
8
9
@Override
public void afterPropertiesSet() throws Exception {
resolveResourceLocations();

if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}
// --snip--
}

当使用的资源解析器列表为空时,默认添加最基本的资源解析器 PathResourceResolver

尝试添加额外的资源解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
// 以 / 结尾表示目录,否则认为是文件
handler.setLocations(Collections.singletonList(new ClassPathResource("static/")));
// 不使用默认的资源解析器,而是使用自行添加的
handler.setResourceResolvers(List.of(
// 读取资源时使用缓存
new CachingResourceResolver(new ConcurrentMapCache("cache1")),
// 读取压缩资源
new EncodedResourceResolver(),
// 最基本的:从磁盘上读取静态资源
new PathResourceResolver()
));
return handler;
}

添加了三个资源解析器:

  1. CachingResourceResolver:对静态资源进行缓存
  2. EncodedResourceResolver:对静态资源进行压缩
  3. PathResourceResolver:最基本的资源处理器

还要注意添加的顺序,先尝试从缓存中获取,再尝试获取压缩文件,最后才是直接从磁盘上读取。

针对 EncodedResourceResolver来说,Spring 不会自行对静态资源进行压缩,需要在配置类中提供压缩方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostConstruct
@SuppressWarnings("all")
public void initGzip() throws IOException {
Resource resource = new ClassPathResource("static");
File dir = resource.getFile();
for (File file : dir.listFiles(pathname -> pathname.getName().endsWith(".html"))) {
System.out.println(file);
try (FileInputStream fis = new FileInputStream(file);
GZIPOutputStream fos = new GZIPOutputStream(new FileOutputStream(file.getAbsoluteFile() + ".gz"))) {
byte[] bytes = new byte[8 * 1024];
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
}
}
}

配置类对应的 Bean 初始化阶段时,将 ClassPath 路径下 static目录中的静态资源进行压缩。

比如 static目录下的 r1.html会被压缩成 r1.html.gz,在访问 r1.html 时,会访问压缩文件 r1.html.gz,由浏览器识别并解压成 r1.html 进行访问,减少网络传输数据量。

35.3 欢迎页处理

将访问 根路径 的请求,映射到某一欢迎页。这个功能由 WelcomePageHandlerMapping 完成。

设置静态资源欢迎页为 ClassPath 下 static目录中的 index.html 文件:

1
2
3
4
5
6
7
8
9
10
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
Resource resource = context.getResource("classpath:static/index.html");
return new WelcomePageHandlerMapping(
null,
context,
resource,
"/**"
);
}

程序会根据配置的欢迎页映射器生成一个实现了 Controller接口的处理器,使用 SimpleControllerHandlerAdapter执行生成的处理器:

1
2
3
4
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}

重启程序,控制台会输出一条如下的日志,表示欢迎页配置成功:

o.s.b.a.w.s.WelcomePageHandlerMapping - Adding welcome page: class path resource [static/index.html]  

如果重启程序后访问 localhost:8080 并没有跳转到配置的欢迎页,可以重新编译项目后在运行。

总结

WelcomePageHandlerMapping作为欢迎页映射器,只将根路径,即 / 映射到配置的欢迎页。

它内置了一个处理器,名为 ParameterizableViewController,该处理器不执行逻辑,仅根据固定的视图名 forward:index.html 去寻找视图。

SimpleControllerHandlerAdapter用于调用处理器,根据重定向到根路径的 index.html 页面,执行静态资源处理器,访问 static目录下的 index.html 文件(在配置类中自行配置的)。

35.4 映射器与适配器总结

HandlerMapping 用于建立请求路径与控制器之间的映射关系:

  • RequestMappingHandlerMapping:解析 @RequestMapping及其派生注解,建立请求路径与控制器方法之间的映射关系
  • WelcomePageHandlerMapping:映射 /根路径,寻找欢迎页
  • BeanNameUrlHandlerMapping:与 Bean 的名称进行匹配,要求名称必须以 /开头
  • RouterFunctionMapping:将 RequestPredicate映射到 HandlerFunction
  • SimpleUrlHandlerMapping:静态资源映射

映射器之间的顺序也是有要求的,SpringBoot 中的映射器按上述顺序排序。

HandlerAdapter用于对各种处理器进行适配调用(适配器 模式):

  • RequestMappingHandlerAdapter:执行被 @RequestMapping 标记的控制器方法,内部还会使用参数解析器、返回值处理器对控制器方法的参数、返回值进行处理(组合 模式)
  • SimpleControllerHandlerAdapter:执行实现了 Controller接口的处理器
  • HandlerFunctionAdapter:处理 HandlerFunction函数式接口
  • HttpRequestHandlerAdapter:处理 HttpRequestHandler接口,用于静态资源处理

ResourceHttpRequestHandler 中的 setResourceResolvers() 方法是 责任链 模式体现

36. MVC 处理流程

当浏览器发送一个请求 http://localhost:8080/hello 后,请求到达服务器,其处理流程是:

服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术

  • 路径:默认映射路径为 /,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器
    • jsp 不会匹配到 DispatcherServlet
    • 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
  • 创建:在 SpringBoot 中,由 DispatcherServletAutoConfiguration这个自动配置类提供 DispatcherServlet的 bean
  • 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
    • HandlerMapping,初始化时记录映射关系
    • HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
    • HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
    • ViewResolver

DispatcherServlet会利用 RequestMappingHandlerMapping查找控制器方法

  • 例如根据 /hello 路径找到 @RequestMapping("/hello")对应的控制器方法

  • 控制器方法会被封装为 HandlerMethod对象,并结合匹配到的拦截器一起返回给 DispatcherServlet

  • HandlerMethod和拦截器合在一起称为 HandlerExecutionChain(调用链)对象

DispatcherServlet接下来会:

  1. 调用拦截器的 preHandle方法,返回一个布尔类型的值。若返回 true,则放行,进行后续调用,反之拦截请求,不进行后续调用;
  2. RequestMappingHandlerAdapter调用处理器方法,准备数据绑定工厂、模型工厂、将 HandlerMethod完善为 ServletInvocableHandlerMethod
    • @ControllerAdvice 全局增强点 1️⃣:利用 @ModelAttribute 补充模型数据
    • @ControllerAdvice 全局增强点 2️⃣:利用 @InitBinder 补充自定义类型转换器
    • 使用 HandlerMethodArgumentResolver准备参数
      • @ControllerAdvice 全局增强点 3️⃣:利用 RequestBodyAdvice 接口对请求体增强
    • 调用 ServletInvocableHandlerMethod
    • 使用 HandlerMethodReturnValueHandler处理返回值
    • 根据 ModelAndViewContainer获取 ModelAndView
      • 如果返回的 ModelAndView为 null,不走第 4 步视图解析及渲染流程
        • 例如,有的返回值处理器调用了 HttpMessageConverter来将结果转换为 JSON,这时 ModelAndView就为 null
      • 如果返回的 ModelAndView不为 null,会在第 4 步走视图解析及渲染流程
      • @ControllerAdvice 全局增强点 4️⃣:利用 ResponseBodyAdvice对响应体增强
  3. 调用拦截器的 postHandle方法
  4. 处理异常或视图渲染
    • 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver处理异常流程
      • @ControllerAdvice 全局增强点 5️⃣:利用 @ExceptionHandler 进行统一异常处理
    • 正常,走视图解析及渲染流程
  5. 调用拦截器的 afterCompletion()方法

37. SpringBoot 骨架项目

使用 IDEA 创建 SpringBoot 项目时,会创建出 .mvn 目录、HELP.mdmvnwmvnw.cmd 等不必要的文件。

如果是 Linux 环境下,执行以下命令获取 SpringBoot 的骨架,并添加 webmysqlmybatis 依赖:

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. SpringBoot War 项目

38.1 项目的构建

利用 IDEA 创建新模块 test_war,区别在于选择的打包方式是 War

image-20250224202623172

选择依赖时,勾选 Spring Web。

一般来说,选择 War 作为打包方式都是为了使用 JSP,因为 JSP 不能配合 Jar 打包方式使用。

JSP 文件的存放路径是固定的,在 src/main 目录下的 webapp 目录,如果没有 webapp 目录,需要自行创建。之后新建 hello.jsp

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>Hello!</h3>
</body>
</html>

之后新建控制器类 HelloController,编写控制器方法 hello(),返回值类型是 String,要求返回的是视图名称:

1
2
3
4
5
6
7
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}

最后要在配置文件中配置视图的前缀、后缀,使控制器方法返回的视图名称对应视图名称的 JSP 页面:

1
2
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

38.2 项目的测试

使用外置 Tomcat 测试

首先得安装外置 Tomcat,省略安装步骤。

然后在 IDEA 的 Run/Debug Configurations 中进行配置,选择安装的外置 Tomcat:

配置Tomcat-Server

然后在 Deployment 中指定当前项目的部署方式和应用程序上下文路径:

修改Tomcat-Server的Deployment

尽管使用外置 Tomcat 进行测试,但主启动类不能少:

1
2
3
4
5
6
@SpringBootApplication
public class TestWarApplication {
public static void main(String[] args) {
SpringApplication.run(TestWarApplication.class, args);
}
}

除此之外,还要编写 ServletInitializer,在外置 Tomcat 启动时,找到 SpringBoot 项目的主启动类,执行 SpringBoot 流程:

1
2
3
4
5
6
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(TestWarApplication.class);
}
}

如果没有 ServletInitializer 类,则无法使 SpringBoot 项目使用外置 Tomcat。

运行程序后,访问 localhost:8080/hello,页面进入编写的 hello.jsp 页面。

使用内嵌 Tomcat 测试

打包方式为 Jar 时,直接运行主启动类,然后访问对应的请求路径即可跳转到指定的视图中,那打包访问变成 War 之后,使用这种方式还能够成功跳转吗?

程序运行成功后,访问 localhost:8080/hello,页面并没有按照预期跳转到 hello.jsp 页面中,而是下载了该页面。

这是因为内嵌 Tomcat 中不具备 JSP 解析能力,如果要想使其具备解析 JSP 的能力,需要添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>

之后再访问 localhost:8080/hello,页面进入编写的 hello.jsp 页面。

使用内嵌 Tomcat 测试遇到的问题

  • 添加 tomcat-embed-jasper 依赖后,访问 localhost:8080/hello,仍在下载 hello.jsp

    答:清理浏览器缓存,在浏览器的 DevTools 中的 Network 内 勾选 Disable cache 以禁用缓存。

  • 添加 tomcat-embed-jasper 依赖后,访问 localhost:8080/hello,页面 404。
    答:设置运行主启动类的 Run/Debug Configurations 中的 Working directory 为当前模块所在目录。

参考链接:springboot 在 idea 多模块下子模块的 web 项目用内置 tomcat 启动访问 jsp 报 404

39. SpringBoot 启动过程

39.1 SpringApplication 的构造

SpringBoot 的主启动类

1
2
3
4
5
6
@SpringBootApplication
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}

其中 SpringApplication#run() 方法是核心方法:

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

最终使用 new 关键字构造了 SpringApplication 对象,然后调用了非静态 run() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}

构造 SpringApplication 对象时做了如下几件事:

  1. 获取 Bean Definition
  2. 推断应用类型
  3. 添加 ApplicationContext 初始化器
  4. 添加事件监听器
  5. 主类推断

获取 Bean Definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
public class A39_1 {
public static void main(String[] args) {
SpringApplication spring = new SpringApplication(A39_1.class);

// 创建并初始化 Spring 容器
ConfigurableApplicationContext context = spring.run(args);
Arrays.stream(context.getBeanDefinitionNames()).forEach(i -> {
System.out.println("name: " + i +
" 来源: " + context.getBeanFactory().getBeanDefinition(i).getResourceDescription());
});
context.close();
}

static class Bean1 {
}

static class Bean2 {
}

@Bean
public Bean2 bean2() {
return new Bean2();
}
}

运行 main() 方法后,控制台打印出错误信息:

***************************
APPLICATION FAILED TO START
***************************
Description:
Web application could not be started as there was no org.springframework.boot.web.servlet.server.ServletWebServerFactory bean defined in the context.
Action:
Check your application's dependencies for a supported servlet web server.
Check the configured web application type.

这是因为添加了 spring-boot-starter-web 依赖,但 Spring 容器中并没有 ServletWebServerFactory 类型的 Bean。向容器中添加即可:

1
2
3
4
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}

之后在运行 main() 方法:

name: org.springframework.context.annotation.internalConfigurationAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalAutowiredAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalCommonAnnotationProcessor 来源: null
name: org.springframework.context.event.internalEventListenerProcessor 来源: null
name: org.springframework.context.event.internalEventListenerFactory 来源: null
name: a39_1 来源: null
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean2 来源: com.itheima.a39.A39_1
name: servletWebServerFactory 来源: com.itheima.a39.A39_1

来源为 null 的 Bean 是由 Spring 提供的“内置” Bean。

使用 XML 配置文件添加 Bean,并利用 setSources() 方法设置创建 ApplicationContext的其他源:

1
2
3
4
5
public static void main(String[] args) {
SpringApplication spring = new SpringApplication(A39_1.class);
spring.setSources(Collections.singleton("classpath:b01.xml"));
// --snip--
}

再次运行 main() 方法,控制台打印的内容多了一条:

name: bean1 来源: class path resource [b01.xml]  

推断应用类型

应用类型的推断在构造方法中可以看到:

1
2
3
4
5
6
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 推断应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// --snip--
}

推断逻辑由 WebApplicationType 枚举中的 deduceFromClasspath() 方法完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static WebApplicationType deduceFromClasspath() {
// ClassUtils.isPresent() 判断类路径下是否存在某个类
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
// 响应式 Web 应用
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
// 非 Web 应用
return WebApplicationType.NONE;
}
}
// Web 应用
return WebApplicationType.SERVLET;
}

利用反射调用 deduceFromClasspath() 方法:

1
2
3
4
5
6
7
8
9
10
@SneakyThrows
public static void main(String[] args) {
// --snip--

Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath");
deduceFromClasspath.setAccessible(true);
System.out.println("\t应用类型为: " + deduceFromClasspath.invoke(null));

// --snip--
}
  应用类型为: SERVLET

添加 ApplicationContext 初始化器

调用 SpringApplication 对象的 run() 方法时会创建 ApplicationContext,最后调用 ApplicationContextrefresh() 方法完成初始化。

在创建与初始化完成之间的一些拓展功能就由 ApplicationContext 初始化器完成。

SpringApplication 的构造方法中,添加的初始化器信息从配置文件中读取:

1
2
3
4
5
6
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 从配置文件中读取初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// --snip--
}

也可以调用 SpringApplication 对象的 addInitializers() 方法添加自定义初始化器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SneakyThrows
public static void main(String[] args) {
// --snip--

spring.addInitializers(applicationContext -> {
if (applicationContext instanceof GenericApplicationContext) {
GenericApplicationContext context = (GenericApplicationContext) applicationContext;
context.registerBean("bean3", Bean3.class);
}
});

// 创建并初始化 Spring 容器
ConfigurableApplicationContext context = spring.run(args);
Arrays.stream(context.getBeanDefinitionNames()).forEach(i -> {
System.out.println("name: " + i +
" 来源: " + context.getBeanFactory().getBeanDefinition(i).getResourceDescription());
});
context.close();
}

static class Bean3 {
}

运行 main() 方法后,控制台打印的 Bean 又多了一条:

name: bean3 来源: null  

添加事件监听器

与添加 ApplicationContext 初始化器一样,在 SpringApplication 的构造方法中,添加的事件监听器信息从配置文件中读取:

1
2
3
4
5
6
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 从配置文件中读取事件监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// --snip--
}

可以调用 SpringApplication 对象的 addListeners() 方法添加自定义事件监听器:

1
2
3
4
5
6
7
8
@SneakyThrows
public static void main(String[] args) {
// --snip--
// 输出所有事件信息
spring.addListeners(event -> System.out.println("\t事件为: " + event));
// --snip--
context.close();
}

运行 main() 方法后,控制台打印的事件信息汇总后如下:

事件类型为: class org.springframework.boot.context.event.ApplicationStartingEvent
事件类型为: class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationContextInitializedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationPreparedEvent
事件类型为: class org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
事件类型为: class org.springframework.context.event.ContextRefreshedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationStartedEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.boot.context.event.ApplicationReadyEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.context.event.ContextClosedEvent

主类推断

主类推断在构造方法中可以看到:

1
2
3
4
5
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 主类推断
this.mainApplicationClass = deduceMainApplicationClass();
}

推断逻辑由 deduceMainApplicationClass() 方法完成,利用反射调用该方法:

1
2
3
4
5
6
7
8
9
10
@SneakyThrows
public static void main(String[] args) {
// --snip--

Method deduceMainApplicationClass = SpringApplication.class.getDeclaredMethod("deduceMainApplicationClass");
deduceMainApplicationClass.setAccessible(true);
System.out.println("\t主类是: " + deduceMainApplicationClass.invoke(spring));

// --snip--
}
主类是: class com.itheima.a39.A39_1  

39.2 SpringApplication#run()的分析

第一步:获取 SpringApplicationRunListeners

在执行 run() 方法时,首先会获取到 SpringApplicationRunListeners,它是事件发布器的组合,能够在 SpringBoot 启动的各个阶段中发布事件。

SpringApplicationRunListeners 中使用 SpringApplicationRunListener 来描述单个事件发布器,SpringApplicationRunListener 是一个接口,它有且仅有一个实现类 EventPublishingRunListener

在 SpringBoot 中,事件发布器都是在配置文件中读取,从 META-INF/spring.factories 中读取,该文件中有这样一句:

1
2
3
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

自行实现从 META-INF/spring.factories 配置文件中读取事件发布器信息,并发布各种事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class A39_2 {
@SneakyThrows
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
app.addListeners(i -> System.out.println(i.getClass()));

// 获取事件发送器实现类名
List<String> names = SpringFactoriesLoader.loadFactoryNames(
SpringApplicationRunListener.class,
A39_2.class.getClassLoader()
);
for (String name : names) {
// System.out.println(name);
Class<?> clazz = Class.forName(name);
Constructor<?> constructor = clazz.getConstructor(SpringApplication.class, String[].class);
SpringApplicationRunListener publisher = (SpringApplicationRunListener) constructor.newInstance(app, args);

// 发布事件
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
// spring boot 开始启动
publisher.starting(bootstrapContext);
// 环境信息准备完毕
publisher.environmentPrepared(bootstrapContext, new StandardEnvironment());
// 创建 spring 容器,调用初始化器之后发布此事件
GenericApplicationContext context = new GenericApplicationContext();
publisher.contextPrepared(context);
// 所有 bean definition 加载完毕
publisher.contextLoaded(context);
// spring 容器初始化完毕(调用 refresh() 方法后)
context.refresh();
publisher.started(context, null);
// spring boot 启动完毕
publisher.ready(context, null);

// 启动过程中出现异常,spring boot 启动出错
publisher.failed(context, new Exception("出错了"));
}
}
}

在 SpringBoot 启动过程中,总共发布 7 种事件。

运行 main() 方法后,控制台打印出:

class org.springframework.boot.context.event.ApplicationStartingEvent
class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
class org.springframework.boot.context.event.ApplicationContextInitializedEvent
class org.springframework.boot.context.event.ApplicationPreparedEvent
class org.springframework.context.event.ContextRefreshedEvent
class org.springframework.boot.context.event.ApplicationStartedEvent
class org.springframework.boot.availability.AvailabilityChangeEvent
class org.springframework.boot.context.event.ApplicationReadyEvent
class org.springframework.boot.availability.AvailabilityChangeEvent
class org.springframework.boot.context.event.ApplicationFailedEvent

但打印出的事件种类并不止 7 种,这是因为包含了其他事件发布器发布的事件,EventPublishingRunListener 发布的事件的全限定类名包含 boot.context.event,根据这个条件重新计算,恰好 7 个。

第二步:封装启动 args

调用 DefaultApplicationArguments 的构造方法,传入 args 即可:

1
2
3
4
5
6
7
8
@SneakyThrows
@SuppressWarnings("all")
public static void main(String[] args) {
// --snip--
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args");
DefaultApplicationArguments arguments = new DefaultApplicationArguments(args);
// --snip--
}

第三步:准备 Environment 添加命令行参数

Environment 即环境对象,是对配置信息的抽象,配置信息的来源有多种,比如:系统环境变量、properties 配置文件、YAML 配置文件等等。

SpringBoot 提供了名为 ApplicationEnvironment 的类表示环境对象,它是 Spring 中 StandardEnvironment 环境对象的子类。

ApplicationEnvironment的类图

默认情况下,创建的 ApplicationEnvironment 对象中配置信息的来源只有两个:

  • 系统属性
  • 系统变量
1
2
3
4
5
6
7
8
package org.springframework.boot;

public class Step3 {
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().forEach(System.out::println);
}
}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}    

针对相同名称的配置信息,按照来源的先后顺序获取。

获取 JAVA_HOME 的配置信息:

1
2
3
4
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
System.out.println(env.getProperty("JAVA_HOME"));
}
D:\environment\JDK1.8  

由于 PropertiesPropertySource 中并不存在名为 JAVA_HOME 的配置信息,因此从系统环境变量 SystemEnvironmentPropertySource 中获取 JAVA_HOME 的配置信息。

在 IDEA 的 Run/Debug Configurations 中的 VM options 添加 -DJAVA_HOME=abc,使得 PropertiesPropertySource 中存在名为 JAVA_HOME 的配置信息:

添加-DJAVA_HOME=abc配置信息

之后再运行 main() 方法,控制台打印出:

abc

如果想从配置文件 application.properties 中读取配置信息,可以添加配置信息的来源。配置文件的优先级最低,添加来源时调用 addLast() 方法:

1
2
3
4
5
6
7
8
@SneakyThrows
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(new ResourcePropertySource(new ClassPathResource("application.properties")));
env.getPropertySources().forEach(System.out::println);

System.out.println(env.getProperty("author.name"));
}
1
author.name="lxda"
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
"lxda"

而在 SpringBoot 中,这里只添加 SimpleCommandLinePropertySource,并且它的优先级最高,使用 addFirst() 方法添加:

1
2
3
4
5
6
7
8
9
@SneakyThrows
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(new ResourcePropertySource(new ClassPathResource("application.properties")));
env.getPropertySources().addFirst(new SimpleCommandLinePropertySource(args));
env.getPropertySources().forEach(System.out::println);

System.out.println(env.getProperty("author.name"));
}

运行 main() 方法前,需要添加程序参数 --author.name=lxda:

SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
lxda

第四步:添加 ConfigurationPropertySources

有一 step4.properties 文件,其内容如下:

1
2
3
user.first-name=George
user.middle_name=Walker
user.lastName=Bush

尝试读取文件中的内容:

1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(
new ResourcePropertySource("step4", new ClassPathResource("step4.properties"))
);
env.getPropertySources().forEach(System.out::println);
System.out.println(env.getProperty("user.first-name"));
System.out.println(env.getProperty("user.middle-name"));
System.out.println(env.getProperty("user.last-name"));
}

step4.properties 文件中配置信息的 key 是 user.middle_name,但在读取时,使用的是 user.middle-name;还有 user.lastName 的 key,但读取时使用 user.last-name。能读取成功吗?

PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='step4'}
George
null
null

显然是不行的,为了能读取成功,需要实现 松散绑定,添加 ConfigurationPropertySources

1
2
3
4
5
6
@SneakyThrows
public static void main(String[] args) {
// --snip--
ConfigurationPropertySources.attach(env);
// --snip--
}
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='step4'}
George
Walker
Bush

第五步:使用 EnvironmentPostProcessorApplicationListener 进行环境对象后置处理

在第三步中 只 添加 SimpleCommandLinePropertySource,读取 properties、YAML 配置文件的源就是在第五步中添加的。

完成这样功能需要使用到 EnvironmentPostProcessor,其具体实现是 ConfigDataEnvironmentPostProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
ApplicationEnvironment env = new ApplicationEnvironment();

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强前");
env.getPropertySources().forEach(System.out::println);
ConfigDataEnvironmentPostProcessor processor1 = new ConfigDataEnvironmentPostProcessor(
new DeferredLogs(), new DefaultBootstrapContext()
);
processor1.postProcessEnvironment(env, app);

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强后");
env.getPropertySources().forEach(System.out::println);
System.out.println(env.getProperty("author.name"));

RandomValuePropertySourceEnvironmentPostProcessor processor2 =
new RandomValuePropertySourceEnvironmentPostProcessor(new DeferredLog());
processor2.postProcessEnvironment(env, app);
}
>>>>>>>>>>>>>>>>>>>>>>>> 增强前
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
>>>>>>>>>>>>>>>>>>>>>>>> 增强后
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
"lxda"

EnvironmentPostProcessor 还有一个有趣的实现:RandomValuePropertySourceEnvironmentPostProcessor,该实现提供了随机值的生成。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 再次增强后");
env.getPropertySources().forEach(System.out::println);
System.out.println(env.getProperty("random.string"));
System.out.println(env.getProperty("random.int"));
System.out.println(env.getProperty("random.uuid"));
}
>>>>>>>>>>>>>>>>>>>>>>>> 再次增强后
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
5ef4038a709215938cbd3e1c031f66dd
1481116109
18548e0b-8bad-458b-b38e-bf793aa24ced

在 SpringBoot 中的实现是不会采取上述示例代码的方式来添加后置处理器,同样会从 META-INF/spring.factories 配置文件中读取并初始化后置处理器:

1
2
3
4
5
6
7
8
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor

SpringBoot 中读取 META-INF/spring.factories 配置文件初始化环境后置处理器,再执行处理逻辑的功能由 EnvironmentPostProcessorApplicationListener 完成。它是一个事件监听器,同样是在 META-INF/spring.factories 配置文件中读取并初始化的:

1
2
3
4
5
6
7
8
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

要想该监听器成功监听到事件,需要在第五步中发布一个事件,而事件的发布由第一步获取的事件发布器完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
app.addListeners(new EnvironmentPostProcessorApplicationListener());
ApplicationEnvironment env = new ApplicationEnvironment();

List<String> names = SpringFactoriesLoader.loadFactoryNames(EnvironmentPostProcessor.class, Step5.class.getClassLoader());
names.forEach(System.out::println);

EventPublishingRunListener publisher = new EventPublishingRunListener(app, args);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强前");
env.getPropertySources().forEach(System.out::println);
publisher.environmentPrepared(new DefaultBootstrapContext(), env);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强后");
env.getPropertySources().forEach(System.out::println);
}
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor
org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
>>>>>>>>>>>>>>>>>>>>>>>> 增强前
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
>>>>>>>>>>>>>>>>>>>>>>>> 增强后
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''} 

配置文件中 EnvironmentPostProcessor 的实现有很多,但根据上述打印出的信息,生效的并不多,是否生效与项目的依赖配置有关。

第六步:绑定 spring.main 前缀的配置信息到 SpringApplication 对象

使用 @ConfigurationProperties 注解可以指定一个前缀,SpringBoot 将根据指定的前缀和属性名称在配置文件中寻找对应的信息并完成注入,其底层是利用 Binder 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SneakyThrows
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(
new ResourcePropertySource("step4", new ClassPathResource("step4.properties"))
);

User user = Binder.get(env).bind("user", User.class).get();
System.out.println(user);

User existUser = new User();
Binder.get(env).bind("user", Bindable.ofInstance(existUser));
System.out.println(existUser);
}

@Getter
@Setter
@ToString
static class User {
private String firstName;
private String middleName;
private String lastName;
}
Step6.User(firstName=George, middleName=Walker, lastName=Bush)
Step6.User(firstName=George, middleName=Walker, lastName=Bush)

在第六步中,绑定 spring.main 前缀的配置信息到 SpringApplication 对象也是利用了 Binder

假设 step6.properties 配置文件的信息如下:

1
2
spring.main.banner-mode=off
spring.main.lazy-initialization=true

绑定 spring.main 开头的配置信息到 SpringApplication 对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SneakyThrows
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
ApplicationEnvironment env = new ApplicationEnvironment();
env.getPropertySources().addLast(
new ResourcePropertySource("step6", new ClassPathResource("step6.properties"))
);

Class<? extends SpringApplication> clazz = app.getClass();
Field bannerMode = clazz.getDeclaredField("bannerMode");
bannerMode.setAccessible(true);
Field lazyInitialization = clazz.getDeclaredField("lazyInitialization");
lazyInitialization.setAccessible(true);
System.out.println(bannerMode.get(app));
System.out.println(lazyInitialization.get(app));
Binder.get(env).bind("spring.main", Bindable.ofInstance(app));
System.out.println(bannerMode.get(app));
System.out.println(lazyInitialization.get(app));
}
CONSOLE
false
OFF
true  

第七步:打印 Banner

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
ApplicationEnvironment env = new ApplicationEnvironment();
SpringApplicationBannerPrinter printer = new SpringApplicationBannerPrinter(
new DefaultResourceLoader(),
new SpringBootBanner()
);

printer.print(env, Step7.class, System.out);
}

除此之外还可以自定义文字和图片 Banner,文字 Banner 的文件类型需要是 txt,图片 Banner 的文件类型需要是 gif。

文字 Banner:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// --snip--

// 测试文字 banner
env.getPropertySources().addLast(new MapPropertySource(
"custom",
Collections.singletonMap("spring.banner.location", "banner1.txt")
));
printer.print(env, Step7.class, System.out);
}

文字 Banner 可以从 网站 上自定义。

图片 Banner:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// --snip--

// 测试图片 banner
env.getPropertySources().addLast(new MapPropertySource(
"custom",
Collections.singletonMap("spring.banner.image.location", "banner2.gif")
));
printer.print(env, Step7.class, System.out);
}

获取 Spring 或 SpringBoot 的版本号可以使用:

1
2
System.out.println("SpringBoot: " + SpringBootVersion.getVersion());
System.out.println("Spring: " + SpringVersion.getVersion());

第八到十一步:完成 Spring 容器的创建

  • 第八步:创建容器。在构造 SpringApplication 时已经推断出应用的类型,使用应用类型直接创建即可。
  • 第九步:准备容器。回调在构造 SpringApplication 时添加的初始化器。
  • 第十步:加载 Bean 定义。从配置类、XML 配置文件读取 BeanDefinition,或者扫描某一包路径下的 BeanDefinition。
  • 第十一步:调用 ApplicationContextrefresh() 方法,完成 Spring 容器的创建。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@SneakyThrows
@SuppressWarnings("all")
public static void main(String[] args) {
SpringApplication app = new SpringApplication();
app.addInitializers(applicationContext -> System.out.println("执行初始化器增强..."));

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 8. 创建容器");
GenericApplicationContext context = createApplicationContext(WebApplicationType.SERVLET);

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 9. 准备容器");
for (ApplicationContextInitializer initializer : app.getInitializers()) {
initializer.initialize(context);
}

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 10. 加载 Bean 定义");
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
AnnotatedBeanDefinitionReader reader1 = new AnnotatedBeanDefinitionReader(beanFactory);
XmlBeanDefinitionReader reader2 = new XmlBeanDefinitionReader(beanFactory);
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(beanFactory);

reader1.register(Config.class);
reader2.loadBeanDefinitions(new ClassPathResource("b03.xml"));
scanner.scan("com.itheima.a39.sub");

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 11. refresh 容器");
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println("name: " + name + " 来源: " + beanFactory.getBeanDefinition(name).getResourceDescription());
}
}

private static GenericApplicationContext createApplicationContext(WebApplicationType type) {
GenericApplicationContext context = null;
switch (type) {
case SERVLET:
context = new AnnotationConfigServletWebServerApplicationContext();
break;
case REACTIVE:
context = new AnnotationConfigReactiveWebServerApplicationContext();
break;
case NONE:
context = new AnnotationConfigApplicationContext();
break;
}
return context;
}

涉及到的配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class Bean4 {

}

static class Bean5 {

}

@Configuration
static class Config {
@Bean
public Bean5 bean5() {
return new Bean5();
}

@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}

XML 配置文件:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bean4" class="com.itheima.a39.A39_3.Bean4"/>

</beans>

com.itheima.a39.sub 包下的 Bean 信息:

1
2
3
4
5
6
7
8
package com.itheima.a39.sub;

import org.springframework.stereotype.Component;

@Component
public class Bean7 {
}

运行 main() 方法后,控制台打印出的 Bean 信息:

name: org.springframework.context.annotation.internalConfigurationAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalAutowiredAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalCommonAnnotationProcessor 来源: null
name: org.springframework.context.event.internalEventListenerProcessor 来源: null
name: org.springframework.context.event.internalEventListenerFactory 来源: null
name: a39_3.Config 来源: null
name: bean4 来源: class path resource [b03.xml]
name: bean7 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\com\itheima\a39\sub\Bean7.class]
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean5 来源: com.itheima.a39.A39_3$Config
name: servletWebServerFactory 来源: com.itheima.a39.A39_3$Config

第十二步:执行 Runner

在 SpringBoot 启动成功后,可以执行一些 Runner,进行一些预处理或测试。Runner 有两种,分别是 CommandLineRunnerApplicationRunner

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}

@FunctionalInterface
public interface ApplicationRunner {
void run(ApplicationArguments args) throws Exception;
}

它们都是函数式接口,内部的抽象方法长得也很像,只不过:

  • CommandLineRunner 直接接收启动参数;
  • ApplicationRunner 则是接收封装后的 ApplicationArguments,即 第二步 封装的对象。

在配置类中添加这两种类型的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public CommandLineRunner commandLineRunner() {
return args -> System.out.println("commandLineRunner()..." + Arrays.toString(args));
}

@Bean
public ApplicationRunner applicationRunner() {
return args -> {
// 获取原始参数
System.out.println("applicationRunner()..."
+ Arrays.toString(args.getSourceArgs()));
// 获取选项名称,参数中带有 `--` 的参数
System.out.println(args.getOptionNames());
// 获取选项值
System.out.println(args.getOptionValues("server.port"));
// 获取非选项参数
System.out.println(args.getNonOptionArgs());
};
}

执行 Runner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SneakyThrows
@SuppressWarnings("all")
public static void main(String[] args) {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args");
DefaultApplicationArguments arguments = new DefaultApplicationArguments(args);

// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner");
for (CommandLineRunner runner : context.getBeansOfType(CommandLineRunner.class).values()) {
runner.run(args);
}

for (ApplicationRunner runner : context.getBeansOfType(ApplicationRunner.class).values()) {
runner.run(arguments);
}
}

运行 main() 方法时,需要添加程序参数 --server.port=8080 debug

执行Runner添加程序参数

>>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner
commandLineRunner()...[--server.port=8080, debug]
applicationRunner()...[--server.port=8080, debug]
[server.port]
[8080]
[debug] 

步骤总结

  1. 得到 SpringApplicationRunListeners 事件发布器

    • 发布 Application Starting 事件 1️⃣
  2. 封装启动 args

  3. 准备 Environment 添加命令行参数

  4. ConfigurationPropertySources 处理

    • 发布 Application Environment 已准备事件 2️⃣
  5. 通过 EnvironmentPostProcessorApplicationListener 进行 env 后处理

    • application.propertiesStandardConfigDataLocationResolver 解析

    • spring.application.json

  6. 绑定 spring.mainSpringApplication 对象

  7. 打印 Banner

  8. 创建容器

  9. 准备容器

    • 发布 Application Context 已初始化事件 3️⃣
  10. 加载 Bean 定义

    • 发布 Application Prepared 事件 4️⃣
  11. refresh 容器

    • 发布 Application Started 事件 5️⃣
  12. 执行 Runner

    • 发布 Application Ready 事件 6️⃣

    • 这其中有异常,发布 Application Failed 事件 7️⃣

40. Tomcat 内嵌容器

Tomcat 基本结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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-INF
web.xml

40.1 内嵌 Tomcat 的使用

内嵌 Tomcat 的使用分为 6 步:

  1. 创建 Tomcat
  2. 创建项目文件夹,即 docBase 文件夹
  3. 创建 Tomcat 项目,在 Tomcat 中称为 Context
  4. 编程添加 Servlet
  5. 启动 Tomcat
  6. 创建连接器,设置监听端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SneakyThrows
public static void main(String[] args) {
// 1. 创建 Tomcat
Tomcat tomcat = new Tomcat();
tomcat.setBaseDir("tomcat");
// 2. 创建项目文件夹,即 docBase 文件夹
File docBase = Files.createTempDirectory("boot.").toFile();
docBase.deleteOnExit();
// 3. 创建 tomcat 项目,在 tomcat 中称为 Context
Context context = tomcat.addContext("", docBase.getAbsolutePath());
// 4. 编程添加 Servlet
context.addServletContainerInitializer((set, servletContext) -> {
HelloServlet servlet = new HelloServlet();
// 还要设置访问 Servlet 的路径
servletContext.addServlet("hello", servlet).addMapping("/hello");
}, Collections.emptySet());
// 5. 启动 tomcat
tomcat.start();
// 6. 创建连接器,设置监听端口
Connector connector = new Connector(new Http11Nio2Protocol());
connector.setPort(8080);
tomcat.setConnector(connector);
}

自行实现 HelloServlet 继承 HttpServlet,重写 doGet() 方法:

1
2
3
4
5
6
7
8
9
10
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 8117441197359625079L;

@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().print("<h3>hello</h3>");
}
}

运行 main() 方法后,在浏览器访问 localhost:8080/hello,页面显示 hello

40.2 与 Spring 整合

选择不支持内嵌 Tomcat 的 Spring 容器,使其使用前文中的 Tomcat:

1
2
3
4
5
6
7
public static WebApplicationContext getApplicationContext() {
// 使用不支持内嵌 Tomcat 的 Spring 容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(Config.class);
context.refresh();
return context;
}

容器中注册了 Config Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
static class Config {
@Bean
public DispatcherServletRegistrationBean registrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}

@Bean
public DispatcherServlet dispatcherServlet(WebApplicationContext applicationContext) {
/*
* 必须为 DispatcherServlet 提供 AnnotationConfigWebApplicationContext,
* 否则会选择 XmlWebApplicationContext 实现
*/
return new DispatcherServlet(applicationContext);
}

@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
return handlerAdapter;
}

@RestController
static class MyController {
@GetMapping("hello2")
public Map<String,Object> hello() {
return Collections.singletonMap("hello2", "hello2, spring!");
}
}
}

Tomcat 在添加 Servlet 时,添加 DispatcherServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SneakyThrows
public static void main(String[] args) {
// --snip--

WebApplicationContext springContext = getApplicationContext();

// 4. 编程添加 Servlet
context.addServletContainerInitializer((set, servletContext) -> {
HelloServlet servlet = new HelloServlet();
// 还要设置访问 Servlet 的路径
servletContext.addServlet("hello", servlet).addMapping("/hello");

DispatcherServlet dispatcherServlet = springContext.getBean(DispatcherServlet.class);
servletContext.addServlet("dispatcherServlet", dispatcherServlet).addMapping("/");
}, Collections.emptySet());

// --snip--
}

运行 main() 方法,在浏览器中访问 localhost:8080/hello2,页面上显示:

{"hello2":"hello2, spring!"}  

添加 Servlet 时只添加了一个 DispatcherServlet,但 Spring 容器中可能存在多个 Servlet,这些 Servlet 也应该被添加,因此可以获取 ServletRegistrationBean 类型的 Bean 并执行方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SneakyThrows
public static void main(String[] args) {
// --snip--

WebApplicationContext springContext = getApplicationContext();

// 4. 编程添加 Servlet
context.addServletContainerInitializer((set, servletContext) -> {
HelloServlet servlet = new HelloServlet();
// 还要设置访问 Servlet 的路径
servletContext.addServlet("hello", servlet).addMapping("/hello");

// Spring 容器中可能存在多个 Servlet
for (ServletRegistrationBean registrationBean : springContext.getBeansOfType(ServletRegistrationBean.class).values()) {
registrationBean.onStartup(servletContext);
}
}, Collections.emptySet());

// --snip--
}

运行 main() 方法,在浏览器中访问 localhost:8080/hello2,页面显示同样的内容。

41. 自动配置

41.1 自动配置类原理

有以下 4 个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 模拟第三方配置类
*/
static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1();
}
}

@ToString
@NoArgsConstructor
@AllArgsConstructor
static class Bean1 {
private String name;
}

/**
* 模拟第三方配置类
*/
static class AutoConfiguration2 {
@Bean
public Bean2 bean2() {
return new Bean2();
}
}

static class Bean2 {

}

其中 AutoConfiguration1AutoConfiguration2 用来模拟第三方配置类,注意它们并没有被 @Configuration 注解标记,因此在未进行其他操作时,不会被添加到 Spring 容器中。

然后编写自己的配置类,使用 @Import 注解将第三方配置类添加到 Spring 容器中:

1
2
3
4
@Configuration
@Import({AutoConfiguration1.class, AutoConfiguration2.class})
static class Config {
}
1
2
3
4
5
6
7
8
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
context.registerBean(ConfigurationClassPostProcessor.class);
context.refresh();

Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
}

运行 main() 方法后,控制台打印出:

config
org.springframework.context.annotation.ConfigurationClassPostProcessor
com.itheima.A41$AutoConfiguration1
bean1
com.itheima.a41.A41$AutoConfiguration2
bean2

如果有多个第三方配置类,难不成到一个个地导入?

可以使用导入选择器 ImportSelector,重写 selectImports() 方法,返回需要自动装配的 Bean 的全限定类名数组:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Import(MyImportSelector.class)
static class Config {

}

static class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()};
}
}

但这样的方式相比最初的方式并没有本质区别,如果 selectImports() 方法返回的全限定类名可以从文件中读取,就更方便了。

在当前项目的类路径下创建 META-INF/spring.factories 文件,约定一个 key,对应的 value 即为需要指定装配的 Bean:

1
2
3
4
# 内部类作为 key 时,最后以 $ 符号分割
com.itheima.a41.A41$MyImportSelector=\
com.itheima.a41.A41.AutoConfiguration1, \
com.itheima.a41.A41.AutoConfiguration2

修改 selectImports() 方法实现逻辑:

1
2
3
4
5
6
7
static class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> names = SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null);
return names.toArray(new String[0]);
}
}

运行 main() 方法后,控制台打印出同样的结果。

SpringFactoriesLoader.loadFactoryNames() 不仅会扫描当前项目路径下的 META-INF/spring.factories 文件,还会扫描项目所关联的 Jar 包下的 META-INF/spring.factories 文件。

针对 SpringBoot 来说,自动装配的 Bean 使用如下语句加载:

1
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, null);

SpringBoot 2.7.0 及其以后版本的自动装配

在 SpringBoot 2.7.0 及其以后的版本中,SpringBoot 不再通过读取 META-INF/spring.factories 文件中 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的 values 来实现自动装配。

为了更贴合 SPI 机制,SpringBoot 将读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中的内容,该文件中每一行都表示需要自动装配的 Bean 的全限定类名,可以使用 # 作为注释。其加载方式使用:

1
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader());

其中 AutoConfiguration 是一个注解,它的全限定类名为 org.springframework.boot.autoconfigure.AutoConfiguration

也就是说可以自定义一个注解,创建 META-INF/spring/full-qualified-annotation-name.imports 文件,在文件里声明需要自动装配的类:

1
2
3
4
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutoConfiguration {
}
1
2
3
4
public class A41 {
static class Bean3 {
}
}

创建 META-INF/spring/com.itheima.a41.MyAutoConfiguration.imports 文件:

1
com.itheima.a41.A41$Bean3

修改 selectImports() 方法实现逻辑:

1
2
3
4
5
6
7
8
9
static class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> names = new ArrayList<>(SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null));
// 读取新版自动装配文件
ImportCandidates.load(MyAutoConfiguration.class, null).forEach(names::add);
return names.toArray(new String[0]);
}
}

运行 main() 方法后,Spring 容器中的 Bean 多了 一个:

com.itheima.a41.A41$Bean3  

定义了冲突的 Bean

第三方装配了 Bean1

1
2
3
4
5
6
static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1("第三方");
}
}

用户又自行定义了 Bean1

1
2
3
4
5
6
7
8
@Configuration
@Import(MyImportSelector.class)
static class Config {
@Bean
public Bean1 bean1() {
return new Bean1("本项目");
}
}

修改测试的 main() 方法:

1
2
3
4
5
6
public static void main(String[] args) {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>");
System.out.println(context.getBean(Bean1.class));
}
>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)  

用户自行定义的 Bean 生效了,这是因为:@Import 导入的 Bean 先于配置类中 @Bean 定义的 Bean 执行,后者覆盖前者,使得用户自定义的 Bean 生效。

但在 SpringBoot 中不是这样的,当后续添加的 Bean 想覆盖先前添加的 Bean,会出现错误。模拟 SpringBoot 的设置:

1
2
3
4
5
6
7
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
// 默认是 true,SpringBoot 修改为 false,使得无法进行覆盖
context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false);

// --snip--
}
Exception in thread "main" org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'bean1' defined in com.itheima.a41.A41$Config: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=config; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in com.itheima.a41.A41$Config] for bean 'bean1': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=com.itheima.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/itheima/a41/A41$AutoConfiguration1.class]] bound. 

那这样是合理的吗?

显然不是。比如 SpringBoot 默认的数据连接池是 Hikari,如果用户想换成 Druid,岂不是做不到?

实际情况下是能做到的,这又是怎么做到的呢?

首先需要使用户的配置类中定义的 Bean 先于 @Import 导入的 Bean 添加到 Spring 容器中,只需将选择器 MyImportSelector 实现的 ImportSelector 接口更换成其子接口 DeferredImportSelector 即可:

1
2
3
static class MyImportSelector implements DeferredImportSelector {
// --snip--
}

再次运行 main() 方法:

Exception in thread "main" org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'bean1' defined in class path resource [com/itheima/a41/A41$AutoConfiguration1.class]: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=com.itheima.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/itheima/a41/A41$AutoConfiguration1.class]] for bean 'bean1': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=config; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in com.itheima.a41.A41$Config] bound.

尽管还是出现了异常,但异常信息中显示的是在配置类定义的 Bean 已存在,第三方装配的 Bean 无法再添加,这表明 Bean 的添加顺序修改成功。

最后在第三方定义的 Bean 上添加 @ConditionalOnMissingBean 注解,表示容器中存在同名的 Bean 时忽略该 Bean 的添加:

1
2
3
4
5
6
7
static class AutoConfiguration1 {
@Bean
@ConditionalOnMissingBean
public Bean1 bean1() {
return new Bean1("第三方");
}
}

再次运行 main() 方法,不再出现异常:

>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)

41.2 Aop 自动配置

确保当前模块下已导入:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

使用 AopAutoConfiguration 自动装配与 AOP 相关的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestAopAuto {
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
// 注册常用后置处理器
AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());
context.registerBean(Config.class);
context.refresh();
Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
}

@Configuration
@Import(MyImportSelector.class)
static class Config {
}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{AopAutoConfiguration.class.getName()};
}
}
}
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
com.itheima.a41.TestAopAuto$Config
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration$CglibAutoProxyConfiguration
org.springframework.aop.config.internalAutoProxyCreator
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

com.itheima.a41.TestAopAuto$Config 为分割线,上方是添加的一些后置处理器,下方就是 AOP 自动装配添加的 Bean。

在配置类 AopAutoConfiguration 中,使用注解判断配置类是否生效。首先是最外层的 AopAutoConfiguration

1
2
3
4
5
@AutoConfiguration
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
// --snip--
}

根据 @ConditionalOnProperty 注解配置的信息:如果配置文件中存在前缀spring.aop名称auto 的 key,并且其对应的 value 是 true 时,配置类 AopAutoConfiguration 生效;如果配置文件中未显式配置,该配置类也生效。

不使用配置文件,使用 StandardEnvironment 指定 spring.aop.auto 的值为 false

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
StandardEnvironment env = new StandardEnvironment();
env.getPropertySources().addLast(
new SimpleCommandLinePropertySource("--spring.aop.auto=false")
);
context.setEnvironment(env);

// --snip--
}
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
com.itheima.a41.TestAopAuto$Config

如果 spring.aop.auto 的值是 true,又会成功添加上 AOP 自动装配的 Bean。

再看 AopAutoConfiguration 的内部类:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
static class AspectJAutoProxyingConfiguration {
// --snip--
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
static class ClassProxyingConfiguration {
// --snip--
}

其内部存在两个类:AspectJAutoProxyingConfigurationClassProxyingConfiguration

使用了 @ConditionalOnClass 注解判断 Advice.class 存在时,AspectJAutoProxyingConfiguration 生效;使用 @ConditionalOnMissingClass 注解判断 org.aspectj.weaver.Advice 不存在时,ClassProxyingConfiguration 生效。

由于先前导入了 spring-boot-starter-aop 依赖,Advice.class 是存在的,AspectJAutoProxyingConfiguration 将生效。

AspectJAutoProxyingConfiguration 内部又有两个配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
static class JdkDynamicAutoProxyConfiguration {

}

@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
static class CglibAutoProxyConfiguration {

}

这两个配置类通过使用 @ConditionalOnProperty 注解判断配置文件中是否存在 spring.aop.proxy-target-class 配置来让对应的配置类生效。

由于并未显式配置,因此 CglibAutoProxyConfiguration 将生效。

无论哪个配置类生效,它们都被 @EnableAspectJAutoProxy 标记,这个注解相当于是添加了些配置的 @Import 注解:

1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
boolean proxyTargetClass() default false;

boolean exposeProxy() default false;
}

向 Spring 容器中添加 AspectJAutoProxyRegistrar 类型的 Bean。

AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,可以使用编程的方式来注册一些 Bean:

1
2
3
4
5
6
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
// --snip--
}
}

AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary() 方法是注册 Bean 的主要逻辑:

1
2
3
4
5
6
7
8
9
@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, (Object)null);
}

@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {
return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}

最终注册了 AnnotationAwareAspectJAutoProxyCreator

使用 org.springframework.aop.config.internalAutoProxyCreator 作为名称,获取 AnnotationAwareAspectJAutoProxyCreator 类型的 Bean,并查看其 proxyTargetClass 属性是否为 true:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
// --snip--

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>");
AnnotationAwareAspectJAutoProxyCreator creator =
context.getBean("org.springframework.aop.config.internalAutoProxyCreator", AnnotationAwareAspectJAutoProxyCreator.class);
System.out.println(creator.isProxyTargetClass()); // true
}

【补充】ImportBeanDefinitionRegistrar 接口

将 Bean 注入到 Spring 的大致流程是:

  • 利用 BeanDefinitionReader 读取配置文件或注解信息,为每一个 Bean 生成一个 BeanDefinition
  • BeanDefinition 注册到 BeanDefinitionRegistry
  • 当需要创建 Bean 对象时,从 BeanDefinitionRegistry 中取出对应的 BeanDefinition,利用这个 BeanDefinition 来创建 Bean
  • 如果创建的 Bean 是单例的,Spring 会将这个 Bean 保存到 SingletonBeanRegistry 中,即三级缓存中的第一级缓存,需要时直接从这里获取,而不是重复创建

也就是说 Spring 是通过 BeanDefinition 去创建 Bean 的,而 BeanDefinition 会被注册到 BeanDefinitionRegistry 中,因此可以拿到 BeanDefinitionRegistry 直接向里面注册 BeanDefinition 达到将 Bean 注入到 Spring 的目标。

ImportBeanDefinitionRegistrar 接口就可以直接拿到 BeanDefinitionRegistry

1
2
3
4
5
6
7
8
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
this.registerBeanDefinitions(importingClassMetadata, registry);
}

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}

该接口需要搭配 @Import注解使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static void main(String[] args) {
// AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
GenericApplicationContext context = new GenericApplicationContext();
// AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());

context.registerBean(ConfigurationClassPostProcessor.class);
context.registerBean("config", Config.class);
context.refresh();

Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println(context.getBean(User.class));
}

@Configuration
@Import({MyImportBeanDefinitionRegistrar.class})
static class Config {

}

static class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 构建 BeanDefinition
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(User.class)
.addPropertyValue("name", "lxd")
.addPropertyValue("age", 20)
.getBeanDefinition();
// 注册构建好的 BeanDefinition
registry.registerBeanDefinition("user", beanDefinition);
}
}

@Setter
@ToString
static class User {
private String name;
private int age;
}
org.springframework.context.annotation.ConfigurationClassPostProcessor
config
user
TestImportBeanDefinitionRegistrar.User(name=lxd, age=20)  

注意: 使用时一定要确保 Spring 容器中存在 ConfigurationClassPostProcessor 类型的 Bean。

除此之外,使用 BeanDefinitionRegistryPostProcessor 接口也能拿到 BeanDefinitionRegistry

1
2
3
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}

41.3 数据库相关的自动配置

确保当前模块下已导入:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>

DataSource 自动配置

自行实现导入选择器,并使用 @Import 注解进行导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@Import(MyImportSelector.class)
static class Config {

}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
DataSourceAutoConfiguration.class.getName(),
MybatisAutoConfiguration.class.getName(),
DataSourceTransactionManagerAutoConfiguration.class.getName(),
TransactionAutoConfiguration.class.getName()
};
}
}

在 main()方法中打印导入的 Bean 信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
StandardEnvironment env = new StandardEnvironment();
env.getPropertySources().addLast(new SimpleCommandLinePropertySource(
"--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring",
"--spring.datasource.username=root",
"--spring.datasource.password=123456"
));
context.setEnvironment(env);
AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory());
context.registerBean(Config.class);

context.refresh();
for (String name : context.getBeanDefinitionNames()) {
String resourceDescription = context.getBeanDefinition(name).getResourceDescription();
if (resourceDescription != null)
System.out.println(name + " 来源: " + resourceDescription);
}
}

未使用配置文件,而是使用 StandardEnvironment 设置了一些数据库连接信息。

最后只打印有明确来源的 Bean 信息,其中有一条:

dataSource 来源: class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]

名叫 dataSource 的 Bean 的来源为什么是 DataSourceConfiguration,而不是 DataSourceAutoConfiguration 呢?

查看 DataSourceAutoConfiguration 的源码,实现与 AopAutoConfiguration 类似,都是通过注解来判断需要导入哪些 Bean,有两个关键的内部类 EmbeddedDatabaseConfigurationPooledDataSourceConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {

@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {

}

@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {

}
}

它们都被 @Conditional 注解标记。当项目支持内嵌数据源时,EmbeddedDatabaseConfiguration 生效;当项目支持基于数据库连接池的数据源时,PooledDataSourceConfiguration 生效。

SpringBoot 默认的数据库连接池是 Hikari,因此 PooledDataSourceConfiguration 生效,最终使用 @Import 导入一系列 Bean,导入的这些 Bean 都是 DataSourceConfiguration 的内部类,因此dataSource 的 Bean 的来源是 DataSourceConfiguration

DataSourceConfiguration 中,通过 @ConditionalOnClass 注解判断某些 Class 是否存在来使某种数据库连接池生效。

由于导入了 mybatis-spring-boot-starter,其内部依赖 mybatis-spring-boot-jdbc,而它又依赖了 HikariCP,因此最终数据库连接池 Hikari 生效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}

}

Hikari#dataSource() 方法中,接受一个 DataSourceProperties 类型的参数,这要求 Spring 容器中存在 DataSourceProperties 类型的 Bean。

在最初的 DataSourceAutoConfiguration 自动配置类上有个 @EnableConfigurationProperties 注解,它将 DataSourceProperties 添加到容器中:

1
2
3
4
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
// --snip-=
}

DataSourceProperties 中会绑定配置文件中以 spring.datasource 为前缀的配置:

1
2
3
4
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
// --snip--
}

获取 DataSourceProperties 类型的 Bean,并打印其 urlusernamepassword

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
StandardEnvironment env = new StandardEnvironment();
env.getPropertySources().addLast(new SimpleCommandLinePropertySource(
"--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring",
"--spring.datasource.username=root",
"--spring.datasource.password=123456"
));
context.setEnvironment(env);

// --snip--

DataSourceProperties properties = context.getBean(DataSourceProperties.class);
System.out.println(properties.getUrl());
System.out.println(properties.getUsername());
System.out.println(properties.getPassword());
}
jdbc:mysql://localhost:3306/advanced_spring
root
123456 

MyBatis 自动配置

接下来看看 MyBatis 的自动配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
// --snip--

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// --snip--
}

// --snip--

@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
// --snip--
}

@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
// --snip--
}

// --snip--
}

MybatisAutoConfiguration 生效的条件有两个:

  • 类路径下存在 SqlSessionFactorySqlSessionFactoryBean
  • Spring 容器中有且仅有一个 DataSource 类型的 Bean

它还添加了 MybatisProperties 类型的 Bean 到 Spring 容器中,并与配置文件中以 mybatis 为前缀的信息绑定。

@AutoConfigureAfter 注解指定了当前自动配置类在 DataSourceAutoConfigurationMybatisLanguageDriverAutoConfiguration 两个自动配置类解析完成之后再解析。

接下来遇到 sqlSessionFactory() 方法:

1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// --snip--
}

依赖 Spring 容器中的 DataSource,当容器中不存在 SqlSessionFactory 时,将其添加到 Spring 容器中。

然后是 sqlSessionTemplate() 方法,它与添加 SqlSessionFactory 到 Spring 容器的逻辑一样:

1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
// --snip--
}

SqlSessionTemplate 也是 SqlSession 的实现,提供了与当前线程绑定的 SqlSession。针对多个方法调用,如果它们来自同一个线程,那么获取到的 SqlSession 对象是同一个。这也是为什么有了 DefaultSqlSession 作为 SqlSession 的实现了,还需要 SqlSessionTemplate

在 MyBatis 中,使用 MapperFactoryBean 将接口转换为对象,其核心是 getObject() 方法:

1
2
3
public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}

方法中获取了 sqlSession 对象,而获取的就是 SqlSessionTemplate 对象:

1
2
3
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}

最后来到 MapperScannerRegistrarNotFoundConfiguration 内部类:

1
2
3
4
5
6
@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
// --snip--
}

利用 @ConditionalOnMissingBean 判断 Spring 容器中缺失 MapperFactoryBeanMapperScannerConfigurer 时,该配置类生效。生效时利用 @Import 导入 AutoConfiguredMapperScannerRegistrar

1
2
3
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
// --snip--
}

AutoConfiguredMapperScannerRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,允许通过编程的方式将 Bean 添加到 Spring 容器中,而这里是去扫描 Mapper 接口,将其转换为对象添加到 Spring 容器中。

在 main()所在类的包路径下创建 mapper 包,并新建三个接口,其中两个被 @Mapper 注解标记:

1
2
3
4
5
6
7
8
9
10
@Mapper
public interface Mapper1 {
}

@Mapper
public interface Mapper2 {
}

public interface Mapper3 {
}

运行 main()方法,查看 Mapper1Mapper2 是否被添加到 Spring 容器中。

结果是否定的。因为 没有设置要扫描的包路径 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
// --snip--

String packageName = TestDataSourceAuto.class.getPackage().getName();
System.out.println("当前包名: " + packageName);
//记录引导类的包名
AutoConfigurationPackages.register(context.getDefaultListableBeanFactory(),
packageName);

context.refresh();
for (String name : context.getBeanDefinitionNames()) {
String resourceDescription = context.getBeanDefinition(name).getResourceDescription();
if (resourceDescription != null)
System.out.println(name + " 来源: " + resourceDescription);
}

// --snip--
}
当前包名: com.itheima.a41
mapper1 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\com\itheima\a41\mapper\Mapper1.class]
mapper2 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\com\itheima\a41\mapper\Mapper2.class]

@MapperScan 注解与 MybatisAutoConfiguration 在功能上很类似,只不过:

  • @MapperScan 可以指定具体的扫描路径,未指定时会把引导类范围内的所有接口当做 Mapper 接口;
  • MybatisAutoConfiguration 关注所有被 @Mapper 注解标记的接口,忽略未被 @Mapper 标记的接口。

事务自动配置

事务自动配置与 DataSourceTransactionManagerAutoConfigurationTransactionAutoConfiguration 有关。

DataSourceTransactionManagerAutoConfiguration 配置了 DataSourceTransactionManager 用来执行事务的提交、回滚操作。

TransactionAutoConfiguration 在功能上对标 @EnableTransactionManagement,包含以下三个 Bean:

  • BeanFactoryTransactionAttributeSourceAdvisor:事务切面类,包含通知和切点
  • TransactionInterceptor:事务通知类,由它在目标方法调用前后加入事务操作
  • AnnotationTransactionAttributeSource:解析 @Transactional 及事务属性,还包含了切点功能

如果自定义了 DataSourceTransactionManager 或是在引导类加了 @EnableTransactionManagement,则以自定义为准。

41.4 MVC 自动配置

MVC 的自动配置需要用到四个类:

  • 配置内嵌 Tomcat 服务器工厂:ServletWebServerFactoryAutoConfiguration
  • 配置 DispatcherServlet:DispatcherServletAutoConfiguration
  • 配置 WebMVC 各种组件:WebMvcAutoConfiguration
  • 配置 MVC 的错误处理:ErrorMvcAutoConfiguration

查看自动配置与 MVC 相关的 Bean 的信息、来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TestMvcAuto {
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
context.registerBean(Config.class);
context.refresh();
for (String name : context.getBeanDefinitionNames()) {
String source = context.getBeanDefinition(name).getResourceDescription();
if (source != null) {
System.out.println(name + " 来源:" + source);
}
}
context.close();
}

@Configuration
@Import(MyImportSelector.class)
static class Config {

}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
// 配置内嵌 Tomcat 服务器工厂
ServletWebServerFactoryAutoConfiguration.class.getName(),
// 配置 DispatcherServlet
DispatcherServletAutoConfiguration.class.getName(),
// 配置 WebMVC 各种组件
WebMvcAutoConfiguration.class.getName(),
// 配置 MVC 的错误处理
ErrorMvcAutoConfiguration.class.getName()
};
}
}
}

41.5 自定义自动配置类

在 SpringBoot 自动装配时添加自定义组件分为两步:

  1. 在类路径下自定义 META-INF/spring.factories 文件,以 org.springframework.boot.autoconfigure.EnableAutoConfiguration 为 key,设置需要自动装配的自定义组件的全限定类名为 value
  2. 编写配置类,在配置类上使用 @EnableAutoConfiguration 注解,并将其添加到 Spring 容器中
    在实际项目开发中,省略第二步,SpringBoot 的会自动扫描。

SpringBoot 2.7.0 及其以后版本

在类路径下自定义 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,文件中 每一行 表示需要进行自动装配的类的全限定类名,因此不能随意换行。

在这个文件中,以 # 开头的行表示注释。

42. 条件装配底层

42.1 @Conditional

在 SpringBoot 的自动配置中,经常看到 @Conditional 注解的使用,使用该注解可以按条件加载配置类。

@Conditional 注解并不具备条件判断功能,而是通过指定的 Class 列表来进行判断,指定的 Class 需要实现 Condition 接口。

假设有这样一个需求:通过判断类路径下是否存在 com.alibaba.druid.pool.DruidDataSource 类来加载不同的配置类,当存在 DruidDataSource 时,加载 AutoConfiguration1,反之加载 AutoConfiguration2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public static void main(String[] args) throws IOException {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
context.registerBean(ConfigurationClassPostProcessor.class);
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}
}

@Configuration
@Import(MyImportSelector.class)
static class Config {
}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()};
}
}

static class MyCondition1 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 存在 Druid 依赖
return ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource", null);
}
}

static class MyCondition2 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 不存在 Druid 依赖
return !ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource", null);
}
}

/**
* 模拟第三方的配置类
*/
@Configuration
@Conditional(MyCondition1.class)
static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1();
}
}

/**
* 模拟第三方的配置类
*/
@Configuration
@Conditional(MyCondition2.class)
static class AutoConfiguration2 {
@Bean
public Bean2 bean2() {
return new Bean2();
}
}

static class Bean1 {
}

static class Bean2 {
}

此时并未导入 druid 依赖,AutoConfiguration2 应该生效,运行 main() 方法后,控制台打印出:

config
org.springframework.context.annotation.ConfigurationClassPostProcessor
com.itheima.a42.A42$AutoConfiguration2
bean2

导入 druid 依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.15</version>
</dependency>

再次运行 main()方法:

config
org.springframework.context.annotation.ConfigurationClassPostProcessor
com.itheima.a42.A42$AutoConfiguration1
bean1  

42.2 @ConditionalOnXxx

在 SpringBoot 的自动配置中,经常看到 @ConditionalOnXxx 注解的使用,这种注解是将某个 @Conditional 的判断进行了封装,比如 ConditionalOnClass 就是用于判断某个 Class 是否存在。

因此针对上文中的代码可以做出修改:

  • 自定义 @ConditionalOnClass 注解,填入需要判断的全限定类名和判断条件;
  • 移除模拟的第三方配置上的 @Conditional 注解,而是使用自定义的 @ConditionalOnClass
  • Condition 接口的使用类重写的 matches() 方法利用 @ConditionalOnClass 注解进行条件判断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public static void main(String[] args) throws IOException {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
context.registerBean(ConfigurationClassPostProcessor.class);
context.refresh();

for (String name : context.getBeanDefinitionNames()) {
System.out.println(name);
}
}

@Configuration
@Import(MyImportSelector.class)
static class Config {
}

static class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()};
}
}

static class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//键是注解中定义的属性名,值是对应的属性值
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnClass.class.getName());
Optional<Map<String, Object>> optional = Optional.ofNullable(attributes);
String className = optional.map(i -> String.valueOf(i.get("className"))).orElse("");
boolean exists = optional.map(i -> i.get("exists"))
.map(String::valueOf)
.map(Boolean::parseBoolean).orElse(false);
boolean present = ClassUtils.isPresent(className, null);
return exists == present;
}
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Conditional(MyCondition.class)
private @interface ConditionalOnClass {
/**
* true 判断存在 false 判断不存在
*/
boolean exists();

/**
* 要判断的类名
*/
String className();
}

@Configuration
@ConditionalOnClass(className = "com.alibaba.druid.pool.DruidDataSource", exists = true)
static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1();
}
}

@Configuration
@ConditionalOnClass(className = "com.alibaba.druid.pool.DruidDataSource", exists = false)
static class AutoConfiguration2 {
@Bean
public Bean2 bean2() {
return new Bean2();
}
}

static class Bean1 {
}

static class Bean2 {
}

在导入 druid 依赖或未导入 druid 依赖的情况下运行 main() 方法,控制台打印结果与【42.1 @Conditional】一样。

43. FactoryBean

FactoryBean 是一个接口,可以实现该接口,并指定一个泛型,在重写的方法指定泛型类型对象的创建,然后将实现类交由 Spring 管理,最后 Spring 容器中会增加泛型类型的 Bean。这个 Bean 并不是完全受 Spring 管理,或者说部分受 Spring 管理。

为什么这么说呢?

首先定义一个 Bean2,交由 Spring 管理,但它不是重点:

1
2
3
@Component
public class Bean2 {
}

然后定义 Bean1,它未交由 Spring 管理,但是在其内部注入了 Bean2、定义初始化方法、实现 Aware 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
public class Bean1 implements BeanFactoryAware {
private Bean2 bean2;

@Autowired
public void setBean2(Bean2 bean2) {
this.bean2 = bean2;
}

public Bean2 getBean2() {
return this.bean2;
}

@PostConstruct
public void init() {
log.debug("init");
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
log.debug("setBeanFactory({})", beanFactory);
}
}

定义 Bean1FactoryBean,实现 FactoryBean 接口,指定泛型为 Bean1,将其交由 Spring 管理,Bean 的名称是 bean1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
@Component("bean1")
public class Bean1FactoryBean implements FactoryBean<Bean1> {
@Override
public Bean1 getObject() throws Exception {
Bean1 bean1 = new Bean1();
log.debug("create bean: {}", bean1);
return bean1;
}

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

@Override
public boolean isSingleton() {
return true;
}
}

使用这种方式添加到 Spring 容器中的 Bean 的名称是 bean1,但 Bean 的类型不是 Bean1FactoryBean,或者 FactoryBean,而是 Bean1

1
2
3
4
5
6
7
8
9
10
 @ComponentScan
public class A43 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(A43.class);
Bean1 bean1 = context.getBean("bean1", Bean1.class);
System.out.println(bean1);

context.close();
}
}

运行 main()方法后,控制台打印出:

com.itheima.a43.Bean1FactoryBean     - create bean: com.itheima.a43.Bean1@2667f029 
com.itheima.a43.Bean1@2667f029  

Bean1 类型的 Bean 被成功添加到 Spring 容器中,但根据打印的日志信息可以看出这个 Bean 没有经历依赖注入阶段、没有回调 Aware 接口、没有经历初始化阶段,其创建是由重写的 getObject() 方法完成的。

这个 Bean 就真的没有经历 Spring Bean 的生命周期中的任何阶段吗?

定义 Bean1PostProcessor,实现 BeanPostProcessor 接口,在 bean1 初始化前后打印日志信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Component
public class Bean1PostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if ("bean1".equals(beanName) && bean instanceof Bean1) {
log.debug("before [{}] init", beanName);
}
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("bean1".equals(beanName) && bean instanceof Bean1) {
log.debug("after [{}] init", beanName);
}
return bean;
}
}

运行 main()方法后,控制台打印出:

com.itheima.a43.Bean1FactoryBean     - create bean: com.itheima.a43.Bean1@6a28ffa4 
com.itheima.a43.Bean1PostProcessor   - after [bean1] init 
com.itheima.a43.Bean1@6a28ffa4  

bean1 进行了初始化后的增强逻辑,但未进行初始化前的增强逻辑。

创建代理对象的时机就是在初始化后,因此由 FactoryBean 创建的 Bean 可以进行代理增强 。

FactoryBean接口

FactoryBean接口中有三个可以被重写的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

@Nullable
T getObject() throws Exception;

@Nullable
Class<?> getObjectType();

default boolean isSingleton() {
return true;
}
}

其中:

  • getObject() 用于构造 Bean 对象
  • getObjectType() 用于返回 Bean 对象的类型,以便可以通过类型从容器中获取 Bean
  • isSingleton() 每次获取的 Bean 对象是否是单例的

从容器中获取 Bean 时可以通过名称获取、可以通过类型获取、也可以通过名称和类型一起获取。如果重写的 getObjectType() 方法返回了 null,那么通过类型从容器中获取 Bean 时,将抛出 NoSuchBeanDefinitionException 异常,并提示没有指定类型的 Bean。

如果重写的 isSingleton() 方法返回 true,那么每次充容器中获取 Bean 对象都是同一个,反之则不是。

注意:FactoryBean 构造的单例 Bean 不会存放在 DefaultSingletonBeanRegistrysingletonFactories 中,而是在 AbstractAutowireCapableBeanFactoryfactoryBeanInstanceCache 中。

获取FactoryBean类型的 Bean

肯定不能简单地通过名称获取,那会返回其泛型参数类型的 Bean,那通过类型获取呢?比如:

1
context.getBean(Bean1FactoryBean.class)

答案是可行的。

除此之外,还可以在名称前添加 &,然后通过名称来获取:

1
context.getBean("&bean1")

44. @Indexed

Spring 在进行组件扫描时,会遍历项目中依赖的所有 Jar 包中类路径下所有的文件,找到被 @Component 及其衍生注解标记的类,然后把它们组装成 BeanDefinition 添加到 Spring 容器中。

如果扫描的返回过大,势必会大大地影响项目启动速度。

为了优化扫描速度,引入以下依赖,Spring 将扫描过程提前到编译期:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<optional>true</optional>
</dependency>

现有如下类信息:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Bean1 {
}

@Component
public class Bean2 {
}

@Component
public class Bean3 {
}

这几个类都与 A44 存放于同一包路径下:

1
2
3
4
5
6
7
8
9
10
public class A44 {
public static void main(String[] args) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 组件扫描核心类
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(beanFactory);
scanner.scan(A44.class.getPackage().getName());

Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
}
}

运行 main()方法后,控制台打印出:

bean2
bean3
bean1
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory 

bean1bean2bean3 都被添加到 Spring 容器中。

在编译生成的 target 目录下的 classes/META-INF/spring.components 文件里有以下信息:

1
2
3
com.itheima.a44.Bean1=org.springframework.stereotype.Component
com.itheima.a44.Bean2=org.springframework.stereotype.Component
com.itheima.a44.Bean3=org.springframework.stereotype.Component

删除最后两条信息,再次运行 main() 方法

bean1
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory

此时只有 bean1 被添加到 Spring 容器中,也就是说会先以 spring.components 文件中的信息为主。

spring.components 是怎么实现的?

它是在引入 spring-context-indexer 依赖后,在编译期根据类是否被 @Indexed 注解标记,生成 spring.components 文件及内容。

到目前为止,虽然都没显式使用 @Indexed 注解,但它包含在 @Component 注解中:

1
2
3
4
5
6
7
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";
}

总结

导入 spring-context-indexer 依赖后,在编译期根据 @Indexed 生成 META-INF/spring.components 文件。

Spring 在扫描组件时,如果发现 META-INF/spring.components 文件存在,以它为准加载 BeanDefinition,反之遍历包含 Jar 包类路径下所有 class 信息。

45. Spring 代理的特点

在 Spring 的代理中,依赖注入和初始化针对的是目标对象,代理对象和目标对象是两个对象,两者的成员变量不会共享。

确保项目中已导入以下依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

依赖注入和初始化针对的是目标对象

现有如下类信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Slf4j
@Component
public class Bean1 {

protected Bean2 bean2;

protected boolean initialized;

@Autowired
public void setBean2(Bean2 bean2) {
log.debug("setBean2(Bean2 bean2)");
this.bean2 = bean2;
}

@PostConstruct
public void init() {
log.debug("init");
initialized = true;
}

public Bean2 getBean2() {
log.debug("getBean2()");
return bean2;
}

public boolean isInitialized() {
log.debug("isInitialized()");
return initialized;
}
}
1
2
3
@Component
public class Bean2 {
}

Bean1 中的每个方法定制一个前置通知:

1
2
3
4
5
6
7
8
9
10
11
@Aspect
@Component
public class MyAspect {
/**
* 对 Bean1 中所有的方法进行匹配
*/
@Before("execution(* com.itheima.a45.Bean1.*(..))")
public void before() {
System.out.println("before");
}
}

有一 SpringBoot 主启动类,它与 Bean1Bean2MyAspect 在同一包路径下,确保它们能被自动添加到 Spring 容器中:

1
2
3
4
5
6
7
@SpringBootApplication
public class A45 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A45.class, args);
context.close();
}
}

运行 main()方法后,控制台打印出:

com.itheima.a45.Bean1                - setBean2(Bean2 bean2) 
com.itheima.a45.Bean1                - init   

Bean1 中的依赖注入和初始化被成功执行,但 并没有被增强。

由于 Bean1 被增强了,从 Spring 容器中获取的对象将是代理对象:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A45.class, args);
Bean1 proxy = context.getBean(Bean1.class);

proxy.setBean2(new Bean2());
proxy.init();

context.close();
}
before
com.itheima.a45.Bean1                - setBean2(Bean2 bean2) 
before
com.itheima.a45.Bean1                - init   

主动调用的 setBean2()init() 方法 都被增强。

代理对象与目标对象的成员变量不共享

尝试打印代理对象和目标对象的成员变量信息(直接访问,不使用方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A45.class, args);
Bean1 proxy = context.getBean(Bean1.class);

showProxyAndTarget(proxy);

context.close();
}

@SneakyThrows
public static void showProxyAndTarget(Bean1 proxy) {
System.out.println(">>>>> 代理中的成员变量");
System.out.println("\tinitialized = " + proxy.initialized);
System.out.println("\tbean2 = " + proxy.bean2);

if (proxy instanceof Advised) {
Advised advised = (Advised) proxy;
System.out.println(">>>>> 目标中的成员变量");
Bean1 target = (Bean1) advised.getTargetSource().getTarget();
System.out.println("\tinitialized = " + target.initialized);
System.out.println("\tbean2 = " + target.bean2);
}
}
>>>>> 代理中的成员变量
	initialized = false
	bean2 = null
>>>>> 目标中的成员变量
	initialized = true
	bean2 = com.itheima.45.Bean2@771db12c 

由于依赖注入和初始化只针对目标对象,因此代理对象中的成员变量的值都是初始值。

在实际应用过程中,不会直接去访问成员变量,而是通过方法去访问:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A45.class, args);
Bean1 proxy = context.getBean(Bean1.class);

showProxyAndTarget(proxy);

System.out.println(">>>>>>>>>>>>>>>>>>>");
System.out.println(proxy.getBean2());
System.out.println(proxy.isInitialized());

context.close();
}
before
com.itheima.a45.Bean1                - getBean2() 
com.itheima.a45.Bean2@771db12c
before
com.itheima.a45.Bean1                - isInitialized() 
true  

通过方法访问代理对象的成员变量时,这些方法会被增强,同时代理对象中的方法又会去调用目标对象的方法,从而读取出正确的值。

只会对可以被重写的方法进行增强

在 Bean1 中增加几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class Bean1 {
// --snip--

public void m1() {
System.out.println("m1() 成员方法");
}

final public void m2() {
System.out.println("m2() final 方法");
}

static public void m3() {
System.out.println("m3() static 方法");
}

private void m4() {
System.out.println("m4() private 方法");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void main(String[] args) {
// --snip--

// static、final、private 修饰的方法不会被增强
proxy.m1();
proxy.m2();
Bean1.m3();
Method m4 = Bean1.class.getDeclaredMethod("m4");
m4.setAccessible(true);
m4.invoke(proxy);

context.close();
}
before
m1() 成员方法
m2() final 方法
m3() static 方法
m4() private 方法  

能被重写的成员方法成功被增强,但被 final 修饰的、被 static 修饰的方法和private方法由于无法被重写,因此它们不能被增强。如果想增强这些方法,可以使用 AspectJ 编译器增强或者 Agent 类加载。

46. @Value 注入底层

1
2
3
4
5
6
public class Bean1 {
@Value("${JAVA_HOME}")
private String home;
@Value("18")
private int age;
}

需要解析 @Value("${JAVA_HOME}")@Value("18") 的值,其中 JAVA_HOME 以系统环境变量填充,18 为整型。

解析分为两步:

  1. 获取 @Value 注解中 value 属性值;
  2. 解析属性值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
@SuppressWarnings("all")
public class A46 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A46.class);
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();

ContextAnnotationAutowireCandidateResolver resolver =
new ContextAnnotationAutowireCandidateResolver();
resolver.setBeanFactory(beanFactory);

test1(context, resolver);
test2(context, resolver);
}

@SneakyThrows
private static void test1(AnnotationConfigApplicationContext context,
ContextAnnotationAutowireCandidateResolver resolver) {
DependencyDescriptor dd1 =
new DependencyDescriptor(Bean1.class.getDeclaredField("home"), false);
// 获取 @Value 的内容
String value = resolver.getSuggestedValue(dd1).toString();
System.out.println(value);

// 解析 ${}
value = context.getEnvironment().resolvePlaceholders(value);
System.out.println(value);
}
}
${JAVA_HOME}
D:\environment\JDK1.8  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SneakyThrows
private static void test2(AnnotationConfigApplicationContext context,
ContextAnnotationAutowireCandidateResolver resolver) {
DependencyDescriptor dd1 =
new DependencyDescriptor(Bean1.class.getDeclaredField("age"), false);
// 获取 @Value 的内容
String value = resolver.getSuggestedValue(dd1).toString();
System.out.println("@Value 的 value 属性值: " + value);

// 解析 ${}
value = context.getEnvironment().resolvePlaceholders(value);
System.out.println("解析得到的值: " + value);
System.out.println("解析得到的值的类型: " + value.getClass());
// 转成字段的类型
Object age = context.getBeanFactory()
.getTypeConverter()
.convertIfNecessary(value, dd1.getDependencyType());
System.out.println("转换后的类型: " + age.getClass());
}
@Value 的 value 属性值: 18
解析得到的值: 18
解析得到的值的类型: class java.lang.String
转换后的类型: class java.lang.Integer  

EL 表达式的解析

有如下几个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Bean2 {
@Value("#{@bean3}")
private Bean3 bean3;
}

@Component("bean3")
public class Bean3 {
}

static class Bean4 {
@Value("#{'hello, ' + '${JAVA_HOME}'}")
private String value;
}

同样要求解析 @Value 中的 value 属性值。

如果沿用 test2() 方法进行解析,控制台打印出:

@Value 的 value 属性值: #{@bean3}
解析得到的值: #{@bean3}
解析得到的值的类型: class java.lang.String
Exception in thread "main" org.springframework.beans.ConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'com.itheima.a46.A46$Bean3'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.itheima.a46.A46$Bean3': no matching editors or conversion strategy found

最后一步数据转换出了问题,无法将 String 转换成 A46$Bean3 类型,也就是说解析 @bean3 失败了,程序仍然把它当成字符串,而不是注入的 Bean。

为了解析成功,需要在转换前解析 #{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@SneakyThrows
public static void main(String[] args) {
// --snip--

test3(context, resolver, Bean2.class.getDeclaredField("bean3"));
System.out.println(">>>>>>>>>>>>>>>>>>>");
test3(context, resolver, Bean4.class.getDeclaredField("value"));
}

private static void test3(AnnotationConfigApplicationContext context,
ContextAnnotationAutowireCandidateResolver resolver,
Field field) {
DependencyDescriptor dd1 = new DependencyDescriptor(field, false);
// 获取 @Value 的内容
String value = resolver.getSuggestedValue(dd1).toString();
System.out.println("@Value 的 value 属性值: " + value);

// 解析 ${}
value = context.getEnvironment().resolvePlaceholders(value);
System.out.println("解析得到的值: " + value);
System.out.println("解析得到的值的类型: " + value.getClass());

// 解析 #{}
Object bean3 = context.getBeanFactory()
.getBeanExpressionResolver()
.evaluate(value, new BeanExpressionContext(context.getBeanFactory(), null));

// 类型转换
Object result = context.getBeanFactory()
.getTypeConverter()
.convertIfNecessary(bean3, dd1.getDependencyType());
System.out.println("转换后的类型: " + result.getClass());
}
@Value 的 value 属性值: #{@bean3}
解析得到的值: #{@bean3}
解析得到的值的类型: class java.lang.String
转换后的类型: class com.itheima.a46.A46$Bean3
>>>>>>>>>>>>>>>>>>>
@Value 的 value 属性值: #{'hello, ' + '${JAVA_HOME}'}
解析得到的值: #{'hello, ' + 'D:\environment\JDK1.8'}
解析得到的值的类型: class java.lang.String
转换后的类型: class java.lang.String  

47. @Autowired 注入底层

47.1 注入方式

按成员变量类型注入

现有Bean1 类如下:

1
2
3
4
static class Bean1 {
@Autowired
private Bean2 bean2;
}

需要被注入的对象所在类:

1
2
3
@Component("bean2")
static class Bean2 {
}

从容器中获取需要被注入的 Bean 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class A47_1{
@SneakyThrows
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A47_1.class);
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();

DependencyDescriptor dd1 =
new DependencyDescriptor(Bean1.class.getDeclaredField("bean2"), false);
System.out.println(beanFactory.doResolveDependency(dd1, "bean1", null, null));
}
}
com.itheima.a47.A47_1$Bean2@222545dc  

按参数类型注入

Bean1 进行修改:

1
2
3
4
5
6
7
8
static class Bean1 {
// --snip--

@Autowired
public void setBean2(Bean2 bean2) {
this.bean2 = bean2;
}
}

根据 setBean2() 方法的 Bean2 类型参数进行注入:

1
2
3
4
5
6
7
8
9
@SneakyThrows
public static void main(String[] args) {
// --snip--

Method setBean2 = Bean1.class.getDeclaredMethod("setBean2", Bean2.class);
DependencyDescriptor dd2 =
new DependencyDescriptor(new MethodParameter(setBean2, 0), false);
System.out.println(beanFactory.doResolveDependency(dd2, "bean1", null, null));
}
com.itheima.a47.A47_1$Bean2@222545dc  

包装为 Optional<Bean2>

对 Bean1 进行修改:

1
2
3
4
5
6
static class Bean1 {
// --snip--

@Autowired
private Optional<Bean2> bean3;
}

如果直接按照以下方式获取 DependencyDescriptor 对象:

1
2
DependencyDescriptor dd3 =
new DependencyDescriptor(Bean1.class.getDeclaredField("bean3"), false);

dd3.getDependencyType() 方法将返回 OptionalClass 对象,这显然是不对的。

Spring 为 DependencyDescriptor 提供了解决这个问题的方法,即“增加嵌套等级”来获取内嵌类型:

1
dd3.increaseNestingLevel();

执行 increaseNestingLevel() 方法后,dd3.getDependencyType() 方法返回的 Bean2Class 对象。

因此注入 Optional<Bean2> 类型的成员变量应该按照:

1
2
3
4
5
6
7
8
9
10
11
12
@SneakyThrows
public static void main(String[] args) {
// --snip--

DependencyDescriptor dd3 =
new DependencyDescriptor(Bean1.class.getDeclaredField("bean3"), false);
if (Optional.class.equals(dd3.getDependencyType())) {
dd3.increaseNestingLevel();
Object result = beanFactory.doResolveDependency(dd3, "bean1", null, null);
System.out.println(Optional.ofNullable(result));
}
}
Optional[com.itheima.a47.A47_1$Bean2@222545dc]  

注入 Optional 对象和使用 @Autowired(required = false) 的作用是一样的,当容器中不存在目标 Bean 时,不会抛出 NoSuchBeanDefinitionException 异常。

包装为 ObjectFactory<Bean2>

Bean1 进行修改:

1
2
3
4
5
6
static class Bean1 {
// --snip--

@Autowired
private ObjectFactory<Bean2> bean4;
}

注入 ObjectFactory<Bean2> 类型的对象与注入 Optional<Bean2> 类型的对象类似,只不过 ObjectFactory 提供了 延迟注入 的能力,也就是说 Bean2 对象不会立即被注入,而是在需要时才被注入。

ObjectFactory 是一个函数式接口:

1
2
3
4
@FunctionalInterface
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}

注入的应该是 ObjectFactory 对象,在调用该对象的 getObject() 方法时,Bean2 对象才被注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SneakyThrows
public static void main(String[] args) {
// --snip--

DependencyDescriptor dd4 =
new DependencyDescriptor(Bean1.class.getDeclaredField("bean4"), false);
if (ObjectFactory.class.equals(dd4.getDependencyType())) {
dd4.increaseNestingLevel();
ObjectFactory<Bean2> objectFactory = () ->
(Bean2) beanFactory.doResolveDependency(dd4, "bean1", null, null);
System.out.println(objectFactory.getObject());
}
}
com.itheima.a47.A47_1$Bean2@222545dc  

ObjectFactory 类似的还有个名为 ObjectProvider 的接口,后者继承了前者。

ObjectFactory 相比,ObjectProvider 提供了类似于 Optional 的安全注入功能,当容器中不存在目标 Bean 时, 不会抛出 NoSuchBeanDefinitionException 异常。ObjectProvider 提供的 getIfAvailable() 在获取不存在的 Bean 时,不会抛出异常,而是返回 null

@Lazy 的处理

Bean1 进行修改,在成员变量 bean2 上使用 @Lazy 注解:

1
2
3
4
5
6
7
static class Bean1 {
@Autowired
@Lazy
private Bean2 bean2;

// --snip--
}

对于 @Lazy 注解标记的成员变量,注入的对象不再是目标对象,而是其代理对象,因此不能使用 DefaultListableBeanFactory 对象的 doResolveDependency() 方法来获取注入的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void main(String[] args) {
// --snip--

DependencyDescriptor dd5 =
new DependencyDescriptor(Bean1.class.getDeclaredField("bean2"), false);
ContextAnnotationAutowireCandidateResolver resolver =
new ContextAnnotationAutowireCandidateResolver();
resolver.setBeanFactory(beanFactory);
// 根据 @Lazy 创建代理对象
Object proxy = resolver.getLazyResolutionProxyIfNecessary(dd5, "bean1");
System.out.println(proxy);
System.out.println(proxy.getClass());
}
com.itheima.a47.A47_1$Bean2@222545dc
class com.itheima.a47.A47_1$Bean2$$EnhancerBySpringCGLIB$$d631a20c  

@Lazy 实现的 延迟注入 (前面讲的 ObjectFactoryObjectProvider 也有延迟注入功能,但与 @Lazy 的实现不一样)不是不注入,而是注入目标对象的代理对象,当使用到代理对象中的方法时,代理对象就会去 Spring 容器中寻找真正的目标对象,然后调用目标对象对应的方法。

@Lazy 的实现细节可以在 ContextAnnotationAutowireCandidateResolver 中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ContextAnnotationAutowireCandidateResolver
extends QualifierAnnotationAutowireCandidateResolver {

@Override
@Nullable
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor,
@Nullable String beanName) {
// 如果有 @Lazy 注解,就创建代理对象
return (isLazy(descriptor) ?
buildLazyResolutionProxy(descriptor, beanName) : null);
}

protected boolean isLazy(DependencyDescriptor descriptor) {
for (Annotation ann : descriptor.getAnnotations()) {
// 获取 @Lazy 注解信息
Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
// --snip--
}
return false;
}

protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor,
final @Nullable String beanName) {
// --snip--
// 获取代理对象
return pf.getProxy(dlbf.getBeanClassLoader());
}

}

补充:包装为 Provider<Bean2>

Provider 接口是由 JSR-330 提出,要想使用此接口,需要导入以下依赖:

1
2
3
4
5
6
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<!-- 不要惊讶,版本号就是 1 -->
<version>1</version>
</dependency>

对 Bean1 进行修改:

1
2
3
4
5
6
static class Bean1 {
// --snip--

@Autowired
private Provider<Bean2> bean5;
}

注入 Provider 类型的对象与注入 ObjectFactory<Bean2> 类型的对象极其相似,Provider 也提供了 延迟注入 的能力,注入的是 Provider 对象,在调用该对象的 get() 方法时,Bean2 对象才被注入:

1
2
3
4
5
6
7
8
9
10
11
12
@SneakyThrows
public static void main(String[] args) {
// --snip--

DependencyDescriptor dd6 = new DependencyDescriptor(Bean1.class.getDeclaredField("bean5"), false);
if (Provider.class.equals(dd6.getDependencyType())) {
dd6.increaseNestingLevel();
Provider<Bean2> provider = () ->
(Bean2) beanFactory.doResolveDependency(dd6, "bean1", null, null);
System.out.println(provider.get());
}
}
com.itheima.a47.A47_1$Bean2@222545dc  

Optional 类型、ObjectFactory 类型、ObjectProvider 类型、JSR-330 提供的类型的注入逻辑可在 DefaultListableBeanFactory#resolveDependency() 方法中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
@Nullable
public Object resolveDependency(DependencyDescriptor descriptor,
@Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames,
@Nullable TypeConverter typeConverter) throws BeansException {

descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
// 对 Optional 的处理
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
// 对 ObjectFactory、ObjectProvider 的处理
}
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
// 对 JSR-330 提供的类型的处理
}
else {
// 其他处理
}
}

47.2 类型匹配细节

无论是 @Value 注入,还是 @Autowired 注入,最终都会调用 DefaultListableBeanFactory#doResolveDependency() 方法。

现有如下几个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface Dao<T> {

}

@Component("dao1")
static class Dao1 implements Dao<Student> {
}

@Component("dao2")
static class Dao2 implements Dao<Teacher> {
}

static class Student {
}

static class Teacher {
}

interface Service {
}

@Component("service1")
static class Service1 implements Service {
}

@Component("service2")
static class Service2 implements Service {
}

@Component("service3")
static class Service3 implements Service {
}

有一目标类 Target,对其进行依赖注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
static class Target {
@Autowired
private Service[] serviceArray;
@Autowired
private List<Service> serviceList;
@Autowired
private ConfigurableApplicationContext applicationContext;
@Autowired
private Dao<Teacher> dao;
@Autowired
@Qualifier("service2")
private Service service;
}

数据类型

Spring 容器中肯定不存在数组类型且元素类型为 Service 的 Bean 对象,因此注入的 Service 数组应当是容器中 Service 类型的 Bean 数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Configuration
@SuppressWarnings("all")
public class A47_2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A47_2.class);
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();

testArray(beanFactory);
}

@SneakyThrows
private static void testArray(DefaultListableBeanFactory beanFactory) {
DependencyDescriptor dd1 =
new DependencyDescriptor(Target.class.getDeclaredField("serviceArray"), true);
if (dd1.getDependencyType().isArray()) {
// 获取数组中的元素类型
Class<?> componentType = dd1.getDependencyType().getComponentType();
System.out.println(componentType);
String[] names = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
beanFactory,
componentType
);
List<Object> beans = new ArrayList<>();
for (String name : names) {
System.out.println(name);
Object bean = dd1.resolveCandidate(name, componentType, beanFactory);
beans.add(bean);
}
Object array = beanFactory.getTypeConverter()
.convertIfNecessary(beans, dd1.getDependencyType());
System.out.println(array);
}
}
}
interface com.itheima.a47.A47_2$Service
service3
service2
service1
com.itheima.a47.A47_2$Service;@49139829  

相关源码可在 DefaultListableBeanFactory#resolveMultipleBeans() 方法中看到。

List类型

注入 List<Service> 类型数据的逻辑与注入 Service[] 类型数据的逻辑类似,只不过在容器中寻找目标 Bean 时不再通过数组元素类型,而是通过 List 的泛型类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SneakyThrows
private static void testList(DefaultListableBeanFactory beanFactory) {
DependencyDescriptor dd2 =
new DependencyDescriptor(Target.class.getDeclaredField("serviceList"), true);
if (List.class.equals(dd2.getDependencyType())) {
// 获取泛型信息
Class<?> resolve = dd2.getResolvableType().getGeneric().resolve();
System.out.println(resolve);
List<Object> list = new ArrayList<>();
String[] names =
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, resolve);
for (String name : names) {
Object bean = dd2.resolveCandidate(name, resolve, beanFactory);
list.add(bean);
}
System.out.println(list);
}
}
interface com.itheima.a47.A47_2$Service
[com.itheima.a47.A47_2$Service3@35e2d654, com.itheima.a47.A47_2$Service2@1bd4fdd, com.itheima.a47.A47_2$Service1@55183b20]  

注意: 对于注入的集合类型数据,注入的类型必须是 Collection 及其 子接口,比如不支持直接注入 ArrayList 类型的数据。

相关源码可在 DefaultListableBeanFactory#resolveMultipleBeans() 方法中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Nullable
private Object resolveMultipleBeans(DependencyDescriptor descriptor,
@Nullable String beanName,
@Nullable Set<String> autowiredBeanNames,
@Nullable TypeConverter typeConverter) {
Class<?> type = descriptor.getDependencyType();
if (descriptor instanceof StreamDependencyDescriptor) {
// --snip--
}
else if (type.isArray()) {
// --snip--
}
else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
// 就是这里的判断
}
else if (Map.class == type) {
// --snip--
}
else {
return null;
}
}

从源码中可以看到,@Autowired 还支持 Map 类型数据的注入,此时注入的 Map 的 key 是 Bean 的名称,value 是 Bean 对象,这种方式常常配合策略模式使用。需要注意的是,只支持注入 Map 接口,不支持其子类。

特殊类型 ConfigurableApplicationContext

ConfigurableApplicationContextApplicationContext 接口的子接口。

需要注意的是,在 Spring 容器中并不存在 ConfigurableApplicationContext 类型、或 ApplicationContext 类型的 Bean。

Spring 容器中的所有单例 Bean 对象存放在 DefaultListableBeanFactory 中,在 DefaultListableBeanFactory 父类 DefaultSingletonBeanRegistry 中有一成员变量:

1
2
3
4
5
6
7
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// --snip--

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// --snip--
}

singletonObjects 用于存放 Spring 容器中的所有单例 Bean 对象。

类似 ApplicationContextBeanFactory 类型的对象则是放在 DefaultListableBeanFactory 中的 resolvableDependencies 中:

1
2
3
4
5
6
7
8
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
// --snip--

private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<>(16);

// --snip--
}

这些特殊对象是在调用 ApplicationContextrefresh() 方法时添加到 resolvableDependencies 中的。可在 AbstractApplicationContextrefresh() 方法中看到:

1
2
3
4
5
6
7
8
9
10
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// --snip--

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

// --snip--
}
}
1
2
3
4
5
6
7
8
9
10
11
12
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// --snip--

// BeanFactory interface not registered as resolvable type in a plain factory.
// MessageSource registered (and found for autowiring) as a bean.
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// --snip--
}

因此在注入诸如 ConfigurableApplicationContext 特殊类型的对象时,不能直接使用 getBean() 方法获取,而是应该从 resolvableDependencies 集合中获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SneakyThrows
private static void testApplicationContext(DefaultListableBeanFactory beanFactory) {
DependencyDescriptor dd3 = new DependencyDescriptor(
Target.class.getDeclaredField("applicationContext"),
true
);
Field resolvableDependencies =
DefaultListableBeanFactory.class.getDeclaredField("resolvableDependencies");
resolvableDependencies.setAccessible(true);
Map<Class<?>, Object> dependencies =
(Map<Class<?>, Object>) resolvableDependencies.get(beanFactory);
for (Map.Entry<Class<?>, Object> entry : dependencies.entrySet()) {
// 左边类型 右边类型
if (entry.getKey().isAssignableFrom(dd3.getDependencyType())) {
System.out.println(entry.getValue());
break;
}
}
}
org.springframework.beans.factory.support.DefaultListableBeanFactory@7364985f: defining beans
 [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,
org.springframework.context.annotation.internalAutowiredAnnotationProcessor,
org.springframework.context.annotation.internalCommonAnnotationProcessor,
org.springframework.context.event.internalEventListenerProcessor,
org.springframework.context.event.internalEventListenerFactory,
a47_2,service3,service2,service1,dao2,dao1]; root of factory hierarchy  

泛型类型

容器中 Dao 类型的 Bean 有多个,而依赖注入的是 Dao<Teacher> 类型的对象,因此需要判断容器中的 Bean 对象泛型类型是否为指定类型。判断逻辑可以使用 ContextAnnotationAutowireCandidateResolverisAutowireCandidate() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SneakyThrows
private static void testGeneric(DefaultListableBeanFactory beanFactory) {
DependencyDescriptor dd4 =
new DependencyDescriptor(Target.class.getDeclaredField("dao"), true);
Class<?> type = dd4.getDependencyType();
ContextAnnotationAutowireCandidateResolver resolver =
new ContextAnnotationAutowireCandidateResolver();
resolver.setBeanFactory(beanFactory);
// 循环所有的目标类型 Bean 名称
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) {
BeanDefinition bd = beanFactory.getMergedBeanDefinition(name);
// 对比 BeanDefinition 的泛型与 DependencyDescriptor 的泛型是否匹配
if (resolver.isAutowireCandidate(new BeanDefinitionHolder(bd, name), dd4)) {
System.out.println(name);
System.out.println(dd4.resolveCandidate(name, type, beanFactory));
}
}
}
dao2
com.itheima.a47.A47_2$Dao2@74f0ea28  

@Qualifier

当容器中存在多个相同类型的 Bean 对象,在执行依赖注入时可以使用 @Qualifier 注解来指定需要注入的 Bean 对象的名称。判断逻辑同样使用 ContextAnnotationAutowireCandidateResolverisAutowireCandidate() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SneakyThrows
private static void testQualifier(DefaultListableBeanFactory beanFactory) {
DependencyDescriptor dd5 =
new DependencyDescriptor(Target.class.getDeclaredField("service"), true);
Class<?> type = dd5.getDependencyType();
ContextAnnotationAutowireCandidateResolver resolver =
new ContextAnnotationAutowireCandidateResolver();
resolver.setBeanFactory(beanFactory);
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) {
BeanDefinition bd = beanFactory.getMergedBeanDefinition(name);
// DependencyDescriptor 对象中包含了 @Qualifier 注解信息
if (resolver.isAutowireCandidate(new BeanDefinitionHolder(bd, name), dd5)) {
System.out.println(name);
System.out.println(dd5.resolveCandidate(name, type, beanFactory));
}
}
}
service2
com.itheima.a47.A47_2$Service2@1bd4fdd  

@Primary

当容器中存在多个相同类型的 Bean 对象时,在执行依赖注入时除了可以使用 @Qualifier 注解外,还可以在被注入的 Bean 对象所在类上使用 @Primary 注解,指定执行依赖注入时使用的主要 Bean 对象。

如果 Bean 对象的所在类被 @Primary 注解标记,那么在构造 BeanDefinition 时就会记录这个信息。

通常情况下,@Primary 注解只有一个作用在同种类型的 Bean 上,存在多个时,Spring 依旧无法区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class Target1 {
@Autowired
private Service service;
}

interface Service {
}

@Component("service1")
static class Service1 implements Service {
}

@Primary
@Component("service2")
static class Service2 implements Service {
}

@Component("service3")
static class Service3 implements Service {
}
1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
private static void testPrimary(DefaultListableBeanFactory beanFactory) {
DependencyDescriptor dd =
new DependencyDescriptor(Target1.class.getDeclaredField("service"), false);
Class<?> type = dd.getDependencyType();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) {
if (beanFactory.getMergedBeanDefinition(name).isPrimary()) {
System.out.println("primary: " + name);
}
}
}
primary: service2  

默认规则

当容器中存在多个相同类型的 Bean 对象时,除了使用 @Qualifier@Primary 注解外,@Autowired 注解还支持按照成员变量名称进行匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static class Target2 {
@Autowired
private Service service3;
}

interface Service {
}

@Component("service1")
static class Service1 implements Service {
}

@Primary
@Component("service2")
static class Service2 implements Service {
}

@Component("service3")
static class Service3 implements Service {
}
1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
private static void testDefault(DefaultListableBeanFactory beanFactory) {
DependencyDescriptor dd =
new DependencyDescriptor(Target2.class.getDeclaredField("service3"), false);
Class<?> type = dd.getDependencyType();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) {
if (name.equals(dd.getDependencyName())) {
System.out.println("default: " + name);
}
}
}
default: service3

48. 事件 - 监听器

Spring 提供了事件发布 - 监听(订阅)机制,利用该机制可以令主要业务与附加业务解耦,是观察者模式的一种体现。

本节介绍事件的监听,事件的发布将在下一节进行介绍。

48.1 监听器的实现

实现 ApplicationListener 接口

需求模拟:假设现有一个【主线业务】,要求在【主线业务】执行完成之后,执行【发送短信】、【发送邮件】。

常规写法是将这三种业务依次写在一个方法里,程序顺序执行它们,但这将增加代码的耦合性。

利用 Spring 提供的事件发布 - 监听(订阅)机制来解决这个问题可以大大地降低代码的耦合性。

首先定义事件的类型,用于后续的发布和监听,该事件类必须继承 ApplicationEvent 抽象类:

1
2
3
4
5
6
7
static class MyEvent extends ApplicationEvent {
private static final long serialVersionUID = -1541319641201302606L;

public MyEvent(Object source) {
super(source);
}
}

【主线业务】执行完成后,发送事件:

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@Component
static class MyService {
@Autowired
private ApplicationEventPublisher publisher;

public void doBusiness() {
log.debug("主线业务");

publisher.publishEvent(new MyEvent("MyService.doBusiness()"));
}
}

之后定义两个监听器,监听发布的事件,执行【发送短信】、【发送邮件】,定义的监听器需要实现 ApplicationListener 接口,并交由 Spring 管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@Component
static class SmsApplicationListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
log.debug("发送短信");
}
}

@Slf4j
@Component
static class EmailApplicationListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
log.debug("发送邮件");
}
}

最后调用 doBusiness() 串联整个逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@Configuration
public class A48_1 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A48_1.class);
context.getBean(MyService.class).doBusiness();
context.close();
}

// --snip--
}

运行 main()方法后,控制台打印出:

[main] com.itheima.a48.A48_1$MyService      - 主线业务 
[main] i.m.a.A48_1$EmailApplicationListener - 发送邮件 
[main] i.m.a.A48_1$SmsApplicationListener  - 发送短信   

使用 @EventListener 注解

监听器的实现除了实现 ApplicationListener 接口外,还可以使用 @EventListener 注解。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@Component
static class SmsService {
@EventListener
public void listener(MyEvent myEvent) {
log.debug("发送短信");
}
}

@Slf4j
@Component
static class EmailService {
@EventListener
public void listener(MyEvent myEvent) {
log.debug("发送邮件");
}
}

运行 main() 方法后,控制台输出相同的内容。

48.2 异步事件

从上文控制台输出的信息可知,事件都是在主线程 main 中被监听,都是同步执行的。

那怎么发送异步事件呢?

ApplicationEventPublisher 底层利用了 SimpleApplicationEventMulticaster 来发布事件,SimpleApplicationEventMulticaster 在发布事件时可以指定是否使用线程池,如果实现了线程池,那么就是异步事件,反之为同步。

因此可以先添加一个线程池 Bean,然后使 SimpleApplicationEventMulticaster 利用这个线程池 Bean 来发送事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
return executor;
}

/**
* 方法名称必须是 applicationEventMulticaster,才能对 Bean 进行覆盖
*/
@Bean
public SimpleApplicationEventMulticaster applicationEventMulticaster(ThreadPoolTaskExecutor executor) {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
// 使用线程池异步发送事件
multicaster.setTaskExecutor(executor);
return multicaster;
}

注意:SimpleApplicationEventMulticaster 类型的 Bean 的名称必须是 applicationEventMulticaster,这样才能覆盖原本未使用线程池的 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class A48_2 {

@SneakyThrows
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A48_2.class);
context.getBean(MyService.class).doBusiness();

// 睡两秒,消息监听成功后才关闭容器
TimeUnit.SECONDS.sleep(2);
context.close();
}

// --snip--
}

运行 main()方法后,控制台打印出:

[main] com.itheima.a48.A48_2$MyService      - 主线业务 
[executor-1] com.itheima.a48.A48_2$EmailService   - 发送邮件 
[executor-2] com.itheima.a48.A48_2$SmsService     - 发送短信   

【主线业务】、【发送短信】和【发送邮件】都在不同线程中被执行。

【补充】@Async 实现异步事件

实际开发中,在监听器方法或监听器类上添加 @Async 注解即可实现异步事件。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Component
static class SmsService {
@Async
@EventListener
public void listener(MyEvent myEvent) {
log.debug("发送短信");
}
}

@Slf4j
@Component
static class EmailService {
@Async
@EventListener
public void listener(MyEvent myEvent) {
log.debug("发送邮件");
}
}

运行 main() 方法后,根据控制台打印出的信息可知仍是同步消息,这是因为 没有开启异步支持。

在配置类上使用 @EnableAsync 注解,开启异步支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 @Slf4j
@Configuration
@EnableAsync(proxyTargetClass = true)
public class TestAsync {

public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(TestAsync.class);
context.getBean(MyService.class).doBusiness();

context.close();
}

// --snip--

@Bean
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
return executor;
}
}

也可以注入自行实现的线程池 Bean,使用这个 Bean 来发布异步事件。当然也可以不注入,Spring 提供了默认的线程池。

建议: 使用 @EnableAsync 注解时,尽量指定 proxyTargetClass 的属性值为 true,采用 CGLib 动态代理,避免监听器类实现接口而监听器方法又未在基类中声明时,导致使用默认 JDK 动态代理失败。

运行 main() 方法,控制台打印出:

[main] com.itheima.a48.TestAsync$MyService  - 主线业务 
[executor-2] com.itheima.a48.TestAsync$SmsService - 发送短信 
[executor-1] com.itheima.a48.TestAsync$EmailService  - 发送邮件  

48.3 自定义事件监听注解

无论是实现 ApplicationListener 接口,还是使用 @EventListener 注解,监听器类都需要交由 Spring 管理,那 Spring 是怎么实现事件的监听的呢?

以自定义事件监听注解为例,简单了解 Spring 的事件监听机制。

自定义 @MyListener 注解:

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyListener {
}

定义事件类型 MyEvent

1
2
3
4
5
6
7
static class MyEvent extends ApplicationEvent {
private static final long serialVersionUID = -6388410688691384516L;

public MyEvent(Object source) {
super(source);
}
}

定义两个监听器类,监听 MyEvent 事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@Component
static class SmsService {
@MyListener
public void listener(MyEvent myEvent) {
log.debug("发送短信");
}
}

@Slf4j
@Component
static class EmailService {
@MyListener
public void listener(MyEvent myEvent) {
log.debug("发送邮件");
}
}

再定义 MyService,在该业务类中的 doBusiness() 方法发送 MyEvent 事件:

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@Component
static class MyService {
@Autowired
private ApplicationEventPublisher publisher;

public void doBusiness() {
log.debug("主线业务");
publisher.publishEvent(new MyEvent("MyService.doBusiness()"));
}
}

接下来就是解析 @MyListener 注解了:

  1. 首先需要获取容器中的所有 Bean 对象
  2. 查看这些 Bean 对象中是否存在被 @MyListener 注解标记的方法
  3. 将被 @MyListener 注解标记的方法转换成事件监听器添加到 Spring 容器中(适配器模式),这些事件监听器需要判断监听的事件类型是否与原始方法的参数类型一致,一致的情况下才执行方法
  4. 在第一步中需要拿到容器中的所有 Bean 对象,因此前面的逻辑要在 Spring 中所有单例 Bean 初始化完成后才执行,可以使用 SmartInitializingSingleton 接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Bean
public SmartInitializingSingleton smartInitializingSingleton(ConfigurableApplicationContext context) {
// Spring 中所有单例 Bean 初始化完成后调用此处理器
return () -> {
for (String name : context.getBeanDefinitionNames()) {
Object bean = context.getBean(name);
for (Method method : bean.getClass().getMethods()) {
if (method.isAnnotationPresent(MyListener.class)) {
// 添加事件监听器
context.addApplicationListener((event) -> {
// 监听器方法的事件类型
Class<?> eventType = method.getParameterTypes()[0];
if (eventType.isAssignableFrom(event.getClass())) {
try {
method.invoke(bean, event);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
}
};
}
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class A48_3 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(A48_3.class);
context.getBean(MyService.class).doBusiness();
context.close();
}

// --snip--
}
[main] com.itheima.a48.A48_3$MyService      - 主线业务 
[main] com.itheima.a48.A48_3$EmailService   - 发送邮件 
[main] com.itheima.a48.A48_3$SmsService     - 发送短信  

48.4 【补充】监听器执行顺序

当一个事件有多个对应的监听器时,这些监听器的执行顺序是不确定的。

如果需要监听器按指定的顺序执行,可以使监听器类实现 SmartApplicationListener 接口,重写 getOrder() 方法,指定监听器优先级。除此之外,使用 @Order 注解、在实现 ApplicationListener 接口的基础上再实现 Ordered 接口也能实现相同的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Slf4j
@Component
static class SmsService1 {
@Order(1)
@EventListener
public void listener(MyEvent myEvent) {
log.debug("发送短信-1");
}
}

@Slf4j
@Component
static class EmailService1 {
@Order(2)
@EventListener
public void listener(MyEvent myEvent) {
log.debug("发送邮件-1");
}
}

@Slf4j
@Component
static class SmsService2 implements ApplicationListener<MyEvent>, Ordered {

@Override
public int getOrder() {
return 3;
}

@Override
public void onApplicationEvent(MyEvent event) {
log.debug("发送短信-2");
}
}

@Slf4j
@Component
static class EmailService2 implements SmartApplicationListener {

@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return MyEvent.class.equals(eventType);
}

@Override
public int getOrder() {
return 4;
}

@Override
public void onApplicationEvent(ApplicationEvent event) {
log.debug("发送邮件-2");
}
}
[main] i.m.a.TestSmartApplicationListener$MyService - 主线业务 
[main] i.m.a.TestSmartApplicationListener$SmsService1 - 发送短信-1 
[main] i.m.a.TestSmartApplicationListener$EmailService1 - 发送邮件-1 
[main] i.m.a.TestSmartApplicationListener$SmsService2 - 发送短信-2 
[main] i.m.a.TestSmartApplicationListener$EmailService2 - 发送邮件-2   

48.5 【补充】事件的传播

当 Spring 容器嵌套 Spring 容器时,通过子容器发布事件,能够在父容器监听到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
parent.registerBean(MyListener.class);
parent.refresh();

AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext();
child.setParent(parent);
child.refresh();

// 通过子容器发布事件,能够在父容器监听到
child.publishEvent(new MyEvent("子容器发送的事件..."));
}

static class MyEvent extends ApplicationEvent {
private static final long serialVersionUID = -7002403082731659626L;

public MyEvent(String source) {
super(source);
}
}

@Slf4j
static class MyListener {
@EventListener
public void listener(MyEvent myEvent) {
log.debug(String.valueOf(myEvent.getSource()));
}
}
[main] i.m.a.EventPropagationTest$MyListener - 子容器发送的事件... 

48.6 【补充】带泛型的事件

现有一个事件类 MutationEvent,接收一个泛型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
static class MutationEvent<T> extends ApplicationEvent {
private static final long serialVersionUID = -2718823625228147843L;

private final T source;

private final String type;

public MutationEvent(T data, String type) {
super(data);
this.source = data;
this.type = type;
}
}

事件对应的监听器 MutationEventListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Getter
@AllArgsConstructor
static class Pizza {
private final String name;
private final double price;
}

@Getter
@AllArgsConstructor
static class ChineseHamburger {
private final double price;
private final String size;
}

static class MutationEventListener {
@EventListener
public void handlePizza(MutationEvent<Pizza> event) {
System.out.println("监听到 Pizza...");
System.out.println("类型是: " + event.getType());
Pizza pizza = event.getSource();
System.out.println("Pizza 名称为: " + pizza.getName()
+ ", 价格为: " + pizza.getPrice());
}

@EventListener
public void handleChineseHamburger(MutationEvent<ChineseHamburger> event) {
System.out.println("监听到肉夹馍...");
System.out.println("类型是: " + event.getType());
ChineseHamburger hamburger = event.getSource();
System.out.println("肉夹馍的价格是: " + hamburger.getPrice()
+ ", 大小是: " + hamburger.getSize());
}
}

尝试发布 MutationEvent<Pizza> 类型的事件,看看监听器是否能监听到:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(MutationEventListener.class);
context.refresh();

Pizza pizza = new Pizza("NewYorkPizza", 25);
context.publishEvent(new MutationEvent<>(pizza, "ONE"));
}

运行 main() 方法后,抛出 ClassCastException 异常,提示无法将 Pizza 转换成 ChineseHamburger,事件监听失败。

由于泛型擦除,无法通过事件真正的内部对象类型来分发事件,为了解决这个问题,需要使类实现 ResolvableTypeProvider 接口。

如果未实现 ResolvableTypeProvider 接口:

  • 但实现了 ApplicationEvent 接口,尽管在监听器方法和发布事件时都指定了泛型参数信息,但所有的监听器方法都会被执行,由此可能产生 ClassCastException
  • 也未实现 ApplicationEvent 接口,就算发送的泛型事件的内部对象类型与监听器指定的泛型事件的内部对象类型一样,也不会监听成功。
1
2
3
4
5
6
7
8
9
10
@Getter
static class MutationEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
// --snip--

@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(),
ResolvableType.forInstance(this.source));
}
}

再次运行 main() 方法后,不再抛出异常,控制台打印出:

监听到 Pizza...
类型是: ONE
Pizza 名称为: NewYorkPizza, 价格为: 25.0  

再发布泛型参数类型为 ChineseHamburger 的事件:

1
2
3
4
5
6
public static void main(String[] args) {
// --snip--

ChineseHamburger hamburger = new ChineseHamburger(18, "M");
context.publishEvent(new MutationEvent<>(hamburger, "TWO"));
}
监听到 Pizza...
类型是: ONE
Pizza 名称为: NewYorkPizza, 价格为: 25.0
监听到肉夹馍...
类型是: TWO
肉夹馍的价格是: 18.0, 大小是: M  

49. 事件 - 发布器

49.1 自定义事件发布

前文说到,事件的发布使用了 SimpleApplicationEventMulticaster,它的顶层接口是 ApplicationEventMulticaster,尝试自定义 ApplicationEventMulticaster 的实现类,实现事件的发布。

ApplicationEventMulticaster 接口的抽象方法有很多,本节只实现重要方法,采用默认适配器处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
abstract static class AbstractApplicationEventMulticaster implements ApplicationEventMulticaster {

@Override
public void addApplicationListener(ApplicationListener<?> listener) {
}

@Override
public void addApplicationListenerBean(String listenerBeanName) {
}

@Override
public void removeApplicationListener(ApplicationListener<?> listener) {
}

@Override
public void removeApplicationListenerBean(String listenerBeanName) {
}

@Override
public void removeApplicationListeners(Predicate<ApplicationListener<?>> predicate) {
}

@Override
public void removeApplicationListenerBeans(Predicate<String> predicate) {
}

@Override
public void removeAllListeners() {
}

@Override
public void multicastEvent(ApplicationEvent event) {
}

@Override
public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
}
}

需要实现的方法有两个:

  • addApplicationListenerBean():收集容器中所有的监听器
  • multicastEvent() :发布事件

收集监听器时,需要获取监听器支持的事件类型,将原始的监听器封装为支持事件类型检查的监听器,这种监听器在发布事件时,使用线程池支持发布异步事件。

发布事件时,遍历容器中所有监听器,当监听器支持的事件类型与发布的事件类型一致时才发布事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Bean
@SuppressWarnings("all")
public ApplicationEventMulticaster applicationEventMulticaster(ConfigurableApplicationContext context, ThreadPoolTaskExecutor executor) {
return new AbstractApplicationEventMulticaster() {
private List<GenericApplicationListener> listeners = new ArrayList<>();

// 收集监听器
public void addApplicationListenerBean(String name) {
ApplicationListener listener = context.getBean(name, ApplicationListener.class);
// System.out.println(listener);
// 获取该监听器支持的事件类型
ResolvableType type = ResolvableType.forClass(listener.getClass()).getInterfaces()[0].getGeneric();
// System.out.println(type);

// 将原始的 listener 封装为支持事件类型检查的 listener
GenericApplicationListener genericApplicationListener = new GenericApplicationListener() {
/**
* 是否支持某种事件类型
* @param eventType 真实的事件类型
* @return 是否支持某事件类型
*/
public boolean supportsEventType(ResolvableType eventType) {
return type.isAssignableFrom(eventType);
}

public void onApplicationEvent(ApplicationEvent event) {
executor.submit(() -> listener.onApplicationEvent(event));
}
};

listeners.add(genericApplicationListener);
}

// 发布事件
public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
for (GenericApplicationListener listener : listeners) {
if (listener.supportsEventType(ResolvableType.forClass(event.getClass()))) {
listener.onApplicationEvent(event);
}
}
}
};
}

49.2 非 ApplicationEvent 事件

如果发送的事件不是 ApplicationEvent 类型时,Spring 会将其包装为 PayloadApplicationEvent 并用泛型技术解析事件对象的原始类型。

包装为 PayloadApplicationEvent 类型的逻辑无需实现,直接使用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
interface Inter {
}

static class Bean1 implements Inter {
}

@Bean
@SuppressWarnings("all")
public ApplicationEventMulticaster applicationEventMulticaster(ConfigurableApplicationContext context, ThreadPoolTaskExecutor executor) {
return new A49.AbstractApplicationEventMulticaster() {
private List<GenericApplicationListener> listeners = new ArrayList<>();

{
listeners.add(new GenericApplicationListener() {
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof PayloadApplicationEvent) {
PayloadApplicationEvent<?> payloadApplicationEvent =
(PayloadApplicationEvent<?>) event;
System.out.println(payloadApplicationEvent.getPayload());
}
}

@Override
public boolean supportsEventType(ResolvableType eventType) {
System.out.println(eventType);
// eventType --> PayloadApplicationEvent<Object>
// eventType --> PayloadApplicationEvent<String>
return (Inter.class.isAssignableFrom(eventType.getGeneric().toClass()));
}
});
}

@Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, null);
}

@SuppressWarnings("all")
@Override
public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
listeners.stream().filter(applicationListener -> {
if (eventType == null) {
return false;
}
if (applicationListener instanceof GenericApplicationListener) {
GenericApplicationListener listener =
(GenericApplicationListener) applicationListener;
return listener.supportsEventType(eventType);
}
return false;
}).forEach(listener -> listener.onApplicationEvent(event));
}
};
}
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class TestEventPublisher {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(TestEventPublisher.class);

context.publishEvent(new Object());
context.publishEvent("aaaa");
context.publishEvent(new Bean1());
}
}

运行 main()方法后,控制台打印出:

org.springframework.context.PayloadApplicationEvent
org.springframework.context.PayloadApplicationEvent
org.springframework.context.PayloadApplicationEvent
com.itheima.a49.TestEventPublisher$Bean1@7ba18f1b 

49.3 【补充】Spring 内置事件

Spring 的内置事件有很多,在此列举几个与 Spring 容器启动相关的事件,如果需要在 Spring 容器启动的某个时刻进行一些操作,就可以监听这些事件:

事件类型 触发时机
ContextRefreshedEvent 在调用 ConfigurableApplicationContext 接口中的 refresh() 方法时触发
ContextStartedEvent 在调用 ConfigurableApplicationContextstart() 方法时触发
ContextStoppedEvent 在调用 ConfigurableApplicationContextstop() 方法时触发
ContextClosedEvent ApplicationContext 被关闭时触发该事件,也就是调用 close() 方法触发