Spring常见问题解决 - this指针造成AOP失效
一. this指针造成AOP失效问题
1.1 案例
1.自定义一个UserService
:(本文的关键先生)
@Service
public class UserService {
public void login() throws InterruptedException {
System.out.println("Login!");
this.getUserName();
}
public void getUserName() throws InterruptedException {
System.out.println("My Name is Test");
Thread.sleep(1000);
}
}
2.定义AOP
,希望计算getUserName
方法消耗了多长时间。
@Aspect
@Service
public class UserAop {
// 这里写你的UserService的路径名.增强的方法名
@Around("execution(* com.service.UserService.getUserName()) ")
public void check(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("getUserName method time cost(ms): " + (end - start));
}
}
3.Controller
类:
@Controller
public class MyController {
@Autowired
private UserService userService;
@GetMapping("/hello")
@ResponseBody
public String hello() throws InterruptedException {
userService.login();
return "Hello";
}
}
4.启动类:@EnableAspectJAutoProxy
开启AOP
功能。
@SpringBootApplication
@EnableAspectJAutoProxy
public class Main8080 {
public static void main(String[] args) {
SpringApplication.run(Main8080.class, args);
}
}
5.访问对应路径,结果如下:
可见,我们虽然通过@Around
织入了我们自定义的增强逻辑,但是结果却并没有运行到相关代码,即AOP
失效了。
1.2 原理分析
我们首先来说下Spring
的AOP
,总的来说就是通过Cglib
或者JDK动态代理
的方式实现在运行期对方法的动态增强。本质上也就是一个代理模式。
那么针对本文的案例,我们对getUserName
方法做了增强逻辑:
@Around("execution(* com.service.UserService.getUserName()) ")
那么,在UserService
类的login
方法中,我们希望调用的这个getUserName
是一个增强后的方法:
public void login() throws InterruptedException {
System.out.println("Login!");
this.getUserName();
}
可是事实与预想的却不太一样。我们知道,如果AOP
功能实现成功的话,本质上是调用了对应方法的一个代理。那么我们先debug
看下,调用getUserName
的对象是什么:
没啥特别的地方,就是一个普通的UserService
对象。我们再来看下Controller
中的userService
是什么:
是一个通过Cglib
增强后的对象实例。很明显两者不是同一个。那么就容易看出问题出在这了。我写过一篇文章:Spring源码系列: AOP实现里面对于AOP
的实现原理讲的很详细了。这里面仅仅贴出跟本文息息相关的一些代码:
AOP
的最后一步就是创建代理对象:
// 5.创建代理
Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
protected Object createProxy(Class<?> beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}
// 1.创建代理工厂类,并且复制当前类的属性
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
// 2.校验:给定的bean使用目标类还是其接口代理
if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
// 添加代理接口
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
// 3.设置代理工厂的相关属性,将增强数组和目标对象加入其中
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
// 4.用来控制代理工厂被配置之后,是否允许修改通知
proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
ClassLoader classLoader = getProxyClassLoader();
if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) {
classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
}
// 5.主要还是委托ProxyFactory去创建代理类。
return proxyFactory.getProxy(classLoader);
}
}
其中,这段代码显得尤为重要:proxyFactory.setTargetSource(targetSource);
也就是说,代理对象它里面依旧包含了源对象的引用。即AOP的实现依旧离不开源对象。
- 因为
AOP
本质上,只不过是重写了被代理对象的方法,而当真正调用被代理对象的方法时,是通过原始对象来进行调用的,而不是被代理对象。 - 因此如果在被代理方法的内部使用了
this
指针,那么这个this
指的就是原始对象,而不是被代理对象。
那么我们需要看下Cglib
对于代理的实现,来验证上面的说法。Spring源码系列: AOP实现这里我讲了Cglib
对于方法的拦截是通过将自定义的拦截器加入到Callback
中并调用代理时,通过激活intercept
方法来实现。
而源码中我们可以发现,Spring
将拦截器都加入到了DynamicAdvisedInterceptor
这个类中,而该类又是MethodInterceptor
的实现类。因此具体的Cglib
方式的AOP
代理必然在其中实现:
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {
@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
TargetSource targetSource = this.advised.getTargetSource();
try {
// 同JDK代理,处理一些自调用的特殊情况,暴露对象
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
// 1.获取拦截器链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
// 2.若拦截器为空,且方法是可以公共访问的。直接调用源方法
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
}
else {
// 3.进入链中,和jdk 动态代理实现是类似的,只是MethodInvocation实现类不同而已
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
}
我们主要关注这段代码:
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
那么它的taget
是啥呢?我们看下外层的传参target = targetSource.getTarget();
,和我们在创建代理对象时proxyFactory.setTargetSource(targetSource);
,set
进去的对象是同一个,都指向了原始对象。
Spring
中,如果某个实例是需要通过Cglib
方式来完成代理的,那么最终的实现在于CglibMethodInvocation
这个类中。我们可以确定,这里执行invoke
的时候,使用的也就是原始对象this.target
。
private static class CglibMethodInvocation extends ReflectiveMethodInvocation {
@Override
protected Object invokeJoinpoint() throws Throwable {
if (this.methodProxy != null) {
return this.methodProxy.invoke(this.target, this.arguments);
}
else {
return super.invokeJoinpoint();
}
}
}
这里可以得出结论:
- 在一个被动态代理的对象执行完
AOP
所有的增强逻辑之后,最终都会使用被代理对象作为实例调用真实的方法,即相当于调用了:target.method ()
方法。 - 即
AOP
动态代理方法的时候,实际上会通过原始对象进行调用。因此这里的this
指的是原始对象。而不是代理对象本身。 - 而只有代理对象才拥有增强的逻辑。
1.3 解决
我们可以通过AopContext
来获取一个代理对象。首先在启动类的注解中,添加属性exposeProxy
:
@EnableAspectJAutoProxy(exposeProxy = true)
然后代码login
改成:
public void login() throws InterruptedException {
System.out.println("Login!");
UserService userService = (UserService) AopContext.currentProxy();
userService.getUserName();
}
结果如下:
我们可以deubg
看下,此时的UserService
对象实例是咋样的:
可见这里不再是一个普通的没有被增强过的UserService
实例了。另外做个补充,无论你的this
用在哪里,只要是AOP
的调用链中引用的,那么this
指的就是原始对象本身。如果程序里面不通过this
指针来调用被增强的方法,那么问题倒是没有的。 证明如下:
另一种方式(不推荐):可以通过@Autowired
注解引入自己:若报错循环依赖,可以加个@Lazy
注解。
@Service
public class UserService {
@Autowired
private UserService userService;
public void login() throws InterruptedException {
System.out.println("Login!");
userService.getUserName();
}
public void getUserName() throws InterruptedException {
System.out.println("My Name is Test");
Thread.sleep(1000);
}
}
二. 总结
- 首先,不要在使用
AOP
的过程中,在增强方法的内部或者是业务代码上调用增强方法上,使用this
指针。 - 一旦有
this.增强方法()
代码的出现,你就要注意了,因为这会导致AOP功能失效。 - 因为
AOP
的本质就是创建代理对象,而代理对象里面存储了原始对象的引用。 - 被代理对象只负责执行那些新加入的逻辑(增强方法),而原函数的调用则是通过原始对象来执行的。因此在执行
AOP
过程中,执行原函数的时候,this
指针指的是原始对象,而非被代理对象。 - 故
this.method()
相当于原始对象.method()
。