转载

springboot使用切面方式(自定义注解)动态连接多数据源

前言

springboot里面可以有多种方式配置多数据源。之前写过将mapper与指定数据源锁定,但是随着项目的扩大,动态使用多数据源已经不可避免,这些天将代码搭建好了并成功上线使用。记录以供各路大神重复造轮子。(ps:springboot版本 1.5.9)

配置以及代码

分析:动态切换数据源,我们可不可以自定义一个注解,然后在需要切换数据源的地方加上自定义的注解,让程序自动切换呢?很显然是可以的。

自定义注解:

/**
 * create by liuliang
 * on 2019-09-03  19:16
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MFTDataSource {
    DataSourceEnum value() default DataSourceEnum.DB_EXPEND;
}

注解定义好了,我们需要有一个切面,去环绕这个注解,做我们需要的增强。为了简洁方便,我将使用枚举来切换多数据源。

/**
 * create by liuliang
 * on 2019-09-03  19:17
 */
@Component
@Slf4j
@Aspect
@Order(-1)
public class DataSourceAspect {


    @Pointcut("@within(com.meifute.expend.config.datasource.MFTDataSource) || @annotation(com.meifute.expend.config.datasource.MFTDataSource)")
    public void pointCut(){

    }

    @Before("pointCut() && @annotation(dataSource)")
    public void doBefore(MFTDataSource dataSource){
        log.info("===========选择数据源=========>>> "+dataSource.value().getValue());
        DataSourceContextHolder.setDataSource(dataSource.value().getValue());
    }

    @After("pointCut()")
    public void doAfter(){
        DataSourceContextHolder.clear();
    }
}

枚举类:

/**
 * create by liuliang
 * on 2019-09-03  19:14
 */
public enum DataSourceEnum {

    DB_EXPEND("expend"),DB_USER("user"),DB_AGENT("agent"),DB_ADMIN("admin");

    private String value;

    DataSourceEnum(String value){this.value=value;}

    public String getValue() {
        return value;
    }
}

上面定义好了以后,我们需要使用一个线程变量,来存放当前注解所在线程的连接信息,在进入注解所在方法的时候,给线程变量赋值指定数据源连接,在结束的时候,就清理当前线程:

/**
 * create by liuliang
 * on 2019-09-03  19:12
 */
public class DataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<>();

    /**
     *  设置数据源
     * @param db
     */
    public static void setDataSource(String db){
        contextHolder.set(db);
    }

    /**
     * 取得当前数据源
     * @return
     */
    public static String getDataSource(){
        return contextHolder.get();
    }

    /**
     * 清除上下文数据
     */
    public static void clear(){
        contextHolder.remove();
    }
}

当然,我们需要把切换数据源的工作交给spring,这里继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法,使用我们自定义的线程变量获取当前数据源。

/**
 * create by liuliang
 * on 2019-09-03  19:14
 */
public class MultipleDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

当然,最重要的多数据源配置,我们可以这样配置:

@Configuration
@MapperScan("com.meifute.expend.mapper.*")
public class MyBatiesPlusConfiguration {

    //expend
    @Bean(name = "expend")
    @ConfigurationProperties(prefix = "spring.datasource.druid.expend" )
    public DataSource expend() {
        DruidDataSource build = DruidDataSourceBuilder.create().build();
        return build;
    }

    //user
    @Bean(name = "user")
    @ConfigurationProperties(prefix = "spring.datasource.druid.user" )
    public DataSource user() {
        return DruidDataSourceBuilder.create().build();
    }


