本文主要介绍 AspectJ 编译时织入(Compile Time Weaving, CTW)的技术,并附有详细示例代码。
AspectJ 是一个 AOP 的具体实现框架。AOP(Aspect Oriented Programming)即面向切面编程,可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
AspectJ不但可以通过预编译方式(CTW)和运行期动态代理的方式织入切面,还可以在载入(Load Time Weaving, LTW)时织入。
AspectJ 扩展了Java,定义了一些专门的AOP语法。官网上这样描述:
a seamless aspect-oriented extension to the Java programming language Java platform compatible easy to learn and use在网上,关于 Spring + AspectJ Annotation动态代理或者AspectJ 载入时织入(Load Time Weaving, LTW)的文章特别多(其特点是:前者需要用Java编写切面并加以注释,后者需要编写 aop.xml 文件并在启动 Java 时带上参数-javaagent),所以本文就不在涉及。
编译时织入是 AspectJ 的一个重要功能,因为 AspectJ 有一个专门的编译器用来生成遵守 Java 字节编码规范的 Class 文件。
要完成代码通过 AspectJ 编译时织入,通常需要两步:
mvn archetype:generate在出来的列表中选择 maven-archetype-quickstart 即可。
然后,修改 pom.xml,增加 aspectj 相关内容,如下
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.opoo.samples</groupId> <artifactId>aspectj-sample</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>aspectj-sample</name> <url>http://opoo.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.4</version> <configuration> <compilerVersion>1.5</compilerVersion> <fork>true</fork> <source>1.5</source> <target>1.5</target> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.4</version> <configuration> <verbose>true</verbose> <privateScope>true</privateScope> <showWeaveInfo>true</showWeaveInfo> <source>1.5</source> <target>1.5</target> <complianceLevel>1.5</complianceLevel> <!-- <encoding>UTF-8</encoding> --> <verbose>false</verbose> <outxml>true</outxml> <aspectLibraries> <!-- 此处定义外部的aspect包,例如spring的事务aspect包 。这里引用的包必须在依赖中声明 --> <!-- <aspectLibrary> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </aspectLibrary> --> </aspectLibraries> </configuration> <executions> <execution> <goals> <goal>compile</goal><!-- use this goal to weave all your main classes --> <!-- <goal>test-compile</goal> --> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-eclipse-plugin</artifactId> <version>2.6</version> <configuration> <ajdtVersion>1.6.11</ajdtVersion> </configuration> </plugin> </plugins> </build> </project>通过命令创建Eclipse工程
mvn eclipse:eclipse将项目 import 到 Eclipse。
创建一个 Service 接口(SampleService.java),如下:
public interface SampleService { int add(int x, int y); String getPassword(String username); }其实现类 (SampleServiceImpl.java) 如下:
public class SampleServiceImpl implements SampleService{ public int add(int x, int y) { return x + y; } @AuthCheck public String getPassword(String username) { return "password"; } }下面我们定义切面(Aspect) (SampleAspect.aj 注意后缀是aj)
import org.aspectj.lang.reflect.MethodSignature; import java.lang.reflect.Method; public aspect SampleAspect{ /** * 切入点:SampleService继承树中所有 public 且以 add 开头的方法。SampleServiceImpl#add(int,int)方法满足这个条件。 */ public pointcut serviceAddMethods(): execution(public * org.opoo.samples.aspectj.SampleService+.add*(..)); Object around(): serviceAddMethods(){ Object oldValue = proceed(); System.out.println("原值是:" + oldValue); return Integer.MIN_VALUE; } /** * 切入点:SampleService继承树中所有标注了AuthCheck的方法。 */ public pointcut serviceAuthCheckAnnotatedMethods(): execution(* org.opoo.samples.aspectj.SampleService+.*(..)) && @annotation(AuthCheck); before(): serviceAuthCheckAnnotatedMethods(){ if(1==1){//权限检查代码 throw new IllegalStateException("权限不足"); } } /** * 切入点:SampleService继承树中所有 public 的方法。 */ public pointcut serviceAllPublicMethods(): execution(public * org.opoo.samples.aspectj.SampleService+.*(..)); after(): serviceAllPublicMethods(){ MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature(); Method method = methodSignature.getMethod(); System.out.println("[LOG] 方法被调用了: " + method); } }第一个切入点是 SampleService 继承树中所有 public 且以 add 开头的方法。SampleServiceImpl#add(int,int) 方法满足这个条件。织入的代码是环绕织入,改变了被切入方法的返回值,不管输入参数如何都返回一个新值。
第二个切入点是 SampleService 继承树中所有标注了 AuthCheck 的方法,采用前置织入,模拟权限检查功能,本例中始终无权限,被织入方法调用时应该发生异常。
第三个切入点是 SampleService 继承树中所有 public 的方法。采用后置织入,模拟日志功能,记录方法调用情况。
然后通过 mvn 编译
mvn test-compile编译后查看 target\classes 目录中的字节码文件,可以发现,SampleAspect.aj 也被编译成了 java 字节码。在编译过程中的输出信息可以看出切点织入情况:
[INFO] Join point 'method-execution(int org.opoo.samples.aspectj.SampleServiceImpl.add(int, int))' in Type 'org.opoo.samples.aspectj.SampleServiceImpl' (SampleServiceImpl.java:23) advised by around advice from 'org.opoo.samples.aspectj.SampleAspect' (SampleAspect.aj:30) [INFO] Join point 'method-execution(int org.opoo.samples.aspectj.SampleServiceImpl.add(int, int))' in Type 'org.opoo.samples.aspectj.SampleServiceImpl' (SampleServiceImpl.java:23) advised by after advice from 'org.opoo.samples.aspectj.SampleAspect' (SampleAspect.aj:41) [INFO] Join point 'method-execution(java.lang.String org.opoo.samples.aspectj.SampleServiceImpl.getPassword(java.lang.String))' in Type 'org.opoo.samples.aspectj.SampleServiceImpl' (SampleServiceImpl.java:31) advised by after advice from 'org.opoo.samples.aspectj.SampleAspect' (SampleAspect.aj:41) [INFO] Join point 'method-execution(java.lang.String org.opoo.samples.aspectj.SampleServiceImpl.getPassword(java.lang.String))' in Type 'org.opoo.samples.aspectj.SampleServiceImpl' (SampleServiceImpl.java:31) advised by before advice from 'org.opoo.samples.aspectj.SampleAspect' (SampleAspect.aj:54)注意:虽然 Eclipse 安装了 AJDT 插件,但有时候可能编译的字节码中并没有织入切面,所以建议还是执行 maven 命令进行编译。
编译完成后就可以在 Eclipse 中执行单元测试检验成果了。
为了有个更直观的印象,我们可以反编译类 SampleServiceImpl 来看看字节码中的切面织入情况。这里我们使用 jad 作为反编译工具,命令如下(在项目根目录即 pom.xml 文件所在目录执行):
d:\path-to-jad\jad.exe -sjava -o -r -ff -d target\src target\classes\**\*.class查看 target\src 目录反编译出的 SampleServiceImpl.java 文件,部分代码如下:
public class SampleServiceImpl implements SampleService { public int add(int x, int y) { int i = x; int j = y; int k; try { k = Conversions.intValue(add_aroundBody1${'$'}advice(this, i, j, SampleAspect.aspectOf(), null)); } catch(Throwable throwable) { SampleAspect.aspectOf().ajc${'$'}after${'$'}org_opoo_samples_aspectj_SampleAspect${'$'}3${'$'}8192077e(ajc${'$'}tjp_0); throw throwable; } SampleAspect.aspectOf().ajc${'$'}after${'$'}org_opoo_samples_aspectj_SampleAspect${'$'}3${'$'}8192077e(ajc${'$'}tjp_0); return k; } //...
可以看出,字节码中已经织入了我们所定义的切面。
编译时织入总结
优点:
缺点:
Source |
|