因为Spring源码这个东西实在是太难学了,学了忘,忘了学,反反复复,还有很多晦涩难懂的东西,甚至现在我感觉仍然是入门,只能说是把每节课学到的东西给记录一下,将来能多次回顾。笔记参考视频-黑马程序员

这几篇老文章都是我之前用飞书云文档写的,因转换格式问题,本文之前已写完的文档图片太多了,不想一一转化了,大家可以访问我的飞书云文档来查看这些图片–https://miu7shl031o.feishu.cn/drive/folder/WJiBfqX9klvKNEdpTHccfrEQn3e

容器接口

以 SpringBoot 的启动类为例:

@Slf4j
@SpringBootApplication
public class A01Application {
public static void main(String[] args) {
SpringApplication.run(A01Application.class, args);
}
}

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

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

ConfigurableApplicationContext

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

img

1.1 BeanFactory介绍

  1. 它是 ApplicationContext 的父接口
  2. 它才是 Spring 的核心容器,主要的 ApplicationContext 实现组合了它的功能,也就是说,BeanFactory 是 ApplicationContext 中的一个成员变量。
  3. 常用的 context.getBean(“xxx”) 方法,其实是调用了 BeanFactory 的 getBean() 方法。

1.2 BeanFactory 作用

IDEA中查看BeanFactory的相关方法

img

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

1.3DefaultListableBeanFactory

1.3.1DefaultListableBeanFactory简单介绍

  1. DefaultListableBeanFactory 实现了 BeanFactory 接口,它能管理 Spring 中所有的 Bean,当然也包含 Spring 容器中的那些单例对象。
  2. DefaultListableBeanFactory 还继承了DefaultSingletonBeanRegistry 类,这个类就是用来管理 Spring 容器中的单例对象。

1.3.2源码分析

进入DefaultSingletonBeanRegistry它有一个 Map 类型的成员变量 singletonObjects

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

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

演示

现有如下两个 Bean:

@Component
public class Component1 {
}

@Component
public class Component2 {
}

编写程序查看 singletonObjects 中是否存在这两个 Bean 的信息:

@Slf4j
@SpringBootApplication
public class A01Application {
@SneakyThrows
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);

Field singletonObjects = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects");
singletonObjects.setAccessible(true);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
Map<String, Object> map = (Map<String, Object>) singletonObjects.get(beanFactory);
map.entrySet().stream().filter(e -> e.getKey().startsWith("component"))
.forEach(e -> System.out.println(e.getKey() + "=" + e.getValue()));

context.close();
}
}

输出结果:

component1=indi.lcp.bean.a01.Component1@25a5c7db
component2=indi.lcp.bean.a01.Component2@4d27d9d

1.4ApplicationContext

附一张ConfigurableApplicationContext的继承类图

img

可以看出ApplicationContext 除了继承 BeanFactory 外,还继承了:

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

依次深入看一下这四个类

1.4.1MessageSource

在 SpringBoot 项目的 resources 目录下创建 messages.properties、messages_en.properties、messages_zh_CN.properties、messages_zh_TW.properties 四个国际化文件,除 messages.properties 外,其余三个文件内容如下:

lcp=Li congpu
lcp=李从浦
lcp=李從浦

测试

@SneakyThrows
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
// --snip--

System.out.println(context.getMessage("thanks", null, Locale.ENGLISH));
System.out.println(context.getMessage("thanks", null, Locale.SIMPLIFIED_CHINESE));
System.out.println(context.getMessage("thanks", null, Locale.TRADITIONAL_CHINESE));
context.close();
}

控制台打印出:

Li congpu
李从浦
李從浦

1.4.2ResourcePatternResolver

@SneakyThrows
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
// --snip--

Resource[] resources = context.getResources("classpath:application.properties");
Assert.isTrue(resources.length > 0, "加载类路径下的 application.properties 文件失败");

// 使用 classpath* 可以加载 jar 里类路径下的 resource
resources = context.getResources("classpath*:META-INF/spring.factories");
Assert.isTrue(resources.length > 0, "加载类路径下的 META-INF/spring.factories 文件失败");
context.close();
}

1.4.3EnvironmentCapable

@SneakyThrows
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
// --snip--

System.out.println(context.getEnvironment().getProperty("java_home"));
System.out.println(context.getEnvironment().getProperty("properties.name"));
context.close();
}

java_home 是从环境变量中读取,properties.name 则是从 application.yml 配置文件中读取。

1.4.4ApplicationEventPublisher

定义事件类 UserRegisteredEvent:

public class UserRegisteredEvent extends ApplicationEvent {
private static final long serialVersionUID = 6319117283222183184L;

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

将 Component1 作为发送事件的 Bean:

@Slf4j
@Component
public class Component1 {
@Autowired
private ApplicationEventPublisher context;

public void register() {
log.debug("事件");
context.publishEvent(new UserRegisteredEvent(this));
}
}

将 Component2 作为事件监听器:

@Slf4j
@Component
public class Component2 {
@EventListener
public void aaa(UserRegisteredEvent event) {
log.debug("{}", event);
log.debug("监听到事件");
}
}

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

@SneakyThrows
@SuppressWarnings("unchecked")
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
// --snip--

context.getBean(Component1.class).register();
context.close();
}

控制台输出

indi.lcp.bean.a01.Component1      - 事件 
indi.lcp.bean.a01.Component2 - indi.lcp.bean.a01.UserRegisteredEvent[source=indi.lcp.bean.a01.Component1@25a5c7db]
indi.lcp.bean.a01.Component2 - 监听到事件

容器实现

2.1 BeanFactory 的实现

有如下类,尝试将 Config 添加到 Bean 工厂中:

@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()");
}
}

需要使用到 BeanFactory 的一个实现类: DefaultListableBeanFactory。有了 Bean 工厂,还需要定义 Bean,之后再把定义的 Bean 注册到工厂即可。

public static void main(String[] args) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// bean 的定义(class,scope,初始化,销毁)
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Config.class)
.setScope("singleton")
.getBeanDefinition();
beanFactory.registerBeanDefinition("config", beanDefinition);

// 只有 config
Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
}
config

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

  • 解析配置类

根据对 @Configuration 和 @Bean 两个注解的认识可知,Bean 工厂中应该还存在 bean1 和 bean2,那为什么现在没有呢?

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

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

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

那为什么在 Bean 工厂中依旧没有 bean1 和 bean2 呢?

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

使用处理器很简单,先获取到处理器,然后再使用即可。像internalConfigurationAnnotationProcessor 这样的 Bean,都有一个共同的类型,名为 BeanFactoryPostProcessor,因此可以:

public static void main(String[] args) {
// --snip--
// 使用后置处理器
// BeanFactoryPostProcessor 补充了一些 Bean 的定义
beanFactory.getBeansOfType(BeanFactoryPostProcessor.class).values().forEach(i -> i.postProcessBeanFactory(beanFactory));
Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
}
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
  • 依赖注入

bean1 和 bean2 已经被添加到 Bean 工厂中,尝试获取 bean1 中的 bean2,查看 bean2 是否成功注入到 bean1 中:

public static void main(String[] args) {
// --snip--
System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}
null

bean2 没有成功被注入到 bean1 中。

在先前添加到 BeanFactory 中的后置处理器里,有名为 internalAutowiredAnnotationProcessor 和 internalCommonAnnotationProcessor 的两个后置处理器。前者用于解析 @Autowired 注解,后者用于解析 @Resource 注解,它们都有一个共同的类型 BeanPostProcessor,因此可以:

public static void main(String[] args) {
// --snip--
System.out.println("---------------------------------------------");
// Bean 后置处理器,对 Bean 的生命周期的各个阶段提供拓展,例如 @AutoWired @Resource...
beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor);
System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}
----------------------------------------
[main] DEBUG indi.lcp.bean.a02.TestBeanFactory$Bean1 - 构造 Bean1()
[main] DEBUG indi.lcp.bean.a02.TestBeanFactory$Bean2 - 构造 Bean2()
indi.lcp.bean.a02.TestBeanFactory$Bean2@6ee12bac

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

除此之外还可以发现:当需要使用 Bean 时,Bean 才会被创建,即按需加载。那有没有什么办法预先就初始化好单例对象呢?

public static void main(String[] args) {
// --snip--

// 预先初始化单例对象(完成依赖注入和初始化流程) 懒汉变饿汉
beanFactory.preInstantiateSingletons();
System.out.println("---------------------------------------------");
beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor);
System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}
[main] DEBUG indi.lcp.bean.a02.TestBeanFactory$Bean1 - 构造 Bean1()
[main] DEBUG indi.lcp.bean.a02.TestBeanFactory$Bean2 - 构造 Bean2()
----------------------------------------
indi.lcp.bean.a02.TestBeanFactory$Bean2@6ee12bac

学到了什么->BeanFactory 不会做的事 :

  • 主动调用 BeanFactory 后置处理器;

  • 主动添加 Bean 后置处理器;

  • 主动初始化单例对象;

  • 解析 ${} 和 #{}

  • 扩展:后置处理器的排序

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

@Configuration
static class Config {
// --snip--

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

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

interface Inter {

}

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

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

@Slf4j
static class Bean1 {
// --snip--

@Autowired
@Resource(name = "bean4")
private Inter bean3;

private Inter getInter() {
return bean3;
}
}

向 Bean 工厂中添加了 bean3 和 bean4,并且计划在 bean1 中注入 Inter 类型的 Bean。

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

如果只使用 @Autowired,首先会按照类型注入,如果同种类型的 Bean 有多个,再按照变量名称注入,如果再注入失败,就报错;如果只使用 @Resource,也会采取与 @Autowired 一样的注入策略,只不过 @Resource 注解还可以指定需要注入 Bean 的 id(使用 name 属性进行指定),如果指定了需要注入 Bean 的 id,就直接按照指定的 id 进行注入,如果失败就报错。

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

public static void main(String[] args) {
// --snip--
System.out.println(beanFactory.getBean(Bean1.class).getInter());
}
indi.lcp.bean.a02.TestBeanFactory$Bean3@8e0379d

根据打印的结果可知,@Autowired 先生效了,这是因为 internalAutowiredAnnotationProcessor 排在 internalCommonAnnotationProcessor 之前。可以查看它们的先后关系

public static void main(String[] args) {
// --snip--
beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(i -> {
System.out.println(">>>> " + i);
beanFactory.addBeanPostProcessor(i);
});
}
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@6385cb26
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@38364841

也可以改变它们的顺序,然后再查看注入的是 bean3 还是 bean4:

public static void main(String[] args) {
// --snip--
beanFactory.getBeansOfType(BeanPostProcessor.class).values().stream()
.sorted(Objects.requireNonNull(beanFactory.getDependencyComparator()))
.forEach(i -> {
System.out.println(">>>> " + i);
beanFactory.addBeanPostProcessor(i);
});
System.out.println(beanFactory.getBean(Bean1.class).getInter());
}
>>>> org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@6385cb26
>>>> org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@38364841
indi.lcp.bean.a02.TestBeanFactory$Bean4@52e677af

改变 BeanPostProcessor 的先后顺序后,@Resource(name = “bean4”) 生效了,成功注入了 bean4。

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

在调用的 AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); 方法源码中有:

public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) {
registerAnnotationConfigProcessors(registry, null);
}

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());
}
}

// --snip--
}

设置的 AnnotationAwareOrderComparator 比较器会根据设置的 order 信息进行比较。

AutowiredAnnotationBeanPostProcessor 设置的 order 是:

private int order = Ordered.LOWEST_PRECEDENCE - 2;

CommonAnnotationBeanPostProcessor 设置的 order 是:

public CommonAnnotationBeanPostProcessor() {
setOrder(Ordered.LOWEST_PRECEDENCE - 3);
// --snip--
}

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

2.2 ApplicationContext 的实现

/**
* @author lcp
* @date 2022/12/23 23:32
*/
@Slf4j
public class A02Application {

static class Bean1 {

}

static class Bean2 {
@Getter
@Setter
private Bean1 bean1;
}
}

2.2.1 ClassPathXmlApplicationContext

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

<?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="indi.lcp.bean.a02.A02Application.Bean1"/>

<bean id="bean2" class="indi.lcp.bean.a02.A02Application.Bean2">
<property name="bean1" ref="bean1" />
</bean>
</beans>
private static void testClassPathXmlApplicationContext() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("b01.xml");
Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println(context.getBean(Bean2.class).getBean1());
}
bean1
bean2
indi.lcp.bean.a02.A02Application$Bean1@2db7a79b

2.2.2 FileSystemXmlApplicationContext

与 ClassPathXmlApplicationContext 相比,FileSystemXmlApplicationContext 是基于磁盘路径下 xml 格式的配置文件来创建。

private static void testFileSystemXmlApplicationContext() {
// 使用相对路径时,以模块为起点(IDEA 中需要设置 Working directory),也支持绝对路径
FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("bean\\src\\main\\resources\\b01.xml");
Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println(context.getBean(Bean2.class).getBean1());
}
bean1
bean2
indi.lcp.bean.a02.A02Application$Bean1@2db7a79b
  • 从XML 文件中读取 Bean 的定义

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

private static void testXmlBeanDefinitionReader() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
System.out.println("读取之前...");
Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println("读取之后...");
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
// reader.loadBeanDefinitions(new ClassPathResource("b01.xml"));
reader.loadBeanDefinitions(new FileSystemResource("bean\\src\\main\\resources\\b01.xml"));
Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
}
读取之前...
读取之后...
bean1
bean2

2.2.3 AnnotationConfigApplicationContext

基于 Java 配置类来创建。首先定义配置类:

@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;
}
}
private static void testAnnotationConfigApplicationContext() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Config.class);
Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println(context.getBean(Bean2.class).getBean1());
}
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
indi.lcp.bean.a02.A02Application$Bean1@1f0f1111

与前面两种基于 XML 创建 ApplicationContext 的方式相比,使用 AnnotationConfigApplicationContext 后,使得容器中多了一些后置处理器相关的 Bean。

如果要在先前的两种方式中也添加上这些 Bean,可以在 XML 进行配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="bean1" class="indi.lcp.bean.a02.A02Application.Bean1"/>

<bean id="bean2" class="indi.lcp.bean.a02.A02Application.Bean2">
<property name="bean1" ref="bean1" />
</bean>

<!-- 添加后置处理器 -->
<context:annotation-config />
</beans>

2.2.4 AnnotationConfigServletWebServerApplicationContext

基于 Java 配置类来创建,用于 web 环境。首先定义配置类:

@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;
};
}
}
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;
  • HierarchicalBeanFactory:Hierarchical 意为“层次化”,通常表示一种具有层级结构的概念或组织方式,这种层次化结构可以通过父子关系来表示对象之间的关联,比如树、图、文件系统、组织架构等。根据该接口下的方法可知,能够获取到父容器,说明 BeanFactory 有父子容器概念;
  • AutowireCapableBeanFactory:提供了创建 Bean、自动装配 Bean、属性填充、Bean 初始化、依赖注入等能力,比如 @Autowired 注解的底层实现就依赖于该接口的 resolveDependency() 方法;
  • ConfigurableBeanFactory:该接口并未直接继承至 BeanFactory,而是继承了 HierarchicalBeanFactory。Configurable 意为“可配置的”,就是说该接口用于对 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

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

  • 注册与获取 Bean

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

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 接口体系

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

ApplicationContext 接口继承了许多接口,其中:

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

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

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()。

Bean 的生命周期

自定义一个 SpringBoot 的主启动类:

@SpringBootApplication
public class A03Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A03Application.class, args);
// 调用 close 方法,显示生命周期的销毁阶段
context.close();
}
}

在启动类所在路径下再定义一个类,使其能够被自动装配:

@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("销毁");
}
}
indi.lcp.bean.a03.LifeCycleBean : 构造
indi.lcp.bean.a03.LifeCycleBean : 依赖注入: D:\environment\JDK1.8
indi.lcp.bean.a03.LifeCycleBean : 初始化
indi.lcp.bean.a03.LifeCycleBean : 销毁

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

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

@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;
}
}

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

indi.lcp.bean.a03.MyBeanPostProcessor  : <<<<<<<<<< 实例化之前执行,这里返回的对象会替换掉原本的 bean
indi.lcp.bean.a03.LifeCycleBean : 构造
indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 实例化之后执行,如果返回 false 会跳过依赖注入节点
indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 依赖注入阶段执行,如 @Autowired@Value@Resource
indi.lcp.bean.a03.LifeCycleBean : 依赖注入: D:\environment\JDK1.8
indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 初始化执行之前,这里返回的对象会替换掉原本的 bean,如 @PostConstruct@ConfigurationProperties
indi.lcp.bean.a03.LifeCycleBean : 初始化
indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的 bean,如代理增强
indi.lcp.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 销毁执行之前,如 @PreDestroy
indi.lcp.bean.a03.LifeCycleBean : 销毁

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

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

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

static class MyBeanFactory {
public Object getBean() {
Object bean = new Object();
System.out.println("构造 " + bean);
System.out.println("依赖注入 " + bean);
System.out.println("初始化 " + bean);
return bean;
}
}

假设现在需要在依赖注入之后,初始化之前进行其他的操作,那首先能想到的就是在这个位置直接书写相关操作的代码,但这会使代码更加臃肿、增加耦合性,显然不是一种好方式。

可以定义一个接口:

interface BeanPostProcessor {
void inject(Object bean);
}

然后对 MyBeanFactory 进行修改:

static class MyBeanFactory {
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;
}

private List<BeanPostProcessor> processors = new ArrayList<>();

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

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

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

Bean的生命周期

img

Bean 后置处理器

4.1 常见的 Bean 后置处理器

现有如下三个类:

/**
* @author lcp
* @date 2022/12/25 22:55
*/
@Slf4j
@ToString
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 生效");
}
}
public class Bean2 {
}

public class Bean3 {
}

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

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 后置处理器相关的 Bean,使得 Bean1 中使用的注解能够生效。

public static void main(String[] args) {
// --snip--

context.registerBean("bean3", Bean3.class);

// 解析值注入内容
context.getDefaultListableBeanFactory().setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
// @Autowired @Value
context.registerBean(AutowiredAnnotationBeanPostProcessor.class);

context.refresh();

// --snip--
}
indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@29b5cd00
indi.lcp.bean.a04.Bean1 - @Value 生效: D:\environment\JDK1.8

@Autowired 和 @Value 注解成功生效,但 @Resource、@PostConstruct 和 @PreDestroy 依旧没有生效,因此还需要添加解析它们的 Bean 后置处理器。

public static void main(String[] args) {
// --snip--

// @Resource @PostConstruct @PreDestroy
context.registerBean(CommonAnnotationBeanPostProcessor.class);

// --snip--
}
indi.lcp.bean.a04.Bean1 - @Resource 生效: indi.lcp.bean.a04.Bean3@12cdcf4
indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@6121c9d6
indi.lcp.bean.a04.Bean1 - @Value 生效: D:\environment\JDK1.8
indi.lcp.bean.a04.Bean1 - @PostConstruct 生效
INFO indi.lcp.bean.a04.Bean1 - @PreDestroy 生效

解析 @ConfigurationProperties

使用 @ConfigurationProperties 可以指定配置信息的前缀,使得配置信息的读取更加简单。比如:

@Getter
@Setter
@ToString
@ConfigurationProperties(prefix = "java")
public class Bean4 {
private String home;
private String version;
}

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

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

public static void main(String[] args) {
// --snip--

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

// --snip--

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

// --snip--
}

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

