1. 程式人生 > >Spring Boot教程十四:基於自定義註解的AOP資料來源自動切換

Spring Boot教程十四:基於自定義註解的AOP資料來源自動切換

上一篇文章講到了多資料來源的配置和手動切換,手動切換費時費力,下面我們改進一下,改成基於註解的AOP資料來源自動切換。

基礎知識不在贅述,直接上程式碼:


public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    /*
      *管理所有的資料來源id;
      *主要是為了判斷資料來源是否存在;
      */
    public static List<String> dataSourceIds = new ArrayList<String>();

    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static String getDataSourceType() {
        return contextHolder.get();
    }

    public static void clearDataSourceType() {
        contextHolder.remove();
    }

    public static boolean containsDataSourceType(String DataSourceId) {
        return dataSourceIds.contains(DataSourceId);
    }
}



/**
 * @author Shuyu.Wang
 * @package:com.ganinfo.datasource
 * @className:
 * @description:動態資料來源(需要繼承AbstractRoutingDataSource)
 * @date 2018-08-01 15:47
 **/

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        System.err.println("DataSourceContextHolder.getDataSourceType()       "+DataSourceContextHolder.getDataSourceType());
        return DataSourceContextHolder.getDataSourceType();
    }
}




@Aspect
@Order(-10)//保證該AOP在@Transactional之前執行
@Component
public class DynamicDataSourceAspect {

    @Pointcut("@annotation(com.ganinfo.datasource.TargetDataSource)")
    public void annotationPointcut(){
        System.out.println("======我是一個切入點");
    };
    /*
        * @Before("@annotation(ds)")
        *的意思是:
        * @Before:在方法執行之前進行執行:
        * @annotation(targetDataSource):
        *會攔截註解targetDataSource的方法,否則不攔截;
        */
    @Before("annotationPointcut()")
    public void changeDataSource(JoinPoint joinPoint) throws Throwable {
        MethodSignature signature=(MethodSignature) joinPoint.getSignature();
        Method method=signature.getMethod();
        TargetDataSource targetDataSource=method.getAnnotation(TargetDataSource.class);
        System.out.println("註解的攔截方法名註解內容前:"+targetDataSource.value());
        //獲取當前的指定的資料來源;
        String dsId = targetDataSource.value();
        //如果不在我們注入的所有的資料來源範圍之內,那麼輸出警告資訊,系統自動使用預設的資料來源。
        if (!DataSourceContextHolder.containsDataSourceType(dsId)) {
            System.err.println("資料來源[{}]不存在,使用預設資料來源 > {}" + targetDataSource.value() + joinPoint.getSignature());
        } else {
            System.out.println("UseDataSource : {} > {}" + targetDataSource.value() +"====="+ joinPoint.getSignature());
            //找到的話,那麼設定到動態資料來源上下文中。
            DataSourceContextHolder.setDataSourceType(targetDataSource.value());
            System.out.println("資料來源切換為:"+ DataSourceContextHolder.getDataSourceType());
        }
    }

    @After("annotationPointcut()")
    public void restoreDataSource(JoinPoint joinPoint) {
        MethodSignature signature=(MethodSignature) joinPoint.getSignature();
        Method method=signature.getMethod();
        TargetDataSource targetDataSource=method.getAnnotation(TargetDataSource.class);
        System.out.println("註解的攔截方法名註解內容前:"+targetDataSource.value());
        //方法執行完畢之後,銷燬當前資料來源資訊,進行垃圾回收。
        DataSourceContextHolder.clearDataSourceType();

        System.out.println("lear後:"+ DataSourceContextHolder.getDataSourceType());

    }


}



@Configuration // 該註解類似於spring配置檔案
public class MyBatisConfig {
    @Autowired
    private Environment env;

    private String MYBATIS_CONFIG = "config/mybatis.xml";

    /**
     * 建立資料來源(資料來源的名稱:方法名可以取為XXXDataSource(),XXX為資料庫名稱,該名稱也就是資料來源的名稱)
     */
    @Bean("masterDbDataSource")
    public DataSource masterDbDataSource() throws Exception {
        Properties props = new Properties();
        props.put("driverClassName", env.getProperty("spring.datasource.driverClassName"));
        props.put("url", env.getProperty("spring.datasource.url"));
        props.put("username", env.getProperty("spring.datasource.username"));
        props.put("password", env.getProperty("spring.datasource.password"));
        return DruidDataSourceFactory.createDataSource(props);
    }

