使用 AOP(面向切面编程)技术在 Spring Boot 应用程序中对所有控制器方法的执行进行日志记录。具体来说,它拦截所有控制器中的公共方法,记录方法的输入参数和返回结果
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@Aspect
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MethodExecutionLogAspect {
@Resource
private ObjectMapper objectMapper;
// 切入点拦截所有 Controller 方法
@Pointcut("execution(public * com.example.*.controller.*Controller.*(..))")
private void controller() {
}
@Around("controller()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
logMethodEntry(joinPoint);
Object result = joinPoint.proceed();
logMethodExit(result);
return result;
}
private void logMethodEntry(ProceedingJoinPoint joinPoint) {
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
String params = objectToString(joinPoint.getArgs());
log.info("==============处理方法入参:{} {}", parameterNames, params);
} catch (Exception ex) {
log.error("AOP 在方法入参日志记录时出错: {}", ex.getMessage(), ex);
}
}
private void logMethodExit(Object result) {
try {
log.info("==============处理方法出参: {}", objectToString(new Object[]{result}));
} catch (Exception ex) {
log.error("AOP 在方法出参日志记录时出错: {}", ex.getMessage(), ex);
}
}
private String objectToString(Object[] objs) {
if (objs == null || objs.length == 0) {
return "empty";
}
StringBuilder stringBuilder = new StringBuilder("[");
for (Object obj : objs) {
if (stringBuilder.length() > 1000) {
stringBuilder.append("...太长省略");
break;
}
String s;
if (obj == null) {
s = "{}";
} else if (obj instanceof ServletResponse) {
s = "{Response}";
} else if (obj instanceof ServletRequest) {
s = "{Request}";
} else {
s = convertObjectToString(obj);
}
stringBuilder.append(s).append(",");
}
int length = stringBuilder.length();
if (length > 1) {
stringBuilder.deleteCharAt(length - 1); // 移除最后一个逗号
}
stringBuilder.append("]");
return stringBuilder.toString();
}
private String convertObjectToString(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
return obj.toString();
}
}
}
主要作用
- 日志记录:在方法执行之前记录方法的输入参数,在方法执行之后记录返回结果。
- 增强方法执行的可调试性和可追踪性:通过记录输入和输出,方便开发人员调试和跟踪方法的执行情况。
详细解释
定义切面(Aspect):
@Aspect
:声明这是一个切面类。@Component
:将这个切面类声明为 Spring 容器中的一个 Bean。@Slf4j
:提供日志记录功能,使用log
对象记录日志。@Order(Ordered.HIGHEST_PRECEDENCE)
:设置切面的优先级为最高。
定义切入点(Pointcut):
@Pointcut("execution(public * com.example.*.controller.*Controller.*(..))")
:定义一个切入点,拦截所有com.example.*.controller
包及其子包下的控制器类中的公共方法。
环绕通知(Around Advice):
@Around("controller()")
:定义一个环绕通知,围绕在指定切入点的方法执行前后。around(ProceedingJoinPoint joinPoint)
:环绕通知方法,接收一个ProceedingJoinPoint
参数,它提供对被拦截方法的访问。
方法入参日志记录:
logMethodEntry(joinPoint)
:记录方法的输入参数。- 获取方法签名和参数名称。
- 将参数转换为字符串。
- 记录日志。
方法执行:
Object result = joinPoint.proceed();
:执行被拦截的方法,并获取返回结果。
方法出参日志记录:
logMethodExit(result)
:记录方法的返回结果。
参数和结果的转换:
objectToString(Object[] objs)
:将方法参数或返回结果转换为字符串,处理null
值、ServletRequest
和ServletResponse
对象。convertObjectToString(Object obj)
:将对象转换为 JSON 字符串,如果转换失败则调用toString()
方法。
代码的意义
这段代码对于开发和运维团队来说非常有用:
- 调试:当方法执行出现问题时,通过日志可以快速定位问题,查看方法的输入和输出。
- 审计:记录所有方法调用的输入输出,可以用于审计和追踪。
- 监控:通过记录方法执行的详细信息,可以监控系统的运行状态和性能。
示例日志输出
执行日志可能会如下:
INFO MethodExecutionLogAspect - ==============处理方法入参:["input"] ["example"]
INFO MethodExecutionLogAspect - ==============处理方法出参: ["Hello example"]