public static void main(String[] args) {
// --snip--

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

// --snip--
// @ConfigurationProperties
ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory());

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

// --snip--
}
Bean4(home=D:\environment\JDK1.8\jre, version=1.8.0_251)

4.2 AutowiredAnnotationBeanPostProcessor

通过前文可知 AutowiredAnnotationBeanPostProcessor 用于解析 @Autowired 和 @Value 注解,那它究竟是怎么工作的呢?

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());

// 查看哪些属性、方法加了 @Autowired,这称之为 InjectionMetadata
AutowiredAnnotationBeanPostProcessor postProcessor = new AutowiredAnnotationBeanPostProcessor();
postProcessor.setBeanFactory(beanFactory);

Bean1 bean1 = new Bean1();
System.out.println(bean1);
// 执行依赖注入,@Autowired、@Value
postProcessor.postProcessProperties(null, bean1, "bean1");
System.out.println(bean1);
}
Bean1(bean2=null, bean3=null, home=null)
21:31:27.409 [main] INFO indi.lcp.bean.a04.Bean1 - @Value 生效: ${JAVA_HOME}
21:31:27.419 [main] INFO indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@5bcab519
Bean1(bean2=indi.lcp.bean.a04.Bean2@5bcab519, bean3=null, home=${JAVA_HOME})

在未调用 AutowiredAnnotationBeanPostProcessor#postProcessProperties() 方法时,Bean1 中的 bean2、bean3 和 home 都没有注入成功,而在调用之后,成功注入了 bean2 和 home,但 home 的值似乎有点奇怪,并没有打印出前文中相同的值,可能是因为没有成功解析 #{}?

至于 bean3 为什么没注入成功,是因为 bean3 的注入是利用 @Resource,而不是 @Autowired。如果对 Bean1 进行修改:

public class Bean1 {
// --snip--

@Autowired
private Bean3 bean3;

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

// --snip--
}
Bean1(bean2=null, bean3=null, home=null)
21:36:36.402 [main] INFO indi.lcp.bean.a04.Bean1 - @Value 生效: ${JAVA_HOME}
21:36:36.406 [main] INFO indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@490ab905
Bean1(bean2=indi.lcp.bean.a04.Bean2@490ab905, bean3=indi.lcp.bean.a04.Bean3@56ac3a89, home=${JAVA_HOME})

成功注入了 bean3。如果想要成功注入 home,则需要在 BeanFactory 中添加 #{} 的解析器:

public static void main(String[] args) {
// --snip--

// ${} 的解析器
beanFactory.addEmbeddedValueResolver(new StandardEnvironment()::resolvePlaceholders);

// --snip--

postProcessor.postProcessProperties(null, bean1, "bean1");
System.out.println(bean1);
}
Bean1(bean2=null, bean3=null, home=null)
indi.lcp.bean.a04.Bean1 - @Value 生效: D:\environment\JDK1.8
indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@4fe3c938
Bean1(bean2=indi.lcp.bean.a04.Bean2@4fe3c938, bean3=indi.lcp.bean.a04.Bean3@5383967b, home=D:\environment\JDK1.8)

AutowiredAnnotationBeanPostProcessor#postProcessProperties()

源码如下:

@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() 用于查找指定的 bean 对象中哪些地方使用了 @Autowired、@Value 等与注入相关的注解,并将这些信息封装在 InjectionMetadata 对象中,之后调用其 inject() 方法利用反射完成注入。

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

@SneakyThrows
public static void main(String[] args) {
// --snip--

AutowiredAnnotationBeanPostProcessor postProcessor = new AutowiredAnnotationBeanPostProcessor();
postProcessor.setBeanFactory(beanFactory);

Bean1 bean1 = new Bean1();

Method method = AutowiredAnnotationBeanPostProcessor.class.getDeclaredMethod("findAutowiringMetadata", String.class, Class.class, PropertyValues.class);
method.setAccessible(true);
// 获取 Bean1 上加了 @Value、@Autowired 注解的成员变量、方法参数信息
InjectionMetadata metadata = (InjectionMetadata) method.invoke(postProcessor, "bean1", Bean1.class, null);
// 此处断点
System.out.println(metadata);
}

InjectionMetadata 中有一个名为 injectedElements 的集合类型成员变量,injectedElements 存储了被相关注解标记的成员变量、方法的信息,因为 Bean1 中的 bean3 成员变量、setBean2() ,setHome() 方法恰好被 @Autowired 注解标记。

然后按照源码一样,调用 InjectionMetadata#inject() 方法进行依赖注入:

@SneakyThrows
public static void main(String[] args) {
// --snip--

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

// 调用 InjectionMetadata 来进行依赖注入,注入时按类型查找值
metadata.inject(bean1, "bean1", null);
System.out.println(bean1);
}
indi.lcp.bean.a04.Bean1 - @Value 生效: D:\environment\JDK1.8
indi.lcp.bean.a04.Bean1 - @Autowired 生效: indi.lcp.bean.a04.Bean2@5383967b
Bean1(bean2=indi.lcp.bean.a04.Bean2@5383967b, bean3=indi.lcp.bean.a04.Bean3@2ac273d3, home=D:\environment\JDK1.8)

调用 inject() 方法后会利用反射进行依赖注入,但在反射之前,肯定得先拿到被注入的对象或值,那这些对象或值是怎么取到的呢?

可以通过以下代码概括:

@SneakyThrows
public static void main(String[] args) {
// --snip--

// 如何按类型查找值
Field bean3 = Bean1.class.getDeclaredField("bean3");
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);
}
indi.lcp.bean.a04.Bean3@2ac273d3
indi.lcp.bean.a04.Bean2@192b07fd
D:\environment\JDK1.8

BeanFactory 后置处理器

5.1 常见的 BeanFactory 后置处理器

先引入要用到的依赖:

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

需要用到的类信息:

package indi.lcp.bean.a05;

@Configuration
@ComponentScan("indi.lcp.bean.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;
}
}
package indi.lcp.bean.a05;

@Slf4j
public class Bean1 {
public Bean1() {
System.out.println("我被 Spring 管理啦");
}
}
package indi.lcp.bean.a05.component;

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

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

public static void main(String[] args) {
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 信息,也就是说 Config 类中的 @ComponentScan 和 @Bean 注解都没有生效。

根据经验,显然是因为缺少某个后置处理器。

public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);
// @ComponentScan @Bean @Import @ImportResource
context.registerBean(ConfigurationClassPostProcessor.class);

// --snip--
}
indi.lcp.bean.a05.component.Bean2 - 我被 Spring 管理啦
indi.lcp.bean.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 后置处理器。

以下两个接口被 @Mapper 注解标记:

package indi.lcp.bean.a05.mapper;

@Mapper
public interface Mapper1 {
}

@Mapper
public interface Mapper2 {
}

然后添加解析 @Mapper 注解的后置处理器:

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", "indi.lcp.bean.a05.mapper"));

// --snip--
}

其中的 basePackage 是 MapperScannerConfigurer 中的一个成员变量,表示需要扫描的包路径,设置的值恰好是被 @Mapper 注解标记的接口所在的包路径。

控制台打印的信息中增加了:

mapper1
mapper2

5.2 模拟实现

移除向容器中添加的 ConfigurationClassPostProcessor 和 MapperScannerConfigurer 两个后置处理器,自行编码模拟它们功能的实现。

  • 组件扫描之 @ComponentScan

在 Bean2 所在包路径下再增加两个类,用于后续测试:

package indi.lcp.bean.a05.component;

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

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

编写 ComponentScanPostProcessor 用于实现 @ComponentScan 注解的解析:

/**
* @author lcp
* @date 2023/1/7 22:13
*/
public class ComponentScanPostProcessor implements BeanDefinitionRegistryPostProcessor {

/**
* 调用 context.refresh() 方法时回调
*/
@Override
@SneakyThrows
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
ComponentScan componentScan = AnnotationUtils.findAnnotation(Config.class, ComponentScan.class);
if (componentScan != null) {
for (String packageName : componentScan.basePackages()) {
System.out.println(packageName);
// indi.lcp.bean.a05.component -> classpath*:indi/lcp/bean/a05/component/**/**.class
String path = "classpath*:" + packageName.replace(".", "/") + "/**/**.class";
// Resource[] resources = context.getResources(path);
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(path);
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();
for (Resource resource : resources) {
MetadataReader reader = factory.getMetadataReader(resource);
AnnotationMetadata annotationMetadata = reader.getAnnotationMetadata();
// System.out.println("类名: " + reader.getClassMetadata().getClassName());
// System.out.println("是否加了 @Component: " + annotationMetadata.hasAnnotation(Component.class.getName()));
// System.out.println("是否加了 @Component 派生: " + annotationMetadata.hasMetaAnnotation(Component.class.getName()));
if (annotationMetadata.hasAnnotation(Component.class.getName())
|| annotationMetadata.hasMetaAnnotation(Component.class.getName())) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(reader.getClassMetadata().getClassName())
.getBeanDefinition();
String name = generator.generateBeanName(beanDefinition, registry);
registry.registerBeanDefinition(name, beanDefinition);
}
}
}
}
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

}
}
public static void main(String[] args) {
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();
}
indi.lcp.bean.a05.component
indi.lcp.bean.a05.component.Bean2 : 我被 Spring 管理啦
indi.lcp.bean.a05.component.Bean3 : 我被 Spring 管理啦
config
indi.lcp.bean.a05.ComponentScanPostProcessor
bean2
bean3

没使用 ConfigurationClassPostProcessor 也实现了 @ComponentScan 注解的解析!

  • @Bean 的解析

Config 类中再增加一个方法作为干扰项:

@Configuration
@ComponentScan("indi.lcp.bean.a05.component")
public class Config {

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

// --snip--
}

与解析 @ComponentScan 一样,自行编写一个 BeanFactoryPostProcessor 的实现类用于解析 @Bean 注解:

/**
* @author lcp
* @date 2023/1/7 22:55
*/
public class AtBeanPostProcessor implements BeanDefinitionRegistryPostProcessor {

@Override
@SneakyThrows
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
MetadataReader reader = factory.getMetadataReader(new ClassPathResource("indi/lcp/bean/a05/Config.class"));
Set<MethodMetadata> methods = reader.getAnnotationMetadata().getAnnotatedMethods(Bean.class.getName());
for (MethodMetadata method : methods) {
System.out.println(method);
String initMethod = method.getAnnotationAttributes(Bean.class.getName()).get("initMethod").toString();

String methodName = method.getMethodName();
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition()
.setFactoryMethodOnBean(methodName, "config")
// 工厂方法、构造方法的注入模式使用构造器模式
.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
if (StringUtils.hasLength(initMethod)) {
builder.setInitMethodName(initMethod);
}
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
registry.registerBeanDefinition(methodName, beanDefinition);
}
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

}
}

在构造 BeanDefinition 时调用了 setAutowireMode() 方法设置注入模式,这是因为在 Config 类中有一特殊的被 @Bean 标记的方法:

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

接收一个 DataSource 类型的参数,需要将容器中这个类型的 Bean 进行注入,设置的 AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR 注入模式则能完成这个功能。

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();
}
indi.lcp.bean.a05.Config.bean1()
indi.lcp.bean.a05.Config.sqlSessionFactoryBean(javax.sql.DataSource)
indi.lcp.bean.a05.Config.dataSource()
indi.lcp.bean.a05.Bean1 : 我被 Spring 管理啦
com.alibaba.druid.pool.DruidDataSource : {dataSource-1,root} inited
config
indi.lcp.bean.a05.AtBeanPostProcessor
bean1
sqlSessionFactoryBean
dataSource
  • @Mapper 的解析

@Mapper 注解是在接口上使用的,但根据前文内容可知,@Mapper 被解析后在 Spring 容器中也存在与被标记的接口相关的 Bean。

难道 Spring 能管理接口?

那肯定是不行的,Spring 只能管理对象这是毋庸置疑的。那这些接口是怎么变成对象被 Spring 管理的呢?

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

在 Config 添加注册 Mapper1 和 Mapper2 的方法:

@Bean
public MapperFactoryBean<Mapper1> mapper1(SqlSessionFactory sqlSessionFactory) {
MapperFactoryBean<Mapper1> factoryBean = new MapperFactoryBean<>(Mapper1.class);
factoryBean.setSqlSessionFactory(sqlSessionFactory);
return factoryBean;
}

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

再运行 main() 方法可以看到容器中存在名为 mapper1 和 mapper2 的 Bean。

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

移除 Config 类中的 mapper1() 和 mapper2() 方法,自行编写 BeanDefinitionRegistryPostProcessor 接口的实现类完成 @Mapper 注解的解析:

/**
* @author lcp
* @date 2023/1/7 23:45
*/
public class MapperPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
@SneakyThrows
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:indi/lcp/bean/a05/mapper/**/*.class");
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 beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MapperFactoryBean.class)
.addConstructorArgValue(classMetadata.getClassName())
.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
.getBeanDefinition();
AbstractBeanDefinition bd = BeanDefinitionBuilder.genericBeanDefinition(classMetadata.getClassName())
.getBeanDefinition();
String name = generator.generateBeanName(bd, registry);
registry.registerBeanDefinition(name, beanDefinition);
}
}
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

}
}
@SneakyThrows
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("config", Config.class);

context.registerBean(AtBeanPostProcessor.class);
/*
* AtBeanPostProcessor 的注册不能少,因为需要容器中存在 SqlSessionFactoryBean
* 而 SqlSessionFactoryBean 是在配置类中利用 @Bean 进行注册的
*/
context.registerBean(MapperPostProcessor.class);

context.refresh();

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

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

容器中存在 mapper1 和 mapper2 两个 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 接口中抽象方法的参数:

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

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

@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("lcp");
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=lcp, bean2=null)

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

Aware 接口

6.1. Aware 接口

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

  • BeanNameAware 注入 Bean 的名字
  • BeanFactoryAware 注入 BeanFactory 容器
  • ApplicationContextAware 注入 ApplicationContext 容器
  • EmbeddedValueResolverAware 解析 ${}
/**
* @author lcp
* @date 2023/1/8 16:12
*/
@Slf4j
public class MyBean implements BeanNameAware, ApplicationContextAware {
@Override
public void setBeanName(String name) {
log.info("当前 Bean: " + this + "名字叫: " + name);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
log.info("当前 Bean: " + this + "容器是: " + applicationContext);
}
}
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("myBean", MyBean.class);

context.refresh();
context.close();
}
当前 Bean: indi.lcp.bean.a06.MyBean@16f7c8c1名字叫: myBean
当前 Bean: indi.lcp.bean.a06.MyBean@16f7c8c1容器是: org.springframework.context.support.GenericApplicationContext@2669b199

6.2. InitializingBean

@Slf4j
public class MyBean implements BeanNameAware, ApplicationContextAware, InitializingBean {
// --snip--

@Override
public void afterPropertiesSet() throws Exception {
log.info("当前 Bean: " + this + " 初始化");
}
}

再次运行 main() 方法有:

当前 Bean: indi.lcp.bean.a06.MyBean@16f7c8c1名字叫: myBean
当前 Bean: indi.lcp.bean.a06.MyBean@16f7c8c1容器是: org.springframework.context.support.GenericApplicationContext@2669b199
当前 Bean: indi.lcp.bean.a06.MyBean@df27fae 初始化

当同时实现 Aware 接口和 InitializingBean 接口时,会先执行 Aware 接口。

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

@Autowired 和 @PostConstruct 注解的解析需要使用 Bean 后置处理器,属于拓展功能,而这些接口属于内置功能,不加任何拓展 Spring 就能识别。在某些情况下,拓展功能会失效,而内容功能不会失效。

@Slf4j
public class MyBean implements BeanNameAware, ApplicationContextAware, InitializingBean {
// --snip--

@Autowired
public void setApplicationContextWithAutowired(ApplicationContext applicationContext) {
log.info("当前 Bean: " + this + " 使用 @Autowired 注解,容器是: " + applicationContext);
}

@PostConstruct
public void init() {
log.info("当前 Bean: " + this + " 使用 @PostConstruct 注解初始化");
}
}

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

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

6.3. 失效的 @Autowired 注解

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

@Slf4j
@Configuration
public class MyConfig1 {
@Autowired
public void setApplicationContext(ApplicationContext applicationContext) {
log.info("注入 ApplicationContext");
}

@PostConstruct
public void init() {
log.info("初始化");
}
}
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("myConfig1", MyConfig1.class);
context.registerBean(AutowiredAnnotationBeanPostProcessor.class);
context.registerBean(CommonAnnotationBeanPostProcessor.class);
// 解析配置类中的注解
context.registerBean(ConfigurationClassPostProcessor.class);

context.refresh();
context.close();
}
indi.lcp.bean.a06.MyConfig1 : 注入 ApplicationContext
indi.lcp.bean.a06.MyConfig1 : 初始化

@Autowired 和 @PostConstruct 注解成功被解析。

如果再对 Config1 进行一点小小的修改呢?

@Slf4j
@Configuration
public class MyConfig1 {
// --snip--

@Bean
public BeanFactoryPostProcessor processor1() {
return processor -> log.info("执行 processor1");
}
}

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

如果再运行 main() 方法:

indi.lcp.bean.a06.MyConfig1            : 执行 processor1

processor1() 方法成功生效,但 @Autowired 和 @PostConstruct 注解的解析失败了。

对于 context.refresh(); 方法来说,它主要按照以下顺序干了三件事:

  • 执行 BeanFactory 后置处理器;
  • 添加 Bean 后置处理器;
  • 创建和初始化单例对象。

比如当 Java 配置类不包括 BeanFactoryPostProcessor 时:

BeanFactoryPostProcessor 会在 Java 配置类初始化之前执行。

当 Java 配置类中定义了BeanFactoryPostProcessor 时,如果要创建配置类中的 BeanFactoryPostProcessor 就必须 提前 创建和初始化 Java 配置类。

在创建和初始化 Java 配置类时,由于 BeanPostProcessor 还未准备好,无法解析配置类中的 @Autowired 等注解,导致 @Autowired 等注解失效:

要解决这个问题也很简单,使用相关接口的功能实现注入和初始化:

@Slf4j
@Configuration
public class MyConfig2 implements InitializingBean, ApplicationContextAware {
@Override
public void afterPropertiesSet() throws Exception {
log.info("初始化");
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
log.info("注入 ApplicationContext");
}

@Bean
public BeanFactoryPostProcessor processor2() {
return processor -> log.info("执行 processor2");
}
}

修改下 main() 方法:

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();
}
indi.lcp.bean.a06.MyConfig2 : 注入 ApplicationContext
indi.lcp.bean.a06.MyConfig2 : 初始化
indi.lcp.bean.a06.MyConfig2 : 执行 processor2
  1. 总结

  2. Aware 接口提供了一种 内置 的注入手段,可以注入 BeanFactory、ApplicationContext;

  3. InitializingBean 接口提供了一种 内置 的初始化手段;

  4. 内置的注入和初始化不受拓展功能的影响,总会被执行,因此 Spring 框架内部的类总是使用这些接口。

初始化与销毁

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

  1. 依赖于后置处理器提供的拓展功能
  2. 相关接口的功能
  3. 使用 @Bean 注解中的属性进行指定

当同时存在以上三种方式时,它们的执行顺序也将按照上述顺序进行执行。

包含三种初始化方式的 Bean:

