似懂非懂的 AspectJ

news2024/9/25 17:20:21

今天想和小伙伴们聊一下我们在使用 Spring AOP 时,一个非常常见的概念 AspectJ。

1. 关于代理

小伙伴们知道,Java 23 种设计模式中有一种模式叫做代理模式,这种代理我们可以将之称为静态代理,Spring AOP 我们常说是一种动态代理,那么这两种代理的区别在哪里呢?

1.1 静态代理

这种代理在我们日常生活中其实非常常见,例如房屋中介就相当于是一个代理,当房东需要出租房子的时候,需要发布广告、寻找客户、清理房间。。。由于比较麻烦,因此房东可以将租房子这件事情委托给中间代理去做。这就是一个静态代理。

我通过一个简单的代码来演示一下,首先我们有一个租房的接口,如下:

public interface Rent {
    void rent();
}

房东实现了该接口,表示想要出租房屋:

public class Landlord implements Rent{
    @Override
    public void rent() {
        System.out.println("房屋出租");
    }
}

中介作为中间代理,也实现了该接口,同时代理了房东,如下:

public class HouseAgent implements Rent {
    private Landlord landlord;

    public HouseAgent(Landlord landlord) {
        this.landlord = landlord;
    }

    public HouseAgent() {
    }

    @Override
    public void rent() {
        publishAd();
        landlord.rent();
        agencyFee();
    }

    public void publishAd() {
        System.out.println("发布招租广告");
    }

    public void agencyFee() {
        System.out.println("收取中介费");
    }
}

可以看到,中介的 rent 方法中,除了调用房东的 rent 方法之外,还调用了 publishAd 和 agencyFee 两个方法。

接下来客户租房,只需要和代理打交道就可以了,如下:

public class Client {
    public static void main(String[] args) {
        Landlord landlord = new Landlord();
        HouseAgent houseAgent = new HouseAgent(landlord);
        houseAgent.rent();
    }
}

这就是一个简单的代理模式。无论大家是否有接触过 Java 23 种设计模式,上面这段代码应该都很好理解。

这是静态代理。

1.2 动态代理

动态代理讲究在不改变原类原方法的情况下,增强目标方法的功能,例如,大家平时使用的 Spring 事务功能,在不改变目标方法的情况下,就可以通过动态代理为方法添加事务处理能力。再比如松哥在 TienChin 项目中所讲的日志处理、接口幂等性处理、多数据源处理等,都是动态代理能力的体现:

从实现原理上,我们又可以将动态代理划分为两大类:

  • 编译时增强。
  • 运行时增强。

1.2.1 编译时增强

编译时增强,这种有点类似于 Lombok 的感觉,就是在编译阶段就直接生成了代理类,将来运行的时候,就直接运行这个编译生成的代理类,AspectJ 就是这样一种编译时增强的工具。

AspectJ 全称是 Eclipse AspectJ, 其官网地址是: http://www.eclipse.org/aspectj,截止到本文写作时,目前最新版本为:1.9.7。

从官网我们可以看到 AspectJ 的定位:

  1. 基于 Java 语言的面向切面编程语言。
  2. 兼容 Java。
  3. 易学易用。

使用 AspectJ 时需要使用专门的编译器 ajc。

1.2.2 运行时增强

运行时增强则是指借助于 JDK 动态代理或者 CGLIB 动态代理等,在内存中临时生成 AOP 动态代理类,我们在 Spring AOP 中常说的动态代理,一般是指这种运行时增强。

我们平日开发写的 Spring AOP,基本上都是属于这一类。

2. AspectJ 和 Spring AOP

经过前面的介绍,相信大家已经明白了 AspectJ 其实也是 AOP 的一种实现,只不过它是编译时增强。

接下来,松哥再通过三个具体的案例,来和小伙伴们演示编译时增强和运行时增强。

2.1 AspectJ

首先,在 IDEA 中想要运行 AspectJ,需要先安装 AspectJ 插件,就是下面这个:

安装好之后,我们需要在 IDEA 中配置一下,使用 ajc 编译器代替 javac(这个是针对当前项目的设置,所以可以放心修改):

