springboot2.X动态数据源切换
springboot2.X+mybatis+druid
参考了网上多个版本的文章和代码,此demo作为记录
1、maven引用
springboot2.x+druid+mybatis+aop.
// An highlighted block
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<!-- org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql-connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2、application.yml配置文件信息(多数据源)
spring:
datasource:
druid:
#stat-view-servlet:
#enabled: true
#login-username: admin
#login-password: admin
master:
url: jdbc:mysql://localhost:3306/restzzpt?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
username: root
password: !!!!!!!
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#配置初始化大小、最小、最大
initial-size: 5
min-idle: 2
max-active: 100
#配置获取连接等待超时的时间
max-wait: 60000
#打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
#
test-on-borrow: true
test-on-return: true
test-while-idle: true
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 30000
#配置一个连接在池中最小生存的时间,单位是毫秒--max-evictable-idle-time-millis:
min-evictable-idle-time-millis: 300000
#配置监控统计拦截的filters
filters: stat,wall,slf4j
#合并多个DruidDataSource的监控数据
use-global-dataSource-stat: true
remove-adandoned: true
slave:
url: jdbc:mysql://localhost:3306/restzzpt-1?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
username: root
password: !!!!
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#配置初始化大小、最小、最大
initial-size: 6
min-idle: 3
max-active: 101
#配置获取连接等待超时的时间
max-wait: 60000
#打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
#
test-on-borrow: true
test-on-return: true
test-while-idle: true
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 30000
#配置一个连接在池中最小生存的时间,单位是毫秒--max-evictable-idle-time-millis:
min-evictable-idle-time-millis: 300000
#配置监控统计拦截的filters
filters: stat,wall,slf4j
#合并多个DruidDataSource的监控数据
use-global-dataSource-stat: true
remove-adandoned: true
3、注解类com.demo.annotation.DS
/**
*
* @author zhao
* @description
* 2020年4月5日
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
String name() default "";
}
4、多数据源切换工具类
/**
* 配置多数据源
* 借助ThreadLocal类,通过ThreadLocal类传递数据源的参数
* @author zhao
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public static final String mysql= "master";
public static final String erpread= "slave";
//本地线程,获取当前正在执行的currentThread
public static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static void setCustomerType(String customerType) {
contextHolder.set(customerType);
}
public static String getCustomerType() {
return contextHolder.get();
}
public static void clearCustomerType() {
contextHolder.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return getCustomerType();
}
}
切面类:
/**
* 多数据源,切面处理类
* @author zhao
* @description
* 2020年4月5日
*/
@Slf4j
@Aspect
@Order(-1)// 该切面应当先于 @Transactional 执行
@Component
public class DataSourceAspect{
@Pointcut("@annotation(com.demo.annotation.DS)")
public void dataSourcePointCut() {
};
/**
* 切换数据源
* @param point
* @param dataSource
*/
@Before("dataSourcePointCut()")
public void switchDataSource(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if(ds == null){
DynamicDataSource.setCustomerType(DynamicDataSource.mysql);
log.debug("set datasource is " + DynamicDataSource.mysql);
}else {
DynamicDataSource.setCustomerType(ds.name());
log.debug("set datasource is " + ds.name());
}
}
/**
* 重置数据源
* @param point
* @param dataSource
*/
@After("dataSourcePointCut()")
public void restoreDataSource(JoinPoint point) {
// 将数据源置为默认数据源
DynamicDataSource.clearCustomerType();
}
}
5、数据源信息注入
因多数据源配置,需要排除springboot默认加载类(启动类中加上@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)),需要手动在配置类中配置数据源信息以及mybatis配置信息。
@Configuration
@MapperScan("com.demo.dao")
public class DataSourceConfig {
//注意:这里需要该注解声明是默认数据源
@Primary
@Bean(name="master",initMethod = "init")
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource master(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name="slave",initMethod = "init")
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slave(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(master());
// 配置多数据源
Map<Object, Object> dataBaseMap = new HashMap<>();
dataBaseMap.put("master", master());
dataBaseMap.put("slave", slave());
dynamicDataSource.setTargetDataSources(dataBaseMap);
return dynamicDataSource;
}
/**
* 为了做多数据源,将生成org.apache.ibatis.session.SqlSessionFactory的配置抽出到使用@Configuration修饰的配置类中。
* application.properties配置文件中关于驼峰转换的配置内容没有放进去
* 解决方案:将application.properties mybatis的congfig加载到类中,再注入到SqlSessionFactoryBean中
* @return
*/
// @Bean
// @ConfigurationProperties("mybatis.configuration")
// public org.apache.ibatis.session.Configuration globalMybatisConfig(){
// return new org.apache.ibatis.session.Configuration();
// }
@Bean(name="sqlServerSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource")DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory .setDataSource(dataSource);
// 扫描Model
sessionFactory.setTypeAliasesPackage("com.demo.*.entity");
// 扫描映射文件
sessionFactory .setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:dao/*.xml"));
org.apache.ibatis.session.Configuration mybatisConf = new org.apache.ibatis.session.Configuration();
//设置但JDBC类型为空时,某些驱动程序 要指定值,default:other
mybatisConf.setJdbcTypeForNull(null);
//[是否 启用 数据中 A_column 自动映射 到 java类中驼峰命名的属性 default:fasle]
mybatisConf.setMapUnderscoreToCamelCase(true);
//解决 mybatis不返回查询结果为空的字段
mybatisConf.setCallSettersOnNulls(true);
sessionFactory .setConfiguration(mybatisConf);
// sessionFactory .setConfiguration(globalMybatisConfig());
return sessionFactory .getObject();
}
@Bean(name="sqlServerTransactionManager")//事务管理@Transactional(TransactionManager="mysqlTransactionManager")
public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource")DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name="sqlServerSqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlServerSqlSessionFactory")SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
6、druid监控配置(可选)
参考druid官方文档:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
@Configuration
public class DruidConfig {
@Bean
@ConditionalOnMissingBean
public ServletRegistrationBean druidServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
//白名单
// servletRegistrationBean.addInitParameter("allow", "192.168.6.195");
//IP黑名单(存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page.
// servletRegistrationBean.addInitParameter("deny", "192.168.6.73");
//用于登陆的账号密码
servletRegistrationBean.addInitParameter("loginUsername", "admin");
servletRegistrationBean.addInitParameter("loginPassword", "admin");
//是否能重置数据
servletRegistrationBean.addInitParameter("resetEnable", "true");
return servletRegistrationBean;
}
/**
* @Date: 2019/5/29 20:51
* @Description: 注册filter信息,用于拦截
*/
@Bean
public FilterRegistrationBean<WebStatFilter> filterRegistrationBean() {
//创建过滤器
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(new WebStatFilter());
//设置过滤器过滤路径
filterRegistrationBean.addUrlPatterns("/*");
//忽略过滤得形式
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
7、在service实现层加上数据源注解:
@Service("userService")
@Transactional
public class ApiUserServiceImpl implements ApiUserService {
@Autowired
private ApiUserDao apiUserDao;
@DS(name="master")
@Override
public ApiUserEntity queryByUsername(String username) {
return apiUserDao.queryByUsername(username);
}
@DS(name="slave")
@Override
public ApiUserEntity queryByUsernameSlave(String username) {
return apiUserDao.queryByUsername(username);
}
}
测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicDataSourceTest {
@Autowired
private ApiUserService apiUserService;
@Test
public void test(){
//数据源1
ApiUserEntity user1 = apiUserService.queryByUsername("13612345678");
System.out.println(ToStringBuilder.reflectionToString(user1));
//数据源2
ApiUserEntity user2 = apiUserService.queryByUsernameSlave("13612345678");
System.out.println(ToStringBuilder.reflectionToString(user2));
//数据源1
//手动切换数据源,不用注解的方式。这个操作可以在controller层使用。
//重点: 实际操作证明,切换的时候最好清空一下
DynamicDataSource.clearCustomerType();
//切换数据源,设置后 就OK了。可以随时切换过来(在controller层切换)
DynamicDataSource.setCustomerType(DynamicDataSource.mysql);
ApiUserEntity user3 = apiUserService.queryByUsername11("13612345678");
System.out.println(ToStringBuilder.reflectionToString(user3));
}
}