@Slf4j
public class Bean1 implements InitializingBean {
@PostConstruct
public void init() {
log.info("初始化1");
}

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

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

包含三种销毁方式的 Bean:

@Slf4j
public class Bean2 implements DisposableBean {
@PreDestroy
public void destroy1() {
log.info("销毁1");
}

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

public void destroy3() {
log.info("销毁3");
}
}

测试:

@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();
}
}
indi.lcp.bean.a07.Bean1 : 初始化1
indi.lcp.bean.a07.Bean1 : 初始化2
indi.lcp.bean.a07.Bean1 : 初始化3
indi.lcp.bean.a07.Bean2 : 销毁1
indi.lcp.bean.a07.Bean2 : 销毁2
indi.lcp.bean.a07.Bean2 : 销毁3

Scope

8.1 Scope 的类型与销毁

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

  • singleton:单例(默认值)。容器启动时创建(未设置延迟),容器关闭时销毁
  • prototype:多例。每次使用时创建,不会自动销毁,需要调用 DefaultListableBeanFactory#destroyBean() 进行销毁
  • request:作用于 Web 应用的请求范围。每次请求用到此 Bean 时创建,请求结束时销毁
  • session:作用于 Web 应用的会话范围。每个会话用到此 Bean 时创建,会话结束时销毁
  • application:作用于 Web 应用的 ServletContext。Web 容器用到此 Bean 时创建,容器关闭时销毁

前两个取值不再赘述,重点看下后三个取值。

@Slf4j
@Component
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class BeanForRequest {
@PreDestroy
public void destroy() {
log.info("destroy");
}
}
@Slf4j
@Component
@Scope(WebApplicationContext.SCOPE_SESSION)
public class BeanForSession {
@PreDestroy
public void destroy() {
log.info("destroy");
}
}
@Slf4j
@Component
@Scope(WebApplicationContext.SCOPE_APPLICATION)
public class BeanForApplication {
@PreDestroy
public void destroy() {
log.info("destroy");
}
}

编写一个 Controller 进行测试:

@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>";
}
}

主启动类:

@SpringBootApplication
public class A08Application {

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

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

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

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

<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: indi.lcp.bean.a08.BeanForRequest@34d37122
session scope: indi.lcp.bean.a08.BeanForSession@75ee7b19
application scope: indi.lcp.bean.a08.BeanForApplication@68b50897

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

request scope: indi.lcp.bean.a08.BeanForRequest@2db4ac39
session scope: indi.lcp.bean.a08.BeanForSession@75ee7b19
application scope: indi.lcp.bean.a08.BeanForApplication@68b50897

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

这是因为刷新页面后就产生了一个新的请求,而 request 的作用范围只在一个请求内,因此每一个新请求就对应一个新的对象。

那要怎么改变 session scope 呢?

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

request scope: indi.lcp.bean.a08.BeanForRequest@2286f290
session scope: indi.lcp.bean.a08.BeanForSession@4f025f73
application scope: indi.lcp.bean.a08.BeanForApplication@68b50897

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

  • 销毁

当刷新页面后,除了 request scope 的值发生变化外,在 IDEA 的控制台能看到以下信息:

indi.lcp.bean.a08.BeanForRequest       : destroy

这表示 request 作用范围的 Bean 进行了销毁,执行了销毁方法。

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

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

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

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

// --snip--
}

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

indi.lcp.bean.a08.BeanForSession       : destroy

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

8.2. Scope 失效分析

现有两个类:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class F1 {
}
@Getter
@Component
public class E {
@Autowired
private F1 f1;
}

之后进行测试:

@Slf4j
@ComponentScan("indi.lcp.bean.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();
}
}

现在问题来了:F1 被 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 标记,之后向 e 中注入了 f1,那么 log.info(“{}”, e.getF1()); 打印出的 f1 应该都不是同一个对象吗?

indi.lcp.bean.a09.A09Application       : indi.lcp.bean.a09.F1@5fdcaa40
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@5fdcaa40
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@5fdcaa40

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

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

img

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

img

解决方式一

@Getter
@Component
public class E {
@Lazy
@Autowired
private F1 f1;
}

再修改下 main() 方法,打印下 f1 的 Class 信息,查看是否是代理对象:

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();
}
indi.lcp.bean.a09.A09Application : class indi.lcp.bean.a09.F1$$EnhancerBySpringCGLIB$$ea96cbb5
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@37271612
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@4c309d4d
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F1@37883b97

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

解决方式二

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

@Component
@Scope(
value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,
proxyMode = ScopedProxyMode.TARGET_CLASS
)
public class F2 {
}

@Getter
@Component
public class E {

@Autowired
private F2 f2;
}

之后再测试:

@Slf4j
@ComponentScan("indi.lcp.bean.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();
}
}
indi.lcp.bean.a09.A09Application : class indi.lcp.bean.a09.F2$$EnhancerBySpringCGLIB$$f28665e2
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F2@2525ff7e
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F2@524d6d96
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F2@152aa092

解决方式三

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class F3 {
}

@Component
public class E {

@Autowired
private ObjectFactory<F3> f3;

public F3 getF3() {
return f3.getObject();
}
}
@Slf4j
@ComponentScan("indi.lcp.bean.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());
log.info("{}", e.getF3());

context.close();
}
}
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F3@76f2bbc1
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F3@306cf3ea

解决方式四

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class F4 {
}

@Component
public class E {

@Autowired
private ApplicationContext applicationContext;

public F4 getF4() {
return applicationContext.getBean(F4.class);
}
}
@Slf4j
@ComponentScan("indi.lcp.bean.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();
}
}
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F4@2beee7ff
indi.lcp.bean.a09.A09Application : indi.lcp.bean.a09.F4@5136d012

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

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

AspectJ 编译器增强

创建一个 SpringBoot 项目,除了常见的依赖外,记得导入 AOP 相关的依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Service 类:

@Service
public class MyService {
private static final Logger log = LoggerFactory.getLogger(MyService.class);
public void foo() {
log.info("foo()");
}
}

切面类,注意这个切面类没有被 Spring 管理:

/**
* 注意此切面并未被 Spring 管理
*/
@Aspect
public class MyAspect {
private static final Logger log = LoggerFactory.getLogger(MyAspect.class);

@Before("execution(* indi.lcp.service.MyService.foo())")
public void before() {
log.info("before()");
}
}

用于测试的主启动类:

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

运行主启动类后,控制台会显示:

indi.lcp.A10Application                : service class: class indi.lcp.service.MyService
indi.lcp.aop.MyAspect : before()
indi.lcp.service.MyService : foo()

如果完全按照上述步骤进行,会发现 输出结果和给出的结果不一样。

在揭晓答案前,查看 service.getClass() 打印出的信息,它打印出的是原始类的 Class 信息,而非代理类的 Class 信息。

如果要问到 Spring AOP 的实现原理是什么,一下就能想到的是使用了代理,但这里并没有使用代理,依旧实现了增强。

这是因为在 pom.xml 中还引入了一个插件:

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

之后不再使用 IDEA 自带的编译器进行编译,而是使用 Maven 编译

编译之后查看生成的 target 文件夹下的 MyService.class 文件:

@Service
public class MyService {
private static final Logger log = LoggerFactory.getLogger(MyService.class);

public MyService() {
}

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

可以看到在 foo() 方法中增加了一行代码:MyAspect.aspectOf().before();,也就是这行代码对 foo() 方法实现了增强。

这种方式属于编译时增强,和 Lombok 类似。

既然如此,那岂不是说使用这种方式时,没有 Spring 容器也能实现方法的增强?

确实如此。

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();
}
}
indi.lcp.A10Application - service class: class indi.lcp.service.MyService
indi.lcp.aop.MyAspect - before()
indi.lcp.service.MyService - foo()

除此之外,使用这种方式,就算 foo() 方法是静态方法,也能够成功增强。

public class MyService {
private static final Logger log = LoggerFactory.getLogger(MyService.class);
public static void foo() {
log.info("foo()");
}
}
public class A10Application {
public static void main(String[] args) {
MyService.foo();
}
}
indi.lcp.aop.MyAspect - before()
indi.lcp.service.MyService - foo()

Agent 类加载

重新创建一个 SpringBoot 项目,同样需要导入 AOP 相关的依赖。

一个 Service 类:

@Slf4j
@Service
public class MyService {
final public void foo() {
log.info("foo()");
bar();
}

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

一个切面类,注意这个切面类没有被 Spring 管理:

@Slf4j
@Aspect
public class MyAspect {

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

一个用于测试的主启动类:

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

}

运行主启动类后,控制台会显示:

indi.lcp.A11Application                : service class: class indi.lcp.service.MyService
indi.lcp.aop.MyAspect : before()
indi.lcp.service.MyService : foo()
indi.lcp.aop.MyAspect : before()
indi.lcp.service.MyService : bar()

如果完全按照上述步骤进行,会发现输出结果和给出的结果不一样。

那是怎么达到增强的效果呢?

首先得在 resources 目录下新建 META-INF 文件夹,并在 META-INF 目录下新建 aop.xml 文件,其内容如下:

<aspectj>
<aspects>
<!-- 切面类全限定类名 -->
<aspect name="indi.lcp.aop.MyAspect"/>
<weaver options="-verbose -showWeaveInfo">
<!-- 被增强方法所在类的全限定类名 -->
<include within="indi.lcp.service.MyService"/>
<!-- 切面类全限定类名 -->
<include within="indi.lcp.aop.MyAspect"/>
</weaver>
</aspects>
</aspectj>

在运行 main() 方法前添加 VM options:

-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 文件,会发现其内容并没有被修改,可以断定不是编译时增强,这里是在类加载时增强

动态代理

  1. JDK 动态代理

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

public class JdkProxyDemo {
interface Foo {
void foo();
}

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

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

// 用来加载在运行期间动态生成的字节码
ClassLoader classLoader = JdkProxyDemo.class.getClassLoader();
Foo proxy = (Foo) Proxy.newProxyInstance(classLoader, new Class[]{Foo.class}, (p, method, params) -> {
System.out.println("before...");
// 目标.方法(参数) --> 方法.invoke(目标, 参数)
Object result = method.invoke(target, params);
System.out.println("after...");
// 也返回目标方法执行的结果
return result;
});

proxy.foo();
}
}

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

before...
target foo
after...

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

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

11.2. CGLib 动态代理

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

public class CglibProxyDemo {
static class Target {
public void foo() {
System.out.println("target foo");
}
}

public static void main(String[] args) {
Target target = new Target();

Target proxy = (Target) Enhancer.create(Target.class, (MethodInterceptor) (obj, method, params, methodProxy) -> {
System.out.println("before...");
// 用方法反射调用目标
Object result = method.invoke(target, params);
System.out.println("after...");
return result;
});

proxy.foo();
}
}

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

before...
target foo
after...

调用目标方法的方式有三种,上文使用的是:

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

使用这种方式,将利用反射对目标方法进行调用。

还可以使用 methodProxy 不利用反射对目标方法进行调用:

// 内部没使用反射,需要目标(spring 的选择)
Object result = methodProxy.invoke(target, args);
// 内部没使用反射,需要代理
Object result = methodProxy.invokeSuper(obj, args);
  • 与 JDK 动态代理相比,CGLib 动态代理无需实现接口
  • 代理对象和目标对象是父子关系,也就是说代理类继承了目标类
  • 由于代理类继承了目标类,因此目标类不能被 final 修饰,否则将出现以下异常信息:
java.lang.IllegalArgumentException: Cannot subclass final class indi.lcp.a12.CglibProxyDemo$Target
  • 代理类继承目标类后,还会重写目标类中要求被增强的方法,因此被增强的方法不能被 final 修饰,否则将无法被增强,但不会抛出异常

JDK 动态代理原理

12.1 JDK 动态代理的模拟

模拟 JDK 动态代理的实现:

interface Foo {
void foo();
}

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

public class $Proxy0 implements A13.Foo{
@Override
public void foo() {
// 1. 功能增强
System.out.println("before...");
// 2. 调用目标
new A13.Target().foo();
}
}

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

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

before...
target foo

代码的实现很简单,但仔细想一下,如果是 JDK 中的实现:

“功能增强”的代码实现会直接硬编码吗?直接打印?

“调用目标”的代码就这样直接写上去?存不存在满足某些条件才调用目标的场景呢?

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

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

interface Foo {
void foo();
}

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

interface InvocationHandler {
void invoke();
}

public class $Proxy0 implements A13.Foo{

private final A13.InvocationHandler h;

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

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

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

运行 main() 方法,控制台依旧成功打印出:

before...
target foo

多个抽象方法的接口

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

interface Foo {
void foo();

void bar();
}

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

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

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

public class $Proxy0 implements A13.Foo{

private final A13.InvocationHandler h;

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

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

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

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

此时再执行 main() 方法,控制台上打印出:

before...
target foo
before...
target foo

打印结果有点问题。当调用代理对象的 bar() 方法时,输出了 target foo,而不是 bar() 方法应该打印的 target bar。

原因就出在实现 InvocationHandler 的 invoke() 方法时,依旧只调用了目标类的 foo() 方法,而不是 bar() 方法。

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

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

interface InvocationHandler {
void invoke(Method method, Object[] params) throws Throwable;
}

增加参数之后需要修改代理类,并将实现的抽象方法的 Method 对象与参数传递给 invoke() 方法:

public class $Proxy0 implements A13.Foo{

private final A13.InvocationHandler h;

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

@Override
@SneakyThrows
public void foo() {
Method method = A13.Foo.class.getMethod("foo");
h.invoke(method, new Object[0]);
}

@Override
@SneakyThrows
public void bar() {
Method method = A13.Foo.class.getMethod("bar");
h.invoke(method, new Object[0]);
}
}

还需要修改下 main() 方法中 InvocationHandler 的实现,利用传递的 Method 对象和参数信息反射调用目标方法:

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

再执行 main() 方法,控制台上打印出:

before...
target foo
before...
target bar

有返回值的抽象方法

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

interface Foo {
void foo();

int bar();
}

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

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

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

目标类很简单,直接返回,那代理类返回什么?

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

interface InvocationHandler {
Object invoke(Method method, Object[] params) throws Throwable;
}
public class $Proxy0 implements A13.Foo {

private final A13.InvocationHandler h;

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

@Override
public void foo() {
try {
Method foo = A13.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 = A13.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() 方法的返回值:

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
100

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

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

public class $Proxy0 implements A13.Foo {

private final A13.InvocationHandler h;

public $Proxy0(A13.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 {
return (int) h.invoke(bar, new Object[0]);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}

static Method foo;
static Method bar;

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

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

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

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

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

public class $Proxy0 implements A13.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 参数:

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 接口无异,注释自定义的 InvocationHandler,更换为 JDK 提供的 InvocationHandler 接口。

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

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 来进一步减少代码:

import java.lang.reflect.InvocationHandler;

public class $Proxy0 extends Proxy implements A13.Foo {

private static final long serialVersionUID = -6909541593982979501L;

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

// --snip--
}

12.2 代理类的源码

JDK 动态代理生成的代理类是以字节码的形式存在的,并不存在所谓的 .java 文件,但也不是说就没办法看到生成的代理类信息了。

以【11.1 JDK 动态代理】中的程序为例,查看 JDK 生成的代理类信息。

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

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

知道被反编译文件的全限定类名

程序不能中断,需要存在 Java 进程

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

@SneakyThrows
public static void main(String[] args) {
// 原始对象
Target target = new Target();

// 用来加载在运行期间动态生成的字节码
ClassLoader classLoader = JdkProxyDemo.class.getClassLoader();
Foo proxy = (Foo) Proxy.newProxyInstance(classLoader, new Class[]{Foo.class}, (p, method, params) -> {
System.out.println("before...");
// 目标.方法(参数) --> 方法.invoke(目标, 参数)
Object result = method.invoke(target, params);
System.out.println("after...");
// 也返回目标方法执行的结果
return result;
});

// 打印代理类的全限定类名
System.out.println(proxy.getClass());

proxy.foo();

// 只要不在控制台上输入并回车,程序就不会终端
System.in.read();
}

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

class indi.lcp.a12.$Proxy0
before...
target foo
after...

其中的 indi.lcp.a12.$Proxy0 就是生成的代理类的全限定类名,可以把它复制下来,之后按照【10. Agent 类加载】中使用 Arthas 的方式 indi.lcp.a12.$Proxy0 进行反编译即可。

将生成的代理类字节码文件保存在磁盘上

除了借助外部工具外,还可以直接将 JDK 生成的代理类字节码文件保存在磁盘上,其做法与【Lambda 与序列化】一文中将函数式接口动态生成的 Class 保存到磁盘上类似。

可以在 main() 方法开头加一句:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 或者
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

不同版本的 JDK 添加的配置信息不同,至于具体是哪一个可以查看 JDK 中 ProxyGenerator 类中的 saveGeneratedFiles 成员变量,比如:

private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

显然,此处应该使用第一种方式来设置系统属性。最终的 main() 方法如下:

public static void main(String[] args) {
// 将动态代理生成的 class 保存到磁盘
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

// 原始对象
Target target = new Target();

// 用来加载在运行期间动态生成的字节码
ClassLoader classLoader = JdkProxyDemo.class.getClassLoader();
Foo proxy = (Foo) Proxy.newProxyInstance(classLoader, new Class[]{Foo.class}, (p, method, params) -> {
// --snip--
});

proxy.foo();
}

除此之外,还可以通过在运行前添加 VM options:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

如果既在代码里设置了系统属性,又配置了 VM options,最终以代码中的配置为主。

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

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

public $Proxy0(InvocationHandler var1) throws {
super(var1);
}

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

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final void foo() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("indi.lcp.a12.JdkProxyDemo$Foo").getMethod("foo");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

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

12.3 JDK 代理类字节码生成

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

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

ASM Bytecode outline 插件在高版本 Java 中可能无法很好地工作,建议在 Java8 环境下使用。

自行编写一个接口和代理类:

public interface Foo {
void foo();
}

public class $Proxy0 extends Proxy implements Foo {
private static final long serialVersionUID = 6059465134835974286L;

static Method foo;

static {
try {
foo = 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, foo, null);
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
}

将上述代码进行编译,编译成功后在 $Proxy0 文件中右击,选择 Show Bytecode outline 浏览当前类对应的字节码信息

查看 ASMified,并拷贝其内容,复制到 $Proxy0Dump 中:

import jdk.internal.org.objectweb.asm.*;

/**
* @author lcp
* @date 2023/1/16 22:04
*/
public class $Proxy0Dump implements Opcodes {

public static byte[] dump() throws Exception {

ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;

// 生成类信息
cw.visit(52, ACC_PUBLIC + ACC_SUPER, "indi/lcp/$Proxy0", null, "java/lang/reflect/Proxy", new String[]{"indi/lcp/Foo"});

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

{
fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "serialVersionUID", "J", null, new Long(6059465134835974286L));
fv.visitEnd();
}
{
fv = cw.visitField(ACC_STATIC, "foo", "Ljava/lang/reflect/Method;", null, null);
fv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PROTECTED, "<init>", "(Ljava/lang/reflect/InvocationHandler;)V", null, null);
mv.visitParameter("h", 0);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(26, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V", false);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLineNumber(27, l1);
mv.visitInsn(RETURN);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLocalVariable("this", "Lindi/lcp/$Proxy0;", null, l0, l2, 0);
mv.visitLocalVariable("h", "Ljava/lang/reflect/InvocationHandler;", null, l0, l2, 1);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "foo", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
Label l1 = new Label();
Label l2 = new Label();
mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Throwable");
mv.visitLabel(l0);
mv.visitLineNumber(32, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "indi/lcp/$Proxy0", "h", "Ljava/lang/reflect/InvocationHandler;");
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "indi/lcp/$Proxy0", "foo", "Ljava/lang/reflect/Method;");
mv.visitInsn(ACONST_NULL);
mv.visitMethodInsn(INVOKEINTERFACE, "java/lang/reflect/InvocationHandler", "invoke", "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;", true);
mv.visitInsn(POP);
mv.visitLabel(l1);
mv.visitLineNumber(35, l1);
Label l3 = new Label();
mv.visitJumpInsn(GOTO, l3);
mv.visitLabel(l2);
mv.visitLineNumber(33, l2);
mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Throwable"});
mv.visitVarInsn(ASTORE, 1);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLineNumber(34, l4);
mv.visitTypeInsn(NEW, "java/lang/reflect/UndeclaredThrowableException");
mv.visitInsn(DUP);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/UndeclaredThrowableException", "<init>", "(Ljava/lang/Throwable;)V", false);
mv.visitInsn(ATHROW);
mv.visitLabel(l3);
mv.visitLineNumber(36, l3);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
Label l5 = new Label();
mv.visitLabel(l5);
mv.visitLocalVariable("throwable", "Ljava/lang/Throwable;", null, l4, l3, 1);
mv.visitLocalVariable("this", "Lindi/lcp/$Proxy0;", null, l0, l5, 0);
mv.visitMaxs(4, 2);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
Label l1 = new Label();
Label l2 = new Label();
mv.visitTryCatchBlock(l0, l1, l2, "java/lang/NoSuchMethodException");
mv.visitLabel(l0);
mv.visitLineNumber(19, l0);
mv.visitLdcInsn(Type.getType("Lindi/lcp/Foo;"));
mv.visitLdcInsn("foo");
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Class");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;", false);
mv.visitFieldInsn(PUTSTATIC, "indi/lcp/$Proxy0", "foo", "Ljava/lang/reflect/Method;");
mv.visitLabel(l1);
mv.visitLineNumber(22, l1);
Label l3 = new Label();
mv.visitJumpInsn(GOTO, l3);
mv.visitLabel(l2);
mv.visitLineNumber(20, l2);
mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/NoSuchMethodException"});
mv.visitVarInsn(ASTORE, 0);
Label l4 = new Label();
mv.visitLabel(l4);
mv.visitLineNumber(21, l4);
mv.visitTypeInsn(NEW, "java/lang/NoSuchMethodError");
mv.visitInsn(DUP);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/NoSuchMethodException", "getMessage", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/NoSuchMethodError", "<init>", "(Ljava/lang/String;)V", false);
mv.visitInsn(ATHROW);
mv.visitLabel(l3);
mv.visitLineNumber(23, l3);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitLocalVariable("e", "Ljava/lang/NoSuchMethodException;", null, l4, l3, 0);
mv.visitMaxs(3, 1);
mv.visitEnd();
}
cw.visitEnd();

return cw.toByteArray();
}
}

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

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();
}
}

运行 main() 方法后,在工作目录下生成 $Proxy0.class。

工作目录查看方式:

img

也就是说会在 D:\Code\IdeaCode\advanced-spring 目录下生成 $Proxy0.class 文件,IDEA 反编译后的内容与手动编写的 $Proxy0.java 文件的内容无异。

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

public static void main(String[] args) throws Exception {
byte[] dump = $Proxy0Dump.dump();

ClassLoader classLoader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return super.defineClass(name, dump, 0, dump.length);
}
};

Class<?> proxyClass = classLoader.loadClass("indi.lcp.$Proxy0");
Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
Foo fooProxy = (Foo) constructor.newInstance((InvocationHandler) (proxy, method, args1) -> {
System.out.println("before...");
System.out.println("模拟调用目标");
return null;
});

fooProxy.foo();
}

