前言
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]]----------
到底是为什么呢?其实原因和动态代理有关。这里我给出结果和怎么处理,具体的解释大姐可以参考这位大佬的文章,解析的很详细我就不重复了。
解决方法:
在配置文件将动态代理的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]]----------