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