    @Bean("slaveDb2DataSource")
    public DataSource slaveDb2DataSource() throws Exception {
        Properties props = new Properties();
        props.put("driverClassName", env.getProperty("custom.datasource.ds1.driverClassName"));
        props.put("url", env.getProperty("custom.datasource.ds1.url"));
        props.put("username", env.getProperty("custom.datasource.ds1.username"));
        props.put("password", env.getProperty("custom.datasource.ds1.password"));
        return DruidDataSourceFactory.createDataSource(props);
    }

    /**
     * @Primary 該註解表示在同一個介面有多個實現類可以注入的時候,預設選擇哪一個,而不是讓@autowire註解報錯
     * @Qualifier 根據名稱進行注入,通常是在具有相同的多個型別的例項的一個注入(例如有多個DataSource型別的例項)
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource(@Qualifier("masterDbDataSource") DataSource masterDbDataSource, @Qualifier("slaveDb2DataSource") DataSource slaveDb2DataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("masterDbDataSource", masterDbDataSource);
        targetDataSources.put("slaveDb2DataSource", slaveDb2DataSource);
        DataSourceContextHolder.dataSourceIds.add("masterDbDataSource");
        DataSourceContextHolder.dataSourceIds.add("slaveDb2DataSource");
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法
        dataSource.setDefaultTargetDataSource(slaveDb2DataSource);// 預設的datasource設定為masterDbDataSource
        return dataSource;
    }

    /**
     * 根據資料來源建立SqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDbDataSource") DataSource masterDbDataSource, @Qualifier("slaveDb2DataSource") DataSource slaveDb2DataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 下邊兩句僅僅用於*.xml檔案,如果整個持久層操作不需要使用到xml檔案的話(只用註解就可以搞定),則不加
     /*   sqlSessionFactoryBean.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));//*/
        /** 設定mybatis configuration 掃描路徑 */
        System.out.println(env.getProperty("mybatis.configLocation"));
        sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(MYBATIS_CONFIG));
        // 指定資料來源(這個必須有,否則報錯)
        sqlSessionFactoryBean.setDataSource(this.dataSource(masterDbDataSource, slaveDb2DataSource));
        /** 新增mapper 掃描路徑 */
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 配置事務管理器
     */
    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
        return new DataSourceTransactionManager(dataSource);
    }
}


/**
 * @author Shuyu.Wang
 * @package:com.ganinfo.datasource
 * @className:
 * @description:在方法上使用,用於指定使用哪個資料來源
 * @date 2018-08-01 20:56
 **/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String value();
}




package com.ganinfo.datasource.mapper;

import com.ganinfo.datasource.TargetDataSource;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

/**
 * @author Shuyu.Wang
 * @package:com.ganinfo.datasource.mapper
 * @className:
 * @description:
 * @date 2018-08-01 16:03
 **/
@Mapper
public interface Test {

    @TargetDataSource(value ="masterDbDataSource")
    @Select("SELECT name FROM t_user limit 1")
    List<Map<String,Object>> getList();


    @TargetDataSource(value ="slaveDb2DataSource")
    @Select("SELECT * FROM t_base_place limit 1")
    List<Map<String,Object>> getList2();
}




@Service
public class TestService {
    @Autowired
    private Test test;

    @TargetDataSource(value = "masterDbDataSource")
   public  List<Map<String, Object>> getList() {
        return test.getList();
    }


//    @TargetDataSource(value = "ds1")
    @TargetDataSource(value ="slaveDb2DataSource")
    public  List<Map<String, Object>> getList2() {
        return test.getList2();
    }


}







#主資料來源,預設的
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://172.16.1.28:3306/closedauth?zeroDateTimeBehavior=convertToNull&amp;useUnicode=true&amp;characterEncoding=utf-8
spring.datasource.username = cweserver
spring.datasource.password = cweserveryzhh

#更多資料來源
custom.datasource.names=ds1
custom.datasource.ds1.driverClassName = com.mysql.jdbc.Driver
custom.datasource.ds1.url = jdbc:mysql://172.16.1.28:3306/closeddata?zeroDateTimeBehavior=convertToNull&amp;useUnicode=true&amp;characterEncoding=utf-8
custom.datasource.ds1.username = cweserver
custom.datasource.ds1.password = cweserveryzhh

這裡切記註解要在service方法中加,不能再mapper中新增,不然無法被aop攔截;

測試方法如下:


@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class TestTest {
    @Autowired
    private com.ganinfo.datasource.mapper.Test test;
    @Autowired
    private TestService testService;

    @Test
    public void getList2() throws Exception {
        List<Map<String, Object>> list = testService.getList2();
        System.out.println(GsonUtil.GsonString(list));
//        log.info("");
    }
    @org.junit.Test
    public void getList() throws Exception {
        List<Map<String, Object>> list = testService.getList();
        System.out.println(GsonUtil.GsonString(list));
    }



}