一、动态数据源
动态数据源是一个很实用的功能,能够在运行时切换数据源执行不同的数据库操作,下文将通过spring整合Mybatis手写一个动态数据源,主要使用到的技术有AOP、静态代理模式,本文主要内容是动态数据源,对于其它边边角角的内容不多阐述。
二、步骤
spring boot配置文件:
logging:
level:
com.my.dynamicdatasource: debug
org.springframework: warn
spring:
datasource:
druid:
default:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/mybatis_study?serverTimezone=GMT%2B8
username: root
password: 123456
driverClassName: com.mysql.cj.jdbc.Driver
slave:
type: com.alibaba.druid.pool.DruidDataSource
enabled: false
url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
username: root
password: 123456
driverClassName: com.mysql.cj.jdbc.Driver
# MyBatis
mybatis:
mapperLocations: classpath:mappers/*Mapper.xml
configLocation: classpath:mybatis-config.xml
说明:在spring boot中注册需要使用到的多个数据源,关于druid的设置不是本文重点,所以就没有配置。
DataSourceKey枚举类:
public enum DataSourceKey {
DEFAULT,SLAVE
}
说明;:该类用来标识切换数据源,实际用多少个数据源就需要创建对应数量的枚举类。
DataSource注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
DataSourceKey value() default DataSourceKey.DEFAULT;
}
说明:用来标记在Service或Mapper上,用于辅助AOP切换数据源操作。
UserServiceImpl服务类:
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
@DataSource
public User selectById(Integer id) {
return userMapper.selectById(id);
}
@Override
@DataSource(value = DataSourceKey.SLAVE)
public User selectByIdSlave(Integer id) {
return userMapper.selectById(id);
}
}
说明:service层实现类。
UserMapper类:
public interface UserMapper {
User selectById(Integer id);
}
DataSourceConfig数据源配置类:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid.default")
public DataSource dataSourceDefault() {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource dataSourceSlave() {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
/**
* 动态数据源
* @param dataSourceDefault 默认数据源
* @param dataSourceSlave 从数据源
* @return
*/
@Bean
@Primary
public DataSource dataSource(DataSource dataSourceDefault,DataSource dataSourceSlave) {
DynamicDataSource dataSource = new DynamicDataSource(dataSourceDefault);
dataSource.putDateSource(DataSourceKey.DEFAULT,dataSourceDefault);
dataSource.putDateSource(DataSourceKey.SLAVE,dataSourceSlave);
return dataSource;
}
}
说明:创建数据源,对于动态数据源一定要加上 @Primary注解。
DataSourceAspect切面类:
@Aspect
@Component
public class DataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
//切点
@Pointcut("@annotation(com.my.dynamicdatasource.DataSource)")
public void pointCut() {
}
@Around(value = "pointCut()")
public Object aroud(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
DataSourceKey dataSourceKey = dataSource.value();
if(dataSourceKey == null) {
dataSourceKey = DataSourceKey.DEFAULT;
}
logger.info("本次使用的数据源是:"+dataSourceKey.toString());
DynamicDataSource.DynamicDataSourceContextHolder.putDataSourceKey(dataSourceKey);
try {
return joinPoint.proceed();
}finally {
DynamicDataSource.DynamicDataSourceContextHolder.clearDateSource();
}
}
}
说明:AOP切面类,在service或mapper调用之前进行数据源的切换操作。
DynamicDataSource动态数据源类:
public class DynamicDataSource implements DataSource {
public DynamicDataSource(DataSource defaultDataSource) {
this.defaultDataSource = defaultDataSource;
}
private Map<DataSourceKey,DataSource> dataSourceMap = new ConcurrentHashMap();
private DataSource defaultDataSource;
private DataSource getDateSource(){
DataSourceKey dataSourceKey = DynamicDataSourceContextHolder.getDataSourceKey();
return dataSourceMap.get(dataSourceKey);
}
@Override
public Connection getConnection() throws SQLException {
return getDateSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return getDateSource().getConnection(username,password);
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return getDateSource().unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return getDateSource().isWrapperFor(iface);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return getDateSource().getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
getDateSource().setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
getDateSource().setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return getDateSource().getLoginTimeout();
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return getDateSource().getParentLogger();
}
public void putDateSource(DataSourceKey dataSourceKey,DataSource dataSource) {
dataSourceMap.put(dataSourceKey,dataSource);
}
/**
* 静态内部类,用于从线程中获取当前要使用的数据源
*/
public static class DynamicDataSourceContextHolder {
private static ThreadLocal<DataSourceKey> context = new ThreadLocal();
private static DataSourceKey getDataSourceKey() {
return context.get();
}
public static void putDataSourceKey(DataSourceKey dataSourceKey) {
if(dataSourceKey == null) {
dataSourceKey = DataSourceKey.DEFAULT;
}
context.set(dataSourceKey);
}
public static void clearDateSource() {
context.remove();
}
}
}
说明:对所有数据源的代理,主要是对获取数据源操作进行增强,通过静态内部类的ThreadLocal,根据当前线程存储的@DataSource上的DataSourceKey去进行数据源的修改。
DynamicDatasourceApplication主程序类:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@MapperScan("com.my.dynamicdatasource.mapper")
public class DynamicDatasourceApplication {
public static void main(String[] args) {
SpringApplication.run(DynamicDatasourceApplication.class, args);
}
}
说明:主程序类,注意必须添加排出掉DataSourceAutoConfiguration.class,否则spring boot会自动加载配置文件中配置的第一个数据源,导致冲突。
测试类:
@SpringBootTest
class DynamicDatasourceApplicationTests {
@Autowired
private UserService userService;
@Test
public void test() {
User user = userService.selectById(1);
System.out.println(user);
User user1 = userService.selectByIdSlave(2);
}
}
三、原理说明
1.使用AOP切面对标记了@DataSource注解的方法进行拦截,然后将其要使用的对应的数据源标识存放到ThreadLocal中,等到获取数据源时获取。
2.创建一个静态代理对象,把其设置为Mybatis的数据源获取类,拦截了所有实际数据源的获取,当Mybatis需要一个数据源时,调用的是该代理对象,该代理对象再获取当前线程中存放在ThreadLocal的数据源标识从而获取对应标识的数据源返回。