今天想和小伙伴们聊一下我们在使用 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 使用更多。