大家好,我是 Mr.Sun,一名热爱技术和分享的程序员。
📖 个人博客:Mr.Sun的博客
✨ 微信公众号:「Java技术宇宙」
期待与你交流,让我们一起在技术道路上成长。
一、相关的POM文件
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mrsun</groupId>
<artifactId>example</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<lombok.version>1.18.10</lombok.version>
<commons-pool2.version>2.6.2</commons-pool2.version>
<mybatis-spring-boot-starter.version>2.0.0</mybatis-spring-boot-starter.version>
<druid.version>1.1.12</druid.version>
<mongodb-spring-boot-starter.version>2.2.4.RELEASE</mongodb-spring-boot-starter.version>
<rocketmq.version>4.3.0</rocketmq.version>
<jackson-databind.version>2.10.1</jackson-databind.version>
<druid-spring-boot-starter.version>1.2.1</druid-spring-boot-starter.version>
</properties>
<dependencies>
<!--Mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--Druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
</project>
二、Properties配置
这里配置了两个数据源,一个是default,一个是salve,两个数据源公用同一套Druid连接池
properties
spring.dynamic.datasource.default.url=jdbc:mysql://localhost:3306/ry-vue?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.dynamic.datasource.default.username=root
spring.dynamic.datasource.default.password=123456
spring.dynamic.datasource.default.driver-class-name=com.mysql.cj.jdbc.Driver
spring.dynamic.datasource.default.type=com.alibaba.druid.pool.DruidDataSource
spring.dynamic.datasource.salve.url=jdbc:mysql://localhost:3306/example?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.dynamic.datasource.salve.username=root
spring.dynamic.datasource.salve.password=123456
spring.dynamic.datasource.salve.driver-class-name=com.mysql.cj.jdbc.Driver
spring.dynamic.datasource.salve.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.initialSize=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=30
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=SELECT 1
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
spring.datasource.druid.filters=stat,wall,log4j
三、Druid连接池
java
@Data
@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DruidDataSourceProperties {
private int initialSize;
private int minIdle;
private int maxActive;
private int maxWait;
private int timeBetweenEvictionRunsMillis;
private String validationQuery;
private boolean testWhileIdle;
private boolean testOnBorrow;
private boolean testOnReturn;
private String filters;
public DruidDataSource dataSource(DruidDataSource datasource) {
/** 配置初始化大小、最小、最大 */
datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);
/** 配置获取连接等待超时的时间 */
datasource.setMaxWait(maxWait);
/** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
/**
* 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
*/
datasource.setValidationQuery(validationQuery);
/** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
datasource.setTestWhileIdle(testWhileIdle);
/** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
datasource.setTestOnBorrow(testOnBorrow);
/** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
datasource.setTestOnReturn(testOnReturn);
try {
datasource.setFilters(filters);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return datasource;
}
}
四、动态数据源配置
定义多数据源枚举
java
public enum DsType {
DEFAULT("default"),
SALVE("salve");
private final String name;
DsType(String name) {
this.name = name;
}
public static DsType getDsType(String name) {
for (DsType value : DsType.values()) {
if (value.name.equals(name)) {
return value;
}
}
throw new RuntimeException("没有匹配到数据源类型");
}
}
切换数据源注解@DS
java
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
public DsType value() default DsType.DEFAULT;
}
动态数据源配置
核心就是在AbstractRoutingDataSource这个类,设置好多数据源配置和默认数据源,在执行SQL的时候设置数据源,然后拿到配置的数据源来执行,其中多线程隔离使用ThreadLocal来实现
Druid监控:http://localhost:8080/druid/index.html admin/123456(代码里配置的)
java
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.dynamic")
@EnableConfigurationProperties(value = DruidDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Autowired
private DruidDataSourceProperties druidDataSourceProperties;
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceConfig.class);
// spring.dynamic.datasource依赖注入
Map<String, DataSourceProperties> datasource;
@Bean("dynamicDataSource")
public DynamicDataSource dynamicDataSource() {
Map<Object, Object> targetDataSource = new HashMap<>();
datasource.forEach((k, v) -> {
DsType dsType = DsType.getDsType(k);
targetDataSource.put(dsType, initDruidDataSource(v));
});
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSource);
dynamicDataSource.setDefaultTargetDataSource(targetDataSource.get(DsType.DEFAULT));
logger.info("初始化默认数据源:{}", DsType.DEFAULT);
return dynamicDataSource;
}
private DruidDataSource initDruidDataSource(DataSourceProperties properties) {
DruidDataSource druidDataSource = druidDataSourceProperties.dataSource(DruidDataSourceBuilder.create().build());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
druidDataSource.setDriverClassName(properties.getDriverClassName());
return druidDataSource;
}
public static class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DsTypeContainer.getDataSource();
}
}
public static class DsTypeContainer {
private static final ThreadLocal<DsType> DS_TYPE = new ThreadLocal<>();
public static void setDataSource(DsType dataSource) {
if (dataSource == null) {
dataSource = DsType.DEFAULT;
}
logger.info("设置数据源:{}", dataSource);
DS_TYPE.set(dataSource);
}
public static DsType getDataSource() {
DsType dataSource = DS_TYPE.get();
if (dataSource == null) {
dataSource = DsType.DEFAULT;
}
logger.info("获取数据源:{}", dataSource);
return dataSource;
}
public static void clearDataSource() {
DS_TYPE.remove();
}
}
/**
* 配置Druid的监控
* http://localhost:8080/druid/index.html
* @return
*/
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
Map<String,String> initParams = new HashMap<>();
initParams.put("loginUsername","admin");
initParams.put("loginPassword","123456");
initParams.put("allow","");
//initParams.put("deny","127.0.0.1");
bean.setInitParameters(initParams);
return bean;
}
}
五、(方式一) AOP切换
扫描接口类和方法上存在@DS注解,其中方法上的优先级大于类上的
java
@Aspect
@Component
public class DataSourceAop {
/**
* 扫描所有与这个注解有关的
* :@within:用于匹配所有持有指定注解类型内的方法和类;
* 也就是说只要有一个类上的有这个,使用@within这个注解,就能拿到下面所有的方法
*:@annotation:用于匹配当前执行方法持有指定注解的方法,而这个注解只针对方法
*
* 不添加扫描路径,应该是根据启动类的扫描范围执行的
*/
@Pointcut("@annotation(com.mrsun.aspectj.DS) " +
"|| @within(com.mrsun.aspectj.DS)")
public void doPointCut() {
}
@Around("doPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
DS dataSource = getDataSource(joinPoint);
if (dataSource != null) {
DynamicDataSourceConfig.DsTypeContainer.setDataSource(dataSource.value());
}
try {
return joinPoint.proceed();
} finally {
//关闭线程资源 在执行方法之后
DynamicDataSourceConfig.DsTypeContainer.clearDataSource();
}
}
/**
* 获取类或者方法上的注解
* 先获取方法上的注解,然后在获取类上的注解,这就实现了方法上数据源切换优先于类上的
* @param joinPoint 正在执行的连接点
* @return 注解
*/
private DS getDataSource(ProceedingJoinPoint joinPoint) {
MethodSignature method = (MethodSignature) joinPoint.getSignature();
// 检查方法上的注解
DS methodAnnotation = method.getMethod().getAnnotation(DS.class);
if (methodAnnotation != null) {
return methodAnnotation;
}
// 检查目标类的注解
Class<?> targetClass = joinPoint.getTarget().getClass();
DS classAnnotation = targetClass.getAnnotation(DS.class);
if (classAnnotation != null) {
return classAnnotation;
}
// 检查所有接口上的注解
for (Class<?> interfaceClass : targetClass.getInterfaces()) {
DS interfaceAnnotation = interfaceClass.getAnnotation(DS.class);
if (interfaceAnnotation != null) {
return interfaceAnnotation;
}
}
// 如果仍未找到,返回默认值或抛出异常
return null;
}
}
六、测试
java
@Mapper
public interface IDefaultMapper {
@Select("select * from sys_user")
List<Map<String,String>> selectDefaultUser();
}
java
@Mapper
@DS(value = DsType.SALVE)
public interface ISalveMapper {
@Select("select * from example_user")
List<Map<String,String>> selectSalveUser();
}
这样的话,IDefaultMapper里执行的都是default数据源的,ISalveMapper里所有方法都是执行salve数据源的,如果在这里面的其中一个方法加上@DS(value=DsType.DEFAULT),那么方法上的数据源优先级大于类上的
java
@SpringBootTest
@RunWith(SpringRunner.class)
public class ExampleTest {
@Resource
private IDefaultMapper defaultMapper;
@Resource
private ISalveMapper salveMapper;
@Test
public void test() {
System.out.println(defaultMapper.selectDefaultUser());
System.out.println("==========================");
System.out.println(salveMapper.selectSalveUser());
System.out.println("==========================");
System.out.println(defaultMapper.selectDefaultUser());
}
}
这样就可以观察日志数据源切换和查询出来的数据是否正确
七、(方式二) Mybatis插件切换
除了使用AOP的方式来实现数据源切换,也可以用Mybatis的插件拦截器,在每次执行SQL的时候,都会判断下当前方法或者类上是否存在@DS注解,然后根据类型切换数据源
如果使用这种方式,请把AOP拦截器相关代码给注释了,也即是DataSourceAop.class类
实现拦截器
java
/**
* @author hwsun3
* @date 2025/3/27
* @desc
*/
@Slf4j
@Intercepts
(
{
// 这里应该就是重载Executor.class类的方法,参数参考里面的
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
}
)
public class DataSourceInterceptor implements Interceptor {
private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>(256);
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
try {
handleDataSourceSwitch(ms);
return invocation.proceed();
} finally {
DynamicDataSourceConfig.DsTypeContainer.clearDataSource();
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
System.out.println("mybatis-plugin-properties:===========");
System.out.println(properties);
}
private void handleDataSourceSwitch(MappedStatement ms) {
try {
// 1. 解析Mapper接口信息
String mapperClassName = ms.getId().substring(0, ms.getId().lastIndexOf("."));
String methodName = ms.getId().substring(ms.getId().lastIndexOf(".") + 1);
// 2. 获取原始接口类(处理动态代理)
Class<?> mapperInterface = ClassUtils.resolveClassName(mapperClassName, ClassUtils.getDefaultClassLoader());
Class<?> userClass = ClassUtils.getUserClass(mapperInterface);
// 3. 获取方法对象(带缓存)
Method method = METHOD_CACHE.computeIfAbsent(ms.getId(), k -> {
try {
// 通过MappedStatement获取实际参数类型
Class<?>[] paramTypes = ms.getParameterMap().getParameterMappings()
.stream()
.map(ParameterMapping::getJavaType)
.toArray(Class[]::new);
return userClass.getMethod(methodName, paramTypes);
} catch (NoSuchMethodException e) {
log.warn("Method not found: {}.{}", mapperClassName, methodName);
return null;
}
});
if (method == null) {
return;
}
// 4. 注解查找(支持接口继承)
DS dsAnnotation = AnnotationUtils.findAnnotation(method, DS.class);
if (dsAnnotation == null) {
dsAnnotation = AnnotationUtils.findAnnotation(userClass, DS.class);
}
// 5. 设置数据源
if (dsAnnotation != null) {
DynamicDataSourceConfig.DsTypeContainer.setDataSource(dsAnnotation.value());
log.debug("Switched to datasource: {} for {}.{}",
dsAnnotation.value(), userClass.getSimpleName(), method.getName());
}
} catch (Exception e) {
log.error("DataSourceInterceptor error: {}", e.getMessage(), e);
}
}
}
MyBatisConfig配置拦截器
java
@Configuration
public class MyBatisConfig {
@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return configuration -> {
// 添加自定义拦截器
configuration.addInterceptor(new DataSourceInterceptor());
};
}
}
作者:Mr.Sun | 「Java技术宇宙」主理人
专注分享硬核技术干货与编程实践,让编程之路更简单。
📖 深度文章:个人博客「Mr.Sun的博客」
🚀 最新推送:微信公众号「Java技术宇宙」
加我为好友(sunhw0305),备注"加群"免费加入技术交流群