有如下几个需要修改的点:

  1. 首先修改编译器为 ajc。
  2. 将使用的 Java 版本改为 8,这个一共有两个地方需要修改。
  3. 设置 aspectjtools.jar 的位置,这个 jar 包需要自己提前准备好,可以从 Maven 官网下载,然后在这里配置 jar 的路径,配置完成之后,点击 test 按钮进行测试,测试成功就会弹出来图中的弹框。

对于第 3 步所需要的 jar,也可以在项目的 Maven 中添加如下依赖,自动下载,下载到本地仓库之后,再删除掉 pom.xml 中的配置即可:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjtools</artifactId>
    <version>1.9.7.M3</version>
</dependency>

这样,开发环境就准备好了。

接下来,假设我有一个银行转帐的方法:

public class MoneyService {

    public void transferMoney() {
        System.out.println("转账操作");
    }
}

我想给这个方法添加事务,那么我就新建一个 Aspect,如下:

public aspect TxAspect {
    void around():call(void MoneyService.transferMoney()){
        System.out.println("开启事务");
        try {
            proceed();
            System.out.println("提交事务事务");
        } catch (Exception e) {
            System.out.println("回滚事务");
        }
    }
}

这就是 AspectJ 的语法,跟 Java 有点像,但是不太一样。需要注意的是,这个 TxAspect 不是一个 Java 类,它的后缀是 .aj

proceed 表示继续执行目标方法,前后逻辑比较简单,我就不多说了。

最后,我们去运行转账服务:

public class Demo01 {
    public static void main(String[] args) {
        MoneyService moneyService = new MoneyService();
        moneyService.transferMoney();
    }
}

运行结果如下:

这就是一个静态代理。

为什么这么说呢?我们通过 IDEA 来查看一下 TxAspect 编译之后的结果:

@Aspect
public class TxAspect {
    static {
        try {
            ajc$postClinit();
        } catch (Throwable var1) {
            ajc$initFailureCause = var1;
        }

    }

    public TxAspect() {
    }

    @Around(
        value = "call(void MoneyService.transferMoney())",
        argNames = "ajc$aroundClosure"
    )
    public void ajc$around$org_javaboy_demo_p2_TxAspect$1$3b99afea(AroundClosure ajc$aroundClosure) {
        System.out.println("开启事务");

        try {
            ajc$around$org_javaboy_demo_p2_TxAspect$1$3b99afeaproceed(ajc$aroundClosure);
            System.out.println("提交事务事务");
        } catch (Exception var2) {
            System.out.println("回滚事务");
        }

    }

    public static TxAspect aspectOf() {
        if (ajc$perSingletonInstance == null) {
            throw new NoAspectBoundException("org_javaboy_demo_p2_TxAspect", ajc$initFailureCause);
        } else {
            return ajc$perSingletonInstance;
        }
    }

    public static boolean hasAspect() {
        return ajc$perSingletonInstance != null;
    }
}

再看一下编译之后的启动类:

public class Demo01 {
    public Demo01() {
    }

    public static void main(String[] args) {
        MoneyService moneyService = new MoneyService();
        transferMoney_aroundBody1$advice(moneyService, TxAspect.aspectOf(), (AroundClosure)null);
    }
}

可以看到,都是修改后的内容了。

所以说 AspectJ 的作用就有点类似于 Lombok,直接在编译时期将我们的代码改了,这就是编译时增强。

2.2 Spring AOP

Spring AOP 在开发的时候,其实也使用了 AspectJ 中的注解,像我们平时使用的 @Aspect、@Around、@Pointcut 等,都是 AspectJ 里边提供的,但是 Spring AOP 并未借鉴 AspectJ 的编译时增强,Spring AOP 没有使用 AspectJ 的编译器和织入器,Spring AOP 还是使用了运行时增强。

运行时增强可以利用 JDK 动态代理或者 CGLIB 动态代理来实现。我分别来演示。

2.2.1 JDK 动态代理

JDK 动态代理有一个要求,就是被代理的对象需要有接口,没有接口不行,CGLIB 动态代理则无此要求。

