封面画师:T5-茨舞(微博) 封面ID:104963785
参考视频:黑马程序员Spring视频教程,全面深度讲解spring5底层原理
源码仓库:mofan212/advanced-spring (github.com)
37. Boot 骨架项目
使用 IDEA 创建 SpringBoot 项目时,会创建出 .mvn
目录、HELP.md
、mvnw
和 mvnw.cmd
等不必要的文件。
如果是 Linux 环境下,执行以下命令获取 SpringBoot 的骨架,并添加 web
、mysql
、mybatis
依赖:
1 curl -G https://start.spring.io/pom.xml -d dependencies=web,mysql,mybatis -o pom.xml
也可以使用 Postman 等接口测试工具来实现。
更多用法执行以下命令进行参考:
1 curl https://start.spring.io
但说实话,实际开发时宁愿直接使用 IDEA 创建项目。
38. Boot War 项目
38.1 项目的构建
利用 IDEA 创建新模块 test_war
,区别在于选择的打包方式是 War
:
选择依赖时,勾选 Spring Web。
一般来说,选择 War
作为打包方式都是为了使用 JSP,因为 JSP 不能配合 Jar
打包方式使用。
JSP 文件的存放路径是固定的,在 src/main
目录下的 webapp
目录,如果没有 webapp
目录,需要自行创建。之后新建 hello.jsp
:
1 2 3 4 5 6 7 8 9 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h3>Hello!</h3> </body> </html>
之后新建控制器类 HelloController
,编写控制器方法 hello()
,返回值类型是 String
,要求返回的是视图名称:
1 2 3 4 5 6 7 @Controller public class HelloController { @RequestMapping("/hello") public String hello () { return "hello" ; } }
最后要在配置文件中配置视图的前缀、后缀,使控制器方法返回的视图名称对应视图名称的 JSP 页面:
1 2 spring.mvc.view.prefix =/ spring.mvc.view.suffix =.jsp
38.2 项目的测试
使用外置 Tomcat 测试
首先得安装外置 Tomcat,省略安装步骤。
然后在 IDEA 的 Run/Debug Configurations 中进行配置,选择安装的外置 Tomcat:
然后在 Deployment 中指定当前项目的部署方式和应用程序上下文路径:
尽管使用外置 Tomcat 进行测试,但主启动类不能少:
1 2 3 4 5 6 @SpringBootApplication public class TestWarApplication { public static void main (String[] args) { SpringApplication.run(TestWarApplication.class, args); } }
除此之外,还要编写 ServletInitializer
,在外置 Tomcat 启动时,找到 SpringBoot 项目的主启动类,执行 SpringBoot 流程:
1 2 3 4 5 6 public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure (SpringApplicationBuilder application) { return application.sources(TestWarApplication.class); } }
如果没有 ServletInitializer
类,则无法使 SpringBoot 项目使用外置 Tomcat。
运行程序后,访问 localhost:8080/hello
,页面进入编写的 hello.jsp
页面。
使用内嵌 Tomcat 测试
打包方式为 Jar 时,直接运行主启动类,然后访问对应的请求路径即可跳转到指定的视图中,那打包访问变成 War 之后,使用这种方式还能够成功跳转吗?
程序运行成功后,访问 localhost:8080/hello
,页面并没有按照预期跳转到 hello.jsp
页面中,而是下载了该页面。
这是因为内嵌 Tomcat 中不具备 JSP 解析能力,如果要想使其具备解析 JSP 的能力,需要添加依赖:
1 2 3 4 5 <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-jasper</artifactId > <scope > provided</scope > </dependency >
之后再访问 localhost:8080/hello
,页面进入编写的 hello.jsp
页面。
使用内嵌 Tomcat 测试遇到的问题
添加 tomcat-embed-jasper
依赖后,访问 localhost:8080/hello
,仍在下载 hello.jsp
。
答:清理浏览器缓存,在浏览器的 DevTools 中的 Network 内 勾选 Disable cache
以禁用缓存。
添加 tomcat-embed-jasper
依赖后,访问 localhost:8080/hello
,页面 404。
答:设置运行主启动类的 Run/Debug Configurations 中的 Working directory 为当前模块所在目录。
参考链接:springboot 在idea多模块下 子模块的web项目用内置tomcat启动访问jsp报404
39. Boot 启动过程
39.1 SpringApplication 的构造
SpringBoot 的主启动类类似于:
1 2 3 4 5 6 @SpringBootApplication public class BootApplication { public static void main (String[] args) { SpringApplication.run(BootApplication.class, args); } }
其中 SpringApplication#run()
方法是核心方法:
1 2 3 4 5 6 7 public static ConfigurableApplicationContext run (Class<?> primarySource, String... args) { return run(new Class <?>[] { primarySource }, args); } public static ConfigurableApplicationContext run (Class<?>[] primarySources, String[] args) { return new SpringApplication (primarySources).run(args); }
最终使用 new
关键字构造了 SpringApplication
对象,然后调用了非静态 run()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public SpringApplication (Class<?>... primarySources) { this (null , primarySources); } @SuppressWarnings({ "unchecked", "rawtypes" }) public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null" ); this .primarySources = new LinkedHashSet <>(Arrays.asList(primarySources)); this .webApplicationType = WebApplicationType.deduceFromClasspath(); this .bootstrapRegistryInitializers = new ArrayList <>( getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this .mainApplicationClass = deduceMainApplicationClass(); }
构造 SpringApplication
对象时做了如下几件事:
获取 Bean Definition 源
推断应用类型
添加 ApplicationContext
初始化器
添加事件监听器
主类推断
获取 Bean Definition 源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Configuration public class A39_1 { public static void main (String[] args) { SpringApplication spring = new SpringApplication (A39_1.class); ConfigurableApplicationContext context = spring.run(args); Arrays.stream(context.getBeanDefinitionNames()).forEach(i -> { System.out.println("name: " + i + " 来源: " + context.getBeanFactory().getBeanDefinition(i).getResourceDescription()); }); context.close(); } static class Bean1 { } static class Bean2 { } @Bean public Bean2 bean2 () { return new Bean2 (); } }
运行 main()
方法后,控制台打印出错误信息:
***************************
APPLICATION FAILED TO START
***************************
Description:
Web application could not be started as there was no org.springframework.boot.web.servlet.server.ServletWebServerFactory bean defined in the context.
Action:
Check your application's dependencies for a supported servlet web server.
Check the configured web application type.
这是因为添加了 spring-boot-starter-web
依赖,但 Spring 容器中并没有 ServletWebServerFactory
类型的 Bean。向容器中添加即可:
1 2 3 4 @Bean public TomcatServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (); }
之后在运行 main()
方法:
name: org.springframework.context.annotation.internalConfigurationAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalAutowiredAnnotationProcessor 来源: null
name: org.springframework.context.annotation.internalCommonAnnotationProcessor 来源: null
name: org.springframework.context.event.internalEventListenerProcessor 来源: null
name: org.springframework.context.event.internalEventListenerFactory 来源: null
name: a39_1 来源: null
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean2 来源: indi.mofan.a39.A39_1
name: servletWebServerFactory 来源: indi.mofan.a39.A39_1
来源为 null
的 Bean 是由 Spring 提供的“内置” Bean。
使用 XML 配置文件添加 Bean,并利用 setSources()
方法设置创建 ApplicationContext
的其他源:
1 2 3 4 5 public static void main (String[] args) { SpringApplication spring = new SpringApplication (A39_1.class); spring.setSources(Collections.singleton("classpath:b01.xml" )); }
再次运行 main()
方法,控制台打印的内容多了一条:
name: bean1 来源: class path resource [b01.xml]
推断应用类型
应用类型的推断在构造方法中可以看到:
1 2 3 4 5 6 public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .webApplicationType = WebApplicationType.deduceFromClasspath(); }
推断逻辑由 WebApplicationType
枚举中的 deduceFromClasspath()
方法完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static WebApplicationType deduceFromClasspath () { if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null ) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null ) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null )) { return WebApplicationType.REACTIVE; } for (String className : SERVLET_INDICATOR_CLASSES) { if (!ClassUtils.isPresent(className, null )) { return WebApplicationType.NONE; } } return WebApplicationType.SERVLET; }
利用反射调用 deduceFromClasspath()
方法:
1 2 3 4 5 6 7 8 9 10 @SneakyThrows public static void main (String[] args) { Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath" ); deduceFromClasspath.setAccessible(true ); System.out.println("\t应用类型为: " + deduceFromClasspath.invoke(null )); }
应用类型为: SERVLET
添加 ApplicationContext
初始化器
调用 SpringApplication
对象的 run()
方法时会创建 ApplicationContext
,最后调用 ApplicationContext
的 refresh()
方法完成初始化。
在创建与初始化完成之间的一些拓展功能就由 ApplicationContext
初始化器完成。
在 SpringApplication
的构造方法中,添加的初始化器信息从配置文件中读取:
1 2 3 4 5 6 public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); }
也可以调用 SpringApplication
对象的 addInitializers()
方法添加自定义初始化器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @SneakyThrows public static void main (String[] args) { spring.addInitializers(applicationContext -> { if (applicationContext instanceof GenericApplicationContext) { GenericApplicationContext context = (GenericApplicationContext) applicationContext; context.registerBean("bean3" , Bean3.class); } }); ConfigurableApplicationContext context = spring.run(args); Arrays.stream(context.getBeanDefinitionNames()).forEach(i -> { System.out.println("name: " + i + " 来源: " + context.getBeanFactory().getBeanDefinition(i).getResourceDescription()); }); context.close(); } static class Bean3 {}
运行 main()
方法后,控制台打印的 Bean 又多了一条:
name: bean3 来源: null
添加事件监听器
与添加 ApplicationContext
初始化器一样,在 SpringApplication
的构造方法中,添加的事件监听器信息从配置文件中读取:
1 2 3 4 5 6 public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); }
可以调用 SpringApplication
对象的 addListeners()
方法添加自定义事件监听器:
1 2 3 4 5 6 7 8 @SneakyThrows public static void main (String[] args) { spring.addListeners(event -> System.out.println("\t事件为: " + event)); context.close(); }
运行 main()
方法后,控制台打印的事件信息汇总后如下:
事件类型为: class org.springframework.boot.context.event.ApplicationStartingEvent
事件类型为: class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationContextInitializedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationPreparedEvent
事件类型为: class org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
事件类型为: class org.springframework.context.event.ContextRefreshedEvent
事件类型为: class org.springframework.boot.context.event.ApplicationStartedEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.boot.context.event.ApplicationReadyEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.boot.availability.AvailabilityChangeEvent
事件类型为: class org.springframework.context.event.ContextClosedEvent
主类推断
主类推断在构造方法中可以看到:
1 2 3 4 5 public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .mainApplicationClass = deduceMainApplicationClass(); }
推断逻辑由 deduceMainApplicationClass()
方法完成,利用反射调用该方法:
1 2 3 4 5 6 7 8 9 10 @SneakyThrows public static void main (String[] args) { Method deduceMainApplicationClass = SpringApplication.class.getDeclaredMethod("deduceMainApplicationClass" ); deduceMainApplicationClass.setAccessible(true ); System.out.println("\t主类是: " + deduceMainApplicationClass.invoke(spring)); }
主类是: class indi.mofan.a39.A39_1
39.2 SpringApplication#run() 的分析
第一步:获取 SpringApplicationRunListeners
在执行 run()
方法时,首先会获取到 SpringApplicationRunListeners
,它是事件发布器的组合,能够在 SpringBoot 启动的各个阶段中发布事件。
SpringApplicationRunListeners
中使用 SpringApplicationRunListener
来描述单个事件发布器,SpringApplicationRunListener
是一个接口,它有且仅有一个实现类 EventPublishingRunListener
。
在 SpringBoot 中,事件发布器都是在配置文件中读取,从 META-INF/spring.factories
中读取,该文件中有这样一句:
1 2 3 org.springframework.boot.SpringApplicationRunListener =\ org.springframework.boot.context.event.EventPublishingRunListener
自行实现从 META-INF/spring.factories
配置文件中读取事件发布器信息,并发布各种事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class A39_2 { @SneakyThrows public static void main (String[] args) { SpringApplication app = new SpringApplication (); app.addListeners(i -> System.out.println(i.getClass())); List<String> names = SpringFactoriesLoader.loadFactoryNames( SpringApplicationRunListener.class, A39_2.class.getClassLoader() ); for (String name : names) { Class<?> clazz = Class.forName(name); Constructor<?> constructor = clazz.getConstructor(SpringApplication.class, String[].class); SpringApplicationRunListener publisher = (SpringApplicationRunListener) constructor.newInstance(app, args); DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext (); publisher.starting(bootstrapContext); publisher.environmentPrepared(bootstrapContext, new StandardEnvironment ()); GenericApplicationContext context = new GenericApplicationContext (); publisher.contextPrepared(context); publisher.contextLoaded(context); context.refresh(); publisher.started(context, null ); publisher.ready(context, null ); 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 容器的创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @SneakyThrows @SuppressWarnings("all") public static void main (String[] args) { SpringApplication app = new SpringApplication (); app.addInitializers(applicationContext -> System.out.println("执行初始化器增强..." )); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 8. 创建容器" ); GenericApplicationContext context = createApplicationContext(WebApplicationType.SERVLET); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 9. 准备容器" ); for (ApplicationContextInitializer initializer : app.getInitializers()) { initializer.initialize(context); } System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 10. 加载 Bean 定义" ); DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(); AnnotatedBeanDefinitionReader reader1 = new AnnotatedBeanDefinitionReader (beanFactory); XmlBeanDefinitionReader reader2 = new XmlBeanDefinitionReader (beanFactory); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner (beanFactory); reader1.register(Config.class); reader2.loadBeanDefinitions(new ClassPathResource ("b03.xml" )); scanner.scan("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; }
涉及到的配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static class Bean4 {} static class Bean5 {} @Configuration static class Config { @Bean public Bean5 bean5 () { return new Bean5 (); } @Bean public ServletWebServerFactory servletWebServerFactory () { return new TomcatServletWebServerFactory (); } }
XML 配置文件:
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean id ="bean4" class ="indi.mofan.a39.A39_3.Bean4" /> </beans >
indi.mofan.a39.sub
包下的 Bean 信息:
1 2 3 4 5 6 7 package indi.mofan.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\mofan\a39\sub\Bean7.class]
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源: null
name: bean5 来源: indi.mofan.a39.A39_3$Config
name: servletWebServerFactory 来源: indi.mofan.a39.A39_3$Config
第二步:封装启动 args
调用 DefaultApplicationArguments
的构造方法,传入 args
即可:
1 2 3 4 5 6 7 8 @SneakyThrows @SuppressWarnings("all") public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args" ); DefaultApplicationArguments arguments = new DefaultApplicationArguments (args); }
第十二步:执行 Runner
在 SpringBoot 启动成功后,可以执行一些 Runner
,进行一些预处理或测试。Runner
有两种,分别是 CommandLineRunner
和 ApplicationRunner
:
1 2 3 4 5 6 7 8 9 @FunctionalInterface public interface CommandLineRunner { void run (String... args) throws Exception; } @FunctionalInterface public interface ApplicationRunner { void run (ApplicationArguments args) throws Exception; }
它们都是函数式接口,内部的抽象方法长得也很像,只不过:
CommandLineRunner
直接接收启动参数;
ApplicationRunner
则是接收封装后的 ApplicationArguments
,即 第二步 封装的对象。
在配置类中添加这两种类型的 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Bean public CommandLineRunner commandLineRunner () { return args -> System.out.println("commandLineRunner()..." + Arrays.toString(args)); } @Bean public ApplicationRunner applicationRunner () { return args -> { System.out.println("applicationRunner()..." + Arrays.toString(args.getSourceArgs())); System.out.println(args.getOptionNames()); System.out.println(args.getOptionValues("server.port" )); System.out.println(args.getNonOptionArgs()); }; }
执行 Runner
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SneakyThrows @SuppressWarnings("all") public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args" ); DefaultApplicationArguments arguments = new DefaultApplicationArguments (args); 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
:
>>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner
commandLineRunner()...[--server.port=8080, debug]
applicationRunner()...[--server.port=8080, debug]
[server.port]
[8080]
[debug]
第三步:准备 Environment
添加命令行参数
Environment
即环境对象,是对配置信息的抽象,配置信息的来源有多种,比如:系统环境变量、properties 配置文件、YAML 配置文件等等。
SpringBoot 提供了名为 ApplicationEnvironment
的类表示环境对象,它是 Spring 中 StandardEnvironment
环境对象的子类。
默认情况下,创建的 ApplicationEnvironment
对象中配置信息的来源只有两个:
1 2 3 4 5 6 7 8 9 10 11 12 package org.springframework.boot;public class Step3 { public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().forEach(System.out::println); } }
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
针对相同名称的配置信息,按照来源的先后顺序获取。
获取 JAVA_HOME
的配置信息:
1 2 3 4 public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); System.out.println(env.getProperty("JAVA_HOME" )); }
D:\environment\JDK1.8
由于 PropertiesPropertySource
中并不存在名为 JAVA_HOME
的配置信息,因此从系统环境变量 SystemEnvironmentPropertySource
中获取 JAVA_HOME
的配置信息。
在 IDEA 的 Run/Debug Configurations 中的 VM options 添加 -DJAVA_HOME=abc
,使得 PropertiesPropertySource
中存在名为 JAVA_HOME
的配置信息:
之后再运行 main()
方法,控制台打印出:
abc
如果想从配置文件 application.properties
中读取配置信息,可以添加配置信息的来源。配置文件的优先级最低,添加来源时调用 addLast()
方法:
1 2 3 4 5 6 7 8 @SneakyThrows public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast(new ResourcePropertySource (new ClassPathResource ("application.properties" ))); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("author.name" )); }
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
"mofan"
而在 SpringBoot 中,这里 只 添加 SimpleCommandLinePropertySource
,并且它的优先级最高,使用 addFirst()
方法添加:
1 2 3 4 5 6 7 8 9 @SneakyThrows public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast(new ResourcePropertySource (new ClassPathResource ("application.properties" ))); env.getPropertySources().addFirst(new SimpleCommandLinePropertySource (args)); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("author.name" )); }
运行 main()
方法前,需要添加程序参数 --author.name=默烦
:
SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='class path resource [application.properties]'}
默烦
第四步:添加 ConfigurationPropertySources
有一 step4.properties
文件,其内容如下:
1 2 3 user.first-name =George user.middle_name =Walker user.lastName =Bush
尝试读取文件中的内容:
1 2 3 4 5 6 7 8 9 10 11 @SneakyThrows public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast( new ResourcePropertySource ("step4" , new ClassPathResource ("step4.properties" )) ); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("user.first-name" )); System.out.println(env.getProperty("user.middle-name" )); System.out.println(env.getProperty("user.last-name" )); }
step4.properties
文件中配置信息的 key 是 user.middle_name
,但在读取时,使用的是 user.middle-name
;还有 user.lastName
的 key,但读取时使用 user.last-name
。能读取成功吗?
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='step4'}
George
null
null
显然是不行的,为了能读取成功,需要实现 松散绑定 ,添加 ConfigurationPropertySources
:
1 2 3 4 5 6 @SneakyThrows public static void main (String[] args) { ConfigurationPropertySources.attach(env); }
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ResourcePropertySource {name='step4'}
George
Walker
Bush
第五步:使用 EnvironmentPostProcessorApplicationListener
进行环境对象后置处理
在第三步中 只 添加 SimpleCommandLinePropertySource
,读取 properties、YAML 配置文件的源就是在第五步中添加的。
完成这样功能需要使用到 EnvironmentPostProcessor
,其具体实现是 ConfigDataEnvironmentPostProcessor
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) { SpringApplication app = new SpringApplication (); ApplicationEnvironment env = new ApplicationEnvironment (); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强前" ); env.getPropertySources().forEach(System.out::println); ConfigDataEnvironmentPostProcessor processor1 = new ConfigDataEnvironmentPostProcessor ( new DeferredLogs (), new DefaultBootstrapContext () ); processor1.postProcessEnvironment(env, app); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强后" ); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("author.name" )); RandomValuePropertySourceEnvironmentPostProcessor processor2 = new RandomValuePropertySourceEnvironmentPostProcessor (new DeferredLog ()); processor2.postProcessEnvironment(env, app); }
>>>>>>>>>>>>>>>>>>>>>>>> 增强前
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
>>>>>>>>>>>>>>>>>>>>>>>> 增强后
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
"mofan"
EnvironmentPostProcessor
还有一个有趣的实现:RandomValuePropertySourceEnvironmentPostProcessor
,该实现提供了随机值的生成。
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 再次增强后" ); env.getPropertySources().forEach(System.out::println); System.out.println(env.getProperty("random.string" )); System.out.println(env.getProperty("random.int" )); System.out.println(env.getProperty("random.uuid" )); }
>>>>>>>>>>>>>>>>>>>>>>>> 再次增强后
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
5ef4038a709215938cbd3e1c031f66dd
1481116109
18548e0b-8bad-458b-b38e-bf793aa24ced
在 SpringBoot 中的实现是不会采取上述示例代码的方式来添加后置处理器,同样会从 META-INF/spring.factories
配置文件中读取并初始化后置处理器:
1 2 3 4 5 6 7 8 org.springframework.boot.env.EnvironmentPostProcessor =\ org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\ org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\ org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\ org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\ org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\ org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor
SpringBoot 中读取 META-INF/spring.factories
配置文件初始化环境后置处理器,再执行处理逻辑的功能由 EnvironmentPostProcessorApplicationListener
完成。它是一个事件监听器,同样是在 META-INF/spring.factories
配置文件中读取并初始化的:
1 2 3 4 5 6 7 8 org.springframework.context.ApplicationListener =\ org.springframework.boot.ClearCachesApplicationListener,\ org.springframework.boot.builder.ParentContextCloserApplicationListener,\ org.springframework.boot.context.FileEncodingApplicationListener,\ org.springframework.boot.context.config.AnsiOutputApplicationListener,\ org.springframework.boot.context.config.DelegatingApplicationListener,\ org.springframework.boot.context.logging.LoggingApplicationListener,\ org.springframework.boot.env.EnvironmentPostProcessorApplicationListener
要想该监听器成功监听到事件,需要在第五步中发布一个事件,而事件的发布由第一步获取的事件发布器完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String[] args) { SpringApplication app = new SpringApplication (); app.addListeners(new EnvironmentPostProcessorApplicationListener ()); ApplicationEnvironment env = new ApplicationEnvironment (); List<String> names = SpringFactoriesLoader.loadFactoryNames(EnvironmentPostProcessor.class, Step5.class.getClassLoader()); names.forEach(System.out::println); EventPublishingRunListener publisher = new EventPublishingRunListener (app, args); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强前" ); env.getPropertySources().forEach(System.out::println); publisher.environmentPrepared(new DefaultBootstrapContext (), env); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 增强后" ); env.getPropertySources().forEach(System.out::println); }
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor
org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
>>>>>>>>>>>>>>>>>>>>>>>> 增强前
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
>>>>>>>>>>>>>>>>>>>>>>>> 增强后
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
配置文件中 EnvironmentPostProcessor
的实现有很多,但根据上述打印出的信息,生效的并不多,是否生效与项目的依赖配置有关。
第六步:绑定 spring.main
前缀的配置信息到 SpringApplication
对象
使用 @ConfigurationProperties
注解可以指定一个前缀,SpringBoot 将根据指定的前缀和属性名称在配置文件中寻找对应的信息并完成注入,其底层是利用 Binder
实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @SneakyThrows public static void main (String[] args) { SpringApplication app = new SpringApplication (); ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast( new ResourcePropertySource ("step4" , new ClassPathResource ("step4.properties" )) ); User user = Binder.get(env).bind("user" , User.class).get(); System.out.println(user); User existUser = new User (); Binder.get(env).bind("user" , Bindable.ofInstance(existUser)); System.out.println(existUser); } @Getter @Setter @ToString static class User { private String firstName; private String middleName; private String lastName; }
Step6.User(firstName=George, middleName=Walker, lastName=Bush)
Step6.User(firstName=George, middleName=Walker, lastName=Bush)
在第六步中,绑定 spring.main
前缀的配置信息到 SpringApplication
对象也是利用了 Binder
。
假设 step6.properties
配置文件的信息如下:
1 2 spring.main.banner-mode =off spring.main.lazy-initialization =true
绑定 spring.main
开头的配置信息到 SpringApplication
对象中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SneakyThrows public static void main (String[] args) { SpringApplication app = new SpringApplication (); ApplicationEnvironment env = new ApplicationEnvironment (); env.getPropertySources().addLast( new ResourcePropertySource ("step6" , new ClassPathResource ("step6.properties" )) ); Class<? extends SpringApplication > clazz = app.getClass(); Field bannerMode = clazz.getDeclaredField("bannerMode" ); bannerMode.setAccessible(true ); Field lazyInitialization = clazz.getDeclaredField("lazyInitialization" ); lazyInitialization.setAccessible(true ); System.out.println(bannerMode.get(app)); System.out.println(lazyInitialization.get(app)); Binder.get(env).bind("spring.main" , Bindable.ofInstance(app)); System.out.println(bannerMode.get(app)); System.out.println(lazyInitialization.get(app)); }
CONSOLE
false
OFF
true
第七步:打印 Banner
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { ApplicationEnvironment env = new ApplicationEnvironment (); SpringApplicationBannerPrinter printer = new SpringApplicationBannerPrinter ( new DefaultResourceLoader (), new SpringBootBanner () ); printer.print(env, Step7.class, System.out); }
除此之外还可以自定义文字和图片 Banner,文字 Banner 的文件类型需要是 txt
,图片 Banner 的文件类型需要是 gif
。
文字 Banner:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { env.getPropertySources().addLast(new MapPropertySource ( "custom" , Collections.singletonMap("spring.banner.location" , "banner1.txt" ) )); printer.print(env, Step7.class, System.out); }
文字 Banner 可以从 网站 上自定义。
图片 Banner:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { env.getPropertySources().addLast(new MapPropertySource ( "custom" , Collections.singletonMap("spring.banner.image.location" , "banner2.gif" ) )); printer.print(env, Step7.class, System.out); }
获取 Spring 或 SpringBoot 的版本号可以使用:
1 2 System.out.println("SpringBoot: " + SpringBootVersion.getVersion()); System.out.println("Spring: " + SpringVersion.getVersion());
步骤总结
得到 SpringApplicationRunListeners
事件发布器
发布 Application Starting 事件 1️⃣
封装启动 args
准备 Environment
添加命令行参数
ConfigurationPropertySources
处理
发布 Application Environment 已准备事件 2️⃣
通过 EnvironmentPostProcessorApplicationListener
进行 env 后处理
application.properties
由 StandardConfigDataLocationResolver
解析
spring.application.json
绑定 spring.main
到 SpringApplication
对象
打印 Banner
创建容器
准备容器
发布 Application Context 已初始化事件 3️⃣
加载 Bean 定义
发布 Application Prepared 事件 4️⃣
refresh 容器
发布 Application Started 事件 5️⃣
执行 Runner
40. Tomcat 内嵌容器
Tomcat 基本结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Server └───Service ├───Connector (协议, 端口) └───Engine └───Host(虚拟主机 localhost) ├───Context1 (应用 1, 可以设置虚拟路径, / 即 url 起始路径; 项目磁盘路径, 即 docBase) │ │ index.html │ └───WEB-INF │ │ web.xml (servlet, filter, listener) 3.0 │ ├───classes (servlet, controller, service ...) │ ├───jsp │ └───lib (第三方 jar 包) └───Context2 (应用 2) │ index.html └───WEB-INF web.xml
40.1 内嵌 Tomcat 的使用
内嵌 Tomcat 的使用分为 6 步:
创建 Tomcat
创建项目文件夹,即 docBase
文件夹
创建 Tomcat 项目,在 Tomcat 中称为 Context
编程添加 Servlet
启动 Tomcat
创建连接器,设置监听端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @SneakyThrows public static void main (String[] args) { Tomcat tomcat = new Tomcat (); tomcat.setBaseDir("tomcat" ); File docBase = Files.createTempDirectory("boot." ).toFile(); docBase.deleteOnExit(); Context context = tomcat.addContext("" , docBase.getAbsolutePath()); context.addServletContainerInitializer((set, servletContext) -> { HelloServlet servlet = new HelloServlet (); servletContext.addServlet("hello" , servlet).addMapping("/hello" ); }, Collections.emptySet()); tomcat.start(); Connector connector = new Connector (new Http11Nio2Protocol ()); connector.setPort(8080 ); tomcat.setConnector(connector); }
自行实现的 Servlet 需要继承 HttpServlet
,并重写 doGet()
方法:
1 2 3 4 5 6 7 8 9 10 public class HelloServlet extends HttpServlet { private static final long serialVersionUID = 8117441197359625079L ; @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8" ); resp.getWriter().print("<h3>hello</h3>" ); } }
运行 main()
方法后,在浏览器访问 localhost:8080/hello
,页面显示 hello
。
40.2 与 Spring 整合
首先肯定需要一个 Spring 容器,选择不支持内嵌 Tomcat 的 Spring 容器,使其使用前文中的 Tomcat:
1 2 3 4 5 6 7 public static WebApplicationContext getApplicationContext () { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext (); context.register(Config.class); context.refresh(); return context; }
容器中注册了 Config
Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration static class Config { @Bean public DispatcherServletRegistrationBean registrationBean (DispatcherServlet dispatcherServlet) { return new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); } @Bean public DispatcherServlet dispatcherServlet (WebApplicationContext applicationContext) { return new DispatcherServlet (applicationContext); } @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter () { RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter (); handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter ())); return handlerAdapter; } @RestController static class MyController { @GetMapping("hello2") public Map<String,Object> hello () { return Collections.singletonMap("hello2" , "hello2, spring!" ); } } }
Tomcat 在添加 Servlet 时,添加 DispatcherServlet
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SneakyThrows public static void main (String[] args) { WebApplicationContext springContext = getApplicationContext(); context.addServletContainerInitializer((set, servletContext) -> { HelloServlet servlet = new HelloServlet (); servletContext.addServlet("hello" , servlet).addMapping("/hello" ); DispatcherServlet dispatcherServlet = springContext.getBean(DispatcherServlet.class); servletContext.addServlet("dispatcherServlet" , dispatcherServlet).addMapping("/" ); }, Collections.emptySet()); }
运行 main()
方法,在浏览器中访问 localhost:8080/hello2
,页面上显示:
{"hello2":"hello2, spring!"}
添加 Servlet 时只添加了一个 DispatcherServlet
,但 Spring 容器中可能存在多个 Servlet,这些 Servlet 也应该被添加,因此可以获取 ServletRegistrationBean
类型的 Bean 并执行 `` 方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @SneakyThrows public static void main (String[] args) { WebApplicationContext springContext = getApplicationContext(); context.addServletContainerInitializer((set, servletContext) -> { HelloServlet servlet = new HelloServlet (); servletContext.addServlet("hello" , servlet).addMapping("/hello" ); for (ServletRegistrationBean registrationBean : springContext.getBeansOfType(ServletRegistrationBean.class).values()) { registrationBean.onStartup(servletContext); } }, Collections.emptySet()); }
运行 main()
方法,在浏览器中访问 localhost:8080/hello2
,页面显示同样的内容。
41. 自动配置
41.1 自动配置类原理
有以下四个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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 容器中:
1 2 3 4 @Configuration @Import({AutoConfiguration1.class, AutoConfiguration2.class}) static class Config {}
1 2 3 4 5 6 7 8 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); }
运行 main()
方法后,控制台打印出:
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
indi.mofan.a41.A41$AutoConfiguration1
bean1
indi.mofan.a41.A41$AutoConfiguration2
bean2
如果有多个第三方配置类,难不成到一个个地导入?
可以使用导入选择器 ImportSelector
,重写 selectImports()
方法,返回需要自动装配的 Bean 的全限定类名数组:
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration @Import(MyImportSelector.class) static class Config {} static class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()}; } }
但这样的方式相比最初的方式并没有本质区别,甚至更麻烦,还多了一个类。如果 selectImports()
方法返回的全限定类名可以从文件中读取,就更方便了。
在当前项目的类路径下创建 META-INF/spring.factories
文件,约定一个 key,对应的 value 即为需要指定装配的 Bean:
1 2 3 4 indi.mofan.a41.A41$MyImportSelector =\ indi.mofan.a41.A41.AutoConfiguration1, \ indi.mofan.a41.A41.AutoConfiguration2
修改 selectImports()
方法实现逻辑:
1 2 3 4 5 6 7 static class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { List<String> names = SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null ); return names.toArray(new String [0 ]); } }
运行 main()
方法后,控制台打印出同样的结果。
SpringFactoriesLoader.loadFactoryNames()
不仅只扫描当前项目类型路径下的 META-INF/spring.factories
文件,而是会扫描包括 Jar 包里类路径下的 META-INF/spring.factories
文件。
针对 SpringBoot 来说,自动装配的 Bean 使用如下语句加载:
1 SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, null );
SpringBoot 2.7.0 及其以后版本的自动装配
在 SpringBoot 2.7.0 及其以后的版本中,SpringBoot 不再通过读取 META-INF/spring.factories
文件中 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration
的 values 来实现自动装配。
为了更贴合 SPI 机制,SpringBoot 将读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中的内容,该文件中每一行都表示需要自动装配的 Bean 的全限定类名,可以使用 #
作为注释。其加载方式使用:
1 ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader());
其中 AutoConfiguration
是一个注解,它的全限定类名为 org.springframework.boot.autoconfigure.AutoConfiguration
。
也就是说可以自定义一个注解,创建 META-INF/spring/full-qualified-annotation-name.imports
文件,在文件里声明需要自动装配的类:
1 2 3 4 5 6 package indi.mofan.a41;@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyAutoConfiguration {}
1 2 3 4 5 6 package indi.mofan.a41;public class A41 { static class Bean3 { } }
创建 META-INF/spring/indi.mofan.a41.MyAutoConfiguration.imports
文件:
1 indi.mofan.a41.A41$Bean3
修改 selectImports()
方法实现逻辑:
1 2 3 4 5 6 7 8 9 static class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { List<String> names = new ArrayList <>(SpringFactoriesLoader.loadFactoryNames(MyImportSelector.class, null )); ImportCandidates.load(MyAutoConfiguration.class, null ).forEach(names::add); return names.toArray(new String [0 ]); } }
运行 main()
方法后,Spring 容器中的 Bean 多了 一个:
indi.mofan.a41.A41$Bean3
定义了冲突的 Bean
第三方装配了 Bean1
:
1 2 3 4 5 6 static class AutoConfiguration1 { @Bean public Bean1 bean1 () { return new Bean1 ("第三方" ); } }
用户又自行定义了 Bean1
:
1 2 3 4 5 6 7 8 @Configuration @Import(MyImportSelector.class) static class Config { @Bean public Bean1 bean1 () { return new Bean1 ("本项目" ); } }
修改测试的 main()
方法:
1 2 3 4 5 6 public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>" ); System.out.println(context.getBean(Bean1.class)); }
最终谁会生效呢?
>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)
用户自行定义的 Bean 生效了,这是因为:@Import
导入的 Bean 先于配置类中 @Bean
定义的 Bean 执行,后者覆盖前者,使得用户自定义的 Bean 生效。
但在 SpringBoot 中不是这样的,当后续添加的 Bean 想覆盖先前添加的 Bean,会出现错误。模拟 SpringBoot 的设置:
1 2 3 4 5 6 7 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false ); }
Exception in thread "main" org.springframework.beans.factory.support.BeanDefinitionOverrideException : Invalid bean definition with name 'bean1' defined in indi.mofan.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.mofan.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.mofan.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [indi/mofan/a41/A41$AutoConfiguration1.class]] bound.
那这样是合理的吗?
显然不是。比如 SpringBoot 默认的数据连接池是 Hikari,如果用户想换成 Druid,岂不是做不到?
实际情况下是能做到的,这又是怎么做到的呢?
首先需要使用户的配置类中定义的 Bean 先于 @Import
导入的 Bean 添加到 Spring 容器中,只需将选择器 MyImportSelector
实现的 ImportSelector
接口更换成其子接口 DeferredImportSelector
即可:
1 2 3 static class MyImportSelector implements DeferredImportSelector { }
再次运行 main()
方法:
Exception in thread "main" org.springframework.beans.factory.support.BeanDefinitionOverrideException : Invalid bean definition with name 'bean1' defined in class path resource [indi/mofan/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.mofan.a41.A41$AutoConfiguration1; factoryMethodName=bean1; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [indi/mofan/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.mofan.a41.A41$Config] bound.
尽管还是出现了异常,但异常信息中显示的是在配置类定义的 Bean 已存在,第三方装配的 Bean 无法再添加,这表明 Bean 的添加顺序修改成功。
最后在第三方定义的 Bean 上添加 @ConditionalOnMissingBean
注解,表示容器中存在同名的 Bean 时忽略该 Bean 的添加:
1 2 3 4 5 6 7 static class AutoConfiguration1 { @Bean @ConditionalOnMissingBean public Bean1 bean1 () { return new Bean1 ("第三方" ); } }
再次运行 main()
方法,不再出现异常:
>>>>>>>>>>>>>>>>>>>>>>>>>>>
A41.Bean1(name=本项目)
41.2 Aop 自动配置
确保当前模块下已导入:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
使用 AopAutoConfiguration
自动装配与 AOP 相关的 Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class TestAopAuto { public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory()); context.registerBean(Config.class); context.refresh(); Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println); } @Configuration @Import(MyImportSelector.class) static class Config { } static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{AopAutoConfiguration.class.getName()}; } } }
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
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
:
1 2 3 4 5 @AutoConfiguration @ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true) public class AopAutoConfiguration { }
根据 @ConditionalOnProperty
注解配置的信息:如果配置文件中存在 前缀 为 spring.aop
,名称 为 auto
的 key,并且其对应的 value 是 true
时,配置类 AopAutoConfiguration
生效;如果配置文件中未显式配置,该配置类也生效。
不使用配置文件,使用 StandardEnvironment
指定 spring.aop.auto
的值为 false
:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); StandardEnvironment env = new StandardEnvironment (); env.getPropertySources().addLast( new SimpleCommandLinePropertySource ("--spring.aop.auto=false" ) ); context.setEnvironment(env); }
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
的内部类:
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Advice.class) static class AspectJAutoProxyingConfiguration { } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingClass("org.aspectj.weaver.Advice") @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true) static class ClassProxyingConfiguration { }
其内部存在两个类:AspectJAutoProxyingConfiguration
和 ClassProxyingConfiguration
。
使用了 @ConditionalOnClass
注解判断 Advice.class
存在时,AspectJAutoProxyingConfiguration
生效;使用 @ConditionalOnMissingClass
注解判断 org.aspectj.weaver.Advice
不存在时,ClassProxyingConfiguration
生效。
由于先前导入了 spring-boot-starter-aop
依赖,Advice.class
是存在的,AspectJAutoProxyingConfiguration
将生效。
AspectJAutoProxyingConfiguration
内部又有两个配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration(proxyBeanMethods = false) @EnableAspectJAutoProxy(proxyTargetClass = false) @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false") static class JdkDynamicAutoProxyConfiguration {} @Configuration(proxyBeanMethods = false) @EnableAspectJAutoProxy(proxyTargetClass = true) @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true) static class CglibAutoProxyConfiguration {}
这两个配置类通过使用 @ConditionalOnProperty
注解判断配置文件中是否存在 spring.aop.proxy-target-class
配置来让对应的配置类生效。
由于并未显式配置,因此 CglibAutoProxyConfiguration
将生效。
无论哪个配置类生效,它们都被 @EnableAspectJAutoProxy
标记,这个注解相当于是添加了些配置的 @Import
注解:
1 2 3 4 5 6 7 8 9 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({AspectJAutoProxyRegistrar.class}) public @interface EnableAspectJAutoProxy { boolean proxyTargetClass () default false ; boolean exposeProxy () default false ; }
向 Spring 容器中添加 AspectJAutoProxyRegistrar
类型的 Bean。
AspectJAutoProxyRegistrar
实现了 ImportBeanDefinitionRegistrar
接口,可以使用编程的方式来注册一些 Bean:
1 2 3 4 5 6 class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); } }
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary()
方法是注册 Bean 的主要逻辑:
1 2 3 4 5 6 7 8 9 @Nullable public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary (BeanDefinitionRegistry registry) { return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, (Object)null ); } @Nullable public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary (BeanDefinitionRegistry registry, @Nullable Object source) { return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); }
最终注册了 AnnotationAwareAspectJAutoProxyCreator
。
使用 org.springframework.aop.config.internalAutoProxyCreator
作为名称,获取 AnnotationAwareAspectJAutoProxyCreator
类型的 Bean,并查看其 proxyTargetClass
属性是否为 true
:
1 2 3 4 5 6 7 8 public static void main (String[] args) { System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>" ); AnnotationAwareAspectJAutoProxyCreator creator = context.getBean("org.springframework.aop.config.internalAutoProxyCreator" , AnnotationAwareAspectJAutoProxyCreator.class); System.out.println(creator.isProxyTargetClass()); }
【补充】ImportBeanDefinitionRegistrar
接口
将 Bean 注入到 Spring 的大致流程是:
利用 BeanDefinitionReader
读取配置文件或注解信息,为每一个 Bean 生成一个 BeanDefinition
将 BeanDefinition
注册到 BeanDefinitionRegistry
中
当需要创建 Bean 对象时,从 BeanDefinitionRegistry
中取出对应的 BeanDefinition
,利用这个 BeanDefinition
来创建 Bean
如果创建的 Bean 是单例的,Spring 会将这个 Bean 保存到 SingletonBeanRegistry
中,即三级缓存中的第一级缓存,需要时直接从这里获取,而不是重复创建
也就是说 Spring 是通过 BeanDefinition
去创建 Bean 的,而 BeanDefinition
会被注册到 BeanDefinitionRegistry
中,因此可以拿到 BeanDefinitionRegistry
直接向里面注册 BeanDefinition
达到将 Bean 注入到 Spring 的目标。
ImportBeanDefinitionRegistrar
接口就可以直接拿到 BeanDefinitionRegistry
:
1 2 3 4 5 6 7 8 public interface ImportBeanDefinitionRegistrar { default void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { this .registerBeanDefinitions(importingClassMetadata, registry); } default void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { } }
该接口需要搭配 @Import
注解使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); 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) { AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(User.class) .addPropertyValue("name" , "mofan" ) .addPropertyValue("age" , 20 ) .getBeanDefinition(); 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
:
1 2 3 public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor { void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry var1) throws BeansException; }
41.3 数据库相关的自动配置
确保当前模块下已导入:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.3.0</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > </dependency >
DataSource 自动配置
自行实现导入选择器,并使用 @Import
注解进行导入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration @Import(MyImportSelector.class) static class Config {} static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{ DataSourceAutoConfiguration.class.getName(), MybatisAutoConfiguration.class.getName(), DataSourceTransactionManagerAutoConfiguration.class.getName(), TransactionAutoConfiguration.class.getName() }; } }
在 main()
方法中打印导入的 Bean 信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); StandardEnvironment env = new StandardEnvironment (); env.getPropertySources().addLast(new SimpleCommandLinePropertySource ( "--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring" , "--spring.datasource.username=root" , "--spring.datasource.password=123456" )); context.setEnvironment(env); AnnotationConfigUtils.registerAnnotationConfigProcessors(context.getDefaultListableBeanFactory()); context.registerBean(Config.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { String resourceDescription = context.getBeanDefinition(name).getResourceDescription(); if (resourceDescription != null ) System.out.println(name + " 来源: " + resourceDescription); } }
未使用配置文件,而是使用 StandardEnvironment
设置了一些数据库连接信息。
最后只打印有明确来源的 Bean 信息,其中有一条:
dataSource 来源: class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]
名叫 dataSource
的 Bean 的来源为什么是 DataSourceConfiguration
,而不是 DataSourceAutoConfiguration
呢?
查看 DataSourceAutoConfiguration
的源码,实现与 AopAutoConfiguration
类似,都是通过注解来判断需要导入哪些 Bean,有两个关键的内部类 EmbeddedDatabaseConfiguration
和 PooledDataSourceConfiguration
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @AutoConfiguration(before = SqlInitializationAutoConfiguration.class) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") @EnableConfigurationProperties(DataSourceProperties.class) @Import(DataSourcePoolMetadataProvidersConfiguration.class) public class DataSourceAutoConfiguration { @Configuration(proxyBeanMethods = false) @Conditional(EmbeddedDatabaseCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import(EmbeddedDataSourceConfiguration.class) protected static class EmbeddedDatabaseConfiguration { } @Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration { } }
它们都被 @Conditional
注解标记。当项目支持内嵌数据源时,EmbeddedDatabaseConfiguration
生效;当项目支持基于数据库连接池的数据源时,PooledDataSourceConfiguration
生效。
SpringBoot 默认的数据库连接池是 Hikari
,因此 PooledDataSourceConfiguration
生效,最终使用 @Import
导入一系列 Bean,导入的这些 Bean 都是 DataSourceConfiguration
的内部类,因此dataSource
的 Bean 的来源是 DataSourceConfiguration
。
在 DataSourceConfiguration
中,通过 @ConditionalOnClass
注解判断某些 Class 是否存在来使某种数据库连接池生效。
由于导入了 mybatis-spring-boot-starter
,其内部依赖 mybatis-spring-boot-jdbc
,而它又依赖了 HikariCP
,因此最终数据库连接池 Hikari
生效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HikariDataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true) static class Hikari { @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") HikariDataSource dataSource (DataSourceProperties properties) { HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } }
在 Hikari#dataSource()
方法中,接受一个 DataSourceProperties
类型的参数,这要求 Spring 容器中存在 DataSourceProperties
类型的 Bean。
在最初的 DataSourceAutoConfiguration
自动配置类上有个 @EnableConfigurationProperties
注解,它将 DataSourceProperties
添加到容器中:
1 2 3 4 @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { }
在 DataSourceProperties
中会绑定配置文件中以 spring.datasource
为前缀的配置:
1 2 3 4 @ConfigurationProperties(prefix = "spring.datasource") public class DataSourceProperties implements BeanClassLoaderAware , InitializingBean { }
获取 DataSourceProperties
类型的 Bean,并打印其 url
、username
和 password
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) { GenericApplicationContext context = new GenericApplicationContext (); StandardEnvironment env = new StandardEnvironment (); env.getPropertySources().addLast(new SimpleCommandLinePropertySource ( "--spring.datasource.url=jdbc:mysql://localhost:3306/advanced_spring" , "--spring.datasource.username=root" , "--spring.datasource.password=123456" )); context.setEnvironment(env); DataSourceProperties properties = context.getBean(DataSourceProperties.class); System.out.println(properties.getUrl()); System.out.println(properties.getUsername()); System.out.println(properties.getPassword()); }
jdbc:mysql://localhost:3306/advanced_spring
root
123456
MyBatis 自动配置
接下来看看 MyBatis 的自动配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Configuration @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties({MybatisProperties.class}) @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class}) public class MybatisAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception { } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate (SqlSessionFactory sqlSessionFactory) { } @Configuration @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class}) @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { } }
MybatisAutoConfiguration
生效的条件有两个:
类路径下存在 SqlSessionFactory
和 SqlSessionFactoryBean
Spring 容器中有且仅有一个 DataSource
类型的 Bean
它还添加了 MybatisProperties
类型的 Bean 到 Spring 容器中,并与配置文件中以 mybatis
为前缀的信息绑定。
@AutoConfigureAfter
注解指定了当前自动配置类在 DataSourceAutoConfiguration
和 MybatisLanguageDriverAutoConfiguration
两个自动配置类解析完成之后再解析。
接下来遇到 sqlSessionFactory()
方法:
1 2 3 4 5 @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory (DataSource dataSource) throws Exception { }
依赖 Spring 容器中的 DataSource
,当容器中不存在 SqlSessionFactory
时,将其添加到 Spring 容器中。
然后是 sqlSessionTemplate()
方法,它与添加 SqlSessionFactory
到 Spring 容器的逻辑一样:
1 2 3 4 5 @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate (SqlSessionFactory sqlSessionFactory) { }
SqlSessionTemplate
也是 SqlSession
的实现,提供了与当前线程绑定的 SqlSession
。针对多个方法调用,如果它们来自同一个线程,那么获取到的 SqlSession
对象是同一个。这也是为什么有了 DefaultSqlSession
作为 SqlSession
的实现了,还需要 SqlSessionTemplate
。
在 MyBatis 中,使用 MapperFactoryBean
将接口转换为对象,其核心是 getObject()
方法:
1 2 3 public T getObject () throws Exception { return this .getSqlSession().getMapper(this .mapperInterface); }
方法中获取了 sqlSession
对象,而获取的就是 SqlSessionTemplate
对象:
1 2 3 public SqlSession getSqlSession () { return this .sqlSessionTemplate; }
最后来到 MapperScannerRegistrarNotFoundConfiguration
内部类:
1 2 3 4 5 6 @Configuration @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class}) @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class}) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { }
利用 @ConditionalOnMissingBean
判断 Spring 容器中缺失 MapperFactoryBean
和 MapperScannerConfigurer
时,该配置类生效。生效时利用 @Import
导入 AutoConfiguredMapperScannerRegistrar
:
1 2 3 public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware , EnvironmentAware, ImportBeanDefinitionRegistrar { }
AutoConfiguredMapperScannerRegistrar
实现了 ImportBeanDefinitionRegistrar
接口,允许通过编程的方式加 Bean 添加到 Spring 容器中,而这里是去扫描 Mapper 接口,将其转换为对象添加到 Spring 容器中。
在 main()
所在类的包路径下创建 mapper
包,并新建三个接口,其中两个被 @Mapper
注解标记:
1 2 3 4 5 6 7 8 9 10 @Mapper public interface Mapper1 {} @Mapper public interface Mapper2 {} public interface Mapper3 {}
运行 main()
方法,查看 Mapper1
和 Mapper2
是否被添加到 Spring 容器中。
结果是否定的。因为 没有设置要扫描的包路径 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) { 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); } }
当前包名: 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
标记的接口。
事务自动配置
事务自动配置与 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 的信息、来源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class TestMvcAuto { public static void main (String[] args) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext (); context.registerBean(Config.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { String source = context.getBeanDefinition(name).getResourceDescription(); if (source != null ) { System.out.println(name + " 来源:" + source); } } context.close(); } @Configuration @Import(MyImportSelector.class) static class Config { } static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{ ServletWebServerFactoryAutoConfiguration.class.getName(), DispatcherServletAutoConfiguration.class.getName(), WebMvcAutoConfiguration.class.getName(), 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. 条件装配底层
42.1 @Conditional
在 SpringBoot 的自动配置中,经常看到 @Conditional
注解的使用,使用该注解可以按条件加载配置类。
@Conditional
注解并不具备条件判断功能,而是通过指定的 Class
列表来进行判断,指定的 Class
需要实现 Condition
接口。
假设有这样一个需求:通过判断类路径下是否存在 com.alibaba.druid.pool.DruidDataSource
类来加载不同的配置类,当存在 DruidDataSource
时,加载 AutoConfiguration1
,反之加载 AutoConfiguration2
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public static void main (String[] args) throws IOException { GenericApplicationContext context = new GenericApplicationContext (); context.registerBean("config" , Config.class); context.registerBean(ConfigurationClassPostProcessor.class); context.refresh(); for (String name : context.getBeanDefinitionNames()) { System.out.println(name); } } @Configuration @Import(MyImportSelector.class) static class Config {} static class MyImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String []{AutoConfiguration1.class.getName(), AutoConfiguration2.class.getName()}; } } static class MyCondition1 implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { return ClassUtils.isPresent("com.alibaba.druid.pool.DruidDataSource" , null ); } } static class MyCondition2 implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { 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
依赖:
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.15</version > </dependency >
再次运行 main()
方法:
config
org.springframework.context.annotation.ConfigurationClassPostProcessor
indi.mofan.a42.A42$AutoConfiguration1
bean1
42.2 @ConditionalOnXxx
在 SpringBoot 的自动配置中,经常看到 @ConditionalOnXxx
注解的使用,这种注解是将某个 @Conditional
的判断进行了封装,比如 ConditionalOnClass
就是用于判断某个 Class
是否存在。
因此针对上文中的代码可以做出修改:
自定义 @ConditionalOnClass
注解,填入需要判断的全限定类名和判断条件;
移除模拟的第三方配置上的 @Conditional
注解,而是使用自定义的 @ConditionalOnClass
;
Condition
接口的使用类重写的 matches()
方法利用 @ConditionalOnClass
注解进行条件判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 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 { 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】一样。