【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
第一步,当即将要执行目标方法时
第二步,执行环绕通知
第三步,即将执行目标方法
第四步,执行目标方法
当方法为private
如果方法是private时,如下所示直接进入了目标方法,但是此时的controller仍旧是代理对象,且swiperService与bookService均为null。
既然代理类没有private方法,如何进入了addModelInfo方法中?
我们可以看到addModelInfo方法上有注解@ModelAttribute,其触发时机在当前controller的每个方法被调用前。也就是说这是由springmvc的机制决定的,这时方法的触发不会区分private还是public。
【2】对象代理与方法请求
当Bean实例化过程中会触发BeanPostProcessor
的动作。其中 AbstractAutoProxyCreator
的postProcessAfterInitialization
方法(这个方法在initializeBean方法中触发) 中我们可以看到其会尝试对Bean进行代理。
如下图所示在finishBeanFactoryInitialization方法中会调用preInstantiateSingletons进行Bean的预实例化。
如下所示在AbstractAutowireCapableBeanFactory
的initializeBean
方法中会分别调用BeanPostProcessor
的postProcessBeforeInitialization
与postProcessAfterInitialization
方法。
① 代理前后的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);
反射调用目标方法。