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

news2024/12/24 9:39:44

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 {
    @Autowired
    ElectricService 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 {
	@Autowired
    ElectricService 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 {
    @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);
    }
}

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

@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) {
    ...
        // ①创建并配置enhancer
        Enhancer enhancer = createEnhancer();
        ...
        // ②获取Callback:包含DynamicAdvisedInterceptor,即MethodInterceptor
        Callback[] callbacks = getCallbacks(rootClass);
    	...
        // ③生成代理对象并创建代理,即设置enhancer的callback值
        return createProxyClassAndInstance(enhancer, callbacks);
    }
    ...
}

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

protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    // 创建代理类class
    Class<?> proxyClass = enhancer.createClass();
    Object proxyInstance = null;
	// 一般为true
    if (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 {
    @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.getAdminUser().getPayNum();  // 原来该步骤处报NPE
        System.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.coloradmin.cn/o/1083355.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【CANN训练营】Ascend算子开发入门笔记

基础概念 什么是Ascend C Ascend C是CANN针对算子开发场景推出的编程语言,原生支持C和C标准规范&#xff0c;最大化匹配用户开发习惯&#xff1b;通过多层接口抽象、自动并行计算、孪生调试等关键技术&#xff0c;极大提高算子开发效率&#xff0c;助力AI开发者低成本完成算子…

膝关节检测之1设计目标手势与物体交互的动画

原来只用unity自带的IK&#xff0c;发现背部不能动&#xff0c;且手和手指的移动和旋转试了好像没法通过animation实现&#xff08;加入关键帧并修改最终状态的数值后播放没有变化&#xff0c;确定最终关键帧的数值已经改了的&#xff09;。看资料&#xff0c;发现final IK&…

如何使用前端构建工具(如Webpack、Parcel)?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

C++安装qt软件教程

目录 一、工具 二、安装步骤 1.1next 1.2安装目录 1.3安装环境设置选项 1.4Qt5.14.2 --> MinGW 7.3.0 64-bit 1.5 Qt5.14.2 --> 3D以下全选 1.6下一步 1.7下一步 1.8安装 三、什么是 Qt Qt 是一个跨平台的 C图形用户界面应用程序框架。 它为应用程序开发者提…

软件工程与计算总结(七)需求文档化与验证

目录 一.文档化的原因 二.需求文档基础 1.需求文档的交流对象 2.用例文档 3.软件需求规格说明文档 三.需求文档化要点 1.技术文档协作要点 2.需求书写要点 3.软件需求规格说明文档属性要点 四.评审软件需求规格说明文档 1.需求验证与确认 2.评审需求的注意事项 五…

北京消防展隆重举行,汉威科技消防安全创新方案引关注

10月10日&#xff0c;第二十届中国国际消防设备技术交流展览会在北京市顺义区中国国际展览中心新馆隆重举行。 据悉&#xff0c;该展会由中国消防协会举办&#xff0c;是世界三大消防品牌展会之一&#xff0c;吸引了40多个国家和地区的1000余家单位参展&#xff0c;参展产品超万…

面试官:谈谈 Go 内存分配策略

大家好&#xff0c;我是木川 Go语言内置运行时&#xff08;就是runtime&#xff09;&#xff0c;抛弃了传统的内存分配方式&#xff0c;改为自主管理。这样可以自主地实现更好的内存使用模式&#xff0c;比如内存池、预分配等等。这样&#xff0c;不会每次内存分配都需要进行系…

找不到x3daudio1_7.dll怎么解决?x3daudio1_7.dll的5个修复方法

电脑已经成为我们生活和工作中不可或缺的一部分。然而&#xff0c;在使用电脑的过程中&#xff0c;我们常常会遇到各种问题&#xff0c;其中之一就是“找不到x3daudio1_7.dll&#xff0c;无法运行启动软件或者游戏”。这个问题可能会影响到我们的正常使用&#xff0c;甚至可能导…

2023 年 Arm A-Profile 架构发展

随着人工智能 (AI) 的兴起和安全威胁的加剧,计算需求不断发展,作为世界设备核心的基础计算架构也必须不断发展。这就是为什么我们的工程团队向普遍存在的 Arm 架构添加新功能和技术,然后软件团队确保软件尽可能无缝地适应这些未来的功能和技术。 Arm架构是如何开发的 Arm …

一种高速1553B总线通信接口模块

一种高速1553B总线通信接口模块 引言 -- 在现代航空电子系统中&#xff0c;总线通信技术已成为不可或缺的一部分1553B总线作为军用标准总线&#xff0c;被广泛应用于各种军事和航天领域。本文主要介绍了一种高速1553B总线通信接口模块的设计与实现方法。该模块不仅具有高可…

Unity基础课程之物理引擎5-射线的使用方法总结

在实际游戏开发时&#xff0c;不可避免地要用到各种射线检测。即便是一个不怎么用到物理系统的游戏&#xff0c;也很可能要用到射线检测机制。换句话说&#xff0c;射线检测在现代游戏开发中应用得非常广泛&#xff0c;超越了物理游戏的范围。下面简单举几个例子。 &#xff0…

游戏设计模式专栏(八):Cocos中最常见的设计模式之一

点击上方亿元程序员关注和★星标 引言 大家好&#xff0c;我是亿元程序员&#xff0c;一位有着8年游戏行业经验的主程。 本系列是《和8年游戏主程一起学习设计模式》&#xff0c;让糟糕的代码在潜移默化中升华&#xff0c;欢迎大家关注分享收藏订阅。 组合模式是一种在Cocos…

前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS进阶(三)

思维导图 1.编程思想 1.1 面向过程编程 1.2 面向对象编程 (oop) 2. 构造函数 3. 原型 3.1 原型 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IE…

精品Python的农村振兴平台防贫助农

《[含文档PPT源码等]精品Python的农村振兴平台设计与实现-爬虫》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程等&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;python 使用框架&#xff1a;Django 前端技术&#xff1a;J…

LeetCode【279】完全平方数

题目&#xff1a; 思路&#xff1a; https://www.acwing.com/solution/leetcode/content/114877/ 1、给定数字&#xff0c;是由若干个完全平方数组合而成&#xff0c;求使用的最小的完全平方数的个数&#xff0c;如果这些完全平方数已知&#xff0c;则完全等同于百元百鸡问题…

adobe firefly image2重磅发布

萤火虫图像2&#xff08;Firefly Image2&#xff09;是由adobe的一种新的图像生成模型。它是萤火虫图像的改进版本&#xff0c;具有以下特点&#xff1a; 更逼真的图像&#xff1a;萤火虫图像2使用了更先进的图像生成技术&#xff0c;能够生成更逼真的图像。更丰富的细节&…

codesys【手轮】

一般4线&#xff0c;也有6线 电压&#xff1a;DC5v&#xff0c;12v&#xff0c;24v 脉冲当量&#xff1a;一圈100脉&#xff0c;25脉 计数器不能【-1000】【1000】 因为一循环会多一个计数 要【-1000】【999】或者【-999】【1000】 PLC计数案例&#xff1a; // QQ750273008…

AndroidStudio模拟器,没有Google Play的就有ROOT权限

正确选择版本 测试 D:\>adb shell emulator64_x86_64:/ $ su emulator64_x86_64:/ #

selinux相关学习笔记-简单selinux部分的解决

selinux问题判断&#xff1a; 1 日志查看&#xff1a; logcat -b all 查看所有日志 如果自己程序有类似如下的avc:denied打印&#xff0c;基本上可以认为有selinux问题&#xff0c;这里有avc: denied相关的关键字 I Thread-2: type1400 audit(0.0:53): avc: denied { search }…

1688拍立淘API接口分享

拍立淘接口&#xff0c;顾名思义&#xff0c;就是通过图片搜索到相关商品列表。通过此接口&#xff0c;可以实现图片搜索爆款商品等功能。 接口地址&#xff1a;1688.item_search_img 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&…