由于项目的业务逻辑可能涉及多个数据库,对于同一个代码Project而言,需要具备动态切换数据源的功能,如果项目中ORM框架使用的是Mybatis-plus,就可以通过@DS注解实现动态数据源切换
功能,本篇基于Mybatis基础上的AbstractRoutingDataSource再利用AOP实现注解切换多数据源
pom.xml
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
application.properties
spring.datasource.first.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.first.url=jdbc:oracle:thin:...
spring.datasource.first.username=...
spring.datasource.first.password=...
spring.datasource.second.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.second.url=jdbc:oracle:thin:...
spring.datasource.second.username=...
spring.datasource.second.password=...
核心思想在于数据库配置类中配置数据源时,使用AbstractRountingDataSource的子类,所以我们需要先创建一个子类,并且重写其determineCurrentLookupKey方法
AbstractRountingDataSource子类:
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static final String defaultType = "first";
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object,Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource); //默认数据源
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
public static void setDataSource(String dataSourceKey){
if (StringUtils.isEmpty(dataSourceKey)){
dataSourceKey = defaultType;
}
contextHolder.set(dataSourceKey);
System.err.println("当前数据源切换为: " + dataSourceKey);
}
public static Object getDataSource(){
String s = contextHolder.get() == null ? defaultType : contextHolder.get();
System.err.println("当前数据源类型为: " + s );
return contextHolder.get();
}
public static void clearDataSource(){
contextHolder.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
}
数据库配置类:
@Configuration
public class OracleMybatisConfig {
@Bean(name = "firstDataSource")
@ConfigurationProperties(prefix = "spring.datasource.first") // prefix值必须是application.properteis中对应属性的前缀
public DataSource oracleDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondDataSource")
@ConfigurationProperties(prefix = "spring.datasource.second") // prefix值必须是application.properteis中对应属性的前缀
public DataSource secondOracleDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource dynamicDataSource(@Qualifier("firstDataSource") DataSource firstDataSource, @Qualifier("secondDataSource") DataSource secondDataSource) {
HashMap<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("first",firstDataSource);
targetDataSources.put("second",secondDataSource);
return new DynamicDataSource(firstDataSource,targetDataSources);
}
@Bean(name = "oracleSqlSessionFactory")
public SqlSessionFactory oracleSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
bean.setMapperLocations(resolver.getResources("classpath:mapper/*/*.xml"));
return bean.getObject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
以上的两个类已经实现了通过AbstractRountingDataSource切换数据源的功能,在需要切换数据源的方法前后加上ThreadLocal的set和clear方法即可,ThreadLocal用完后要记得clear,否则会导致内存泄漏
接下来我们继续写个AOP的方法注解来美观代码
注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DataSource {
String name() default "";
}
切面:
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.config.DataSource)")
public void dataSourcePointCut(){};
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable{
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
DynamicDataSource.setDataSource(dataSource == null ? null : dataSource.name());
try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
}
}
}
另外启动配置类要加上:
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@EnableAspectJAutoProxy(exposeProxy = true,proxyTargetClass = true)
@Import({OracleMybatisConfig.class})
public class ...Application {
public static void main(String[] args) {
SpringApplication.run(....class);
}
}
写完后, 使用注解切换数据源只需要在方法上面加上
@DataSource(name = “目标数据源的key”)
注意: 因为是基于AOP实现的,所以如果碰到切换数据源失败的情况,一定要检查下是不是存在AOP失效的可能
@Service
public class StudentServiceImpl implements StudentService{
@Autowired
private StudentDao dao;
public Object study(){
return readBook("select * from book");
}
//在readBook处切换失败
@DataSource(name = "second") //注解失效
public Object readBook(String sql){
return dao.excute(sql);
}
}
原因分析:调用study()方法时,Spring的动态代理会生成一个代理对象,代理对象会去执行invoke方法,并执行一些其他操作, 当在被代理对象的方法中调用被代理对象的其他方法(readBook方法)时,此刻其他方法(readBook方法)并没有用代理调用,而是被代理对象本身调用的
@Service
public class StudentServiceImpl implements StudentService{
@Autowired
private StudentDao dao;
//在study处切换成功
@DataSource(name = "second") //注解生效
public Object study(){
return readBook("select * from book");
}
public Object readBook(String sql){
return dao.excute(sql);
}
}
但有时业务场景需要必须在readBook处切换数据源
解决思路: 此处失效原因已经明悉,所以解决方法就是使得readBook方法被代理调用
如:
@Service
public class StudentServiceImpl implements StudentService{
@Autowired
private StudentDao dao;
//将本身注入进来,调用readBook方法时使用本身调用
@Autowired
private StudentServiceImpl studentServiceImpl;
public Object study(){
return studentServiceImpl.readBook("select * from book");
}
//在study处切换成功
@DataSource(name = "second") //注解生效
public Object readBook(String sql){
return dao.excute(sql);
}
}
或者写一个Util类给Spring管理,专门提供切换数据源方法,使用得时候注入这个Util类调用方法即可