Boot 骨架项目

使用 IDEA 创建 SpringBoot 项目时,会创建出 .mvn 目录、HELP.mdmvnwmvnw.cmd 等不必要的文件。
如果是 Linux 环境下,执行以下命令获取 SpringBoot 的骨架,并添加 webmysqlmybatis 依赖:

curl -G https://start.spring.io/pom.xml -d dependencies=web,mysql,mybatis -o pom.xml

Boot War 项目

利用 IDEA 创建新模块 test_war,区别在于选择的打包方式是 War
创建test_war模块.png
选择依赖时,勾选 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

测试

外置 Tomcat

首先得安装外置 Tomcat,省略安装步骤。
然后在 IDEA 的 Run/Debug Configurations 中进行配置,选择安装的外置 Tomcat:

配置Tomcat-Server.png

然后在 Deployment 中指定当前项目的部署方式和应用程序上下文路径:
修改Tomcat-Server的Deployment.png
编写 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>

Boot 启动流程

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

构造阶段主要完成:

  1. 获取 Bean Definition 源
  2. 推断应用类型
  3. 添加 ApplicationContext 初始化器
  4. 添加事件监听器
  5. 主类推断
获取 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();
}
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.mofan.a39.A39_1
name: servletWebServerFactory 来源: indi.mofan.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--
}
name: bean1 来源: class path resource [b01.xml]
推断应用类型

推断逻辑由 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;
}

方法底层是通过扫描 jar 包内的部分特殊文件来达成的。

添加 ApplicationContext 初始化器

调用 SpringApplication 对象的 run() 方法时会创建 ApplicationContext,最后调用 ApplicationContextrefresh() 方法完成初始化。
在创建与初始化完成之间的一些拓展功能就由 ApplicationContext 初始化器完成。
SpringApplication 的构造方法中,添加的初始化器信息从配置文件中读取:

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

也可以调用 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 {
}
添加事件监听器

与添加 ApplicationContext 初始化器一样,在 SpringApplication 的构造方法中,添加的事件监听器信息从配置文件中读取:

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

可以调用 SpringApplication 对象的 addListeners() 方法添加自定义事件监听器:

@SneakyThrows
public static void main(String[] args) {
    // --snip--
    // 输出所有事件信息
    spring.addListeners(event -> System.out.println("\t事件为: " + event));
    // --snip--
    context.close();
}
主类推断

主类推断在构造方法中可以看到:

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

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
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("出错了"));
        }
    }
}
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。
  • 第十一步:调用 ApplicationContextrefresh() 方法,完成 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.mofan.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;
}
第二步:封装启动 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 有两种,分别是 CommandLineRunnerApplicationRunner

@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
执行Runner添加程序参数.png

>>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner
commandLineRunner()...[--server.port=8080, debug]
applicationRunner()...[--server.port=8080, debug]

[server.port]

[8080] [debug]

第三步:准备 Environment 添加命令行参数

Environment 即环境对象,是对配置信息的抽象,配置信息的来源有多种,比如:系统环境变量、properties 配置文件、YAML 配置文件等等。
SpringBoot 提供了名为 ApplicationEnvironment 的类表示环境对象,它是 Spring 中 StandardEnvironment 环境对象的子类。
image.png
默认情况下,创建的 ApplicationEnvironment 对象中配置信息的来源只有两个:

  • 系统属性
  • 系统变量

针对相同名称的配置信息,按照来源的先后顺序获取。
获取 JAVA_HOME 的配置信息:

public static void main(String[] args) {
    ApplicationEnvironment env = new ApplicationEnvironment();
    System.out.println(env.getProperty("JAVA_HOME"));
}

由于 PropertiesPropertySource 中并不存在名为 JAVA_HOME 的配置信息,因此从系统环境变量 SystemEnvironmentPropertySource 中获取 JAVA_HOME 的配置信息。
在 IDEA 的 Run/Debug Configurations 中的 VM options 添加 -DJAVA_HOME=abc,使得 PropertiesPropertySource 中存在名为 JAVA_HOME 的配置信息:
添加-DJAVA_HOME=abc配置信息.png
之后再运行 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"));
}

