Druid过滤器动态注册

1. 目标

使用 Druid 过滤器,可以实现对 Java 应用的数据库操作进行一些灵活的控制,为了便于使用,可能需要对其进行动态注册,以下进行分析

相关内容:

MyBatis拦截器动态注册

2. Druid 过滤器动态注册

2.1. 未生效的注册方式

通过以下方式动态注册 Druid 过滤器,之后执行数据库操作时,发现注册的 Druid 过滤器(TestDruidFilter)处理获取数据库连接方法 dataSource_getConnection() 及其他方法未被执行

  • initialSize

在定义 Druid 数据源时,将初始的连接数大小 initialSize 设置为大于 0 的值

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="initialSize" value="10"/>
</bean>
  • proxyFilters

在 Spring XML 中通过 filters 或 proxyFilters 属性为 Druid 数据源注册 Druid 过滤器

<bean id="testDruidFilter2" class="xxx.TestDruidFilter2"/>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!--        <property name="filters" value="stat,log4j2"/>    -->
    <property name="proxyFilters">
        <list>
            <ref bean="testDruidFilter2"/>
        </list>
    </property>
</bean>
  • 动态注册 Druid 过滤器

在 Bean(TestDruidFilterRegister1)中通过构造函数注入 Druid 数据源,通过 Druid 数据源的 setProxyFilters() 方法添加 Druid 过滤器(TestDruidFilter),如下所示:

@Service
@Lazy(false)
public class TestDruidFilterRegister1 {

    public TestDruidFilterRegister1(Map<String, DruidDataSource> druidDataSourceMap) {
        TestDruidFilter testDruidFilter = new TestDruidFilter();
        for (Map.Entry<String, DruidDataSource> entry : druidDataSourceMap.entrySet()) {
            DruidDataSource druidDataSource = entry.getValue();
            druidDataSource.setProxyFilters(Collections.singletonList(testDruidFilter));
        }
    }
}

public class TestDruidFilter extends FilterEventAdapter {
    @Override
    public DruidPooledConnection dataSource_getConnection(FilterChain chain, DruidDataSource dataSource, long maxWaitMillis) throws SQLException {
        return super.dataSource_getConnection(chain, dataSource, maxWaitMillis);
    }
}

2.2. 问题分析

2.2.1. 调用 Druid 过滤器处理获取数据库连接方法的条件

参考 Spring、MyBatis、Druid、MySQL 不使用事务执行 SQL 语句分析 Spring、MyBatis、Druid、MySQL 使用事务执行 SQL 语句分析,使用 Druid、MyBatis 组件的 Java 应用,不使用事务或使用事务执行数据库操作时,都需要调用 Druid 数据源的 DruidDataSource.getConnection() 方法获取数据库连接

  • DruidDataSource.getConnection()

DruidDataSource.getConnection() 方法的代码如下:

    public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
        init();

        final int filtersSize = filters.size();
        if (filtersSize> 0) {
            FilterChainImpl filterChain = createChain();
            try {
                return filterChain.dataSource_connect(this, maxWaitMillis);
            } finally {
                recycleFilterChain(filterChain);
            }
        } else {
            return getConnectionDirect(maxWaitMillis);
        }
    }

filters 字段为当前数据源中已注册的过滤器列表,当数量大于 0 时,会调用 filterChain.dataSource_connect() 方法,对应 com.alibaba.druid.filter.FilterChainImpl 类

  • FilterChainImpl.dataSource_connect()

FilterChainImpl.dataSource_connect() 方法的代码如下:

    public DruidPooledConnection dataSource_connect(DruidDataSource dataSource,
                                                    long maxWaitMillis) throws SQLException {
        if (this.pos < filterSize) {
            DruidPooledConnection conn = nextFilter().dataSource_getConnection(this, dataSource, maxWaitMillis);
            return conn;
        }

        return dataSource.getConnectionDirect(maxWaitMillis);
    }

pos 字段代表当前所使用的过滤器的序号,filterSize 为注册的过滤器的总数