    //agent
    @Bean(name = "agent")
    @ConfigurationProperties(prefix = "spring.datasource.druid.agent" )
    public DataSource agent() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "admin")
    @ConfigurationProperties(prefix = "spring.datasource.druid.admin" )
    public DataSource admin() {
        return DruidDataSourceBuilder.create().build();
    }
    /**
     * 动态数据源配置
     * @return
     */
    @Bean
    @Primary
    public DataSource multipleDataSource(@Qualifier("expend") DataSource expend,
                                         @Qualifier("user") DataSource user,
                                         @Qualifier("agent") DataSource agent,
                                         @Qualifier("admin") DataSource admin
                                         ) {
        MultipleDataSource multipleDataSource = new MultipleDataSource();
        Map< Object, Object > targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnum.DB_EXPEND.getValue(), expend);
        targetDataSources.put(DataSourceEnum.DB_USER.getValue(), user);
        targetDataSources.put(DataSourceEnum.DB_AGENT.getValue(), agent);
        targetDataSources.put(DataSourceEnum.DB_ADMIN.getValue(), admin);
        //添加数据源
        multipleDataSource.setTargetDataSources(targetDataSources);
        //设置默认数据源
        multipleDataSource.setDefaultTargetDataSource(expend());
        return multipleDataSource;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(multipleDataSource(expend(),user(),agent(),admin()));
//        sqlSessionFactory.setDataSource(db2());
        sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*Mapper.xml"));

        MybatisConfiguration configuration = new MybatisConfiguration();
        //configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sqlSessionFactory.setConfiguration(configuration);
        PaginationInterceptor pagination = new PaginationInterceptor();
        pagination.setDialectType("mysql");
        sqlSessionFactory.setPlugins(new Interceptor[]{ //PerformanceInterceptor(),OptimisticLockerInterceptor()
                pagination //添加分页功能
        });
        return sqlSessionFactory.getObject();
    }

}


@Configuration
public class DruidConfiguration {

    @Bean
    public ServletRegistrationBean startViewServlet(){
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
        // IP白名单
        servletRegistrationBean.addInitParameter("allow","127.0.0.1");
        // IP黑名单(共同存在时,deny优先于allow)
        servletRegistrationBean.addInitParameter("deny","127.0.0.1");
        //控制台管理用户
        servletRegistrationBean.addInitParameter("loginUsername","admin");
        servletRegistrationBean.addInitParameter("loginPassword","123456");
        //是否能够重置数据
        servletRegistrationBean.addInitParameter("resetEnable","false");
        return servletRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean statFilter(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
        //添加过滤规则
        filterRegistrationBean.addUrlPatterns("/*");
        //忽略过滤的格式
        filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }
}

好了,到此代码层面就配置结束了,需要在我们的配置文件,与MyBatiesPlusConfiguration配置的对应即可(为了安全,我把关键信息隐藏了,各位大佬记得改为自己的连接方式):

spring:
  datasource:
    druid:
      expend:
        url: jdbc:mysql://172.19.95.**:3332/mft_expend?useUnicode=true&characterEncoding=utf8
        username: **
        password: **
        driver-class-name: com.mysql.jdbc.Driver
      user:
        url: jdbc:mysql://172.19.95.**:3332/m_mall_user?useUnicode=true&characterEncoding=utf8
        username: **
        password: **
        driver-class-name: com.mysql.jdbc.Driver
      agent:
        url: jdbc:mysql://www.**.cn:3306/m_mall_agent?useUnicode=true&characterEncoding=utf8
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver
      admin:
        url: jdbc:mysql://172.19.95.**:3332/m_mall_admin?useUnicode=true&characterEncoding=utf8
        username: **
        password: **
        driver-class-name: com.mysql.jdbc.Driver

至此,多数据源动态切换配置结束。下面我们看一下使用方式和注意事项:

使用方式

controller:

/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *
 * @author liuliang
 * @since 2019-09-07
 */
@Slf4j
@RestController
@RequestMapping("/expend/admin")
@Api(tags = "admin平台", value = "admin平台")
public class AdminController extends BaseController {

    @Autowired
    private AdminServiceImpl adminService;


    @GetMapping("/test")
    public void test(){
        adminService.test();;
    }
}

service:

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author liuliang
 * @since 2019-09-07
 */
@Service
@Slf4j
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {

    @Autowired
    private MallAgentChangeLogServiceImpl changeLogService;
    
    @Override
    public void test(){

        changeLogService.test1();
        changeLogService.test2();

      }
}

changeLogService

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author liuliang
 * @since 2019-09-05
 */
@Service
@Slf4j
public class MallAgentChangeLogServiceImpl extends ServiceImpl<MallAgentChangeLogMapper, MallAgentChangeLog> implements IMallAgentChangeLogService {