而在 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"));
}
添加程序参数--author.name=默烦.png
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

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--
}
第五步:使用 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:/''}
"mofan"

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 配置文件中读取并初始化的:
image.png
要想该监听器成功监听到事件,需要在第五步中发布一个事件,而事件的发布由第一步获取的事件发布器完成:

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:/''}
第六步:绑定 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.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);
}

image.png
图片 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());
小结

:::danger

  1. 得到 SpringApplicationRunListeners,名字取得不好,实际是事件发布器
  • 发布 application starting 事件1️⃣
  1. 封装启动 args(一类是选项参数,以--开头,另外是非选项参数)
  2. 准备 Environment 添加命令行参数
  3. ConfigurationPropertySources 处理
  • 发布 application environment 已准备事件2️⃣
  1. 通过 EnvironmentPostProcessorApplicationListener 进行 env 后处理
  • application.properties,由 StandardConfigDataLocationResolver 解析
  • spring.application.json
  1. 绑定 spring.main 到 SpringApplication 对象
  2. 打印 banner
  3. 创建容器
  4. 准备容器
  • 发布 application context 已初始化事件3️⃣
  1. 加载 bean 定义
  • 发布 application prepared 事件4️⃣
  1. refresh 容器
  • 发布 application started 事件5️⃣
  1. 执行 runner
  • 发布 application ready 事件6️⃣
  • 这其中有异常,发布 application failed 事件7️⃣
    :::

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

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

与 Spring 整合

首先肯定需要一个 Spring 容器,选择不支持内嵌 Tomcat 的 Spring 容器,使其使用前文中的 Tomcat:

AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();

容器中注册了 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--
}

添加 Servlet 时只添加了一个 DispatcherServlet,但 Spring 容器中可能存在多个 Servlet,这些 Servlet 也应该被添加,因此可以获取 ServletRegistrationBean 类型的 Bean 并执行 onStartup 方法:

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

自动配置

自动配置类原理

/**
  * 模拟第三方配置类
  */
static class AutoConfiguration1 {
    @Bean
    public Bean1 bean1() {
        return new Bean1();
    }
}

@ToString
@NoArgsConstructor
@AllArgsConstructor
static class Bean1 {
    private String name;
}

/**
 * 模拟第三方配置类
 */
static class AutoConfiguration2 {
    @Bean
    public Bean2 bean2() {
        return new Bean2();
    }
}

static class Bean2 {

}

其中 AutoConfiguration1AutoConfiguration2 用来模拟第三方配置类,注意它们并没有被 @Configuration 注解标记,因此在未进行其他操作时,不会被添加到 Spring 容器中。
然后编写自己的配置类,使用 @Import 注解将第三方配置类添加到 Spring 容器中:

@Configuration
@Import({AutoConfiguration1.class, AutoConfiguration2.class})
static class Config {
}

如果有多个第三方配置类,难不成到一个个地导入?
可以使用导入选择器 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.mofan.a41.A41$MyImportSelector=\
indi.mofan.a41.A41.AutoConfiguration1, \
indi.mofan.a41.A41.AutoConfiguration2
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]);
    }
}

SpringFactoriesLoader.loadFactoryNames() 不仅只扫描当前项目类型路径下的 META-INF/spring.factories 文件,而是会扫描包括 Jar 包里类路径下的 META-INF/spring.factories 文件。
针对 SpringBoot 来说,自动装配的 Bean 使用如下语句加载:

SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, null);

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

:::info
其中 AutoConfiguration 是一个注解,它的全限定类名为 org.springframework.boot.autoconfigure.AutoConfiguration
:::

定义了冲突的 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("本项目");
    }
}

用户自行定义的 Bean 生效了,这是因为:@Import 导入的 Bean 先于配置类中 @Bean 定义的 Bean 执行,后者覆盖前者,使得用户自定义的 Bean 生效。
但在 SpringBoot 中不是这样的,当后续添加的 Bean 想覆盖先前添加的 Bean,会出现错误。模拟 SpringBoot 的设置:

// 默认是 true,SpringBoot 修改为 false,使得无法进行覆盖
context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false);

