Spring AOP
Spring AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架的一个重要组成部分,它提供了一种强大的方式来增加横切关注点(cross-cutting concerns)到现有的代码中,而不需要修改代码的本身。在传统的OOP中,这些横切关注点(如日志、安全、事务管理等)通常是通过在多个地方复制和粘贴代码来实现的,这导致了代码的耦合和难以维护。而AOP提供了一种更好的方式来处理这些问题。
核心概念
- Aspect(切面):通常定义为一个Java类,它包含了切入点和通知的定义。
- Join Point(连接点):程序执行过程中的一个特定的点,例如方法执行或异常处理。在Spring AOP中,连接点总是代表方法执行。
- Pointcut(切入点):一个表达式,用于匹配一组连接点。当方法执行时,会检查该方法是否与切入点匹配。
- Advice(通知):在切面的连接点上执行的动作。
切面
在Spring AOP中,切面是一个关键的抽象概念,它代表了一个横切关注点,即将多个对象的某个特定行为(如日志、事务、安全等)抽象成一个独立的模块。
切面可以看作是通知(Advice)和切入点(Pointcut)的组合,其中通知定义了在切入点匹配的方法执行前后要执行的代码,而切入点定义了哪些方法会被拦截。
定义切面
在Spring中,可以使用@Aspect
注解来定义一个切面。
切面类通常是一个普通的Java类,它会被Spring容器管理。
@Aspect
@Component
public class LoggingAspect {
// 切入点和通知的定义
}
切面的织入
切面的织入(Weaving)指的是将切面的通知(Advice)插入到目标对象的执行流程中的过程。
在Spring AOP中,织入是在运行时通过代理对象来完成的。Spring AOP使用JDK动态代理或CGLIB代理来创建代理对象。
织入的方式
Spring AOP支持以下几种织入方式:
- 编译时织入:这种方式在编译时期就将切面的代码织入到目标类中,需要特殊的编译器支持,如AspectJ的ajc编译器。Spring AOP通常不使用这种方式。
- 加载时织入:在类加载到JVM时进行织入,这同样需要特殊的类加载器,如AspectJ的LTW(Load-Time Weaving)。Spring AOP也可以与AspectJ LTW集成,但这不是Spring AOP默认的工作方式。
- 运行时织入:这是Spring AOP默认的工作方式。它通过代理对象来实现织入,可以在运行时动态地添加或删除切面。Spring AOP使用JDK动态代理或CGLIB代理来创建代理对象。
运行时织入的代理对象
Spring AOP根据以下规则来决定使用哪种代理方式:
- 如果目标对象实现了至少一个接口,则使用JDK动态代理。
- 如果目标对象没有实现任何接口,则使用CGLIB代理。
JDK动态代理
JDK动态代理是基于接口的代理方式,它利用反射机制动态创建一个符合某一接口的代理类及其对象。代理类实现了接口的所有方法,并在每个方法中插入通知代码,然后调用目标对象的方法。
CGLIB代理
CGLIB(Code Generation Library)是一个强大的、高性能的代码生成库。它通过继承的方式创建目标对象的子类,并在子类中覆盖非final的方法,插入通知代码,然后调用父类(即目标对象)的方法。
启用自动代理
在Spring中,要启用自动代理,可以在配置文件中使用<aop:aspectj-autoproxy>
标签,或者在Java配置类上使用@EnableAspectJAutoProxy
注解。
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// ...
}
织入的过程
当启用自动代理后,Spring容器中的每个bean在创建时都会被检查是否需要创建代理对象:
- 如果一个bean被标记为
@Aspect
,则它不会被代理,因为它是切面本身。 - 如果一个bean是切点的目标,则Spring AOP会为其创建一个代理对象。
- 代理对象会拦截方法调用,并根据切面的定义来执行通知。
通知
通知是指切面中的具体行为,它定义了在特定的连接点上要执行的操作。
通知是切面的核心,因为它包含了切面要完成的工作以及何时执行这个工作。
Spring AOP支持五种类型的通知,每种通知都在特定的时刻执行。
五种类型的通知
- 前置通知(Before Advice):在目标方法执行之前执行。通过
@Before
注解来声明。
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
// 这里写前置通知的逻辑
}
- 后置通知(After Advice):在目标方法执行之后执行,无论方法是否抛出异常。通过
@After
注解来声明。
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice(JoinPoint joinPoint) {
// 这里写后置通知的逻辑
}
- 返回后通知(After Returning Advice):在目标方法正常返回后执行。通过
@AfterReturning
注解来声明。
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
// 这里写返回后通知的逻辑
}
- 抛出异常后通知(After Throwing Advice):在目标方法抛出异常后执行。通过
@AfterThrowing
注解来声明。
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint, Throwable e) {
// 这里写抛出异常后通知的逻辑
}
- 环绕通知(Around Advice):包围一个连接点(如方法调用),可以在方法调用前后自定义行为。通过
@Around
注解来声明。
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法执行前的逻辑
Object result = joinPoint.proceed(); // 执行目标方法
// 方法执行后的逻辑
return result;
}
通知的参数
通知的方法可以接受以下几种类型的参数
- JoinPoint:
JoinPoint
接口提供了一个方法执行的详细信息,例如方法名称、参数类型、参数值等。你可以在任何通知方法中添加JoinPoint
作为参数,以便获取这些信息。
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
// 可以通过joinPoint获取方法执行的详细信息
System.out.println("Method: " + joinPoint.getSignature().getName());
}
- Returning:
@AfterReturning
注解的通知方法可以接收一个名为returning
的参数,这个参数的值是目标方法的返回值。你可以通过returning
属性指定一个参数名,该参数将接收返回值。
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(Object result) {
// result参数接收目标方法的返回值
System.out.println("Method returned: " + result);
}
- Throwing:
@AfterThrowing
注解的通知方法可以接收一个名为throwing
的参数,这个参数的值是目标方法抛出的异常。你可以通过throwing
属性指定一个参数名,该参数将接收异常。
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "e")
public void afterThrowingAdvice(Throwable e) {
// e参数接收目标方法抛出的异常
System.out.println("Method threw exception: " + e.getMessage());
}
- ProceedingJoinPoint:
ProceedingJoinPoint
是JoinPoint
的子接口,专门用于环绕通知(@Around
)。它提供了一个proceed()
方法,用于继续执行目标方法。在环绕通知中,你必须调用proceed()
方法,以执行目标方法,并可以在调用前后添加自定义逻辑。
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
// 目标方法执行前的逻辑
Object result = pjp.proceed(); // 执行目标方法
// 目标方法执行后的逻辑
return result;
}
注意事项
JoinPoint
和ProceedingJoinPoint
参数必须放在通知方法的参数列表的第一个位置。returning
和throwing
参数是可选的,只在@AfterReturning
和@AfterThrowing
通知中使用。- 在环绕通知中,
proceed()
方法可以调用多次,这会导致目标方法执行多次。 - 如果通知方法不需要任何参数,可以省略不写。
通知的执行顺序
在一个方法执行时,如果有多个通知与之关联,它们的执行顺序如下:
- 前置通知
- 环绕通知的前置部分(
proceed
方法调用之前) - 目标方法执行
- 环绕通知的后置部分(
proceed
方法调用之后) - 返回后通知
- 后置通知
- 如果目标方法抛出异常,则执行抛出异常后通知
切点
切点是一个表达式,用于匹配一组连接点,这些连接点通常是方法的执行。
切点定义了哪些方法将被拦截,以便执行通知。
切点表达式
Spring AOP使用AspectJ的切点表达式语言来定义切点。以下是一些常用的切点表达式元素:
- 执行表达式(
execution
):用于匹配方法执行的连接点。 - within表达式(
within
):限制匹配特定类型内的方法执行。 - this表达式(
this
):限制匹配特定代理对象的执行方法。 - target表达式(
target
):限制匹配特定目标对象的执行方法。 - args表达式(
args
):限制匹配特定参数类型的方法执行。 - @within表达式(
@within
):限制匹配带有特定注解的类内的方法执行。 - @target表达式(
@target
):限制匹配带有特定注解的目标对象的方法执行。 - @args表达式(
@args
):限制匹配参数带有特定注解的方法执行。 - @annotation表达式(
@annotation
):限制匹配带有特定注解的方法执行。
执行表达式用于匹配方法执行的连接点。它是最常用的切入点表达式。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
modifiers-pattern
:方法的访问修饰符,如public
、private
等(可选)。ret-type-pattern
:方法的返回类型,可以使用*
表示任何返回类型。declaring-type-pattern
:方法所在的类型,可以使用*
表示任何类型(可选)。name-pattern
:方法名,可以使用*
表示任何方法名。param-pattern
:方法的参数列表,可以使用..
表示任何数量的参数。throws-pattern
:方法抛出的异常类型,可以使用*
表示任何异常类型(可选)。 示例:
execution(public * *(..)) // 匹配所有公共方法的执行
execution(* set*(..)) // 匹配所有以"set"开头的方法的执行
execution(* com.example.service.*.*(..)) // 匹配com.example.service包下所有方法的执行
切点组合
切点表达式可以通过逻辑运算符进行组合,包括:
- 逻辑与(
&&
):匹配两个切点表达式的交集。 - 逻辑或(
||
):匹配两个切点表达式的并集。 - 逻辑非(
!
):匹配切点表达式的补集。
@Pointcut("execution(* com.example.service.*.*(..)) && within(com.example..*)")
public void serviceLayerPointcut() {}
@Pointcut("execution(* com.example.repository.*.*(..)) && within(com.example..*)")
public void repositoryLayerPointcut() {}
@Pointcut("serviceLayerPointcut() || repositoryLayerPointcut()")
public void serviceOrRepositoryLayerPointcut() {}
在上面的例子中,serviceOrRepositoryLayerPointcut
切点将匹配服务层和仓库层的方法执行。
定义切点
在Spring AOP中,可以使用@Pointcut
注解来定义切点。切点通常定义在一个切面(Aspect)类中,并且可以重用。
查看代码
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void loggingPointcut() {}
@Before("loggingPointcut()")
public void logBefore(JoinPoint joinPoint) {
// 这里是前置通知的逻辑
}
@After("loggingPointcut()")
public void logAfter(JoinPoint joinPoint) {
// 这里是后置通知的逻辑
}
}
在上面的例子中,loggingPointcut
方法是一个空方法,它使用@Pointcut
注解定义了一个切点表达式。这个表达式匹配所有在com.example.service
包下的方法的执行。然后,logBefore
和logAfter
方法使用这个切点表达式来指定它们应该在哪些方法上执行。
连接点
连接点是指程序执行过程中的一个特定的点,例如方法的执行、异常的处理等。
在AOP中,这些点可以被切面拦截,以添加额外的行为,如日志记录、安全检查、事务管理等。
连接点是AOP实现的关键概念,因为它们定义了切面可以在何时何地插入横切关注点。
常见的连接点
- 方法执行:这是最常见的连接点,表示一个方法的执行。在Spring AOP中,你可以通过切入点表达式来选择哪些方法执行时会被拦截。
- 方法调用:表示对另一个方法的调用。这与方法执行不同,因为方法调用连接点发生在调用方法的过程中,而方法执行连接点发生在被调用方法的过程中。
- 字段访问:表示对字段的访问,包括读取和写入操作。Spring AOP通常不处理字段访问连接点,因为它专注于方法级别的拦截。
- 异常处理:表示当一个方法抛出异常时。你可以通过
@AfterThrowing
通知来拦截异常处理连接点。
连接点与切入点的关系
连接点是程序执行中的具体点,而切入点是一个表达式,用于匹配一组连接点。当一个连接点被一个切入点匹配时,它就变成了一个“切入点连接点”(Pointcut Join Point),这意味着切面将会在这个连接点上执行相应的通知。
示例
假设你有一个UserService
类,其中有一个名为saveUser
的方法:
public class UserService {
public void saveUser(User user) {
// 方法体
}
}
当saveUser
方法被调用时,它的执行就是一个连接点。如果你定义了一个切入点表达式,比如execution(* com.example.UserService.saveUser(..))
,它就会匹配saveUser
方法的执行连接点。这意味着每当saveUser
方法执行时,与之关联的通知将会被执行。
使用流程
Spring Boot AOP的使用流程相对简单,它允许你在不修改现有代码的情况下,添加横切关注点(如日志、安全、事务管理等)。
添加依赖
首先,确保项目中包含了Spring Boot的AOP依赖。如果使用的是Maven,可以在pom.xml
文件中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
如果想要使用AspectJ的注解,需要添加AspectJ
的依赖。
定义切面类
创建一个Java类,并使用@Aspect
注解来标记它为一个切面。
查看代码
@Aspect
@Component
public class LoggingAspect {
// 定义切入点表达式
@Pointcut("execution(* com.example.service.*.*(..))")
public void loggingPointcut() {}
// 定义前置通知
@Before("loggingPointcut()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().toShortString());
}
// 定义后置通知
@After("loggingPointcut()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature().toShortString());
}
}
启用自动代理
在Spring Boot的配置类上使用@EnableAspectJAutoProxy
注解来启用自动代理。
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// ...
}
使用切面
现在,你调用UserService
中的saveUser
方法时,LoggingAspect
中的前置和后置通知将会被触发。
@Service
public class UserService {
public void saveUser(User user) {
// 业务逻辑
}
}