满足 “this.pos < filterSize” 时,说明当前所使用的过滤器的序号小于注册的过滤器的总数,会调用 nextFilter().dataSource_getConnection() 方法,即调用下一个过滤器的 dataSource_getConnection() 方法

若不满足以上条件,则调用 dataSource.getConnectionDirect() 方法,即直接调用数据源获取连接的方法,不再调用过滤器进行处理

在 FilterChainImpl 类的其他过滤方法中,也存在类似的处理,即满足 “this.pos < filterSize” 时调用下一个过滤器进行处理;不满足时直接调用原始方法

  • FilterChainImpl 构造函数

FilterChainImpl 类的 filterSize 字段是 final,在构造函数中赋值,等于 getFilters() 方法返回的数量,即数据源的 getProxyFilters() 方法返回的数量

    public FilterChainImpl(DataSourceProxy dataSource) {
        this.dataSource = dataSource;
        this.filterSize = getFilters().size();
    }

    public FilterChainImpl(DataSourceProxy dataSource, int pos) {
        this.dataSource = dataSource;
        this.pos = pos;
        this.filterSize = getFilters().size();
    }

    public List<Filter> getFilters() {
        return dataSource.getProxyFilters();
    }
  • FilterChainImpl 创建过程

在 DruidDataSource.init() 方法中会创建数据库连接,创建的数量等于 initialSize,相关代码如下:

                // init connections
                while (poolingCount < initialSize) {
                    try {
                        PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
                        DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
                        connections[poolingCount++] = holder;
                    } catch (SQLException ex) {
                        LOG.error("init datasource error, url:" + this.getUrl(), ex);
                        if (initExceptionThrow) {
                            connectError = ex;
                            break;
                        } else {
                            Thread.sleep(3000);
                        }
                    }
                }

以上 createPhysicalConnection() 方法会创建 FilterChainImpl,触发 FilterChainImpl 类的 filterSize 字段赋值,以下为调用堆栈

<init>:44, FilterChainImpl (com.alibaba.druid.filter)
createChain:296, DruidAbstractDataSource (com.alibaba.druid.pool)
createPhysicalConnection:1693, DruidAbstractDataSource (com.alibaba.druid.pool)
createPhysicalConnection:1789, DruidAbstractDataSource (com.alibaba.druid.pool)
init:939, DruidDataSource (com.alibaba.druid.pool)
  • 调用 Druid 过滤器处理获取数据库连接方法的条件总结

Druid 数据源获取数据库连接的方法 DruidDataSource.getConnection() 中,当过滤器数量大于 0 时,会调用 FilterChainImpl.dataSource_connect() 方法

在 FilterChainImpl.dataSource_connect() 及其他方法中,方法能够被执行的过滤器的总数不超过 filterSize,即 filterSize 代表了能够生效的过滤器总数

Druid 数据源的初始化方法 DruidDataSource.init() 中,会间接调用到 FilterChainImpl 的构造函数,会对 filterSize 字段进行赋值

假如某个 Druid 过滤器在 Druid 数据源的初始化方法 DruidDataSource.init() 执行之后注册,则此时 FilterChainImpl 类的 filterSize 字段已经被赋值,该过滤器的过滤方法在进行对应数据库操作时不会被调用

因此,Druid 过滤器处理获取数据库连接的 dataSource_getConnection() 及其他方法,必须在 Druid 数据源的初始化方法 DruidDataSource.init() 之前注册,才能够被调用

2.2.2. DruidDataSource.init() 方法与动态注册 Druid 过滤器的顺序

参考 Spring Bean 创建顺序与 @Order 注解

以上通过 Bean 的构造函数注入 Druid 数据源的方式,在 TestDruidFilterRegister1 类的构造函数之前之前,Druid 数据源 DruidDataSource 类对应 Bean 的创建过程已经执行完毕,包括构造函数、初始化方法等

在 Spring XML 中定义 Druid 数据源 DruidDataSource 对应的 Bean 时,需要将 bean 元素的 init-method 属性设置为 “init”,即 Bean 的初始化方法

