使用 AOP(面向切面编程)可以帮助你实现横切关注点(如日志记录、事务管理、权限控制等)的分离,比如你对外提供了一个接口,接口上线后产品又要求需要对接口的出入参做记录,以提供数据支撑供业务分析以及方便问题排查。
你想着,这不简单嘛,我只要在接口返回的时候发送一个MQ 记录下来不就行了嘛,于是你兴致勃勃的打开代码一看,傻眼了,这个接口里有众多的if()
判断,并且每个if
判断里都会有结果返回,难道在所有的if
返回前都发送一个消息嘛?示例代码如下:
class BussinessService{
public Response invoke(Param param){
if(参数校验失败){
// 发送消息?
return new Response(400,参数校验失败);
}
if(无数据返回){
// 发送消息?
return new Response(204,返回内容为空);
}
// 。。。。。省略一大堆 if 判断及返回
// 正常业务逻辑
// 发送消息?
return new Response(200,success,result);
}
}
在所有的if
返回前都执行一个逻辑有两个点非常不友好,一个点是如果将来新增if
判断,仍然需要新增发送消息代码,第二个点是太具有侵入性,需要修改原来代码。这时候,使用切面就是一个比较好的选择。在日常开发中,Spring Aop
就是较为常见的切面的选择。下面就对 Spring Aop
切面做详细介绍。
Spring Aop
的基本使用我们现在常用的是 SpringBoot 项目,一般来说,依赖会被自动引入。
org.springframework.boot
spring-boot-starter-aop
这里我们假设场景是在某个方法执行前,进行日志的打印,这里创建一个名为LoggingAspect
的切面类。
切面类是用来定义横切逻辑的地方,我们需要使用@Aspect
注解标明这是一个切面类,此外,需要使用@Component
使 SpringBoot 项目能够扫描到这个 Bean。
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
}
简单来说,切入点表达式就是说你要拦截哪个方法的调用。
这里我们假设要拦截ControllerTest#test
方法的执行,方法定义如下,则我们的切入点表达式定义为:"execution(* com.gumei.webapp.ControllerTest.test(..))"
,这里我们先不细究切面表达式的定义规则,先将功能实现,在下文中会对切面表达式的规则做详细介绍。
@Controller
@CrossOrigin
public class ControllerTest {
@ResponseBody
@RequestMapping("/test")
public Response test(Param param){
Response response = new Response();
response.setAge(param.age);
response.setName(param.name);
response.setDate(new Date());
response.setLocalDateTime(LocalDateTime.now());
return response;
}
}
Advice
通俗的理解通知就是在方法的某个时间节点执行时发出的消息,比如要在方法调用之前监听则使用@Before
通知。至于不同通知的类型及可接收/返回的参数,也将在下文中进行详细介绍。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.gumei.webapp.ControllerTest.test(..))")
public void logBefore(JoinPoint joinPoint){
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
}
此时,我们已经完成了一个 Aop 切面的基本使用步骤,启动项目后,在浏览器输入http://localhost:8787/test?age=3&name=xiaoming
,便可以看到控制台打印的消息:Before method: test
。这样我们就使用Spring Aop
完成了一个基础功能,至于更复杂的功能,无非是在上述基础上根据业务需求进行丰富罢了。
Spring Aop
概念剖析在上文中,我们涉及了很多概念没有进行详细解释,没有解释的目的是方便读者能先快速上手使用,如果解释太多不方便理解。但现在在有了上述基础使用的基础之上,再来解释这些概念便是水到渠成了。上文涉及到的概念有切入点表达式(Pointcut Expression
)、通知Advice
类型和连接点(Join Point),下面将对这些概念做详细解析。
Pointcut Expression
)上文提到,所谓切入点表达式,无非就是要拦截哪个方法的调用。说的更详细一些:
在 Spring AOP 中,切入点表达式(Pointcut Expression)用于指定在哪些连接点(Join Point)上应用通知(Advice)。切入点表达式是 AOP 的核心部分,它定义了哪些方法应该被拦截,以及在哪些地方应用横切逻辑。理解切入点表达式的语法和用法是有效使用 AOP 的关键。
切入点表达式主要通过 execution
语法来定义。以下是 execution
表达式的基本格式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
大部分参数都是可选的,去除可选参数后,仅剩下如下所示的格式,下述格式也是我们日常最为常用的格式:
execution(ret-type-pattern name-pattern(param-pattern))
public
、private
等),通常省略。*
。*
。本举例中有很多两个点 ..
这种语法,下文会对其进行详细说明,这里就理解为通配符即可。
匹配特定包下的所有方法
匹配 com.example.service
包下的所有类的所有方法。
execution(* com.example.service.*.*(..))
匹配特定类的所有方法
匹配 com.example.service.MyService
类中的所有方法。
execution(* com.example.service.MyService.*(..))
匹配返回类型为 String
的方法
匹配 com.example.service
包下的所有类中返回类型为 String
的方法。
execution(String com.example.service.*.*(..))
匹配方法名以 get
开头的方法
匹配 com.example.service
包下的所有类中方法名以 get
开头的方法。
execution(* com.example.service.*.get*(..))
匹配带有特定参数的方法
匹配 com.example.service
包下的所有类中第一个参数为 String
的方法。
execution(* com.example.service.*.*(String, ..))
上文中关闭切入点表达式示例中,有很多两个点 ..
这种语法,这里对其进行详细说明。
在 Spring AOP 的切入点表达式中,两个点 ..
是一个通配符,表示零个或多个任意类型的参数或包,使用 ..
可以极大地提高切入点表达式的灵活性,使得 AOP 能够更广泛地应用于不同的方法和类。。它的使用场景主要有两个:
匹配方法参数:在方法参数列表中使用 ..
可以匹配任意数量和类型的参数。
匹配包及其子包:在包名中使用 ..
可以匹配该包及其所有子包。
使用场景示例
execution(* com.example.service.MyService.someMethod(..))
这个表达式匹配 MyService
类中的 someMethod
方法,无论它有多少个参数或参数的类型是什么。
execution(* com.example..service.*.*(..))
这个表达式匹配 com.example
包及其所有子包中的 service
包下的所有类的所有方法。这里的 ..
通配符用于匹配 com.example
包下的所有子包。
execution(* com.example.service.MyService.*(..))
这个表达式匹配 MyService
类中的所有方法,无论方法名是什么,也无论参数是什么。
除了 execution
以外,Spring AOP 还支持其他类型的切入点表达式,进行更高阶的使用:
within:限制匹配特定类型内的方法。
匹配 com.example.service
包及其子包下的所有类的方法。
within(com.example.service..*)
this:匹配当前 AOP 代理对象的类型。
匹配代理对象实现 MyService
接口的方法。
this(com.example.service.MyService)
target:匹配目标对象的类型。
匹配目标对象实现 MyService
接口的方法。
target(com.example.service.MyService)
args:匹配方法参数的类型。
匹配第一个参数为 String
类型的方法。
args(java.lang.String)
@annotation:匹配被特定注解标记的方法。
匹配被 @Transactional
注解标记的方法。
@annotation(org.springframework.transaction.annotation.Transactional)
Advice
在 Spring AOP 中,通知(Advice)是实际执行横切逻辑的地方。Spring AOP 提供了多种类型的通知,每种通知类型在目标方法执行的不同阶段切入。以下是各种通知类型的详细讲解,包括它们的使用方法和参数示例。
下面将对各种通知类型以及其能够接收的出入参做详细介绍:
@Before
通知@Before
通知在目标方法执行之前运行。
示例:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class BeforeAdviceExample {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
}
参数:
JoinPoint
:提供对连接点处的可用信息的访问,如方法名、参数等。@After
通知@After
通知在目标方法执行完成后运行,无论方法是否成功返回或抛出异常。
示例:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AfterAdviceExample {
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature().getName());
}
}
@AfterReturning
通知@AfterReturning
通知在目标方法成功返回后运行。
示例:
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AfterReturningAdviceExample {
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("Method returned: " + result);
}
}
参数:
returning
:指定返回值的变量名,用于捕获目标方法的返回值。@AfterThrowing
通知@AfterThrowing
通知在目标方法抛出异常后运行。
示例:
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AfterThrowingAdviceExample {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void logAfterThrowing(Throwable error) {
System.out.println("Method threw exception: " + error);
}
}
参数:
throwing
:指定异常的变量名,用于捕获目标方法抛出的异常。@Around
通知@Around
通知是最强大的通知类型,可以在目标方法执行之前和之后进行自定义逻辑。它还可以选择不调用目标方法来完全取代方法的执行。
@Around
通知需要调用 proceed()
方法来执行目标方法,否则目标方法不会被执行。示例:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AroundAdviceExample {
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("After method: " + joinPoint.getSignature().getName());
return result;
}
}
参数:
ProceedingJoinPoint
:是 JoinPoint
的子接口,允许控制目标方法的执行。proceed()
方法用于执行目标方法。JoinPoint
JoinPoint
是一个接口,用于提供有关当前连接点的静态信息。它通常用于 @Before
、@After
、@AfterReturning
和 @AfterThrowing
通知中。
Object[] getArgs()
:返回被拦截方法的参数列表。String getKind()
:返回连接点的类型(例如,方法调用)。Signature getSignature()
:返回被拦截方法的签名信息,包括方法名、返回类型等。Object getTarget()
:返回目标对象(被代理的对象)。Object getThis()
:返回当前代理对象。ProceedingJoinPoint
是 JoinPoint
的子接口,专门用于 @Around
通知。它不仅提供静态信息,还允许控制目标方法的执行。
Object proceed()
:继续执行被拦截的方法,并返回其结果。Object proceed(Object[] args)
:使用新的参数继续执行被拦截的方法。ProceedingJoinPoint
允许在目标方法执行前后插入自定义逻辑,甚至可以完全取代目标方法的执行。
通过学习本文,你将能够熟练掌握Spring Aop
的使用,快去项目中试一试吧!
以上,祝你今天愉快。