1问题描述
同事开发中遇到这样一个问题,就是controller的请求方法用private修饰,会导致controller通过@Autowired或者@Resource注入的属性为null,在进行请求调用的时候,用到注入的属性就会报空指针
2问题复现
我们先来尝试复现问题,写个demo,尝试调用,结果如下:
可以看到是private方法,怎么testService注入不是为null?这就疑问了,难道搞错了?
再细看公司项目,发现有个全局的日志切面,对controller做了动态代理
我们加下切面再看看
再尝试调用,结果如下:
确实service为null,问题复现出来了
3问题分析
3.1初步分析
既然是加了动态代理才会出现问题,说明是动态代理对private不生效。
细看一下controller被代理后是怎样的,如下图:
可以看到是做了cglib代理
但是这就疑问了,不是说代理对private方法不生效吗?怎么代理对象还能调用到我们目标业务方法test?到底controller的代理是怎么做的?源码是怎样的?
当对现象理解不通时,就只有查看源码,才能捋清楚了..下面开始深入源码级别分析
3.2深入源码分析
第一步:了解cglib代理的原理
(此处不对cglib的整体源码流程做详细分析,需要的自行学习)
cglib的代理示例:
//要被代理的TestController
public class TestController {
//send是public方法,用于做对比
public void send() {
System.out.println("send");
}
//send2是private方法,用于测试问题
private void send2() {
System.out.println("send2");
}
}
//代理拦截器MyMethodInterceptor
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("前置处理");
Object object = methodProxy.invokeSuper(o, args); //调用目标业务方法
System.out.println("后置处理");
return object;
}
}
//代理测试CglibTest
public class CglibTest {
public static void main(String[] args) throws Exception{
//打印cglib生成的代理类class
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\cglibProxyClass");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestController.class);//设置被代理类
enhancer.setCallback(new MyMethodInterceptor());//设置拦截器
TestController proxyService = (TestController) enhancer.create();//创建代理类
//反射调用public修饰的send方法
Method sendMethod = TestController.class.getDeclaredMethod("send");
sendMethod.setAccessible(true);
sendMethod.invoke(proxyService);
//反射调用private修饰的send2方法
Method send2Method = TestController.class.getDeclaredMethod("send2");
send2Method.setAccessible(true);
send2Method.invoke(proxyService);
}
}
分析:
准备了2个方法调用测试,send2是private方法,用于测试问题,send是public方法,用于做对比。CglibTest 测试类生成代理对象proxyService后,通过反射调用send和send2方法,因为springmvc是通过反射调用的controller方法,所以为了更好的复现和分析问题,这里也用反射调用。
查看生成的代理class
在调式send和send2方法调用前,我们先单纯执行生成代理方法(如下图)
public class CglibTest {
public static void main(String[] args) throws Exception{
//打印cglib生成的代理类class
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\cglibProxyClass");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestController.class);//设置被代理类
enhancer.setCallback(new MyMethodInterceptor());//设置拦截器
TestController proxyService = (TestController) enhancer.create();//创建代理类
}
}
通过DebuggingClassWriter打印cglib生成的代理字节码,这里放在了D:\\cglibProxyClass目录下
我们看第二个字节码类
//生成的代理类是继承了要被代理的TestController
public class TestController$$EnhancerByCGLIB$$8ee56bef extends TestController implements Factory {
//动态代理执行回调的拦截器(就是MyMethodInterceptor)
private MethodInterceptor CGLIB$CALLBACK_0;
//拦截器中调用methodProxy.invokeSuper(o, args) 会调到这个方法
final void CGLIB$send$0() {
super.send(); //调用父类方法
}
//代理类重写父类TestController的send方法
public final void send() {
//获取拦截器
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
//如果拦截器不为空,则执行拦截器逻辑
var10000.intercept(this, CGLIB$send$0$Method, CGLIB$emptyArgs, CGLIB$send$0$Proxy);
} else {
//如果拦截器为空,则直接调用父类方法
super.send();
}
}
}
分析:
- 可以看到代理类继承了要被代理的TestController,确实是生成了代理子类
- 代理类重写了父类的方法send,所以在用代理对象调用send方法时候,实际调用就不是父类的send方法了,而是代理类的send方法,接着代理类的send方法会回到拦截器的intercept方法。整个流程就比较清晰了,这也明白了cglib是如何回调拦截器了
- 重点关注:没有重写send2方法,这也看出了没法代理private方法
- 再看拦截器的逻辑,当回调到拦截器之后,可以做前置处理和后置处理,当然中间就是通过methodProxy.invokeSuper(o, args)调用父类的send方法(如下图)
methodProxy.invokeSuper(o, args)具体的调用源码不再细看,主要的逻辑就是cglib通过标志位的方式,回调到上面源码看的 CGLIB$send$0方法(如下图),CGLIB$send$0里面就通过super.send()来调用到父类的send的类方法
源码的调用逻辑大致就清楚了,总结一下调用流程:
==》代理对象调用send方法(实际调用的是代理对象的send方法)
==》send方法里面回调了拦截器的intercept方法
==》intercept方法调用了methodProxy.invokeSuper方法
==》methodProxy.invokeSuper方法里面通过标志位调用到了CGLIB$send$0方法
==》CGLIB$send$0方法通过super.send()调用到了父类send方法
debug调用1:看send方法的调用
1生成代理对象
分析:断点打在调用send方法的地方,这里只是生成代理对象,还没开始调用send方法
2然后调用到代理类的send方法(此处debug不了,直接看源码解释)
分析:send方法里面会调用到拦截器
3然后调用到拦截器的intercept方法
分析:这里调用了methodProxy.invokeSuper(o, args),会回调到父类的send方法
4然后看父类send方法
分析:
- public修饰的send,确实经过了代理增强逻辑,又回调了目标send方法
- 注意看具体调用的对象实例是代理类实例
debug调用2:看send2方法的调用
1生成代理对象
分析:
- 断点打在调用send2方法的地方,这里只是生成代理对象,还没开始调用send2方法
2然后看send2方法的调用
这里就有意思了,这个调用,调到哪去?我们上面在看代理类class的时候,是明确知道代理是没有重写send2方法的(如下图)
那调到哪去呢?????
当代理子类没有重写send2方法的时候,就是直接调用到父类的send2方法了
3然后看父类的send2方法
分析:
- 这里就有意思了,首先通过反射,能不能调用到父类的private方法?答案是:能。
- 虽然调用到了父类的方法,但是少了中间增强逻辑的环节呀。所以cglib代理对private方法的代理情况是:通过反射也是能调用到private方法,但是没法走增强逻辑,实际就是没法代理。
第二步:了解cglib在spring中应用做法
Cglib在spring中应用的做法跟上面cglib的原生做法大体是一致的。
不同的点是spring在MethodInterceptor拦截器中回调父类方法的方式跟上面的不一样。
我们知道,上面回调父类方法是通过代理对象调用super.send()方法这种方式
那spring是怎么做的呢?
我们知道,spring在项目启动的时候,会创建TestController的bean对象,因为我们这里有LogAspect切面对TestController做代理,那spring会在实例化TestController之后,又生成TestController的代理对象,在生成代理对应的时候,会把原本的TestController对象放到MethodInterceptor里面,类似如下这种写法:
(注:只是大致表达spring的写法,实际spring的写法肯定是更抽象,封装更多)
public class MyMethodInterceptor implements MethodInterceptor {
private TestController testController;
public MyMethodInterceptor(TestController testController){
this.testController = testController;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
System.out.println("前置处理");
Object object = method.invoke(testController, args); //通过反射调用目标业务方法
System.out.println("后置处理");
return object;
}
}
public static void main(String[] args) throws Exception{
TestController testController = new TestController();//原本实例对象
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestController.class);//设置被代理类
//设置拦截器,把testController放到MyMethodInterceptor里面
enhancer.setCallback(new MyMethodInterceptor(testController));
TestController proxyController = (TestController) enhancer.create();//创建代理类
}
通过上面示例可以看到,把testController 放到MyMethodInterceptor 里面,在intercept回调业务方法的时候,就通过testController 实例反射调用目标方法method.invoke(testController, args),注意,这种调用方式,调用目标方法的对象就不是testController 的代理对象了,而是testController 原生对象,这个是spring和cglib代理调用目标方法的很大的区别。
思考:为什么spring不直接复用cglib原生的调用目标方法的方式呢?Cglib的代理对象是目标类的子类,调用目标方法只需要用super.send()就可以了,这么便捷为啥不沿用呢?
答:
先说沿用会出现的问题:
如果沿用super.send()的方式,那么调用的时候,TestController 注入的属性都会为null,这样就会报空指针
原因分析:
Spring在实例化TestController 的时候,是先做了属性填充,把TestController 依赖的testService填充进来,然后再做初始化,在初始化后置处理器里生成TestController 的代理对象,所以TestController的代理对象是对TestController 做了一层封装,代理对象是没有testService属性的
这样就明白了spring非要用原生对象调用目标方法的用苦良心了。
第三步:基于上面的背景进一步分析问题
好了,上面对动态代理做了一大堆的原理介绍,下面就基于这些知识来分析我们的业务问题。
这里从源码分析,要结合springmvc调用controller的源码
分正常情况和异常情况剖析
正常情况:controller方法为public
1 请求调用,走到springmvc调用controller
分析:
- 这里是springmvc通过反射调用controller
- 这里的this.getBean(),也就是反射调用bean是代理对象
- 既然走的是代理对象,那肯定不是直接就调用到目标方法,这里是正常情况,方法是用public修饰的,所以根据上面的知识,应该是走到了拦截器的intercept里面。在spring里面,cglib的拦截器是DynamicAdvisedInterceptor
2然后看DynamicAdvisedInterceptor的intercept方法
分析:根据上面的spring知识,这里会通过TestController实例,通过反射调用到目标方法
3然后看目标方法
分析:调用到目标方法的实例是TestController原生对象,TestService也是有值的,符合预期,没问题
异常情况:controller方法为private
1 请求调用,走到springmvc调用controller
分析:
- 这里是springmvc通过反射调用controller
- 这里的this.getBean(),也就是反射调用bean是代理对象
- 看着没啥毛病,好像跟正常情况一样呀。仔细一想,结合上面的知识点,就会发现,cglib是没代理private方法的,所以这里没法像上面正常情况一样走到拦截器里面,所以这里就是直接调用到了目标方法
2然后看目标方法
分析:
- 有了上面知识的铺垫,我们再次看到这个问题场景,已经没有疑问了。
- 虽然代理对象也能调用到private方法,但是实际走的只是单纯的反射调用,中间没有经过增强逻辑的处理
- 通过上面spring的知识可以知道,如果经过中间增强逻辑的处理,实际调用到目标方法的对象就不会是代理对象,而是原生对象
- 这里没有经过增强逻辑的处理,所以实际调用到目标方法的对象就是代理对象,通过上面的知识又可以知道,代理对象的testSerivce是没有赋值的,所以这里testSerivce为null,就没有疑问了。
至此,问题就分析清楚了........