运行上述代码后,控制台打印出:

before...
模拟调用目标

12.4 JDK 反射优化

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

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

相比于正常调用方法,利用反射的性能要稍微低一些,JDK 有怎么反射进行优化吗?

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");
}
}

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

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 类中的信息,内容如下:

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());
}
}
}

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

TestMethodProxy.foo((int)c);

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

CGLib 动态代理原理

13.1 CGLib 动态代理的模拟

同样先模拟下 CGLib 动态代理的模拟:

被代理的类,即目标类:

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 动态代理生成的代理类:

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);
}
}
}
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);
}

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

before
save()
before
save(int)
before
save(long)

13.2 MethodProxy

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

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

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

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

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 指方法描述符(【Lambda 与序列化】一文中介绍了关于 Java 描述符的更多内容);
  • 参数 name1 指带 增强 功能的方法名称;
  • 参数 name2 指带 原始 功能的方法名称。
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);
}
}
}

main() 方法中不再使用 Method 类型的参数对方法进行调用,而是使用 MethodProxy 类型的参数:

// 内部没有反射调用,但需要结合目标对象使用
return methodProxy.invoke(target, args);

或者:

// 内部没有反射调用,但需要结合代理对象使用
return methodProxy.invokeSuper(o, args);

MethodProxy 原理

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

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

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

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) 方法。

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

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");

/**
* <p>获取目标方法的编号</p>
* <p>
* Target 目标类中的方法:
* save() 0
* save(int) 1
* save(long) 2
* </p>
*
* @param signature 包含方法名称、参数返回值
* @return 方法编号
*/
public int getIndex(Signature signature) {
if (s0.equals(signature)) {
return 0;
}
if (s1.equals(signature)) {
return 1;
}
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;
}
if (index == 1) {
((Target) target).save((int) args[0]);
return null;
}
if (index == 2) {
((Target) target).save((long) args[0]);
return null;
}
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});
}
}

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

save()
save(long)

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

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");

/**
* <p>获取代理方法的编号</p>
* <p>
* Proxy 代理类中的方法:
* saveSuper() 0
* saveSuper(int) 1
* saveSuper(long) 2
* </p>
*
* @param signature 包含方法名称、参数返回值
* @return 方法编号
*/
public int getIndex(Signature signature) {
if (s0.equals(signature)) {
return 0;
}
if (s1.equals(signature)) {
return 1;
}
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).saveSuper();
return null;
}
if (index == 1) {
((Proxy) proxy).saveSuper((int) args[0]);
return null;
}
if (index == 2) {
((Proxy) proxy).saveSuper((long) args[0]);
return null;
}
throw new RuntimeException("无此方法");
}

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

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

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 代理类。

JDK 和 CGLib 的统一

15.1. advisor

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

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

img

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

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

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

本节将重点介绍 advisor 切面。

15.2 切面与代理对象的创建

通过以下四步创建切面和代理:

  • 备好切点
  • 备好通知
  • 备好切面
  • 创建代理

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

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 的工厂,其内部通过不同的情况选择不同的代理实现,更方便地创建代理对象。

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();
}

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

class indi.lcp.a15.A15$Target1$$EnhancerBySpringCGLIB$$381723d1
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 信息:

factory.setInterfaces(target.getClass().getInterfaces());

之后再运行 main(),控制台打印出:

class indi.lcp.a15.$Proxy0
before...
target1 foo
after...
target1 bar

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

再设置 factory 对象的 proxyTargetClass 为 true:

factory.setProxyTargetClass(true);

运行 main() 方法后,控制台打印出以下内容,选择 CGLib 动态代理作为动态代理的实现方式:

class indi.lcp.a15.A15$Target1$$EnhancerBySpringCGLIB$$34c2d9b8
before...
target1 foo
after...
target1 bar

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

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();
}

运行 main() 方法后,控制台打印出以下内容,依旧选择 CGLib 动态代理作为动态代理的实现方式:

class indi.lcp.a15.A15$Target2$$EnhancerBySpringCGLIB$$4bb2ac74
before...
target2 foo
after...
target2 bar

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

  • JdkDynamicAopProxy
  • ObjenesisCglibAopProxy

img

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

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

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

切点匹配

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

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() {

}
}

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

false
true
true
false

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

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

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

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() {

}
}

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

true
false
true
true

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

从 @Aspect 到 Advisor

  1. AnnotationAwareAspectJAutoProxyCreator

准备一下类:

  • 两个目标类:

一个使用 @Aspect 的高级切面

一个利用配置类实现的低级切面 Advisor

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:

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();
}

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

aspect1
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
advisor3
advices

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

AnnotationAwareAspectJAutoProxyCreator 有两个主要作用:

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

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

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

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

findEligibleAdvisors() 方法

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

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

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

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();
}

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

org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR
org.springframework.aop.support.DefaultPointcutAdvisor: pointcut [AspectJExpressionPointcut: () execution(* foo())]; advice [org.springframework.aop.framework.autoproxy.A17$Config$$Lambda$56/802243390@7bd4937b]
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
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();
}

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

class org.springframework.aop.framework.autoproxy.A17$Target1$$EnhancerBySpringCGLIB$$634976f6
class org.springframework.aop.framework.autoproxy.A17$Target2

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

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

public static void main(String[] args) {
// --snip--

((Target1) o1).foo();

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

切面的顺序控制

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

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

@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 注解是无法生效的。

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

@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 初始化完成之后

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

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

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。运行 main() 方法后,控制台打印出:

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 初始化完成后,额外打印了一句日志信息:

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,出现循环依赖:

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 列表。在搜索过程中,如果遇到高级切面,则会将其转换成低级切面。

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

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 注解为例:

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);
}
}

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

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.1 统一转换成环绕通知

通知相关注解都对应一个原始通知类,在 Spring 底层会将这些通知转换成环绕通知 MethodInterceptor。如果原始通知类本就实现了 MethodInterceptor 接口,则无需转换。

原始通知类 是否需要转换成 MethodInterceptor

AspectJMethodBeforeAdvice ✅

AspectJAfterReturningAdvice ✅

AspectJAfterThrowingAdvice ❌

AspectJAfterAdvice ❌

AspectJAroundAdvice ❌

使用 ProxyFactory 无论基于哪种方式创建代理对象,最终调用 advice(通知,或者说通知对应的方法)的都是 MethodInvocation 对象。

项目中存在的 advisor(原本的低级切面和由高级切面转换得到的低级切面)往往不止一个,它们一个套一个地被调用,因此需要一个调用链对象,即 MethodInvocation。

MethodInvocation 需要知道 advice 有哪些,还需要知道目标对象是哪个。调用次序如下:

img

由上图可知,环绕 通知最适合作为 advice,而 Before、AfterReturning 都应该转换成环绕通知。

统一转换成环绕通知的形式,体现了设计模式中的适配器模式:

对外更方便使用和区分各种通知类型

对内统一都是环绕通知,统一使用 MethodInterceptor 表示

通过 ProxyFactory 对象的 getInterceptorsAndDynamicInterceptionAdvice() 方法将其他通知统一转换为 MethodInterceptor 环绕通知:

注解原始通知类适配器拦截器
@BeforeAspectJMethodBeforeAdviceMethodBeforeAdviceAdapterMethodBeforeAdviceInterceptor
@AfterReturningAspectJAfterReturningAdviceAspectJAfterReturningAdviceAfterReturningAdviceInterceptor

转换得到的通知都是静态通知,体现在 getInterceptorsAndDynamicInterceptionAdvice() 方法中的 Interceptors 部分,这些通知在被调用时无需再次检查切点,直接调用即可。

切面类与目标类:

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");
}
}

将高级切面转换成低级切面,并将通知统一转换成环绕通知:

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

根据打印信息可知:

前置通知 AspectJMethodBeforeAdvice 被转换成 MethodBeforeAdviceInterceptor

环绕通知 AspectJAroundAdvice 保持不变

后置通知 AspectJAfterReturningAdvice 被转换成 AfterReturningAdviceInterceptor

18.2 调用链执行

高级切面成功转换成低级切面,切面中的通知也全部转换成环绕通知 MethodInterceptor,最后还要调用这些通知和目标方法。

这个调用交由调用链对象 MethodInvocation 来完成,在调用链对象中存放了所有经过转换得到的环绕通知和目标方法。

MethodInvocation 是一个接口,其最根本的实现是 ReflectiveMethodInvocation。

构建 ReflectiveMethodInvocation 对象需要 6 个参数:

  1. proxy:代理对象
  2. target:目标对象
  3. method:目标对象中的方法对象
  4. arguments:调用目标对象中的方法需要的参数
  5. targetClass:目标对象的 Class 对象
  6. interceptorsAndDynamicMethodMatchers:转换得到的环绕通知列表
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。但调用链对象不是已经创建好了吗?

这是因为调用链在执行过程会调用到很多通知,而某些通知内部可能需要使用调用链对象。因此需要将调用链对象存放在某一位置,使所有通知都能获取到调用链对象。

这个“位置”就是 当前线程。

那怎么将调用链对象放入当前线程呢?

可以在所有通知的最外层再添加一个环绕通知,其作用是将调用链对象放入当前线程。

img

可以使用 Spring 提供的 ExposeInvocationInterceptor 作为最外层的环绕通知。

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();
}

再次运行 main() 方法不再报错,控制台打印出:

before1
around...before
before2
target foo
around...after
afterReturning

18.3 模拟实现调用链

调用链执行过程是一个递归过程。执行 proceed() 方法将调用调用链中下一个通知或目标方法。当调用链中没有通知时,就调用目标方法,反之调用下一个通知。

这体现了设计模式中的责任链模式。

目标类 Target:

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

实现 MethodInterceptor 接口,编写两个环绕通知:

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 接口,实现自己的调用链:

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;
}
}

编写 main() 方法,执行调用链,查看控制台输出结果:

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 返回最终的结果

动态通知调用

前文的示例都是静态通知调用,无需参数绑定,执行时无需切点信息,性能较高。

相应地就有动态通知调用,它需要参数绑定,执行时还需要切点信息,性能较低。比如:

@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:

static class Target {
public void foo(int x) {
System.out.printf("target foo(%d)\n", x);
}
}

配置类 MyConfig:

@Configuration
static class MyConfig {
@Bean
public AnnotationAwareAspectJAutoProxyCreator proxyCreator() {
return new AnnotationAwareAspectJAutoProxyCreator();
}

@Bean
public MyAspect myAspect() {
return new MyAspect();
}
}

编写 main() 方法,新建 Spring 容器,查找符合条件的切面,将所有通知转换成环绕通知:

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);
}
}

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

org.springframework.aop.interceptor.ExposeInvocationInterceptor@73e22a3d
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@47faa49c
org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher@28f2a10f

第一个 ExposeInvocationInterceptor 对象是 Spring 添加的环绕通知,第二个 MethodBeforeAdviceInterceptor 对象是前置通知转换得到的环绕通知,那 InterceptorAndDynamicMethodMatcher 对象是什么呢?

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 修饰,也没提供获取的方式,但可以使用反射:

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);
}
}

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

普通环绕通知:org.springframework.aop.interceptor.ExposeInvocationInterceptor@73e22a3d
普通环绕通知:org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@47faa49c
环绕通知和切点:org.springframework.aop.framework.InterceptorAndDynamicMethodMatcher@f736069
切点为:AspectJExpressionPointcut: (int x) execution(* foo(..)) && args(x)
通知为:org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor@6da21078

根据打印的切点信息可知,InterceptorAndDynamicMethodMatcher 对象的确对应了动态通知调用。

最后创建调用链对象,执行通知和原始方法:。

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)

动态通知调用需要切点信息,需要对参数进行匹配和绑定,复杂程度高,性能比静态通知调用低。

RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter

20.1 DispatcherServlet 的初始化

选择支持内嵌Tomcat 服务器的 Spring 容器作为 ApplicationContext 的实现:

public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}

WebConfig 作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂、DispatcherServlet 和 DispatcherServlet 注册对象。

