原生AspectJ
AspectJ 是为了解决面向切面的编程而诞生的,通过它可以在Java应用程序中织入横切关注点,比如日志、性能分析、安全检查等。AspectJ 定义了一套自己的语法,拥有自己的编译器。
AOP与AspectJ以及SpringAOP的关系
- AOP: Aspect-Oriented Programming 即面向切面的编程,是一种编程模式,或者说是一种编程思想.
- AspectJ:是一个实现了 AOP 的框架,它提供了一种基于 Java 语言的面向切面编程语言扩展。AspectJ 具有强大的 AOP 功能,可以在编译期、类加载期或运行时织入切面,从而使开发人员能够更加灵活地控制切面的应用时机和范围
- Spring AOP 是 Spring 框架中的 AOP 模块,是对Spring 管理的 Bean 进行切面编程的一种实现,并且完全支持 AspectJ 的语法,除此之外,与 AspectJ 没有关系,它是通过动态代理(JDK Proxy 或者 CGLib)来实现的.
它们三者的关系如下图所示:
AspectJ 代码织入的方式
AspectJ 代码织入的方式有三种:
- 编译时织入(Compile-time weaving, CTW): 在编译目标程序时,将切面代码编译到目标代码中,这种方式需要使用特殊的编译器来支持,比如 Aspect 编译器:ajc.
- 类装载时织入(Load-time weaving, LTW): 当目标程序加载到 JVM 时,通过特殊的代理机制动态织入切面代码。这种方式需要使用特殊的代理库来支持,例如 AspectJ Weaver(aspectjweaver.jar). 一般都是通过 javaagent 来实现。
- 运行时织入(Runtime weaving, RTW): 在目标程序运行时,通过特殊的API动态织入切面代码,这种方式需要使用 AspectJ 提供的 API 来实现,例如 AspectJProxyFactory 类.
其中,编译时织入和类装载时织入,需要在编译和运行时使用特殊的参数和配置来启用,而运行时织入可以通过代码动态实现.
一般来说,编译时织入和类装载时织入可以在编译时或打包时完成,从而可以提高目标程序的性能。而运行时织入则可以更灵活地动态地控制切面的织入和撤销,但可能会影响目标程序的性能。
AspectJ 语法
AspectJ 语法分为三种
- 直接使用 AspectJ 语法编写 aspect 类
- 基于 XML 配置文件的语法
- 注解的语法
编写aspect类
这种方式的切面类,通常以 .aj 结尾,并且更加灵活,
例如,以下是一个使用 .aj 文件编写的切面类的示例:
public aspect MyAspect {
pointcut myPointcut(): execution(* com.example.MyClass.myMethod(..));
before(): myPointcut() {
// advice code
}
}
注意,在使用 .aj 文件编写切面时,需要使用特定的编译器(例如 ajc 命令)进行编译,而不能直接使用普通的 Java 编译器(例如 javac 命令)。另外,由于 .aj 文件使用的是 AspectJ 语法而不是标准的 Java 语法,因此需要在编译时指定 AspectJ 运行时库的类路径,例如:
ajc -cp /path/to/aspectjrt.jar MyAspect.aj
这个命令将编译 MyAspect.aj 文件,并将 /path/to/aspectjrt.jar 文件添加到类路径中,以便编译器可以查找 AspectJ 运行时库中的类和方法。
实战
- 安装 ajc 编译器
我们首先需要下载 AspectJ 的编译器 ajc,
可以通过 eclipse 官网进行下载:
https://www.eclipse.org/aspectj/downloads.php
下载完成后,运行下面的命令进行安装:
java -jar aspectj-1.x.x.jar
安装完成后,还需添加环境变量,之后在命令行运行 ajc,check是否安装成功.
- 编写要被织入的Java目标代码
public class UserService {
public int addUser() {
System.out.println("add user");
return 1;
}
}
public class AjcTest {
public static void main(String[] args) {
UserService userService = new UserService();
userService.addUser();
}
}
- 编写切面类
public aspect AuthAspect {
before():execution(* com.kklab.clouddemo.ajc.*.*(..)) {
System.out.println("process auth check");
}
}
- 编译
ajc -cp /path/to/aspectjrt.jar -d *.java *.aj
-d 命令可以让class文件在当前文件夹下生成
生成的UserService.class文件如下, 可以看到在 addUser 方法中已经织入了一行代码,这行代码将会调用我们定义的切面方法.
public class UserService {
public UserService() {
}
public int addUser() {
AuthAspect.aspectOf().ajc$before$com_kklab_clouddemo_ajc_AuthAspect$1$204a3380();
System.out.println("add user");
return 1;
}
}
假如我们将 aspect 类中 execution 改为 call,再运行生成 class 文件会发现,并没有在 UserService.class 中织入代码,而是在调用 UserSerivce 方法的地方织入了代码:
public class AjcTest {
public AjcTest() {
}
public static void main(String[] args) {
UserService userService = new UserService();
AuthAspect.aspectOf().ajc$before$com_kklab_clouddemo_ajc_AuthAspect$1$1dafce7a();
userService.addUser();
}
}
这也充分说明了 execution 和 call 两者的区别:
execution 用在方法执行的位置织入,而 call 是在方法调用的位置织入.
在编写切面时,需要根据具体的需求选择合适的切入点类型。
注解语法
AspectJ 的注解语法是基于 Java 注解的,可以使用 @Aspect 注解声明一个切面类,使用其他注解声明切点和通知等元素。
以下是常用的 AspectJ 注解及其用法:
- @Aspect:用于声明一个切面类。
- @Pointcut:用于声明一个切点
- @Before:用于声明一个前置通知。
- @After:用于声明一个后置通知。
- @Around:用于声明一个环绕通知。
- @AfterReturning:用于声明一个返回通知。
- @AfterThrowing:用于声明一个异常通知。
除了以上注解外,AspectJ 还提供了其他一些注解,例如 @DeclareParents 用于声明一个引入通知,以及 @DeclarePrecedence 用于声明切面类之间的优先级顺序等。
实战
我们创建一个普通的Java类文件,不过这次要在这个类上面加一些 aspect 的注解:
@Aspect
public class LogAspect {
@Before("execution(* com.kklab.clouddemo.ajc.*.*(..))")
public void logBefore() {
System.out.println("Before executing add method.");
}
}
然后再使用 ajc 编译一下,由于注解是在jdk 1.5 之后才出现的,所以这次我们需要指定一下 ajc 的 jdk 兼容模式:
ajc -cp /path/to/aspectjrt.jar -d *.java *.aj -1.8
编译之后,查看 class 文件,发现同样对 UserService.class 进行了织入:
public class UserService {
public UserService() {
}
public int addUser() {
AuthAspect.aspectOf().ajc$before$com_kklab_clouddemo_ajc_AuthAspect$1$204a3380();
LogAspect.aspectOf().logBeforeAdd();
System.out.println("add user");
return 1;
}
}
AspectJ 表达式语法
AspectJ 表达式语法是用来声明切入点的,可以指定匹配的目标方法或者构造方法。
AspectJ 表达式语法非常的灵活,可以根据需要使用各种模式匹配规则来定位目标 JoinPoint,
下面是 AspectJ 表达式语法的一些常用模式:
方法调用模式
call([可见性模式] [返回类型模式] 包名模式.类名模式.方法名模式([参数模式]) [throws模式])
示例:
// 匹配可见性为 public,返回类型为 void,包名为 com.example,类名以 Service 结尾,方法名以 save 开头,且有一个参数的方法。
call(public void com.example.*Service.save(*))
// 匹配所有可见性,返回类型为 int,包名为 com.example,类名以 Dao 结尾,方法名为 get 开头,且不接受参数的方法。
call(int com.example.Dao.get())
// 匹配可见性为 private 或 protected,返回类型为任意类型,包名为 com.example,类名以 *Dao 结尾,方法名为 *,且任意参数的方法。
call(private|protected * com.example.*Dao.*(*))
方法执行模式
execution([可见性模式] [返回类型模式] 包名模式.类名模式.方法名模式([参数模式]) [throws模式])
示例:
// 匹配可见性为 public,返回类型为 void,包名为 com.example,类名以 Service 结尾,方法名以 save 开头,且有一个参数的方法。
execution(public void com.example.*Service.save(*))
// 匹配所有可见性,返回类型为 int,包名为 com.example,类名以 Dao 结尾,方法名为 get 开头,且不接受参数的方法。
execution(int com.example.Dao.get())
// 匹配可见性为 private 或 protected,返回类型为任意类型,包名为 com.example,类名以 *Dao 结尾,方法名为 *,且任意参数的方法。
execution(private|protected * com.example.*Dao.*(*))
Maven 自动化构建
codehaus 提供了一个 ajc 的编译插件 aspectj-maven-plugin,只要在 maven 配置文件中添加这个插件,
就可以使用 maven 来编译 AspectJ 了.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.10</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<complianceLevel>1.8</complianceLevel>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。