深入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()方法只简单输出一行日志,代码如下。

public interface HelloService {
    void sayHello();
}
public class HelloServiceImpl implements HelloService {
    private static final Logger LOGGER = LoggerFactory.getLogger(HelloServiceImpl.class);
    @Override
    public void sayHello() {
        LOGGER.info("hello");
    }
}

新建自动配置类CustomAutoConfigure

自动配置类CustomAutoConfigure用于实例化一个HelloServiceImpl类的对象,创建bean实例时会输出初始化日志,具体代码如下。

@Configuration
@ConditionalOnClass(HelloServiceImpl.class)
public class CustomAutoConfigure {
    private static final Logger LOGGER = LoggerFactory.getLogger(CustomAutoConfigure.class);
    @Bean
    @ConditionalOnMissingBean
    public HelloService helloService() {
        LOGGER.info("Init helloService");
        return new HelloServiceImpl();
    }
}

打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
public class CustomStarterTest {
    @Autowired
    private HelloService helloService;
    @Test
    public void test(){
        helloService.sayHello();
    }
}

从上图的执行结果可以看到,单元测试类启动的时候会通过CustomAutoConfigure实例化一个HelloServiceImpl对象。

往期推荐

深入Spring Boot (十五):web.xml去哪了

深入Spring Boot (十四):jar/war打包解决方案

Java开发人员必知的常用类库,这些你都知道吗?

聊一聊Redis官方置顶推荐的Java客户端Redisson

我画了25张图展示线程池工作原理和实现原理,原创干货,建议先收藏再阅读

Spring框架你敢写精通,面试官就敢问@Autowired注解的实现原理

面试被问为什么使用Spring Boot?答案好像没那么简单

Spring声明式事务处理的实现原理,来自面试官的穷追拷问

没使用加号拼接字符串,面试官竟然问我为什么

面试官一步一步的套路你,为什么SimpleDateFormat不是线程安全的

都说ThreadLocal被面试官问烂了,可为什么面试官还是喜欢继续问

Java注解是如何玩转的,面试官和我聊了半个小时

学之多,而后知之少!朋友们点【在看】是我持续更新的最大动力!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值