一、動態數據源
動態數據源是一個很實用的功能,能夠在運行時切換數據源執行不同的數據庫操作,下文將通過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的數據源標識從而獲取對應標識的數據源返回。