那我们想要覆盖 bean 该如何实现呢?
首先需要使用户的配置类中定义的 Bean 先于 @Import 导入的 Bean 添加到 Spring 容器中,只需将选择器 MyImportSelector 实现的 ImportSelector 接口更换成其子接口 DeferredImportSelector 即可:

static class MyImportSelector implements DeferredImportSelector {
    // --snip--
}

尽管还是出现了异常,但异常信息中显示的是在配置类定义的 Bean 已存在,第三方装配的 Bean 无法再添加,这表明 Bean 的添加顺序修改成功。
最后在第三方定义的 Bean 上添加 @ConditionalOnMissingBean 注解,表示容器中存在同名的 Bean 时忽略该 Bean 的添加:

static class AutoConfiguration1 {
    @Bean
    @ConditionalOnMissingBean
    public Bean1 bean1() {
        return new Bean1("第三方");
    }
}

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.mofan.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.mofan.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.mofan.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--
}

其内部存在两个类:AspectJAutoProxyingConfigurationClassProxyingConfiguration
使用了 @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", "mofan")
                .addPropertyValue("age", 20)
                .getBeanDefinition();
        // 注册构建好的 BeanDefinition
        registry.registerBeanDefinition("user", beanDefinition);
    }
}

@Setter
@ToString
static class User {
    private String name;
    private int age;
}
org.springframework.context.annotation.ConfigurationClassPostProcessor
config
user
TestImportBeanDefinitionRegistrar.User(name=mofan, age=20)

注意: 使用时一定要确保 Spring 容器中存在 ConfigurationClassPostProcessor 类型的 Bean。
除此之外,使用 BeanDefinitionRegistryPostProcessor 接口也能拿到 BeanDefinitionRegistry

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
    void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}

数据库相关的自动配置

确保当前模块下已导入:

<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,有两个关键的内部类 EmbeddedDatabaseConfigurationPooledDataSourceConfiguration

@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,并打印其 urlusernamepassword

public static void main(String[] args) {
    GenericApplicationContext context = new GenericApplicationContext();
    StandardEnvironment env = new StandardEnvironment();
    env.getPropertySources().addLast(new SimpleCommandLinePropertySource(
        "--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring",
        "--spring.datasource.username=root",
        "--spring.datasource.password=123456"
    ));
    context.setEnvironment(env);

    // --snip--

    DataSourceProperties properties = context.getBean(DataSourceProperties.class);
    System.out.println(properties.getUrl());
    System.out.println(properties.getUsername());
    System.out.println(properties.getPassword());
}
jdbc:mysql://localhost:3306/advanced_spring
root
123456

MyBatis 自动配置

接下来看看 MyBatis 的自动配置类:

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
    // --snip--

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // --snip--
    }

    // --snip--

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        // --snip--
    }

    @Configuration
    @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
        // --snip--
    }

    // --snip--
}

MybatisAutoConfiguration 生效的条件有两个:

  • 类路径下存在 SqlSessionFactorySqlSessionFactoryBean
  • Spring 容器中有且仅有一个 DataSource 类型的 Bean

它还添加了 MybatisProperties 类型的 Bean 到 Spring 容器中,并与配置文件中以 mybatis 为前缀的信息绑定。
@AutoConfigureAfter 注解指定了当前自动配置类在 DataSourceAutoConfigurationMybatisLanguageDriverAutoConfiguration 两个自动配置类解析完成之后再解析。
接下来遇到 sqlSessionFactory() 方法:

@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 容器中缺失 MapperFactoryBeanMapperScannerConfigurer 时,该配置类生效。生效时利用 @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() 方法,查看 Mapper1Mapper2 是否被添加到 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.mofan.a41
mapper1 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\mofan\a41\mapper\Mapper1.class]
mapper2 来源: file [D:\Code\IdeaCode\advanced-spring\boot\target\classes\indi\mofan\a41\mapper\Mapper2.class]

@MapperScan 注解与 MybatisAutoConfiguration 在功能上很类似,只不过:

  • @MapperScan 可以指定具体的扫描路径,未指定时会把引导类范围内的所有接口当做 Mapper 接口;
  • MybatisAutoConfiguration 关注所有被 @Mapper 注解标记的接口,忽略未被 @Mapper 标记的接口。

