Spring框架是我们使用比较多的一个框架,而AOP又是Spring的核心特性之一,本篇文章将介绍一下AOP的切点表达式、通知等特性及如何使用Spring AOP。
AOP(Aspect-Oriented Programming,面向切面编程) 是一种编程范式,旨在将横切关注点与核心业务逻辑相分离,以提高代码的模块化性、可维护性和复用性。
在传统的面向对象编程中,程序的功能被模块化为类和方法,但某些功能可能会跨越多个类和方法,如日志记录、事务管理、安全控制等,这些功能不属于核心业务逻辑,但又必须在多个地方重复使用,导致代码重复和耦合性增加。
AOP提供了一种机制,可以将这些横切关注点单独定义,并在需要的地方插入到应用程序中,而不必修改核心业务逻辑。
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP(面向切面编程) 语法,并拥有一个专门的编译器,用于生成遵守Java字节编码规范的Class文件。
AspectJ可以单独使用,也可以整合到其他框架中。当单独使用AspectJ时,需要使用专门的编译器ajc。AspectJ属于静态织入,通过修改代码来实现,包括编译期织入等多种织入时机。
Spring集成AspectJ,可以在Spring中方便的使用AOP。
Spring AOP核心概念主要包括以下几个方面:
这些核心概念共同构成了AOP的基础,使得我们能够模块化地处理横切关注点,从而提高代码的可维护性和可重用性。
Pointcut 表达式 是用来定义切入点的规则,它决定了哪些连接点(方法调用或方法执行)将会被通知所影响。在 Spring AOP 中,Pointcut 表达式通常由以下几种规则和通配符组成:
@Pointcut("execution(* com.example.myapp.service.*.*(..))")
表示匹配com.example.myapp.service
包下所有类的所有方法执行。@Pointcut("within(com.example.myapp.service.*)")
表示表示匹配com.example.myapp.service
包下所有类的所有方法的执行。@Pointcut("this(com.example.myapp.service.MyService)")
表示匹配当前代理对象类型为com.example.myapp.service.MyService
的所有方法的执行。this()
不同,target()
是基于目标对象类型,而不是代理类型。示例:@Pointcut("target(com.example.myapp.service.MyServiceImpl)")
表示匹配目标对象类型为com.example.myapp.service.MyServiceImpl
的所有方法的执行。@Pointcut("args(java.io.Serializable)")
表示匹配方法执行时至少有一个参数是java.io.Serializable
类型的连接点。@Pointcut("@annotation(com.example.myapp.annotation.MyAnnotation)")
表示匹配执行的方法上带有com.example.myapp.annotation.MyAnnotation
注解的连接点。@Pointcut("@target(com.example.annotation.MyAnnotation)")
表示匹配目标对象类型上带有com.example.myapp.annotation.MyAnnotation
注解的方法执行。within()
类似,但它是基于注解而不是包或类。示例: @Pointcut("@within(com.example.myapp.annotation.MyAnnotation)")
表示匹配带有MyAnnotation
注解的类的方法执行。@Pointcut("bean(myServiceImpl)")
表示匹配Spring容器中名称为myServiceImpl
bean的方法的执行。带有 @ 符的切点表达式都是需要指定注解的连接点。
这些规则可以通过逻辑运算符(如 &&、||、! )进行组合,以实现更复杂的 Pointcut 匹配规则。我们可以根据自己的需求,灵活地使用这些规则来定义切入点表达式,实现对目标方法的精确匹配和监控。
execution()
表达式使用的比较多,最复杂的一个表达式,这里重点介绍一下。
execution()
表达式的语法结构如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
其中,各部分的含义如下:
modifiers-pattern
: 方法的访问修饰符,如 public
、protected
等,可以省略。ret-type-pattern
: 方法的返回类型,如 void
、int
等。declaring-type-pattern
: 方法所属的类的类型模式,可以使用通配符 *
匹配任意字符。name-pattern
: 方法的名称模式,可以使用通配符 *
匹配任意字符。param-pattern
: 方法的参数模式,包括参数类型和个数。throws-pattern
: 方法抛出的异常类型。execution(public * *(..))
execution(* set*(..))
execution(* com.xyz.service.AccountService.*(..))
execution(* com.xyz.service.*.*(..))
execution(* com.xyz.service..*.*(..))
execution(* com.example.service.MyService.myMethod(String, int))
execution()
表达式中,通配符 *
可以用来匹配任意字符或任意个数的字符。execution()
表达式时,需要注意合理地组织表达式,以确保精准地匹配目标方法。总的来说,execution()
方法提供了一种灵活且强大的方式来定义切点表达式,从而精确定位需要添加通知的目标方法。
在 Spring AOP 中,通知(Advice)是在切入点(Pointcut)上执行的代码。Spring 提供了几种类型的通知,每种类型都对应着在连接点执行前、执行后或抛出异常时执行的不同代码逻辑。这些通知对应着不同的注解,常用的通知注解包括:
通知的执行顺序为: @Around -> @Before -> @AfterReturning(不抛异常情况) 或者 @AfterThrowing(抛异常情况) -> @After
这些通知注解可以与 Pointcut 表达式结合使用,实现对目标方法的拦截和处理。通过选择合适的通知类型,开发者可以根据需求在不同的时间点插入自定义的逻辑,实现对方法调用的控制和增强。
讲了那么多概念性的东西,下面来看怎么使用Spring AOP。
在Spring 中使用AOP也很简单,主要分3步:
我这里使用的是Springboot 3.1.5、jdk 17,如果是Springboot低版本的可能需要引入 spring-boot-starter-aop
依赖,高版本的AOP已经包含在spring-boot-starter-web
依赖中了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring官网中介绍,使用Spring AOP要在启动类或者配置类中加上 @EnableAspectJAutoProxy
注解开启 AspectJ 注解的支持,在我使用的这个版本中并不需要,如果你的项目中切面未生效可以尝试使用该注解。
定义一个接口,下面用于对这个接口及其实现类进行拦截:
public interface AopService {
/**
* 两数除法
* @param a
* @param b
* @return
*/
BigDecimal divide(BigDecimal a, BigDecimal b);
/**
* 两数加法
* @param a
* @param b
* @return
*/
BigDecimal add(BigDecimal a, BigDecimal b);
}
@Service
public class MyAopServiceImpl implements AopService{
/**
* 两数除法
*
* @param a
* @param b
* @return
*/
@Override
public BigDecimal divide(BigDecimal a, BigDecimal b) {
return a.divide(b , RoundingMode.UP);
}
/**
* 两数加法
*
* @param a
* @param b
* @return
*/
@Override
public BigDecimal add(BigDecimal a, BigDecimal b) {
return a.add(b);
}
}
新建一个类,在类上加上@Aspect
注解,标记该类为切面。
@Component
@Aspect
public class AspectComponent {
}
在切面中使用@Pointcut
注解定义切点表达式,然后在通知注解中使用定义好的切点。在该示例中主要对AopService#divide()
方法进行拦截。
@Component
@Aspect
public class AspectComponent {
/**
* 匹配AopService接口的divide方法
*/
@Pointcut("execution(* site.suncodernote.aop.AopService.divide(..))")
void dividePointCut(){
}
/**
* 匹配AopService接口的divide方法
*/
@Pointcut("within(site.suncodernote.aop.AopService+)")
void withinPointCut(){
}
/**
* 匹配AopService接口的add方法 或者 divide方法
*/
@Pointcut("execution(* site.suncodernote.aop.AopService.add(..)) || execution(* site.suncodernote.aop.AopService.divide(..))")
void addOrDividePointCut(){
}
@Before("dividePointCut()")
public void beforeDivide(JoinPoint joinPoint){
System.out.println("---------------------@Before----------------");
printJoinPoint(joinPoint);
}
@After("dividePointCut()")
public void afterDivide(JoinPoint joinPoint){
System.out.println("---------------------@After----------------");
printJoinPoint(joinPoint);
}
@AfterReturning(pointcut = "dividePointCut()" , returning = "result")
public void afterReturningDivide(JoinPoint joinPoint , BigDecimal result){
System.out.println("---------------------@AfterReturning----------------");
System.out.println("返回结果="+result);
printJoinPoint(joinPoint);
}
@AfterThrowing(pointcut = "dividePointCut()" , throwing = "e")
public void afterThrowingDivide(JoinPoint joinPoint ,Exception e){
System.out.println("---------------------@AfterThrowing----------------");
System.out.println("异常:"+e.getMessage());
printJoinPoint(joinPoint);
}
@Around("dividePointCut()")
public Object aroundDivide(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("---------------------@Around----------------");
printJoinPoint(joinPoint);
Object[] args = joinPoint.getArgs();
Object result = null;
try {
//执行方法
result = joinPoint.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("返回值:"+result);
return result;
}
private void printJoinPoint(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
System.out.println("方法名:"+signature.getName());
System.out.println("方法参数:"+ Arrays.toString(args));
System.out.println();
}
}
写个简单的单元测试,调用AopService#divide()
方法,然后看一下输出结果。
@SpringBootTest
public class AOPTest {
@Resource
private AopService aopService;
@Test
public void testAOP() {
BigDecimal a = new BigDecimal(1);
BigDecimal b = new BigDecimal(2);
// aopService.add(a, b);
aopService.divide(a, b);
}
}
测试结果:
---------------------@Around----------------
方法名:divide
方法参数:[1, 2]
---------------------@Before----------------
方法名:divide
方法参数:[1, 2]
---------------------@AfterReturning----------------
返回结果=1
方法名:divide
方法参数:[1, 2]
---------------------@After----------------
方法名:divide
方法参数:[1, 2]
返回值:1
从测试结果中通知执行的顺序是按照我们上面所说的执行顺序执行的。
本文介绍了Spring AOP的常用的切点表达式、通知注解等,我们可以利用AOP对业务逻辑的各个部分进行隔离,使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率。