Spring常见问题解决 - AOP调用被拦截类的属性报NPE

  • 一. 案例复现
  • 二. 被拦截类的属性为何是null?
    • 2.1 原理分析
    • 2.2 解决
      • 2.2.1 为何加一个 get 方法就可以避免NPE?
    • 2.3 总结

和本篇文章有关的另一篇文章Spring常见问题解决 - this指针造成AOP失效。

一. 案例复现

项目结构:

1.首先,我们自定义个简单的User类:

public class User {private String name;public User() {}public User(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}

2.我们有一个AdminService类,里面有个public类型的属性,以及一个方法。

@Service
public class AdminService {public final User user = new User("LJJ");public void request() {System.out.println("Request to Admin");}
}

3.再写一个UserService类:

@Service
public class UserService {@Autowiredprivate AdminService adminService;public void login() throws InterruptedException {System.out.println("Login!");UserService userService = (UserService) AopContext.currentProxy();userService.getUserName();}public void getUserName() throws InterruptedException {System.out.println("My Name is User");adminService.request();System.out.println("AdminUserName: " + adminService.user.getName());Thread.sleep(1000);}
}

4.编写AOP:我们计算getUserName方法消耗了多少时间。

@Aspect
@Service
public class UserAop {@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));}
}

访问结果如下:

接下来,我们希望在调用AdminService.request()之前,先模拟一次进行Admin用户的登录,那么我们同样可以通过AOP的方式去织入对应的逻辑。例如在UserAop中添加:

@Before("execution(* com.service.AdminService.request(..)) ")
public void logAdminLogin(JoinPoint pjp) throws Throwable {System.out.println("Admin Login ...");
}

此时我们在执行一遍:

从这个结果我们看出了什么?

  1. 织入确实成功了,在获取AdminName的时候,执行了Admin Login操作。
  2. 但是管理员名称获得失败,因为adminService.user.getName()这段代码抛了NPE
  3. 但是很奇怪呀,我AdminService这个属性怎么可能为null呢?我可是有构造函数执行的呀!

那么接下来我们就应该考虑到,是不是AOP操作的时候,这个代理对象有什么特殊的逻辑?请注意,本案例中,AdminService就是其中一个被拦截类。

二. 被拦截类的属性为何是null?

我在Spring源码系列- AOP实现这篇文章说过,关于AOP实现的两种方案的区别:

  • JDK动态代理只能对实现了接口的类生成代理,不能针对类
  • Cglib代理针对类进行代理。对指定的类生成一个子类,覆盖其中的方法。(注意对应的方法不要声明为final,否则无法重写)

而针对本文的案例来看,对于AdminService类而言,它并没有实现任何的接口,因此它在AOP代理下的机制是通过Cglib来实现的。可以验证一下:

2.1 原理分析

实际上,上面debug过程中贴的截图,它是AdminService的一个子类,它会覆盖所有public以及protected的方法。而内部的调用则交给原始对象来执行。我们来看下Spring中关于Cglib的一个具体实现:

入口在于CglibAopProxy.getProxy()