@Configuration
@ComponentScan
public class WebConfig {
/**
* 内嵌 Web 容器工厂
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
return new TomcatServletWebServerFactory();
}

/**
* 创建 DispatcherServlet
*/
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

/**
* 注册 DispatcherServlet,Spring MVC 的入口
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
}

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

Tomcat initialized with port(s): 8080 (http)
Root WebApplicationContext: initialization completed in 2132 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 的初始化时机

断点 DispatcherServlet 的 onRefresh() 方法中 this.initStrategies(context); 的所在行:

protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}

以 DEBUG 方式重启程序,此时程序尚未执行到断点处。

再次在浏览器中访问 localhost:8080,程序执行到断点处。

查看调用栈可知,是从 GenericServlet 的 init() 方法执行到 onRefresh() 方法的:

public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}

因此 DispatcherServlet 的初始化流程走的是 Servlet 的初始化流程。

使 DispatcherServlet 在Tomcat 服务器启动时被初始化

修改添加到 Spring 容器的 DispatcherServlet 注册 Bean:

@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 注解标记的方法的参数直接在方法中使用即可:

server.port=9090
spring.mvc.servlet.load-on-startup=1
@Configuration
@ComponentScan
@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
/**
* 内嵌 Web 容器工厂
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties) {
return new TomcatServletWebServerFactory(serverProperties.getPort());
}

/**
* 创建 DispatcherServlet
*/
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

/**
* 注册 DispatcherServlet,Spring MVC 的入口
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
return registrationBean;
}
}

再次重启程序,根据控制台输出的内容可知,Tomcat 此时监听的端口是 9090,DispatcherServlet 也在 Tomcat 启动时被初始化。

DispatcherServlet 初始化时执行的操作

回到 DispatcherServlet 的 onRefresh() 方法,它又调用了 initStrategies() 方法:

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() 为例:

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,即处理器映射器,用于建立请求路径与控制器方法的映射关系。

RequestMappingHandlerMapping 是 HandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。

当 Spring 容器中没有 HandlerMapping 的实现时,尽管 DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。

在配置类中将 RequestMappingHandlerMapping 添加到 Spring 容器:

@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}

定义一个控制器类:

@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,再获取请求路径与映射器方法的映射关系,并根据给定请求获取控制器方法:

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((k, v) -> System.out.println(k + " = " + v));
// 根据给定请求获取控制器方法,返回处理器执行链
HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
System.out.println(chain);
}
{GET [/test1]} = indi.lcp.a20.Controller1#test1()
{POST [/test2]} = indi.lcp.a20.Controller1#test2(String)
{PUT [/test3]} = indi.lcp.a20.Controller1#test3(String)
{ [/test4]} = indi.lcp.a20.Controller1#test4()
HandlerExecutionChain with [indi.lcp.a20.Controller1#test1()] and 0 interceptors

getHandler() 方法返回的对象时处理器执行链,不仅包含映射器方法,还包含需要执行的拦截器信息

需要导入以下依赖:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>

20.3 RequestMappingHandlerAdapter

RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。

同样需要在配置类中将 RequestMappingHandlerAdapter 添加到 Spring 容器,但该类中需要测试的方法被 protected 修饰,无法直接使用,因此创建一个子类,将子类添加到 Spring 容器中:

public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
return super.invokeHandlerMethod(request, response, handlerMethod);
}
}
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}

在 main() 方法中测试 RequestMappingHandlerAdapter 的 invokeHandlerMethod() 方法:

public static void main(String[] args) throws Exception {
// --snip--

MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test2");
request.setParameter("name", "lcp");
MockHttpServletResponse response = new MockHttpServletResponse();
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request, response, ((HandlerMethod) handlerMapping.getHandler(request).getHandler()));
}

实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?

显然是需要解析 @RequestParam 注解。

Spring 支持许多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 MyRequestMappingHandlerAdapter 的 getArgumentResolvers() 方法获取所有参数解析器。

Spring 也支持许多种类的控制器方法返回值类型,使用 MyRequestMappingHandlerAdapter 的 getReturnValueHandlers() 方法获取所有返回值处理器。

自定义参数解析器

假如经常需要使用到请求头中的 Token 信息,自定义 @Token 注解,使用该注解标记控制器方法的哪个参数来获取 Token 信息:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}

使 test3() 控制器方法参数被 @Token 标记:

@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
log.debug("test3({})", token);
return null;
}

自定义参数解析器:

public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(Token.class) != null;
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return webRequest.getHeader("token");
}
}

将参数解析器添加到 HandlerAdapter 中:

@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter();
adapter.setCustomArgumentResolvers(Collections.singletonList(tokenArgumentResolver));
return adapter;
}

测试执行 test3() 控制器方法:

public static void main(String[] args) throws Exception {
// --snip--

MockHttpServletRequest tokenRequest = new MockHttpServletRequest("PUT", "/test3");
tokenRequest.addHeader("token", "token info");
handlerAdapter.invokeHandlerMethod(tokenRequest, response, ((HandlerMethod) handlerMapping.getHandler(tokenRequest).getHandler()));
}

自定义返回值处理器

当 @ResponseBody 标记了控制器方法时,方法的返回值会转换成 JSON 写入响应体中。

自定义 @Yml 注解,被 @Yml 注解标记的控制器方法的返回值会转换成 YAML 写入响应体中。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Yml {
}

使 test4() 控制器方法被 @Yml 注解标记:

@RequestMapping("/test4")
@Yml
public User test4() {
log.debug("test4");
return new User("张三", 18);
}

自定义返回值处理器将返回值转换成 YAML:

public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return returnType.getMethodAnnotation(Yml.class) != null;
}

@Override
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 转换返回结果为 YAML
String str = new Yaml().dump(returnValue);
// 将 YAML 字符串写入响应体
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setContentType("text/plain;charset=utf-8");
response.getWriter().print(str);
// 设置请求已经处理完毕
mavContainer.setRequestHandled(true);
}
}

将返回值处理器添加到 HandlerAdapter 中:

@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter();
YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler();
adapter.setCustomReturnValueHandlers(Collections.singletonList(ymlReturnValueHandler));
return adapter;
}

测试执行 test4() 控制器方法:

public static void main(String[] args) throws Exception {
// --snip--

MockHttpServletRequest test4Req = new MockHttpServletRequest("GET", "/test4");
handlerAdapter.invokeHandlerMethod(test4Req, response, ((HandlerMethod) handlerMapping.getHandler(test4Req).getHandler()));
byte[] content = response.getContentAsByteArray();
System.out.println(new String(content, StandardCharsets.UTF_8));
}

参数解析器

Spring 提供了许多种类的控制器方法参数解析器,定义一个包含多个不同种类参数的控制器方法:

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
) {
}
}

@Getter
@Setter
@ToString
static class User {
private String name;
private int age;
}

将控制器方法封装成 HandlerMethod 并打印方法中每个参数的信息:

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 appendAt = annotations.length() > 0 ? "@" + annotations + " " : "";
// 设置参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
System.out.println("[" + parameter.getParameterIndex() + "] " + appendAt +
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

21.1. @RequestParam

@RequestParam 注解的解析需要使用到 RequestParamMethodArgumentResolver 参数解析器。构造时需要两个参数:

  • beanFactory:Bean 工厂对象。需要解析 ${} 时,就需要指定 Bean 工厂对象
  • useDefaultResolution:布尔类型参数。为 false 表示只解析添加了 @RequestParam 注解的参数,为 true 针对未添加 @RequestParam 注解的参数也使用该参数解析器进行解析。

RequestParamMethodArgumentResolver 利用 resolveArgument() 方法完成参数的解析,该方法需要传递四个参数:

  • parameter:参数对象
  • mavContainer:ModelAndView 容器,用来存储中间的 Model 结果
  • webRequest:由 ServletWebRequest 封装后的请求对象
  • binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 18 转换成整
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 appendAt = annotations.length() > 0 ? "@" + annotations + " " : "";
// 设置参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
String paramInfo = "[" + parameter.getParameterIndex() + "] " + appendAt +
parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName();

if (resolver.supportsParameter(parameter)) {
Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(Objects.requireNonNull(v).getClass());
System.out.println(paramInfo + " -> " + v);
} else {
System.out.println(paramInfo);
}
}
}
class java.lang.String
[0] @RequestParam String name1 -> zhangsan
class java.lang.String
[1] String name2 -> lisi
class java.lang.Integer
[2] @RequestParam int age -> 18
class java.lang.String
[3] @RequestParam String home1 -> D:\environment\JDK1.8
class org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@f2ff811
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.

控制器方法 test() 的前 5 个参数解析成功,但在解析第 6 个参数时产生了异常。

这是因为在构造 RequestParamMethodArgumentResolver 对象时,将 useDefaultResolution 设置为 true,针对未添加 @RequestParam 注解的参数都使用该参数解析器进行解析。第 6 个参数需要的 id 信息使用该解析器解析得到的结果是 null,无法将 null 值赋值给基本类型 int,显然第 6 个及其以后的参数应该使用其他参数解析器进行解析。

多个参数解析器的组合 - 组合模式

不同种类的参数需要不同的参数解析器,当前使用的参数解析器不支持当前参数的解析时,就应该换一个参数解析器进行解析。

可以将所有参数解析器添加到一个集合中,然后遍历这个集合,实现上述需求。

Spring 提供了名为 HandlerMethodArgumentResolverComposite 的类,对上述逻辑进行封装。

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 v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(paramInfo + " -> " + v);
} else {
System.out.println(paramInfo);
}
}
}

21.2 @PathVariable

@PathVariable 注解的解析需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。

使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。

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
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@11c9af63
[5] @PathVariable int id -> 123

21.3 @RequestHeader

@RequestHeader 注解的解析需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

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
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@3943a2be
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json

21.4 @CookieValue

@CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

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
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@1329eff
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456

21.5 @Value

@Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

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
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@46fa7c39
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8

21.6 HttpServletRequest

HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。

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
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5f683daf
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@152aa092

ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到:

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 user1 和 User user2 两种参数来说,尽管后者没有使用 @ModelAttribute 注解,但它们使用的是同一种解析器。

添加两个 ServletModelAttributeMethodProcessor 参数解析器,先解析带 @ModelAttribute 注解的参数,再解析不带 @ModelAttribute 注解的参数。

通过 ServletModelAttributeMethodProcessor 解析得到的数据还会被存入 ModelAndViewContainer 中。存储的数据结构是一个 Map,其 key 为 @ModelAttribute 注解指定的 value 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。

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
) {
}
}
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
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@2beee7ff
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5fa07e12
[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}
[12] @RequestBody User user3 -> A21.User(name=李四, age=20)
模型数据: {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 参数解析器。构造时需要传入一个消息转换器列表。

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
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5e17553a
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@13bc8645
[10] @ModelAttribute User user1 -> A21.User(name=张三, age=18)
[11] User user2 -> A21.User(name=张三, age=18)
[12] @RequestBody User user3 -> A21.User(name=李四, age=20)

@RequestBody User user3 参数数据通过 JSON 数据得到,与上一节的解析进行区分。

除此之外,添加的参数解析器顺序也影响着解析结果:

new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)

先添加解析 @ModelAttribute 注解的解析器,再添加解析 @RequestBody 注解的解析器,最后添加解析省略了 @ModelAttribute 注解的解析器。如果更换最后两个解析器的顺序,那么 @RequestBody User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor。

因此 String name2 参数也能通过添加同种参数但不同构造参数的解析器进行解析,注意添加的解析器的顺序,先处理对象,再处理单个参数:

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 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5e17553a
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@13bc8645
[10] @ModelAttribute User user1 -> A21.User(name=张三, age=18)
[11] User user2 -> A21.User(name=张三, age=18)
[12] @RequestBody User user3 -> A21.User(name=李四, age=20)

获取参数名

在项目的 src 目录外创建一个 Bean2.java 文件,使其不会被 IDEA 自动编译:

package indi.lcp.a22;

public class Bean2 {
public void foo(String name, int age) {

}
}

将命令行切换到 Bean2.java 文件所在目录的位置,执行 javac .\Bean2.java 命令手动编译 Bean2.java。查看 Bean2.class 文件的内容:

package indi.lcp.a22;1

public class Bean2 {
public Bean2() {
}

public void foo(String var1, int var2) {
}
}

编译生成的 class 文件中的 foo() 方法的参数名称不再是 name 和 age,也就是说直接使用 javac 命令进行编译得到的字节码文件不会保存方法的参数名称。

执行 javac -parameters .\Bean2.java 再次编译 Bean2.java,并查看得到的 Bean2.class 文件内容:

package indi.lcp.a22;

public class Bean2 {
public Bean2() {
}

public void foo(String name, int age) {
}
}

foo() 方法的参数名称得以保留。

还可以使用 javap -c -v .\Bean2.class 命令反编译 Bean2.class,foo() 方法的反编译结果如下:

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 6: 0
MethodParameters:
Name Flags
name
age

foo() 方法的参数信息被保存在 MethodParameters 中,可以使用 反射 获取:

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.class,foo() 方法的反编译结果如下:

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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lindi/lcp/a22/Bean2;
0 1 1 name Ljava/lang/String;
0 1 2 age I

foo() 方法的参数信息被保存在 LocalVariableTable 中,不能使用反射获取,但可以使用 ASM 获取,使用 Spring 封装的解析工具:

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,而是使用的是 DefaultParameterNameDiscoverer。DefaultParameterNameDiscoverer 将两种实现进行了统一:

public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {

public DefaultParameterNameDiscoverer() {
if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}

}

javac -g 的局限性

假设有这样一个接口:

package indi.lcp.a22;

public interface Bean1 {
public void foo(String name, int age);
}

如果使用 javac -g .\Bean1.java 命令进行编译后,再利用 javap 查看 foo() 方法的反编译结果:

public abstract void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC, ACC_ABSTRACT

并没有记录抽象方法 foo() 的参数信息。

如果使用 javac -parameters .\Bean1.java 呢?

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.1 三种转换接口

底层第一套转换接口与实现

img

Printer 把其它类型转为 String

Parser 把 String 转为其它类型

Formatter 综合 Printer 与 Parser 的功能

Converter 把类型 S 转为类型 T

Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合

FormattingConversionService 利用其它接口实现转换

底层第二套转换接口

由 JDK 提供,而不是 Spring。

img

PropertyEditor 将 String 与其它类型相互转换

PropertyEditorRegistry 可以注册多个 PropertyEditor 对象

可以通过 FormatterPropertyEditorAdapter 与第一套接口进行适配

高层转换接口与实现

img

它们都实现了 TypeConverter 高层转换接口,在转换时会用到 TypeConverterDelegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(使用 Facade 门面模式)

首先查看是否存在实现了 PropertyEditorRegistry 的自定义转换器,@InitBinder 注解实现的就是自定义转换器(用了适配器模式把 Formatter 转为需要的 PropertyEditor)

再查看是否存在 ConversionService 实现

再利用默认的 PropertyEditor 实现

最后有一些特殊处理

SimpleTypeConverter 仅做类型转换

BeanWrapperImpl 利用 Property,即 Getter/Setter,为 Bean 的属性赋值,,必要时进行类型转换

DirectFieldAccessor 利用 Field,即字段,为 Bean 的字段赋值,必要时进行类型转换

ServletRequestDataBinder 为 Bean 的属性执行绑定,必要时进行类型转换,根据布尔类型成员变量 directFieldAccess 选择利用 Property 还是 Field,还具备校验与获取校验结果功能

23.2 使用示例

SimpleTypeConverter

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

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)
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)
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 方法,可以调用 DataBinder 的 initDirectFieldAccess() 方法使数据绑定逻辑走字段赋值,而不是属性赋值:

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。

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 绑定器工厂

现有如下两个类:

@Getter
@Setter
@ToString
public static class User {
private Date birthday;
private Address address;
}

@Getter
@Setter
@ToString
public static class Address {
private String name;
}

在 Web 环境下进行数据绑定:

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);
}

birthday 和 address.name 都能绑定成功吗?

TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=成都))

birthday 绑定失败,要想使其绑定成功,需要自定义转换器,有两种方式:

使用 Spring 提供的 ConversionService

使用 JDK 提供的 PropertyEditorRegistry

创建 DataBinder 的职责交由 DataBinderFactory 完成,以便添加各种选项,拓展不同的自定义转换器。

@SneakyThrows
public static void main(String[] args) {
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 注解标记的方法:

static class MyController {
@InitBinder
public void myMethod(WebDataBinder dataBinder) {
// 拓展 dataBinder 的转换器
dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 进行拓展"));
}
}

以 WebDataBinder 作为方法参数,在方法体内添加自定义转换器 MyDateFormatter:

@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 对象:

@SneakyThrows
public static void main(String[] args) {
// --snip--

InvocableHandlerMethod handlerMethod =
new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("myMethod", WebDataBinder.class));

ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(handlerMethod), null);

// --snip--
}

再次执行 main(),birthday 被成功绑定:

[DEBUG] indi.lcp.a23.MyDateFormatter      - >>>>>> 进入了: 用 @InitBinder 进行拓展 
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))

这种方式使用了 JDK 提供的 PropertyEditorRegistry,证据就在 WebDataBinder 的 addCustomFormatter() 方法中:

public void addCustomFormatter(Formatter<?> formatter) {
FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter);
getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter);
}

ConversionService 拓展

选择 FormattingConversionService 作为 ConversionService 的实现,向其中添加自定义转换器 MyDateFormatter。

构造 DataBinderFactory 时传入 WebBindingInitializer 的实现,因此将 FormattingConversionService 封装成 ConfigurableWebBindingInitializer 传入 DataBinderFactory 的构造方法中:

@SneakyThrows
public static void main(String[] args) {
// --snip--

// 使用 ConversionService 转换
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式拓展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);

// --snip--
}
[DEBUG] indi.lcp.a23.MyDateFormatter - >>>>>> 进入了: 用 ConversionService 方式拓展转换功能
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))

如果同时存在 @InitBinder 和 ConversionService,将以 @InitBinder 为主,@InitBinder 实现的转换器属于自定义转换器,自定义转换器的优先级更高:

@SneakyThrows
public static void main(String[] args) {
// --snip--

FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式拓展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
InvocableHandlerMethod handlerMethod =
new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("myMethod", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(handlerMethod), initializer);

// --snip--
}
[DEBUG] indi.lcp.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))

默认的 ConversionService

ConversionService 有一个默认实现 DefaultFormattingConversionService,它还是 FormattingConversionService 的子类:

@SneakyThrows
public static void main(String[] args) {
// --snip--

DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(conversionService);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);

// --snip--
}

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

TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=成都))

birthday 绑定失败,默认的 ConversionService 需要搭配注解 @DateTimeFormat 使用。在目标类的字段上使用该注解标记,并指定被转换的日期格式:

@Getter
@Setter
@ToString
public static class User {
@DateTimeFormat(pattern = "yyyy|MM|dd")
private Date birthday;
private Address address;
}

再次运行 main() 方法:

TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))

在 SpringBoot 中还提供了 ApplicationConversionService,它也是 FormattingConversionService 的子类,上述代码将 DefaultFormattingConversionService 换成 ApplicationConversionService 也能达到相同效果。

23.4 Spring 的泛型操作技巧

有一基类 BaseDao,接收一个泛型参数:

public class BaseDao<T> {
T findOne() {
return null;
}
}

围绕 BaseDao 有如下五个类:

public class EmployeeDao extends BaseDao {
}

public class Student {
}

public class StudentDao extends BaseDao<Student> {
}

public class Teacher {
}

public class TeacherDao extends BaseDao<Teacher> {
}

尝试获取 BaseDao 子类泛型参数:

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: indi.lcp.a23.sub.BaseDao
TeacherDao type class: class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
EmployeeDao type: class indi.lcp.a23.sub.BaseDao
EmployeeDao type class: class java.lang.Class
class indi.lcp.a23.sub.Teacher
>>>>>>>>>>>>>>>>>>>>>>>
class indi.lcp.a23.sub.Teacher
>>>>>>>>>>>>>>>>>>>>>>>
class indi.lcp.a23.sub.Student

ControllerAdvice 之 @InitBinder

功能与使用

@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 foo() {
}
}
}

当 @InitBinder 作用的方法存在于被 @ControllerAdvice 标记的类里面时,是对 所有 控制器都生效的自定义类型转换器。当 @InitBinder 作用的方法存在于被 @Controller 标记的类里面时,是 只对当前 控制器生效的自定义类型转换器。

@InitBinder 的来源有两个:

  1. @ControllerAdvice 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录
  2. @Controller 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在控制器方法首次执行时解析并记录
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())
);
}

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

indi.lcp.a24.A24                  - 1. 刚开始... 
indi.lcp.a24.A24 - 全局的 @InitBinder 方法 [binder3]
indi.lcp.a24.A24 - 控制器的 @InitBinder 方法 []

全局的 @InitBinder 方法被解析并记录,但控制器中被 @InitBinder 标记的方法并没有被解析记录。

模拟调用控制器方法:

public static void main(String[] args) throws Exception {
// --snip--

log.debug("1. 刚开始...");
showBindMethods(handlerAdapter);

Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
getDataBinderFactory.setAccessible(true);
log.debug("2. 模拟调用 Controller1 的 foo 方法...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo")));
showBindMethods(handlerAdapter);

log.debug("3. 模拟调用 Controller2 的 bar 方法时...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller2(), WebConfig.Controller2.class.getMethod("bar")));
showBindMethods(handlerAdapter);

context.close();
}
1. 刚开始...
全局的 @InitBinder 方法 [binder3]
控制器的 @InitBinder 方法 []
2. 模拟调用 Controller1 的 foo 方法...
全局的 @InitBinder 方法 [binder3]
控制器的 @InitBinder 方法 [Controller1.binder1]
3. 模拟调用 Controller2 的 bar 方法时...
全局的 @InitBinder 方法 [binder3]
控制器的 @InitBinder 方法 [Controller1.binder1, Controller2.binder22, Controller2.binder21]

首次调用控制器中的方法时,控制器中被 @InitBinder 标记方法被解析记录。

控制器方法执行流程

ServletInvocableHandlerMethod 的组成

img

HandlerMethod 需要:

  • bean,即哪个 Controller
  • method,即 Controller 中的哪个方法

ServletInvocableHandlerMethod 需要:

  • WebDataBinderFactory,用于对象绑定、类型转换
  • ParameterNameDiscoverer,用于参数名解析
  • HandlerMethodArgumentResolverComposite,用于解析参数
  • HandlerMethodReturnValueHandlerComposite,用于处理返回值

控制器方法执行流程

以 RequestMappingHandlerAdapter 为起点,创建 WebDataBinderFactory,添加自定义类型转换器,再创建 ModelFactory,添加 Model 数据

img

接下来调用 ServletInvocableHandlerMethod,主要完成三件事:

  • 准备参数
  • 反射调用控制器方法
  • 处理返回值

ControllerAdvice 之 @ModelAttribute

准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置

功能与使用

@ModelAttribute 可以作用在参数上和方法上。

当其作用在参数上时,会将请求中的参数信息 按名称 注入到指定对象中,并将这个对象信息自动添加到 ModelMap 中。当未指定 @ModelAttribute 的 value 时,添加到 ModelMap 中的 key 是对象类型首字母小写对应的字符串。此时的 @ModelAttribute 注解由 ServletModelAttributeMethodProcessor 解析。

当其作用在方法上时:

如果该方法在被 @Controller 注解标记的类中,会在当前控制器中每个控制器方法执行前执行被 @ModelAttribute 标记的方法,如果该方法有返回值,自动将返回值添加到 ModelMap 中。当未指定 @ModelAttribute 的 value 时,添加到 ModelMap 中的 key 是返回值类型首字母小写对应的字符串。

如果该方法在被 @ControllerAdvice 注解标记的类中,会在所有控制器方法执行前执行该方法。

作用在方法上的 @ModelAttribute 注解由 RequestMappingHandlerAdapter 解析。

@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() 方法:

foo
{u=WebConfig.User(name=lcp), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

再解析方法上的 @ModelAttribute 注解:

@SneakyThrows
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);

RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();

MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "lcp");

// --snip--

// 获取模型工厂
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, binderFactory);

// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);

// --snip--
}
foo
{a=aa, u=WebConfig.User(name=lcp), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

{a=aa} 也被放入到 ModelAndViewContainer 中。

返回值处理器

含有多种返回值的控制器

@Slf4j
static class Controller {
public ModelAndView test1() {
log.debug("test1()");
ModelAndView mav = new ModelAndView("view1");
mav.addObject("name", "张三");
return mav;
}

public String test2() {
log.debug("test2()");
return "view2";
}

@ModelAttribute
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}

public User test4() {
log.debug("test4()");
return new User("王五", 30);
}

public HttpEntity<User> test5() {
log.debug("test5()");
return new HttpEntity<>(new User("赵六", 40));
}

public HttpHeaders test6() {
log.debug("test6()");
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/html");
return headers;
}

@ResponseBody
public User test7() {
log.debug("test7()");
return new User("钱七", 50);
}
}

@Getter
@Setter
@ToString
@AllArgsConstructor
public static class User {
private String name;
private int age;
}

测试渲染视图需要用到的配置

为测试对视图的渲染,采用 Freemarker 进行测试,先导入 Freemarker 依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

Freemarker 配置类:

@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;
}
}

渲染视图使用的方法:

@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 对象的方法

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 的响应信息。

@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 中,之后根据视图信息找到对应的模板页面,再将模型数据填充到模板页面中,完成视图的渲染。

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:

<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>view1</title>
</head>
<body>
<h1>Hello! ${name}</h1>
</body>
</html>
indi.lcp.a27.A27$Controller - test1()
{name=张三}
view1
indi.lcp.a27.A27 - >>>>>> 渲染视图
indi.lcp.a27.A27 - 没有获取到视图名, 采用默认视图名: view1
indi.lcp.a27.WebConfig$1$1 - View name 'view1', model {name=张三}
indi.lcp.a27.WebConfig$1$1 - Rendering [/view1.ftl]
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>view1</title>
</head>
<body>
<h1>Hello! 张三</h1>
</body>
</html>

27.2 字符串类型

控制器方法的返回值是字符串类型时,返回的字符串即为视图的名称。与 ModelAndView 类型的返回值相比,不包含模型数据。

此种类型的返回值由 ViewNameMethodReturnValueHandler 处理,构造时无需传入任何参数。

private static void test2(ApplicationContext context) {
testReturnValueProcessor(context, "test2", null, null);
}

对应的模板页面 view2.ftl:

<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>view2</title>
</head>
<body>
<h1>Hello!</h1>
</body>
</html>
indi.lcp.a27.A27$Controller - test2()
{}
view2
indi.lcp.a27.A27 - >>>>>> 渲染视图
indi.lcp.a27.A27 - 没有获取到视图名, 采用默认视图名: view2
indi.lcp.a27.WebConfig$1$1 - View name 'view2', model {}
indi.lcp.a27.WebConfig$1$1 - Rendering [/view2.ftl]
<!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 标记,并指定请求地址,比如:

@ModelAttribute
@RequestMapping("/test3")
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}

当未找到视图名称时,默认以请求路径作为视图名称。

但在本节测试中省略了路径映射这一步,因此需要通过编程的方式将请求路径解析后的结果放入 request 作用域中。

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:

<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>test3</title>
</head>
<body>
<h1>Hello! ${user.name} ${user.age}</h1>
</body>
</html>
indi.lcp.a27.A27$Controller - test3()
{user=A27.User(name=李四, age=20)}
null
indi.lcp.a27.A27 - >>>>>> 渲染视图
indi.lcp.a27.A27 - 没有获取到视图名, 采用默认视图名: test3
indi.lcp.a27.WebConfig$1$1 - View name 'test3', model {user=A27.User(name=李四, age=20)}
indi.lcp.a27.WebConfig$1$1 - Rendering [/test3.ftl]
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>test3</title>
</head>
<body>
<h1>Hello! 李四 20</h1>
</body>
</html>

针对控制器方法 test4() 也可以按照相同方式测试:

private static void test4(ApplicationContext context) {
String methodName = "test4";
testReturnValueProcessor(context, methodName, mockHttpServletRequestConsumer(methodName), null);
}

对应的模板页面 test4.ftl:

<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>test4</title>
</head>
<body>
<h1>Hello! ${user.name} ${user.age}</h1>
</body>
</html>
indi.lcp.a27.A27$Controller - test4()
{user=A27.User(name=王五, age=30)}
null
indi.lcp.a27.A27 - >>>>>> 渲染视图
indi.lcp.a27.A27 - 没有获取到视图名, 采用默认视图名: test4
indi.lcp.a27.WebConfig$1$1 - View name 'test4', model {user=A27.User(name=王五, age=30)}
indi.lcp.a27.WebConfig$1$1 - Rendering [/test4.ftl]
<!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() 方法中得以论证:

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 一进入方法就设置请求处理完毕
mavContainer.setRequestHandled(true);

// --snip--
}

HttpEntity 中包含了状态码、响应体信息和响应头信息。

尝试在请求处理完毕后,输出响应体信息:

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);
}
indi.lcp.a27.A27$Controller - test5()
{}
null
Content-Type = application/json
{"name":"赵六","age":40}

27.5 HttpHeaders

与 HttpEntity 相比,HttpHeaders 只包含响应头信息,HttpHeaders 类型的返回值由 HttpHeadersReturnValueHandler 处理,构造时无需传入任何参数。

与 HttpEntity 一样,这种类型的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。

private static void test6(ApplicationContext context) {
testReturnValueProcessor(context, "test6", null, RESPONSE_CONSUMER);
}
indi.lcp.a27.A27$Controller - test6()
{}
null
Content-Type = text/html

27.6 @ResponseBody

@ResponseBody 标记的方法的返回值由 RequestResponseBodyMethodProcessor 处理,构造时需要传入一个消息转换器列表。

这样的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。

private static void test7(ApplicationContext context) {
testReturnValueProcessor(context, "test7", null, RESPONSE_CONSUMER);
}
indi.lcp.a27.A27$Controller - test7()
{}
null
Content-Type = application/json
{"name":"钱七","age":50}

消息转换器

在构造参数解析器 RequestResponseBodyMethodProcessor、返回值解析器 HttpEntityMethodProcessor 和 HttpEntityMethodProcessor 时,都需要传入消息转换器列表。

消息转换器的基类是 HttpMessageConverter。

介绍两个常见的消息转换器的实现:

  • MappingJackson2XmlHttpMessageConverter
  • MappingJackson2XmlHttpMessageConverter

一个 User 类:

@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 格式的数据:

@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 格式的数据:

@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 时,需要额外导入依赖:

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<User><name>李四</name><age>20</age></User>

将 JSON 格式的数据转换成 User 对象:

@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)

如果存在多个消息转换器呢?

@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 格式的数据:

{"name":"张三","age":18}

调换添加的消息转换器顺序:

@SneakyThrows
public static void test4() {
// --snip--

RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2XmlHttpMessageConverter(),
new MappingJackson2HttpMessageConverter()
));

// --snip--
}

这下会将 User 对象转换成 XML 格式的数据:

<User><name>张三</name><age>18</age></User>

再将添加的消息转换器顺序还原,在请求头中添加 Accept 信息,指定数据格式为 XML:

@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 信息为主:

<User><name>张三</name><age>18</age></User>

在上文基础上,在指定响应的 Content-Type 为 application/json:

@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 的信息为主:

{"name":"张三","age":18}

总结

@ResponseBody 注解由 RequestResponseBodyMethodProcessor 解析,但涉及到的数据格式转换由消息转换器完成。

当存在多个消息转换器时,如果选择 MediaType:

  • 首先看 @RequestMapping 注解的 produces 为主,相当于设置了响应的 Content-Type,比如:
@ResponseBody
@RequestMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
public User user () {
return null;
}
  • 再看请求头中的 Accept 是否指定了目标格式
  • 最后按照消息转换器的添加顺序进行转换

ControllerAdvice 之 ResponseBodyAdvice

ResponseBodyAdvice 是一个接口,对于实现了这个接口并被 @ControllerAdvice 标记的类来说,能够在调用每个控制器方法返回结果前,调用重写的 ResponseBodyAdvice 接口中的 beforeBodyWrite() 方法对返回值进行增强。

现有一个控制器类与内部使用到的 User 类:

@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;
}

调用控制器方法,并输出响应数据:

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));
context.close();
}

运行 main() 方法后,控制台输出

{"name":"王五","age":18}

在实际开发场景中常常需要对返回的数据类型进行统一,比如都返回 Result 类型:

@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 进行增强:

@ControllerAdvice
static class MyControllerAdvice implements ResponseBodyAdvice<Object> {

@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
/*
* 满足条件才转换
* 1. 控制器方法被 @ResponseBody 注解标记
* 2. 控制器方法所在类被 @ResponseBody 注解或包含 @ResponseBody 注解的注解标记
*/
return returnType.getMethodAnnotation(ResponseBody.class) != null
|| AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null;
}