事务自动配置
事务自动配置与 DataSourceTransactionManagerAutoConfigurationTransactionAutoConfiguration 有关。
DataSourceTransactionManagerAutoConfiguration 配置了 DataSourceTransactionManager 用来执行事务的提交、回滚操作。
TransactionAutoConfiguration 在功能上对标 @EnableTransactionManagement,包含以下三个 Bean:

  • BeanFactoryTransactionAttributeSourceAdvisor:事务切面类,包含通知和切点
  • TransactionInterceptor:事务通知类,由它在目标方法调用前后加入事务操作
  • AnnotationTransactionAttributeSource:解析 @Transactional 及事务属性,还包含了切点功能

如果自定义了 DataSourceTransactionManager 或是在引导类加了 @EnableTransactionManagement,则以自定义为准。

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

自定义自动配置类

在 SpringBoot 自动装配时添加自定义组件分为两步:

  1. 在类路径下自定义 META-INF/spring.factories 文件,以 org.springframework.boot.autoconfigure.EnableAutoConfiguration 为 key,设置需要自动装配的自定义组件的全限定类名为 value
  2. 编写配置类,在配置类上使用 @EnableAutoConfiguration 注解,并将其添加到 Spring 容器中

在实际项目开发中,省略第二步,SpringBoot 的会自动扫描。

SpringBoot 2.7.0 及其以后版本

在类路径下自定义 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,文件中 每一行 表示需要进行自动装配的类的全限定类名,因此不能随意换行。
在这个文件中,以 # 开头的行表示注释。

条件装配底层

@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.mofan.a42.A42$AutoConfiguration1
bean1

导入 druid 依赖,再次运行 main() 方法:

config
org.springframework.context.annotation.ConfigurationClassPostProcessor
indi.mofan.a42.A42$AutoConfiguration1
bean1

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

其他拓展

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.mofan.a43.Bean1FactoryBean     - create bean: indi.mofan.a43.Bean1@2667f029 
indi.mofan.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.mofan.a43.Bean1FactoryBean     - create bean: indi.mofan.a43.Bean1@6a28ffa4 
indi.mofan.a43.Bean1PostProcessor   - after [bean1] init 
indi.mofan.a43.Bean1@6a28ffa4

bean1 进行了初始化后的增强逻辑,但未进行初始化前的增强逻辑。
创建代理对象的时机就是在初始化后,因此由 FactoryBean 创建的 Bean 可以进行代理增强

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 不会存放在 DefaultSingletonBeanRegistrysingletonFactories 中,而是在 AbstractAutowireCapableBeanFactoryfactoryBeanInstanceCache 中。

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

这没什么奇怪的,bean1bean2bean3 都被添加到 Spring 容器中。
在编译生成的 target 目录下的 classes/META-INF/spring.components 文件里有以下信息:

indi.mofan.a44.Bean1=org.springframework.stereotype.Component
indi.mofan.a44.Bean2=org.springframework.stereotype.Component
indi.mofan.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.mofan.a45.Bean1.*(..))")
    public void before() {
        System.out.println("before");
    }
}

有一 SpringBoot 主启动类,它与 Bean1Bean2MyAspect 在同一包路径下,确保它们能被自动添加到 Spring 容器中:

@SpringBootApplication
public class A45 {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(A45.class, args);
        context.close();
    }
}

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

indi.mofan.a45.Bean1                - setBean2(Bean2 bean2) 
indi.mofan.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.mofan.a45.Bean1                - setBean2(Bean2 bean2) 
before
indi.mofan.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.mofan.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.mofan.a45.Bean1                - getBean2() 
indi.mofan.a45.Bean2@771db12c
before
indi.mofan.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.mofan.a46.A46$Bean3'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'indi.mofan.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.mofan.a46.A46$Bean3
>>>>>>>>>>>>>>>>>>>
@Value 的 value 属性值: #{'hello, ' + '${JAVA_HOME}'}
解析得到的值: #{'hello, ' + 'D:\environment\JDK1.8'}
解析得到的值的类型: class java.lang.String
转换后的类型: class java.lang.String
此作者没有提供个人介绍
最后更新于 2024-08-27