深入Spring Boot (十六):从源码分析自动配置原理
切换Druid连接池
在分析SpringBoot自动配置实现原理之前,先来看一下在使用SpringBoot开发的项目代码中如何将数据库连接池切换成Druid。
对于数据库连接池的选择,SpringBoot官方更偏向于推荐使用HikariCP,原因是他们认为HikariCP的性能和并发性比较好,如果当前代码的classpath路径下存在HikariCP的jar包,则会优先使用HikariCP数据库连接池;如果当前代码的classpath路径下不存在HikariCP的jar包,存在Tomcat数据库连接池的jar包,则会使用Tomcat数据库连接池;如果HikariCP的jar包和Tomcat数据库连接池的jar包都不存在,存在Commons DBCP2的jar包,则会使用DBCP2数据库连接池;如果上述三种数据库连接池的jar包都不存在,而Oracle UCP(Oracle Universal Connection Pool)相关jar包存在,则使用Oracle UCP数据库连接池。
既然,SpringBoot对数据库连接池的选择是使用上面的算法,是动态选择的,那为什么本文最开始说的是“如何将数据库连接池切换成Druid”呢?那是因为如果你的依赖管理中使用到了spring-boot-starter-jdbc或spring-boot-starter-data-jpa这两个starters,依赖列表中会自动依赖HikariCP,也就是说,此时默认使用的是HikariCP数据库连接池。
关于数据库连接池的性能和并发性,本文不做阐述,连接池的选择仁者见仁智者见智。接下来,我们看一下如何将默认的HikariCP切换成Druid,完整示例代码地址:https://github.com/wind7rui/SpringBoot2.x-example/tree/main/DataSource-Druid。
排除HikariCP
首先,删除依赖管理中的HikariCP依赖、排除依赖管理中的HikariCP传递依赖,spring-boot-starter-jdbc或spring-boot-starter-data-jpa这两个starters依赖中会传递依赖HikariCP,需要排除,以下以Maven构建管理工具为例。
添加Druid依赖
添加Druid的jar包依赖,使用druid的starters:druid-spring-boot-starter。
配置Druid连接池参数
在application.properties中添加Druid数据库连接池参数配置,以下为示例配置。
完成以上步骤即完成了Druid连接池的切换,代码运行时就可以使用Druid数据库连接池了,是不是很简单!
自动配置原理
上述的示例通过简单的操作即完成了Druid连接池的切换,这其中就用到了SpringBoot的自动配置特性,官方说自动配置是聪明且智能的,下面我们一起来看一下这个聪明且智能的自动配置是如何实现的。
基于SpringBoot开发的代码一般都会有一个包含main()方法的应用启动类,并且会使用@SpringBootApplication注解标注在这个类上,例如如下代码。
分析自动配置原理的入口就从SpringApplication.run()开始,接下来的源码分析以重点代码为主,对于不重要的代码忽略分析。SpringApplication是SpringBoot提供用于通过Java main方法的方式启动Spring应用的启动类。进入SpringApplication类的run()方法,具体代码如下。
上面的代码我们重点关注refreshContext(context),这个方法的执行会进入Spring应用上下文里bean解析和bean对象的创建的方法,即AbstractApplicationContext中的refresh()方法,代码如下。
看过Spring源码的对这个方法应该不陌生,这个方法是Spring Ioc容器启动时的核心方法,主要用于bean的解析、实例化、初始化、依赖注入、激活BeanFactory处理器、注册BeanPostProcessors等,这里我们重点关注invokeBeanFactoryPostProcessors(beanFactory)方法,这个方法用于激活各种BeanFactory处理器,即激活BeanFactoryPostProcessor接口的实现类,BeanFactoryPostProcessor可以在Spring Ioc容器实例化任何其它bean时读取bean的元数据和修改元数据,我们所熟知的PropertyPlaceholderConfigurer就是基于BeanFactoryPostProcessor接口实现的。invokeBeanFactoryPostProcessors(beanFactory)的执行会进入如下代码。
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法会执行所有的BeanFactoryPostProcessors,这个方法内容很长,我们重点关注invokeBeanFactoryPostProcessors方法中调用的invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup())代码,具体代码如下。
在这个for循环执行的时候会遍历执行ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()方法,ConfigurationClassPostProcessor类实现了BeanFactoryPostProcessor接口,用于解析@Configuration注解标记的类,ConfigurationClassPostProcessor类的postProcessBeanDefinitionRegistry方法具体代码如下。
上图中的代码,重点关注最后一行processConfigBeanDefinitions(registry),这个方法执行的时候会通过ConfigurationClassParser的parse方法解析@Configuration注解标记的类,具体代码如下。
这里我们重点关注最后一行代码,deferredImportSelectorHandler.process()方法的执行会进入如下代码。
上图的代码重点关注handler.processGroupImports(),这个方法的执行会解析@Configuration注解标记的类上的@Import注解,解析的过程中会调用@Import注解中value属性值类的process方法。
分析到这里,我们先回到应用启动类BootApplication,这个类被@SpringBootApplication注解标注,我们看一下这个注解的源码。
它被@SpringBootConfiguration注解和@EnableAutoConfiguration注解标注,而@SpringBootConfiguration被@Configuration注解标注,@EnableAutoConfiguration被@Import注解标注,同时指定了@Import注解的value=AutoConfigurationImportSelector.class。
我们继续分析源码,此时我们可以看到前面对@Configuration注解标注的类进行解析的操作,其实就是对BootApplication,对@Import注解的解析就是对BootApplication上的@EnableAutoConfiguration中的@Import,所以,handler.processGroupImports()方法的执行最终会执行到AutoConfigurationImportSelector类中的AutoConfigurationGroup的process方法,具体代码如下。
上面的代码重点关注getAutoConfigurationEntry的执行,这里会调用AutoConfigurationImportSelector类的getAutoConfigurationEntry方法,具体代码如下。
getAutoConfigurationEntry方法的执行,最终会搜索类路径下所有jar包中META-INF/spring.factories文件中的所有EnableAutoConfiguration指定的类,这是什么意思呢,看下图就知道了。
从上图可以看到,SpringBoot的jar包中已经预设好了一些自动配置的类,这些自动配置的类会被getAutoConfigurationEntry方法检索到,返回一个自动配置类的列表,后续的流程将这些自动配置类解析成BeanDefinition,通过AbstractApplicationContext类refresh()方法中的finishBeanFactoryInitialization(beanFactory)完成自动配置类的实例化和初始化。那这些自动配置类都做了什么呢?我们以DispatcherServletAutoConfiguration为例,看一下它都自动帮我们做了什么,部分代码如下。
通过上图中的代码解释,可以看到DispatcherServletAutoConfiguration会自动实例化一个dispatcherServlet,但是必须满足一定的条件,如当前是web应用、存在spring-webmvc的jar包、当前Spring应用上下文中不存在DispatcherServlet实例、存在servlet的jar包等,自动配置类只有在被满足条件的情况下才可以被触发,执行一些bean的实例化操作,代替一些我们经常通过代码或配置实现的初始化或实例化bean的操作。
既然,SpringBoot自动配置是这样实现的,那Druid连接池的自动配置是不是这样的呢?我们打开druid-spring-boot-starter jar包META-INF路径下spring.factories文件看一下就明白了。
通过上图可以看到,Druid的自动配置也是基于上述的原理实现的。SpringBoot提供了自动配置可扩展的口子,开发人员只要在jar包中META-INF/spring.factories文件中使用org.springframework.boot.autoconfigure.EnableAutoConfiguration作为key指定自定义的自动配置类,在SpringBoot应用启动时会自动触发自定义自动配置类的自动配置操作。
最后,对SpringBoot的自动配置原理做一个小结。使用SpringBoot开发的代码在运行时会搜索类路径下所有jar包中META-INF/spring.factories文件中所有以EnableAutoConfiguration为key指定的自动配置类,执行自动配置类的实例化和初始化,这些自动配置类是否会被实例化,需要满足一定条件,例如当前类路径下是否含有相应类的jar包等,满足条件则执行自动配置类中的一些实例化操作。
自定义一个starter
通过上面的分析,我们已经知道了SpringBoot中自动配置是如何玩转的,我们按照这个套路也来实现一个简单的starter:custom-starter,完整示例代码地址:https://github.com/wind7rui/SpringBoot2.x-example/tree/main/Custom-Starter。
创建custom-starter项目
新建项目custom-starter,在pom.xml中添加spring-boot-autoconfigure依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.4.0</version>
</dependency>
新增spring.factories文件
在代码的resources目录下新建META-INF目录,然后在这个目录下新建spring.factories文件,文件内容以EnableAutoConfiguration为key指定的自动配置类。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.example.custom.spring.boot.autoconfigure.CustomAutoConfigure
新建Service
创建HelloService接口和接口的实现类HelloServiceImpl,实现类的sayHello()方法只简单输出一行日志,代码如下。
publicinterfaceHelloService{
void sayHello();
}
publicclassHelloServiceImplimplementsHelloService{
privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(HelloServiceImpl.class);
@Override
publicvoid sayHello(){
LOGGER.info("hello");
}
}
新建自动配置类CustomAutoConfigure
自动配置类CustomAutoConfigure用于实例化一个HelloServiceImpl类的对象,创建bean实例时会输出初始化日志,具体代码如下。
@Configuration
@ConditionalOnClass(HelloServiceImpl.class)
publicclassCustomAutoConfigure{
privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(CustomAutoConfigure.class);
@Bean
@ConditionalOnMissingBean
publicHelloService helloService(){
LOGGER.info("Init helloService");
returnnewHelloServiceImpl();
}
}
打Starter jar包
将当前项目代码打包成jar包。
引入自定义custom-starter
在使用的项目代码的pom.xml中添加custom-starter jar包依赖。
<dependency>
<groupId>org.example</groupId>
<artifactId>custom-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
验证custom-starter
编写单元测试类CustomStarterTest.java,验证Starter是否可以正常使用,具体代码如下。
@RunWith(SpringRunner.class)
@SpringBootTest
publicclassCustomStarterTest{
@Autowired
privateHelloService helloService;
@Test
publicvoid test(){
helloService.sayHello();
}
}
从上图的执行结果可以看到,单元测试类启动的时候会通过CustomAutoConfigure实例化一个HelloServiceImpl对象。
往期推荐:
深入Spring Boot (十四):jar/war打包解决方案
聊一聊Redis官方置顶推荐的Java客户端Redisson
我画了25张图展示线程池工作原理和实现原理,原创干货,建议先收藏再阅读
Spring框架你敢写精通,面试官就敢问@Autowired注解的实现原理
面试被问为什么使用Spring Boot?答案好像没那么简单
面试官一步一步的套路你,为什么SimpleDateFormat不是线程安全的
都说ThreadLocal被面试官问烂了,可为什么面试官还是喜欢继续问
学之多,而后知之少!朋友们点【在看】是我持续更新的最大动力!