    @MFTDataSource(DataSourceEnum.DB_AGENT)
    public void test1(){
        log.info(this+"");
//        adminMapper.test1();
    }


    @MFTDataSource(DataSourceEnum.DB_EXPEND)
    public void test2(){
        log.info(this+"");
//        adminMapper.test2();
    }
}

执行代码:通过日志我们可以看到,按照我们预期的效果动态切换了,非常棒!

2019-09-24 10:11:01,916 INFO [http-nio-5550-exec-1] [DirectJDKLog.java:179] Initializing Spring FrameworkServlet 'dispatcherServlet'
2019-09-24 10:11:01,916 INFO [http-nio-5550-exec-1] [FrameworkServlet.java:489] FrameworkServlet 'dispatcherServlet': initialization started
2019-09-24 10:11:01,949 INFO [http-nio-5550-exec-1] [FrameworkServlet.java:508] FrameworkServlet 'dispatcherServlet': initialization completed in 33 ms
2019-09-24 10:11:01,999 INFO [http-nio-5550-exec-1] [RequestLogAdvice.java:68] -----------GET-------------request[/expend/admin/test]--start--[request params:{}]----------
2019-09-24 10:11:02,012 INFO [http-nio-5550-exec-1] [DataSourceAspect.java:32] ===========选择数据源=========>>> agent
2019-09-24 10:11:02,017 INFO [http-nio-5550-exec-1] [MallAgentChangeLogServiceImpl.java:29] com.meifute.expend.service.impl.MallAgentChangeLogServiceImpl@81a56b2
2019-09-24 10:11:02,018 INFO [http-nio-5550-exec-1] [DataSourceAspect.java:32] ===========选择数据源=========>>> expend
2019-09-24 10:11:02,018 INFO [http-nio-5550-exec-1] [MallAgentChangeLogServiceImpl.java:36] com.meifute.expend.service.impl.MallAgentChangeLogServiceImpl@81a56b2
2019-09-24 10:11:02,113 INFO [http-nio-5550-exec-1] [RequestLogAdvice.java:83]  ----------------------response[/expend/admin/test]--end--[耗时:19ms,response result:null, response path[/expend/admin/test]]----------

其实到这里多数据源动态连接就告一段落了,但是这里会涉及到一个知识点,我也是踩坑才爬出来的,下面记录一下:

我们看到们,刚刚我们从adminservice调用了两次ChangeLogService,如果我们直接从adminservice调用自己类里面的方法两次,会不会有同样的效果呢?测试一下:

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author liuliang
 * @since 2019-09-07
 */
@Service
@Slf4j
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {

    @Autowired
    private MallAgentChangeLogServiceImpl changeLogService;

    @Override
    public void test(){
        test1();
        test2();
//        changeLogService.test1();
//        changeLogService.test2();

    }


    @MFTDataSource(DataSourceEnum.DB_AGENT)
    public void test1(){
        log.info(this+"");

    }


    @MFTDataSource(DataSourceEnum.DB_EXPEND)
    public void test2(){
        log.info(this+"");
    }

}

其他代码不变,我们调用自己对象的两个方法后会惊奇的发现,数据源切换的aop居然一次都没有拦截到(没有打印“选择数据源=========>>> XX”):

2019-09-24 10:16:41,738 INFO [http-nio-5550-exec-1] [DirectJDKLog.java:179] Initializing Spring FrameworkServlet 'dispatcherServlet'
2019-09-24 10:16:41,739 INFO [http-nio-5550-exec-1] [FrameworkServlet.java:489] FrameworkServlet 'dispatcherServlet': initialization started
2019-09-24 10:16:41,766 INFO [http-nio-5550-exec-1] [FrameworkServlet.java:508] FrameworkServlet 'dispatcherServlet': initialization completed in 27 ms
2019-09-24 10:16:41,917 INFO [http-nio-5550-exec-1] [RequestLogAdvice.java:68] -----------GET-------------request[/expend/admin/test]--start--[request params:{}]----------
2019-09-24 10:16:41,949 INFO [http-nio-5550-exec-1] [AdminServiceImpl.java:43] com.meifute.expend.service.impl.AdminServiceImpl@2b98c310
2019-09-24 10:16:41,950 INFO [http-nio-5550-exec-1] [AdminServiceImpl.java:50] com.meifute.expend.service.impl.AdminServiceImpl@2b98c310
2019-09-24 10:16:42,090 INFO [http-nio-5550-exec-1] [RequestLogAdvice.java:83]  ----------------------response[/expend/admin/test]--end--[耗时:33ms,response result:null, response path[/expend/admin/test]]----------

到底是为什么呢?其实原因和动态代理有关。这里我给出结果和怎么处理,具体的解释大姐可以参考这位大佬的文章,解析的很详细我就不重复了。

原因:SpringAOP对于最外层的函数只拦截public方法,不拦截protected和private方法,另外不会对最外层的public方法内部调用的其他方法也进行拦截,即只停留于代理对象所调用的方法。

解决方法:

在配置文件将动态代理的bean暴露出来,然后aop获取到当前的动态代理对象,再去调用就好了(解释连接: https://www.cnblogs.com/chihi... ):

@MapperScan("com.meifute.expend.mapper")
@SpringBootApplication
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass=true,exposeProxy = true)//就是这个配置!!!
public class ExpendApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExpendApplication.class, args);
    }

}