class CglibAopProxy implements AopProxy, Serializable {@Overridepublic Object getProxy(@Nullable ClassLoader classLoader) {// ...// 1.创建Enhancer类,作为主要的操作类Enhancer enhancer = createEnhancer();// ...// 2.设置拦截器Callback[] callbacks = getCallbacks(rootClass);// ...// 3.创建代理对象return createProxyClassAndInstance(enhancer, callbacks);// ...catch}
}

我们看下最后一步,关于代理对象的创建流程:

createProxyClassAndInstance(enhancer, callbacks);

对于这个函数,实际上,CglibAopProxy 还有个子类 ObjenesisCglibAopProxy ,它重写了这个方法。而实际代码跑起来发现,具体的执行逻辑也确实在子类:

我们来看下子类里面的一个大致实现:

@Override
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {// 1.创建代理类Class<?> proxyClass = enhancer.createClass();Object proxyInstance = null;//spring.objenesis.ignore默认为false .所以objenesis.isWorthTrying()一般为trueif (objenesis.isWorthTrying()) {try {// 2.创建对应的代理类实例proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());}// ..}if (proxyInstance == null) {// 3.如果objenesis实例化对象失败,再使用常规的方法,即反射来创建实例try {Constructor<?> ctor = (this.constructorArgs != null ?proxyClass.getDeclaredConstructor(this.constructorArgTypes) :proxyClass.getDeclaredConstructor());ReflectionUtils.makeAccessible(ctor);proxyInstance = (this.constructorArgs != null ?ctor.newInstance(this.constructorArgs) : ctor.newInstance());}// ..}((Factory) proxyInstance).setCallbacks(callbacks);return proxyInstance;
}
  1. 首先通过objenesis来实例化一个对象。
  2. 如果第一种不成功,再通过普通的反射来实例化一个对象。

然后我们来看下objenesis来实例化一个对象的一个调用栈:

这里大家可以根据这个调用栈,debug过程中,一个个往下看就行了,我贴个栈信息:

newConstructorForSerialization:357, ReflectionFactory (sun.reflect)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
newConstructorForSerialization:44, SunReflectionFactoryHelper (org.springframework.objenesis.instantiator.sun)
<init>:41, SunReflectionFactoryInstantiator (org.springframework.objenesis.instantiator.sun)
newInstantiatorOf:68, StdInstantiatorStrategy (org.springframework.objenesis.strategy)
newInstantiatorOf:125, SpringObjenesis (org.springframework.objenesis)
getInstantiatorOf:113, SpringObjenesis (org.springframework.objenesis)
newInstance:102, SpringObjenesis (org.springframework.objenesis)
createProxyClassAndInstance:62, ObjenesisCglibAopProxy (org.springframework.aop.framework)
getProxy:206, CglibAopProxy (org.springframework.aop.framework)

到这里我们知道,最后是通过ReflectionFactory.newConstructorForSerialization()来完成实例化的。而这个方法创建出来的对象是不会初始化类成员变量的。


我们来验证下

public class Test {private User user = new User("LJJ");private final User user2 = new User("LJJ");public String name = "Hello";public final String str = "ssss";public static void main(String[] args) throws Exception {ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();Constructor constructor = reflectionFactory.newConstructorForSerialization(Test.class, Object.class.getDeclaredConstructor());constructor.setAccessible(true);Test t = (Test) constructor.newInstance();System.out.println(t.user);System.out.println(t.user2);System.out.println(t.name);System.out.println(t.str);}
}

结果如下:

因此,对于本文而言,通过AOP创建的AdminService代理对象它的成员user是一个null值。

2.2 解决

既然我们无法直接从外部访问到这个user,我们可以从内部去访问,我们为user成员添加一个get方法:

public final User user = new User("LJJ");public User getUser() {return user;
}

那么UserService在访问的时候做出更改:

System.out.println("AdminUserName: " + adminService.user.getName());
↓↓↓↓↓↓↓↓
System.out.println("AdminUserName: " + adminService.getUser().getName());

那么再运行一遍结果如下:可见是正常的。

2.2.1 为何加一个 get 方法就可以避免NPE?

我们上文说到过,创建Cglib代理类的实现大概分为三个步骤:

class CglibAopProxy implements AopProxy, Serializable {@Overridepublic Object getProxy(@Nullable ClassLoader classLoader) {// ...// 1.创建Enhancer类,作为主要的操作类Enhancer enhancer = createEnhancer();// ...// 2.设置拦截器Callback[] callbacks = getCallbacks(rootClass);// ...// 3.创建代理对象return createProxyClassAndInstance(enhancer, callbacks);// ...catch}
}

而我们在2.1节中,针对于被拦截类的属性为null的问题,主要围绕着第三步来说的。那么这里,对于我们解决方案而言,仅仅是加了一个userget方法,就可以通过getUser的方式拿到一个非空对象,也是匪夷所思的。

我们知道第二步中。Spring将拦截器都加入到了DynamicAdvisedInterceptor这个类中,而该类又是MethodInterceptor的实现类。因此具体的Cglib方式的AOP代理必然在其中实现:

private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {@Override@Nullablepublic 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);}}}
}

