前言
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]]----------