在以上情况下,DruidDataSource.init() 方法会比 TestDruidFilterRegister1 类的构造函数先执行,也比对应的 Druid 过滤器注册 DruidDataSource.setProxyFilters() 方法先执行

因此,在 Bean 的构造函数注入 Druid 数据源时注册的 Druid 过滤器,对于 Druid 数据源初始化时创建的数据库连接(数量等于 initialSize),处理获取数据库连接的 dataSource_getConnection() 方法不会被调用

2.3. 生效的注册方式

2.3.1. 原理分析

为了使动态注册的 Druid 过滤器处理获取数据库连接的 dataSource_getConnection() 及其他方法能够被调用,需要使注册 Druid 过滤器的操作早于 Druid 数据源的 DruidDataSource.init() 方法执行

参考以上 “Spring Bean 创建顺序与 @Order 注解”,Spring Bean 的创建顺序如下:

创建 Bean 实例
    1.a. 假如 Bean 有使用构造函数注入,则按照以下顺序执行
        1.a.1. 执行相同的步骤创建构造函数注入中被依赖的 Bean
        1.a.2. 调用当前 Bean 的有参数构造函数创建实例
    1.b. 假如 Bean 没有使用构造函数注入,则调用 Bean 的无参数构造函数创建实例
    2. 对 Bean 的字段注入被依赖的 Bean 实例进行创建
        执行相同的步骤创建被依赖的 Bean
    3. 调用 Bean 的 set 方法注入被依赖的 Bean 实例
        执行相同的步骤创建被依赖的 Bean
    4. 调用 Bean 实现 ApplicationContextAware 接口的 setApplicationContext() 方法
    5. 调用 BeanPostProcessor 实现类的 postProcessBeforeInitialization() 方法
    6. 调用 Bean 实现 InitializingBean 接口的 afterPropertiesSet() 方法
    7. 调用 Bean 的初始化方法(XML 中的 init-method、或有 @PostConstruct 注解的方法)
    8. 调用 BeanPostProcessor 实现类的 postProcessAfterInitialization() 方法
(创建所有的 Bean 之后)触发 ContextRefreshedEvent 事件

DruidDataSource.init() 方法对于第 7 步 Bean 的初始化方法,要早于该方法执行,只能选择在 BeanPostProcessor 实现类的 postProcessBeforeInitialization() 方法叶进行处理,因为其他的方法需要在 Druid 数据源 DruidDataSource 类中实现

2.3.2. 实现说明

在 BeanPostProcessor 实现类的 postProcessBeforeInitialization() 方法中,当处理到 DruidDataSource 实例时,调用 setProxyFilters() 方法添加 Druid 过滤器,简单示例如下:

@Service
@Lazy(false)
public class TestDruidFilterRegister2 implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (!(bean instanceof DruidDataSource)) {
            return bean;
        }
        DruidDataSource druidDataSource = (DruidDataSource) bean;
        TestDruidFilter testDruidFilter = new TestDruidFilter();
        druidDataSource.setProxyFilters(Collections.singletonList(testDruidFilter));
        return bean;
    }
}

通过以上方式添加 Druid 过滤器,可以保证处理获取数据库连接 dataSource_getConnection() 及其他方法能够被执行

以上方式添加的 Druid 过滤器在已有的 Druid 过滤器后面,执行顺序也靠后

2.3.3. 使添加的 Druid 过滤器靠前的方式

假如需要使添加的 Druid 过滤器执行顺序靠前,则需要添加到前面,可以在 BeanPostProcessor 实现类的 postProcessBeforeInitialization() 方法中使用以下方式:

        if (!(bean instanceof DruidDataSource)) {
            return bean;
        }
        DruidDataSource druidDataSource = (DruidDataSource) bean;
        List<Filter> filterList = druidDataSource.getProxyFilters();
        TestDruidFilter testDruidFilter = new TestDruidFilter();
        filterList.add(0, testDruidFilter);
        return bean;
  • 12
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值