赞
踩
最近在项目中使用Druid时,从SpringBoot 1.5.11升级到2.2.13后,通过监控发现连接池属性为默认值,自定义属性值未生效,今天一起探究下Druid连接池自动装配过程。
我们在项目中对于系统配置参数一般采用 “动静分离” 模式:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
druid:
initial-size: 1
max-wait: 20000
max-active: 20
min-idle: 1
spring.datasource.uap.url=jdbc:mysql://localhost:3306/nacos
spring.datasource.uap.username=root
spring.datasource.uap.password=12345678
@Configuration
public class DruidDataSourceConfig {
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties(prefix = "spring.datasource.uap")
public DataSource druidDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
以上代码在SpringBoot 1.5.11版本运行正常,在SpringBoot2.2.13版本中连接池属性未生效。
@ConfigurationProperties("spring.datasource.druid")
public class DruidDataSourceWrapper extends DruidDataSource implements InitializingBean {
@Autowired
private DataSourceProperties basicProperties;
@Override
public void afterPropertiesSet() throws Exception {
//if not found prefix 'spring.datasource.druid' jdbc properties ,'spring.datasource' prefix jdbc properties will be used.
if (super.getUsername() == null) {
super.setUsername(basicProperties.determineUsername());
}
if (super.getPassword() == null) {
super.setPassword(basicProperties.determinePassword());
}
if (super.getUrl() == null) {
super.setUrl(basicProperties.determineUrl());
}
if (super.getDriverClassName() == null) {
super.setDriverClassName(basicProperties.getDriverClassName());
}
}
}
可以发现:
DruidDataSourceWrapper
继承于DruidDataSource
,且实现了InitializingBean
接口DruidDataSourceWrapper
使用了@ConfigurationProperties("spring.datasource.druid")
注解DruidDataSource属性来源于四部分:
DataSourceProperties源码:
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
/**
* Whether to generate a random datasource name.
*/
private boolean generateUniqueName = true;
/**
* Datasource name to use if "generate-unique-name" is false. Defaults to "testdb"
* when using an embedded database, otherwise null.
*/
private String name;
/**
* Fully qualified name of the connection pool implementation to use. By default, it
* is auto-detected from the classpath.
*/
private Class<? extends DataSource> type;
/**
* Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
*/
private String driverClassName;
/**
* JDBC URL of the database.
*/
private String url;
/**
* Login username of the database.
*/
private String username;
/**
* Login password of the database.
*/
private String password;
参见:
SpringBean初始化源码:
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
//核心 - 调用Bean的各种XxxxAware接口的方法,进行相关属性填充
invokeAwareMethods(beanName, bean);
//核心 - 调用BeanPostProcessor的前置处理方法
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
//核心 - 调用Bean的初始化方法(init-method或者@PostConstruct注解标记的方法)
try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
//核心 - 调用BeanPostProcessor的后置处理方法
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
BeanPostProcessor的处理方法:
在Bean初始化前,调用所有BeanPostProcessors
的postProcessBeforeInitialization
方法,在refresh()
中会进行BeanPostProcessors
的注册,即registerBeanPostProcessors
,而BeanPostProcessors
中有2个重要的方法:
public interface BeanPostProcessor {
/**
* Apply this BeanPostProcessor to the given new bean instance <i>before</i> any bean
* initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
* or a custom init-method). The bean will already be populated with property values.
*/
//实例化、依赖注入完毕,在调用显示的初始化之前完成一些定制的初始化任务
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
/**
* Apply this BeanPostProcessor to the given new bean instance <i>after</i> any bean
* initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
* or a custom init-method). The bean will already be populated with property values.
*/
//实例化、依赖注入、初始化完毕时执行
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
基于@ConfigurationProperties
注解的属性注入类实际就发生在SpringBean初始化过程中,调用的是BeanPostProcessor
的前置处理方法,ConfigurationPropertiesBindingPostProcessor
是一个BeanPostProcessor
,它通常被框架添加到容器,用于解析bean组件上的注解@ConfigurationProperties
,将属性源中的属性设置到bean组件。
该方法的实现如下 (SpringBoot 2.2.13):
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
return bean;
}
上面的方法主要做两件事情 :
我们点开ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName),发现这样一个方法:
public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) {
Method factoryMethod = findFactoryMethod(applicationContext, beanName);
return create(beanName, bean, bean.getClass(), factoryMethod);
}
我们不难看出ConfigurationPropertiesBean.get(ApplicationContext applicationContext, Object bean, String beanName)
是用来返回一个ConfigurationPropertiesBean
,也就是我们定义的配置类的Bean,既可以是直接注解在配置类本身的也可以是注解在@Bean方法上的(其实就是上文提到的@ConfigurationProperties的两种用法)。当然它也可以返回一个null,如果它没用@ConfigurationProperties注解。
private void bind(ConfigurationPropertiesBean bean) {
......
......
try {
// 使用配置属性绑定工具 configurationPropertiesBinder 进行属性绑定,
// 目标bean是bean,已经被包装成 target
this.binder.bind(target);
}
catch (Exception ex) {
throw new ConfigurationPropertiesBindException(bean, ex);
}
}
从以上分析可以看出,ConfigurationPropertiesBindingPostProcessor主要负责以下任务 :
ConfigurationPropertiesBinder
;@ConfigurationProperties
的bean组件,使用配置属性绑定工具ConfigurationPropertiesBinder
对它们进行配置属性绑定。注意 : 配置属性的绑定细节由ConfigurationPropertiesBinder负责,而不是由ConfigurationPropertiesBindingPostProcessor负责。
1.5.11版本源码:
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// 获取当前正在创建的bean上的注解属性@ConfigurationProperties
ConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);
// 如果当前bean使用了注解 @ConfigurationProperties ,则进行配置属性绑定,如果没有,则直接跳过配置属性绑定
if (annotation != null) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
// 获取当前正在创建的beanName的注解属性@ConfigurationProperties
annotation = this.beans.findFactoryAnnotation(beanName, ConfigurationProperties.class);
// 如果当前bean使用了注解 @ConfigurationProperties ,则进行配置属性绑定,如果没有,则直接跳过配置属性绑定
if (annotation != null) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
return bean;
}
基于以上代码分析,发现:
版本 | 差异 |
---|---|
SpringBoot 1.5.11 | 既扫描BeanClass上的注解,又扫描BeanName的注解 (配置类) |
SpringBoot 2.2.13 | 扫描BeanName或BeanClass的注解,以BeanName (配置类) 优先 |
这就可以得出以下结论:
@ConfigurationProperties("spring.datasource.druid")
注解未注入连接池,导致连接池的属性为默认值;@ConfigurationProperties("spring.datasource.uap")
注解仍然有效① 修改配置类
@Configuration
public class DruidDataSourceConfig {
@Primary
@Bean(name = "dataSource")
// @ConfigurationProperties(prefix = "spring.datasource.uap") 删除该行注解
public DataSource druidDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
② 修改配置文件application.yaml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
druid:
url: jdbc:mysql://localhost:3306/nacos # 新增该行
username: root # 新增该行
password: 12345678 # 新增该行
initial-size: 1
max-wait: 20000
max-active: 20
min-idle: 1
datasource.properties
配置文件可以删除了。
① 修改配置文件datasource.properties
spring.datasource.uap.url=jdbc:mysql://localhost:3306/nacos
spring.datasource.uap.username=root
spring.datasource.uap.password=12345678
spring.datasource.uap.initial-size=1
spring.datasource.uap.max-wait=20000
spring.datasource.uap.max-active=20
spring.datasource.uap.min-idle=1
② 修改配置文件application.yaml
删除spring.datasource.druid下的连接池属性,stat、filter等配置可以保留。
① 修改配置类
@Configuration
public class DruidDataSourceConfig {
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties(prefix = "spring.datasource") // 删除.uap
public DataSource druidDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
② 修改配置文件application.yaml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/nacos # 新增该行
username: root # 新增该行
password: 12345678 # 新增该行
druid:
initial-size: 1
max-wait: 20000
max-active: 20
min-idle: 1
datasource.properties
配置文件可以删除了。
① 可以添加进VM参数中,比如-Ddruid.username=czce
② 可以通过代码设置属性值
属性列表如下:
druid.url
druid.username
druid.password
......
详情请查看DruidDataSource#configFormProperty
方法
比较简单,不再赘述。
① 底层开源框架升级时,需明确当前版本到目标版本的差异点,并逐项分析对当前项目的影响;
② 当升级到新版本时,有些属性、配置等可能已经被重命名或删除了,通过添加spring-boot-properties-migrator
依赖,能在工程启动过程中,在控制台打印出一些环境配置相关信息,来帮助我们做升级适配。切记修复完升级后的各种问题,记得将spring-boot-properties-migrator
依赖项从工程中删除;
③ 公共基础设施依赖,比如Redis、ES等,建议进行二次封装,将第三方库的影响封装在技术组件内部,由统一技术组进行维护适配,降低对应用的影响;
④ 自动化测试的重要性,依赖于人工分析排查仅能解决一部分问题,如果项目中有完备的自动化测试用例,那么框架升级会省时又省心;
⑤ 无论是版本升级,还是其它故障导致的BUG,我们在了解源码的基础上,只需耐心且细心的去DEBUG,就能解决百分之八十的问题,而且对个人而言,也是从“cv程序员”往资深程序员的一种能力成长,不要害怕问题,要敢于直面问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。