首先引入相关依赖,只列出了核心部分
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
接下来是具体的实现步骤。
保证数据源在同一线程下切换后不被其他线程修改,所以我们将数据源信息保存在ThreadLocal中共享。
package com.myfund.wxapplet.confignew;
/**
* 动态数据源持有者,负责利用ThreadLocal存取数据源名称
*
* @author: haocheng
* @date: 2019-06-11 15:32
*
*/
public class DynamicDataSourceHolder {
//本地线程共享对象
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void putDataSouce(String name){
THREAD_LOCAL.set(name);
}
public static String getDataSource(){
return THREAD_LOCAL.get();
}
public static void removeDataSource(){
THREAD_LOCAL.remove();
}
}
spring为我们提供了AbstractRoutingDataSource,即带路由的数据源。继承后我们需要实现它的determineCurrentLookupKey(),该方法用于自定义实际数据源名称的路由选择方法,由于我们将信息保存到了ThreadLocal中,所以只需要从中拿出来即可
package com.myfund.wxapplet.confignew;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源实现类
*
* @author: haocheng
* @date: 2019-06-11 15:36
*
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
//数据源路由,此方用于产生要选取的数据源逻辑名称
@Override
protected Object determineCurrentLookupKey() {
//从线程共享中获取数据源名称
return DynamicDataSourceHolder.getDataSource();
}
}
我们切换数据源时,一般都是在调用mapper或service接口的方法前实现,所以我们定义一个方法注解,当AOP检测到方法上有该注解时,根据注解中value对应的名称进行切换。
package com.myfund.wxapplet.confignew;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 目标数据源注解,注解在方法上指定数据源的名称
*
* @author: haocheng
* @date: 2019-06-11 15:39
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
//此处接收数据源名称
String value();
}
动态数据源切换是基于AOP的,所以我们需要声明一个AOP切面,并在切面前做数据源切换,切面完成后移除数据源名称。
package com.myfund.wxapplet.confignew;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 数据源AOP切面定义
*
* @author: haocheng
* @date: 2019-06-11 15:41
*/
@Component
@Aspect
@Slf4j
public class DataSourceAspect {
//切入点在service层的方法上,配置aop的切入点
@Pointcut("execution( * com.myfund.wxapplet.service..*.*(..))")
public void dataSourcePointCut() {
}
//切入点只对@Service注解的类上的@DataSource方法生效
// @Pointcut(value="@within(org.springframework.stereotype.Service) && @annotation(dataSource)" )
// public void dataSourcePointCut(DataSource dataSource) {
// }
@Before("dataSourcePointCut()")
public void before(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
String method = joinPoint.getSignature().getName();
Class<?>[] clazz = target.getClass().getInterfaces();
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
try {
Method m = clazz[0].getMethod(method, parameterTypes);
//如果方法上存在切换数据源的注解,则根据注解内容进行数据源切换
if (m != null && m.isAnnotationPresent(DataSource.class)) {
DataSource annotation = m.getAnnotation(DataSource.class);
String dataSourceName = annotation.value();
DynamicDataSourceHolder.putDataSouce(dataSourceName);
log.debug("-----current thread " + Thread.currentThread().getName() + " add " + dataSourceName + " to ThreadLocal-----");
} else {
log.debug("switch datasource fail, use default");
}
} catch (NoSuchMethodException e) {
log.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
}
}
//执行完切面后,清空线程共享中的数据源名称
@After("dataSourcePointCut()")
public void after(JoinPoint joinPoint){
DynamicDataSourceHolder.removeDataSource();
}
}
使用springboot自带的默认连接池Hikari,先定义一个配置类
package com.myfund.wxapplet.confignew;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 实际数据源配置
*
* @author: haocheng
* @date: 2019-06-11 16:17
*
*/
@Component
@Data
@ConfigurationProperties(prefix = "spring.datasource")
public class DBProperties {
private HikariDataSource primary;
private HikariDataSource secondary;
private HikariDataSource thirdary;
private HikariDataSource fourthary;
private HikariDataSource fifthary;
}
application.yml配置内容
spring:
datasource:
primary:
database: sql_server
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
jdbc-url: jdbc:sqlserver://10.20.34.122;DatabaseName=SecondaryData_New
username:
password:
max-active: 10
max-idle: 5
min-idle: 5
secondary:
database: mysql
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/wxapplet?characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
username:
password:
max-active: 10
max-idle: 5
min-idle: 5
thirdary:
database: sql_server
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
jdbc-url: jdbc:sqlserver://10.20.34.122;DatabaseName=PrimaryData_New
username:
password:
max-active: 10
max-idle: 5
min-idle: 5
fourthary:
database: sql_server
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
jdbc-url: jdbc:sqlserver://10.20.34.121;DatabaseName=CMS_NEW
username:
password:
max-active: 10
max-idle: 5
min-idle: 5
fifthary:
database: sql_server
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
jdbc-url: jdbc:sqlserver://10.20.34.122;DatabaseName=GPDB
username:
password:
max-active: 10
max-idle: 5
min-idle: 5
package com.myfund.wxapplet.confignew;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源配置
*
* @author: haocheng
* @date: 2019-06-11 16:26
*
*/
@Configuration
@EnableScheduling
@Slf4j
public class DataSourceConfig {
@Autowired
private DBProperties dbProperties;
/**
* 设置动态数据源,通过@Primary 来确定主DataSource
*
*/
@Bean(name = "dataSource")
public DynamicDataSource dataSource(){
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//1.设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(dbProperties.getPrimary());
//2.配置多数据源
Map<Object, Object> map = new HashMap<>();
map.put("primary", dbProperties.getPrimary());
map.put("secondary", dbProperties.getSecondary());
map.put("thirdary", dbProperties.getThirdary());
map.put("fourthary", dbProperties.getFourthary());
map.put("fifthary", dbProperties.getFifthary());
//3.存放数据源集
dynamicDataSource.setTargetDataSources(map);
return dynamicDataSource;
}
}
public interface UserInfoMapper {
/**
* 默认从primary数据源中获取用户信息
*/
UserInfo selectByOddUserId(Integer id);
/**
* 指定从thirdary数据源中获取用户信息
*/
@DataSource("thirdary")
UserInfo selectByEvenUserId(Integer id);
}
因为使用了spring-boot会自动配置Autoconfiguration,所以我们需要在启动类注解上作如下修改,不让spring-boot给我们自动配置。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)