本文将讨论在基于koca-support
开发和扩展时遇到的不兼容问题,并讨论不兼容原因,给出可能的解决方案。
问题发现
-
koca-tracing
需要支持基于DataSource
的链路跟踪,需要对DataSource
做代理或扩展,发现通用解决方案无法完成对DataSource
的链路跟踪,并且会使事务失效,但通用方案可以支持标准的Spring DataSource
; -
koca-metrics
需要暴露DruidDataSource
的相关指标,发现没有办法添加druid filter,koca-support
并不支持druid filter。
问题分析
DataSource链路跟踪
- 数据库链路跟踪可以基于
JDBC
来实现,但是对于商业数据库,如oracle
、sqllserver
,它们的JDBC Driver
并不是开源的或者并不支持拦截,导致无法基于这类JDBC Driver
做扩展,因此需要在代码抽象层进行链路跟踪,spring boot
或者koca
体系下,基于DataSource
是最合适的方案,向上与orm无关,向下与数据库和JDBC
无关。 - 由于
DataSource
有很多种实现,比如HikariCP
、DruidDataSource
等,不同的实现有不同的扩展方式,为了避免case by case地支持,直接使用动态代理DataSource
、Connection
、Statement
,在对应方法执行前后埋点,是一个比较通用的方式。 - 动态代理一个典型的实现方案就是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生命周期中,初始化之前的所有阶段将全部失效:
- 上图为正常Bean初始化的生命周期:
@Bean
注解的对象首先会被加载到BeanDefinition
列表,然后经过一系列生命周期的流程,最后才会初始化成可以使用的Bean,但registerSingleton()
会直接生成可以使用的Bean,生命周期里的所有初始化流程全部都跳过了,导致不能在初始化前后对DataSource
增强。
DataSource监控指标
- spring boot默认的数据库连接池
HikariCP
会暴露出链接池相关指标,但是Druid
相关指标并不会暴露,因此需要暴露DruidDataSource
的指标,便于监控和调整连接池; - 对于不同数据库连接池有不同的监控方式,
HikariCP
内置监控,DruidDataSource
基于DruidFilter
监控,因此需要新建一个DruidFilter
来记录相关指标; - 但是
koca-support
的DruidDataSource
并不支持DruidFilter
,导致监控指标没有办法通过Filter的方式实现,使用者也没有任何办法可以让filter生效。
解决方案
关键点:
- 能够在一个方法里动态注册多个
DataSource
,并且让@ConditionalOnMissingBean(DataSource.class)
失效; - 注册的
DataSource
能够正确被Spring Bean生命周期管理; - 初始化
DuridDataSource
时能够让filter生效; - 保证数据源之间的事物正确,保证
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 名称以外,还为它们设置dataSrouce
和transactionManager
的别名,这样既能保证让@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);
}
}