Spring AOP(面向切面编程)是Spring框架的一个关键特性之一。它提供了一种在应用程序中实现横切关注点的方法,这些关注点通常会散布在应用程序的多个模块中,并且与核心业务逻辑存在交叉。
AOP通过将关注点从它们所影响的对象中分离出来,使得开发人员能够更好地关注业务逻辑的实现,而不必担心与之交织在一起的横切关注点。在Spring中,这些横切关注点可以包括日志记录、安全性、事务管理、性能监控等等。
Spring AOP的核心概念是切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)和引入(Introduction)。
Spring AOP使用代理模式来实现横切关注点的管理。在运行时,Spring会动态地创建代理对象,将通知织入到目标对象的方法调用中。
通过使用Spring AOP,开发人员可以更好地实现关注点的模块化和重用,从而提高代码的可维护性和可扩展性。
Spring 官网:https://spring.io/
Spring 文档:https://docs.spring.io/spring-framework/reference/
本文档基于 Spring Boot 以及注解使用 Spring AOP 功能。
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在 Spring 管理的 Bean 类上使用
@Aspect
注解就可以定义一个切面。
package com.example.demo.aspects;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 切面
*/
@Aspect
@Component
public class DemoAspect {
}
在切面类的方法使用
@Pointcut
注解来定义切点,然后在通知注解中使用方法签名来指定切点。 切点表达式用来匹配切入的目标类和方法。目标类只能是 Spring 容器管理的类,切面只能切入 Bean 中的方法。
package com.example.demo.aspects;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 切面
*/
@Aspect
@Component
public class DemoAspect {
/**
* 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
*/
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void pointcut() {
}
/**
* 环绕通知
*
* @param pjp 切点
* @return Object
* @throws Throwable 异常
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("before");
Object result = pjp.proceed();
System.out.println("after");
return result;
}
}
HelloController
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zibo
* @date 2023/5/15 12:55
* @slogan 真正的大师永远怀着一颗学徒的心。——易大师
*/
@RestController
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/world")
public String helloWorld() {
System.out.println("hello world");
return "Hello World!";
}
}
访问地址:http://localhost:8080/hello/world
# 响应结果
Hello World!
# 控制台
before
Hello World!
after
AOP 中的通知是基于连接点(Join point)业务逻辑的一种增强,Spring AOP 提供了下面五种通知类型:
AOP 的连接点一般是指目标类的方法,五种通知类型执行的节点如下:
Spring AOP 中一个目标类可以被多个切面切入,多个切面也可以切入一个目标类。
使用 @Order
注解来指定切面的优先级,来控制切面的执行顺序。
在注册切面 Bean 的时候指定 @Order
,如下:
@Order(1)
@Aspect
@Component
public class FirstAspect {
// ......
}
JoinPoint
:JoinPoint是Spring AOP中表示连接点的对象。它提供了访问连接点的信息,如目标对象、方法签名、方法参数等。通过JoinPoint参数,可以获取有关当前正在执行的连接点的信息。ProceedingJoinPoint
:ProceedingJoinPoint是JoinPoint的一个子接口,它只在使用环绕通知时才会使用。它提供了proceed()方法,用于执行连接点方法。ProceedingJoinPoint参数可以用于在环绕通知中控制连接点方法的执行。org.aspectj.lang.JoinPoint.StaticPart
:JoinPoint.StaticPart表示连接点的静态部分。它提供了与JoinPoint相同的信息,但不提供对连接点方法的执行控制。org.aspectj.lang.Signature
:Signature表示连接点方法的签名。它提供了对方法名称、修饰符、返回类型、参数类型等的访问。org.aspectj.lang.ProceedingJoinPoint.StaticPart
:ProceedingJoinPoint.StaticPart是ProceedingJoinPoint的静态部分。它提供了与ProceedingJoinPoint相同的信息,但不提供对连接点方法的执行控制。org.aspectj.lang.JoinPoint.EnclosingStaticPart
:JoinPoint.EnclosingStaticPart表示连接点所在的静态部分。它提供了与JoinPoint相同的信息,但是可以用于获取连接点所在的类或切面的信息。这些参数可以根据需要选择性地在通知方法中使用,以获取关于连接点和方法的相关信息。
package com.example.demo.aspects;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
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 DemoAspect {
/**
* 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
*/
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void pointcut() {
}
/**
* 前置通知
*
* @param jp 切点
*/
@Before("pointcut()")
public void beforeAdvice(JoinPoint jp) {
System.out.println("前置通知");
// 获取方法名称
String methodName = jp.getSignature().getName();
System.out.println("方法名称:" + methodName);
}
}
前置通知
方法名称:helloWorld
hello world # 此句的方法内部打印内容
package com.example.demo.aspects;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 切面
*/
@Aspect
@Component
public class DemoAspect {
/**
* 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
*/
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void pointcut() {
}
/**
* 后置通知
*
* @param jp 切点
* @param result 返回值
*/
@AfterReturning(pointcut = "pointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint jp, Object result) {
System.out.println("后置通知");
// 获取方法名称
String methodName = jp.getSignature().getName();
System.out.println("方法名称:" + methodName);
// 获取返回值
System.out.println("返回值:" + result);
}
}
hello world # 此句的方法内部打印内容
后置通知
方法名称:helloWorld
返回值:Hello World!
package com.example.demo.aspects;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 切面
*/
@Aspect
@Component
public class DemoAspect {
/**
* 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
*/
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void pointcut() {
}
/**
* 环绕通知
*
* @param proceedingJoinPoint 连接点
* @return 连接点方法的返回值
* @throws Throwable 可能抛出的异常
*/
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知 - 前置逻辑");
// 获取方法名称
String methodName = proceedingJoinPoint.getSignature().getName();
System.out.println("方法名称:" + methodName);
// 执行连接点方法
Object result = proceedingJoinPoint.proceed();
System.out.println("环绕通知 - 后置逻辑");
// 可以对返回值进行处理或修改
System.out.println("返回值:" + result);
return result;
}
}
环绕通知 - 前置逻辑
方法名称:helloWorld
hello world # 此句的方法内部打印内容
环绕通知 - 后置逻辑
返回值:Hello World!
package com.example.demo.aspects;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 切面
*/
@Aspect
@Component
public class DemoAspect {
/**
* 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
*/
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void pointcut() {
}
/**
* 最终通知
*
* @param jp 切点
*/
@After("pointcut()")
public void afterAdvice(JoinPoint jp) {
System.out.println("最终通知");
// 获取方法名称
String methodName = jp.getSignature().getName();
System.out.println("方法名称:" + methodName);
}
}
hello world # 此句的方法内部打印内容
最终通知
方法名称:helloWorld
package com.example.demo.aspects;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 切面
*/
@Aspect
@Component
public class DemoAspect {
/**
* 切点:匹配"com.example.demo.controller"包中所有类的所有方法。
*/
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void pointcut() {
}
/**
* 异常通知
*
* @param jp 切点
* @param exception 异常对象
*/
@AfterThrowing(pointcut = "pointcut()", throwing = "exception")
public void afterThrowingAdvice(JoinPoint jp, Exception exception) {
System.out.println("异常通知");
// 获取方法名称
String methodName = jp.getSignature().getName();
System.out.println("方法名称:" + methodName);
// 获取异常信息
System.out.println("异常信息:" + exception.getMessage());
}
}
HelloController
制造异常package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zibo
* @date 2023/5/15 12:55
* @slogan 真正的大师永远怀着一颗学徒的心。——易大师
*/
@RestController
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/world")
public String helloWorld() {
System.out.println("hello world");
int num = 10 / 0;
return "Hello World!";
}
}
hello world # 此句的方法内部打印内容
异常通知
方法名称:helloWorld
异常信息:/ by zero
切入点指示符用来指示切入点表达式目的,在 Spring AOP 中目前只有执行方法这一个连接点,Spring AOP 支持的 AspectJ 切入点指示符,切入点表达式可以使用 &&、||、!来组合切入点表达式,还可以使用类型匹配的通配符来进行匹配。
类型匹配通配符 | 说明 |
---|---|
* | 表示匹配任何数量字符。示例:java.*.String,表示匹配 java 包下的任何"一级子包"下的 String 类型; 如匹配 java.lang.String,但不匹配java.lang.ss.String |
… | 表示任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。示例:java…* ,表示匹配java包及任何子包下的任何类型; 如匹配java.lang.String、java.lang.annotation.Annotation |
+ | 仅能作为后缀放在类型模式后边,匹配指定类型的子类型; |
execution
切点表达式用于定义切点的匹配规则,根据方法的修饰符、返回类型、方法名、参数类型和异常类型等来进行匹配。
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
:方法抛出的异常类型。使用完全限定的类名表示具体的异常类型。可选。匹配 public 方法:
execution(public * *(..))
匹配名称以
set
开头的方法:
execution(* set*(..))
匹配指定类的所有方法:
execution(* com.example.demo.service.UserService.*(..))
匹配指定包及其子包下的类或接口的所有方法:
execution(* com.example.demo.service..*(..))
匹配带有特定注解的方法:
execution(@com.example.demo.annotation.Loggable * *(..))
匹配返回类型为指定类型的方法:
execution(java.util.List<com.example.demo.model.User> com.example.demo.service.UserService.*(..))
这些示例展示了不同的 execution
切点表达式的用法,你可以根据具体的需求和要匹配的方法特征来定义切点表达式。
within
切点表达式用于定义切点的作用范围,根据类型(类或接口)来匹配其中的方法执行。
within(type-pattern)
匹配指定类中的所有方法:
within(com.example.demo.service.UserService)
匹配指定包及其子包下的所有类或接口的方法:
within(com.example.demo.service..*)
匹配指定包中的所有类或接口的方法:
within(com.example.demo.service.*)
匹配指定包及其子包下的所有类的所有方法:
within(com.example.demo..*)
这些示例展示了使用 within
切点表达式的一些常见用法,你可以根据具体的需求和要匹配的类型来定义切点表达式。
需要注意的是,within
切点表达式只能匹配到类型级别,无法直接匹配到具体的方法。
this
切点表达式用于匹配当前代理对象所实现的接口类型,并选择这些接口中定义的方法作为切点。
this(type)
这个示例表示匹配当前代理对象所实现的
com.example.demo.service.UserService
接口中定义的所有方法。
this(com.example.demo.service.UserService)
需要注意的是,this
切点表达式只能匹配到当前代理对象实现的接口方法,并不包括其实现类或其他接口的方法。
target
切点表达式用于匹配目标对象的类型,并选择这些类型中定义的方法作为切点。
target(type)
这个示例表示匹配目标对象的类型为
com.example.demo.service.UserService
,即选择目标对象为该类型的所有方法作为切点。
target(com.example.demo.service.UserService)
需要注意的是,target
切点表达式匹配的是目标对象的类型,而不是当前代理对象的类型。这意味着它会选择目标对象的方法,而不考虑当前代理对象的实现类或其他接口的方法。
args
切点表达式用于匹配方法的参数类型,并选择具有匹配参数类型的方法作为切点。
args(type-pattern)
匹配带有一个整型参数的方法:
args(int)
匹配带有任意参数类型的方法:
args(*)
匹配带有一个字符串参数的方法:
args(java.lang.String)
匹配带有两个整型参数的方法:
args(int, int)
需要注意的是,args
切点表达式仅匹配参数类型,而不考虑参数名称。它只选择具有匹配参数类型的方法,而不限制参数的个数或顺序。
bean
切点表达式用于匹配 Spring 容器中的 Bean 名称,并选择具有匹配名称的 Bean 的方法作为切点。
bean(beanNamePattern)
匹配名称为 “userService” 的 Bean:
bean(userService)
匹配名称以 “service” 结尾的 Bean:
bean(*Service)
匹配名称以 “service” 开头并且包含 “impl” 的 Bean:
bean(service*impl)
需要注意的是,bean
切点表达式匹配的是 Spring 容器中的 Bean 名称,而不是具体的类名或接口名。
@within
切点表达式用于匹配被特定注解标注的类及其子类中定义的方法作为切点。
@within(annotation-type)
@within(org.springframework.stereotype.Service)
这个示例表示匹配被 @Service
注解标注的类及其子类中定义的所有方法作为切点。
package com.example.demo.aspects;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
/**
* 切面
*/
@Aspect
@Component
public class DemoAspect {
/**
* 异常通知
*
* @param jp 切点
*/
@Before("@within(service)")
public void beforeAdvice(JoinPoint jp, Service service) {
System.out.println("前置通知");
// 获取方法名称
String methodName = jp.getSignature().getName();
System.out.println("方法名称:" + methodName);
// 获取注解的值
String value = service.value();
System.out.println("注解的值:" + value);
}
}
@target
切点表达式用于匹配目标对象所属的类上标注的注解类型,并选择这些类中定义的方法作为切点。
@target(annotation-type)
这个示例表示匹配目标对象所属的类上标注了
@Service
注解的所有方法作为切点。
@target(org.springframework.stereotype.Service)
需要注意的是,@target
切点表达式匹配的是目标对象所属的类上的注解,而不是当前代理对象所属的类上的注解。它会选择目标对象所属的类中定义的方法,而不考虑当前代理对象的实现类或其他接口的方法。
@annotation
切点表达式用于匹配被特定注解标注的方法,并选择这些方法作为切点。
@annotation(annotation-type)
这个示例表示匹配被
@RequestMapping
注解标注的方法作为切点。
@annotation(org.springframework.web.bind.annotation.RequestMapping)
需要注意的是,@annotation
切点表达式匹配的是方法上的注解,而不是类级别的注解。它会选择被注解标注的方法,而不包括其他方法或类级别的注解。
@args
切点表达式用于匹配方法参数上具有特定注解的方法,并选择这些方法作为切点。
@args(annotation-type)
这个示例表示匹配方法参数上具有
@PathVariable
注解的方法作为切点。
@args(org.springframework.web.bind.annotation.PathVariable)
需要注意的是,@args
切点表达式匹配的是方法参数上的注解,而不是方法本身或类级别的注解。它会选择具有特定注解的方法,而不包括其他方法或类级别的注解。
切点表达式的组合可以使用逻辑运算符 &&
(与)、||
(或)、!
(非)来组合多个切点表达式。括号可以用于明确定义优先级和逻辑关系。
使用逻辑运算符
&&
(与):
execution(public * com.example.service.*Service.*(..)) && @within(org.springframework.stereotype.Service)
该示例表示匹配包名为 com.example.service
的类中,被 @Service
注解标注的方法。
使用逻辑运算符
||
(或):
execution(public * com.example.controller.*Controller.*(..)) || execution(public * com.example.service.*Service.*(..))
该示例表示匹配包名为 com.example.controller
的类中的方法或者包名为 com.example.service
的类中的方法。
使用逻辑运算符
!
(非):
!execution(public void com.example.controller.AdminController.logout())
该示例表示匹配除了 com.example.controller.AdminController
类中的 logout
方法之外的所有方法。
使用括号进行优先级和逻辑关系的定义:
(execution(public * com.example.service.*Service.*(..)) && @within(org.springframework.stereotype.Service)) || execution(public * com.example.controller.*Controller.*(..))
该示例表示匹配被 @Service
注解标注的 com.example.service
包中的类的方法,以及匹配 com.example.controller
包中的类的方法。
Ordered
接口或使用 @Order
注解来指定切面的执行顺序。ProceedingJoinPoint
对象的 proceed()
方法,以继续执行目标方法。否则,目标方法将被阻止执行。