@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 添加到返回值处理器中。

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}}

如果将控制器方法修改成以下形式,也能输出相同的结果:

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

异常处理

DispatcherServlet 中对异常处理的核心方法是 processHandlerException(),在这个方法中会对所有异常解析器进行遍历,然后使用每个异常解析器对异常信息进行处理。

存放异常解析器的是 DispatcherServlet 中泛型为 HandlerExceptionResolver、名为 handlerExceptionResolvers 的列表成员变量。

HandlerExceptionResolver 是一个接口,本节讲解解析 @ExceptionHandler 注解的异常解析器 ExceptionHandlerExceptionResolver。

四个控制器类,测试异常处理方法被 @ResponseBody 注解标记、异常处理方法返回 ModelAndView、嵌套异常和对异常处理方法的参数处理:

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());
}
}
@SneakyThrows
public static void main(String[] args) {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
// 调用该方法,添加默认的参数解析器和返回值处理器
resolver.afterPropertiesSet();

MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception e = new ArithmeticException("除以零");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));

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());

// 嵌套异常
handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));

// 异常处理方法参数处理
handlerMethod = new HandlerMethod(new Controller4(), Controller4.class.getMethod("foo"));
e = new Exception("e4");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

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

{"error":"除以零"}
{error=除以零}
test2
{"error":"除以零"}{"error":"e3"}
org.springframework.mock.web.MockHttpServletRequest@7c1e2a9e
{"error":"除以零"}{"error":"e3"}{"error":"e4"}

ControllerAdvice 之 @ExceptionHandler

控制器中被 @ExceptionHandler 标记的异常处理方法只会在当前控制器中生效,如果想要某个异常处理方法全局生效,则需要将异常处理方法编写在被 @ControllerAdvice 注解标记的类中。

一个“朴素”的控制器类:

static class Controller1 {
public void foo() {
}
}

当不存在任何异常处理方法时,调用控制器中的 foo() 方法:

@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,并声明全局异常处理方法:

@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 容器中获取:

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

Tomcat 异常处理

可以利用 @ExceptionHandler 和 @ControllerAdvice 注解全局对控制器方法中抛出的异常进行处理,但针对诸如 filter 中不在控制器方法中的异常就变得无能为力了。

因此需要一个更上层的“异常处理者”,这个“异常处理者”就是 Tomcat 服务器。

32.1 Tomcat 的错误页处理

首先将“老三样”利用配置类添加到 Spring 容器中,还要将 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 也添加到 Spring 容器中。

必要的控制器也不能少,控制器方法手动制造异常,但不提供使用 @ExceptionHandler 实现的异常处理方法,将产生的异常交由 Tomcat 处理:

@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 容器,并输出所有的路径映射信息:

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));
}

运行 main() 方法后,控制台只输出一条路径映射信息:

映射路径: { [/test]}        方法信息: indi.lcp.a32.WebConfig$MyController#test()

在浏览器中访问 http://localhost:8080/test 地址:

img

显示 Tomcat 的错误处理页,并在页面中输出了错误信息。

Tomcat 默认提供的错误处理方式返回的是 HTML 格式的数据,但需要返回 JSON 格式的数据又该怎么自定义呢?

修改 Tomcat 默认的错误处理路径,并添加后置处理器进行注册:

/**
* 修改了 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 格式的数据:

@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]}        方法信息: indi.lcp.a32.WebConfig$MyController#error(HttpServletRequest)
映射路径: { [/test]} 方法信息: indi.lcp.a32.WebConfig$MyController#test()

在浏览器中访问 http://localhost:8080/test

img

32.2 BasicErrorController

BasicErrorController 是由 SpringBoot 提供的类,它也是一个控制器:

@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// --snip--
}

它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /error。

向容器中添加 BasicErrorController,构造 BasicErrorController 时需要传递两个参数:

  • errorAttributes:错误属性,可以理解成封装的错误信息对象
  • errorProperties:也可以翻译成错误属性,用于对输出的错误信息进行配置
@Bean
public BasicErrorController basicErrorController() {
return new BasicErrorController(new DefaultErrorAttributes(), new ErrorProperties());
}

移除前文添加的 error() 控制器方法。

再次重启程序,控制台输出的路径映射信息为:

映射路径: { [/error]}        方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
映射路径: { [/test]} 方法信息: indi.lcp.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 格式的数据,比如:

{
"timestamp": 1674736682248,
"status": 500,
"error": "Internal Server Error",
"path": "/test"
}

timestamp、status 等响应内容就是错误属性 errorAttributes 的中包含的内容。

返回的数据中并没有显示异常信息,可以通过配置文件进行配置:

server.error.include-exception=true

也可以在添加 BasicErrorController 到 Spring 容器中时,设置错误属性 errorProperties:

@Bean
public BasicErrorController basicErrorController() {
ErrorProperties errorProperties = new ErrorProperties();
errorProperties.setIncludeException(true);
return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}

重启程序,再次使用接口测试工具访问 http://localhost:8080/test:

{
"timestamp": 1674736991768,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.ArithmeticException",
"path": "/test"
}

使用浏览器访问

如果使用浏览器访问 http://localhost:8080/test,又会回到“解放前”,显示与 Tomcat 的默认错误处理页相同的内容。

这是因为使用浏览器访问时,将调用 BasicErrorController 中的 errorHtml() 控制器方法:

@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:

@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 的名称进行匹配,还需要添加一个解析器:

@Bean
public ViewResolver viewResolver() {
// View 类型的 Bean 的名称即为视图名称
return new BeanNameViewResolver();
}

重启程序,使用浏览器访问 http://localhost:8080/test:

img

控制台还打印出:

{timestamp=Thu Jan 26 21:01:50 CST 2023, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, path=/test}

BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter

33.1 功能与使用

BeanNameUrlHandlerMapping 与 RequestMappingHandlerMapping 类似,也是用于解析请求路径,只不过 BeanNameUrlHandlerMapping 将根据请求路径在 Spring 容器中寻找同名的 Bean,对请求进行处理,这个 Bean 必须 以 / 开头。比如:请求路径为 /c1,寻找的 Bean 的名称也是 /c1。

SimpleControllerHandlerAdapter 与 RequestMappingHandlerAdapter 也类似,也是用于调用控制器方法,但要求控制器类必须实现 org.springframework.web.servlet.mvc.Controller 接口。

现有三个控制器类:

@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:

@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 BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
return new BeanNameUrlHandlerMapping();
}

@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
}
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);

}

运行 main() 方法后,在浏览器中访问 http://localhost:8080/c1,页面上显示 this is c1。更换请求路径为 c2、c3 后,也会出现类似的信息。

33.2 自定义实现

在配置类 WebConfig 中移除 Spring 提供的 BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter,手动编码实现它们的功能。

为了与前文的测试形成对比,将 Controller2 的 Bean 名称设置为 c2,而不是 /c2,使其不能被解析到。

@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。

RouterFunctionMapping 与 HandlerFunctionAdapter

RouterFunctionMapping 在初始化时,在 Spring 容器中收集所有 RouterFunction,RouterFunction 包括两部分:

  1. RequestPredicate:设置映射条件
  2. HandlerFunction:处理逻辑

当请求到达时,根据映射条件找到 HandlerFunction,即 handler,然后使用 HandlerFunctionAdapter 调用 handler。

@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"));
}
}
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}

运行 main() 方法后,在浏览器中访问 http://localhost:8080/r1,页面上显示 this is r1,访问 r2 时也类似。

SimpleUrlHandlerMapping 与HttpRequestHandlerAdapter

35.1 功能与使用

概括一下,这两个主要用于静态资源处理,SimpleUrlHandlerMapping 用于静态资源映射,而静态资源处理器是 ResourceHttpRequestHandler,HttpRequestHandlerAdapter 用于处理器。

@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():

@Override
public void afterPropertiesSet() throws Exception {
resolveResourceLocations();

if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}
// --snip--
}

当使用的资源解析器列表为空时,默认添加最基本的资源解析器 PathResourceResolver。

尝试添加额外的资源解析器:

@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
// 以 / 结尾表示目录,否则认为是文件
handler.setLocations(Collections.singletonList(new ClassPathResource("static/")));
// 不使用默认的资源解析器,而是使用自行添加的
handler.setResourceResolvers(Arrays.asList(
// 读取资源时使用缓存
new CachingResourceResolver(new ConcurrentMapCache("cache1")),
// 读取压缩资源
new EncodedResourceResolver(),
// 最基本的:从磁盘上读取静态资源
new PathResourceResolver()
));
return handler;
}

添加了三个资源解析器:

  • CachingResourceResolver:对静态资源进行缓存
  • EncodedResourceResolver:对静态资源进行压缩
  • PathResourceResolver:最基本的资源处理器

还要注意添加的顺序,先尝试从缓存中获取,再尝试获取压缩文件,最后才是直接从磁盘上读取。

针对 EncodedResourceResolver 来说,Spring 不会自行对静态资源进行压缩,需要在配置类中提供压缩方法:

@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 文件:

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
Resource resource = context.getResource("classpath:static/index.html");
return new WelcomePageHandlerMapping(
null,
context,
resource,
"/**"
);
}

程序会根据配置的欢迎页映射器生成一个实现了 Controller 接口的处理器,使用 SimpleControllerHandlerAdapter 执行生成的处理器:

@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 时,会直接访问静态资源 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() 方法是 责任链 模式体现。

MVC 处理流程

当浏览器发送一个请求 http://localhost:8080/hello 后,请求到达服务器,其处理流程是:

服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术

路径:默认映射路径为 /,即会匹配到所有请求 URL,可作为请求的统一入口,DispatcherServlet 也被称之为 前控制器。但也有例外:

JSP 不会匹配到 DispatcherServlet

其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet

创建:在 SpringBoot 中,由自动配置类 DispatcherServletAutoConfiguration 提供 DispatcherServlet 的 Bean

初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量

HandlerMapping,初始化时记录映射关系

HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器

HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器

ViewResolver

DispatcherServlet 利用 RequestMappingHandlerMapping 查找控制器方法

例如根据 /hello 路径找到被 @RequestMapping(“/hello”) 标记的控制器方法,控制器方法会被封装成 HandlerMethod 对象,并结合 匹配到的拦截器 一起返回给 DispatcherServlet。

HandlerMethod 和 拦截器 合称为 HandlerExecutionChain(调用链)对象。

DispatcherServlet 接下来会

调用拦截器的 preHandle() 方法,返回一个布尔类型的值。若返回 true,则放行,进行后续调用,反之拦截请求,不进行后续调用;

RequestMappingHandlerAdapter 调用处理器方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod

@ControllerAdvice 全局增强点 1️⃣:利用 @ModelAttribute 补充模型数据

@ControllerAdvice 全局增强点 2️⃣:利用 @InitBinder 补充自定义类型转换器

使用 HandlerMethodArgumentResolver 准备参数

@ControllerAdvice 全局增强点 3️⃣:利用 RequestBodyAdvice 接口对请求体增强

调用 ServletInvocableHandlerMethod

使用 HandlerMethodReturnValueHandler 处理返回值

@ControllerAdvice 全局增强点 4️⃣:利用 RequestBodyAdvice 对响应体增强

根据 ModelAndViewContainer 获取 ModelAndView

如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程。例如返回值处理器调用了 HttpMessageConverter 将结果转换为 JSON,这时 ModelAndView 就为 null

如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程

调用拦截器的 postHandle() 方法

处理异常或视图渲染

如果 1~3 步中出现异常,使用 ExceptionHandlerExceptionResolver 处理异常流程

@ControllerAdvice 全局增强点 5️⃣:利用 @ExceptionHandler 进行统一异常处理

未出现异常时,进行视图解析及渲染流程

调用拦截器的 afterCompletion() 方法

Boot 骨架项目

使用 IDEA 创建 SpringBoot 项目时,会创建出 .mvn 目录、HELP.md、mvnw 和 mvnw.cmd 等不必要的文件。

如果是 Linux 环境下,执行以下命令获取 SpringBoot 的骨架,并添加 web、mysql、mybatis 依赖:

curl -G https://start.spring.io/pom.xml -d dependencies=web,mysql,mybatis -o pom.xml

也可以使用 Postman 等接口测试工具来实现。

更多用法执行以下命令进行参考:

curl https://start.spring.io

Boot War 项目

38.1 项目的构建

利用 IDEA 创建新模块 test_war,区别在于选择的打包方式是 War:

img

选择依赖时,勾选 Spring Web。

一般来说,选择 War 作为打包方式都是为了使用 JSP,因为 JSP 不能配合 Jar 打包方式使用。

JSP 文件的存放路径是固定的,在 src/main 目录下的 webapp 目录,如果没有 webapp 目录,需要自行创建。之后新建 hello.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>Hello!</h3>
</body>
</html>

之后新建控制器类 HelloController,编写控制器方法 hello(),返回值类型是 String,要求返回的是视图名称:

@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}

最后要在配置文件中配置视图的前缀、后缀,使控制器方法返回的视图名称对应视图名称的 JSP 页面:

spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

38.2 项目的测试

使用外置 Tomcat 测试

首先得安装外置 Tomcat,省略安装步骤。

然后在 IDEA 的 Run/Debug Configurations 中进行配置,选择安装的外置 Tomcat:

img

然后在 Deployment 中指定当前项目的部署方式和应用程序上下文路径:

img

尽管使用外置 Tomcat 进行测试,但主启动类不能少:

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

除此之外,还要编写 ServletInitializer,在外置 Tomcat 启动时,找到 SpringBoot 项目的主启动类,执行 SpringBoot 流程:

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 的能力,需要添加依赖:

<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

Boot 启动过程

39.1 SpringApplication 的构造

SpringBoot 的主启动类类似于:

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

其中 SpringApplication#run() 方法是核心方法:

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() 方法。

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 对象时做了如下几件事:

获取 Bean Definition 源

推断应用类型

添加 ApplicationContext 初始化器

添加事件监听器

主类推断

获取 Bean Definition 源

@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。向容器中添加即可:

@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 来源: indi.lcp.a39.A39_1
name: servletWebServerFactory 来源: indi.lcp.a39.A39_1

来源为 null 的 Bean 是由 Spring 提供的“内置” Bean。

使用 XML 配置文件添加 Bean,并利用 setSources() 方法设置创建 ApplicationContext 的其他源:

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]

推断应用类型

应用类型的推断在构造方法中可以看到:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 推断应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// --snip--
}

推断逻辑由 WebApplicationType 枚举中的 deduceFromClasspath() 方法完成:

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() 方法:

@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,最后调用 ApplicationContext 的 refresh() 方法完成初始化。

在创建与初始化完成之间的一些拓展功能就由 ApplicationContext 初始化器完成。

在 SpringApplication 的构造方法中,添加的初始化器信息从配置文件中读取:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 从配置文件中读取初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// --snip--
}

也可以调用 SpringApplication 对象的 addInitializers() 方法添加自定义初始化器:

@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 的构造方法中,添加的事件监听器信息从配置文件中读取:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 从配置文件中读取事件监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// --snip--
}

可以调用 SpringApplication 对象的 addListeners() 方法添加自定义事件监听器:

@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

主类推断

主类推断在构造方法中可以看到:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// --snip--
// 主类推断
this.mainApplicationClass = deduceMainApplicationClass();
}

推断逻辑由 deduceMainApplicationClass() 方法完成,利用反射调用该方法:

@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 indi.lcp.a39.A39_1

39.2 SpringApplication#run() 的分析

第一步:获取 SpringApplicationRunListeners

在执行 run() 方法时,首先会获取到 SpringApplicationRunListeners,它是事件发布器的组合,能够在 SpringBoot 启动的各个阶段中发布事件。

SpringApplicationRunListeners 中使用 SpringApplicationRunListener 来描述单个事件发布器,SpringApplicationRunListener 是一个接口,它有且仅有一个实现类 EventPublishingRunListener。

在 SpringBoot 中,事件发布器都是在配置文件中读取,从 META-INF/spring.factories 中读取,该文件中有这样一句:

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

自行实现从 META-INF/spring.factories 配置文件中读取事件发布器信息,并发布各种事件:

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 个。

第八到十一步:完成 Spring 容器的创建

第八步:创建容器。在构造 SpringApplication 时已经推断出应用的类型,使用应用类型直接创建即可。

第九步:准备容器。回调在构造 SpringApplication 时添加的初始化器。

第十步:加载 Bean 定义。从配置类、XML 配置文件读取 BeanDefinition,或者扫描某一包路径下的 BeanDefinition。

第十一步:调用 ApplicationContext 的 refresh() 方法,完成 Spring 容器的创建。

@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("indi.lcp.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;
}

涉及到的配置类:

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 配置文件:

<?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="indi.lcp.a39.A39_3.Bean4"/>

</beans>

indi.lcp.a39.sub 包下的 Bean 信息:

package indi.lcp.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\indi\lcp\a39\sub\Bean7.class]
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean5 来源: indi.lcp.a39.A39_3$Config
name: servletWebServerFactory 来源: indi.lcp.a39.A39_3$Config

第二步:封装启动 args

调用 DefaultApplicationArguments 的构造方法,传入 args 即可:

@SneakyThrows
@SuppressWarnings("all")
public static void main(String[] args) {
// --snip--
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args");
DefaultApplicationArguments arguments = new DefaultApplicationArguments(args);
// --snip--
}

第十二步:执行 Runner

在 SpringBoot 启动成功后,可以执行一些 Runner,进行一些预处理或测试。Runner 有两种,分别是 CommandLineRunner 和 ApplicationRunner:

@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}

@FunctionalInterface
public interface ApplicationRunner {
void run(ApplicationArguments args) throws Exception;
}

它们都是函数式接口,内部的抽象方法长得也很像,只不过:

CommandLineRunner 直接接收启动参数;

ApplicationRunner 则是接收封装后的 ApplicationArguments,即 第二步 封装的对象。

在配置类中添加这两种类型的 Bean:

@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:

@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:

img

>>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner
commandLineRunner()...[--server.port=8080, debug]
applicationRunner()...[--server.port=8080, debug]
[server.port]
[8080]
[debug]

第三步:准备 Environment 添加命令行参数

Environment 即环境对象,是对配置信息的抽象,配置信息的来源有多种,比如:系统环境变量、properties 配置文件、YAML 配置文件等等。

SpringBoot 提供了名为 ApplicationEnvironment 的类表示环境对象,它是 Spring 中 StandardEnvironment 环境对象的子类。

img

默认情况下,创建的 ApplicationEnvironment 对象中配置信息的来源只有两个:

  • 系统属性
  • 系统变量
package org.springframework.boot;

/**
* @author lcp
* @date 2023/1/28 12:12
*/
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 的配置信息:

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 的配置信息:

img

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

abc

如果想从配置文件 application.properties 中读取配置信息,可以添加配置信息的来源。配置文件的优先级最低,添加来源时调用 addLast() 方法:

@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"));
}
author.name="lcp"
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
"lcp"

而在 SpringBoot 中,这里 只 添加 SimpleCommandLinePropertySource,并且它的优先级最高,使用 addFirst() 方法添加:

@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=默烦:

img

SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
默烦

第四步:添加 ConfigurationPropertySources

有一 step4.properties 文件,其内容如下:

user.first-name=George
user.middle_name=Walker
user.lastName=Bush

尝试读取文件中的内容:

@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:

@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。

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:/''}
"lcp"

EnvironmentPostProcessor 还有一个有趣的实现:RandomValuePropertySourceEnvironmentPostProcessor,该实现提供了随机值的生成。

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 配置文件中读取并初始化后置处理器:

# 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 配置文件中读取并初始化的:

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

要想该监听器成功监听到事件,需要在第五步中发布一个事件,而事件的发布由第一步获取的事件发布器完成:

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 实现的。

@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 配置文件的信息如下:

spring.main.banner-mode=off
spring.main.lazy-initialization=true

绑定 spring.main 开头的配置信息到 SpringApplication 对象中:

@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

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:

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:

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 的版本号可以使用:

System.out.println("SpringBoot: " + SpringBootVersion.getVersion());
System.out.println("Spring: " + SpringVersion.getVersion());

步骤总结

  1. 得到 SpringApplicationRunListeners 事件发布器
  • 发布 Application Starting 事件
  1. 封装启动 args
  2. 准备 Environment 添加命令行参数
  3. ConfigurationPropertySources 处理
  • 发布 Application Environment 已准备事件
  1. 通过 EnvironmentPostProcessorApplicationListener 进行 env 后处理
  • application.properties 由 StandardConfigDataLocationResolver 解析
  • spring.application.json
  1. 绑定 spring.main 到 SpringApplication 对象
  2. 打印 Banner
  3. 创建容器
  4. 准备容器
  • 发布 Application Context 已初始化事件
  1. 加载 Bean 定义
  • 发布 Application Prepared 事件
  1. refresh 容器
  • 发布 Application Started 事件
  1. 执行 Runner
  • 发布 Application Ready 事件
  • 这其中有异常,发布 Application Failed 事件

Tomcat 内嵌容器

Tomcat 基本结构:

Server
└───Service
├───Connector (协议, 端口)
└───Engine
└───Host(虚拟主机 localhost)
├───Context1 (应用 1, 可以设置虚拟路径, / 即 url 起始路径; 项目磁盘路径, 即 docBase)
│ │ index.html
│ └───WEB-INF
│ │ web.xml (servlet, filter, listener) 3.0
│ ├───classes (servlet, controller, service ...)
│ ├───jsp
│ └───lib (第三方 jar 包)
└───Context2 (应用 2)
│ index.html
└───WEB-INF
web.xml

40.1 内嵌 Tomcat 的使用

内嵌 Tomcat 的使用分为 6 步:

  1. 创建 Tomcat
  2. 创建项目文件夹,即 docBase 文件夹
  3. 创建 Tomcat 项目,在 Tomcat 中称为 Context
  4. 编程添加 Servlet
  5. 启动 Tomcat
  6. 创建连接器,设置监听端口
@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);
}

自行实现的 Servlet 需要继承 HttpServlet,并重写 doGet() 方法:

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 整合

首先肯定需要一个 Spring 容器,选择不支持内嵌 Tomcat 的 Spring 容器,使其使用前文中的 Tomcat:

public static WebApplicationContext getApplicationContext() {
// 使用不支持内嵌 Tomcat 的 Spring 容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(Config.class);
context.refresh();
return context;
}

容器中注册了 Config Bean:

@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:

@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 并执行 `` 方法,

@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.1 自动配置类原理

有以下四个类:

/**
* 模拟第三方配置类
*/
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 {

}


其中 AutoConfiguration1 和 AutoConfiguration2 用来模拟第三方配置类,注意它们并没有被 @Configuration 注解标记,因此在未进行其他操作时,不会被添加到 Spring 容器中。

然后编写自己的配置类,使用 @Import 注解将第三方配置类添加到 Spring 容器中:

@Configuration
@Import({AutoConfiguration1.class, AutoConfiguration2.class})
static class Config {
}
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
indi.lcp.a41.A41$AutoConfiguration1
bean1
indi.lcp.a41.A41$AutoConfiguration2
bean2

如果有多个第三方配置类,难不成到一个个地导入?

可以使用导入选择器 ImportSelector,重写 selectImports() 方法,返回需要自动装配的 Bean 的全限定类名数组:

@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:

# 内部类作为 key 时,最后以 $ 符号分割
indi.lcp.a41.A41$MyImportSelector=\
indi.lcp.a41.A41.AutoConfiguration1, \
indi.lcp.a41.A41.AutoConfiguration2

修改 selectImports() 方法实现逻辑:

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 使用如下语句加载:

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 的全限定类名,可以使用 # 作为注释。其加载方式使用:

ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader());

其中 AutoConfiguration 是一个注解,它的全限定类名为 org.springframework.boot.autoconfigure.AutoConfiguration。

也就是说可以自定义一个注解,创建 META-INF/spring/full-qualified-annotation-name.imports 文件,在文件里声明需要自动装配的类

package indi.lcp.a41;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutoConfiguration {
}
package indi.lcp.a41;

public class A41 {
static class Bean3 {
}
}

创建 META-INF/spring/indi.lcp.a41.MyAutoConfiguration.imports 文件:

indi.lcp.a41.A41$Bean3

修改 selectImports() 方法实现逻辑:

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 多了 一个:

indi.lcp.a41.A41$Bean3

定义了冲突的 Bean

第三方装配了 Bean1:

static class AutoConfiguration1 {
@Bean
public Bean1 bean1() {
return new Bean1("第三方");
}
}

用户又自行定义了 Bean1:

@Configuration
@Import(MyImportSelector.class)
static class Config {
@Bean
public Bean1 bean1() {
return new Bean1("本项目");
}
}

修改测试的 main() 方法:

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 的设置:

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 indi.lcp.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 indi.lcp.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=indi.lcp.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [indi/lcp/a41/A41$AutoConfiguration1.class]] bound.

那这样是合理的吗?

显然不是。比如 SpringBoot 默认的数据连接池是 Hikari,如果用户想换成 Druid,岂不是做不到?

实际情况下是能做到的,这又是怎么做到的呢?

首先需要使用户的配置类中定义的 Bean 先于 @Import 导入的 Bean 添加到 Spring 容器中,只需将选择器 MyImportSelector 实现的 ImportSelector 接口更换成其子接口 DeferredImportSelector 即可

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 [indi/lcp/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=indi.lcp.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [indi/lcp/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 indi.lcp.a41.A41$Config] bound.

尽管还是出现了异常,但异常信息中显示的是在配置类定义的 Bean 已存在,第三方装配的 Bean 无法再添加,这表明 Bean 的添加顺序修改成功。

最后在第三方定义的 Bean 上添加 @ConditionalOnMissingBean 注解,表示容器中存在同名的 Bean 时忽略该 Bean 的添加:

static class AutoConfiguration1 {
@Bean
@ConditionalOnMissingBean
public Bean1 bean1() {
return new Bean1("第三方");
}
}

再次运行 main() 方法,不再出现异常:

>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)

41.2 Aop 自动配置

确保当前模块下已导入:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

使用 AopAutoConfiguration 自动装配与 AOP 相关的 Bean:

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
indi.lcp.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

以 indi.lcp.a41.TestAopAuto$Config 为分割线,上方是添加的一些后置处理器,下方就是 AOP 自动装配添加的 Bean。

在配置类 AopAutoConfiguration 中,使用注解判断配置类是否生效。首先是最外层的 AopAutoConfiguration:

@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:

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
indi.lcp.a41.TestAopAuto$Config

如果 spring.aop.auto 的值是 true,又会成功添加上 AOP 自动装配的 Bean。

再看 AopAutoConfiguration 的内部类:

@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--
}

其内部存在两个类:AspectJAutoProxyingConfiguration 和 ClassProxyingConfiguration。

使用了 @ConditionalOnClass 注解判断 Advice.class 存在时,AspectJAutoProxyingConfiguration 生效;使用 @ConditionalOnMissingClass 注解判断 org.aspectj.weaver.Advice 不存在时,ClassProxyingConfiguration 生效。

由于先前导入了 spring-boot-starter-aop 依赖,Advice.class 是存在的,AspectJAutoProxyingConfiguration 将生效。

AspectJAutoProxyingConfiguration 内部又有两个配置类:

@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 注解:

@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:

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
// --snip--
}
}

AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary() 方法是注册 Bean 的主要逻辑:

@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:

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:

public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
this.registerBeanDefinitions(importingClassMetadata, registry);
}

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}

该接口需要搭配 @Import 注解使用。

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", "lcp")
.addPropertyValue("age", 20)
.getBeanDefinition();
// 注册构建好的 BeanDefinition
registry.registerBeanDefinition("user", beanDefinition);
}
}

@Setter
@ToString
static class User {
private String name;
private int age;
}

注意: 使用时一定要确保 Spring 容器中存在 ConfigurationClassPostProcessor 类型的 Bean。

除此之外,使用 BeanDefinitionRegistryPostProcessor 接口也能拿到 BeanDefinitionRegistry:

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}

41.3 数据库相关的自动配置

确保当前模块下已导入:

<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 注解进行导入:

@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 信息:

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,有两个关键的内部类 EmbeddedDatabaseConfiguration 和 PooledDataSourceConfiguration:

@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 生效:

@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 添加到容器中:

@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
// --snip-=
}

在 DataSourceProperties 中会绑定配置文件中以 spring.datasource 为前缀的配置:

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
// --snip--
}

获取 DataSourceProperties 类型的 Bean,并打印其 url、username 和 password:

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 自动配置

@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 生效的条件有两个:

类路径下存在 SqlSessionFactory 和 SqlSessionFactoryBean

Spring 容器中有且仅有一个 DataSource 类型的 Bean

它还添加了 MybatisProperties 类型的 Bean 到 Spring 容器中,并与配置文件中以 mybatis 为前缀的信息绑定。

