koca-support 和Spring DataSource兼容性 问题讨论

本文将讨论在基于koca-support开发和扩展时遇到的不兼容问题,并讨论不兼容原因,给出可能的解决方案。

问题发现

  • koca-tracing需要支持基于DataSource的链路跟踪,需要对DataSource做代理或扩展,发现通用解决方案无法完成对DataSource的链路跟踪,并且会使事务失效,但通用方案可以支持标准的Spring DataSource
  • koca-metrics需要暴露DruidDataSource的相关指标,发现没有办法添加druid filter,koca-support并不支持druid filter。

问题分析

DataSource链路跟踪

  • 数据库链路跟踪可以基于JDBC来实现,但是对于商业数据库,如oraclesqllserver,它们的JDBC Driver并不是开源的或者并不支持拦截,导致无法基于这类JDBC Driver做扩展,因此需要在代码抽象层进行链路跟踪,spring boot或者koca体系下,基于DataSource是最合适的方案,向上与orm无关,向下与数据库和JDBC无关。
  • 由于DataSource有很多种实现,比如HikariCPDruidDataSource等,不同的实现有不同的扩展方式,为了避免case by case地支持,直接使用动态代理DataSourceConnectionStatement,在对应方法执行前后埋点,是一个比较通用的方式。
  • 动态代理一个典型的实现方案就是AOP或者BeanPostProcessor,AOP更多的是增强业务Bean,需要显示地定义系统类的切入点,对于基础组件来说,并不是一个优雅的实现方案,BeanPostProcessor支持在Bean初始化之后使用动态代理对改Bean增强,这样既不会显示定义系统类的切入点,也不会污染spring bean初始化流程,spring boot 3.x实现了基于DataSource的链路监控,也是用动态代理实现的;
  • 但是koca-support在注册DataSource的时候,直接向ApplicationContext注册了初始化完毕的bean:
    @Bean
    public DataSource dataSource() throws Exception {
        DataSource defaultDataSource = null;
        // noinspection
        for (MultiDataSourceProperties.DataSourceProperties dataSource : kocaMultiDataSource.getDataSources()) {
            // AlibabaRemoveCommentedCode,AlibabaRemoveCommentedCode
            Map<String, Object> poolMap = dataSource.getPool();
            String dataSourceId = dataSource.getId();

            // 默认 Druid数据源
            if (!poolMap.containsKey(TYPE)) {
                poolMap.put(TYPE, DruidDataSource.class.getName());
            }

            DataSource createdDataSource = createDataSource(poolMap);

            // 默认数据源
            if (dataSourceId.equalsIgnoreCase(kocaMultiDataSource.getDefaultDataSourceId())) {
                defaultDataSource = createdDataSource;
            }

            // 有多个数据源 数据源动态注册
            //            if (kocaMultiDataSource.getDataSources().length > 1) {
            // 将数据源注册进spring bean的动态注册
            // @ConditionalOnMissingBean(DataSource.class) 无效
            String dataSourceName = DataSourceNameUtils.getDataSourceName(dataSourceId);
            beanFactory.registerSingleton(dataSourceName, createdDataSource);

            // 将数据源对应的事务管理器 进行动态注册
            String transManagerName = DataSourceNameUtils.getTransManagerName(dataSourceId);
            beanFactory.registerSingleton(transManagerName, new DataSourceTransactionManager(createdDataSource));
            //            }
        }
        return defaultDataSource;
    }
  • beanFactory.registerSingleton()会导致bean跳过初始化阶段,因为该bean已经在注册时就初始化完成了,那么Bean初始化前后的hook将不会生效,bean生命周期中,初始化之前的所有阶段将全部失效:
    spring-bean-life-cycle
  • 上图为正常Bean初始化的生命周期:@Bean注解的对象首先会被加载到BeanDefinition列表,然后经过一系列生命周期的流程,最后才会初始化成可以使用的Bean,但registerSingleton()会直接生成可以使用的Bean,生命周期里的所有初始化流程全部都跳过了,导致不能在初始化前后对DataSource增强。

