一、文章前言
针于公司的项目,排查问题太慢,因为每个人的代码风格不一,日志的打印可能没有严格的要求,所以写以此篇,实现【请求日志打印】和【注解日志打印】两大功能,方便问题排查与日志记录。
二、日志系统的"瑞士军刀":Fastjson + AOP黄金组合
2.1 选型背后的秘密
- Fastjson:阿里开源的JSON处理神器,序列化速度比Jackson快30%
- AOP:日志记录的"上帝视角",不侵入业务代码实现全方位监控
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
2.2 智能JSON工具类开发
java
public class JsonUtils {
// 美化输出+保留空值+防XSS攻击
private static final SerializerFeature[] FEATURES = {
SerializerFeature.PrettyFormat,
SerializerFeature.WriteMapNullValue,
SerializerFeature.DisableCheckSpecialChar
};
// 支持泛型的深度序列化
public static <T> String toJson(T obj) {
return JSON.toJSONString(obj, FEATURES);
}
}
三、日志处理方案
3.1 线程池的参数配置
这里可以根据服务器的实际资源和项目需要进行设置
java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "logExecutor")
public Executor logExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Log-Executor-");
executor.initialize();
return executor;
}
}
3.2 日志上下文设计原则
复习一下Lombok的注解作用:
- @Data:生成getter、setter
- @Builder(toBuilder = true):基于现有对象生成一个新的Builder实例
- @AllArgsConstructor:全参构造函数
- @NoArgsConstructor:无参构造函数
- @With:生成一个带有所有参数的静态工厂方法,每个方法的名称是"withXxx"
- @Accessors(chain = true):开启链式访问模式,像person.setName("Alice").setAge(30)一样连续调用
java
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@With
@Accessors(chain = true)
class LogContext {
// 核心字段
// 全链路追踪ID
private String traceId;
// 请求路径
private String uri;
// HTTP方法
private String method;
// 客户端IP(支持代理识别)
private String ip;
// 扩展字段
// 智能参数过滤
private Object params;
// 响应体格式化
private Object response;
// 精确到毫秒的耗时
private Long costTime;
// 异常堆栈摘要
private String errorStack;
// 添加拷贝方法
public LogContext copy() {
return this.toBuilder().build();
}
}
四、智能日志切面的实现
4.1 抽象日志切面层
抽象日志切面层将请求日志切面和注解日志切面的公共方法抽离了出来形成复用
java
public class AbstractLogAspect {
protected static final Logger log = LoggerFactory.getLogger(RequestLogAspect.class);
protected static final String TRACE_ID = "traceId";
protected static final Set<String> SENSITIVE_KEYS = Set.of("password", "token", "Authorization");
@Resource(name = "logExecutor")
protected Executor logExecutor;
protected Object[] filterParams(Object[] args) {
return Arrays.stream(args)
.map(arg -> {
if (arg instanceof Map<?, ?>) {
return filterSensitiveFields((Map<?, ?>) arg);
}
return arg;
})
.toArray();
}
protected Map<?, ?> filterSensitiveFields(Map<?, ?> map) {
Map<Object, Object> filtered = new HashMap<>(map);
filtered.keySet().removeIf(key -> SENSITIVE_KEYS.contains(key.toString()));
return filtered;
}
protected Object filterResponse(Object result) {
if (result instanceof String) {
return result;
}
return JsonUtils.toJson(result);
}
// 获取客户端真实IP地址
protected String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
4.2 请求日志切面
当用户进行接口请求的时候,日志切面会自动打印请求的参数、接口路径、响应内容等等,详细可看日志上下文设计属性。
java
@Configuration
@Aspect
@Component
@EnableAsync
public class RequestLogAspect extends AbstractLogAspect{
@Pointcut("execution(* cn.bdmcom.*.controller..*.*(..))")
public void controllerPointcut() {
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
String traceId = UUID.randomUUID().toString();
MDC.put(TRACE_ID, traceId);
LogContext logContext = null;
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
logContext = LogContext.builder()
.traceId(traceId)
.uri(request.getRequestURI())
.method(request.getMethod())
.ip(getClientIp(request))
.params(filterParams(pjp.getArgs()))
.build();
// 异步记录请求开始日志(携带MDC上下文)
LogContext finalLogContext = logContext;
logExecutor.execute(() -> {
MDC.put(TRACE_ID, traceId);
log.info("Request Start => {}", JsonUtils.toJson(finalLogContext));
MDC.remove(TRACE_ID);
});
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - startTime;
LogContext responseContext = logContext.copy()
.setCostTime(cost)
.setResponse(filterResponse(result));
logExecutor.execute(() -> {
MDC.put(TRACE_ID, traceId);
log.info("Response => {}", JsonUtils.toJson(responseContext));
MDC.remove(TRACE_ID);
});
return result;
} catch (Throwable th) {
assert logContext != null;
LogContext errorContext = logContext.copy()
.setErrorStack(th.getMessage());
logExecutor.execute(() -> {
MDC.put(TRACE_ID, traceId);
log.error("Error => {}", JsonUtils.toJson(errorContext));
MDC.remove(TRACE_ID);
});
throw th;
} finally {
MDC.remove(TRACE_ID);
}
}
}
打印开始:
打印结束:
4.3 注解日志切面
自定义日志切面注解,可以加到某个方法上,实现对方法的入参、出餐、响应时间等等进行打印记录。
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogPrint {
// 描述日志类型(如 "user_login", "order_create")
String value() default "";
// 是否记录请求参数
boolean recordParam() default true;
// 是否记录响应结果
boolean recordResult() default true;
// 是否记录执行时间
boolean recordTime() default true;
}
注解日志切面实现,原理和请求日志切面类似
java
@Configuration
@Aspect
@Component
@EnableAsync
public class AnnotationLogAspect extends AbstractLogAspect {
private static final String TRACE_ID = "traceId";
// 使用环绕通知处理所有带有LogPrint注解的方法
@Pointcut("@annotation(cn.bdmcom.config.aspect.logger.LogPrint)")
public void logPointcut() {
}
@Around("logPointcut()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
//类名
String clazzName = pjp.getTarget().getClass().getName();
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
//方法名
String methodName = methodSignature.getName();
long startTime = System.currentTimeMillis();
String traceId = UUID.randomUUID().toString();
// 设置MDC跟踪ID
MDC.put(TRACE_ID, traceId);
ServletRequestAttributes attributes;
LogPrint logPrint = null;
LogContext logContext = null;
try {
// 获取当前请求的上下文,必须显式处理null情况
attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
throw new IllegalStateException("No active request found");
}
HttpServletRequest request = attributes.getRequest();
MethodSignature signature = (MethodSignature) pjp.getSignature();
logPrint = signature.getMethod().getAnnotation(LogPrint.class);
// 构建基础日志上下文
logContext = LogContext.builder()
.traceId(traceId)
.costTime(0L)
.uri(clazzName)
.method(methodName)
.ip(getClientIp(request))
.build();
// 处理请求参数
if (logPrint != null && logPrint.recordParam()) {
logContext = logContext.setParams(filterParams(pjp.getArgs()));
}
// 异步记录请求日志(使用MDC传递traceId)
LogContext finalLogContext = logContext;
LogPrint finalLogPrint = logPrint;
logExecutor.execute(() -> {
try {
MDC.put(TRACE_ID, traceId);
log.info("Request Start [{}] => {}", finalLogPrint != null && StrUtil.isNotBlank(finalLogPrint.value()) ? (finalLogPrint.value()) : "N/A", JsonUtils.toJson(finalLogContext));
} finally {
MDC.remove(TRACE_ID);
}
});
} catch (Throwable th) {
// 记录异常日志
assert logContext != null;
LogContext errorContext = logContext.copy().toBuilder()
.traceId(traceId)
.errorStack(th.getMessage())
.build();
LogPrint finalLogPrint1 = logPrint;
logExecutor.execute(() -> {
try {
MDC.put(TRACE_ID, traceId);
log.error("Request Error [{}] => {}", finalLogPrint1 != null && StrUtil.isNotBlank(finalLogPrint1.value()) ? (finalLogPrint1.value()) : "N/A", JsonUtils.toJson(errorContext));
} finally {
MDC.remove(TRACE_ID);
}
});
throw th;
} finally {
// 清除MDC
MDC.remove(TRACE_ID);
// 处理响应日志
try {
long cost = System.currentTimeMillis() - startTime;
assert logContext != null;
LogContext responseContext = logContext.copy().toBuilder()
.traceId(traceId)
.build();
if (logPrint != null && logPrint.recordTime()) {
responseContext = responseContext.setCostTime(cost);
}
if (logPrint != null && logPrint.recordResult()) {
responseContext = responseContext.setResponse(filterResponse(pjp.proceed()));
}
LogPrint finalLogPrint2 = logPrint;
LogContext finalResponseContext = responseContext;
logExecutor.execute(() -> {
try {
MDC.put(TRACE_ID, traceId);
log.info("Request Response [{}] => {}", finalLogPrint2 != null && StrUtil.isNotBlank(finalLogPrint2.value()) ? finalLogPrint2.value() : "N/A", JsonUtils.toJson(finalResponseContext));
} finally {
MDC.remove(TRACE_ID);
}
});
} catch (Exception e) {
// 忽略响应日志记录异常,避免影响主线程
log.warn("Failed to record response log", e);
}
}
return pjp.proceed();
}
}
打印开始:
打印结束:
五、专家级调试技巧
5.1 使用traceId全链路追踪
java
grep 'traceId=20230815123456' info.log
5.2 耗时分析(推荐Arthas工具)
java
trace cn.bdmcom.UserController* '#costTime>100' // 捕获100ms以上请求
5.3 异常画像生成
java
// 自动生成异常频率报表
errorStat-t 5 -i 3600// 统计最近1小时TOP5异常