springboot + druid + Aspectj +Jpa(hibernate) 实现动态数据源切换
我们开发中经常会遇到连接多个数据库的操作,但目前常用的orm框架mybatis、hibernate默认都是只能连接一个数据库。使用原生JDBC虽然可以连接多个数据库,但是却不能应用各种框架所提供的便利。此处提供一种本菜鸟开发中经常使用的多数据源切换方式。
本文先介绍详细的使用步骤,具体的流程讲解在文末进行描述。
1、导入依赖(此处只贴出了核心依赖)
<!-- postgresql -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- spring-data-jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>${hibernate.version}</version>
<exclusions>
<exclusion>
<artifactId>jts-core</artifactId>
<groupId>com.vividsolutions</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
2、在application.properties中配置多个数据源(此处为了简单,只配置了两个)
spring.datasource.druid.test1.url=jdbc:postgresql://10.194.98.235:7017/testdb1
spring.datasource.druid.test1.username=testdb1_user
spring.datasource.druid.test1.password=Owa6TasO
spring.datasource.druid.test1.driverClassName=org.postgresql.Driver
spring.datasource.druid.test2.url=jdbc:postgresql://10.194.101.37:5432/testdb2
spring.datasource.druid.test2.username=postgres
spring.datasource.druid.test2.password=postgres
spring.datasource.druid.test2.driverClassName=org.postgresql.Driver
3、配置数据源注入到spring的容器中(关键点:ConfigurationProperties注解、DruidDataSourceBuilder)
@Configuration
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.test1")
public DataSource timDatasource() {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
@Bean
@ConfigurationProperties("spring.datasource.druid.test2")
public DataSource xresDatasource() {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
@Bean
@Primary
public DynamicDataSource dataSource(DataSource timDatasource,DataSource xresDatasource){
Map<Object,Object> targetDataSource = new HashMap<>();
targetDataSource.put("test1",timDatasource);
targetDataSource.put("test2",xresDatasource);
return new DynamicDataSource(timDatasource,targetDataSource);
}
}
4、配置数据源管理类,用于在创建session前,切换数据源。
public class DynamicDataSource extends AbstractRoutingDataSource{
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
private static Map<Object,Object> allTargetDataSource = null;
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object,Object> targetDataSource){
this.allTargetDataSource = targetDataSource;
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSource);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
public static void setDataSource(String dataSource){
contextHolder.set(dataSource);
}
public static String getDataSource(){
return contextHolder.get();
}
public static void clearDataSource(){
contextHolder.remove();
}
}
5、自定义注解,用于定义哪些方法需要切换到哪种数据源。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfDataSource {
String name() default "";
}
6、AOP切面类,用于监测使用了第五步自定义注解的方法,并调用第四步创建的数据源管理类中的方法,来切换方法执行前使用的数据源。
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.wyt01datasource.modules.dynamicdatasource.SelfDataSource)")
public void dataSourcePointCut(){
}
@Pointcut(value = "execution(* com.wyt01datasource.modules.service.*(..))")
public void controlMethod(){
}
@Before("controlMethod()")
public void dataSourceChange(){
System.out.print("更改数据源为test1");
DynamicDataSource.setDataSource("test1");
}
@After("controlMethod()")
public void clearDataSource(){
System.out.print("清除数据源test1");
DynamicDataSource.clearDataSource();
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable{
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
SelfDataSource dataSource = method.getAnnotation(SelfDataSource.class);
if (dataSource==null){
DynamicDataSource.setDataSource("test2");
System.out.println("切换数据源为:test2");
}else {
DynamicDataSource.setDataSource(dataSource.name());
System.out.println("切换数据源为:"+dataSource.name());
}
try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
}
}
}
7、使用
当我们需要切换数据源时,在对应方法上添加 @SelfDataSource(name = "test1") 的注解即可
8、流程讲解
项目启动时,首先是通过ConfigurationProperties注解将属性文件中的多个数据源配置生成DataSource对象,并配置到抽象类AbstractRoutingDataSource中,该类可以通过key选择当前使用的数据源。我们在刚才使用时,就是通过静态代理Aspectj框架拦截需要切换数据源的方法,然后通过注解中需要切换的数据源的值来设置AbstractRoutingDataSource中的方法需要切换的数据源。
9、踩坑
项目启动时会自动加载数据源,而由于使用了spring-boot-starter-data-jpa依赖,在调用数据源的时候会出现循环依赖报错。这是由于springboot的自动加载机制导致的(具体原因本想追踪源码解释一下,奈何功力太浅,只追踪到了循环的方法。原理则解释不清)。其解决方式是在启动类上添加
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
即配置不让springboot启动时自动加载数据源。而且经过本人实现,如果不通过spring-data-jpa的方式,使用mybatis则不会出现循环依赖的问题。