springboot使用切面方式(自定義註解)動態鏈接多數據源

前言
springboot裏面能夠有多種方式配置多數據源。以前寫過將mapper與指定數據源鎖定,可是隨着項目的擴大,
動態使用多數據源已經不可避免,這些天將代碼搭建好了併成功上線使用。記錄以供各路大神重複造輪子。
(ps:springboot 版本 1.5.9)
配置以及代碼
分析:動態切換數據源,咱們可不能夠自定義一個註解,而後在須要切換數據源的地方加上自定義的註解,
讓程序自動切換呢?很顯然是能夠的。

自定義註解:css

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

註解定義好了,咱們須要有一個切面,去環繞這個註解,作咱們須要的加強。爲了簡潔方便,我將使用枚舉來切換多數據源。html

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

上面定義好了之後,咱們須要使用一個線程變量,來存放當前註解所在線程的鏈接信息,在進入註解所在方法的時候,給線程變量賦值指定數據源鏈接,在結束的時候,就清理當前線程:java

/**
 * 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方法,使用咱們自定義的線程變量獲取當前數據源。mysql

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

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

固然,最重要的多數據源配置,咱們能夠這樣配置:spring

@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配置的對應便可(爲了安全,我把關鍵信息隱藏了,各位大佬記得改成本身的鏈接方式):sql

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:springboot

/**
 * <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:app

/**
 * <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]]----------
相關文章
相關標籤/搜索