Spring常见问题解决 - AOP调用被拦截类的属性报NPE
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 ...");
}
此时我们在执行一遍:
从这个结果我们看出了什么?
- 织入确实成功了,在获取
AdminName
的时候,执行了Admin Login
操作。 - 但是管理员名称获得失败,因为
adminService.user.getName()
这段代码抛了NPE
。 - 但是很奇怪呀,我
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;
}
- 首先通过
objenesis
来实例化一个对象。 - 如果第一种不成功,再通过普通的反射来实例化一个对象。
然后我们来看下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
的问题,主要围绕着第三步来说的。那么这里,对于我们解决方案而言,仅仅是加了一个user
的get
方法,就可以通过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();
那么重点来了:
- 执行代理方法的时候,除去增强的部分,只针对于原方法而言。此时调用的是原始对象的方法。
- 那么对于原始对象
AdminService
而言,user
这个成员对象是已经被初始化过的public final User user = new User("LJJ");
。 - 而针对我们的代码调用来说,
adminService.getUser().getName()
这段代码,adminService
虽然是一个通过Cglib
生成的被代理对象,但是当调用getUser()
函数的时候,实际上引用的是原始对象。因此这里能够取到一个非空值。
当然,我们也可以通过另外一种方式去创建Cglib
实例。也就是通过普通的反射方式,而不是通过objenesis
来创建了。我们可以修改启动参数:spring.objenesis.ignore = true
即可:
debug
图:首先isWorthTrying(0不再满足了,直接走下面的普通反射逻辑。不再通过objenesis
来创建实例了。
然后我们再看下被代理对象里面的user
成员变量是否还是null
?
2.3 总结
- 如果一个类没有实现某个接口,那么它在被
AOP
进行代理的时候,是通过Cglib
的方式来创建一个代理对象的。 Cglib
创建代理实例的情况下,默认情况下,会优先采用objenesis
来创建实例对象,再去通过普通的反射来完成。objenesis
创建实例对象的最底层,则是通过ReflectionFactory.newConstructorForSerialization()
来完成实例化的。而这个方法创建出来的对象是不会初始化类成员变量的。 (final
修饰的String
类型和基础数据类型除外)- 因此被代理类对象中的成员变量是
null
(有个例,但针对于本文是null
)。 - 因此我们可以通过给成员变量添加
get
方法,在代码编写的时候,避免直接通过被代理对象.成员变量
的方式去使用成员变量,否则容易造成空指针,需要使用对应的get
方法去获得。 - 本质原因是因为,被代理对象中存储了原始对象的一个引用,而
get
方法是通过原始对象来完成调用的。因此只要原始对象里面,完成了对成员变量的初始化动作,就不会造成NPE
。
Spring常见问题解决 - AOP调用被拦截类的属性报NPE相关推荐
- Qt:解决跨线程调用socket/IO类,导致报错的问题(socket notifiers cannot be enabled from another thread)
Qt有很多IO相关的类,比如说QTcpSocket.QFile,总的来说,在Qt的框架内使用,还是非常方便的. 但是用过其他框架IO类的人,可能有一个很不习惯,就是Qt的所有IO类,都不推荐或者不可以 ...
- ASP.net用法系列:如何从基类调用LINQ/EF类的属性
如果有各种动物,比如Dogs/Cats/Cows/...,都有不同的Age方法,若想从其基类用相同的方法ShowAge来显示这些不同的Age,自然就可以借用基类Animal的virtual函数,比如: ...
- 【Katalon常见问题解决四】浏览器升级后,katalon报错 Unable to open browser with url: ''
以谷歌浏览器为例 浏览器升级后,katalon跑已经录制好的脚本,会报下 Unable to open browser with url: '' 问题是:chromedriver版本不对, 解决方法是 ...
- C++调用MATLAB程序进行混合编程以及常见问题解决
C++调用MATLAB程序进行混合编程以及常见问题解决 C++调用MATLAB程序方法 MATLAB打包生成DLL动态链接库 VS2017环境配置 程序调用 常见问题解决 参考 C++调用MATLAB ...
- 【Java 19】反射 - 反射机制概述、获取Class实例、类的加载与ClassLoader的理解、创建运行时类的对象、获取运行时类的完整结构、调用运行时类的指定结构、动态代理
反射机制概述.获取Class实例.类的加载与ClassLoader的理解.创建运行时类的对象.获取运行时类的完整结构.调用运行时类的指定结构.动态代理 反射 1 Java反射机制概述 1.1 Java ...
- Spring AOP原理及拦截器
原理 AOP(Aspect Oriented Programming),也就是面向方面编程的技术.AOP基于IoC基础,是对OOP的有益补充. AOP将应用系统分为两部分,核心业务逻辑(Core bu ...
- spring面向切面aop拦截器
spring中有很多概念和名词,其中有一些名字不同,但是从功能上来看总感觉是那么的相似,比如过滤器.拦截器.aop等. 过滤器filter.spring mvc拦截器Interceptor .面向切面 ...
- spring常见面试问题_Spring面试问题
spring常见面试问题 另外,请查看我们最新的文章69Spring面试问题与解答–最终清单 . 1)什么是春天? 回答: Spring是控件和面向方面的容器框架的轻量级反转. 2)解释春天? 回答: ...
- 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 ...
最新文章
- jenkins的使用
- pythonprint()_python基础1 print()函数
- Java自带的多线程监控分析工具(VisualVM)
- Python的字符串
- C++找到一个大于或等于n且为2的幂的数字p的算法实现(附完整源码)
- C++多态案例一计算器类
- 智慧工厂如何运转?飞凌FCU2303-5G智能网关来告诉你
- java else if和switch_如何优雅地优化代码中的的if else和switch
- CSS布局的三个关键属性:float、position、display
- Java生产环境下性能监控与调优详解 第3章 基于JVisualVM的可视化监控
- 如何删除C++容器中的值
- java i18n_Java i18n – Java的国际化
- 面向过程和面向对象的区别,通俗易懂
- java 反射 getClass()
- matlab处理波动的数据,波动数据时间序列的分析与处理
- 3.6计算机网络(网络层概述 电路交换 报文交换 分组交换)
- balenaEtcher for mac(U盘启动盘制作工具)
- 数据仓库实践-拉链表设计
- PyTorch(Python)训练MNIST模型移动端IOS上使用Swift实时数字识别
- rips php,审计PHP工具篇之 RIPS
热门文章
- 不要去怨恨上天的不公平
- 2021年8月最好用的苹果cms采集站
- idea debug不显示String对象的@实例ID
- OnePieceReader 一个海贼王漫画阅读工具
- 转:充值系列—充值系统数据库设计(一)
- 2022年重庆二级建造师市政公用工程《城市桥梁工程质量检查与检验》每日练习及答案
- 光通量发光强度照度亮度关系_发光强度、光通量、照度、亮度之间有什么区别呢?用最浅鲜易懂的方法解答一下吧?...
- 计算机硬盘丢失了怎么找回,电脑硬盘空间丢失 硬盘空间丢失怎么办 - 云骑士一键重装系统...
- 云文档服务器开小差,ACER桌面云解决方案(2020年十二月整理).pdf
- centos查看盘符_Centos下磁盘管理的常用命令记录(如查找大文件)