Spring Boot教程十四:基於自定義註解的AOP資料來源自動切換
阿新 • • 發佈:2019-02-04
上一篇文章講到了多資料來源的配置和手動切換,手動切換費時費力,下面我們改進一下,改成基於註解的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&useUnicode=true&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&useUnicode=true&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)); } }