通过 AOP 注解实现自定义数据源切换

背景

近期在重构项目中,由于部分查询的数据库配置是根据不同的用户进行个性化配置的,所以在查询过程中,存在 MyBatis 多源数据库切换问题。

原项目实现方式为,在 MyBatis 查询前,切换到个性化配置,查询完毕后,再切换回默认数据库配置。

现在希望通过注解标注形式,进行重构代码。

原先实现方式

public Remark getRemarkById(Integer id, UserBase userBase){
	SpecialDataSourceUtils.setDBType(DataSourceNameConstant.USER_DB + userBase.getOrgId()); // 切换到个性化配置
	Remark remark = remarkMapper.selectById(id);
	DataSourceContextHolder.setDBType(DataSourceNameConstant.DEFAULT_DB); // 切换回默认数据库配置
	return remark;
}

AOP 方法注解

注解类

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ChangeMySqlMethod {
    boolean isChange() default true;
}

Aspect 切面类

@Slf4j
@Aspect
@Component
@Order(1)
public class ChangeMySqlMethodAspect {

    @Autowired
    DataBasesConfig dataBasesConfig;

    @Pointcut("@annotation(com.netease.mdas.databases.annotations.ChangeMySqlMethod)")
    public void changeMySQL() {}

    @Before("changeMySQL() && @annotation(changeMySqlMethod)")
    public void doBefore(JoinPoint joinPoint, ChangeMySqlMethod changeMySqlMethod) {
        log.info("====doBefore方法进入了====");
        if (changeMySqlMethod.isChange()){
            SpecialDataSourceUtils.setDBType(DataSourceNameConstant.USER_DB + ActiveInfo.orgId());
            MyBatisPlusConfig.setSuffix(ActiveInfo.orgId()); 
        } else {
            DataSourceContextHolder.setDBType(DataSourceNameConstant.DEFAULT_DB);
        }
    }

    @After("changeMySQL()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("====doAfter方法进入了====");
        DataSourceContextHolder.setDBType(DataSourceNameConstant.DEFAULT_DB);
    }
}

重构后实现方式

@ChangeMySqlMethod // 方法注解
public Remark getRemarkById(Integer id){
	return remarkMapper.selectById(id);
}

进一步思考

AOP 方法注解的实现方式,与原先实现方式相比,已经简洁了很多。

但是,如果 Service 的方法特别多,每个方法上面都加上注解的方式也会显得很繁琐。

同时,如果方法中有多个数据源同时查询的情况,还需要重新根据不同的数据源拆分方法,从而保证每个方法中只涉及一个数据源的查询。这样的重构方式并不友好。

那么,有没有办法在 RemarkMapper 类上面直接添加注解,来实现数据源动态切换呢?

由于 Mapper 类实现类是 MyBatis 动态代理生成,无法在实现类上直接添加 AOP 注解,而添加到接口上又无效,所以需要寻找替代方案。

AOP 类注解

不采用 AspectJ 表达式方式定义切点和切面,使用 AnnotationMatchingPointcut 和 DefaultPointcutAdvisor 来定义。

注解类

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ChangeMySqlType {

}

Aspect 切面类

@Slf4j
@Aspect
@Component
@Order(2)
public class ChangeMySqlTypeAspect {

    @Autowired
    DataBasesConfig dataBasesConfig;

    @Bean
    public Advisor dataSourceAdvisor(){
        Pointcut pointcut = new AnnotationMatchingPointcut(ChangeMySqlType.class, true);
        Advice advice = new MethodAroundAdvice(dataBasesConfig);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

Advice 业务增强类

@Slf4j
@Component
public class MethodAroundAdvice implements MethodBeforeAdvice, AfterReturningAdvice {

    private DataBasesConfig dataBasesConfig;

    public MethodAroundAdvice(DataBasesConfig dataBasesConfig){
        this.dataBasesConfig = dataBasesConfig;
    }

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        log.info("before {} called", method.getName());
    }

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        log.info("after {} called", method.getName());
    }
}

重构后的实现方式

Mapper:

@ChangeMySqlType // 类注解
public interface RemarkMapper extends BaseMapper<Remark>{}

方法:

public Remark getRemarkById(Integer id){
	return remarkMapper.selectById(id);
}

至此,可以在 Mapper 类上面直接添加注解,来实现数据源动态切换。