假设我现在有一个计算器接口:

public interface ICalculator {
    int add(int a, int b);
}

这个接口有一个实现类:

public class CalculatorImpl implements ICalculator {
    @Override
    public int add(int a, int b) {
        System.out.println(a + "+" + b + "=" + (a + b));
        return a + b;
    }
}

现在,我想通过动态代理实现统计该接口的执行时间功能,JDK 动态代理如下:

public class Demo02 {
    public static void main(String[] args) {

        CalculatorImpl calculator = new CalculatorImpl();
        ICalculator proxyInstance = (ICalculator) Proxy.newProxyInstance(Demo02.class.getClassLoader(), new Class[]{ICalculator.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                long startTime = System.currentTimeMillis();
                Object invoke = method.invoke(calculator, args);
                long endTime = System.currentTimeMillis();
                System.out.println(method.getName() + " 方法执行耗时 " + (endTime - startTime) + " 毫秒");
                return invoke;
            }
        });
        proxyInstance.add(3, 4);
    }
}

不需要任何额外依赖,都是 JDK 自带的能力:

  1. Proxy.newProxyInstance 方法表示要生成一个动态代理对象。
  2. newProxyInstance 方法有三个参数,第一个是一个类加载器,第二个参数是一个被代理的对象所实现的接口,第三个则是具体的代理逻辑。
  3. 在 InvocationHandler 中,有一个 invoke 方法,该方法有三个参数,分别表示当前代理对象,被拦截下来的方法以及方法的参数,我们在该方法中可以统计被拦截方法的执行时间,通过方式执行被拦截下来的目标方法。
  4. 最终,第一步的方法返回了一个代理对象,执行该代理对象,就有代理的效果了。

上面这个案例就是一个 JDK 动态代理。这是一种运行时增强,在编译阶段并未修改我们的代码。

2.2.2 CGLIB 动态代理

从 SpringBoot2 开始,AOP 默认使用的动态代理就是 CGLIB 动态代理了,相比于 JDK 动态代理,CGLIB 动态代理支持代理一个类。

使用 CGLIB 动态代理,需要首先添加依赖,如下:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

假设我有一个计算器,如下:

public class Calculator {
    public int add(int a, int b) {
        System.out.println(a + "+" + b + "=" + (a + b));
        return a + b;
    }
}

大家注意,这个计算器就是一个实现类,没有接口。

现在,我想统计这个计算器方法的执行时间,首先,我添加一个方法执行的拦截器:

public class CalculatorInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = methodProxy.invokeSuper(o, objects);
        long endTime = System.currentTimeMillis();
        System.out.println(method.getName() + " 方法执行耗时 " + (endTime - startTime) + " 毫秒");
        return result;
    }
}

当把代理方法拦截下来之后,额外要做的事情就在 intercept 方法中完成。通过执行 methodProxy.invokeSuper 可以调用到代理方法。

最后,配置 CGLIB,为方法配置增强:

public class Demo03 {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Calculator.class);
        enhancer.setCallback(new CalculatorInterceptor());
        Calculator calculator = (Calculator) enhancer.create();
        calculator.add(4, 5);
    }
}

这里其实就是创建了字节增强器,为生成的代理对象配置 superClass,然后设置拦截下来之后的回调函数就行了,最后通过 create 方法获取到一个代理对象。

这就是 CGLIB 动态代理。

3. 小结

经过上面的介绍,现在大家应该搞明白了静态代理、编译时增强的动态代理和运行时增强的动态代理了吧~

那么我们在项目中到底该如何选择呢?

先来说 AspectJ 的几个优势吧。

  1. Spring AOP 由于要生成动态代理类,因此,对于一些 static 或者 final 修饰的方法,是无法代理的,因为这些方法是无法被重写的,final 修饰的类也无法被继承。但是,AspectJ 由于不需要动态生成代理类,一切都是编译时完成的,因此,这个问题在 AspectJ 中天然的就被解决了。
  2. Spring AOP 有一个局限性,就是只能用到被 Spring 容器管理的 Bean 上,其他的类则无法使用,AspectJ 则无此限制(话说回来,Java 项目 Spring 基本上都是标配了,所以这点其实到也不重要)。
  3. Spring AOP 只能在运行时增强,而 AspectJ 则支持编译时增强,编译后增强以及运行时增强。
  4. Spring AOP 支持方法的增强,然而 AspectJ 支持方法、属性、构造器、静态对象、final 类/方法等的增强。
  5. AspectJ 由于是编译时增强,因此运行效率也要高于 Spring AOP。
  6. 。。。

