Spring编程常见错误50例-Spring AOP常见错误(上)

Spring AOP常见错误(上)

this调用的当前类方法无法被拦截

问题

假设当前开发负责电费充值的类,同时记录下进行充值的时间(此时需要使用到AOP),并提供电费充值接口:

@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);}
}
@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));}
}
@RestController
public class HelloWorldController {@AutowiredElectricService electricService;@RequestMapping(path = "charge", method = RequestMethod.GET)public void charge() throws Exception{electricService.charge();};
}

但是在访问接口后,计算时间的切面并没有被执行,即在类的内部通过this方式调用的方法没被AOP增强的

原因

通过Debug可知this对应的是普通的ElectricService对象,而在控制器类装配的electricService对象是被Spring增强后的Bean:
在这里插入图片描述
在这里插入图片描述
先补充关于Spring AOP的基础知识:

  • Spring AOP的实现

    • Spring AOP的底层是动态代理,而创建代理的方式有两种:

      • JDK动态代理只能对实现了接口的类生成代理,而不能针对普通类

      • CGLIB可针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法来实现代理对象
        在这里插入图片描述

  • 如何使用Spring AOP

    • 添加依赖:
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
    </dependency>	
    
    • 添加注解:对于非Spring Boot程序,除了添加依赖项外还常会使用@EnableAspectJAutoProxy来开启AOP功能

具体看下创建代理对象的过程:创建代理对象的关键由AnnotationAwareAspectJAutoProxyCreator完成的,它本质上是一种BeanPostProcessor,所以它的执行是在完成原始Bean构建后的初始化Bean中:

// AbstractAutoProxyCreator#postProcessAfterInitialization
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (this.earlyProxyReferences.remove(cacheKey) != bean) {// *需使用AOP时,该方法把创建的原始的Bean对象wrap成代理对象作为Bean返回return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;
}
// AbstractAutoProxyCreator#wrapIfNecessary
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;}...
}
// AbstractAutoProxyCreator#createProxy
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,@Nullable Object[] specificInterceptors, TargetSource targetSource) {...// 创建代理工厂ProxyFactory proxyFactory = new ProxyFactory();proxyFactory.copyFrom(this);// 将通知器(advisors)、被代理对象等信息加入到代理工厂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());
}

只有通过上述工厂才创建出一个代理对象,而之前直接使用this使用的还是普通对象

解决方式

方法的核心在于引用被动态代理创建出来的对象,有以下两种方式:

  • 使用被@Autowired注解的对象替换this
@Service
public class ElectricService {@AutowiredElectricService electricService;public void charge() throws Exception {System.out.println("Electric charging ...");electric.pay();}public void pay() throws Exception {System.out.println("Pay with alipay ...");Thread.sleep(1000);}
}
  • 直接从AopContext获取当前的ProxyAopContext是通过一个ThreadLocalProxy和线程绑定,这样就可随时拿出当前线程绑定的Proxy(前提是在 @EnableAspectJAutoProxy里加配置项exposeProxy = true,表示将代理对象放入到ThreadLocal
@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);}}
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

直接访问被拦截类的属性抛空指针异常

问题

在使用charge方法进行支付时会用到一个管理员用户付款编号,此时新增几个类:

// 包含用户的付款编号信息
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;}
}
@Service
public class AdminUserService {public final User adminUser = new User("202101166");// 用于登录系统public void login() {System.out.println("admin user login...");}
}

在电费充值时需管理员登录并使用其编号进行结算:

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

由于安全需管理员在登录时记录一行日志以便于以后审计管理员操作:

@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 ...");}
}

结果在执行到接口中的electricService.charge()时不仅没打印日志,还执行String payNum = adminUserService.adminUser.getPayNum()NPE,对pay方法进行分析后发现加入AOPadminUserService对象已经是代理对象了,但是它的adminUser属性是null

在这里插入图片描述

原因

增强后的类实际是AdminUserService的子类,它会重写所有publicprotected方法,并在内部将调用委托给原始的AdminUserService实例(以CGLIBProxy的实现类CglibAopProxy为例来看具体的流程)

public Object getProxy(@Nullable ClassLoader classLoader) {...// ①创建并配置enhancerEnhancer enhancer = createEnhancer();...// ②获取Callback:包含DynamicAdvisedInterceptor,即MethodInterceptorCallback[] callbacks = getCallbacks(rootClass);...// ③生成代理对象并创建代理,即设置enhancer的callback值return createProxyClassAndInstance(enhancer, callbacks);}...
}

第三步会执行到CglibAopProxy子类ObjenesisCglibAopProxycreateProxyClassAndInstance方法中:Spring会默认尝试使用objenesis方式实例化对象,如失败则再尝试使用常规方式实例化对象

protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {// 创建代理类classClass<?> proxyClass = enhancer.createClass();Object proxyInstance = null;// 一般为trueif (objenesis.isWorthTrying()) {try {// 创建实例proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());}...}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方式最后使用了JDKReflectionFactory.newConstructorForSerialization方法完成代理对象的实例化,这种方式创建出来的对象不会初始化类成员变量

解决方式

AdminUserService里写getAdminUser方法,从内部访问获取变量:

@Service
public class AdminUserService {public final User adminUser = new User("202101166");public User getAdminUser() {return adminUser;}public void login() {System.out.println("admin user login...");}
}
@Service
public class ElectricService {@Autowiredprivate AdminUserService adminUserService;public void charge() throws Exception {System.out.println("Electric charging ...");this.pay();}public void pay() throws Exception {adminUserService.login();String payNum = adminUserService.getAdminUser().getPayNum();  // 原来该步骤处报NPESystem.out.println("User pay num : " + payNum);System.out.println("Pay with alipay ...");Thread.sleep(1000);}
}

既然代理类的类属性不会被初始化,为啥可通过AdminUserServicegetUser方法获取到代理类实例的属性?当代理类方法被调用后会被Spring拦截,进入到DynamicAdvisedInterceptor#intercept方法,在此方法中获取被代理的原始对象(原始对象的类属性是被实例化过且存在的)

根据原因分析,还可以有另一种解决方式:修改启动参数spring.objenesis.ignore=true

参考

极客时间-Spring 编程常见错误 50 例

https://github.com/jiafu1115/springissue/tree/master/src/main/java/com/spring/puzzle/class5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/133081.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot (1)

目录 1 入门案例 1.1 环境准备 1.2 编写pom.xml 1.3 编写入口程序 1.4 编写接口 1.5 编写配置 1.6 快速部署 1.6.1 打jar包 1.6.2 部署 1.7 访问接口 2 全注解开发 2.1 常用注解 2.2 属性绑定注解 2.2.1 注册组件 2.2.2 ConfigurationProperties(prefix"te…

SQLAlchemy 使用封装实例

类封装 database.py #! /usr/bin/env python # -*- coding: utf-8 -*-import sys import json import logging from datetime import datetimefrom core.utils import classlock, parse_bool from core.config import (MYSQL_HOST,MYSQL_PORT,MYSQL_USER,MYSQL_PASS,MYSQL_DA…

黑马JVM总结(三十二)

&#xff08;1&#xff09;类加载器-线程上下文1 使用的应用程序类加载器来完成类的加载&#xff0c;不是用的启动类加载器&#xff0c;jdk在某些情况下要打破&#xff0c;双亲委派的模式&#xff0c;有时候需要调用应用程序类加载器来完成类的加载&#xff0c;否则有些类它是找…

从读不完一篇文章,到啃下20万字巨著,大模型公司卷起“长文本”

点击关注 文丨郝 鑫 编丨刘雨琦 4000到40万token&#xff0c;大模型正在以“肉眼可见”的速度越变越“长”。 长文本能力似乎成为象征着大模型厂商出手的又一新“标配”。 国外&#xff0c;OpenAI经过三次升级&#xff0c;GPT-3.5上下文输入长度从4千增长至1.6万token&…

黑马JVM总结(三十一)

&#xff08;1&#xff09;类加载器-概述 启动类加载器-扩展类类加载器-应用程序类加载器 双亲委派模式&#xff1a; 类加载器&#xff0c;加载类的顺序是先依次请问父级有没有加载&#xff0c;没有加载自己才加载&#xff0c;扩展类加载器在getParent的时候为null 以为Boots…

STM32 CubeMX PWM三种模式(互补,死区互补,普通)(HAL库)

STM32 CubeMX PWM两种模式&#xff08;HAL库&#xff09; STM32 CubeMX STM32 CubeMX PWM两种模式&#xff08;HAL库&#xff09;一、互补对称输出STM32 CubeMX设置代码部分 二、带死区互补模式STM32 CubeMX设置代码 三、普通模式STM32 CubeMX设置代码部分 总结 一、互补对称输…

运维大数据平台的建设与实践探索

随着企业数字化转型的推进&#xff0c;运维管理面临着前所未有的挑战和机遇。为应对日益复杂且严峻的挑战&#xff0c;数字免疫系统和智能运维等概念应运而生。数字免疫系统和智能运维作为新兴技术&#xff0c;正引领着运维管理的新趋势。数字免疫系统和智能运维都借助大数据运…

基本微信小程序的购物商城系统

项目介绍 随着互联网的趋势的到来&#xff0c;各行各业都在考虑利用互联网将自己的信息推广出去&#xff0c;最好方式就是建立自己的平台信息&#xff0c;并对其进行管理&#xff0c;随着现在智能手机的普及&#xff0c;人们对于智能手机里面的应用购物平台小程序也在不断的使…

vscode 资源管理器移动到右边

目录 vscode 资源管理器移动到右边 vscode 资源管理器移动到右边 点击 文件》首选项》设置》工作台》外观》 找到这个配置下拉选择左右

神秘的锦衣卫

在看明朝电视剧经常听到的一句台词&#xff1a;锦衣卫办案&#xff0c;闲杂人等速速离开。锦衣卫是明朝特务机构&#xff0c;直接听命于皇帝&#xff0c;是亲军卫之一&#xff0c;也是最重要的一卫。 1、卫所制 卫所制是明代最主要的军事制度&#xff0c;其目标是寓兵于农、屯…

Java数据结构第十七章、手撕位图

给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在这40亿个数中。【腾讯】 1. 遍历&#xff0c;时间复杂度O(N) 2. 排序(O(NlogN))&#xff0c;利用二分查找: logN 3. 位图解决 数据是否在给定的整形数据中&#xff0c;结…

C++学习——“面向对象编程”的涵义

以下内容源于C语言中文网的学习与整理&#xff0c;非原创&#xff0c;如有侵权请告知删除。 类是一个通用的概念&#xff0c;C、Java、C#、PHP 等很多编程语言中都支持类&#xff0c;都可以通过类创建对象。我们可以将类看做是结构体的升级版&#xff0c;C语言的晚辈们看到了C…