ins-framework-aoplog 使用指南

目录


一、概述

ins-framework-aoplog 是一个基于 AOP(面向切面编程)的日志记录框架,通过注解和切面技术自动记录方法执行信息,包括方法参数、返回值、执行时间、异常信息等。该框架提供了统一的日志格式,支持分布式链路追踪,是构建可观测性系统的重要组件。

1.1 核心特性

  • 自动日志记录:通过 @LogAnnotation 注解自动记录方法执行信息
  • 统一日志格式:标准化的日志输出格式,便于日志分析和监控
  • 链路追踪:支持业务ID、方法ID、全局追踪ID等,实现完整的调用链追踪
  • 参数和返回值记录:自动记录方法参数和返回值(包括复杂对象)
  • 异常处理:自动捕获和记录异常信息,包括完整堆栈
  • Web 请求追踪:通过 Filter 自动记录 HTTP 请求信息
  • 线程安全:使用 ThreadLocal 保证多线程环境下的数据隔离

1.2 技术架构

┌─────────────────────────────────────────────────────────────┐
│                     应用层 (Application)                      │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │  Controller  │  │   Service    │  │     DAO      │      │
│  │  @LogAnno    │  │  @LogAnno    │  │  (自动拦截)  │      │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘      │
└─────────┼──────────────────┼──────────────────┼─────────────┘
          │                  │                  │
          └──────────────────┼──────────────────┘
                             │
          ┌──────────────────▼──────────────────┐
          │      AOP 切面层 (Aspect Layer)        │
          │  ┌──────────────────────────────┐   │
          │  │    AopLogConfig (自定义)      │   │
          │  │  - @Around                   │   │
          │  │  - @Before                   │   │
          │  │  - @After                    │   │
          │  │  - @AfterThrowing            │   │
          │  └──────────┬───────────────────┘   │
          │             │                        │
          │  ┌──────────▼───────────────────┐   │
          │  │   LogAopFormat (框架核心)     │   │
          │  │  - beforeMethod()            │   │
          │  │  - afterMethod()             │   │
          │  │  - afterThrowing()           │   │
          │  │  - logPrint()                │   │
          │  └──────────┬───────────────────┘   │
          └─────────────┼────────────────────────┘
                        │
          ┌─────────────▼─────────────┐
          │    Web 过滤器层 (Filter)    │
          │  ┌─────────────────────┐   │
          │  │   RequestFilter     │   │
          │  │   WebAopFilter      │   │
          │  └─────────────────────┘   │
          └─────────────┬───────────────┘
                        │
          ┌─────────────▼─────────────┐
          │   日志输出层 (Logger)       │
          │  ┌─────────────────────┐   │
          │  │   Logback/SLF4J     │   │
          │  │   monitor-aoplog.log│   │
          │  └─────────────────────┘   │
          └────────────────────────────┘

二、架构设计

2.1 核心组件

2.1.1 LogAopFormat(日志格式化核心类)

位置ins.framework.aop.log.LogAopFormat

职责

  • 方法执行前的准备工作(记录开始时间、生成方法ID等)
  • 方法执行后的日志格式化输出
  • 异常处理
  • 线程数据管理

关键代码分析

public class LogAopFormat {
    // 使用静态 Map 存储线程数据,key 为 "线程ID|方法序号"
    private static Map<String, String> threadData = new HashMap<>();
    private static Map<String, Integer> threadNum = new HashMap<>();
    
    /**
     * 方法执行前
     * 1. 设置 MDC(Mapped Diagnostic Context)用于日志上下文传递
     * 2. 记录开始时间和日期
     * 3. 生成方法ID(UUID)
     * 4. 管理线程计数器(支持嵌套方法调用)
     */
    public void beforeMethod(JoinPoint jointPoint) {
        // 设置 IP 和域名到 MDC
        if (MDC.get(AopLogConfig.IPFLAG) == null || "".equals(MDC.get(AopLogConfig.IPFLAG))) {
            MDC.put(AopLogConfig.IPFLAG, AopLogConfig.getInstance().getIp() + "-" + AopLogConfig.DOMAIN);
        }
        beforeThreadNum();  // 增加线程计数器
        startDate = sdf.format(date);
        startTime = System.currentTimeMillis();
    }
    
    /**
     * 方法执行后
     * 1. 计算执行时间
     * 2. 格式化并输出日志
     * 3. 清理线程数据
     */
    public void afterMethod(JoinPoint jointPoint) {
        endTime = System.currentTimeMillis();
        times = endTime - startTime;
        logPrint(jointPoint, " [" + times + "] [" + startDate + "] [" + AopThreadLocal.getClientid() + "]", "info");
        afterThreadNum();  // 减少线程计数器
        MDC.remove(AopLogConfig.IPFLAG);
    }
    
    /**
     * 日志输出格式
     * [业务ID] [方法ID] [用户] [类名.方法名] [描述] [执行时间] [开始时间] [客户端ID]
     */
    private static void logPrint(JoinPoint jointPoint, String message, String lever) {
        if ("info".equals(lever)) {
            LOG.info("[{}] [{}] [{}] [{}.{}] [}]{}", 
                getServiceId(),      // 业务ID(同一请求链共享)
                getMethodId(),       // 方法ID(每个方法唯一)
                getLoginUser(),      // 登录用户
                getClassName(jointPoint),  // 类名
                getMethodName(jointPoint), // 方法名
                getDescription(jointPoint), // 描述(来自 @LogAnnotation)
                message);            // 执行时间等信息
        }
    }
}

设计要点

  1. 线程计数器机制:通过 threadNum Map 管理嵌套方法调用,确保方法ID的正确分配和回收
  2. 业务ID共享:同一请求链中的所有方法共享同一个业务ID(ServiceID),便于追踪完整调用链
  3. MDC 使用:利用 SLF4J 的 MDC(Mapped Diagnostic Context)传递上下文信息(如IP、域名)
2.1.2 AopThreadLocal(线程本地存储)

位置ins.framework.aop.constant.AopThreadLocal

职责

  • 存储线程级别的上下文信息
  • 保证多线程环境下的数据隔离
  • 支持分布式链路追踪

关键代码分析

public final class AopThreadLocal {
    // 请求模型(用户信息、IP、URI等)
    private static ThreadLocal<ReqModel> threadLocalRequest = new ThreadLocal<>();
    
    // 业务ID(同一请求链共享)
    private static ThreadLocal<String> aopServiceid = new ThreadLocal<>();
    
    // 客户端ID
    private static ThreadLocal<String> clientid = new ThreadLocal<>();
    
    // 分布式追踪ID
    private static ThreadLocal<String> globalTraceId = new ThreadLocal<>();  // 全局追踪ID
    private static ThreadLocal<String> parentTraceId = new ThreadLocal<>();  // 父追踪ID
    private static ThreadLocal<String> localTraceId = new ThreadLocal<>();   // 本地追踪ID
}

设计要点

  1. ThreadLocal 隔离:每个线程拥有独立的数据副本,避免线程间数据污染
  2. 链路追踪支持:通过 GlobalTraceId、ParentTraceId、LocalTraceId 实现分布式系统的调用链追踪
  3. 请求上下文传递:ReqModel 存储用户信息、IP、URI 等请求相关信息
2.1.3 RequestFilter(请求过滤器)

位置ins.framework.aop.log.tool.RequestFilter

职责

  • 拦截所有 HTTP 请求
  • 提取请求信息(IP、用户、URI等)
  • 设置追踪ID
  • 初始化线程本地变量

关键代码分析