然后service使用的时候,这样获取到当前动态代理对象:

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author liuliang
 * @since 2019-09-07
 */
@Service
@Slf4j
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {

    @Autowired
    private MallAgentChangeLogServiceImpl changeLogService;

    @Override
    public void test(){
//        test1();
//        test2();
//        changeLogService.test1();
//        changeLogService.test2();

        ((AdminServiceImpl)AopContext.currentProxy()).test1();
        ((AdminServiceImpl)AopContext.currentProxy()).test2();

    }


    @MFTDataSource(DataSourceEnum.DB_AGENT)
    public void test1(){
        log.info(this+"");
//        adminMapper.test1();
    }


    @MFTDataSource(DataSourceEnum.DB_EXPEND)
    public void test2(){
        log.info(this+"");
//        adminMapper.test2();
    }

}

这时候,我们发现也正常的切换了:

2019-09-24 10:26:12,613 INFO [http-nio-5550-exec-1] [DirectJDKLog.java:179] Initializing Spring FrameworkServlet 'dispatcherServlet'
2019-09-24 10:26:12,613 INFO [http-nio-5550-exec-1] [FrameworkServlet.java:489] FrameworkServlet 'dispatcherServlet': initialization started
2019-09-24 10:26:12,637 INFO [http-nio-5550-exec-1] [FrameworkServlet.java:508] FrameworkServlet 'dispatcherServlet': initialization completed in 24 ms
2019-09-24 10:26:12,670 INFO [http-nio-5550-exec-1] [RequestLogAdvice.java:68] -----------GET-------------request[/expend/admin/test]--start--[request params:{}]----------
2019-09-24 10:26:12,682 INFO [http-nio-5550-exec-1] [DataSourceAspect.java:32] ===========选择数据源=========>>> agent
2019-09-24 10:26:12,683 INFO [http-nio-5550-exec-1] [AdminServiceImpl.java:44] com.meifute.expend.service.impl.AdminServiceImpl@1e987f59
2019-09-24 10:26:12,683 INFO [http-nio-5550-exec-1] [DataSourceAspect.java:32] ===========选择数据源=========>>> expend
2019-09-24 10:26:12,683 INFO [http-nio-5550-exec-1] [AdminServiceImpl.java:51] com.meifute.expend.service.impl.AdminServiceImpl@1e987f59
2019-09-24 10:26:12,719 INFO [http-nio-5550-exec-1] [RequestLogAdvice.java:83]  ----------------------response[/expend/admin/test]--end--[耗时:13ms,response result:null, response path[/expend/admin/test]]----------
原文  https://segmentfault.com/a/1190000020476310
正文到此结束
Loading...