@AutoConfigureAfter 注解指定了当前自动配置类在 DataSourceAutoConfiguration 和 MybatisLanguageDriverAutoConfiguration 两个自动配置类解析完成之后再解析。

接下来遇到 sqlSessionFactory() 方法:

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// --snip--
}

依赖 Spring 容器中的 DataSource,当容器中不存在 SqlSessionFactory 时,将其添加到 Spring 容器中。

然后是 sqlSessionTemplate() 方法,它与添加 SqlSessionFactory 到 Spring 容器的逻辑一样:

@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
// --snip--
}

SqlSessionTemplate 也是 SqlSession 的实现,提供了与当前线程绑定的 SqlSession。针对多个方法调用,如果它们来自同一个线程,那么获取到的 SqlSession 对象是同一个。这也是为什么有了 DefaultSqlSession 作为 SqlSession 的实现了,还需要 SqlSessionTemplate。

在 MyBatis 中,使用 MapperFactoryBean 将接口转换为对象,其核心是 getObject() 方法:

public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}

方法中获取了 sqlSession 对象,而获取的就是 SqlSessionTemplate 对象:

public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}

最后来到 MapperScannerRegistrarNotFoundConfiguration 内部类:

@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
// --snip--
}

利用 @ConditionalOnMissingBean 判断 Spring 容器中缺失 MapperFactoryBean 和 MapperScannerConfigurer 时,该配置类生效。生效时利用 @Import 导入 AutoConfiguredMapperScannerRegistrar:

public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
// --snip--
}

AutoConfiguredMapperScannerRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,允许通过编程的方式加 Bean 添加到 Spring 容器中,而这里是去扫描 Mapper 接口,将其转换为对象添加到 Spring 容器中。

在 main() 所在类的包路径下创建 mapper 包,并新建三个接口,其中两个被 @Mapper 注解标记:

@Mapper
public interface Mapper1 {
}

@Mapper
public interface Mapper2 {
}

public interface Mapper3 {
}

运行 main() 方法,查看 Mapper1 和 Mapper2 是否被添加到 Spring 容器中。

结果是否定的。因为 没有设置要扫描的包路径 。

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--
}
当前包名: indi.lcp.a41
mapper1 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\lcp\a41\mapper\Mapper1.class]
mapper2 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\lcp\a41\mapper\Mapper2.class]

@MapperScan 注解与 MybatisAutoConfiguration 在功能上很类似,只不过:

@MapperScan 可以指定具体的扫描路径,未指定时会把引导类范围内的所有接口当做 Mapper 接口;

MybatisAutoConfiguration 关注所有被 @Mapper 注解标记的接口,忽略未被 @Mapper 标记的接口。

事务自动配置

事务自动配置与 DataSourceTransactionManagerAutoConfiguration、TransactionAutoConfiguration 有关。

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 的信息、来源:

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 自动装配时添加自定义组件分为两步:

在类路径下自定义 META-INF/spring.factories 文件,以 org.springframework.boot.autoconfigure.EnableAutoConfiguration 为 key,设置需要自动装配的自定义组件的全限定类名为 value

编写配置类,在配置类上使用 @EnableAutoConfiguration 注解,并将其添加到 Spring 容器中

在实际项目开发中,省略第二步,SpringBoot 的会自动扫描。

SpringBoot 2.7.0 及其以后版本

在类路径下自定义 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,文件中 每一行 表示需要进行自动装配的类的全限定类名,因此不能随意换行。

在这个文件中,以 # 开头的行表示注释。

条件装配底层

42.1 @Conditional

在 SpringBoot 的自动配置中,经常看到 @Conditional 注解的使用,使用该注解可以按条件加载配置类。

@Conditional 注解并不具备条件判断功能,而是通过指定的 Class 列表来进行判断,指定的 Class 需要实现 Condition 接口。

假设有这样一个需求:通过判断类路径下是否存在 com.alibaba.druid.pool.DruidDataSource 类来加载不同的配置类,当存在 DruidDataSource 时,加载 AutoConfiguration1,反之加载 AutoConfiguration2。

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
indi.lcp.a42.A42$AutoConfiguration1
bean1

导入 druid 依赖:

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

再次运行 main() 方法:

config
org.springframework.context.annotation.ConfigurationClassPostProcessor
indi.lcp.a42.A42$AutoConfiguration1
bean1

42.2 @ConditionalOnXxx

在 SpringBoot 的自动配置中,经常看到 @ConditionalOnXxx 注解的使用,这种注解是将某个 @Conditional 的判断进行了封装,比如 ConditionalOnClass 就是用于判断某个 Class 是否存在。

因此针对上文中的代码可以做出修改:

自定义 @ConditionalOnClass 注解,填入需要判断的全限定类名和判断条件;

移除模拟的第三方配置上的 @Conditional 注解,而是使用自定义的 @ConditionalOnClass;

Condition 接口的使用类重写的 matches() 方法利用 @ConditionalOnClass 注解进行条件判断。

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】一样。

FactoryBean

FactoryBean 是一个接口,可以实现该接口,并指定一个泛型,在重写的方法指定泛型类型对象的创建,然后将实现类交由 Spring 管理,最后 Spring 容器中会增加泛型类型的 Bean。这个 Bean 并不是完全受 Spring 管理,或者说部分受 Spring 管理。

为什么这么说呢?

首先定义一个 Bean2,交由 Spring 管理,但它不是重点:

@Component
public class Bean2 {
}

然后定义 Bean1,它未交由 Spring 管理,但是在其内部注入了 Bean2、定义初始化方法、实现 Aware 接口:

@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:

@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。

@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() 方法后,控制台打印出:

indi.lcp.a43.Bean1FactoryBean     - create bean: indi.lcp.a43.Bean1@2667f029 
indi.lcp.a43.Bean1@2667f029

Bean1 类型的 Bean 被成功添加到 Spring 容器中,但根据打印的日志信息可以看出这个 Bean 没有经历依赖注入阶段、没有回调 Aware 接口、没有经历初始化阶段,其创建是由重写的 getObject() 方法完成的。

这个 Bean 就真的没有经历 Spring Bean 的生命周期中的任何阶段吗?

定义 Bean1PostProcessor,实现 BeanPostProcessor 接口,在 bean1 初始化前后打印日志信息:

@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() 方法后,控制台打印出:

indi.lcp.a43.Bean1FactoryBean     - create bean: indi.lcp.a43.Bean1@6a28ffa4 
indi.lcp.a43.Bean1PostProcessor - after [bean1] init
indi.lcp.a43.Bean1@6a28ffa4

bean1 进行了初始化后的增强逻辑,但未进行初始化前的增强逻辑。

创建代理对象的时机就是在初始化后,因此由 FactoryBean 创建的 Bean 可以进行代理增强 。

FactoryBean 接口

FactoryBean 接口中有三个可以被重写的方法:

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 不会存放在 DefaultSingletonBeanRegistry 的 singletonFactories 中,而是在 AbstractAutowireCapableBeanFactory 的 factoryBeanInstanceCache 中。

获取 FactoryBean 类型的 Bean

肯定不能简单地通过名称获取,那会返回其泛型参数类型的 Bean,那通过类型获取呢?比如:

context.getBean(Bean1FactoryBean.class)

答案是可行的。

除此之外,还可以在名称前添加 &,然后通过名称来获取(有点指针的味道?):

context.getBean("&bean1")

@Indexed

Spring 在进行组件扫描时,会遍历项目中依赖的所有 Jar 包中类路径下所有的文件,找到被 @Component 及其衍生注解标记的类,然后把它们组装成 BeanDefinition 添加到 Spring 容器中。

如果扫描的返回过大,势必会大大地影响项目启动速度。

为了优化扫描速度,引入以下依赖,Spring 将扫描过程提前到编译期:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<optional>true</optional>
</dependency>

现有如下类信息:

@Component
public class Bean1 {
}

@Component
public class Bean2 {
}

@Component
public class Bean3 {
}

这几个类都与 A44 存放于同一包路径下:

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

这没什么奇怪的,bean1、bean2 和 bean3 都被添加到 Spring 容器中。

在编译生成的 target 目录下的 classes/META-INF/spring.components 文件里有以下信息

indi.lcp.a44.Bean1=org.springframework.stereotype.Component
indi.lcp.a44.Bean2=org.springframework.stereotype.Component
indi.lcp.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 注解中:

@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 信息。

代理进一步理解

在 Spring 的代理中,依赖注入和初始化针对的是目标对象,代理对象和目标对象是两个对象,两者的成员变量不会共享。

确保项目中已导入以下依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

依赖注入和初始化针对的是目标对象

现有如下类信息:

@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;
}
}
@Component
public class Bean2 {
}

为 Bean1 中的每个方法定制一个前置通知:

@Aspect
@Component
public class MyAspect {
/**
* 对 Bean1 中所有的方法进行匹配
*/
@Before("execution(* indi.lcp.a45.Bean1.*(..))")
public void before() {
System.out.println("before");
}
}

有一 SpringBoot 主启动类,它与 Bean1、Bean2 和 MyAspect 在同一包路径下,确保它们能被自动添加到 Spring 容器中:

@SpringBootApplication
public class A45 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(A45.class, args);
context.close();
}
}

运行 main() 方法,控制台输出:

indi.lcp.a45.Bean1                - setBean2(Bean2 bean2) 
indi.lcp.a45.Bean1 - init

Bean1 中的依赖注入和初始化被成功执行,但 并没有被增强。

由于 Bean1 被增强了,从 Spring 容器中获取的对象将是代理对象:

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
indi.lcp.a45.Bean1 - setBean2(Bean2 bean2)
before
indi.lcp.a45.Bean1 - init

主动调用的 setBean2() 和 init() 方法 都被增强。

代理对象与目标对象的成员变量不共享

尝试打印代理对象和目标对象的成员变量信息(直接访问,不使用方法):

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 = indi.lcp.a45.Bean2@771db12c

由于依赖注入和初始化只针对目标对象,因此代理对象中的成员变量的值都是初始值。

在实际应用过程中,不会直接去访问成员变量,而是通过方法去访问:

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
indi.lcp.a45.Bean1 - getBean2()
indi.lcp.a45.Bean2@771db12c
before
indi.lcp.a45.Bean1 - isInitialized()
true

通过方法访问代理对象的成员变量时,这些方法会被增强,同时代理对象中的方法又会去调用目标对象的方法,从而读取出正确的值。

只会对能被重写的方法进行增强

在 Bean1 中增加几个方法:

@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 方法");
}
}
@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 修饰的方法和私有方法由于无法被重写,因此它们不能被增强。如果想增强这些方法,可以使用 AspectJ 编译器增强或者 Agent 类加载。

@Value 注入底层

现有一 Bean1 类如下:

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. 解析属性值
@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
@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 表达式的解析

假设有如下几个类:

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 'indi.lcp.a46.A46$Bean3'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'indi.lcp.a46.A46$Bean3': no matching editors or conversion strategy found

最后一步数据转换出了问题,无法将 String 转换成 A46$Bean3 类型,也就是说解析 @bean3 失败了,程序仍然把它当成字符串,而不是注入的 Bean。

为了解析成功,需要在转换前解析 #{}:

@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 indi.lcp.a46.A46$Bean3
>>>>>>>>>>>>>>>>>>>
@Value 的 value 属性值: #{'hello, ' + '${JAVA_HOME}'}
解析得到的值: #{'hello, ' + 'D:\environment\JDK1.8'}
解析得到的值的类型: class java.lang.String
转换后的类型: class java.lang.String

@Autowired 注入底层

47.1 注入方式

按成员变量类型注入

现有一 Bean1 类如下:

static class Bean1 {
@Autowired
private Bean2 bean2;
}

需要被注入的对象所在类:

@Component("bean2")
static class Bean2 {
}

从容器中获取需要被注入的 Bean 对象:

@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));
}
}
indi.lcp.a47.A47_1$Bean2@222545dc

按参数类型注入

对 Bean1 进行修改:

static class Bean1 {
// --snip--

@Autowired
public void setBean2(Bean2 bean2) {
this.bean2 = bean2;
}
}

根据 setBean2() 方法的 Bean2 类型参数进行注入:

@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));
}
indi.lcp.a47.A47_1$Bean2@222545dc

包装为 Optional

对 Bean1 进行修改:

static class Bean1 {
// --snip--

@Autowired
private Optional<Bean2> bean3;
}

如果直接按照以下方式获取 DependencyDescriptor 对象:

DependencyDescriptor dd3 = 
new DependencyDescriptor(Bean1.class.getDeclaredField("bean3"), false);

其 dd3.getDependencyType() 方法将返回 Optional 的 Class 对象,这显然是不对的。

Spring 提供为 DependencyDescriptor 提供了解决这个问题的方法,即“增加嵌套等级”来获取内嵌类型:

dd3.increaseNestingLevel();

执行 increaseNestingLevel() 方法后,dd3.getDependencyType() 方法返回的 Bean2 的 Class 对象。

因此注入 Optional类型的成员变量应该按照:

@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[indi.lcp.a47.A47_1$Bean2@222545dc]

注入 Optional 对象和使用 @Autowired(required = false) 的作用是一样的,当容器中不存在目标 Bean 时,不会抛出 NoSuchBeanDefinitionException 异常。

包装为 ObjectFactory

对 Bean1 进行修改:

static class Bean1 {
// --snip--

@Autowired
private ObjectFactory<Bean2> bean4;
}

注入 ObjectFactory类型的对象与注入 Optional类型的对象类似,只不过 ObjectFactory 提供了 延迟注入 的能力,也就是说 Bean2 对象不会立即被注入,而是在需要时才被注入。

ObjectFactory 是一个函数式接口:

@FunctionalInterface
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}

注入的应该是 ObjectFactory 对象,在调用该对象的 getObject() 方法时,Bean2 对象才被注入:

@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());
}
}
indi.lcp.a47.A47_1$Bean2@222545dc

与 ObjectFactory 类似的还有个名为 ObjectProvider 的接口,后者继承了前者。

与 ObjectFactory 相比,ObjectProvider 提供了类似于 Optional 的安全注入功能,当容器中不存在目标 Bean 时, 不会抛出 NoSuchBeanDefinitionException 异常。ObjectProvider 提供的 getIfAvailable() 在获取不存在的 Bean 时,不会抛出异常,而是返回 null。

对 @Lazy 的处理

对 Bean1 进行修改,在成员变量 bean2 上使用 @Lazy 注解:

static class Bean1 {
@Autowired
@Lazy
private Bean2 bean2;

// --snip--
}

对于 @Lazy 注解标记的成员变量,注入的对象不再是目标对象,而是其代理对象,因此不能使用 DefaultListableBeanFactory 对象的 doResolveDependency() 方法来获取注入的对象。

@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());
}
indi.lcp.a47.A47_1$Bean2@222545dc
class indi.lcp.a47.A47_1$Bean2$$EnhancerBySpringCGLIB$$d631a20c

@Lazy 实现的 延迟注入 (前面讲的 ObjectFactory 和 ObjectProvider 也有延迟注入功能,但与 @Lazy 的实现不一样)不是不注入,而是注入目标对象的代理对象,当使用到代理对象中的方法时,代理对象就会去 Spring 容器中寻找真正的目标对象,然后调用目标对象对应的方法。

@Lazy 的实现细节可以在 ContextAnnotationAutowireCandidateResolver 中看到:

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

Provider 接口是由 JSR-330 提出,要想使用此接口,需要导入以下依赖:

<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<!-- 不要惊讶,版本号就是 1 -->
<version>1</version>
</dependency>

对 Bean1 进行修改:

static class Bean1 {
// --snip--

@Autowired
private Provider<Bean2> bean5;
}

注入 Provider 类型的对象与注入 ObjectFactory类型的对象极其相似,Provider 也提供了 延迟注入 的能力,注入的是 Provider 对象,在调用该对象的 get() 方法时,Bean2 对象才被注入:

@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());
}
}
indi.lcp.a47.A47_1$Bean2@222545dc

Optional 类型、ObjectFactory 类型、ObjectProvider 类型、JSR-330 提供的类型的注入逻辑可在 DefaultListableBeanFactory#resolveDependency() 方法中看到:

@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() 方法。

现有如下几个类:

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,对其进行依赖注入:

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 数组:

@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 indi.lcp.a47.A47_2$Service
service3
service2
service1
[Lindi.lcp.a47.A47_2$Service;@49139829

相关源码可在 DefaultListableBeanFactory#resolveMultipleBeans() 方法中看到。

List 类型

注入 List类型数据的逻辑与注入 Service[] 类型数据的逻辑类似,只不过在容器中寻找目标 Bean 时不再通过数组元素类型,而是通过 List 的泛型类型:

@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 indi.lcp.a47.A47_2$Service
[indi.lcp.a47.A47_2$Service3@35e2d654, indi.lcp.a47.A47_2$Service2@1bd4fdd, indi.lcp.a47.A47_2$Service1@55183b20]

注意: 对于注入的集合类型数据,注入的类型必须是 Collection 及其 子接口,比如不支持直接注入 ArrayList 类型的数据。

相关源码可在 DefaultListableBeanFactory#resolveMultipleBeans() 方法中看到:

@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

ConfigurableApplicationContext 是 ApplicationContext 接口的子接口。

需要注意的是,在 Spring 容器中并不存在 ConfigurableApplicationContext 类型、或 ApplicationContext 类型的 Bean。

Spring 容器中的所有单例 Bean 对象存放在 DefaultListableBeanFactory 中,在 DefaultListableBeanFactory 父类 DefaultSingletonBeanRegistry 中有一成员变量:

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// --snip--

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

// --snip--
}

singletonObjects 用于存放 Spring 容器中的所有单例 Bean 对象。

类似 ApplicationContext、BeanFactory 类型的对象则是放在 DefaultListableBeanFactory 中的 resolvableDependencies 中:

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
// --snip--

private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<>(16);

// --snip--
}

这些特殊对象是在调用 ApplicationContext 的 refresh() 方法时添加到 resolvableDependencies 中的。可在 AbstractApplicationContext 的 refresh() 方法中看到:

public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// --snip--

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

// --snip--
}
}
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 集合中获取。

@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类型的对象,因此需要判断容器中的 Bean 对象泛型类型是否为指定类型。判断逻辑可以使用 ContextAnnotationAutowireCandidateResolver 的 isAutowireCandidate() 方法:

@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
indi.lcp.a47.A47_2$Dao2@74f0ea28

@Qualifier

当容器中存在多个相同类型的 Bean 对象,在执行依赖注入时可以使用 @Qualifier 注解来指定需要注入的 Bean 对象的名称。判断逻辑同样使用 ContextAnnotationAutowireCandidateResolver 的 isAutowireCandidate() 方法:

@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
indi.lcp.a47.A47_2$Service2@1bd4fdd

@Primary

当容器中存在多个相同类型的 Bean 对象时,在执行依赖注入时除了可以使用 @Qualifier 注解外,还可以在被注入的 Bean 对象所在类上使用 @Primary 注解,指定执行依赖注入时使用的主要 Bean 对象。

如果 Bean 对象的所在类 被 @Primary 注解标记,那么在构造 BeanDefinition 时就会记录这个信息。

通常情况下,@Primary 注解只有一个作用在同种类型的 Bean 上,存在多个时,Spring 依旧无法区分。

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 {
}
@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 注解还支持按照成员变量名称进行匹配。

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 {
}
@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