Spring Boot AOP 切片日志记录
1. 添加依赖
1.1 Maven依赖
<dependencies>
<!-- Spring Boot Starter AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Lombok (可选,简化日志代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
1.2 Gradle依赖
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
2. 启用AOP
在Spring Boot主类上添加 @EnableAspectJAutoProxy 注解:
@SpringBootApplication
@EnableAspectJAutoProxy
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. 基础日志切面
3.1 简单的日志切面
@Aspect
@Component
@Slf4j
public class LoggingAspect {
// 定义切点:拦截所有Controller方法
@Pointcut("execution(* com.example.controller.*.*(..))")
public void controllerPointcut() {}
// 定义切点:拦截所有Service方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 前置通知:方法执行前
@Before("controllerPointcut() || servicePointcut()")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
log.info("开始执行: {}.{} - 参数: {}",
className, methodName, Arrays.toString(args));
}
// 后置通知:方法执行后(无论成功失败)
@After("controllerPointcut() || servicePointcut()")
public void logAfter(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
log.info("执行完成: {}.{}", className, methodName);
}
// 返回通知:方法正常返回后
@AfterReturning(pointcut = "controllerPointcut() || servicePointcut()",
returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
log.info("方法返回: {}.{} - 返回值: {}",
className, methodName, result);
}
// 异常通知:方法抛出异常后
@AfterThrowing(pointcut = "controllerPointcut() || servicePointcut()",
throwing = "exception")
public void logAfterThrowing(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
log.error("方法异常: {}.{} - 异常信息: {}",
className, methodName, exception.getMessage(), exception);
}
}
4. 环绕通知增强版
4.1 使用环绕通知记录执行时间
@Aspect
@Component
@Slf4j
public class PerformanceLoggingAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
@Around("servicePointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
log.info("开始执行: {}.{} - 参数: {}",
className, methodName, Arrays.toString(args));
Object result = null;
try {
result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
log.info("执行完成: {}.{} - 耗时: {}ms - 返回值: {}",
className, methodName, executionTime, result);
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
log.error("执行异常: {}.{} - 耗时: {}ms - 异常: {}",
className, methodName, executionTime, e.getMessage(), e);
throw e;
}
}
}
5. 自定义注解日志
5.1 创建日志注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
/**
* 操作描述
*/
String value() default "";
/**
* 是否记录参数
*/
boolean logParams() default true;
/**
* 是否记录返回值
*/
boolean logResult() default true;
/**
* 操作类型
*/
OperationType operationType() default OperationType.OTHER;
enum OperationType {
CREATE, UPDATE, DELETE, QUERY, OTHER
}
}
5.2 基于注解的日志切面
@Aspect
@Component
@Slf4j
public class AnnotationLoggingAspect {
@Pointcut("@annotation(com.example.annotation.LogOperation)")
public void logOperationPointcut() {}
@Around("logOperationPointcut()")
public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogOperation logOperation = method.getAnnotation(LogOperation.class);
// 获取操作信息
String operation = logOperation.value();
OperationType operationType = logOperation.operationType();
boolean logParams = logOperation.logParams();
boolean logResult = logOperation.logResult();
String methodName = method.getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
// 记录开始日志
log.info("开始执行操作: [{}] {}.{} - 类型: {}",
operation, className, methodName, operationType);
if (logParams) {
log.info("操作参数: {}", Arrays.toString(args));
}
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 记录成功日志
log.info("操作成功: [{}] {}.{} - 耗时: {}ms",
operation, className, methodName, executionTime);
if (logResult && result != null) {
log.info("操作结果: {}", result);
}
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 记录失败日志
log.error("操作失败: [{}] {}.{} - 耗时: {}ms - 异常: {}",
operation, className, methodName, executionTime, e.getMessage(), e);
throw e;
}
}
}
5.3 使用自定义注解
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
@LogOperation(value = "查询用户信息", operationType = OperationType.QUERY)
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping
@LogOperation(value = "创建用户", operationType = OperationType.CREATE,
logParams = true, logResult = true)
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}
@PutMapping("/{id}")
@LogOperation(value = "更新用户信息", operationType = OperationType.UPDATE)
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
User updatedUser = userService.update(id, user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
@LogOperation(value = "删除用户", operationType = OperationType.DELETE)
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
6. 数据库日志记录
6.1 创建日志实体类
@Entity
@Table(name = "sys_operation_log")
@Data
public class OperationLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 操作类型
*/
private String operationType;
/**
* 操作描述
*/
private String description;
/**
* 请求方法
*/
private String method;
/**
* 请求参数
*/
private String params;
/**
* 返回结果
*/
private String result;
/**
* 执行时间(毫秒)
*/
private Long executionTime;
/**
* 操作状态
*/
private String status;
/**
* 错误信息
*/
private String error;
/**
* 操作用户
*/
private String operator;
/**
* 操作IP
*/
private String ip;
/**
* 操作时间
*/
private LocalDateTime createTime;
}
6.2 数据库日志切面
@Aspect
@Component
@Slf4j
public class DatabaseLoggingAspect {
@Autowired
private OperationLogService operationLogService;
@Pointcut("@annotation(com.example.annotation.LogOperation)")
public void logOperationPointcut() {}
@Around("logOperationPointcut()")
public Object logToDatabase(ProceedingJoinPoint joinPoint) throws Throwable {
OperationLog operationLog = new OperationLog();
long startTime = System.currentTimeMillis();
try {
// 设置基本信息
setBasicInfo(joinPoint, operationLog);
// 执行方法
Object result = joinPoint.proceed();
// 设置成功信息
long endTime = System.currentTimeMillis();
operationLog.setExecutionTime(endTime - startTime);
operationLog.setStatus("SUCCESS");
operationLog.setResult(JSON.toJSONString(result));
return result;
} catch (Exception e) {
// 设置失败信息
long endTime = System.currentTimeMillis();
operationLog.setExecutionTime(endTime - startTime);
operationLog.setStatus("FAILED");
operationLog.setError(e.getMessage());
throw e;
} finally {
// 异步保存日志
saveLogAsync(operationLog);
}
}
private void setBasicInfo(ProceedingJoinPoint joinPoint, OperationLog operationLog) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogOperation logOperation = method.getAnnotation(LogOperation.class);
operationLog.setOperationType(logOperation.operationType().name());
operationLog.setDescription(logOperation.value());
operationLog.setMethod(joinPoint.getTarget().getClass().getSimpleName() + "." + method.getName());
operationLog.setParams(JSON.toJSONString(joinPoint.getArgs()));
operationLog.setOperator(getCurrentUser());
operationLog.setIp(getClientIp());
operationLog.setCreateTime(LocalDateTime.now());
}
@Async
private void saveLogAsync(OperationLog operationLog) {
try {
operationLogService.save(operationLog);
} catch (Exception e) {
log.error("保存操作日志失败", e);
}
}
private String getCurrentUser() {
// 从SecurityContext或其他地方获取当前用户
return "anonymous";
}
private String getClientIp() {
// 从RequestContextHolder或其他地方获取客户端IP
return "127.0.0.1";
}
}
7. 请求响应日志
7.1 请求日志切面
@Aspect
@Component
@Slf4j
public class RequestLoggingAspect {
@Pointcut("execution(* com.example.controller.*.*(..))")
public void controllerPointcut() {}
@Around("controllerPointcut()")
public Object logRequestResponse(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = getCurrentRequest();
// 记录请求信息
String requestId = UUID.randomUUID().toString().substring(0, 8);
String method = request.getMethod();
String url = request.getRequestURI();
String queryString = request.getQueryString();
String remoteAddr = request.getRemoteAddr();
log.info("[{}] {} {}?{} - IP: {}", requestId, method, url, queryString, remoteAddr);
// 记录请求头
logHeaders(requestId, request);
// 记录请求体
if ("POST".equals(method) || "PUT".equals(method)) {
logRequestBody(requestId, joinPoint.getArgs());
}
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
// 记录响应信息
log.info("[{}] 响应成功 - 耗时: {}ms", requestId, endTime - startTime);
logResponse(requestId, result);
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
log.error("[{}] 响应失败 - 耗时: {}ms - 异常: {}",
requestId, endTime - startTime, e.getMessage(), e);
throw e;
}
}
private HttpServletRequest getCurrentRequest() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return attributes != null ?
((ServletRequestAttributes) attributes).getRequest() : null;
}
private void logHeaders(String requestId, HttpServletRequest request) {
Enumeration<String> headerNames = request.getHeaderNames();
StringBuilder headers = new StringBuilder();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
headers.append(headerName).append(": ").append(headerValue).append(", ");
}
log.debug("[{}] 请求头: {}", requestId, headers.toString());
}
private void logRequestBody(String requestId, Object[] args) {
try {
log.info("[{}] 请求体: {}", requestId, JSON.toJSONString(args));
} catch (Exception e) {
log.debug("[{}] 请求体序列化失败", requestId, e);
}
}
private void logResponse(String requestId, Object result) {
try {
log.info("[{}] 响应体: {}", requestId, JSON.toJSONString(result));
} catch (Exception e) {
log.debug("[{}] 响应体序列化失败", requestId, e);
}
}
}
8. 配置优化
8.1 日志配置
# application.yml
logging:
level:
com.example.aspect: INFO
org.springframework.aop: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n"
8.2 AOP配置
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
@Bean
public DefaultPointcutAdvisor defaultPointcutAdvisor() {
LogOperationPointcut pointcut = new LogOperationPointcut();
AnnotationLoggingAspect advice = new AnnotationLoggingAspect();
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice(advice);
return advisor;
}
}
9. 性能优化
9.1 异步日志记录
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "logExecutor")
public TaskExecutor logExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Log-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@Aspect
@Component
@Slf4j
public class AsyncLoggingAspect {
@Async("logExecutor")
public void saveLogAsync(OperationLog log) {
// 异步保存日志
}
}
9.2 条件日志记录
@Aspect
@Component
@Slf4j
public class ConditionalLoggingAspect {
@Value("${app.logging.enabled:true}")
private boolean loggingEnabled;
@Value("${app.logging.slow-query-threshold:1000}")
private long slowQueryThreshold;
@Around("execution(* com.example.service.*.*(..))")
public Object conditionalLog(ProceedingJoinPoint joinPoint) throws Throwable {
if (!loggingEnabled) {
return joinPoint.proceed();
}
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
// 只记录慢查询
if (executionTime > slowQueryThreshold) {
String methodName = joinPoint.getSignature().getName();
log.warn("慢查询检测: {} - 耗时: {}ms", methodName, executionTime);
}
return result;
}
}
10. 最佳实践
10.1 避免日志过多
- 合理设置日志级别
- 避免在循环中记录大量日志
- 使用异步日志记录
10.2 敏感信息脱敏
private String maskSensitiveData(String data) {
if (data == null) return null;
// 脱敏手机号
data = data.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
// 脱敏身份证号
data = data.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
// 脱敏邮箱
data = data.replaceAll("(\\w{2})\\w*(\\w@\\w+\\.\\w+)", "$1***$2");
return data;
}
10.3 日志格式统一
public class LogFormat {
public static String formatOperation(String operation, String method, Object... args) {
return String.format("[操作:%s] [方法:%s] [参数:%s]",
operation, method, Arrays.toString(args));
}
public static String formatPerformance(String method, long time, Object result) {
return String.format("[性能监控] [方法:%s] [耗时:%dms] [结果:%s]",
method, time, result);
}
}
11. 总结
Spring Boot AOP日志记录提供了强大的横切关注点处理能力,通过合理使用切面编程可以:
- 统一管理日志记录逻辑
- 减少代码重复
- 提高代码可维护性
- 实现灵活的日志策略
在实际应用中,需要根据业务需求选择合适的日志记录方式,并注意性能优化和敏感信息保护。
6 0
评论 (0)
请先登录后再评论
暂无评论