Spring AOP NPE分析
在 Spring AOP 中,常见的问题是 AOP 加入后,调用代理对象会报 NullPointerException(NPE)。
问题描述
Controller 中的接口没加入 AOP 之前,方法正常运行,一旦加了 AOP,调用 appService
的时候会报 NullPointerException(NPE)。
问题场景
代码样例: 简单写一段 demo,模拟 AOP 的使用场景,所有方法执行前打印 doBefore
,模拟日志记录。
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* plus.naomi.controller..*.*(..))")
public void requestLog() {}
@Before("requestLog()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("doBefore");
}
}
@RestController
public class TestController {
@Autowired
private AppService appService;
@GetMapping("/test")
private String test() {
return appService.test();
}
}
注意事项
- AOP 基于 CGLIB 子类代理,凡是类的方法使用了
private
、static
、final
修饰,那这些方法都不能被 AOP 增强 - 当你在一个类中使用了 AOP,但是在另一个类中调用了这个类的方法,这个方法就不会被 AOP 代理了。
问题分析
在 SpringBoot 中,默认使用的就是 CGLIB 方式来创建代理。
CGLIB 会动态生成一个要代理类的子类,子类重写要代理的类的所有不是
final
的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,在然后加入横切逻辑。
既然 CGLIB 是通过生成子类的方式来创建代理,那么它生成的子类就要继承父类。
但子类只有父类的非 private
的属性、方法,因此由 CGLIB 创建的代理类,不会包含父类中的 private
方法。
如果说因为 private
方法的原因,会导致代理类不会包含此方法的话,那么最多就是 AOP 不会生效,为什么 appService
也没有注入进来呢?
不管方法是否为私有的,TestController 这个 Spring Bean 是已经确定被代理了的。
Spring Bean 生命周期如下:
- 实例化
- 属性注入
- 初始化
- 销毁
public class InvocableHandlerMethod extends HandlerMethod {
// ...
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
try {
// 这里是代理对象执行目标方法,因此可以反射执行 private 方法,不会报 NoSuchMethodException 异常
// 与下文中的 CglibProxyFactory.main() 中一样
return method.invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
// ...
}
}
}
一个 HTTP 请求,会先经过 SpringMVC 中的 DispatcherServlet
,然后找到与之对应的 HandlerMethod
来处理。在后面,会先通过 Spring 的参数解析器,把 Request 参数解析出来,最后通过反射 Method.invoke()
来调用方法,不论方法是 public
和 private
。
getBean()
拿到的就是被代理后的对象,在这个代理对象中,appService
对象为 null。
如果直接调用 appService
不管是否为 private
应该都会报错 NPE,为什么只有 private
方法才会报错,而 public
方法不会呢?
如果是 private
方法,那么在代理类中,不会包含这个方法,此时不能代理方法。此时通过 HandlerMethod
中 Method.invoke()
来调用目标方法,传入的实例对象是 TestController 的代理类,前面我们知道 getBean()
拿到的代理对象中的 appService
对象为 null。所以,执行的时候,才会看到 appService
没有注入,导致 NPE 异常。
private static class CglibMethodInvocation extends ReflectiveMethodInvocation {
// ...
protected Object invokeJoinpoint() throws Throwable {
if (this.methodProxy != null) {
try {
return this.methodProxy.invoke(this.target, this.arguments);
}
catch (CodeGenerationException ex) {
logFastClassGenerationFailure(this.method);
}
}
return super.invokeJoinpoint();
}
}
如果是 public
方法,在代理类中,有它的子类实现。此时通过 HandlerMethod
中 Method.invoke()
来调用目标方法,则会先调用到代理类的拦截器 MethodInterceptor
。拦截器负责链式调用 AOP 方法和目标方法,在拦截器执行过程中,会调用了目标方法。与上面不同的是,此时传入的实例对象并不是代理类,而是代理类的目标对象,目标对象就是 TestController,它的 appService
不为空,所以不会报错。
案例测试
使用 CGLIB 代理方式,创建代理类分别调用 public
和 private
方法。
public class CglibProxyFactory {
public static void main(String[] args) {
// CGLIB 可以代理未实现任何接口的类
// CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
Service proxy = (Service) CglibProxyFactory.getProxy(Service.class);
Method send = proxy.getClass().getDeclaredMethod("send", String.class);
proxy.invoke(proxy, "hello");
System.out.println("========================================");
// 在代理类中,不会包含 private 方法,会抛出 NoSuchMethodException 异常
// proxy.getClass().getDeclaredMethod("get", String.class);
// 代理对象执行目标方法
Method get = Service.class.getDeclaredMethod("get", String.class);
get.setAccessible(true);
get.invoke(proxy, "hello");
}
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
public class Service {
public void send(String message) {
System.out.println("invoke public method send message -> " + message);
}
private void get(String message) {
System.out.println("invoke private method get message -> " + message);
}
}
/**
* 自定义MethodInterceptor
*/
class DebugMethodInterceptor implements MethodInterceptor {
/**
* @param o 代理对象(增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 调用方法之前,我们可以添加自己的操作
System.out.println("before method -> " + method.getName());
// before
// 代理对象调用原始方法
Object object = methodProxy.invokeSuper(o, args);
// after
// 调用方法之后,我们同样可以添加自己的操作
System.out.println("after method -> " + method.getName());
return object;
}
}
执行结果:
这也就解释了,为啥同样是调用 Method.invoke()
,private
方法没有注入成功,而 public
方法正常。 虽然在这个例子里面 CGLIB 的拦截器中代理对象调用的是原始方法,但是在 AOP 中,相关逻辑不一样,前面提到的 CglibMethodInvocation.invokeJoinpoint()
调用原始方法的是代理类的目标对象。
解决方案
修改为
public
方法@RestController public class TestController { @Autowired private AppService appService; // 改为 pulic 方法 @GetMapping("/test") public String test() { return appService.test(); } }
限定只 AOP
public
方法@Aspect @Component public class LogAspect { // 限定只 aop public 方法 @Pointcut("execution(public plus.naomi.controller..*.*(..))") public void requestLog() {} @Before("requestLog()") public void doBefore(JoinPoint joinPoint) { System.out.println("doBefore"); } }
总结
尽量不要在 Controller 里面不要用 @RequestMapping 映射 private
方法。如果一定要用,那么就限定只 AOP public
方法。