[spring boot]动态切换数据源

470 阅读7分钟

该主题概念

多数据源动态切换常用语项目中读写分离。多数据源配置参考下上一篇文章: 多数据源配置

动态切换数据源demo

其实和多数据源配置差不多,你在看完多数据源配置后,需要有一个切换数据源的概念,spring中提供了AbstractRoutingDataSource抽象类来实现。下面配置多数据源的类如出一辙,看明白的可以直接在之前的类里操作。

注销默认配置

注销spring boot自动注入数据源

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AccessingChooseDataSourceApplication {

	public static void main(String[] args) {
		SpringApplication.run(AccessingChooseDataSourceApplication.class, args);
	}
	
}
配置切换所需类

数据源切换该类起切换作用,根据aop注解的方式,与map.key将对应要切换的数据源注入。

package com.example.demo.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created by 神烦 on 2019/9/6.
 */
public class DataSourceContextHolder {

    private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);

    // 默认数据源
    public static String dataSourceName = "primarySource";

    // 防止数据污染
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    // 设置数据源名
    public static void setDataSourceName(String dbType) {
        logger.info("切换 ->{} 数据源", dbType);
        threadLocal.set(dbType);
    }

    // 获取数据源名
    public static String getDataSourceName() {
        return (threadLocal.get());
    }

    // 清除数据源名
    public static void clearDataSourceName() {
        threadLocal.remove();
    }
}

AbstractRoutingDataSource是切换数据源的核心类,在访问数据库时 会调用determineCurrentLookupKey方法,通过此方法实现切换数据源。

package com.example.demo.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * Created by 神烦 on 2019/9/6.
 * 获取数据源信息
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    private static Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);

    // 在访问数据库时 会调用determineCurrentLookupKey方法
    @Override
    protected Object determineCurrentLookupKey() {
        logger.info("多数据源配置获取数据源信息 :{}",DataSourceContextHolder.getDataSourceName());
        return DataSourceContextHolder.getDataSourceName();
    }
}
配置多数据源

先看代码,下面我讲一下该类的作用

package com.example.demo.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by 神烦 on 2019/9/4.
 * 因为去掉了数据源自动加载 所有要手动加载数据源
 */
@Configuration
@MapperScan(basePackages = {"com.example.demo.mapper"}, sqlSessionTemplateRef = "dynamicSqlSessionTemplate")
public class DataSourceConfig {

    // 主库
    @Primary
    @Bean(name = "primarySource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource PrimarySource() {
        return DataSourceBuilder.create().build();
    }

    // 从库
    @Bean(name = "secondSource")
    @ConfigurationProperties(prefix = "spring.datasource.second")
    public DataSource SecondSource() {
        return DataSourceBuilder.create().build();
    }

    // 主从数据源注入 通过aop切换
    @Bean(name = "dynamicSource")
    public DataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 默认数据源
        dynamicDataSource.setDefaultTargetDataSource(SecondSource());  // 默认数据源

        // 配置多数据源
        Map<Object, Object> map = new HashMap(5);
        map.put("primarySource", PrimarySource());
        map.put("secondSource", SecondSource());
        dynamicDataSource.setTargetDataSources(map); // 将多数据源注入

        return dynamicDataSource;
    }


    // 之前把该类分开 通过Authwired dynamicSource
    // 会造成多数据源AbstractRoutingDataSource配置访问不成功的问题 从而造成多数据源切换失败的问题
    @Bean
    public SqlSessionFactoryBean dynamicSqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        return factoryBean;
    }

    // SqlSeesionTemplate是关键一步 将多数据源信息注入
    @Bean
    public SqlSessionTemplate dynamicSqlSessionTemplate() throws Exception {
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(dynamicSqlSessionFactory().getObject());
        return sqlSessionTemplate;
    }
}

这里的配置需要讲一下,DynamicDataSource是DataSource下实现动态切换的类,既然是动态切换,那么就是多数据源,这里我们需要把需要动态切换的类注入,并设置一下默认的数据源。重点来了,之前在整合的时候,为了美观我将动态数据类DynamicDataSource的其他配置放在了另一个类,然后通过authwired注入该DynamicDataSource,结果就是死活都切换不了,后来东改西改,最后才明白问题出现在注入方式上。望众知。

aop切换

添加注释,方便切换数据源

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by 神烦 on 2019/9/6.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSourceChoose {
    String value() default "primarySource";
}

切面实现类

package com.example.demo.aspect;

import com.example.demo.annotation.DataSourceChoose;
import com.example.demo.config.DataSourceContextHolder;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * Created by 神烦 on 2019/9/6.
 */
@Aspect
@Component
public class DynamicDataSourceAspect {
    private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    @Pointcut("execution(public * com.example.demo..*(..)) && @annotation(com.example.demo.annotation.DataSourceChoose))")
    public void pointCutUtil(){

    }

    // 获取注释 更换数据源
    @Before("pointCutUtil()")
    public void updateDataSource(JoinPoint joinPoint){
        logger.info("进入切面类......");
        // 获取当前访问的类
        Class<?> className = joinPoint.getTarget().getClass();
        // 获取访问方法的方法名
        String methodName = joinPoint.getSignature().getName();
        // 获取方法参数的类型
        Class[] argClass = ((MethodSignature)joinPoint.getSignature()).getParameterTypes();
        String dataSource = DataSourceContextHolder.dataSourceName;
        try{
            // 获取访问的对象
            java.lang.reflect.Method method = className.getMethod(methodName, argClass);
            // 判断切面方法是否有注释
            if (method.isAnnotationPresent(DataSourceChoose.class)){
                DataSourceChoose annotation = method.getAnnotation(DataSourceChoose.class);
                // 注解参数
                dataSource = annotation.value();
            }
        }catch (NoSuchMethodException e){
            e.getMessage();
        }
        // 切换数据源
        DataSourceContextHolder.setDataSourceName(dataSource);
        logger.info("切换数据源成功...... {}", dataSource);
    }

