1. 简介
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它通过将程序分解成不同的关注点来解决复杂性问题
。在 AOP 中,一个关注点是一个横切关注点(cross-cutting concern),常见的关注点如日志记录、事务管理、安全性检查等。在传统的面向对象编程中,这些关注点往往会被分散到不同的类和方法中,导致代码重复、难以维护。AOP 提供了一种更好的方式来处理这些横切关注点,它通过将这些关注点从主要业务逻辑中分离出来,将其封装为可重用的模块,并在需要的时候将其动态地织入到主要业务逻辑中
。
AOP 的核心思想是切面(aspect),它是一个横切关注点的实现。切面定义了在何时、何处和如何进行横切关注点的实现。在 AOP 中,切面可以被视为一个横向的功能单元,它在系统中独立存在,与主要业务逻辑相互配合,实现系统的功能。
AOP 可以在很多场景下使用,比如日志记录、事务管理、安全性检查、性能监控、缓存管理等。AOP 已经成为许多企业级应用中不可或缺的一部分。常见的 AOP 框架包括 Spring AOP、AspectJ 等。
2. Spring AOP 和 AspectJ
Spring AOP 和 AspectJ 都是 AOP 的实现框架,但它们之间有一些区别:
- 实现方式:Spring AOP 是基于动态代理实现的,而 AspectJ 是基于编译时织入(CTW)和运行时织入(RTW)实现的。
- AOP 的能力:Spring AOP 只支持方法级别的连接点(join points)和切点(pointcuts),而 AspectJ 支持方法级别和更细粒度的连接点,比如字段访问、对象初始化等。AspectJ 还支持更多的切点表达式和切点选择器。
- 配置方式:Spring AOP 支持使用 XML 配置文件或注解来声明切面和切点,而 AspectJ 支持使用专门的 AspectJ 语言来声明切面和切点。
- 依赖关系:Spring AOP 是 Spring 框架的一部分,可以和 Spring 容器一起使用。而 AspectJ 是一个独立的框架,需要额外的配置和依赖库。
总的来说,Spring AOP 更适合简单的切面场景,比如事务管理、安全性检查、日志记录等,而 AspectJ 更适合复杂的切面场景,比如跨越多个对象、使用更细粒度的连接点、需要复杂的切点表达式等。
3. @Aspect实现切面常用注解
- @before: 前置通知,在方法执行之前执行。
- @After:后置通知,在方法执行后执行。
- @AfterReturning:返回通知,在方法返回结果之后执行。
- @AfterThrowing:异常通知,在方法抛出异常之后执行。
- @Around:环绕通知,围绕着方法执行。
4. 导入依赖包
spring-boot-starter-web包含了spring-boot-starter-aop,spring-boot-starter-aop包含了aspectjweaver,根据实际情况选择其中一个引入即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
5. springboot实现
创建一个实体类
@Aspect
@Component
public class RequestLoggingAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Around("@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController) "
+ "&& (@annotation(org.springframework.web.bind.annotation.RequestMapping) || @annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping))"
+ "&& within(com.example..*)")
public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
logger.info("Request URL: {}", request.getRequestURL().toString());
logger.info("HTTP Method: {}", request.getMethod());
logger.info("IP Address: {}", request.getRemoteAddr());
logger.info("Class Method: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.info("Request Parameters: {}", request.getParameterMap());
Object result = null;
try {
result = joinPoint.proceed();
logger.info("Response: {}", result);
} catch (Exception e) {
logger.error("Exception in {}.{}() with cause = '{}'", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), e.getCause() != null ? e.getCause() : "NULL");
throw e;
} finally {
// 具体的日志记录实现
addLog(joinPoint);
}
return result;
}
}
选择@Around
注解进行切入,是因为这种方式可以获悉方法执行时,是否抛出了异常,进行异常捕获和处理后,将异常再重新抛出,不会影响方法执行的主流程。
需要注意的是,这里仍然需要在 Spring Boot 应用程序启动类中加上 @EnableAspectJAutoProxy
注解来启用 AOP 支持。
@Around
中within(com.example..*)
可以通过包路径
配合正则表达式来指定切点过滤,@annotation
将切面应用于所有被指定注解标记的方法
上, @within(注解类型)
表达式来指定类上的注解
进行过滤。
当前的切面类是具体的实现,不同的子模块可能有不同的需求,可以将当前实体类定义为抽象类,具体的实现由子模块定义service进行实现。同时也可以自定义注解LogOperate
,使用自定义注解标识不需要记录日志的接口方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogOperate {
boolean enable() default true;
}
private void addLog(ProceedingJoinPoint joinPoint, OperateResult result) {
try {
Method method = getMethod(joinPoint);
LogOperate logOperate = AnnotationUtils.getAnnotation(method, LogOperate.class);
if (logOperate == null || logOperate.enable()) {
}
} catch (Exception e) {
log.error("failed.", e);
}
}