1. 背景
在现代软件开发中,面向切面编程(AOP
)是一种强大的编程范式,允许开发者跨越应用程序的多个部分定义横切关注点(如日志记录、事务管理等)。本文将介绍如何在Spring
框架中通过AspectJ
注解以及对应的XML
配置来实现AOP
,在不改变主业务逻辑的情况下增强应用程序的功能。
2. 基于 AspectJ 注解来实现 AOP
对于一个使用Maven
的Spring
项目,需要在pom.xml
中添加以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>
复制代码
确保版本号与使用的Spring
版本相匹配,可以自行调整。
创建业务逻辑接口 MyService:
package com.example.demo.aop;
public interface MyService {
void performAction();
}
复制代码
创建业务逻辑类 MyServiceImpl.java:
package com.example.demo.aop;
import org.springframework.stereotype.Service;
@Service
public class MyServiceImpl implements MyService {
@Override
public void performAction() {
System.out.println("Performing an action in MyService");
}
}
复制代码
定义切面
创建切面类MyAspect.java
,并使用注解定义切面和通知:
package com.example.demo.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.example.demo.aop.MyServiceImpl.performAction(..))")
public void logBeforeAction() {
System.out.println("Before performing action");
}
}
复制代码
@Aspect
注解是用来标识一个类作为AspectJ
切面的一种方式,这在基于注解的AOP
配置中是必需的。它相当于XML
配置中定义切面的方式,但使用注解可以更加直观和便捷地在类级别上声明切面,而无需繁琐的XML
配置。
配置 Spring 以启用注解和 AOP
创建一个Java
配置类来代替XML
配置,使用@Configuration
注解标记为配置类,并通过@ComponentScan
注解来启用组件扫描,通过@EnableAspectJAutoProxy
启用AspectJ
自动代理:
package com.example.demo.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}
复制代码
@EnableAspectJAutoProxy
注解在Spring
中用于启用对AspectJ
风格的切面的支持。它告诉Spring
框架去寻找带有@Aspect
注解的类,并将它们注册为Spring
应用上下文中的切面,以便在运行时通过代理方式应用这些切面定义的通知(Advice
)和切点(Pointcuts
)。
如果不写@EnableAspectJAutoProxy
,Spring
将不会自动处理@Aspect
注解定义的切面,则定义的那些前置通知(@Before
)、后置通知(@After
、@AfterReturning
、@AfterThrowing
)和环绕通知(@Around
)将不会被自动应用到目标方法上。这意味着定义的AOP
逻辑不会被执行,失去了AOP
带来的功能增强。
@Before
注解定义了一个前置通知(Advice
),它会在指定方法执行之前运行。切点表达式execution(* com.example.demo.aop.MyServiceImpl.performAction(..))
精确地定义了这些连接点的位置。在这个例子中,切点表达式指定了MyServiceImpl
类中的performAction
方法作为连接点,而@Before
注解标识的方法(logBeforeAction
)将在这个连接点之前执行,即logBeforeAction
方法(前置通知)会在performAction
执行之前被执行。
创建主类和测试 AOP 功能
主程序如下:
package com.example.demo;
import com.example.demo.aop.MyService;
import com.example.demo.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService service = context.getBean(MyService.class);
service.performAction();
}
}
复制代码
运行结果如下:
3. XML 实现和注解实现 AOP 的代码对比
对于上面的代码,我们将原有基于注解的AOP
配置改写为完全基于XML
的形式,方便大家对比。首先需要移除切面类和业务逻辑类上的所有Spring
相关注解,然后在XML
文件中配置相应的bean
和AOP
逻辑。
移除注解首先,我们移除业务逻辑类和切面类上的所有注解。
MyService.java (无变化,接口保持原样):
package com.example.demo.aop;
public interface MyService {
void performAction();
}
复制代码
MyServiceImpl.java (移除 @Service 注解):
package com.example.demo.aop;
public class MyServiceImpl implements MyService {
@Override
public void performAction() {
System.out.println("Performing an action in MyService");
}
}
复制代码
MyAspect.java (移除 @Aspect 和 @Component 注解,同时去掉方法上的 @Before 注解):
package com.example.demo.aop;
public class MyAspect {
public void logBeforeAction() {
System.out.println("Before performing action");
}
}
复制代码
XML 配置
接下来,删除AppConfig
配置类,在Spring
的XML
配置文件中定义beans
和AOP
配置。
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 定义业务逻辑bean -->
<bean id="myService" class="com.example.demo.aop.MyServiceImpl"/>
<!-- 定义切面 -->
<bean id="myAspect" class="com.example.demo.aop.MyAspect"/>
<!-- AOP配置 -->
<aop:config>
<aop:aspect id="aspect" ref="myAspect">
<aop:pointcut id="serviceOperation" expression="execution(* com.example.demo.aop.MyService.performAction(..))"/>
<aop:before pointcut-ref="serviceOperation" method="logBeforeAction"/>
</aop:aspect>
</aop:config>
</beans>
复制代码
在这个XML
配置中,我们手动注册了MyServiceImpl
和MyAspect
作为beans
,并通过<aop:config>
元素定义了AOP
逻辑。我们创建了一个切点serviceOperation
,用于匹配MyService.performAction(..)
方法的执行,并定义了一个前置通知,当匹配的方法被调用时,MyAspect
的logBeforeAction
方法将被执行。
主类和测试 AOP 功能
主类DemoApplication
的代码不需要改变,只是在创建ApplicationContext
时使用XML
配置文件而不是Java
配置类:
package com.example.demo;
import com.example.demo.aop.MyService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class DemoApplication {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService service = context.getBean(MyService.class);
service.performAction();
}
}
复制代码
运行结果是一样的
4. AOP 通知讲解
在Spring AOP
中,通知(Advice
)定义了切面(Aspect
)在目标方法调用过程中的具体行为。Spring AOP
支持五种类型的通知,它们分别是:前置通知(Before
)、后置通知(After
)、返回通知(After Returning
)、异常通知(After Throwing
)和环绕通知(Around
)。通过使用这些通知,开发者可以在目标方法的不同执行点插入自定义的逻辑。
前置通知是在目标方法执行之前执行的通知,通常用于执行一些预处理任务,如日志记录、安全检查等。
@Before("execution(* com.example.demo.aop.MyServiceImpl.performAction(..))")
public void logBeforeAction() {
System.out.println("Before performing action");
}
复制代码
返回通知在目标方法成功执行之后执行,可以访问方法的返回值。
@AfterReturning(pointcut = "execution(* com.example.demo.aop.MyServiceImpl.performAction(..))", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("Method returned value is : " + result);
}
复制代码
这里在@AfterReturning
注解中指定returning = "result"
时,Spring AOP
框架将目标方法的返回值传递给切面方法的名为result
的参数,因此,切面方法需要有一个与之匹配的参数,类型兼容目标方法的返回类型。如果两者不匹配,Spring
在启动时会抛出异常,因为它无法将返回值绑定到切面方法的参数。
异常通知在目标方法抛出异常时执行,允许访问抛出的异常。
@AfterThrowing(pointcut = "execution(* com.example.demo.aop.MyServiceImpl.performAction(..))", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("@AfterThrowing: Exception in method: " + methodName + "; Exception: " + ex.toString());
}
复制代码
在@AfterThrowing
注解的方法中包含JoinPoint
参数是可选的,当想知道哪个连接点(即方法)引发了异常的详细信息时非常有用,假设有多个方法可能抛出相同类型的异常,而我们想在日志中明确指出是哪个方法引发了异常。通过访问JoinPoint
提供的信息,可以记录下引发异常的方法名称和其他上下文信息,从而使得日志更加清晰和有用。
后置通知在目标方法执行之后执行,无论方法执行是否成功,即便发生异常,仍然会执行。它类似于finally
块。
@After("execution(* com.example.demo.aop.MyServiceImpl.performAction(..))")
public void logAfter() {
System.out.println("After performing action");
}
复制代码
环绕通知围绕目标方法执行,可以在方法调用前后执行自定义逻辑,同时决定是否继续执行目标方法。环绕通知提供了最大的灵活性和控制力。
@Around("execution(* com.example.demo.aop.MyServiceImpl.performAction(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method execution");
Object result = joinPoint.proceed(); // 继续执行目标方法
System.out.println("After method execution");
return result;
}
复制代码
接下来,我们来演示一下,全部代码如下:
服务接口(MyService.java):
package com.example.demo.aop;
public interface MyService {
String performAction(String input);
}
复制代码
服务实现(MyServiceImpl.java):
修改performAction
方法,使其在接收到特定输入时抛出异常
package com.example.demo.aop;
import org.springframework.stereotype.Service;
@Service
public class MyServiceImpl implements MyService {
@Override
public String performAction(String input) {
System.out.println("Performing action with: " + input);
if ("error".equals(input)) {
throw new RuntimeException("Simulated error");
}
return "Processed " + input;
}
}
复制代码
完整的切面类(包含所有通知类型)
切面类(MyAspect.java
) - 保持不变,确保包含所有类型的通知:
package com.example.demo.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.example.demo.aop.MyService.performAction(..))")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("@Before: Before calling performAction");
}
@After("execution(* com.example.demo.aop.MyService.performAction(..))")
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("@After: After calling performAction");
}
@AfterReturning(pointcut = "execution(* com.example.demo.aop.MyService.performAction(..))", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("@AfterReturning: Method returned value is : " + result);
}
@AfterThrowing(pointcut = "execution(* com.example.demo.aop.MyService.performAction(..))", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("@AfterThrowing: Exception in method: " + methodName + "; Exception: " + ex.toString());
}
@Around("execution(* com.example.demo.aop.MyService.performAction(..))")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("@Around: Before method execution");
Object result = null;
try {
result = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
// 如果执行方法出现异常,打印这里
System.out.println("@Around: Exception in method execution");
throw throwable;
}
// 如果执行方法正常,打印这里
System.out.println("@Around: After method execution");
return result;
}
}
复制代码
这里要强调几点:
@Around
环绕通知常见用例是异常捕获和重新抛出。在这个例子中,我们通过ProceedingJoinPoint
的proceed()
方法调用目标方法。如果目标方法执行成功,记录执行后的消息并返回结果。如果在执行过程中发生异常,在控制台上打印出异常信息,然后重新抛出这个异常。这样做可以确保异常不会被吞没,而是可以被上层调用者捕获或由其他异常通知处理。
@AfterThrowing
注解标明这个通知只有在目标方法因为异常而终止时才会执行。throwing
属性指定了绑定到通知方法参数上的异常对象的名称。这样当异常发生时,异常对象会被传递到afterThrowingAdvice
方法中,方法中可以对异常进行记录或处理。
@AfterThrowing
和@AfterReturning
通知不会在同一个方法调用中同时执行。这两个通知的触发条件是互斥的。@AfterReturning
通知只有在目标方法成功执行并正常返回后才会被触发,这个通知可以访问方法的返回值。@AfterThrowing
通知只有在目标方法抛出异常时才会被触发,这个通知可以访问抛出的异常对象。
假设想要某个逻辑总是在方法返回时执行,不管是抛出异常还是正常返回,则考虑放在@After
或者@Around
通知里执行。
配置类
package com.example.demo.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}
复制代码
测试不同情况
为了测试所有通知类型的触发,在主类中执行performAction
方法两次:一次传入正常参数,一次传入会导致异常的参数。
主程序如下:
package com.example.demo;
import com.example.demo.aop.MyService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService service = context.getBean(MyService.class);
try {
// 正常情况
System.out.println("Calling performAction with 'test'");
service.performAction("test");
// 异常情况
System.out.println("\nCalling performAction with 'error'");
service.performAction("error");
} catch (Exception e) {
System.out.println("Exception caught in DemoApplication: " + e.getMessage());
}
}
}
复制代码
在这个例子中,当performAction
方法被第二次调用并传入"error"
作为参数时,将会抛出异常,从而触发@AfterThrowing
通知。
运行结果如下:
5. AOP 时序图
这里展示在Spring AOP
框架中一个方法调用的典型处理流程,包括不同类型的通知(Advice
)的执行时机。
客户端调用方法:
环绕通知开始 (@Around
):
前置通知 (@Before
):
执行目标方法:
方法完成:
返回通知或异常通知:
后置通知 (@After
):
环绕通知结束 (@Around
):
返回结果:
欢迎一键三连~
有问题请留言,大家一起探讨学习
----------------------Talk is cheap, show me the code-----------------------
评论