虽然 AspectJ 有这么多优势,但是 Spring AOP 却有另外一个制胜法宝,那就是简单易用

所以,我们日常开发中,还是 Spring AOP 使用更多。

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

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

相关文章

R语言常用基本函数,含实例:万字总结

目录 vector:创建向量&#xff08;默认填充0&#xff0c;空字符&#xff0c;FALSE&#xff09; data.frame :可以看作由多个向量组成的表格&#xff0c;每个向量代表表格的一列数据 sequence:创建序列(可以简写为seq) dim:获取矩阵或数组的维度信息 length subset&#x…

优思学院|传统流水线生产与单元式生产的差异与优势对比

在制造业领域&#xff0c;生产方式的选择对于生产效率和产品质量至关重要。传统流水线生产和精益生产中的单元式生产是两种常见的生产模式&#xff0c;它们在工作方式、优势以及适用情境上存在着显著的差异。本文将对传统流水线生产和单元式生产进行比较&#xff0c;并探讨它们…

“探索Spring与MyBatis集成的最佳实践与技巧“

目录 引言&#xff1a;1.Spring与MyBatis集成的基本配置2.Spring aop集成pagehelper插件总结附带内容&#xff1a; 引言&#xff1a; 在现代的软件开发中&#xff0c;Spring和MyBatis是两个非常流行的框架。Spring提供了强大的依赖注入和面向切面编程的功能&#xff0c;而MyBa…

炫酷前端页面,鼠标点击特效

最近发现了两个好看的鼠标点击特效&#xff0c;收藏起来用作学习使用&#xff0c;以及更方便的利用 一、鼠标点击弹出爱心 效果图 html源码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><m…

应知道的python基础知识

1、运算符 2、特殊情况下的逻辑运算 3、循环中的else 3.1 while else 3.2 for else 4、列表相关操作 列表的相关操作 4.1增(append, extend, insert) 通过append可以向列表添加元素:列表.append(新元素数据)通过extend可以将另一个列表中的元素逐一添加到列表中:列表.exte…

【推荐】Spring与Mybatis集成

目录 1.概述 2.集成 2.1代码演示&#xff1a; 3.整合 3.1概述 3.2 进行整合分页 接着上两篇&#xff0c;我已经写了Mybatis动态之灵活使用&#xff0c;mybatis的分页和特殊字符的使用方式接下来把它们集成起来&#xff0c;是如何的呢&#x1f447;&#x1f447;&#x1…

深入理解linux内核--系统启动

史前时代&#xff1a;BIOS 计算机在加电的那一刻几乎是毫无用处的&#xff0c;因为RAM芯片中包含的是随机数据&#xff0c;此时还没有操作系统在运行。 在开始启动时&#xff0c;有一个特殊的硬件电路在CPU的一个引脚上产生一个RESET逻辑值。 在RESET产生以后&#xff0c;就把…

pytorch学习(7)——神经网络优化器torch.optim

1 optim 优化器 PyTorch神经网络优化器&#xff08;optimizer&#xff09;通过调整神经网络的参数&#xff08;weight和bias&#xff09;来最小化损失函数&#xff08;Loss&#xff09;。 学习链接&#xff1a; https://pytorch.org/docs/stable/optim.html 1.1 优化器基类 使…

Wlan——无线桥接的基本概念及配置

目录 WDS无线分布式系统 WDS组网拓扑 中继桥接 一对一桥接 一对多桥接 多跳桥接 锐捷无线桥接的配置 根桥的配置(瘦AP是在AC上配置) 非根桥的配置&#xff08;直接在AP上配置&#xff09; WDS无线分布式系统 WDS指AP之间通过无线链路连接两个或多个独立的局域网&…

