基于AOP实现智能日志打印

一、文章前言

针于公司的项目,排查问题太慢,因为每个人的代码风格不一,日志的打印可能没有严格的要求,所以写以此篇,实现【请求日志打印】和【注解日志打印】两大功能,方便问题排查与日志记录。

二、日志系统的"瑞士军刀":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的注解作用:

  1. @Data:生成getter、setter
  2. @Builder(toBuilder = true):基于现有对象生成一个新的Builder实例
  3. @AllArgsConstructor:全参构造函数
  4. @NoArgsConstructor:无参构造函数
  5. @With:生成一个带有所有参数的静态工厂方法,每个方法的名称是"withXxx"
  6. @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异常
相关推荐
27669582926 分钟前
拼多多 anti-token unidbg 分析
java·python·go·拼多多·pdd·pxx·anti-token
安然无虞19 分钟前
31天Python入门——第17天:初识面向对象
后端·爬虫·python·职场和发展
yuhaiqiang25 分钟前
订单交易系统就该这么设计,既优雅又高效
后端
xyliiiiiL37 分钟前
二分算法到红蓝染色
java·数据结构·算法
编程、小哥哥41 分钟前
spring之添加freemarker模版熏染
java·后端·spring
hong_zc1 小时前
Spring 拦截器与统一功能的处理
java·后端·spring
User_芊芊君子1 小时前
【Java】——数组深度解析(从内存原理到高效应用实践)
java·开发语言
珹洺2 小时前
C++从入门到实战(十)类和对象(最终部分)static成员,内部类,匿名对象与对象拷贝时的编译器优化详解
java·数据结构·c++·redis·后端·算法·链表
一 乐2 小时前
网红酒店|基于java+vue的网红酒店预定系统(源码+数据库+文档)
java·开发语言·数据库·毕业设计·论文·springboot·网红酒店预定系统
ai大师3 小时前
给聊天机器人装“短期记忆“:Flask版实现指南
后端·python·gpt·flask·oneapi·中转api·apikey