    @After("pointCutUtil()")
    public void clearDataSource(JoinPoint joinPoint){
        DataSourceContextHolder.clearDataSourceName();
    }

}
dao

再加个数据访问层就ok了,和多数据源相比的话就不需要写多个mapper,少写了不少代码了,代价就是配置略微麻烦。

package com.example.demo.mapper;

import com.example.demo.model.Test;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

/**
 * Created by 神烦 on 2019/9/6.
 */
@Mapper
public interface TestMapper {
    @Select("select * from t_test")
    Test PrimaryTest();
}
测试

创建个controller测试一下来

package com.example.demo.controller;

import com.example.demo.annotation.DataSourceChoose;
import com.example.demo.mapper.TestMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * Created by 神烦 on 2019/9/6.
 */
@RestController
public class MoreDataSourceController {
    private static Logger logger = LoggerFactory.getLogger(MoreDataSourceController.class);

    @Resource
    public TestMapper testMapper;

    @RequestMapping("/primaryTest.json")
    @DataSourceChoose(value = "primarySource")
    public void primaryTest() {
        logger.info("primary测试接口...... {}", testMapper.PrimaryTest().toString());
    }

    @RequestMapping("/secondTest.json")
    @DataSourceChoose(value = "secondSource")
    public void secondTest() {
        logger.info("second数据源测试接口...... {}", testMapper.PrimaryTest().toString());
    }
}

测试结果:

2019-09-07 15:41:31.415  INFO 5456 --- [           main] e.d.AccessingChooseDataSourceApplication : Starting AccessingChooseDataSourceApplication on USER-20180705XA with PID 5456 (D:\accessingChooseDataSource\target\classes started by 神烦 in D:\accessingChooseDataSource)
2019-09-07 15:41:31.417  INFO 5456 --- [           main] e.d.AccessingChooseDataSourceApplication : No active profile set, falling back to default profiles: default
2019-09-07 15:41:33.735  INFO 5456 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2019-09-07 15:41:33.762  INFO 5456 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-09-07 15:41:33.763  INFO 5456 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.24]
2019-09-07 15:41:33.876  INFO 5456 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-09-07 15:41:33.876  INFO 5456 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2362 ms
2019-09-07 15:41:34.626  INFO 5456 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-07 15:41:35.069  INFO 5456 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-07 15:41:35.072  INFO 5456 --- [           main] e.d.AccessingChooseDataSourceApplication : Started AccessingChooseDataSourceApplication in 4.364 seconds (JVM running for 4.939)
2019-09-07 15:41:45.570  INFO 5456 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-09-07 15:41:45.571  INFO 5456 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2019-09-07 15:41:45.587  INFO 5456 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 16 ms
2019-09-07 15:41:45.626  INFO 5456 --- [nio-8080-exec-1] c.e.demo.aspect.DynamicDataSourceAspect  : 进入切面类......
2019-09-07 15:41:45.629  INFO 5456 --- [nio-8080-exec-1] c.e.demo.config.DataSourceContextHolder  : 切换 ->primarySource 数据源
2019-09-07 15:41:45.632  INFO 5456 --- [nio-8080-exec-1] c.e.demo.aspect.DynamicDataSourceAspect  : 切换数据源成功...... primarySource
2019-09-07 15:41:45.658  INFO 5456 --- [nio-8080-exec-1] c.example.demo.config.DynamicDataSource  : 多数据源配置获取数据源信息 :primarySource
2019-09-07 15:41:45.659  INFO 5456 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-09-07 15:41:45.955  INFO 5456 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-09-07 15:41:46.016  INFO 5456 --- [nio-8080-exec-1] c.e.d.c.MoreDataSourceController         : primary测试接口...... Test(id=99999, name=主库, remark=主库数据库)
2019-09-07 15:41:53.804  INFO 5456 --- [nio-8080-exec-4] c.e.demo.aspect.DynamicDataSourceAspect  : 进入切面类......
2019-09-07 15:41:53.804  INFO 5456 --- [nio-8080-exec-4] c.e.demo.config.DataSourceContextHolder  : 切换 ->secondSource 数据源
2019-09-07 15:41:53.805  INFO 5456 --- [nio-8080-exec-4] c.e.demo.aspect.DynamicDataSourceAspect  : 切换数据源成功...... secondSource
2019-09-07 15:41:53.805  INFO 5456 --- [nio-8080-exec-4] c.example.demo.config.DynamicDataSource  : 多数据源配置获取数据源信息 :secondSource
2019-09-07 15:41:53.805  INFO 5456 --- [nio-8080-exec-4] com.zaxxer.hikari.HikariDataSource       : HikariPool-2 - Starting...
2019-09-07 15:41:53.923  INFO 5456 --- [nio-8080-exec-4] com.zaxxer.hikari.HikariDataSource       : HikariPool-2 - Start completed.
2019-09-07 15:41:53.935  INFO 5456 --- [nio-8080-exec-4] c.e.d.c.MoreDataSourceController         : second数据源测试接口...... Test(id=99999, name=从库, remark=从库数据库)
总结

dynamicDataSource多数据源需要注入该类,spring实现多数据源切换得类AbstractRoutingDataSource,在访问数据源时会请求该类下的determineCurrentLookupKey方法,注解要注意别写错了。基本就没啥问题了。

源码地址: github.com/Liyinzuo/ac…

如果有什么不足或者错误的地方欢迎指正~