Day606.SpringAOP常编程错误案例① -Spring编程常见错误
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day606.SpringAOP常编程错误案例① -Spring编程常见错误相关的知识,希望对你有一定的参考价值。
SpringAOP常编程错误案例①
Spring AOP 是 Spring 中除了依赖注入外(DI)最为核心的功能。
顾名思义,AOP 即 Aspect Oriented Programming,翻译为面向切面编程。
而 Spring AOP 则利用 CGlib
和 JDK 动态代理
等方式来实现运行期动态方法增强,其目的是将与业务无关的代码单独抽离出来,使其逻辑不再与业务代码耦合,从而降低系统的耦合性,提高程序的可重用性和开发效率。
追根溯源,我们之所以能无感知地在容器对象方法前后任意添加代码片段,那是由于 Spring 在运行期帮我们把切面中的代码逻辑动态“织入”到了容器对象方法内,所以说 AOP 本质上就是一个代理模式
。
下面记录两种AOP编程中会出现的编程错误案例!
一、this 调用的当前类方法无法被拦截
假设我们系统,这个模块包含一个负责电费充值的类 ElectricService,它含有一个充电方法 charge():
@Service
public class ElectricService
public void charge() throws Exception
System.out.println("Electric charging ...");
this.pay();
public void pay() throws Exception
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
在这个电费充值方法 charge() 中,我们会使用支付宝进行充值。
因此在这个方法中,我加入了 pay() 方法。为了模拟 pay() 方法调用耗时,代码执行了休眠 1 秒,并在 charge() 方法里使用 this.pay() 的方式调用这种支付方法。
但是因为支付服务是第三方接口,我们需要记录下接口调用时间。
@Aspect
@Service
@Slf4j
public class AopConfig
@Around("execution(* com.spring.puzzle.class5.example1.ElectricService.pay()) ")
public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable
long start = System.currentTimeMillis();
joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("Pay method time cost(ms): " + (end - start));
这时候我们就引入了一个 @Around 的增强 ,分别记录在 pay() 方法执行前后的时间,并计算出执行 pay() 方法的耗时。
@RestController
public class HelloWorldController
@Autowired
ElectricService electricService;
@RequestMapping(path = "charge", method = RequestMethod.GET)
public void charge() throws Exception
electricService.charge();
;
最后我们再通过定义一个 Controller 来提供暴露接口。
完成代码后,我们访问上述接口,会发现这段计算时间的切面并没有执行到
,输出日志如下:
Electric charging …
Pay with alipay …
回溯之前的代码可知,在 @Around 的切面类中,我们很清晰地定义了切面对应的方法,但是却没有被执行到。
这说明了在类的内部,通过 this 方式调用的方法,是没有被 Spring AOP 增强的
。这是为什么呢?
首先来设置个断点,调试看看 this 对应的对象是什么样的: 看到这个this就是Service本身
再看看在 Controller 层中自动装配的 ElectricService 对象是什么样:被 Spring 增强代理过的 Bean
首先Spring创建代理对象的过程。先来看下调用栈:
创建代理对象的时机就是创建一个 Bean 的时候,而创建的的关键工作其实是由 AnnotationAwareAspectJAutoProxyCreator 完成的。
它本质上是一种 BeanPostProcessor
。所以它的执行是在完成原始 Bean 构建后的初始化 Bean(initializeBean)过程中。
而它到底完成了什么工作呢?我们可以看下它的 postProcessAfterInitialization 方法
:
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName)
if (bean != null)
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean)
//可以看到,他会进行判断,是否有必须要进行包装
return wrapIfNecessary(bean, beanName, cacheKey);
return bean;
具体到这个 wrap 过程,可参考下面的关键代码行:
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey)
// 省略非关键代码
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY)
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
// 省略非关键代码
上述代码中,createProxy()
调用是创建代理对象的关键。
具体到执行过程,它首先会创建一个代理工厂,然后将通知器(advisors)、被代理对象等信息加入到代理工厂,最后通过这个代理工厂来获取代理对象。一些关键过程参考下面的方法:
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource)
// 省略非关键代码
ProxyFactory proxyFactory = new ProxyFactory();
if (!proxyFactory.isProxyTargetClass())
if (shouldProxyTargetClass(beanClass, beanName))
proxyFactory.setProxyTargetClass(true);
else
evaluateProxyInterfaces(beanClass, proxyFactory);
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
// 省略非关键代码
return proxyFactory.getProxy(getProxyClassLoader());
经过这样一个过程,一个代理对象就被创建出来了。
从 Spring 中获取到的对象都是这个代理对象,所以具有 AOP 功能。而之前直接使用 this 引用到的只是一个普通对象,自然也就没办法实现 AOP 的功能了。
所以从上面看出,只有引用的是被动态代理创建出来的对象,才会被 Spring 增强,具备 AOP 该有的功能。
那么解决方案这里提供两种:
- 第一种:被
@Autowired 注解
的,于是我们的代码可以改成这样,即通过 @Autowired 的方式,在类的内部,自己引用自己:@Service public class ElectricService @Autowired ElectricService electricService; public void charge() throws Exception System.out.println("Electric charging ..."); //this.pay(); electricService.pay(); public void pay() throws Exception System.out.println("Pay with alipay ..."); Thread.sleep(1000);
- 第二种:直接从
AopContext
获取当前的 Proxy,通过一个 ThreadLocal 来将 Proxy 和线程绑定起来,这样就可以随时拿出当前线程绑定的 Proxy。- @EnableAspectJAutoProxy 里加一个配置项 exposeProxy = true ,Spring默认是关闭的
@SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class Application // 省略非关键代码
- 通过AopContext.currentProxy(),获取当前代理对象
@Service public class ElectricService public void charge() throws Exception System.out.println("Electric charging ..."); ElectricService electric = ((ElectricService) AopContext.currentProxy()); electric.pay(); public void pay() throws Exception System.out.println("Pay with alipay ..."); Thread.sleep(1000);
这两种方法的效果其实是一样的,最终我们打印出了期待的日志,到这,问题顺利解决了。
Electric charging ...
Pay with alipay ...
Pay method time cost(ms): 1005
二、通过代理类访问被代理类的成员属性抛空指针异常
在系统中,我们使用了 charge() 方法进行支付。
在统一结算的时候我们会用到一个管理员用户付款编号,这时候就用到了几个新的类。User 类,包含用户的付款编号信息:
public class User
private String payNum;
public User(String payNum)
this.payNum = payNum;
public String getPayNum()
return payNum;
public void setPayNum(String payNum)
this.payNum = payNum;
AdminUserService 类,包含一个管理员用户(User),其付款编号为 2022050393;
另外,这个服务类有一个 login() 方法,用来登录系统。
@Service
public class AdminUserService
public final User adminUser = new User("2022050393");
public void login()
System.out.println("admin user login...");
我们需要修改 ElectricService 类实现这个需求:
在电费充值时,需要管理员登录并使用其编号进行结算。完整代码如下:
@Service
public class ElectricService
@Autowired
private AdminUserService adminUserService;
public void charge() throws Exception
System.out.println("Electric charging ...");
this.pay();
public void pay() throws Exception
adminUserService.login();
String payNum = adminUserService.adminUser.getPayNum();
System.out.println("User pay num : " + payNum);
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
代码完成后,执行 charge() 操作,一切正常:
Electric charging …
admin user login…
User pay num : 2022050393
Pay with alipay …
这时候,由于安全需要,就需要管理员在登录时,记录一行日志以便于以后审计管理员操作。
所以我们添加一个 AOP 相关配置类,具体如下:
@Aspect
@Service
@Slf4j
public class AopConfig
@Before("execution(* com.spring.puzzle.class5.example2.AdminUserService.login(..)) ")
public void logAdminLogin(JoinPoint pjp) throws Throwable
System.out.println("! admin login ...");
添加这段代码后,我们执行 charge() 操作,发现不仅没有相关日志,而且在执行下面这一行代码的时候直接抛出了 NullPointerException:
String payNum = dminUserService.user.getPayNum();
那为什么会出现这个问题呢???
可以看出上面Spring用的是Cglib的方式
去创建代理对象的方式。
CGLIB 中 AOP 的实现是基于 org.springframework.cglib.proxy 包中 Enhancer
和 MethodInterceptor
两个接口来实现的。
整个过程,我们可以概括为三个步骤:
- 定义自定义的 MethodInterceptor 负责委托方法执行;
- 创建 Enhance 并设置 Callback 为上述 MethodInterceptor;
- enhancer.create() 创建代理。
那Cglib的方式,在最后Spring 会默认尝试使用 objenesis 方式实例化
对象,如果失败则再次尝试使用常规方式实例化对象
。
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks)
//创建代理类Class
Class<?> proxyClass = enhancer.createClass();
Object proxyInstance = null;
//spring.objenesis.ignore默认为false
//所以objenesis.isWorthTrying()一般为true
if (objenesis.isWorthTrying())
try
//先尝试使用objenesis的方式实例化对象
// 创建实例
proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
catch (Throwable ex)
// 省略非关键代码
//objenesis的方式实例化对象失败后,就在下面
if (proxyInstance == null)
// 尝试普通反射方式创建实例
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 方式最后使用了 JDK 的 ReflectionFactory.newConstructorForSerialization()
完成了代理对象的实例化。
而如果你稍微研究下这个方法,你会惊讶地发现,这种方式创建出来的对象是不会初始化类成员变量
的。
这个案例的核心是代理类实例的默认构建方式很特别。
在这里,我们可以总结和对比下通过反射来实例化对象的方式,包括:
- java.lang.Class.newInsance()
- java.lang.reflect.Constructor.newInstance()
- sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance()
前两种初始化方式都会同时初始化类成员变量,但是最后一种通过 ReflectionFactory.newConstructorForSerialization().newInstance() 实例化类则不会初始化类成员变量,这就是当前问题的最终答案了。
那知道了为什么,我们无法通过代理类去访问被代理类的成员变量的问题的原因是Spring使用Cglib方式去生成代理对象。其中他最后使用到了objenesis方式去生成代理对象,
这种方式中会去使用sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance()
这种方式去生成对应代理对象。但是这种方式不会去初始化被代理类的成员变量给代理类,这就是为什么。
那提供的这里提供两种解决方案:
我们不可能去直接改写Spring的源码让他换对应使用的生成代理类的方式。
-
通过
类方法
去获取被代理类的成员变量public User getUser() return user;
在 ElectricService 里通过 getUser() 获取 User 对象:
// 原来出错的方式:
//String payNum = = adminUserService.adminUser.getPayNum();
// 修改后的方式:
String payNum = adminUserService.getAdminUser().getPayNum(); -
修改启动参数
spring.objenesis.ignore
,告诉Spring,让他初始化代理类中的被代理类的成员属性
三、总结
-
使用 AOP,实际上就是让 Spring 自动为我们创建一个 Proxy,使得调用者能无感知地调用指定方法。而 Spring 有助于我们在运行期里动态织入其它逻辑,因此,AOP 本质上就是一个动态代理。
-
我们只有访问这些代理对象的方法,才能获得 AOP 实现的功能,所以通过 this 引用是无法正确使用 AOP 功能的。在不能改变代码结果前提下,我们可以通过 @Autowired、AopContext.currentProxy() 等方式获取相应的代理对象来实现所需的功能。
-
我们一般不能直接从代理类中去拿被代理类的属性,这是因为除非我们显示设置 spring.objenesis.ignore 为 true,否则代理类的属性是不会被 Spring 初始化的,我们可以通过在被代理类中增加一个方法来间接获取其属性。
以上是关于Day606.SpringAOP常编程错误案例① -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章
Day616.SpringException常见错误 -Spring常见编程错误