我在另外一篇文章Spring常见问题解决 - this指针造成AOP失效说过,下述代码执行的是代理方法,此时就会被Spring拦截,进入intercept()函数,并且在该函数中通过原始对象来执行原始的方法。

retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();

那么重点来了:

  1. 执行代理方法的时候,除去增强的部分,只针对于原方法而言。此时调用的是原始对象的方法。
  2. 那么对于原始对象AdminService而言,user这个成员对象是已经被初始化过的public final User user = new User("LJJ");
  3. 而针对我们的代码调用来说,adminService.getUser().getName() 这段代码,adminService虽然是一个通过Cglib生成的被代理对象,但是当调用getUser()函数的时候,实际上引用的是原始对象。因此这里能够取到一个非空值。

当然,我们也可以通过另外一种方式去创建Cglib实例。也就是通过普通的反射方式,而不是通过objenesis来创建了。我们可以修改启动参数:spring.objenesis.ignore = true 即可:

debug图:首先isWorthTrying(0不再满足了,直接走下面的普通反射逻辑。不再通过objenesis来创建实例了。

然后我们再看下被代理对象里面的user成员变量是否还是null

2.3 总结

  1. 如果一个类没有实现某个接口,那么它在被AOP进行代理的时候,是通过Cglib的方式来创建一个代理对象的。
  2. Cglib创建代理实例的情况下,默认情况下,会优先采用objenesis来创建实例对象,再去通过普通的反射来完成。
  3. objenesis创建实例对象的最底层,则是通过ReflectionFactory.newConstructorForSerialization()来完成实例化的。而这个方法创建出来的对象是不会初始化类成员变量的。final修饰的String类型和基础数据类型除外)
  4. 因此被代理类对象中的成员变量是null(有个例,但针对于本文是null)。
  5. 因此我们可以通过给成员变量添加get方法,在代码编写的时候,避免直接通过 被代理对象.成员变量的方式去使用成员变量,否则容易造成空指针,需要使用对应的get方法去获得。
  6. 本质原因是因为,被代理对象中存储了原始对象的一个引用,而get方法是通过原始对象来完成调用的。因此只要原始对象里面,完成了对成员变量的初始化动作,就不会造成NPE