picGo+gitee+typora设置图床

picGogiteetypora设置图床 picGogitee设置图床下载picGo软件安装picGo软件gitee操作在gitee中创建仓库在gitee中配置私人令牌 配置picGo在插件设置中搜索gitee插件并进行下载 TyporapicGo设置Typora 下载Typora进行图像设置 picGogitee设置图床 当我了解picGogitee可以设置图床…

每日两题 129求根节点到叶节点数字之和 257二叉树的所有路径

129 题目 给你一个二叉树的根节点 root &#xff0c;树中每个节点都存放有一个 0 到 9 之间的数字。 每条从根节点到叶节点的路径都代表一个数字&#xff1a; 例如&#xff0c;从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。 计算从根节点到叶节点生成的 所有…

pytestx容器化执行引擎

系统架构 前端、后端、pytest均以Docker容器运行服务&#xff0c;单独的容器化执行引擎&#xff0c;项目环境隔离&#xff0c;即用即取&#xff0c;用完即齐&#xff0c;简单&#xff0c;高效。 前端容器&#xff1a;页面交互&#xff0c;请求后端&#xff0c;展示HTML报告 后…

2023-8-25食物链

题目链接&#xff1a;食物链 #include <iostream>using namespace std;const int N 50010;int n, m; int p[N], d[N];int find(int x) {if(p[x] ! x){int t find(p[x]);d[x] d[p[x]];p[x] t;}return p[x]; }int main() {cin >> n >> m;for(int i 1; i…

Could not initialize class net.sf.cglib.beans.BeanMap$Generator异常解决

目录 前言 解决方法 复盘分析 前言 使用easyexcel时&#xff0c;研发环境正常, 服务器编译正常, EasyExcel调用方法的时候, 服务器出现Could not initialize class net.sf.cglib.beans.BeanMap$Generator问题。 报错信息 com.alibaba.excel.exception.ExcelAnalysisExc…

​LeetCode解法汇总2236. 判断根结点是否等于子结点之和

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 描述&#xff1a; 给你一个 …

帆软报表系统查看数据库密码

有子曰&#xff1a;“礼之用&#xff0c;和为贵。先王之道&#xff0c;斯为美。小大由之。有所不行&#xff0c;知和而和&#xff0c;不以礼节之&#xff0c;亦不可行也。” 查看数据库密码 构造payload&#xff0c;访问漏洞url&#xff1a; /ReportServer?opfr_server&…

商品搜索网:连接您与各类商品的桥梁

导语&#xff1a;在如今信息爆炸的时代&#xff0c;购物已经不再是传统的实体店购买&#xff0c;而是通过互联网实现的线上购物方式。而要实现高效的线上购物&#xff0c;商品搜索引擎则成为我们的得力助手。作为国内垂直的商品搜索之一&#xff0c;为中国用户提供全面的数码电…

创建abp vnext项目

需求&#xff1a; 1.使用net core跨平台的方式支持windows和centos系统&#xff1b; 2.实现前后端分离部署 3.框架默认集成用户登录、权限、redis等模块 4.支持多种数据库的方式 5.前端使用vue&#xff0c;不需要使用框架自带的web 1.框架配置官网地址&#xff1a; https://ab…

改进YOLO系列:8.添加SimAM注意力机制

添加SimAM注意力机制 1. SimAM注意力机制论文2. SimAM注意力机制原理3. SimAM注意力机制的配置3.1common.py配置3.2yolo.py配置3.3yaml文件配置1. SimAM注意力机制论文 论文题目:SimAM: A Simple, Parameter-Free Attention Module for Convolutional Neural Network…

Kaggle(3):Predict CO2 Emissions in Rwanda

Kaggle&#xff08;3&#xff09;&#xff1a;Predict CO2 Emissions in Rwanda 1. Introduction 在本次竞赛中&#xff0c;我们的任务是预测非洲 497 个不同地点 2022 年的二氧化碳排放量。 在训练数据中&#xff0c;我们有 2019-2021 年的二氧化碳排放量 本笔记本的内容&am…