public class RequestFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        HttpServletRequest request = (HttpServletRequest) req;
        
        // 1. 设置服务器IP和端口
        if (AopLogConfig.getInstance().getPort() == 0) {
            AopLogConfig.getInstance().setPort(request.getLocalPort());
        }
        AopLogConfig.getInstance().setIp(getIpAddr(request));
        
        // 2. 设置分布式追踪ID
        setTraceId(request);
        
        // 3. 设置请求上下文到 ThreadLocal
        AopThreadLocal.setThreadLocalRequest(new ReqModel(
            request.getSession().getAttribute(AopLogConfig.USERNAME) == null ? ""
                : request.getSession().getAttribute(AopLogConfig.USERNAME).toString(),
            req.getRemoteAddr(), 
            request.getRequestURI()
        ));
        
        chain.doFilter(req, res);
    }
    
    /**
     * 设置追踪ID
     * - 如果请求头中有 GlobalTraceId,说明是分布式调用,使用该ID
     * - 如果没有,说明是入口请求,生成新的 GlobalTraceId
     */
    private void setTraceId(HttpServletRequest request) {
        String pGlobalTraceId = request.getHeader(AopLogConfig.GLOBAL_TRACEID);
        if (pGlobalTraceId == null || "".equals(pGlobalTraceId.trim())) {
            // 入口请求,生成新的追踪ID
            pGlobalTraceId = UUID.randomUUID().toString();
            AopThreadLocal.setParentTraceId("0");
            AopThreadLocal.setLocalTraceId(pGlobalTraceId);
        } else {
            // 分布式调用,使用传递的追踪ID
            AopThreadLocal.setParentTraceId(request.getHeader(AopLogConfig.PARENT_TRACEID));
            AopThreadLocal.setLocalTraceId(UUID.randomUUID().toString());
        }
        AopThreadLocal.setGlobalTraceId(pGlobalTraceId);
    }
}

设计要点

  1. IP 提取策略:优先从 x-forwarded-forProxy-Client-IP 等请求头获取真实IP,支持代理和负载均衡场景
  2. 分布式追踪:通过 HTTP 请求头传递追踪ID,实现跨服务的调用链追踪
  3. 请求上下文初始化:在请求进入时初始化 ThreadLocal,确保后续 AOP 切面可以获取到请求信息
2.1.4 LogAnnotation(日志注解)

位置ins.framework.aop.log.util.LogAnnotation

职责

  • 标记需要记录日志的方法
  • 提供方法的中文名称和描述信息

关键代码分析

@Retention(RetentionPolicy.RUNTIME)  // 运行时保留
@Target({ ElementType.METHOD })      // 只能用于方法
public @interface LogAnnotation {
    /**
     * @return 方法中文名
     */
    String name() default "";
    
    /**
     * @return 方法描述
     */
    String description() default "";
}

使用示例

@LogAnnotation(name = "新增用户", description = "创建新用户,需要提供所有必填字段")
public ApiResponse<Integer> add(@RequestBody SysUserVo userVo) {
    // 方法实现
}

2.2 数据流转过程

HTTP 请求
    │
    ▼
RequestFilter (设置 ThreadLocal、追踪ID)
    │
    ▼
Controller 方法 (@LogAnnotation)
    │
    ▼
AopLogConfig.aroundMethod() (自定义切面)
    │  ├─ 记录参数
    │  ├─ LogAopFormat.beforeMethod() (生成方法ID、记录开始时间)
    │  ├─ 执行方法
    │  ├─ 记录返回值
    │  └─ LogAopFormat.afterMethod() (计算执行时间、输出日志)
    │
    ▼
Service 方法 (@LogAnnotation)
    │
    ▼
AopLogConfig.aroundMethod() (同上)
    │
    ▼
DAO 方法 (自动拦截)
    │
    ▼
AopLogConfig.aroundDaoMethod() (DAO 切面)
    │
    ▼
LogAopFormat.logPrint() → monitor-aoplog.log

三、原理分析

3.1 AOP 切面原理

AOP(Aspect-Oriented Programming)通过代理模式实现横切关注点的分离。ins-framework-aoplog 使用 Spring AOP 和 AspectJ 实现方法拦截。

3.1.1 代理机制

Spring AOP 使用两种代理方式:

  • JDK 动态代理:基于接口的代理,要求目标类实现接口
  • CGLIB 代理:基于继承的代理,通过生成目标类的子类实现代理

对于 Controller 和 Service,通常使用 JDK 动态代理;对于没有接口的类,使用 CGLIB 代理。

3.1.2 切点表达式
// 拦截所有带有 @LogAnnotation 注解的方法
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
public void logAnnotationPointcut() {}

// 拦截 DAO 层所有方法
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void daoMethodPointcut() {}

// 拦截应用层所有方法(排除 DAO 层)
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void applicationMethodPointcut() {}

切点表达式说明

  • @annotation(...):匹配带有指定注解的方法
  • execution(...):匹配方法执行
    • *:匹配任意返回类型
    • pdfc.monitor..*.*(..):匹配 pdfc.monitor 包及其子包下的所有类的所有方法
    • (..):匹配任意参数
3.1.3 通知类型
@Around("logAnnotationPointcut()")   // 环绕通知:可以控制方法执行
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    // 方法执行前
    logAopFormat.beforeMethod(joinPoint);
    
    try {
        // 执行目标方法
        Object result = joinPoint.proceed();
        
        // 方法执行后
        logAopFormat.afterMethod(joinPoint);
        return result;
    } catch (Throwable ex) {
        // 异常处理
        logAopFormat.afterThrowing(joinPoint, ex);
        throw ex;
    }
}

3.2 线程数据管理原理

3.2.1 线程计数器机制

为了支持嵌套方法调用,框架使用线程计数器来管理方法ID:

// 方法执行前:增加计数器
private static void beforeThreadNum() {
    String threadId = getThreadId();  // 获取线程ID
    if (threadNum.get(threadId) == null) {
        threadNum.put(threadId, 1);
    } else {
        threadNum.put(threadId, threadNum.get(threadId) + 1);
    }
    // 为当前方法生成UUID并存储
    threadData.put(threadId + "|" + threadNum.get(threadId), getUuid());
}

// 方法执行后:减少计数器
private static void afterThreadNum() {
    String threadId = getThreadId();
    threadData.remove(threadId + "|" + threadNum.get(threadId));
    Integer count = threadNum.get(threadId);
    if (count != null) {
        if (count == 1) {
            threadNum.remove(threadId);
        } else {
            threadNum.put(threadId, count - 1);
        }
    }
}

示例

线程A执行:
  Controller.add()        → 计数器=1, 方法ID=uuid1
    Service.insert()      → 计数器=2, 方法ID=uuid2
      DAO.insert()        → 计数器=3, 方法ID=uuid3
      DAO.insert() 返回   → 计数器=2
    Service.insert() 返回 → 计数器=1
  Controller.add() 返回   → 计数器=0
3.2.2 业务ID共享机制

同一请求链中的所有方法共享同一个业务ID(ServiceID),便于追踪完整调用链:

private static String getServiceId() {
    String serviceID;
    if (AopThreadLocal.getAopServiceid() != null) {
        // 如果 ThreadLocal 中已有业务ID,复用
        serviceID = AopThreadLocal.getAopServiceid();
    } else {
        // 否则生成新的业务ID并存储
        serviceID = getUuid();
        AopThreadLocal.setAopServiceid(serviceID);
    }
    return serviceID;
}

3.3 日志格式设计

3.3.1 标准日志格式
[时间] [线程] [级别] [IP] [业务ID] [方法ID] [用户] [类名.方法名] [描述] [执行时间] [开始时间] [客户端ID]

示例

[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] 
[28692ac0-e859-4221-83ed-99b0ab01aa99] [48122a66-a9e1-468d-ae83-e8d79741eb47] 
[] [pdfc.monitor.web.SysUserController.add] [}]创建新用户,需要提供所有必填字段 
[79] [2025-11-24 15:21:52,215] [127.0.0.1] [1e749a02-720f-4e25-ba09-f1d3944e104b] [0] [1e749a02-720f-4e25-ba09-f1d3944e104b]

字段说明

  • 业务ID:同一请求链中所有方法共享,用于关联同一业务操作的所有日志
  • 方法ID:每个方法调用唯一,用于标识具体的某个方法执行
  • 用户:当前登录用户(从 Session 中获取)
  • 描述:来自 @LogAnnotationdescription 属性
  • 执行时间:方法执行耗时(毫秒)
  • 客户端ID:请求的唯一标识
3.3.2 参数和返回值日志格式
[参数] [类名.方法名] [参数名(类型)=参数值, ...]
[返回值] [类名.方法名] 返回类型: 类型, 返回值: 值

示例

[参数] [pdfc.monitor.web.SysUserController.add] 
[userVo(SysUserVo)=SysUserVo(id=null, name=测试用户-新增, work=测试工程师, age=25, ...)]

[返回值] [pdfc.monitor.web.SysUserController.add] 
返回类型: ApiResponse, 返回值: ins.framework.web.ApiResponse@3751acd7

四、项目集成

4.1 Maven 依赖

pom.xml 中添加依赖:

<dependencies>
    <!-- AOP Log Framework -->
    <dependency>
        <groupId>ins.framework</groupId>
        <artifactId>ins-framework-aoplog</artifactId>
        <version>6.2.1-SNAPSHOT</version>
    </dependency>
    
    <!-- Spring AOP Support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