DataSource监控指标

  • spring boot默认的数据库连接池HikariCP会暴露出链接池相关指标,但是Druid相关指标并不会暴露,因此需要暴露DruidDataSource的指标,便于监控和调整连接池;
  • 对于不同数据库连接池有不同的监控方式,HikariCP内置监控,DruidDataSource基于DruidFilter监控,因此需要新建一个DruidFilter来记录相关指标;
  • 但是koca-supportDruidDataSource并不支持DruidFilter,导致监控指标没有办法通过Filter的方式实现,使用者也没有任何办法可以让filter生效。

解决方案

关键点:

  1. 能够在一个方法里动态注册多个DataSource,并且让@ConditionalOnMissingBean(DataSource.class)失效;
  2. 注册的DataSource能够正确被Spring Bean生命周期管理;
  3. 初始化DuridDataSource时能够让filter生效;
  4. 保证数据源之间的事物正确,保证DataSource、事务管理相关Bean的名称符合koca-support规范

关于第一点和第二点,可以考虑使用ImportBeanDefinitionRegistrar,这个接口可以实现注册多个BeanDefinition,一是保证动态注册数据源或别名,让@ConditionalOnMissingBean(DataSource.class)失效,二是可以让注册的动态数据源和事务管理器正确被Spring Bean生命周期管理。

基于ImportBeanDefinitionRegistrar注册的Bean,有完整的Bean初始化生命周期,只需在初始化钱的任何一个阶段设置DuridDataSource的Filter,就能完成Filter配置,比如BeanPostPeocessor.postProcessBeforeInitialization(),那么第三点自然也能解决,并且能够在任何其他模块动态添加Filter——只要在初始化之前的任意一个阶段田间Filter即可。

注册数据源时,注册数据源对应的事务管理器,对于默认数据源和默认事务管理器,除了设置它们的bean名称为koca-support DataSourceNameUtils.getDataSourceName(dataSourceId)DataSourceNameUtils.getTransManagerName(dataSourceId)生成的bean 名称以外,还为它们设置dataSroucetransactionManager的别名,这样既能保证让@ConditionalOnMissingBean(DataSource.class)失效,各个数据源和事务管理器bean名称符合规范,事务管理不混乱,也能默认数据源和其对应动态注册的数据源指向同一个Bean,避免因为不规范获取Bean可能出现的问题。

代码示例

MultiDataSourceAutoConfig.java

@NotEmptyCondition("jdbc.defaultDataSourceId")
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(MultiDataSourceProperties.class)
@Import(MultiDataSourceRegistrar.class)
public class MultiDataSourceAutoConfig {

    @Autowired
    private MultiDataSourceProperties kocaMultiDataSource;

    /**
     * 数据源切换切面.
     *
     * @return DataSourceSetAspect
     */
    @Bean
    public DataSourceSetAspect dataSourceSetAspect() {
        return new DataSourceSetAspect();
    }

    @Bean
    public DruidFilterBeanPostProcessor druidFilterBeanPostProcessor() {
        Map<String, MultiDataSourceProperties.DataSourceProperties> map = new HashMap<>(4);
        for (MultiDataSourceProperties.DataSourceProperties dataSource : kocaMultiDataSource.getDataSources()) {
            String dataSourceId = dataSource.getId();
            String dataSourceName = DataSourceNameUtils.getDataSourceName(dataSourceId);
            map.put(dataSourceName, dataSource);
        }
        return new DruidFilterBeanPostProcessor(map);
    }

    static class DruidFilterBeanPostProcessor implements BeanPostProcessor {

        private Map<String, MultiDataSourceProperties.DataSourceProperties> dataSourcePropertiesMap;

        DruidFilterBeanPostProcessor(
            Map<String, MultiDataSourceProperties.DataSourceProperties> dataSourcePropertiesMap) {
            this.dataSourcePropertiesMap = dataSourcePropertiesMap;
        }

        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (dataSourcePropertiesMap.containsKey(beanName) && bean instanceof DruidDataSource) {
                try {
                    ((DruidDataSource) bean)
                        .setFilters((String) dataSourcePropertiesMap.get(beanName).getPool().get("druidFilters"));
                } catch (SQLException e) {
                    throw new BeanInitializationException("Init druid filter failed", e);
                }
            }
            return bean;
        }
        
    }
}

