Spring AOP中private(踩坑)实践总结

【1】方法修饰符为private的坑

如往常一样使用spring aop进行日志记录。定了了日志切面后,兴冲冲加上注解@EnableAspectJAutoProxy(proxyTargetClass=true),发起请求,这时候一个猝不及防的空指针闪了我一下。
在这里插入图片描述

发现这里userService为null!如下图所示(这里this是一个代理类哦):

在这里插入图片描述

这里注意哦,这里方法类型是private。最终的解决方案是将其修改为public(或者protected)。那么为什么呢?


我们这里使用的是CGLIB代理,该代理的原理是:

动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。

既然CGLIB是通过生成子类的方式来创建代理,那么它生成的子类肯定就要继承父类。

关于Java中的继承,有一条很重要的特性就是:

子类拥有父类非 private 的属性、方法。那么也就是说如果父类中有private方法,生成的代理类中是看不到的。

换言之,由CGLIB创建的代理类,不会包含父类中的私有方法。另外由于CGLIB代理类的生成过程,决定了其成员(无论是private还是protected)均是null

① private/public方法触发对象是什么?

或者说调用private/public方法时,对象是代理对象还是实际的spring管理的controller对象。

进入目标方法时,private类型时当前this是代理对象,public类型时当前this是spring管理的controller对象。

当方法为public

第一步,当即将要执行目标方法时

在这里插入图片描述

第二步,执行环绕通知

在这里插入图片描述

第三步,即将执行目标方法

在这里插入图片描述

第四步,执行目标方法

这时候this已经从代理对象转换为了实际对象,其注入的service自然不为null。

在这里插入图片描述

当方法为private

如果方法是private时,如下所示直接进入了目标方法,但是此时的controller仍旧是代理对象,且swiperService与bookService均为null。
在这里插入图片描述

继续问:既然代理类没有private方法,如何进入了addModelInfo方法中?

我们可以看到addModelInfo方法上有注解@ModelAttribute,其触发时机在当前controller的每个方法被调用前。也就是说这是由springmvc的机制决定的,这时方法的触发不会区分private还是public。

继续问:既然代理类没有private方法,如何进入了index方法中?

仔细看上图请求执行过程,方法的调用是基于反射来实现的。所以解决这个疑惑的前提是我们先搞懂,子类一定不能执行/触发父类的private方法吗?

【2】对象代理与方法请求

当Bean实例化过程中会触发BeanPostProcessor的动作。其中 AbstractAutoProxyCreatorpostProcessAfterInitialization方法(这个方法在initializeBean方法中触发) 中我们可以看到其会尝试对Bean进行代理。

如下图所示在finishBeanFactoryInitialization方法中会调用preInstantiateSingletons进行Bean的预实例化。

在这里插入图片描述

如下所示在AbstractAutowireCapableBeanFactoryinitializeBean方法中会分别调用BeanPostProcessorpostProcessBeforeInitializationpostProcessAfterInitialization方法。
在这里插入图片描述

① 代理前后的HomeController

代理前的HomeController

能够看到其是有成员注入的,无论成员修饰符是public、private还是default。

// 本文这里homecontroller三个成员
@Autowired
public SysSwiperService swiperService;
@Autowired
private SysBookService bookService;
@Autowired
ApplicationContext applicationContext;

在这里插入图片描述
代理后的HomeController

在这里插入图片描述

② 方法请求流程

当方法修饰符是public或者protected时

当方法修饰符是public或者protected时,如下所示会触发代理类的index方法然后交给DynamicAdvisedInterceptor执行增强逻辑。

在这里插入图片描述
执行完增强逻辑最终会走到我们的目标方法:

当前this是目标对象,将正常执行方法逻辑。

在这里插入图片描述

当方法修饰符是private时

如下所示,直接走到了目标类的index方法中,但是当前this不是目标对象而是代理对象。这里就会抛出空指针。

在这里插入图片描述

【3】为什么private | public时方法的this不同?

前面我们说过了,private时,方法的this是代理对象。public|protected时,方法的this是目标对象

首先我们可以看一下生成代理对象的字节码文件,这个如何看呢?可以如下所示在启动前配置环境属性用来保存生成的代理类。

@EnableAspectJAutoProxy(proxyTargetClass = true)
@SpringBootApplication
public class RecommendApplication {

    public static void main(String[] args) {
    // 这句话可以获取到代理对象的字节码文件
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"D:\\cglib");
        SpringApplication.run(RecommendApplication.class, args);
    }

}

在这里插入图片描述
我们反编译打开可以看到代理类HomeController$$EnhancerBySpringCGLIB$$51cc54a7继承自HomeController,那么也就是说父类的private的方法代理类是没有的,这个也符合Java中子类继承父类的原则。
在这里插入图片描述
通过字节码源文件可以看到,当方法是private修饰时,代理类没有目标方法。当方法是public或者protected修饰时,代理类有目标方法。

如下所示当触发目标方法时,会先触发代理对象的代理方法,这时就会触发DynamicAdvisedInterceptor的intercept方法(这也就意味着会触发代理增强流程)。也就是下图中的(String)var10000.intercept(this, CGLIB$index$0$Method, new Object[]{var1}, CGLIB$index$0$Proxy)这句代码。var10000指的是CGLIB$CALLBACK_0,也就是CglibAopProxy$DynamicAdvisedInterceptor
在这里插入图片描述
而DynamicAdvisedInterceptor的intercept方法在执行完代理增强逻辑触发我们的目标方法时,会交给真正的target反射调用目标方法。如下图所示,这里的target就是我们真正的目标对象,不再是代理对象。
在这里插入图片描述

这也能够说明当方法是private时,为什么方法的this是代理对象了。在spingmvc解析过程中,InvocableHandlerMethod#doInvoke方法 反射调用的时候,获取的bean就是代理对象。但是代理对象没有该方法不会触发代理增强逻辑,将直接调用目标方法。这里就没有了代理对象与目标对象转换的过程。

总结

  • @Autowired注解的值解析和成员修饰符没有任何关系;
  • 代理类的成员属性如bookService(标注了@Autowired)不会再被Spring解析,也就是为null
  • 方法修饰符为public或者protected都可以实现正常代理,当前this是目标对象;
  • 方法修饰符为private时,目标方法的当前this是代理对象,依赖解析为null(比如bookService)使用时将会抛出空指针;
  • 代理对象执行完增强逻辑处理后会交给target也就是真正目标对象触发method.invoke(target, args);反射调用目标方法。

关于代理对象的增强通知执行流程可以参考博文:

Spring AOP中CGLIB代理对象增强通知执行原理
Spring AOP拦截器调用的实现

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值