Spring AOP自定义注解实现Oracle多数据源切换以及自定义注解失效场景

一.项目背景

由于项目的业务逻辑可能涉及多个数据库,对于同一个代码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

接下来我们继续写个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类调用方法即可

如果碰到问题的话请留言~

你可能感兴趣的:(java,spring,boot,oracle)