4.2 配置文件

4.2.1 logAop.properties

创建 src/main/resources/logAop.properties

# 登录用户变量名(从 Session 中获取用户信息的 key)
loginName_Variable=userCode

# UUID 开关(是否启用 UUID 功能)
uuid_on_off=false
4.2.2 logback-spring.xml

配置 Logback,设置 AOP 日志输出:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 获取应用IP -->
    <conversionRule conversionWord="ip" converterClass="ins.framework.aop.log.util.LogIP" />
    
    <!-- AOP日志 Appender -->
    <appender name="AOPLOG" class="ch.qos.logback.core.rolling.RollingFileAppender"> 
        <File>logs/monitor-aoplog.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/monitor-aoplog-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>500MB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <Pattern>[%date] [%thread] [%level] [%ip] %msg%n</Pattern>
        </encoder> 
    </appender>
    
    <!-- AOPLOG Logger -->
    <logger name="AOPLOG" level="INFO" additivity="false">
        <appender-ref ref="AOPLOG" /> 
    </logger>
    
    <!-- 项目包日志重定向到 AOP 日志 -->
    <logger name="pdfc.monitor" level="DEBUG" additivity="false">
        <appender-ref ref="AOPLOG"/>
        <appender-ref ref="ERROR_LOG"/>
    </logger>
</configuration>

4.3 AOP 配置类

创建 AopLogConfig.java

package pdfc.monitor.config;

import ins.framework.aop.log.LogAopFormat;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;

@Aspect
@Configuration
public class AopLogConfig {
    
    private static final Logger LOG = LoggerFactory.getLogger("AOPLOG");
    private static final Logger ERROR_LOG = LoggerFactory.getLogger("pdfc.monitor");
    private final LogAopFormat logAopFormat = new LogAopFormat();
    
    /**
     * 切点:拦截所有带有 @LogAnnotation 注解的方法
     */
    @Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
    public void logAnnotationPointcut() {}
    
    /**
     * 切点:拦截 DAO 层所有方法
     */
    @Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
    public void daoMethodPointcut() {}
    
    /**
     * 环绕通知:记录参数、返回值、异常
     */
    @Around("logAnnotationPointcut()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 记录参数
        String params = getMethodParams(joinPoint);
        LOG.info("[参数] [{}.{}] {}", 
            joinPoint.getSignature().getDeclaringTypeName(),
            joinPoint.getSignature().getName(), 
            params);
        
        logAopFormat.beforeMethod(joinPoint);
        
        try {
            Object result = joinPoint.proceed();
            
            // 记录返回值
            String returnValue = formatReturnValue(result);
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            LOG.info("[返回值] [{}.{}] 返回类型: {}, 返回值: {}", 
                joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName(),
                signature.getReturnType().getSimpleName(),
                returnValue);
            
            logAopFormat.afterMethod(joinPoint);
            return result;
        } catch (Throwable ex) {
            // 记录异常
            ERROR_LOG.error("方法执行异常", ex);
            LOG.error("[异常] [{}.{}] 参数: {}, 异常: {}", 
                joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName(),
                params,
                ex.getMessage());
            logAopFormat.afterThrowing(joinPoint, ex);
            throw ex;
        }
    }
    
    // DAO 层方法的环绕通知(类似实现)
    @Around("daoMethodPointcut()")
    public Object aroundDaoMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 实现类似
    }
    
    // 参数格式化方法
    private String getMethodParams(JoinPoint joinPoint) {
        // 实现参数格式化
    }
}

4.4 Web 过滤器配置

WebAopLogFilterConfig 是 AOP 日志系统的入口配置类,负责注册两个 Servlet Filter,在 HTTP 请求的生命周期中初始化和清理日志上下文信息。

4.4.1 配置类说明

创建 WebAopLogFilterConfig.java

package pdfc.monitor.config;

import ins.framework.aop.log.tool.RequestFilter;
import ins.framework.aop.log.tool.WebAopFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebAopLogFilterConfig {
    
    @Bean
    public FilterRegistrationBean<RequestFilter> requestFilter() {
        RequestFilter filter = new RequestFilter();
        FilterRegistrationBean<RequestFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("requestFilter");
        registrationBean.setOrder(200);
        return registrationBean;
    }
    
    @Bean
    public FilterRegistrationBean<WebAopFilter> webAopFilter() {
        WebAopFilter filter = new WebAopFilter();
        FilterRegistrationBean<WebAopFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("webAopFilter");
        registrationBean.setOrder(300);
        return registrationBean;
    }
}
4.4.2 RequestFilter(请求过滤器)- Order 200

作用:在请求进入时初始化 AOP 日志上下文

主要工作

  1. 提取服务器信息

    • 获取服务器 IP 地址(支持代理和负载均衡场景)
    • 获取服务器端口号
  2. 设置分布式追踪 ID

    • 如果请求头中有 GlobalTraceId,说明是分布式调用,使用该 ID
    • 如果没有,说明是入口请求,生成新的 GlobalTraceId
    • 设置 ParentTraceIdLocalTraceId
  3. 初始化 ThreadLocal 请求上下文

    • 从 Session 中获取用户信息(userCode
    • 提取客户端 IP 地址
    • 获取请求 URI
    • 将以上信息存入 AopThreadLocal,供后续 AOP 切面使用

执行时机:在所有 Controller 方法执行之前

关键代码逻辑

public class RequestFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        HttpServletRequest request = (HttpServletRequest) req;
        
        // 1. 设置服务器IP和端口
        AopLogConfig.getInstance().setIp(getIpAddr(request));
        AopLogConfig.getInstance().setPort(request.getLocalPort());
        
        // 2. 设置分布式追踪ID
        setTraceId(request);
        
        // 3. 设置请求上下文到 ThreadLocal
        AopThreadLocal.setThreadLocalRequest(new ReqModel(
            request.getSession().getAttribute(AopLogConfig.USERNAME) == null ? ""
                : request.getSession().getAttribute(AopLogConfig.USERNAME).toString(),
            req.getRemoteAddr(), 
            request.getRequestURI()
        ));
        
        chain.doFilter(req, res);
    }
}
4.4.3 WebAopFilter(Web AOP 过滤器)- Order 300

作用:在请求结束时清理 ThreadLocal 数据

主要工作

  1. 清理 ThreadLocal

    • 清除 AopThreadLocal 中的请求上下文
    • 清除追踪 ID 相关的 ThreadLocal 数据
  2. 防止内存泄漏

    • 避免线程池复用导致的数据污染
    • 确保每个请求的日志上下文独立

执行时机:在请求处理完成后(Response 返回前)

为什么需要清理

  • Spring Boot 使用线程池处理请求,同一个线程可能被多个请求复用
  • 如果不清理 ThreadLocal,下一个请求可能会读取到上一个请求的数据
  • 这会导致日志中的用户信息、IP、追踪 ID 等数据混乱
4.4.4 执行流程
HTTP 请求进入
    │
    ▼
RequestFilter (Order 200) - 请求前置处理
    ├─ 提取服务器 IP、端口
    ├─ 设置分布式追踪 ID
    │   ├─ 从请求头获取或生成 GlobalTraceId
    │   ├─ 设置 ParentTraceId
    │   └─ 设置 LocalTraceId
    └─ 初始化 ThreadLocal
        ├─ 用户信息(从 Session 获取)
        ├─ 客户端 IP
        └─ 请求 URI
    │
    ▼
Controller 方法执行
    ├─ AopLogConfig 切面拦截
    ├─ 从 ThreadLocal 获取请求上下文
    ├─ 记录方法参数、返回值
    └─ 日志中包含:用户、IP、追踪 ID 等信息
    │
    ▼
Service 方法执行
    ├─ AopLogConfig 切面拦截
    └─ 使用相同的 ThreadLocal 上下文
    │
    ▼
DAO 方法执行
    ├─ AopLogConfig 切面拦截
    └─ 使用相同的 ThreadLocal 上下文
    │
    ▼
WebAopFilter (Order 300) - 请求后置处理
    └─ 清理 ThreadLocal(防止内存泄漏)
    │
    ▼
HTTP 响应返回
4.4.5 关键配置说明

1. URL 拦截模式

