当前位置:   article > 正文

【经验篇】Druid连接池配置失效问题解析_为什么配置druid连接池,为什么没有启用

为什么配置druid连接池,为什么没有启用

背景

最近在项目中使用Druid时,从SpringBoot 1.5.11升级到2.2.13后,通过监控发现连接池属性为默认值,自定义属性值未生效,今天一起探究下Druid连接池自动装配过程。

示例代码

我们在项目中对于系统配置参数一般采用 “动静分离” 模式:

  • 静态参数:即启动配置文件application.yml中配置的是静态参数,一般不随环境【Dev、Test、Prod】而发生改变
  • 动态参数:该类参数随部署运行环境而变化,一般采用额外properties文件进行配置,SpringBoot应用启动时,进行加载、解析、存入Envionment中。

application.yml配置

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

datasource.properties配置

spring.datasource.uap.url=jdbc:mysql://localhost:3306/nacos
spring.datasource.uap.username=root
spring.datasource.uap.password=12345678
  • 1
  • 2
  • 3

配置类

@Configuration
public class DruidDataSourceConfig {

    @Primary
    @Bean(name = "dataSource")
    @ConfigurationProperties(prefix = "spring.datasource.uap")
    public DataSource druidDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

以上代码在SpringBoot 1.5.11版本运行正常,在SpringBoot2.2.13版本中连接池属性未生效。

源码分析

① 先看一下DruidDataSourceWrapper源码

@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());
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

可以发现:

  • DruidDataSourceWrapper继承于DruidDataSource,且实现了InitializingBean接口
  • DruidDataSourceWrapper使用了@ConfigurationProperties("spring.datasource.druid")注解

② DruidDataSource属性来源

DruidDataSource属性来源于四部分:

  • System.getPropertis();
  • DruidDataSourceConfig配置类druidDataSource();
  • 基于注解的DruidDataSourceWrapper属性注入;
  • InitializingBean的afterPropertiesSet()方法,使用了DataSourceProperties配置类

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;
  • 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

③ Spring Bean的初始化过程

在这里插入图片描述
参见:

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

BeanPostProcessor的处理方法:
在Bean初始化前,调用所有BeanPostProcessorspostProcessBeforeInitialization方法,在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;  
  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

④ ConfigurationProperties属性绑定过程

基于@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;
}
  • 1
  • 2
  • 3
  • 4
  • 5

上面的方法主要做两件事情 :

4.1 获取bean注解@ConfigurationProperties属性

我们点开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);
}
  • 1
  • 2
  • 3
  • 4

我们不难看出ConfigurationPropertiesBean.get(ApplicationContext applicationContext, Object bean, String beanName)是用来返回一个ConfigurationPropertiesBean,也就是我们定义的配置类的Bean,既可以是直接注解在配置类本身的也可以是注解在@Bean方法上的(其实就是上文提到的@ConfigurationProperties的两种用法)。当然它也可以返回一个null,如果它没用@ConfigurationProperties注解。

4.2 执行配置属性绑定

private void bind(ConfigurationPropertiesBean bean) {
	......
	......
	try {
          // 使用配置属性绑定工具 configurationPropertiesBinder 进行属性绑定,
          // 目标bean是bean,已经被包装成 target           
		this.binder.bind(target);
	}
	catch (Exception ex) {
		throw new ConfigurationPropertiesBindException(bean, ex);
	}
}   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

从以上分析可以看出,ConfigurationPropertiesBindingPostProcessor主要负责以下任务 :

  • 准备配置属性绑定工具ConfigurationPropertiesBinder;
  • 过滤容器中每个带有注解@ConfigurationProperties的bean组件,使用配置属性绑定工具ConfigurationPropertiesBinder对它们进行配置属性绑定。

注意 : 配置属性的绑定细节由ConfigurationPropertiesBinder负责,而不是由ConfigurationPropertiesBindingPostProcessor负责。

⑤ SpringBoot版本差异

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

基于以上代码分析,发现:

版本差异
SpringBoot 1.5.11既扫描BeanClass上的注解,又扫描BeanName的注解 (配置类)
SpringBoot 2.2.13扫描BeanName或BeanClass的注解,以BeanName (配置类) 优先

这就可以得出以下结论:

  • 在SpringBoot 2.2.13版本中,由于不再扫描BeanClass上的注解,所以导致@ConfigurationProperties("spring.datasource.druid")注解未注入连接池,导致连接池的属性为默认值;
  • 配置类的@ConfigurationProperties("spring.datasource.uap")注解仍然有效
  • InitializingBean的afterPropertiesSet()方法中使用DataSourceProperties的几个属性也同样有效

解决方案

方案一:使spring.datasource.druid注解生效

① 修改配置类

@Configuration
public class DruidDataSourceConfig {

    @Primary
    @Bean(name = "dataSource")
    // @ConfigurationProperties(prefix = "spring.datasource.uap")  删除该行注解
    public DataSource druidDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

② 修改配置文件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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

datasource.properties配置文件可以删除了。

方案二:使spring.datasource.uap注解生效

① 修改配置文件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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

② 修改配置文件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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

② 修改配置文件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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

datasource.properties配置文件可以删除了。

方案四:使用System.Properties【不推荐】

① 可以添加进VM参数中,比如-Ddruid.username=czce
② 可以通过代码设置属性值
属性列表如下:

druid.url
druid.username
druid.password
......
  • 1
  • 2
  • 3
  • 4

详情请查看DruidDataSource#configFormProperty方法

方案五:自定义代码实现数据源创建

比较简单,不再赘述。

总结

① 底层开源框架升级时,需明确当前版本到目标版本的差异点,并逐项分析对当前项目的影响;
② 当升级到新版本时,有些属性、配置等可能已经被重命名或删除了,通过添加spring-boot-properties-migrator依赖,能在工程启动过程中,在控制台打印出一些环境配置相关信息,来帮助我们做升级适配。切记修复完升级后的各种问题,记得将spring-boot-properties-migrator依赖项从工程中删除;
③ 公共基础设施依赖,比如Redis、ES等,建议进行二次封装,将第三方库的影响封装在技术组件内部,由统一技术组进行维护适配,降低对应用的影响;
④ 自动化测试的重要性,依赖于人工分析排查仅能解决一部分问题,如果项目中有完备的自动化测试用例,那么框架升级会省时又省心;
⑤ 无论是版本升级,还是其它故障导致的BUG,我们在了解源码的基础上,只需耐心且细心的去DEBUG,就能解决百分之八十的问题,而且对个人而言,也是从“cv程序员”往资深程序员的一种能力成长,不要害怕问题,要敢于直面问题。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/336934
推荐阅读
相关标签
  

闽ICP备14008679号