Springboot2.x总结——AOP实现多数据源配置与动态切换

首先引入相关依赖,只列出了核心部分

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
dependency>

接下来是具体的实现步骤。

1. 创建线程共享工具

保证数据源在同一线程下切换后不被其他线程修改,所以我们将数据源信息保存在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();
    }
}

2. 实现动态数据源——AbstractRoutingDataSource

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();
    }
}

3. 自定义方法注解——数据源切换

我们切换数据源时,一般都是在调用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();
}

4. 定义处理AOP切面

动态数据源切换是基于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();
    }

}

5. 定义多个数据源

使用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

6. 采用@Bean注解完成动态数据源对象的申明

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;
    }
}

7. 在mapper或service接口方法上做切换

public interface UserInfoMapper {
    /**
     * 默认从primary数据源中获取用户信息
     */
    UserInfo selectByOddUserId(Integer id);
    /**
     * 指定从thirdary数据源中获取用户信息
     */
    @DataSource("thirdary")
    UserInfo selectByEvenUserId(Integer id);
}

8.去除springboot数据源的自动配置

因为使用了spring-boot会自动配置Autoconfiguration,所以我们需要在启动类注解上作如下修改,不让spring-boot给我们自动配置。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

你可能感兴趣的:(Springboot)