类内部方法调用导致动态数据源失效问题

背景

在最近的一次需求开发中,需要做一个报表的需求,项目中连接了多个数据源,需要从不同的业务方进行数据的读取。开发完成后,发现同一方法内,调用方法使用一个数据源,被调用方法使用了另一个数据源,在执行时,被调用的方法数据源。

示例代码

@Service
public class ReportService {

    public ReportVO orderReport() {
        findOrder();
        ...
        reutrn null;
    }


    @DS("order-datasource")
    public void findOrder() {
        findSku();
        ...
    }

    @DS("sku-datasource")
    public void findSku() {
        ...
    }
}

在方法调用时,发现findOrder方法可以正常执行,在调用findSku时发生报错,原因是表不存在,于是发现findSku的动态数据源注解失效。

原因

注解的实现原理是Spring AOP,而只有通过代理对象调用AOP切面的逻辑才会生效。由于内部调用调用对象为this,非代理对象,切面会失效,所以动态数据源的注解也会失效。

解决方案

方案-1

调用本类的代理对象,通过代理对象调用,使得切面逻辑生效。

PS:需开启 @EnableAspectJAutoProxy(exposeProxy = true)

  • 示例-1
@Service
public class ReportService {

    public ReportVO orderReport() {
        findOrder();
        ...
        reutrn null;
    }


    @DS("order-datasource")
    public void findOrder() {
        Object proxyObject = AopContext.currentProxy();
        ReportService reportService = (ReportService)proxyObject;
        reportService.findSku();
        ...
    }

    @DS("sku-datasource")
    public void findSku() {
        ...
    }
}
  • 示例-2
@Service
public class ReportService {

    @Resource
    private ApplicationContext applicationContext;

    public ReportVO orderReport() {
        findOrder();
        ...
        reutrn null;
    }


    @DS("order-datasource")
    public void findOrder() {
        ReportService reportService = applicationContext.getBean(ReportService.class);
        reportService.findSku();
        ...
    }

    @DS("sku-datasource")
    public void findSku() {
        ...
    }
}

方案-2

避免内部调用,将方法进行拆分到不同的类中。

扩展

  • 为什么手动开发的切面(如AspectJ)在内部调用时仍然生效?
    • AspectJ不依赖动态代理,而是通过字节码增强直接在编译期或类加载期修改目标类的字节码,使切面逻辑直接嵌入目标方法。

    • 因此,即使内部调用(this.method())也会被拦截,因为目标方法已经被修改

    • 示例代码

// 使用AspectJ(非Spring AOP)
public class ReportService {
    public void findOrder() {
        this.findSku(); // ✅ 仍然会被AOP拦截
    }

    public void findSku() {
        System.out.println("inner method");
    }
}

@Aspect
public class LogAspect {
    @Around("execution(* ReportService.*(..))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[AspectJ] Before");
        Object result = pjp.proceed();
        System.out.println("[AspectJ] After");
        return result;
    }
}

// 调用
new ReportService().findOrder();
// 输出:
// [AspectJ] Before
// [AspectJ] Before
// inner method
// [AspectJ] After
// [AspectJ] After

SpringAOP与AspectJ的区别

特性Spring AOP(动态代理)AspectJ(字节码增强)
代理机制JDK动态代理 / CGLIB字节码修改(编译期/加载期)
内部调用拦截❌ 默认不拦截 this.method()✅ 拦截 this.method()
性能运行时动态代理,稍慢编译期优化,更快
适用场景方法级别AOP(Spring Bean)任意类(包括非Spring管理的类)

联想

在这个场景中,动态数据源注解失效,是否可以让我们想到类似事务失效的场景呢,其实原理都是相通的,由于Springboot项目中,默认的切面使用的时Spring AOP,所以在同类内部调用如没有特殊指定,则使用的是类本身对象,而非代理对象,导致失效。

总结

在同类内部调用,我们在设计前期应该进行严格的进行模块逻辑与职责进行拆分,尽量避免同类内部调用。如果出现使用不同的数据源或者不同的事务之类的场景,但是由于某些原因不得不这样做时,我们可以手动的获取代理对象,通过代理对象进行调用,保证切面生效。

你好!对于MyBatis和动态数据源的结合,以及PageHelper分页失效问题,我可以给你提供一些建议。 首先,确保你已经正确配置了动态数据源,并且能够正确切换数据源。你可以使用像Druid这样的连接池和AbstractRoutingDataSource来实现动态数据源的切换。 然后,确保你已经正确配置了PageHelper插件。在MyBatis的配置文件中,需要添加如下配置: ```xml <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="dialect" value="your_dialect"/> </plugin> </plugins> ``` 其中,`your_dialect`需要替换为你正在使用的数据库方言,例如MySQL、Oracle等。 另外,如果你在使用PageHelper时,还使用了MyBatis的动态SQL(例如使用了`<if>`标签),请确保你在PageHelper插件的前面使用了`<script>`标签,例如: ```xml <select id="queryUsers" parameterType="map" resultMap="userResultMap"> <script> SELECT * FROM users WHERE 1=1 <if test="username != null and username != ''"> AND username = #{username} </if> </script> </select> ``` 最后,如果以上步骤都正确配置了,但仍然无法实现分页功能,你可以尝试以下解决方案: 1. 确保你的查询语句返回的结果是一个List型的对象,而不是其他型(如数组)。 2. 确保在调用分页查询方法时,传入了正确的参数(例如页码、每页条数等)。 3. 如果你在使用PageHelper插件之前已经使用了其他的拦截器插件,可能会导致PageHelper失效。尝试调整插件的顺序,将PageHelper插件放在前面。 4. 如果你在使用Spring Boot框架,可能需要在配置上添加`@MapperScan`注解,以确保扫描到MyBatis的Mapper接口。 希望以上建议对你有帮助!如果还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值