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


    和本篇文章有关的另一篇文章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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    @Service
    public class AdminService {
        public final User user = new User("LJJ");
    
        public void request() {
            System.out.println("Request to Admin");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.再写一个UserService类:

    @Service
    public class UserService {
        @Autowired
        private 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    访问结果如下:
    在这里插入图片描述

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

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

    此时我们在执行一遍:
    在这里插入图片描述
    从这个结果我们看出了什么?

    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 {
    	@Override
    	public Object getProxy(@Nullable ClassLoader classLoader) {
    		// ...
    		// 1.创建Enhancer类,作为主要的操作类
    		Enhancer enhancer = createEnhancer();
    		// ...
    		// 2.设置拦截器
    		Callback[] callbacks = getCallbacks(rootClass);
    		// ...
    		// 3.创建代理对象
    		return createProxyClassAndInstance(enhancer, callbacks);
    		// ...catch
    	}
    }	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    createProxyClassAndInstance(enhancer, callbacks);
    
    • 1

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

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

    @Override
    protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    	// 1.创建代理类
    	Class<?> proxyClass = enhancer.createClass();
    	Object proxyInstance = null;
    	//spring.objenesis.ignore默认为false .所以objenesis.isWorthTrying()一般为true
    	if (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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    到这里我们知道,最后是通过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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    结果如下:
    在这里插入图片描述
    因此,对于本文而言,通过AOP创建的AdminService代理对象它的成员user是一个null值。
    在这里插入图片描述

    2.2 解决

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

    public final User user = new User("LJJ");
    
    public User getUser() {
        return user;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

    那么再运行一遍结果如下:可见是正常的。
    在这里插入图片描述

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

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

    class CglibAopProxy implements AopProxy, Serializable {
    	@Override
    	public Object getProxy(@Nullable ClassLoader classLoader) {
    		// ...
    		// 1.创建Enhancer类,作为主要的操作类
    		Enhancer enhancer = createEnhancer();
    		// ...
    		// 2.设置拦截器
    		Callback[] callbacks = getCallbacks(rootClass);
    		// ...
    		// 3.创建代理对象
    		return createProxyClassAndInstance(enhancer, callbacks);
    		// ...catch
    	}
    }	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

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

    private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {
    	@Override
    	@Nullable
    	public 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);
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

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

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

    那么重点来了:

    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
  • 相关阅读:
    windows11安装东山哪吒STU板Linux开发环境(全志D1-H)-操作记录
    使用 Hue 玩转 Amazon EMR(SparkSQL, Phoenix) 和 Amazon Redshift
    php 获取音频时长等信息
    乌班图20.04简易部署k8s+kuboard第三方面板
    从传统到智能 | 拓世法宝AI智能直播一体机为商家注入活力
    CMake中math的使用
    【Android Camera开发】Android Automotive介绍
    vue3 封装自定义指令,监听元素宽高的变化
    WebSocket协议:实现实时双向通信的秘诀
    zabbix-agent主动模式下自定义监控项和监控指标
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126473442