简介
之前我们介绍了字节码增强的一些实现,如果想在业务代码中处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等时,开发人员需要深入研究字节码相关技术,编写业务代码时编写大量字节码重复相关逻辑,后续更不好维护。
面向切面的编程(Aspect-Oriented Programming,AOP) 是一种编程范式,旨在通过允许横切关注点的分离,提高模块化。AOP要实现的是在我们写的代码的基础上进行一定的包装,如在方法执行前、或执行后、或是在执行中出现异常后这些地方进行拦截处理或叫做增强处理。 AOP允许开发者将横切关注点(跨越多个模块的共同逻辑)与主要业务逻辑分离,以便更有效地管理和重用切面逻辑。AOP最早是AOP联盟的组织提出的,指定的一套规范。在AOP中,两个主要的实现框架是AspectJ和Spring AOP。本文将介绍AOP的概念,深入探讨AspectJ和Spring AOP,以及通过实例演示这两者的应用。
Aop的概念
- 连接点(Jointpoint) :表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点,在AOP中表示为在哪里干;
- 切入点(Pointcut) : 选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为在哪里干的集合;
- 通知(Advice) :在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知;在AOP中表示为干什么;
- 方面/切面(Aspect) :横切关注点的模块化,比如上边提到的日志组件。可以认为是通知、引入和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为在哪干和干什么集合;
- 引入(inter-type declaration) :也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象), 在AOP中表示为干什么(引入什么) ;
- 目标对象(Target Object) :需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为对谁干;
- 织入(Weaving) :把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。在AOP中表示为怎么实现的;
- AOP代理(AOP Proxy) :AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。在AOP中表示为怎么实现的一种典型方式;
我们把这些术语串联到一起,方便理解
AspectJ
AspectJ全称是Eclipse AspectJ, 其官网地址是:www.eclipse.org/aspectj/,目前最新版本为:1.9.7
AspectJ是一个功能强大的切面编程框架,支持在编译时和运行时织入切面逻辑。它通过Java语言的扩展来支持切面的声明式定义,引入了新的关键字和语法元素。AspectJ支持多种通知类型,包括前置、后置、环绕、返回和异常通知,以及静态织入和动态织入。
引用官网描述:
a seamless aspect-oriented extension to the Javatm programming language(一种基于Java平台的面向切面编程的语言) Java platform compatible(兼容Java平台,可以无缝扩展) easy to learn and use(易学易用)
AspectJ可以单独使用,也可以整合到其它框架中。单独使用AspectJ时需要使用专门的编译器ajc。
java的编译器是javac,AspectJ的编译器是ajc,aj是首字母缩写,c即compiler。
了解AspectJ应用到java代码的过程(这个过程称为织入),对于织入这个概念,可以简单理解为aspect(切面)应用到目标函数(类)的过程。
对于这个过程,一般分为动态织入和静态织入:
- 动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术
- ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
aspectj例子
使用AspectJ有两种方法:
- 使用AspectJ语言,这种语言和Java几乎一样,也能在AspectJ中调用Java的任何类库,AspectJ只是多了一些关键词
- 使用Java语言开发,然后使用@AspectJ注解
安装
- 下载AspectJ:AspectJ
- 运行:java -jar /Users/user/download/aspectj-1.9.7.jar
- 将bin目录添加到环境变量,将lib目录下的jar添加到CLASSPATH 执行 vim ~/.bash_profile 将aspectj的安装目录配置到环境变量中 shell复制代码ASPECTJ_HOME=/Users/xxx/aspectj1.9 export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$ASPECTJ_HOME/bin:$PATH: 环境变量生效source ~/.bash_profile
切面打印日志的例子
下面我们使用@AspectJ注解写个在方法执行前后打印日志的例子。
- 假设我们有个ProductService接口,我们想在方法出入口增加日志打印
java复制代码public class ProductService {
public void saveProduct(String productName) {
System.out.println("Saving product: " + productName);
}
public String getProduct(String productId) {
System.out.println("Getting product: " + productId);
return "Product Details";
}
}
xml复制代码<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.8</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>1.8</source>
<target>1.8</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
- 首先引入pom
-
pom复制代码<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.7</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency>
- 编写切面处理LoggingAspect
-
java复制代码@Aspect public class LoggingAspect { @Pointcut("execution(public * com.aspectj.service.*.*(..))") protected void logMethods() { } // 前置通知,在方法执行前记录日志 @Before("logMethods()") public void before(JoinPoint pjp) throws Throwable { System.out.println("Entering method: " + pjp.getSignature().getName()); } @After("logMethods()") public void after(JoinPoint pjp) throws Throwable { System.out.println("Exiting method: " + pjp.getSignature().getName()); } }
- 编译
- 如果你是采用 maven 进行管理,可以在 <build> 中加入以下的插件:
- 运行
这是最简单的使用方式,在编译期的时候进行织入,这样编译出来的 .class 文件已经织入了我们的代码,在 JVM 运行的时候其实就是加载了一个普通的被织入了代码的类。
Spring AOP
Spring的一个关键组件是AOP框架。 虽然Spring IOC容器不依赖于AOP(意味着你不需要在IOC中依赖AOP),但AOP为Spring IOC提供了非常强大的中间件解决方案。
Spring AOP 中通知的分类
- 前置通知(Before Advice): 在目标方法被调用前调用通知功能;相关的类org.springframework.aop.MethodBeforeAdvice
- 后置通知(After Advice): 在目标方法被调用之后调用通知功能;相关的类org.springframework.aop.AfterReturningAdvice
- 返回通知(After-returning): 在目标方法成功执行之后调用通知功能;
- 异常通知(After-throwing): 在目标方法抛出异常之后调用通知功能;相关的类org.springframework.aop.ThrowsAdvice
- 环绕通知(Around): 把整个目标方法包裹起来,在被调用前和调用之后分别调用通知功能相关的类org.aopalliance.intercept.MethodInterceptor
Spring AOP 中织入的三种时期
- 编译期: 切面在目标类编译时被织入,这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
- 类加载期: 切面在目标类加载到 JVM 时被织入,这种方式需要特殊的类加载器( ClassLoader ),它可以在目标类引入应用之前增强目标类的字节码。
- 运行期: 切面在应用运行的某个时期被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP 采用的就是这种织入方式。 代理模式可以参考:搞懂Java三种代理模式:静态代理、动态代理和cglib代理
Spring AOP 是基于动态代理来实现,默认如果使用接口的,用JDK提供的动态代理实现,如果是方法则使用CGLIB实现。
spring aop例子
java复制代码import org.springframework.stereotype.Service;
@Service
public class ProductService {
public void saveProduct(String productName) {
System.out.println("Saving product: " + productName);
}
public String getProduct(String productId) {
System.out.println("Getting product: " + productId);
return "Product Details";
}
}
java复制代码@Component
public class AppRunner implements CommandLineRunner {
@Autowired
private ProductService productService;
@Override
public void run(String... args) {
productService.saveProduct("Sample Product");
productService.getProduct("123");
}
}
- pom引入依赖
-
xml复制代码<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 创建一个切面类,使用注解定义通知:
-
java复制代码import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Pointcut("execution(* com.demo.service.*.*(..))") private void logMethods() {} @Before("logMethods()") public void beforeAdvice() { System.out.println("Entering method..."); } @After("logMethods()") public void afterAdvice() { System.out.println("Exiting method..."); } }
- 在上述代码中,使用 @Aspect 注解标记该类为切面类,使用 @Pointcut 定义切入点,然后使用 @Before 和 @After 注解分别定义前置通知和后置通知。
- 创建一个业务类,供切面拦截:
- 启动类增加@EnableAspectJAutoProxy
- 编写一个测试类来验证效果:
- 运行应用程序,你将在控制台看到类似以下的输出:
这个示例展示了如何使用 Spring AOP 和注解在方法调用前后添加日志记录的功能。通过定义切面、切入点和通知,你可以轻松地实现横切关注点的逻辑。
总结
AspectJ与Spring AOP对比
这个表格总结了AspectJ和Spring AOP在不同方面的区别。
特性/方面 | AspectJ | Spring AOP |
实现方式 | 独立的切面编程框架,扩展了Java语法 | Spring框架的一部分,基于动态代理和字节码增强技术 |
功能复杂性 | 提供更高级的切面编程能力,支持复杂的切面逻辑 | 更专注于常见的切面功能,适用于简单的切面逻辑 |
依赖性 | 独立使用,需要AspectJ编译器或插件 | 与Spring框架集成,依赖于Spring框架 |
织入方式 | 支持编译时织入和运行时织入 | 主要使用运行时织入,依赖于动态代理和字节码增强技术 |
使用场景 | 适用于复杂切面逻辑的场景,高要求 | 适用于通用切面需求,轻量级解决方案 |
依赖性注入 | 主要关注切面编程,不直接支持依赖性注入 | 可与Spring的依赖性注入机制集成,支持切面中使用其他Spring功能 |
性能 | 编译期织入,性能较好 | 基于动态代理来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,性能不如AspectJ |
为什么有字节码增强技术还需要aop
虽然字节码增强技术本身已经提供了一种在代码层面进行修改和增强的能力,但是在某些情况下,使用AOP(Aspect-Oriented Programming)仍然是有价值的。以下是一些理由:
- 抽象级别: AOP提供了更高级别的抽象,允许开发者更直观地声明横切关注点,而无需深入研究字节码。这使得代码更具可读性和可维护性,同时也减少了开发者需要了解底层技术细节的负担。
- 可维护性: AOP可以将横切关注点的逻辑与主要业务逻辑分开,使代码更易于维护。使用AOP,开发者可以将跨越多个模块的共同逻辑集中管理,而不会将这些逻辑分散在各个方法中。
- 可重用性: AOP可以将切面逻辑抽象为可重用的模块,可以在多个应用程序中共享和重用。这使得相同的横切关注点可以在不同的场景中轻松应用,而不需要重新编写和修改字节码。
- 解耦和聚焦: AOP使得开发者可以将不同的关注点隔离开来,从而实现更好的模块化。它遵循“单一职责原则”,使得代码更加聚焦,每个模块只需要关注自己的主要逻辑,而不必关心横切关注点。
- 可配置性: AOP允许通过配置文件或注解来管理切面逻辑的应用范围。这使得开发者可以在不改变源代码的情况下,通过配置来控制切面的应用,从而实现更灵活的控制。
- 跨越多种技术栈: AOP不仅限于Java平台,还可以在其他编程语言和技术栈中使用。这使得可以将相同的横切关注点逻辑应用于不同的技术栈和平台。
虽然字节码增强技术在某些情况下可以实现类似的功能,但AOP提供了更高层次的抽象和更好的可维护性,使开发者能够更轻松地管理和重用横切关注点的逻辑。在实际开发中,使用AOP和字节码增强技术的组合可以根据具体情况来平衡开发效率和维护性。