前言
在定位线上问题时,最常用的操作就是查看接口的"入参是什么"以及"返回了什么"。如果手动在每个 Controller 方法中去打印日志,不仅代码臃肿,且难以维护。
通过 Spring AOP(面向切面编程) ,我们可以实现一种非侵入式的日志增强机制:在不修改任何业务代码的前提下,自动捕捉接口的请求参数、响应结果以及整个方法的执行耗时。
关键实现
性能监控 StopWatch
StopWatch 是 spring 提供的轻量级性能监控工具,使用方式如下:
java
StopWatch stopWatch = new StopWatch("order-process");
stopWatch.start("query-db");
// 查询数据库
stopWatch.stop();
stopWatch.start("call-remote");
// 调用远程接口
stopWatch.stop();
stopWatch.start("calculate");
// 业务计算
stopWatch.stop();
log.info("cost info:\n{}", stopWatch.prettyPrint());
// 耗时
log.info("接口耗时:{}ms", stopWatch.getTotalTimeMillis());
切入点
配置切入点 @RestController 或 Controller,对所有接口中的方法进行拦截:
java
@Pointcut("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
public void controllerPointcut() {
}
获取 swagger 接口描述
配置上面切入点的环绕通知,对接口方法增强:
java
/**
* 缓存方法描述,避免频繁反射
*/
private final Map<Method, String> methodDescriptionCache = new ConcurrentHashMap<>();
// swagger 接口全类名(用于反射)
private static final String SWAGGER_OPERATION_CLASS = "io.swagger.v3.oas.annotations.Operation";
/**
* 获取方法描述(尝试从 Swagger 注解中获取)
*
* @param method 方法
* @return 描述信息
*/
private String getMethodDescription(Method method) {
return methodDescriptionCache.computeIfAbsent(method, m -> {
// 尝试获取 Swagger 3 (@Operation)
String summary = getAnnotationValue(m, SWAGGER_OPERATION_CLASS, "summary");
if (StringUtils.hasText(summary)) {
return summary;
}
return "";
});
}
/**
* 反射获取注解属性值
*
* @param method 方法
* @param annotationClass 注解全类名
* @param property 属性名
* @return 属性值
*/
private String getAnnotationValue(Method method, String annotationClass, String property) {
try {
Class<?> clazz = Class.forName(annotationClass);
if (clazz.isAnnotation()) {
@SuppressWarnings("unchecked")
Class<? extends Annotation> annotationType = (Class<? extends Annotation>) clazz;
Annotation annotation = method.getAnnotation(annotationType);
if (annotation != null) {
Method propertyMethod = clazz.getMethod(property);
Object value = propertyMethod.invoke(annotation);
return value != null ? value.toString() : null;
}
}
} catch (Exception e) {
// 忽略异常(类不存在或反射失败)
}
return null;
}
完整实现
配置文件
java
@Data
@ConfigurationProperties(prefix = "x-polaris.web")
public class PolarisWebProperties {
/**
* 请求日志配置
*/
private Log log = new Log();
/**
* 请求日志配置类
*/
@Data
public static class Log {
/**
* 是否开启请求日志切面,默认 true
*/
private boolean enabled = true;
/**
* 是否记录请求参数,默认 true
*/
private boolean logRequest = true;
/**
* 是否记录响应结果,默认 true
*/
private boolean logResponse = true;
}
}
日志切面
java
perties properties;
/**
* 缓存方法描述,避免频繁反射
*/
private final Map<Method, String> methodDescriptionCache = new ConcurrentHashMap<>();
/**
* Swagger Operation 注解全类名
*/
private static final String SWAGGER_OPERATION_CLASS = "io.swagger.v3.oas.annotations.Operation";
@Pointcut("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
public void controllerPointcut() {
}
@Around("controllerPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
PolarisWebProperties.Log logConfig = properties.getLog();
if (!logConfig.isEnabled()) {
return point.proceed();
}
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = null;
HttpServletRequest request = null;
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (attributes != null) {
request = attributes.getRequest();
}
// 获取方法描述
String description = "";
if (point.getSignature() instanceof MethodSignature) {
Method method = ((MethodSignature) point.getSignature()).getMethod();
description = getMethodDescription(method);
}
// 记录请求信息
if (request != null && logConfig.isLogRequest()) {
String method = request.getMethod();
String uri = request.getRequestURI();
String queryString = request.getQueryString();
String args = Arrays.stream(point.getArgs())
.map(arg -> {
if (arg instanceof MultipartFile) {
return "MultipartFile:" + ((MultipartFile) arg).getOriginalFilename();
}
return String.valueOf(arg);
})
.collect(Collectors.joining(", "));
if (StringUtils.hasText(description)) {
log.info("Request Start: ({}) [{}] {} , Args: {}", description, method,
uri + (queryString != null ? "?" + queryString : ""), args);
} else {
log.info("Request Start: [{}] {}, Args: {}", method,
uri + (queryString != null ? "?" + queryString : ""), args);
}
}
result = point.proceed();
return result;
} catch (Throwable e) {
log.error("Request Error: {}", e.getMessage());
throw e;
} finally {
stopWatch.stop();
// 记录响应信息
if (logConfig.isLogResponse()) {
log.info("Request End: Time: {}ms, Result: {}", stopWatch.getTotalTimeMillis(), result);
} else {
log.info("Request End: Time: {}ms", stopWatch.getTotalTimeMillis());
}
}
}
/**
* 获取方法描述(尝试从 Swagger 注解中获取)
*
* @param method 方法
* @return 描述信息
*/
private String getMethodDescription(Method method) {
return methodDescriptionCache.computeIfAbsent(method, m -> {
// 尝试获取 Swagger 3 (@Operation)
String summary = getAnnotationValue(m, SWAGGER_OPERATION_CLASS, "summary");
if (StringUtils.hasText(summary)) {
return summary;
}
return "";
});
}
/**
* 反射获取注解属性值
*
* @param method 方法
* @param annotationClass 注解全类名
* @param property 属性名
* @return 属性值
*/
private String getAnnotationValue(Method method, String annotationClass, String property) {
try {
Class<?> clazz = Class.forName(annotationClass);
if (clazz.isAnnotation()) {
@SuppressWarnings("unchecked")
Class<? extends Annotation> annotationType = (Class<? extends Annotation>) clazz;
Annotation annotation = method.getAnnotation(annotationType);
if (annotation != null) {
Method propertyMethod = clazz.getMethod(property);
Object value = propertyMethod.invoke(annotation);
return value != null ? value.toString() : null;
}
}
} catch (Exception e) {
// 忽略异常(类不存在或反射失败)
}
return null;
}
}
自动配置
java
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "x-polaris.web.log", name = "enabled", havingValue = "true", matchIfMissing = true)
public RequestLogAspect requestLogAspect(PolarisWebProperties properties) {
return new RequestLogAspect(properties);
}