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

30 篇文章 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 {
    @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);
    }
}

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 {
	@Override
	public 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()一般为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. 首先通过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 {
	@Override
	public 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
	@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);
			}
		}
	}
}

我在另外一篇文章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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值