MultiDataSourceRegistrar.java

public class MultiDataSourceRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private static final Logger LOGGER = LoggerFactory.getLogger(MultiDataSourceRegistrar.class);

    private static final String URL = "url";

    private static final String TYPE = "type";

    private static final String DRIVER_CLASS_NAME = "driverClassName";

    private MultiDataSourceProperties multiDataSourceProperties;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        for (MultiDataSourceProperties.DataSourceProperties dataSource : multiDataSourceProperties.getDataSources()) {
            Map<String, Object> poolMap = dataSource.getPool();
            String dataSourceId = dataSource.getId();

            // default druid data source
            if (!poolMap.containsKey(TYPE)) {
                poolMap.put(TYPE, DruidDataSource.class.getName());
            }

            String dataSourceName = DataSourceNameUtils.getDataSourceName(dataSourceId);
            String transManagerName = DataSourceNameUtils.getTransManagerName(dataSourceId);

            // datasource & transaction manager bean definition
            BeanDefinition datasourceBeanDefinition;
            try {
                datasourceBeanDefinition = createDatasourceBeanDefinition(poolMap);
            } catch (ClassNotFoundException e) {
                LOGGER.error("register data source bean definition failed, data source type class not found: {}",
                    poolMap.get(TYPE));
                throw new BeanDefinitionValidationException("data source register failed", e);
            }
            registry.registerBeanDefinition(dataSourceName, datasourceBeanDefinition);

            BeanDefinitionBuilder transBeanDefinitionBuilder =
                BeanDefinitionBuilder.rootBeanDefinition(DataSourceTransactionManager.class);
            transBeanDefinitionBuilder.addConstructorArgReference(dataSourceName);
            BeanDefinition transBeanDefinition = transBeanDefinitionBuilder.getBeanDefinition();
            registry.registerBeanDefinition(transManagerName, transBeanDefinition);

            //default datasource
            if (dataSourceId.equalsIgnoreCase(multiDataSourceProperties.getDefaultDataSourceId())) {
                registry.registerAlias(dataSourceName, "dataSource");
                registry.registerAlias(transManagerName, "transactionManager");
            }
        }

    }

    /**
     * 创建data source bean definition
     * @param properties 配置参数
     * @return data source bean definition
     * @throws ClassNotFoundException type不存在
     */
    private BeanDefinition createDatasourceBeanDefinition(Map<String, Object> properties)
        throws ClassNotFoundException {
        // driver class
        if (!properties.containsKey(DRIVER_CLASS_NAME) && properties.containsKey(URL)) {
            String url = (String) properties.get(URL);
            String driverClass = DatabaseDriver.fromJdbcUrl(url).getDriverClassName();
            properties.put(DRIVER_CLASS_NAME, driverClass);
        }
        //url username
        properties.computeIfAbsent("user", key -> properties.get("username"));
        properties.computeIfAbsent("jdbc-url", key -> properties.get("url"));

        // data source type
        String typeStr = (String) properties.get(TYPE);
        Class<? extends DataSource> type =
            (Class<? extends DataSource>) ClassUtils.forName(typeStr, this.getClass().getClassLoader());
        BeanDefinitionBuilder dsBeanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(type);

        // druid init
        if (type == DruidDataSource.class) {
            dsBeanDefinitionBuilder.setInitMethodName("init");
        }

        // properties
        AbstractBeanDefinition beanDefinition = dsBeanDefinitionBuilder.getBeanDefinition();
        properties.forEach((property, value) -> {
            PropertyValue propertyValue = new PropertyValue(property, value);
            propertyValue.setOptional(true);
            beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
        });

        return beanDefinition;
    }

    @Override
    public void setEnvironment(Environment environment) {
        multiDataSourceProperties = Binder.get(environment).bind("jdbc", MultiDataSourceProperties.class).orElse(null);
    }
}
1 个赞