背景
在最近的一次需求开发中,需要做一个报表的需求,项目中连接了多个数据源,需要从不同的业务方进行数据的读取。开发完成后,发现同一方法内,调用方法使用一个数据源,被调用方法使用了另一个数据源,在执行时,被调用的方法数据源。
示例代码
@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,所以在同类内部调用如没有特殊指定,则使用的是类本身对象,而非代理对象,导致失效。
总结
在同类内部调用,我们在设计前期应该进行严格的进行模块逻辑与职责进行拆分,尽量避免同类内部调用。如果出现使用不同的数据源或者不同的事务之类的场景,但是由于某些原因不得不这样做时,我们可以手动的获取代理对象,通过代理对象进行调用,保证切面生效。