似懂非懂的 AspectJ
今天想和小伙伴们聊一下我们在使用 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 的定位:
- 基于 Java 语言的面向切面编程语言。
- 兼容 Java。
- 易学易用。
使用 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(这个是针对当前项目的设置,所以可以放心修改):
有如下几个需要修改的点:
- 首先修改编译器为 ajc。
- 将使用的 Java 版本改为 8,这个一共有两个地方需要修改。
- 设置 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 自带的能力:
- Proxy.newProxyInstance 方法表示要生成一个动态代理对象。
- newProxyInstance 方法有三个参数,第一个是一个类加载器,第二个参数是一个被代理的对象所实现的接口,第三个则是具体的代理逻辑。
- 在 InvocationHandler 中,有一个 invoke 方法,该方法有三个参数,分别表示当前代理对象,被拦截下来的方法以及方法的参数,我们在该方法中可以统计被拦截方法的执行时间,通过方式执行被拦截下来的目标方法。
- 最终,第一步的方法返回了一个代理对象,执行该代理对象,就有代理的效果了。
上面这个案例就是一个 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 的几个优势吧。
- Spring AOP 由于要生成动态代理类,因此,对于一些 static 或者 final 修饰的方法,是无法代理的,因为这些方法是无法被重写的,final 修饰的类也无法被继承。但是,AspectJ 由于不需要动态生成代理类,一切都是编译时完成的,因此,这个问题在 AspectJ 中天然的就被解决了。
- Spring AOP 有一个局限性,就是只能用到被 Spring 容器管理的 Bean 上,其他的类则无法使用,AspectJ 则无此限制(话说回来,Java 项目 Spring 基本上都是标配了,所以这点其实到也不重要)。
- Spring AOP 只能在运行时增强,而 AspectJ 则支持编译时增强,编译后增强以及运行时增强。
- Spring AOP 支持方法的增强,然而 AspectJ 支持方法、属性、构造器、静态对象、final 类/方法等的增强。
- AspectJ 由于是编译时增强,因此运行效率也要高于 Spring AOP。
- 。。。
虽然 AspectJ 有这么多优势,但是 Spring AOP 却有另外一个制胜法宝,那就是简单易用!
所以,我们日常开发中,还是 Spring AOP 使用更多。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
浅析 GlusterFS 与 JuiceFS 的架构异同
在进行分布式文件存储解决方案的选型时,GlusterFS 无疑是一个不可忽视的考虑对象。作为一款开源的软件定义分布式存储解决方案,GlusterFS 能够在单个集群中支持高达 PiB 级别的数据存储。自从首次发布以来,已经有超过十年的发展历程。目前,该项目主要由 Red Hat 负责维护,并且在全球范围内拥有庞大的用户群体。本文旨在通过对比分析的方式,介绍 GlusterFS 与 JuiceFS 的区别,为您的团队在技术选型过程中提供一些参考。 系统架构对比 GlusterFS GlusterFS 采用的是全分布式的架构,没有中心化节点。GlusterFS 集群主要由服务端和客户端两大部分组成。其中服务端负责管理和存储数据,通常被称为可信存储池(Trusted Storage Pool)。这个存储池由一系列对等的 Server 节点组成,一般会运行两类进程: glusterd:每个节点一个,负责配置管理和分发等。 glusterfsd:每个 Brick 一个,负责处理数据请求和对接底层文件系统。 每个 Brick 上的所有文件可以看成是 GlusterFS 的一个子集,就文件内容而言,...
- 下一篇
版本升级 | v1.0.13发布,传下去:更好用了
新发行版来啦~ 本次更新主要聚焦兼容性的提升及结果报告格式的增加,另外对部分解析逻辑及使用体验进行了优化。在这里特别鸣谢大佬@Hugo-X在社区仓库提交的PR~ 后续,OpenSCA项目组会继续致力于完善本地能力闭环,覆盖更多场景。 v1.0.13更新内容 本地漏洞库兼容多数据格式 支持SQLite、CSV格式结果报告 可选英文版HTML报告 优化JS解析逻辑 支持跳过解压步骤,分析文件目录 支持指定日志文件位置 更新说明 1. 漏洞库兼容多数据格式 本地漏洞库在支持JSON格式的基础上,新增支持SQL数据库格式。按照项目组提供的漏洞库字段样例( https://opensca.xmirror.cn/docs/v1/cli.html )创建好数据表,并在配置文件中配置即可: 图:通过配置文件配置多种漏洞库数据格式 图:MySQL漏洞库样例 2. 新增多种结果报告格式(@Hugo-X) 2.1支持SQLite、CSV报告输出 检测结果报告输出新增SQLite、CSV两种格式,仅需在检测命令的out参数中指定相应的结果文件后缀名为.sqlite、.csv。 目前,OpenSCA可输出...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路