写在前面
书接上文,连接池没生效,启用了一个什么默认的连接池。具体是什么,一起来看看源码吧。
目录
- 写在前面
- 一、问题描述
- 二、本地调试
- 三、升级dynamic-datasource
- 四、新的问题
-
- (一)数据源初始化问题
- [(二)GaussDB updatedTime NULL值问题](#(二)GaussDB updatedTime NULL值问题)
- 五、参考资料
- 写在后面
- 系列文章
一、问题描述
连接池没生效,无外乎就是 yml 的配置没读取到、连接池没创建或者创建失败了。因为没报错,所以极大的可能是 yml 配置没读取到。
二、本地调试
启动项目,debug一下,果然用的是Tomcat的连接池。
这里,默认根据 DATA_SOURCE_TYPE_NAMES 加载,发现 classpath 中有哪个就用哪个。
java
package org.springframework.boot.autoconfigure.jdbc;
public class DataSourceBuilder {
private static final String[] DATA_SOURCE_TYPE_NAMES = new String[] {
"org.apache.tomcat.jdbc.pool.DataSource",
"com.zaxxer.hikari.HikariDataSource",
"org.apache.commons.dbcp.BasicDataSource", // deprecated
"org.apache.commons.dbcp2.BasicDataSource" };
public DataSource build() {
Class<? extends DataSource> type = getType();
DataSource result = BeanUtils.instantiate(type);
maybeGetDriverClassName();
bind(result);
return result;
}
public Class<? extends DataSource> findType() {
if (this.type != null) {
return this.type;
}
for (String name : DATA_SOURCE_TYPE_NAMES) {
try {
// 遍历到第一个就返回了,第一个就是tomcat的
return (Class<? extends DataSource>) ClassUtils.forName(name,
this.classLoader);
}
catch (Exception ex) {
}
}
return null;
}
}
那就再找一下连接池初始化数量,进一步确认。果然也正如料想的那样,初始化数量是10个。
java
package org.apache.tomcat.jdbc.pool;
public class PoolProperties implements PoolConfiguration, Cloneable, Serializable {
private volatile int initialSize = 10;
}
那现在就很明确了,是不是添加完dynamic-datasource之后影响了原有的连接池加载?其实这个时候从properties文件中就已经看出问题了:压根没有dbcp2的节点,貌似也不支持dbcp2连接池(只有druid 和 hikari)
以下是 DynamicDataSourceProperties 和 DataSourceProperty(yml的对应实体)。
java
package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;
/**
* DynamicDataSourceProperties
*
* @author TaoYu Kanyuxia
* @see DataSourceProperties
* @since 1.0.0
*/
@Slf4j
@Getter
@Setter
@ConfigurationProperties(prefix = DynamicDataSourceProperties.PREFIX)
public class DynamicDataSourceProperties {
public static final String PREFIX = "spring.datasource.dynamic";
public static final String HEALTH = PREFIX + ".health";
/**
* 必须设置默认的库,默认master
*/
private String primary = "master";
/**
* 是否启用严格模式,默认不启动. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源primary所设置的数据源
*/
private Boolean strict = false;
/**
* 是否使用p6spy输出,默认不输出
*/
private Boolean p6spy = false;
/**
* 是否使用 spring actuator 监控检查,默认不检查
*/
private boolean health = false;
/**
* 每一个数据源
*/
private Map<String, DataSourceProperty> datasource = new LinkedHashMap<>();
/**
* 多数据源选择算法clazz,默认负载均衡算法
*/
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
/**
* aop切面顺序,默认优先级最高
*/
private Integer order = Ordered.HIGHEST_PRECEDENCE;
/**
* Druid全局参数配置
*/
@NestedConfigurationProperty
private DruidConfig druid = new DruidConfig();
/**
* HikariCp全局参数配置
*/
@NestedConfigurationProperty
private HikariCpConfig hikari = new HikariCpConfig();
/**
* 全局默认publicKey
*/
private String publicKey = CryptoUtils.DEFAULT_PUBLIC_KEY_STRING;
}
package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;
/**
* @author TaoYu
* @since 1.2.0
*/
@Slf4j
@Data
@Accessors(chain = true)
public class DataSourceProperty {
/**
* 加密正则
*/
private static final Pattern ENC_PATTERN = Pattern.compile("^ENC\\((.*)\\)$");
/**
* 连接池名称(只是一个名称标识)</br> 默认是配置文件上的名称
*/
private String pollName;
/**
* 连接池类型,如果不设置自动查找 Druid > HikariCp
*/
private Class<? extends DataSource> type;
/**
* JDBC driver
*/
private String driverClassName;
/**
* JDBC url 地址
*/
private String url;
/**
* JDBC 用户名
*/
private String username;
/**
* JDBC 密码
*/
private String password;
/**
* jndi数据源名称(设置即表示启用)
*/
private String jndiName;
/**
* 自动运行的建表脚本
*/
private String schema;
/**
* 自动运行的数据脚本
*/
private String data;
/**
* 错误是否继续 默认 true
*/
private boolean continueOnError = true;
/**
* 分隔符 默认 ;
*/
private String separator = ";";
/**
* Druid参数配置
*/
@NestedConfigurationProperty
private DruidConfig druid = new DruidConfig();
/**
* HikariCp参数配置
*/
@NestedConfigurationProperty
private HikariCpConfig hikari = new HikariCpConfig();
/**
* 解密公匙(如果未设置默认使用全局的)
*/
private String publicKey;
public String getUrl() {
return decrypt(url);
}
public String getUsername() {
return decrypt(username);
}
public String getPassword() {
return decrypt(password);
}
/**
* 字符串解密
*/
private String decrypt(String cipherText) {
if (StringUtils.hasText(cipherText)) {
Matcher matcher = ENC_PATTERN.matcher(cipherText);
if (matcher.find()) {
try {
return CryptoUtils.decrypt(publicKey, matcher.group(1));
} catch (Exception e) {
log.error("DynamicDataSourceProperties.decrypt error ", e);
}
}
}
return cipherText;
}
}
咋整?难道不用这个动态数据源,自己写一套?项目紧急,时间上不允许呀!
另外,不可能不支持dbcp2连接池额,走,去官网GitHub上看看。
三、升级dynamic-datasource
官网链接 https://github.com/baomidou/dynamic-datasource/tree/v3.3.4/src/main
不看不知道,一看吓一跳。
源码版本直接从V1.1.0直接到V3.3.0,中间的版本没有了。
查看源码,确实从3.3.4版本开始支持dbcp2版本数据源(目前用的是2.5.7),那就升级到3.5.2(4.x之后结构又发生了很大变化)
升级完,yml中配置的连接池生效了 ~
❗️ 技巧:
这也给我们提了一个醒,在引入一个新的框架时,
一定要先去对应的GitHub仓库看源码,不能只依赖于maven仓库。
成熟的产品,大都高版本是兼容低版本的。低版本不行,就去试试高版本~
四、新的问题
(一)数据源初始化问题
好用是好用,但是存在一个新的问题:dynamic-datasource会在项目启动时,加载所有的数据源并进行连接
,然后通过@DS来动态切换数据源。
显然,这和我们的需求不太一样。我们并不想项目一启动就把所有的数据源都加载了,只想primary配置成哪个,就加载哪个数据源。
看源码,找到数据源加载的位置,确实这里把所有数据源全加载了。
以下是加载数据源的源码部分:
java
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {
@Autowired
private DataSourceCreator dataSourceCreator;
protected Map<String, DataSource> createDataSourceMap(
Map<String, DataSourceProperty> dataSourcePropertiesMap) {
Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
DataSourceProperty dataSourceProperty = item.getValue();
String pollName = dataSourceProperty.getPoolName();
if (pollName == null || "".equals(pollName)) {
pollName = item.getKey();
}
dataSourceProperty.setPoolName(pollName);
dataSourceMap.put(pollName, dataSourceCreator.createDataSource(dataSourceProperty));
}
return dataSourceMap;
}
}
那好办,取出primary属性,判断一下就可以了。
修改如下:
java
package com.baomidou.dynamic.datasource.provider;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author TaoYu
*/
@Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {
@Autowired
private DefaultDataSourceCreator defaultDataSourceCreator;
@Autowired
private DynamicDataSourceProperties dynamicDataSourceProperties;
protected Map<String, DataSource> createDataSourceMap(
Map<String, DataSourceProperty> dataSourcePropertiesMap) {
Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
String dsName = item.getKey();
// 只加载 yml配置文件中 primary指定的数据源
if (dsName.equals(dynamicDataSourceProperties.getPrimary())) {
DataSourceProperty dataSourceProperty = item.getValue();
String poolName = dataSourceProperty.getPoolName();
if (poolName == null || "".equals(poolName)) {
poolName = dsName;
}
dataSourceProperty.setPoolName(poolName);
dataSourceMap.put(dsName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
}
}
return dataSourceMap;
}
}
想让源码的文件生效有两种方式:
第一种把jar包中的文件copy出来,在项目下创建相同的包名。
第二种改源码。
因为我们有Nexus私服,这里就采用第二种方式,修改源码,维护一个自己的版本,也方便后期自定义。
构建nexus私服构件,下载源码,在pom中配置,以及maven的settings.xml中的认证,deploy即可到远程私服上查看是否部署成功。
修改依赖版本为自定义构建的版本 3.5.2.companyName。重新部署完项目,发现连接数瞬间变小,由原来的到230多变为40多。
❗️ 注意:这里说一下题外话,项目中还有一个问题,这也是下一个要重构的目标。
目前各个项目单独引用SpringBoot(版本可能还不太一样),数据源dbcp2连接池也就被分散到各个项目中,导致无法统一。
因为dbcp2的版本是在springboot中定义的。
为什么无法统一?看下面示例
在maven项目,父项目中<dependencyManagement>定义一个jar包的版本1.0,
子项目A定义一个jar包版本2.0,子项目B依赖子项目A。
这个时候子项目B的jar包版本不是2.0,而是1.0。
因为子项目A的2.0是会覆盖父项目中的版本,而子项目B只是依赖A,不会覆盖父项目中的1.0。
(二)GaussDB updatedTime NULL值问题
做数据库兼容时,还遇到一个问题:当程序中实体的 updatedTime 日期字段设置为null进行操作时,GaussDB数据库不支持自动更新。
GaussDB没有MySQL的这种 ON UPDATE
功能,它认为你传的updatedTime 的值就是NULL,导致数据库NOT NULL 报错。
sql
updatedTime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
对于这个问题,你可以像Oracle一样通过触发器实现,但是加触发器也需要加很多(还会影响性能)。
我之前有用过MyBatis-plugins的属性填充,那我是否可以为MyBatis自定义一个属性填充?了解MyBatis的我们知道,可以通过拦截器
实现,搞定。
以下为属性填充的拦截器:
java
package com.zhht.mybatis.interceptor;
import com.alibaba.fastjson.JSON;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Objects;
import java.util.Properties;
/**
* 拦截SQL,进行对象属性填充
* 解决GaussDB数据库,表字段不支持 ON UPDATE CURRENT_TIMESTAMP
*/
@Intercepts(
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
)
public class MetaObjectInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(MetaObjectInterceptor.class);
private static final String TABLE_FIELD_CREATE_TIME = "createTime";
private static final String TABLE_FIELD_CREATED_TIME = "createdTime";
private static final String TABLE_FIELD_GMT_CREATE = "gmtCreate";
private static final String TABLE_FIELD_UPDATE_TIME = "updateTime";
private static final String TABLE_FIELD_UPDATED_TIME = "updatedTime";
private static final String TABLE_FIELD_GMT_MODIFIED = "gmtModified";
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
LOGGER.info("MetaObjectInterceptor - intercept [method: {}] start!", ms.getId());
if (!Objects.isNull(parameter)) {
SqlCommandType sqlCommandType = ms.getSqlCommandType();
if (SqlCommandType.INSERT == sqlCommandType || SqlCommandType.UPDATE == sqlCommandType) {
BoundSql boundSql = ms.getBoundSql(parameter);
String beforeParameter = JSON.toJSONString(boundSql.getParameterObject());
LOGGER.info("MetaObjectInterceptor - intercept [method: {}, before params: {}]", ms.getId(), beforeParameter);
Class<?> clazz = parameter.getClass();
if (clazz.getSuperclass().isInstance(Object.class)) {
fillFields(parameter.getClass().getDeclaredFields(), parameter, sqlCommandType);
} else {
Class<?> superclass = clazz.getSuperclass();
fillFields(superclass.getDeclaredFields(), parameter, sqlCommandType);
}
String afterParameter = JSON.toJSONString(boundSql.getParameterObject());
LOGGER.info("MetaObjectInterceptor - intercept [method: {}, after params: {}]", ms.getId(), afterParameter);
}
}
LOGGER.info("MetaObjectInterceptor - intercept [method: {}] successful!", ms.getId());
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* 填充属性
*
* @param declaredFields 参数字段
* @param parameter 参数实体
* @param sqlCommandType sql类型
* @throws IllegalAccessException
*/
private void fillFields(Field[] declaredFields, Object parameter, SqlCommandType sqlCommandType) throws IllegalAccessException {
for (Field field : declaredFields) {
field.setAccessible(true);
if (isNeedFill(sqlCommandType, field.getName(), field.get(parameter))) {
doFill(field, parameter);
}
}
}
/**
* 设置值
*
* @param field 字段
* @param parameter 参数实体
*/
private void doFill(Field field, Object parameter) throws IllegalAccessException {
if (Date.class == field.getType()) {
field.set(parameter, new Date());
} else if (Timestamp.class == field.getType()) {
field.set(parameter, new Timestamp(System.currentTimeMillis()));
} else if (Long.class == field.getType()) {
field.set(parameter, System.currentTimeMillis());
} else {
LOGGER.warn("MetaObjectInterceptor - doFill [type: {} is not support!]", field.getType().getName());
}
}
/**
* 判断字段是否需要填充
* 逻辑:包含且非空
*
* @param sqlCommandType sql类型
* @param fieldName 字段名称
* @param filedValue 字段值
* @return
*/
private boolean isNeedFill(SqlCommandType sqlCommandType, String fieldName, Object filedValue) {
if (SqlCommandType.INSERT.equals(sqlCommandType)) {
// create和update字段,为了效率, 不使用list.contains
if (TABLE_FIELD_CREATE_TIME.equals(fieldName)
|| TABLE_FIELD_CREATED_TIME.equals(fieldName)
|| TABLE_FIELD_GMT_CREATE.equals(fieldName)
|| TABLE_FIELD_UPDATE_TIME.equals(fieldName)
|| TABLE_FIELD_UPDATED_TIME.equals(fieldName)
|| TABLE_FIELD_GMT_MODIFIED.equals(fieldName)) {
return Objects.isNull(filedValue);
}
} else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
// 只考虑update字段,时时更新
if (TABLE_FIELD_UPDATE_TIME.equals(fieldName)
|| TABLE_FIELD_UPDATED_TIME.equals(fieldName)
|| TABLE_FIELD_GMT_MODIFIED.equals(fieldName)) {
return true;
}
}
return false;
}
}
/**
* MyBatis自动配置
*
* @author qiuxianbao
* @date 2023/10/31
*/
@Configuration
@EnableConfigurationProperties
public class MyBatisAutoConfiguration {
/**
* 支持多种数据库产品
* @return
*/
@Bean
@ConditionalOnMissingBean
public DatabaseIdProvider getDatabaseIdProvider() {
VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
databaseIdProvider.setProperties(DatabaseVendorLoadUtils.load());
return databaseIdProvider;
}
/**
* 添加属性填充拦截器
* @return
*/
@Bean
public Interceptor metaObjectInterceptor() {
return new MetaObjectInterceptor();
}
}
至此,项目初步完成改造,后续观察,封版提测。
至于源码的事儿,敬请关注看图说话专栏或者系列文章~
五、参考资料
写在后面
如果本文内容对您有价值或者有启发的话,欢迎点赞、关注、评论和转发。您的反馈和陪伴将促进我们共同进步和成长。
系列文章
【连接池】-从源码到适配(上),你遇到过数据库连接池的问题吗?This connection has been closed
【源码】-MyBatis-如何系统地看源码