registrationBean.addUrlPatterns("/*");
  • /*:拦截所有 URL 路径
  • 也可以配置特定路径,如:/api/*/admin/*
  • 支持多个路径:registrationBean.addUrlPatterns("/api/*", "/admin/*")

2. 执行顺序(Order)

registrationBean.setOrder(200);  // RequestFilter 先执行
registrationBean.setOrder(300);  // WebAopFilter 后执行
  • Order 值越小,执行越早
  • RequestFilter 必须在 WebAopFilter 之前执行
  • 建议 Order 值间隔 100,方便后续插入其他 Filter

3. Filter 注册

@Bean
public FilterRegistrationBean<RequestFilter> requestFilter() {
    // ...
}
  • 使用 @Bean 注解注册为 Spring Bean
  • Spring 容器管理 Filter 的生命周期
  • 可以控制 Filter 的启用/禁用
4.4.6 为什么需要两个过滤器?

RequestFilter(前置)

  • 在请求开始时初始化上下文
  • 确保后续 AOP 切面能获取到请求信息
  • 如果没有它,日志中无法获取用户、IP、追踪 ID 等信息

WebAopFilter(后置)

  • 在请求结束时清理 ThreadLocal
  • 避免线程池复用导致的数据污染
  • 防止内存泄漏

两者配合

  • RequestFilter 负责"初始化"
  • WebAopFilter 负责"清理"
  • 形成完整的请求生命周期管理
4.4.7 实际效果

当有 HTTP 请求进入时:

  1. RequestFilter 执行

    • 从请求头提取或生成追踪 ID:GlobalTraceId=abc-123-def
    • 将用户信息存入 ThreadLocal:userCode=admin
    • 将 IP 存入 ThreadLocal:clientIp=192.168.1.100
    • 将 URI 存入 ThreadLocal:uri=/api/sys/user/add
  2. Controller/Service/DAO 方法执行

    • AopLogConfig 切面拦截方法
    • 从 ThreadLocal 获取请求上下文
    • 记录日志时自动包含:[用户:admin] [IP:192.168.1.100] [追踪ID:abc-123-def]
  3. WebAopFilter 执行

    • 清理 ThreadLocal
    • 确保下次请求不会使用到旧数据
4.4.8 注意事项
  1. 必须配置:没有这两个 Filter,AOP 日志无法获取请求上下文信息(用户、IP、追踪 ID 等)

  2. 执行顺序很重要:RequestFilter 必须在 WebAopFilter 之前执行(Order 值更小)

  3. URL 模式配置

    • /* 表示拦截所有请求
    • 可以根据需要配置特定路径
    • 静态资源(如 CSS、JS、图片)通常不需要日志,可以排除
  4. 线程安全

    • ThreadLocal 保证每个线程的数据隔离
    • 但必须在请求结束时清理,避免内存泄漏
  5. 分布式追踪

    • 如果系统是微服务架构,需要在服务间传递追踪 ID
    • 通过 HTTP 请求头传递:GlobalTraceIdParentTraceId

五、使用方法

5.1 基本使用

5.1.1 Controller 层
@RestController
@RequestMapping("/api/sys/user")
public class SysUserController {
    
    @PostMapping("/add")
    @LogAnnotation(name = "新增用户", description = "创建新用户,需要提供所有必填字段")
    public ApiResponse<Integer> add(@RequestBody SysUserVo userVo) {
        // 方法实现
        // 日志会自动记录:
        // 1. 方法参数(userVo 的完整内容)
        // 2. 方法返回值
        // 3. 执行时间
        // 4. 异常信息(如果有)
    }
}
5.1.2 Service 层
@Service
public class SysUserServiceImpl implements SysUserService {
    
    @Override
    @LogAnnotation(name = "新增用户", description = "创建新用户")
    public int insert(SysUserVo entity) {
        // 方法实现
    }
}
5.1.3 DAO 层

DAO 层方法会自动拦截,无需添加注解:

@Mapper
public interface SysUserDao extends MybatisBaseDao<SysUser, Integer> {
    // 所有方法都会自动记录日志
    int insert(SysUser entity);
    SysUser selectByPrimaryKey(Integer id);
}

5.2 高级用法

5.2.1 自定义切点

可以定义自定义切点来拦截特定包或类的方法:

@Aspect
@Configuration
public class AopLogConfig {
    
    // 拦截特定包下的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void servicePointcut() {}
    
    // 拦截特定注解的方法
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalPointcut() {}
    
    // 组合切点
    @Pointcut("servicePointcut() && transactionalPointcut()")
    public void serviceTransactionalPointcut() {}
}
5.2.2 条件拦截

可以根据条件决定是否记录日志:

@Around("logAnnotationPointcut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    // 只记录特定类的方法
    String className = joinPoint.getSignature().getDeclaringTypeName();
    if (className.contains(".config.")) {
        return joinPoint.proceed();  // 跳过配置类
    }
    
    // 记录日志
    // ...
}
5.2.3 参数过滤

可以过滤敏感参数,避免记录密码等敏感信息:

private String getMethodParams(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    String[] paramNames = signature.getParameterNames();
    
    StringBuilder params = new StringBuilder("[");
    for (int i = 0; i < args.length; i++) {
        String paramName = paramNames[i];
        
        // 过滤敏感参数
        if ("password".equalsIgnoreCase(paramName) || 
            "pwd".equalsIgnoreCase(paramName)) {
            params.append(paramName).append("=***");
        } else {
            params.append(paramName).append("=").append(args[i]);
        }
    }
    params.append("]");
    return params.toString();
}

5.3 日志查询和分析

5.3.1 根据业务ID查询完整调用链
# 查询特定业务ID的所有日志
grep "28692ac0-e859-4221-83ed-99b0ab01aa99" logs/monitor-aoplog.log

# 输出示例:
# [业务ID] [方法ID1] [Controller.add]
# [业务ID] [方法ID2] [Service.insert]
# [业务ID] [方法ID3] [DAO.insert]
5.3.2 根据方法名查询
# 查询特定方法的所有调用
grep "SysUserController.add" logs/monitor-aoplog.log
5.3.3 查询异常日志
# 查询所有异常
grep "\[异常\]" logs/monitor-aoplog.log

# 查询错误日志文件
cat logs/monitor-error.log
5.3.4 性能分析
# 查询执行时间超过 1000ms 的方法
grep -E "\[[0-9]{4,}\]" logs/monitor-aoplog.log

六、项目实践

6.1 当前项目配置

6.1.1 项目结构
monitor_java/
├── src/main/java/pdfc/monitor/
│   ├── config/
│   │   ├── AopLogConfig.java          # AOP 切面配置
│   │   └── WebAopLogFilterConfig.java # Web 过滤器配置
│   ├── web/
│   │   └── SysUserController.java     # Controller(使用 @LogAnnotation)
│   ├── service/
│   │   └── impl/
│   │       └── SysUserServiceImpl.java # Service(使用 @LogAnnotation)
│   └── domain/sys/dao/
│       └── SysUserDao.java            # DAO(自动拦截)
└── src/main/resources/
    ├── logback-spring.xml             # Logback 配置
    └── logAop.properties              # AOP 日志配置
6.1.2 日志文件
  • monitor-aoplog.log:所有 AOP 日志(方法执行、参数、返回值等)
  • monitor-error.log:错误日志(异常堆栈)
  • monitor-sql.log:SQL 执行日志
  • monitor.log:应用运行日志(已重定向到 AOP 日志)

6.2 实际日志示例

6.2.1 完整调用链日志
# 1. Controller 层
[2025-11-24 15:21:52,204] [main] [INFO] [192.168.43.72:null] 
[参数] [pdfc.monitor.web.SysUserController.add] 
[userVo(SysUserVo)=SysUserVo(id=null, name=测试用户-新增, ...)]

[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] 
[28692ac0-e859-4221-83ed-99b0ab01aa99] [48122a66-a9e1-468d-ae83-e8d79741eb47] 
[] [pdfc.monitor.web.SysUserController.add] [}]创建新用户,需要提供所有必填字段 
[79] [2025-11-24 15:21:52,215] [127.0.0.1] [1e749a02-720f-4e25-ba09-f1d3944e104b] [0] [1e749a02-720f-4e25-ba09-f1d3944e104b]

[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] 
[返回值] [pdfc.monitor.web.SysUserController.add] 
返回类型: ApiResponse, 返回值: ins.framework.web.ApiResponse@3751acd7

# 2. Service 层
[2025-11-24 15:21:52,217] [main] [INFO] [192.168.43.72:null] 
[参数] [pdfc.monitor.service.impl.SysUserServiceImpl.insert] 
[entity(SysUserVo)=SysUserVo(id=null, name=测试用户-新增, ...)]

[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] 
[28692ac0-e859-4221-83ed-99b0ab01aa99] [29a1cfe4-2678-4e7a-b553-5a38e6f01797] 
[] [pdfc.monitor.service.impl.SysUserServiceImpl.insert] [}]创建新用户 
[31] [2025-11-24 15:21:52,217] [127.0.0.1] [1e749a02-720f-4e25-ba09-f1d3944e104b] [0] [1e749a02-720f-4e25-ba09-f1d3944e104b]

[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] 
[返回值] [pdfc.monitor.service.impl.SysUserServiceImpl.insert] 
返回类型: int, 返回值: 1

# 3. DAO 层
[2025-11-24 15:21:52,223] [main] [INFO] [192.168.43.72:null] 
[参数] [ins.framework.mybatis.MybatisBaseDao.insert] 
[entity(Object)=SysUser(id=null, name=测试用户-新增, work=测试工程师, age=25, ...)]

[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] 
[28692ac0-e859-4221-83ed-99b0ab01aa99] [a5c4995a-2628-4332-be7c-f82f7e69a9fd] 
[] [ins.framework.mybatis.MybatisBaseDao.insert] [}]NONE 
[25] [2025-11-24 15:21:52,223] [127.0.0.1] [1e749a02-720f-4e25-ba09-f1d3944e104b] [0] [1e749a02-720f-4e25-ba09-f1d3944e104b]

[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] 
[返回值] [ins.framework.mybatis.MybatisBaseDao.insert] 
返回类型: int, 返回值: 1

分析

  • 所有方法共享同一个业务ID:28692ac0-e859-4221-83ed-99b0ab01aa99
  • 每个方法有唯一的方法ID
  • 可以清晰地看到调用链:Controller → Service → DAO
  • 记录了完整的参数和返回值
6.2.2 异常日志示例
# AOP 日志中的异常记录
[2025-11-24 15:21:52,XXX] [main] [ERROR] [192.168.43.72:null] 
[异常] [pdfc.monitor.web.SysUserController.add] 
参数: [userVo(SysUserVo)=...], 异常: NullPointerException - null pointer

# monitor-error.log 中的详细异常堆栈
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - ========== AOP 方法异常 ==========
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - 方法执行异常 - 类: pdfc.monitor.web.SysUserController, 方法: add, 参数: [...], 异常类型: NullPointerException, 异常消息: null pointer
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - 异常堆栈:
java.lang.NullPointerException
    at pdfc.monitor.web.SysUserController.add(SysUserController.java:123)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    ...

七、最佳实践

7.1 注解使用规范

  1. 所有 Controller 方法都应该添加 @LogAnnotation

    @LogAnnotation(name = "新增用户", description = "创建新用户,需要提供所有必填字段")
    
  2. Service 层关键方法添加 @LogAnnotation

    @LogAnnotation(name = "新增用户", description = "创建新用户")
    
  3. DAO 层无需添加注解(自动拦截)

7.2 日志级别控制

  • INFO:正常业务流程日志
  • ERROR:异常和错误日志
  • DEBUG:开发调试日志(生产环境可关闭)

7.3 性能优化

  1. 避免记录过大的对象:对于大对象,可以只记录关键字段
  2. 异步日志:使用异步 Appender 提高性能
  3. 日志级别控制:生产环境适当提高日志级别,减少日志量

7.4 安全考虑

  1. 敏感信息过滤:密码、token 等敏感信息不应记录到日志
  2. 日志文件权限:确保日志文件有适当的访问权限
  3. 日志脱敏:对手机号、身份证号等敏感数据进行脱敏处理

八、AopLogConfig 使用机制详解

8.1 Spring 如何发现和加载 AopLogConfig

8.1.1 注解识别
@Aspect        // 标识这是一个 AOP 切面类
@Configuration // 标识这是一个 Spring 配置类
public class AopLogConfig {
    // ...
}
  • @Configuration:Spring 在启动时会扫描并加载该类为配置 Bean
  • @Aspect:Spring AOP 会识别该类为切面,并处理其中的切点和通知
8.1.2 自动扫描机制
@SpringBootApplication  // 在 MonitorWeb.java 中
public class MonitorWeb {
    public static void main(String[] args){
        SpringApplication.run(MonitorWeb.class, args);
    }
}

@SpringBootApplication 包含 @ComponentScan,默认扫描主类所在包及其子包:

  • 扫描范围:pdfc.monitor 及其子包
  • AopLogConfig 位于 pdfc.monitor.config,会被自动扫描到
8.1.3 AOP 自动代理

spring-boot-starter-aop 依赖会自动启用 AOP:

  • 自动配置 @EnableAspectJAutoProxy
  • 创建 AOP 代理工厂
  • 为匹配切点的 Bean 创建代理对象

8.2 AOP 切面工作流程

8.2.1 切点定义
// 切点1:拦截所有带有 @LogAnnotation 注解的方法
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
public void logAnnotationPointcut() {}

// 切点2:拦截 DAO 层所有方法
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void daoMethodPointcut() {}

// 切点3:拦截应用层所有方法
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void applicationMethodPointcut() {}
8.2.2 通知绑定
@Around("logAnnotationPointcut()")  // 绑定切点1
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    // 环绕通知逻辑
}

@Around("daoMethodPointcut()")  // 绑定切点2
public Object aroundDaoMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    // DAO 层环绕通知逻辑
}

@Around("applicationMethodPointcut()")  // 绑定切点3
public Object aroundApplicationMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    // 应用层环绕通知逻辑
}

8.3 实际调用流程示例

SysUserController.add() 方法为例:

8.3.1 方法定义
@RestController
@RequestMapping("/api/sys/user")
public class SysUserController {
    
    @PostMapping("/add")
    @LogAnnotation(name = "新增用户", description = "创建新用户,需要提供所有必填字段")
    public ApiResponse<Integer> add(@RequestBody SysUserVo userVo) {
        int result = sysUserService.insert(userVo);
        return new ApiResponse<Integer>(result, "新增成功", result);
    }
}
8.3.2 执行流程
1. HTTP 请求到达
   ↓
2. Spring MVC DispatcherServlet 接收请求
   ↓
3. 路由到 SysUserController.add() 方法
   ↓
4. Spring AOP 拦截(因为方法有 @LogAnnotation 注解)
   ↓
5. 匹配切点:logAnnotationPointcut()
   ↓
6. 执行 AopLogConfig.aroundMethod() 环绕通知
   ├─ 6.1 记录方法参数
   │   LOG.info("[参数] [SysUserController.add] [userVo(...)]")
   ├─ 6.2 调用 LogAopFormat.beforeMethod()
   │   ├─ 设置 MDC(IP、域名)
   │   ├─ 生成方法ID(UUID)
   │   ├─ 记录开始时间
   │   └─ 增加线程计数器
   ├─ 6.3 执行目标方法:joinPoint.proceed()
   │   └─ 实际调用 SysUserController.add()
   │       └─ 调用 sysUserService.insert()
   │           └─ 调用 sysUserDao.insert()(DAO 层也会被拦截)
   ├─ 6.4 记录返回值
   │   LOG.info("[返回值] [SysUserController.add] 返回类型: ApiResponse, 返回值: ...")
   └─ 6.5 调用 LogAopFormat.afterMethod()
       ├─ 计算执行时间
       ├─ 输出标准日志格式
       └─ 清理线程数据
   ↓
7. 返回结果给客户端
8.3.3 嵌套调用示例

Controller.add() 调用 Service.insert() 时:

Controller.add() 被拦截
  ├─ AopLogConfig.aroundMethod() 执行
  │   ├─ 记录参数
  │   ├─ LogAopFormat.beforeMethod() → 生成方法ID1
  │   └─ joinPoint.proceed()
  │       └─ Service.insert() 被拦截
  │           ├─ AopLogConfig.aroundMethod() 执行
  │           │   ├─ 记录参数
  │           │   ├─ LogAopFormat.beforeMethod() → 生成方法ID2
  │           │   └─ joinPoint.proceed()
  │           │       └─ DAO.insert() 被拦截
  │           │           ├─ AopLogConfig.aroundDaoMethod() 执行
  │           │           │   ├─ 记录参数
  │           │           │   ├─ LogAopFormat.beforeMethod() → 生成方法ID3
  │           │           │   └─ joinPoint.proceed()
  │           │           │       └─ 执行实际的 SQL
  │           │           │   └─ LogAopFormat.afterMethod() → 清理方法ID3
  │           │           └─ 记录返回值
  │           │   └─ LogAopFormat.afterMethod() → 清理方法ID2
  │           └─ 记录返回值
  │   └─ LogAopFormat.afterMethod() → 清理方法ID1
  └─ 记录返回值

8.4 切点匹配规则

8.4.1 切点1:@annotation 匹配
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")

匹配所有带有 @LogAnnotation 注解的方法:

  • SysUserController.add()@LogAnnotation → 匹配
  • SysUserServiceImpl.insert()@LogAnnotation → 匹配
  • SysUserDao.selectPageCount()@LogAnnotation → 匹配
8.4.2 切点2:execution 匹配(DAO 层)
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")

匹配 pdfc.monitor.domain.sys.dao 包下所有类的所有方法:

  • SysUserDao.insert() → 匹配
  • SysUserDao.selectByPrimaryKey() → 匹配
  • SysUserAddressDao.insert() → 匹配
8.4.3 切点3:execution 匹配(应用层)
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.domain.sys.dao.*.*(..))")

匹配 pdfc.monitor 包及其子包下的所有方法,但排除 DAO 层:

  • SysUserController.add() → 匹配(但也会被切点1匹配,优先执行切点1)
  • SysUserServiceImpl.insert() → 匹配(但也会被切点1匹配,优先执行切点1)
  • SysUserDao.insert() → 不匹配(被排除)

8.5 代理对象创建

Spring AOP 为匹配切点的 Bean 创建代理:

8.5.1 Controller 代理
// 原始对象
SysUserController controller = new SysUserController();

// Spring 创建的代理对象(简化示例)
SysUserController proxy = new Proxy() {
    @Override
    public ApiResponse<Integer> add(SysUserVo userVo) {
        // 1. 执行 AOP 切面逻辑
        AopLogConfig.aroundMethod(joinPoint);
        // 2. 调用原始方法
        return controller.add(userVo);
    }
};
8.5.2 代理类型
  • JDK 动态代理:如果目标类实现了接口(如 Controller 实现接口)
  • CGLIB 代理:如果目标类没有接口(如直接使用 @RestController 的类)

8.6 关键机制总结

机制 说明
自动发现 @SpringBootApplication 自动扫描 @Configuration@Aspect
代理创建 Spring AOP 为匹配切点的 Bean 创建代理对象
切点匹配 运行时检查方法是否匹配切点表达式
通知执行 匹配的方法调用会先执行 AOP 通知,再执行原方法
嵌套调用 支持多层嵌套,每层都有独立的方法ID
线程隔离 使用 ThreadLocal 保证多线程环境下的数据隔离

8.7 使用要点

  1. 无需手动调用:Spring 自动识别并应用切面
  2. 透明代理:业务代码无需感知 AOP 的存在
  3. 性能影响:每次方法调用都会经过代理,但开销很小
  4. 执行顺序:多个切面时,可通过 @Order 控制执行顺序

九、日志级别区分机制

9.1 AOP 日志级别区分原理

9.1.1 LogAopFormat 中的级别控制

LogAopFormat.java 中,通过 logPrint() 方法的 lever 参数控制日志级别:

private static void logPrint(JoinPoint jointPoint, String message, String lever) {
    if ("info".equals(lever)) {
        // 使用 INFO 级别记录正常执行
        LOG.info("[{}] [{}] [{}] [{}.{}] [}]{}", ...);
    } else if ("error".equals(lever)) {
        // 使用 ERROR 级别记录异常
        LOG.error("[{}] [{}] [{}] [{}.{}] [{}]{}", ...);
    }
}

// 正常执行后调用
public void afterMethod(JoinPoint jointPoint) {
    logPrint(jointPoint, " [" + times + "] ...", "info");  // 传入 "info"
}

// 异常时调用
public void afterThrowing(JoinPoint jointPoint, Throwable ex) {
    logPrint(jointPoint, " [方法运行异常] ...", "error");  // 传入 "error"
}
9.1.2 AopLogConfig 中的级别使用

在自定义的 AopLogConfig.java 中:

// INFO 级别:记录正常信息
LOG.info("[参数] [{}.{}] {}", className, methodName, params);
LOG.info("[返回值] [{}.{}] 返回类型: {}, 返回值: {}", ...);

// ERROR 级别:记录异常信息
LOG.error("[异常] [{}.{}] 参数: {}, 异常: {} - {}", ...);
ERROR_LOG.error("========== AOP 方法异常 ==========");
ERROR_LOG.error("异常堆栈:", ex);

9.2 AOP 与 Logback 的集成

9.2.1 集成架构
┌─────────────────────────────────────────┐
│      ins-framework-aoplog 框架          │
│  ┌──────────────────────────────────┐   │
│  │   LogAopFormat.java              │   │
│  │   LoggerFactory.getLogger("AOPLOG")│ │
│  │   LOG.info() / LOG.error()       │   │
│  └──────────────┬───────────────────┘   │
└─────────────────┼───────────────────────┘
                  │
                  │ SLF4J API
                  ▼
┌─────────────────────────────────────────┐
│          SLF4J 门面层                    │
│  (org.slf4j.Logger, LoggerFactory)      │
└─────────────────┬───────────────────────┘
                  │
                  │ 桥接
                  ▼
┌─────────────────────────────────────────┐
│      Logback 实现层                      │
│  (ch.qos.logback.classic.Logger)        │
│  ┌──────────────────────────────────┐   │
│  │   logback-spring.xml 配置        │   │
│  │   - AOPLOG Logger                │   │
│  │   - AOPLOG Appender              │   │
│  │   - 日志格式、滚动策略等          │   │
│  └──────────────┬───────────────────┘   │
└─────────────────┼───────────────────────┘
                  │
                  ▼
         monitor-aoplog.log 文件
9.2.2 依赖关系

ins-framework-aoplog/pom.xml 中:

<dependencies>
    <!-- SLF4J API -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>
    
    <!-- Logback 实现(包含 logback-core 和 logback-classic) -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
</dependencies>
  • logback-classic 包含 logback-coreslf4j-api 到 logback 的桥接
  • 因此 AOP 框架通过 SLF4J API 使用 logback 作为日志实现
9.2.3 Logback 配置中的级别显示

logback-spring.xml 中:

<encoder>
    <Pattern>[%date] [%thread] [%level] [%ip] %msg%n</Pattern>
</encoder>
  • %level:显示日志级别(INFO、DEBUG、ERROR 等)
  • 实际输出:[2025-11-24 15:21:52,204] [main] [INFO] [192.168.43.72:null] ...
9.2.4 Logger 级别控制
<logger name="AOPLOG" level="INFO" additivity="false">
    <appender-ref ref="AOPLOG" /> 
</logger>
  • level="INFO":只记录 INFO 及以上级别(INFO、WARN、ERROR)
  • DEBUG 级别的日志不会被记录(除非改为 level="DEBUG"

9.3 日志级别在日志文件中的识别

9.3.1 AOP 日志文件(monitor-aoplog.log)
[2025-11-24 15:21:52,204] [main] [INFO] [192.168.43.72:null] [参数] ...
[2025-11-24 15:21:52,231] [main] [DEBUG] [192.168.43.72:null] Fetching JDBC Connection ...
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] [返回值] ...

识别方式:

  • 第三个字段 [INFO][DEBUG][ERROR] 表示日志级别
  • 可通过 grep 过滤特定级别
9.3.2 错误日志文件(monitor-error.log)
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - ========== AOP 方法异常 ==========
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - 方法执行异常 - 类: ...
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - 异常堆栈:
java.lang.NullPointerException
    at ...

识别方式:

  • 第三个字段 ERROR 表示日志级别
  • 该文件只包含 ERROR 级别及以上的日志(通过 ThresholdFilter 过滤)

9.4 日志级别查询命令

# 查询所有 INFO 级别的日志
grep "\[INFO\]" logs/monitor-aoplog.log

# 查询所有 ERROR 级别的日志
grep "\[ERROR\]" logs/monitor-aoplog.log

# 查询所有 DEBUG 级别的日志
grep "\[DEBUG\]" logs/monitor-aoplog.log

# 查询异常日志
grep "\[异常\]" logs/monitor-aoplog.log

# 查看错误日志文件(只包含 ERROR 级别)
cat logs/monitor-error.log

9.5 日志级别分类总结

日志级别 使用场景 代码示例 日志文件
INFO 正常业务流程 LOG.info("[参数] ...") monitor-aoplog.log
INFO 方法执行信息 logAopFormat.afterMethod() monitor-aoplog.log
DEBUG 调试信息(SQL、连接等) MyBatis SQL 日志 monitor-aoplog.log
ERROR 异常信息 LOG.error("[异常] ...") monitor-aoplog.log + monitor-error.log
ERROR 详细异常堆栈 ERROR_LOG.error("异常堆栈:", ex) monitor-error.log

9.6 日志级别层次

TRACE < DEBUG < INFO < WARN < ERROR < FATAL
  • TRACE:最详细,通常不使用
  • DEBUG:调试信息(SQL、参数等)
  • INFO:正常业务流程(方法执行、参数、返回值)
  • WARN:警告信息(当前项目未使用)
  • ERROR:错误和异常
  • FATAL:致命错误(当前项目未使用)

9.7 总结

AOP 日志级别的区分方式:

  1. 代码层面

    • LOG.info() → INFO 级别
    • LOG.error() → ERROR 级别
    • LOG.debug() → DEBUG 级别
  2. 日志格式

    • [%level] 在日志中显示级别标记
    • 格式:[时间] [线程] [级别] [IP] 消息
  3. 文件分离

    • monitor-aoplog.log:所有级别(INFO、DEBUG、ERROR 等)
    • monitor-error.log:只包含 ERROR 级别(通过 ThresholdFilter 过滤)
  4. 查询方式

    • 通过 grep 命令按级别过滤
    • 通过日志格式中的 [级别] 字段识别

十、常见问题

10.1 日志没有记录

可能原因

  1. 方法没有添加 @LogAnnotation 注解
  2. AOP 配置类没有被 Spring 扫描到
  3. 切点表达式不正确

解决方法

  1. 检查方法是否添加了 @LogAnnotation
  2. 确认 AopLogConfig 类上有 @Configuration@Aspect 注解
  3. 确认切点表达式是否正确

10.2 参数或返回值没有记录

可能原因

  1. 使用的是 @Before@After,无法获取返回值
  2. 参数格式化方法有问题

解决方法

  1. 使用 @Around 通知,可以获取返回值
  2. 检查参数格式化方法的实现

10.3 线程数据混乱

可能原因

  1. ThreadLocal 没有正确清理
  2. 线程池复用导致 ThreadLocal 数据残留

解决方法

  1. 确保在方法执行后清理 ThreadLocal
  2. 在 Filter 的 finally 块中清理 ThreadLocal

十一、总结

ins-framework-aoplog 是一个功能完善的 AOP 日志框架,通过注解和切面技术实现了自动化的日志记录。其主要优势包括:

  1. 自动化:通过注解自动记录日志,减少代码侵入
  2. 统一格式:标准化的日志格式,便于日志分析和监控
  3. 链路追踪:支持完整的调用链追踪,便于问题定位
  4. 灵活扩展:可以自定义切点和通知,满足不同需求
  5. Logback 集成:通过 SLF4J API 集成 Logback,提供强大的日志管理能力
  6. 日志级别区分:支持 INFO、DEBUG、ERROR 等多种日志级别,便于日志分类和查询

在实际项目中,合理使用该框架可以大大提高系统的可观测性和问题排查效率。

11.1 核心要点回顾

  1. AopLogConfig 自动工作:通过 Spring AOP 机制自动发现和应用,无需手动调用
  2. 日志级别区分:通过 SLF4J API 的方法(info()error())和 Logback 的 %level 格式体现
  3. AOP 与 Logback 集成:框架使用 SLF4J API,底层由 Logback 实现,通过 logback-spring.xml 配置
  4. 完整调用链追踪:通过业务ID、方法ID、追踪ID 实现完整的调用链追踪

十二、AspectJ Pointcut 表达式语法详解

AopLogConfig 中,我们使用了 AspectJ 的 pointcut 表达式来定义切点。本节详细讲解 AspectJ pointcut 表达式的语法规则和用法。

12.1 基本语法结构

AspectJ pointcut 表达式的基本格式:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

其中:

  • ? 表示可选部分
  • modifiers-pattern:修饰符模式(public、private、protected、static 等)
  • ret-type-pattern:返回类型模式
  • declaring-type-pattern:声明类型模式(类名)
  • name-pattern:方法名模式
  • param-pattern:参数模式
  • throws-pattern:异常类型模式

12.2 关键符号说明

12.2.1 星号 * - 通配符

* 用于匹配任意内容,但只能匹配一个层级

用法示例

// 1. 匹配任意返回类型
execution(* pdfc.monitor.service.*.*(..))
// 匹配:pdfc.monitor.service 包下所有类的所有方法
// 不匹配:pdfc.monitor.service.impl 包下的类(因为 * 只匹配一层)

// 2. 匹配任意方法名
execution(* pdfc.monitor.service.UserService.*(..))
// 匹配:UserService 类中的所有方法

// 3. 匹配任意类名
execution(* pdfc.monitor.service.*Service.*(..))
// 匹配:以 Service 结尾的类中的所有方法
// 例如:UserService、OrderService

// 4. 匹配任意包名(单层)
execution(* pdfc.*.service.*.*(..))
// 匹配:pdfc 下任意一级子包中的 service 包下的所有方法
// 例如:pdfc.monitor.service、pdfc.order.service
// 不匹配:pdfc.monitor.web.service(因为 * 只匹配一层)
12.2.2 点 . - 包分隔符

. 用于分隔包名和类名,表示精确的层级关系

用法示例

// 精确匹配包路径
execution(* pdfc.monitor.service.*.*(..))
// 匹配:pdfc.monitor.service 包下的所有类的所有方法
// 不匹配:pdfc.monitor.service.impl(因为 . 表示精确的下一层)
12.2.3 双点 .. - 递归匹配

.. 用于匹配任意层级的包或任意数量的参数

用法示例

// 1. 匹配任意层级的子包
execution(* pdfc.monitor..*.*(..))
// 匹配:pdfc.monitor 包及其所有子包下的所有类的所有方法
// 例如:
//   - pdfc.monitor.service.UserService
//   - pdfc.monitor.service.impl.UserServiceImpl
//   - pdfc.monitor.web.controller.UserController
//   - pdfc.monitor.domain.sys.dao.UserDao

// 2. 匹配任意数量的参数
execution(* pdfc.monitor.service.*.*(..))
// 匹配:任意参数的方法
// 例如:
//   - void method()
//   - void method(String arg)
//   - void method(String arg1, Integer arg2)
//   - void method(String... args)

// 3. 组合使用
execution(* pdfc..service..*.*(..))
// 匹配:pdfc 下任意层级中,包含 service 的包路径下的所有方法
// 例如:
//   - pdfc.monitor.service.UserService
//   - pdfc.order.service.impl.OrderServiceImpl
12.2.4 括号 () - 参数匹配

() 用于匹配方法参数。

用法示例

// 1. 无参数
execution(* pdfc.monitor.service.*.*())
// 匹配:无参数的方法
// 例如:void init()

// 2. 任意参数(使用 ..)
execution(* pdfc.monitor.service.*.*(..))
// 匹配:任意参数的方法
// 例如:
//   - void method()
//   - void method(String arg)
//   - void method(String arg1, Integer arg2)

// 3. 精确参数类型
execution(* pdfc.monitor.service.*.*(String))
// 匹配:只有一个 String 类型参数的方法
// 例如:void method(String name)

// 4. 多个参数
execution(* pdfc.monitor.service.*.*(String, Integer))
// 匹配:第一个参数是 String,第二个参数是 Integer 的方法
// 例如:void method(String name, Integer age)

// 5. 第一个参数任意,后面还有参数
execution(* pdfc.monitor.service.*.*(.., String))
// 匹配:最后一个参数是 String 的方法
// 例如:
//   - void method(String name)
//   - void method(Integer id, String name)

// 6. 第一个参数是 String,后面还有任意参数
execution(* pdfc.monitor.service.*.*(String, ..))
// 匹配:第一个参数是 String,后面还有任意参数的方法
// 例如:
//   - void method(String name)
//   - void method(String name, Integer age)

12.3 项目中的实际例子

12.3.1 示例 1:拦截注解方法
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
public void logAnnotationPointcut() {
}

说明

  • @annotation(...):匹配带有指定注解的方法
  • 匹配所有带有 @LogAnnotation 注解的方法,无论类和方法的位置

匹配示例

// 匹配
@LogAnnotation(name = "查询用户", description = "根据ID查询用户")
public User getUserById(Long id) { ... }

// 不匹配
public User getUserById(Long id) { ... }  // 没有注解
12.3.2 示例 2:拦截 DAO 层方法
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void daoMethodPointcut() {
}

语法分解

  • execution:执行切点
  • *:任意返回类型
  • pdfc.monitor.domain.sys.dao:精确的包路径
  • .*:包下的任意类(* 匹配类名)
  • .*:类中的任意方法(* 匹配方法名)
  • (..):任意参数

匹配示例

// 匹配
package pdfc.monitor.domain.sys.dao;
public interface SysUserDao {
    List<SysUser> findAll();  // ✓
    SysUser findById(Long id);  // ✓
    void save(SysUser user);  // ✓
}

// 不匹配
package pdfc.monitor.domain.sys.dao.impl;  // 不在 dao 包下
public class SysUserDaoImpl { ... }  // ✗

package pdfc.monitor.domain.sys.dao;
public class SysUserHelper { ... }  // 在 dao 包下,但会被匹配
12.3.3 示例 3:拦截应用层方法(排除 DAO)
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void applicationMethodPointcut() {
}

语法分解

  • execution(* pdfc.monitor..*.*(..))
    • pdfc.monitor..pdfc.monitor 包及其所有子包(.. 表示递归)
    • *.*:任意类的任意方法
    • (..):任意参数
  • &&:逻辑与(AND)
  • !execution(...):逻辑非(NOT),排除 DAO 层方法

匹配示例

// 匹配(应用层)
package pdfc.monitor.service;
public class UserService {
    public void method() { ... }  // ✓
}

package pdfc.monitor.web.controller;
public class UserController {
    public void method() { ... }  // ✓
}

package pdfc.monitor.config;
public class AopLogConfig {
    public void method() { ... }  // ✓
}

// 不匹配(被排除)
package pdfc.monitor.domain.sys.dao;
public interface SysUserDao {
    public void method() { ... }  // ✗(被 !execution 排除)
}

12.4 逻辑运算符

AspectJ 支持逻辑运算符来组合多个切点表达式。

12.4.1 逻辑与 &&(AND)
@Pointcut("execution(* pdfc.monitor.service.*.*(..)) && execution(* *Service.*(..))")
public void servicePointcut() {
}
// 匹配:pdfc.monitor.service 包下,类名以 Service 结尾的类的所有方法
12.4.2 逻辑或 ||(OR)
@Pointcut("execution(* pdfc.monitor.service.*.*(..)) || execution(* pdfc.monitor.web.*.*(..))")
public void serviceOrWebPointcut() {
}
// 匹配:service 包或 web 包下的所有方法
12.4.3 逻辑非 !(NOT)
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.config.*.*(..))")
public void excludeConfigPointcut() {
}
// 匹配:pdfc.monitor 包下的所有方法,但排除 config 包

12.5 其他常用 Pointcut 表达式

12.5.1 基于注解的切点
// 匹配带有指定注解的方法
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalPointcut() {
}

// 匹配带有指定注解的类中的所有方法
@Pointcut("@within(org.springframework.stereotype.Service)")
public void serviceClassPointcut() {
}

// 匹配带有指定注解的类及其子类中的所有方法
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void controllerPointcut() {
}
12.5.2 基于类型的切点
// 匹配实现指定接口的所有方法
@Pointcut("execution(* pdfc.monitor.service.*Service+.*(..))")
public void serviceInterfacePointcut() {
}
// + 表示匹配该类型及其子类型

// 匹配指定类的所有方法
@Pointcut("execution(* pdfc.monitor.service.UserService.*(..))")
public void userServicePointcut() {
}
12.5.3 基于包名的切点
// 匹配指定包下的所有方法
@Pointcut("within(pdfc.monitor.service..*)")
public void servicePackagePointcut() {
}
// within 用于匹配包或类,..* 表示包下所有类

// 排除指定包
@Pointcut("within(pdfc.monitor..*) && !within(pdfc.monitor.config..*)")
public void excludeConfigPackagePointcut() {
}
12.5.4 基于方法签名的切点
// 匹配方法名
@Pointcut("execution(* *Service.find*(..))")
public void findMethodPointcut() {
}
// 匹配:类名以 Service 结尾,方法名以 find 开头的方法

// 匹配返回类型
@Pointcut("execution(List pdfc.monitor.service.*.*(..))")
public void listReturnPointcut() {
}
// 匹配:返回类型为 List 的方法

// 匹配参数类型
@Pointcut("execution(* pdfc.monitor.service.*.*(String, ..))")
public void stringFirstParamPointcut() {
}
// 匹配:第一个参数是 String 的方法

12.6 常见用法模式

12.6.1 拦截所有 Controller 方法
@Pointcut("execution(* pdfc.monitor.web.controller..*.*(..))")
public void controllerPointcut() {
}
12.6.2 拦截所有 Service 方法
@Pointcut("execution(* pdfc.monitor.service..*.*(..))")
public void servicePointcut() {
}
12.6.3 拦截所有 DAO 方法
@Pointcut("execution(* pdfc.monitor.domain..dao.*.*(..))")
public void daoPointcut() {
}
// 匹配所有 domain 子包下的 dao 包中的方法
12.6.4 拦截特定方法名模式
// 拦截所有查询方法
@Pointcut("execution(* pdfc.monitor..*.find*(..)) || " +
          "execution(* pdfc.monitor..*.get*(..)) || " +
          "execution(* pdfc.monitor..*.query*(..))")
public void queryMethodPointcut() {
}

// 拦截所有保存方法
@Pointcut("execution(* pdfc.monitor..*.save*(..)) || " +
          "execution(* pdfc.monitor..*.insert*(..)) || " +
          "execution(* pdfc.monitor..*.add*(..))")
public void saveMethodPointcut() {
}
12.6.5 排除特定方法
// 拦截所有方法,但排除 toString、equals、hashCode
@Pointcut("execution(* pdfc.monitor..*.*(..)) && " +
          "!execution(* *.toString()) && " +
          "!execution(* *.equals(..)) && " +
          "!execution(* *.hashCode())")
public void excludeObjectMethodsPointcut() {
}

12.7 符号对比总结

符号 含义 匹配范围 示例
* 通配符 单个层级或单个名称 *.service.* 匹配一层包名
. 包分隔符 精确的下一层 pdfc.monitor 精确匹配
.. 递归匹配 任意层级或任意数量 pdfc.monitor.. 匹配所有子包
() 无参数 精确匹配无参数方法 method()
(..) 任意参数 匹配任意参数的方法 method(..)
(String) 单参数 匹配一个 String 参数 method(String)
(String, ..) 可变参数 第一个是 String,后面任意 method(String, ...)
&& 逻辑与 同时满足 A && B
|| 逻辑或 满足其一 A || B
! 逻辑非 排除 !A

12.8 最佳实践

  1. 精确匹配优先:尽量使用精确的包路径,避免过度使用 ..

    // 推荐
    execution(* pdfc.monitor.service.*.*(..))
    
    // 不推荐(过于宽泛)
    execution(* pdfc..*.*(..))
    
  2. 使用逻辑运算符组合:合理使用 &&||! 来精确控制切点

    // 推荐:明确排除不需要的包
    execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.config.*.*(..))
    
  3. 使用注解切点:对于需要特殊处理的方法,使用注解更清晰

    // 推荐:使用注解标记需要日志的方法
    @Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
    
  4. 避免过度拦截:不要拦截框架类、工具类等,避免性能问题

    // 推荐:排除框架类
    execution(* pdfc.monitor..*.*(..)) && !within(org.springframework..*)
    
  5. 测试切点表达式:使用 AOP 调试工具或日志验证切点是否正确匹配

12.9 调试技巧

如果切点表达式没有按预期工作,可以通过以下方式调试:

  1. 添加日志:在通知方法中添加日志,查看是否被调用

    @Around("yourPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        LOG.info("切点匹配: {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
    
  2. 使用 Spring AOP 调试:在 application.properties 中启用 AOP 调试

    logging.level.org.springframework.aop=DEBUG
    
  3. 验证方法签名:确认目标方法的完整签名(包名、类名、方法名、参数类型)


文档版本:v1.2
最后更新:2025-11-24
作者:Allen
项目:monitor_java

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