一、Mybatis配置多数据源
前面讲了Mybatis配置多数据源配置的包路径分库方式——SpringBoot系列:Spring Boot多数据源,Mybatis方式,这种方式实现多数据源,以包区分,清晰明了,但是很多时候会造成很多代码冗余,比如举例中的userDao,而且新增包需要去修改配置文件。实际中一般并不采用这种方式,而是使用aop,这次就来讲解aop实现动态多数据源。
aop实现多数据源,在pom依赖上,多添加aop的依赖。这个很好理解,没有aop依赖还怎么使用aop。
<!--mysql连接-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
application配置文件方面并没有太大差别,依旧配置多个数据源即可。
server:
port: 10900
spring:
datasource:
master:
# 新版驱动从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据源需要添加时间标准和指定编码格式解决乱码 You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.
url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 1234
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 1234
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
因为是动态加载数据源,所以要排除SpringBoot默认的数据源加载机制。
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class
})
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
排除了自定义的数据源,就需要我们自己自定义数据源配置类。首先创建DynamicDataSource类,该类需要继承AbstractRoutingDataSource抽象类实现determineCurrentLookupKey接口即可。该接口实现非常简单,即从DataSourceContextHolder.getDB()取出数据源的key返回。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
System.out.println("数据源为" + DataSourceContextHolder.getDB());
return DataSourceContextHolder.getDB();
}
}
那DataSourceContextHolder又是什么呢,它其实就是一个ThreadLocal维护的数据源的key值容器,提供了设置、获取和清除ThreadLocal的方法。
public class DataSourceContextHolder {
// 默认数据源
public static final String DEFAULT_DATASOURCE = "master";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 设置数据源名
public static void setDB(String dbType) {
contextHolder.set(dbType);
}
// 获取数据源名
public static String getDB() {
return (contextHolder.get());
}
// 清除数据源名
public static void clearDB() {
contextHolder.remove();
}
}
刚刚还只是创建了我们的动态数据源,那我们该怎么初始化数据源对象呢?很简单,依旧如此,创建配置类。只不过除了masterDataSource和slaveDataSource数据源外,我们主要是要创建出我们的动态数据源对象,即dynamicDataSource,dynamicDataSource对象主要需要设置数据源TargetDataSources和DefaultTargetDataSource默认数据源。
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
DataSource masterDataSource(){
// DataSourceBuilder.create().build();
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
DataSource slaveDataSource(){
return DruidDataSourceBuilder.create().build();
}
/**
* 动态数据源: 通过AOP在不同数据源之间动态切换
* @return
*/
@Primary
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 多数据源map
Map<Object, Object> dsMap = new HashMap<>();
dsMap.put("master", masterDataSource());
dsMap.put("slave", slaveDataSource());
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(dsMap.get(DataSourceContextHolder.DEFAULT_DATASOURCE));
// 配置多数据源
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}
这个我们的动态数据源就配置好了,既然是动态数据源,那怎么动态法呢,什么时候使用master,什么时候使用slave?这时我们的aop就起到了作用。
在aop中,一般对注解进行拦截,所以我们要先定义一个注解,创建注解类DS。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface DS {
String value() default DataSourceContextHolder.DEFAULT_DATASOURCE;
}
然后创建DynamicDataSourceAspect,拦截DS注解。@Aspect声明为拦截器,@Order(1)指明拦截器的顺序,这个很重要,我们需要确保DynamicDataSourceAspect优先于DynamicDataSource作用。@Before在DS注解方法前,其实获取DS注解的值,设置到ThreadLocal的contextHolder对象里,而在@After方法执行之后,我们又将值清空。
@Order(1)
@Aspect
@Component
public class DynamicDataSourceAspect {
@Before("@annotation(DS)")
@SuppressWarnings("rawtypes")
public void beforeSwitchDS(JoinPoint point){
//获得当前访问的class
Class<?> className = point.getTarget().getClass();
//获得访问的方法名
String methodName = point.getSignature().getName();
//得到方法的参数的类型
Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
// 使用的数据源
String dataSource = DataSourceContextHolder.DEFAULT_DATASOURCE;
try {
// 得到访问的方法对象
Method method = className.getMethod(methodName, argClass);
// 判断是否存在@DS注解
if (method.isAnnotationPresent(DS.class)) {
DS annotation = method.getAnnotation(DS.class);
// 取出注解中的数据源名
dataSource = annotation.value();
}
} catch (Exception e) {
e.printStackTrace();
}
// 切换数据源
DataSourceContextHolder.setDB(dataSource);
}
@After("@annotation(DS)")
public void afterSwitchDS(JoinPoint point){
DataSourceContextHolder.clearDB();
}
}
如此一来,回到最开始的DynamicDataSource类的determineCurrentLookupKey方法,最次mybatis执行时,都会从中获取到对应了数据源key值,根据key获取到设置的TargetDataSources中的DataSource对象,从而达到动态加载数据源。
这种方式,dao层不需要做任何区分,仅需要在Service中调用方法上进行注解区分。
@Mapper
public interface IUserDao {
/**
* 添加用户
* @param user
* @return
*/
@Insert("insert into user(username, password) values(#{username}, #{password})")
int add(User user);
}
看一下Service,@DS(value = “master”)和@DS(value = “slave”)不同注解值表明使用不同的数据源。
@Service
public class UserService {
@Autowired
private IUserDao userDao;
/**
* 添加用户
* @param user
*/
// 动态数据源直接使用@Transactional即可
@Transactional
@DS(value = "master")
public void addUser(User user){
userDao.add(user);
// 除零异常,测试事务
int a = 1/0;
}
/**
* slave添加用户
* @param user
*/
@Transactional
@DS(value = "slave")
public void addUserSlave(User user){
userDao.add(user);
// 除零异常,测试事务
int a = 1/0;
}
}
自此,aop实现mybatis动态数据源配置完成。该演示示例中仅仅两个库,实际中结合业务场景,在拦截器中指定合理的分库规则即可。
源码地址:https://github.com/imyanger/springboot-project/tree/master/p15-springboot-muti-aop-mybatis