Spring常见问题解决 - AOP调用被拦截类的属性报NPE相关推荐

  1. Qt:解决跨线程调用socket/IO类,导致报错的问题(socket notifiers cannot be enabled from another thread)

    Qt有很多IO相关的类,比如说QTcpSocket.QFile,总的来说,在Qt的框架内使用,还是非常方便的. 但是用过其他框架IO类的人,可能有一个很不习惯,就是Qt的所有IO类,都不推荐或者不可以 ...

  2. ASP.net用法系列:如何从基类调用LINQ/EF类的属性

    如果有各种动物,比如Dogs/Cats/Cows/...,都有不同的Age方法,若想从其基类用相同的方法ShowAge来显示这些不同的Age,自然就可以借用基类Animal的virtual函数,比如: ...

  3. 【Katalon常见问题解决四】浏览器升级后,katalon报错 Unable to open browser with url: ''

    以谷歌浏览器为例 浏览器升级后,katalon跑已经录制好的脚本,会报下 Unable to open browser with url: '' 问题是:chromedriver版本不对, 解决方法是 ...

  4. C++调用MATLAB程序进行混合编程以及常见问题解决

    C++调用MATLAB程序进行混合编程以及常见问题解决 C++调用MATLAB程序方法 MATLAB打包生成DLL动态链接库 VS2017环境配置 程序调用 常见问题解决 参考 C++调用MATLAB ...

  5. 【Java 19】反射 - 反射机制概述、获取Class实例、类的加载与ClassLoader的理解、创建运行时类的对象、获取运行时类的完整结构、调用运行时类的指定结构、动态代理

    反射机制概述.获取Class实例.类的加载与ClassLoader的理解.创建运行时类的对象.获取运行时类的完整结构.调用运行时类的指定结构.动态代理 反射 1 Java反射机制概述 1.1 Java ...

  6. Spring AOP原理及拦截器

    原理 AOP(Aspect Oriented Programming),也就是面向方面编程的技术.AOP基于IoC基础,是对OOP的有益补充. AOP将应用系统分为两部分,核心业务逻辑(Core bu ...

  7. spring面向切面aop拦截器

    spring中有很多概念和名词,其中有一些名字不同,但是从功能上来看总感觉是那么的相似,比如过滤器.拦截器.aop等. 过滤器filter.spring mvc拦截器Interceptor .面向切面 ...

  8. spring常见面试问题_Spring面试问题

    spring常见面试问题 另外,请查看我们最新的文章69Spring面试问题与解答–最终清单 . 1)什么是春天? 回答: Spring是控件和面向方面的容器框架的轻量级反转. 2)解释春天? 回答: ...

  9. Spring (Bean, IoC, AOP, SpringMVC)

    Spring - Bean, IoC, AOP, SpringMVC Spring 1. 核心容器 1.1 Spring-beans 1.1.1 Bean 的配置 1.1.1.1 自动装配 1.1.1 ...

最新文章

  1. jenkins的使用
  2. pythonprint()_python基础1 print()函数
  3. Java自带的多线程监控分析工具(VisualVM)
  4. Python的字符串
  5. C++找到一个大于或等于n且为2的幂的数字p的算法实现(附完整源码)
  6. C++多态案例一计算器类
  7. 智慧工厂如何运转?飞凌FCU2303-5G智能网关来告诉你
  8. java else if和switch_如何优雅地优化代码中的的if else和switch
  9. CSS布局的三个关键属性:float、position、display
  10. Java生产环境下性能监控与调优详解 第3章 基于JVisualVM的可视化监控
  11. 如何删除C++容器中的值
  12. java i18n_Java i18n – Java的国际化
  13. 面向过程和面向对象的区别,通俗易懂
  14. java 反射 getClass()
  15. matlab处理波动的数据,波动数据时间序列的分析与处理
  16. 3.6计算机网络(网络层概述 电路交换 报文交换 分组交换)
  17. balenaEtcher for mac(U盘启动盘制作工具)
  18. 数据仓库实践-拉链表设计
  19. PyTorch(Python)训练MNIST模型移动端IOS上使用Swift实时数字识别
  20. rips php,审计PHP工具篇之 RIPS

热门文章

  1. 不要去怨恨上天的不公平
  2. 2021年8月最好用的苹果cms采集站
  3. idea debug不显示String对象的@实例ID
  4. OnePieceReader 一个海贼王漫画阅读工具
  5. 转:充值系列—充值系统数据库设计(一)
  6. 2022年重庆二级建造师市政公用工程《城市桥梁工程质量检查与检验》每日练习及答案
  7. 光通量发光强度照度亮度关系_发光强度、光通量、照度、亮度之间有什么区别呢?用最浅鲜易懂的方法解答一下吧?...
  8. 计算机硬盘丢失了怎么找回,电脑硬盘空间丢失 硬盘空间丢失怎么办 - 云骑士一键重装系统...
  9. 云文档服务器开小差,ACER桌面云解决方案(2020年十二月整理).pdf
  10. centos查看盘符_Centos下磁盘管理的常用命令记录(如查找大文件)