目录
PCW编译后织入【post-compile weaving】
前言
AOP编程想必大家都耳熟能详,在spring事务中就使用了AOP编程,默认是使用spring proxy,其底层是基于jdk、cglib的动态代理,当然spring事务也可以通过aspectj来实现。这篇文章就会介绍一个AOP编程范式框架AspectJ。
AspectJ 是一种强大的面向切面编程(AOP)框架,它通过在编译时、编译后、类加载时插入横切关注点(如日志记录、事务管理、性能监控等)来实现代码的模块化和解耦。
AspectJ 的主要优势
-
成熟稳定
AspectJ 自 2001 年发展至今,已经是一个非常成熟和稳定的框架。通常在使用时,不需要过多担心插入的字节码正确性相关的问题
-
灵活性
AspectJ 允许在多个位置插入自定义的代码,如方法调用的位置、方法体内部、读写变量的位置、静态代码块内部以及异常处理位置的前后。它还可以直接将原位置的代码替换为自定义的代码。
-
性能优势
AspectJ 在编译时完成切面织入,因此在运行时不需要额外的动态代理机制,这使得其性能通常优于基于动态代理的 AOP 实现(如 Spring AOP)
三种织入方式
AspectJ有三种织入方式,分别是静态织入【compile-time weaving】、编译后织入【post-compile weaving】、类加载时织入【load-time weaving】,这三种方式其实都是在main方法主程序执行前去完成字节码的修改的,下面分别介绍下这三种方式
准备工作
准备一个maven的父子工程,如下
aspectj的pom依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
CTW静态织入【compile-time weaving】
案例演示在aspectj-def模块中
先定义一个自定义注解
package com.tml.aspectj.def.aspect;
import java.lang.annotation.*;
@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TradeLog {
}
定义一个切面
package com.tml.aspectj.def.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class TradeLogAspect {
@Pointcut("@annotation(com.tml.aspectj.def.aspect.TradeLog) && execution(* *(..))")
private void tradeLogPoint() {
}
@Around("tradeLogPoint()")
public Object tradeLogWithAspectJ(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
System.out.println("TradeLogAspect.tradeLogPoint begin");
Object proceed = joinPoint.proceed();
System.out.println("TradeLogAspect.tradeLogPoint end, --costTime :" + (System.currentTimeMillis() - startTime));
return proceed;
}
}
定义一个目标类
package com.tml.aspectj.def.service;
import com.tml.aspectj.def.aspect.TradeLog;
public class DefService {
@TradeLog
public String sayHello(String name) throws InterruptedException {
Thread.sleep(1500);
return "Hello " + name;
}
}
测试类
package com.tml.aspectj.def.service;
public class Main {
public static void main(String[] args) throws InterruptedException {
DefService defService = new DefService();
String s = defService.sayHello("tml");
System.out.println(s);
}
}
直接运行测试类,结果如下
运行结果并没有增强,切面的逻辑没有执行到,为什么呢?
在CTW模式下,编译器是需要采用一种特有的编译器ajc,这里没有使用ajc编译器,当然没有生效。于是,在pom中增加aspectj-maven-plugin,如下
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<complianceLevel>1.8</complianceLevel>
<showWeaveInfo>true</showWeaveInfo>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
执行maven compile后
看下class文件和之前有什么不同
可以看到编译之后的class文件中有ajc的字眼,这里就表示静态织入成功了,运行一下,看看结果
通过运行结果发现,环绕通知已经切入生效,达到预期。
当然,切面的定义和切面的使用,并不一定在同一个子模块中,假设我切面的定义是在aspectj-def模块中,而目标类是在aspectj-user中,这个又需要怎么配置呢?
先在aspectj-user中定义一个新的目标类
package com.tml.aspectj.user.service;
import com.tml.aspectj.def.aspect.TradeLog;
public class UserService {
@TradeLog
public String sayHello(String name) throws InterruptedException {
Thread.sleep(1500);
return "你好 " + name;
}
}
接着在aspectj-user中定义一个新的测试类
package com.tml.aspectj.user.service;
public class Main {
public static void main(String[] args) throws InterruptedException {
UserService userService = new UserService();
String s = userService.sayHello("tml");
System.out.println(s);
}
}
aspectj-user子工程的pom
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<complianceLevel>1.8</complianceLevel>
<showWeaveInfo>true</showWeaveInfo>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
<aspectLibraries>
<aspectLibrary>
<groupId>com.tml</groupId>
<artifactId>aspectj-def</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
和上一个工程不同之处,在于插件中增加了aspectLibraries的配置,maven compile后,看下目标类的class文件结构
和刚刚的class文件结构不一样,多了一个 UserService$AjcClosure1的class文件
运行看一下结果,如下
环绕通知也执行了,这种情况的静态织入可能是最容易出错的,aspectj-maven插件配置稍微不对,切面就不会生效。
PCW编译后织入【post-compile weaving】
编译后织入,值得是需要为另外一个模块或者jar中的class进行增强,比如我需要在aspectj-user中使用一个新的切面来增强DefService
在aspectj-user中定义一个新的切面
package com.tml.aspectj.user.service;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class PrintAspect {
@Pointcut("execution(* com.tml.aspectj.def.service.*.*(..))")
private void print() {
}
@Before("print()")
public void before(JoinPoint joinPoint) {
System.out.println("当前类className"+joinPoint.getTarget().getClass());
}
}
这里的切入点表达式,刚好能命中aspectj-def中的DefService
aspectj-user模块中新增一个测试类
package com.tml.aspectj.user.service;
import com.tml.aspectj.def.service.DefService;
public class Main1 {
public static void main(String[] args) throws InterruptedException {
DefService defService = new DefService();
String s = defService.sayHello("aspectj");
System.out.println(s);
}
}
aspectj-user的pom配置
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<complianceLevel>1.8</complianceLevel>
<showWeaveInfo>true</showWeaveInfo>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
<aspectLibraries>
<aspectLibrary>
<groupId>com.tml</groupId>
<artifactId>aspectj-def</artifactId>
</aspectLibrary>
</aspectLibraries>
<weaveDependencies>
<dependency>
<groupId>com.tml</groupId>
<artifactId>aspectj-def</artifactId>
</dependency>
</weaveDependencies>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
相比跨模块的静态织入,这里多了weaveDependencies的配置
执行maven compile后,看下class文件的结构
这个是aspectj-user模块的class文件结构,可以看到这里把aspectj-def中的class文件也拷贝过来了
运行看下结果
可以看到DefService上的两个切面都执行了,因为环绕通知的优先级高于前置通知,所以执行结果如上 ,执行结果也是符合预期
LTW类加载时织入【load-time weaving】
LTW方式织入,其实就已经不在依赖ajc的编译器了,他采用的方式是javaagent,这种方式可能在实际应用中使用比较少。javaagent执行时机是在main主程序之前,也就是说他会额外增加项目的启动时间。
这里我们在aspectj-def模块中去验证
首先去掉aspectj-maven插件,意思是不再使用ajc来进行编译织入
接着在resource、META-INF目录下,创建一个aop.xml文件,文件的内容如下
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver>
<!-- 只织入特定包下的类 -->
<include within="com.tml.aspectj.def..*"/>
</weaver>
<aspects>
<!-- 织入这个切面 -->
<aspect name="com.tml.aspectj.def.aspect.TradeLogAspect"/>
</aspects>
</aspectj>
编译好aspectj-def,看下class文件,并没有ajc的增强
项目运行前,增加jvm启动参数,如下
-javaagent:C:\org\aspectj\aspectjweaver\1.9.7\aspectjweaver-1.9.7.jar
运行Main结果如下,符合预期,结果是进行了增强
TradeLogAspect.tradeLogPoint begin
TradeLogAspect.tradeLogPoint end, --costTime :1512
Hello tml
总结
通过上面的多个用例,aspectj的三种织入方式都有了一个基本的了解。
其中,跨模块之间的CTW可能是最为重要的,实际工程应用中也是最常见的,需要尤为注意。
最后,相关的测试代码已经上传到github,如果感兴趣可以前往https://github.com/tianmlin19/aspecj-demo