一、AOP核心概念
什么是AOP?
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它将横切关注点(如日志记录、权限校验等)从业务逻辑中分离出来,达到解耦和代码复用的目的。
传统开发 vs AOP开发
方式 | 传统开发 | AOP开发 |
---|---|---|
结构 | 功能代码与业务逻辑耦合 | 功能模块横向抽取 |
维护 | 修改需改动多处 | 集中管理 |
复用 | 代码复制粘贴 | 一次定义多处使用 |
典型应用场景:
-
日志记录
-
权限校验
-
事务管理
-
性能监控
-
异常处理
AOP核心术语详解
术语 | 英文 | 解释 | 类比 |
---|---|---|---|
切面 | Aspect | 横切关注点的模块化实现,包含切入点和通知 | 日志模块 |
切入点 | PointCut | 定义拦截条件的表达式 | WHERE子句 |
通知 | Advice | 拦截后执行的具体逻辑 | WHAT操作 |
目标对象 | Target | 被增强的业务逻辑对象 | 原始对象 |
连接点 | JoinPoint | 程序执行时的具体位置(Spring中为方法) | 执行点 |
织入 | Weaving | 将切面应用到目标对象的过程 | 装配过程 |
二、Spring AOP通知类型
Spring提供了五种通知类型,覆盖方法调用的各个阶段:
注解 | 执行时机 | 适用场景 |
---|---|---|
@Before | 方法调用前 | 参数校验、权限检查 |
@After | 方法调用后(无论成败) | 资源清理 |
@AfterReturning | 方法成功返回后 | 结果处理 |
@AfterThrowing | 方法抛出异常后 | 异常处理 |
@Around | 方法调用前后 | 全流程控制(最强大) |
三、日志记录实战实现
1. 环境准备
确保项目中包含以下依赖:
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 日志框架 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
2. 自定义日志注解
/**
* 标记需要记录日志的方法
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Loggable {
/**
* 日志级别(默认INFO)
*/
LogLevel level() default LogLevel.INFO;
/**
* 是否记录参数(默认true)
*/
boolean logParams() default true;
/**
* 是否记录返回值(默认false)
*/
boolean logResult() default false;
enum LogLevel {
TRACE, DEBUG, INFO, WARN, ERROR
}
}
3. 增强型日志切面实现
@Aspect
@Component
@Slf4j
public class EnhancedLoggingAspect {
// 定义两种切入点
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}
@Pointcut("@annotation(com.example.annotation.Loggable)")
public void loggableMethod() {}
/**
* 环绕通知实现方法日志记录
*/
@Around("serviceLayer() || loggableMethod()")
public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = method.getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
// 获取注解配置
Loggable loggable = method.getAnnotation(Loggable.class);
boolean logParams = loggable == null || loggable.logParams();
boolean logResult = loggable != null && loggable.logResult();
Loggable.LogLevel level = loggable == null ? Loggable.LogLevel.INFO : loggable.level();
// 记录方法入口
if (logParams) {
logAtLevel(level, "【Enter】{}.{}() - 参数: {}",
className, methodName, Arrays.toString(joinPoint.getArgs()));
} else {
logAtLevel(level, "【Enter】{}.{}()", className, methodName);
}
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
Object result = joinPoint.proceed();
// 记录方法出口
if (logResult) {
logAtLevel(level, "【Exit】{}.{}() - 返回值: {} (耗时: {}ms)",
className, methodName, result, stopWatch.getTotalTimeMillis());
} else {
logAtLevel(level, "【Exit】{}.{}() - 耗时: {}ms",
className, methodName, stopWatch.getTotalTimeMillis());
}
return result;
} catch (Exception e) {
log.error("【Exception】{}.{}() - 异常: {} (耗时: {}ms)",
className, methodName, e.getMessage(), stopWatch.getTotalTimeMillis(), e);
throw e;
}
}
private void logAtLevel(Loggable.LogLevel level, String format, Object... args) {
switch (level) {
case TRACE: log.trace(format, args); break;
case DEBUG: log.debug(format, args); break;
case INFO: log.info(format, args); break;
case WARN: log.warn(format, args); break;
case ERROR: log.error(format, args); break;
default: log.info(format, args);
}
}
}
4. 应用示例
@Service
public class UserService {
// 使用自定义注解配置日志
@Loggable(level = Loggable.LogLevel.DEBUG, logResult = true)
public User getUserById(Long id) {
// 业务逻辑
}
// 自动记录service包下的方法
public List<User> listUsers() {
// 业务逻辑
}
}
四、最佳实践建议
-
性能优化:
- 在高频方法上禁用参数日志(
logParams = false
) - 使用合适的日志级别(生产环境建议INFO以上)
- 对大型对象实现
toString()
优化日志输出
-
安全考虑:
// 敏感参数过滤示例 private String filterSensitiveData(Object arg) { if (arg instanceof String) { String str = (String) arg; if (str.contains("password")) { return "***FILTERED***"; } } return String.valueOf(arg); }
-
扩展方向:
- 集成EL表达式实现动态条件判断
- 增加操作人信息记录
- 结合MDC实现请求链路追踪
五、常见问题解答
Q:AOP失效的常见原因?
1. 同类方法调用(通过代理对象调用可解决)
2. 静态方法无法拦截
3. 未启用AOP(检查@EnableAspectJAutoProxy
)
Q:如何选择切入点表达式?
-
execution()
:最常用,指定方法签名 -
@annotation()
:基于注解匹配 -
within()
:匹配类型声明
通过本文介绍的方式,您可以轻松实现灵活、可配置的日志记录功能,显著提升系统可维护性和开发效率。