Skip to content

请求日志

✨ 自动记录 Controller 请求日志,支持链式扩展和灵活配置。

  • 🎯 自动拦截:自动拦截所有 @Controller@RestController 请求
  • 📊 详细信息:记录请求 URI、方法、参数、执行时间、客户端 IP 等
  • 🔗 链式扩展:支持自定义 RequestInfoChainExecute 扩展处理逻辑
  • 🚫 灵活排除:通过 @IgnoreRequestLog 注解排除不需要记录的接口
  • 🧵 ThreadLocal:自动存储请求上下文,便于全局访问

快速开始

自动启用

引入 krismile-boot3-autoconfigure 依赖后,请求日志切面默认自动启用,无需任何配置。

xml
<dependency>
    <groupId>host.springboot</groupId>
    <artifactId>krismile-boot3-autoconfigure</artifactId>
</dependency>

默认日志输出示例

log
[KS-RequestInfo-Execute-Begin] 检测到请求 [clientIp: 192.168.1.100, userId: 1001, username: admin, requestUri: /api/user/login, requestType: POST, method: com.example.UserController.login(), params: [LoginRequest(username=admin, password=******)]]

[KS-RequestInfo-Execute-End] 检测到请求 [clientIp: 192.168.1.100, userId: 1001, username: admin, requestUri: /api/user/login, requestType: POST, method: com.example.UserController.login(), params: [LoginRequest(username=admin, password=******)], executeTime: 125ms]

核心架构

工作流程

HTTP 请求

RequestLogAop 拦截

解析 RequestInfo (URI, IP, 方法参数等)

存入 ThreadLocal (REQUEST_CONTEXT)

执行 beforeExecute() - 前置链式处理

执行目标 Controller 方法

记录 ResponseInfo (结果, 执行时间)

执行 afterExecute() - 后置链式处理

清理 ThreadLocal

核心类说明

类名职责说明
RequestLogAop请求日志切面拦截 Controller 方法,记录请求信息
RequestInfo请求信息模型封装请求 URI、IP、参数等信息
ResponseInfo响应信息模型封装响应结果、执行时间等信息
RequestInfoChainExecute链式处理器接口定义前置/后置处理方法
DefaultRequestLogChainExecute默认日志处理器框架提供的默认日志输出实现
@IgnoreRequestLog排除注解标记不需要记录日志的类或方法

基础用法

场景1:查看请求日志

框架自动记录所有 Controller 请求的日志:

java
@RestController
@RequestMapping("/api/user")
public class UserController {
    
    @PostMapping("/login")
    public SingleVO<UserDTO> login(@RequestBody LoginRequest request) {
        // 自动记录请求日志:
        // - 请求 URI: /api/user/login
        // - 请求方法: POST
        // - 方法参数: LoginRequest
        // - 执行时间
        return R.okSingle(userService.login(request));
    }
}

日志输出

log
[KS-RequestInfo-Execute-Begin] 检测到请求 [clientIp: 192.168.1.100, userId: null, username: null, requestUri: /api/user/login, requestType: POST, method: com.example.UserController.login(), params: [LoginRequest(username=admin)]]

[KS-RequestInfo-Execute-End] 检测到请求 [clientIp: 192.168.1.100, userId: 1001, username: admin, requestUri: /api/user/login, requestType: POST, method: com.example.UserController.login(), params: [LoginRequest(username=admin)], executeTime: 125ms]

场景2:排除敏感接口

使用 @IgnoreRequestLog 注解排除不需要记录的接口:

java
@RestController
@RequestMapping("/api/health")
public class HealthController {
    
    // 方式1:排除单个方法
    @IgnoreRequestLog
    @GetMapping("/check")
    public BaseVO healthCheck() {
        return R.ok("OK");
    }
}

// 方式2:排除整个 Controller
@IgnoreRequestLog
@RestController
@RequestMapping("/api/monitor")
public class MonitorController {
    
    @GetMapping("/metrics")
    public BaseVO getMetrics() {
        // 整个 Controller 的所有方法都不记录日志
        return R.okSingle(monitorService.getMetrics());
    }
}

场景3:获取请求上下文

在 Service 层或任意位置获取当前请求信息:

java
@Service
public class UserService {
    
    public User login(LoginRequest request) {
        // 从 ThreadLocal 获取请求信息
        RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();
        
        String clientIp = requestInfo.getClientIp();
        String uri = requestInfo.getUri();
        
        // 记录登录日志
        log.info("用户登录 [IP: {}, URI: {}]", clientIp, uri);
        
        // 业务逻辑...
        return user;
    }
}

RequestInfo 数据模型

核心字段

字段类型说明示例
threadIdString线程 ID1
threadNameString线程名称http-nio-8080-exec-1
uriString请求 URI/api/user/login
clientIpString客户端真实 IP192.168.1.100
userAgentStringUser-AgentMozilla/5.0...
osString操作系统Windows 10
browserString浏览器Chrome 120
methodTypeString请求方法类型POST
fullMethodNameString完整方法名com.example.UserController.login()
requestMethodArgsObject[]方法参数[LoginRequest(...)]
userIdString用户 ID1001 (需自定义扩展)
userNameString用户名称admin (需自定义扩展)

使用示例

java
// 获取请求信息
RequestInfo requestInfo = RequestInfo.REQUEST_CONTEXT.get();

// 获取客户端 IP
String clientIp = requestInfo.getClientIp();

// 获取请求 URI
String uri = requestInfo.getUri();

// 获取方法参数
Object[] args = requestInfo.getRequestMethodArgs();

// 获取用户信息(需自定义扩展)
String userId = requestInfo.getUserId();
String userName = requestInfo.getUserName();

自定义链式处理器

步骤1:实现 RequestInfoChainExecute 接口

java
import host.springboot.framework.context.chain.RequestInfoChainExecute;
import host.springboot.framework3.core.model.RequestInfo;
import host.springboot.framework3.core.model.ResponseInfo;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

@Component
public class CustomRequestLogChain implements RequestInfoChainExecute, Ordered {
    
    @Override
    public void beforeExecute(
            HttpServletRequest request, 
            RequestInfo requestInfo) {
        // 前置处理:设置用户信息
        String token = request.getHeader("Authorization");
        if (token != null) {
            UserContext user = parseToken(token);
            requestInfo.setUserId(user.getId());
            requestInfo.setUserName(user.getName());
        }
        
        // 记录自定义日志
        log.info("请求开始 [URI: {}, 用户: {}]", 
            requestInfo.getUri(), requestInfo.getUserName());
    }
    
    @Override
    public void afterExecute(
            HttpServletRequest request,
            RequestInfo requestInfo,
            ResponseInfo responseInfo) {
        // 后置处理:记录慢请求
        long executeTime = responseInfo.getExecutionTime();
        if (executeTime > 3000) {
            log.warn("慢请求告警 [URI: {}, 耗时: {}ms]",
                requestInfo.getUri(), executeTime);
        }
        
        // 保存操作日志到数据库
        saveOperationLog(requestInfo, responseInfo);
    }
    
    @Override
    public int getOrder() {
        return 200; // 执行顺序,数字越小优先级越高
    }
    
    private UserContext parseToken(String token) {
        // 解析 Token 获取用户信息
        return jwtService.parseToken(token);
    }
    
    private void saveOperationLog(RequestInfo req, ResponseInfo resp) {
        // 保存操作日志到数据库
        operationLogService.save(req, resp);
    }
}

步骤2:多个链式处理器执行顺序

java
// 处理器1:用户信息提取(优先级最高)
@Component
public class UserInfoChain implements RequestInfoChainExecute, Ordered {
    @Override
    public int getOrder() {
        return 100; // 优先级最高
    }
    
    @Override
    public void beforeExecute(HttpServletRequest request, RequestInfo info) {
        // 提取用户信息
        info.setUserId(getCurrentUserId());
        info.setUserName(getCurrentUserName());
    }
}

// 处理器2:日志输出(默认处理器)
@Component
public class DefaultRequestLogChainExecute 
        implements RequestInfoChainExecute, Ordered {
    @Override
    public int getOrder() {
        return RequestInfoChainExecute.DEFAULT_EXECUTE_ORDER; // 100
    }
    
    @Override
    public void afterExecute(HttpServletRequest request, 
                            RequestInfo info, ResponseInfo resp) {
        // 输出日志
        log.info("[请求完成] [URI: {}, 耗时: {}ms]", 
            info.getUri(), resp.getExecutionTime());
    }
}

// 处理器3:慢请求监控(优先级最低)
@Component
public class SlowRequestChain implements RequestInfoChainExecute, Ordered {
    @Override
    public int getOrder() {
        return 300; // 优先级最低,最后执行
    }
    
    @Override
    public void afterExecute(HttpServletRequest request, 
                            RequestInfo info, ResponseInfo resp) {
        // 监控慢请求
        if (resp.getExecutionTime() > 3000) {
            alertService.sendSlowRequestAlert(info, resp);
        }
    }
}

执行顺序

beforeExecute 执行顺序:
  1. UserInfoChain (order=100)
  2. DefaultRequestLogChain (order=100)
  3. SlowRequestChain (order=300)

↓ 执行 Controller 方法

afterExecute 执行顺序:
  1. UserInfoChain (order=100)
  2. DefaultRequestLogChain (order=100)
  3. SlowRequestChain (order=300)

常见场景

场景1:记录操作日志

java
@Component
public class OperationLogChain implements RequestInfoChainExecute {
    
    @Autowired
    private OperationLogService operationLogService;
    
    @Override
    public void afterExecute(
            HttpServletRequest request,
            RequestInfo requestInfo,
            ResponseInfo responseInfo) {
        
        // 构建操作日志
        OperationLog log = new OperationLog()
            .setUserId(requestInfo.getUserId())
            .setUserName(requestInfo.getUserName())
            .setUri(requestInfo.getUri())
            .setMethod(requestInfo.getMethodType())
            .setClientIp(requestInfo.getClientIp())
            .setExecuteTime(responseInfo.getExecutionTime())
            .setResult(JSON.toJSONString(responseInfo.getResult()))
            .setCreateTime(LocalDateTime.now());
        
        // 异步保存日志
        operationLogService.saveAsync(log);
    }
}

场景2:慢请求告警

java
@Component
public class SlowRequestAlertChain implements RequestInfoChainExecute {
    
    private static final long SLOW_THRESHOLD = 3000; // 3秒
    
    @Autowired
    private AlertService alertService;
    
    @Override
    public void afterExecute(
            HttpServletRequest request,
            RequestInfo requestInfo,
            ResponseInfo responseInfo) {
        
        long executeTime = responseInfo.getExecutionTime();
        if (executeTime > SLOW_THRESHOLD) {
            // 发送告警
            String message = String.format(
                "慢请求告警:URI=%s, 耗时=%dms, 用户=%s",
                requestInfo.getUri(),
                executeTime,
                requestInfo.getUserName()
            );
            alertService.sendAlert(message);
        }
    }
}

场景3:敏感信息脱敏

java
@Component
public class SensitiveDataMaskChain implements RequestInfoChainExecute {
    
    @Override
    public void beforeExecute(
            HttpServletRequest request,
            RequestInfo requestInfo) {
        
        // 脱敏方法参数
        Object[] args = requestInfo.getRequestMethodArgs();
        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof LoginRequest loginReq) {
                    // 密码脱敏
                    loginReq.setPassword("******");
                } else if (args[i] instanceof UserUpdateRequest userReq) {
                    // 手机号脱敏
                    String phone = userReq.getPhone();
                    if (phone != null && phone.length() == 11) {
                        userReq.setPhone(phone.substring(0, 3) + "****" + phone.substring(7));
                    }
                }
            }
        }
    }
}

场景4:请求限流统计

java
@Component
public class RequestRateLimitChain implements RequestInfoChainExecute {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final int MAX_REQUESTS = 100; // 每分钟最大请求数
    
    @Override
    public void beforeExecute(
            HttpServletRequest request,
            RequestInfo requestInfo) {
        
        String key = "rate_limit:" + requestInfo.getClientIp();
        String minute = LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("yyyyMMddHHmm")
        );
        String rateKey = key + ":" + minute;
        
        // 统计请求次数
        Long count = redisTemplate.opsForValue().increment(rateKey);
        if (count == 1) {
            redisTemplate.expire(rateKey, 60, TimeUnit.SECONDS);
        }
        
        // 超过限流阈值,记录日志
        if (count != null && count > MAX_REQUESTS) {
            log.warn("请求限流 [IP: {}, 次数: {}]", 
                requestInfo.getClientIp(), count);
        }
    }
}

启用与禁用

方式1:配置文件控制(推荐)

application.yml 中配置:

yaml
krismile:
  web:
    request-log:
      # 是否启用请求日志 AOP(默认: true)
      enabled: true
      
      # DEBUG 模式打印的请求头(仅 DEBUG 日志级别生效)
      debug-print-header-names:
        - Authorization
        - User-Agent
        - Content-Type

关闭请求日志

yaml
krismile:
  web:
    request-log:
      enabled: false  # 关闭请求日志功能

方式2:环境变量控制

通过环境变量动态控制:

bash
# 启用请求日志
java -jar app.jar --krismile.web.request-log.enabled=true

# 禁用请求日志
java -jar app.jar --krismile.web.request-log.enabled=false

方式3:自定义配置类

通过 @ConditionalOnProperty 注解控制:

java
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnProperty(
    prefix = "krismile.web.request-log",
    name = "enabled",
    havingValue = "true",
    matchIfMissing = true  // 默认启用
)
public class RequestLogConfig {
    
    /**
     * 自定义 DEBUG 模式打印的请求头
     */
    @Bean
    public DefaultRequestLogChainExecute defaultRequestLogChain() {
        return new DefaultRequestLogChainExecute(
            new String[]{"Authorization", "User-Agent", "X-Request-Id"}
        );
    }
}

配置属性说明

配置项类型默认值说明
krismile.web.request-log.enabledbooleantrue是否启用请求日志 AOP
krismile.web.request-log.debug-print-header-namesString[]["Accept", "Content-Type"]DEBUG 模式打印的请求头名称

不同环境的配置示例

开发环境application-dev.yml):

yaml
krismile:
  web:
    request-log:
      enabled: true  # 开发环境启用,便于调试
      debug-print-header-names:
        - Authorization
        - User-Agent
        - Content-Type
        - X-Request-Id

# 日志级别设置为 DEBUG 以查看详细信息
logging:
  level:
    host.springboot.framework.context.chain: DEBUG

生产环境application-prod.yml):

yaml
krismile:
  web:
    request-log:
      enabled: true  # 生产环境也启用,用于监控和问题排查
      debug-print-header-names:
        - X-Request-Id  # 生产环境只记录必要的请求头

# 日志级别设置为 INFO,不输出 DEBUG 详细信息
logging:
  level:
    host.springboot.framework.context.chain: INFO

测试环境application-test.yml):

yaml
krismile:
  web:
    request-log:
      enabled: false  # 测试环境可以关闭以提高性能

最佳实践

✅ 推荐做法

java
// 1. 使用 @IgnoreRequestLog 排除健康检查等接口
@IgnoreRequestLog
@GetMapping("/health")
public BaseVO health() {
    return R.ok("OK");
}

// 2. 在链式处理器中异步保存日志
@Override
public void afterExecute(HttpServletRequest request, 
                        RequestInfo info, ResponseInfo resp) {
    CompletableFuture.runAsync(() -> {
        operationLogService.save(info, resp);
    });
}

// 3. 设置合理的执行顺序
@Override
public int getOrder() {
    return 100; // 用户信息提取优先级最高
}

// 4. 在 Service 层使用 REQUEST_CONTEXT 获取请求信息
RequestInfo info = RequestInfo.REQUEST_CONTEXT.get();
log.info("当前用户: {}", info.getUserId());

// 5. 对敏感参数进行脱敏
if (arg instanceof SensitiveData data) {
    data.maskSensitiveFields();
}

❌ 不推荐做法

java
// ❌ 不要在链式处理器中执行耗时操作
@Override
public void afterExecute(...) {
    // 同步保存日志会影响请求响应速度
    operationLogService.saveSync(info, resp);
}

// ❌ 不要在 beforeExecute 中抛出异常
@Override
public void beforeExecute(...) {
    if (invalid) {
        throw new RuntimeException("请求无效"); // 会导致切面异常
    }
}

// ❌ 不要记录所有接口日志
@RestController
public class FileController {
    @PostMapping("/upload")
    public BaseVO upload(@RequestParam MultipartFile file) {
        // 文件上传接口应该使用 @IgnoreRequestLog 排除
        // 避免记录大量文件数据
    }
}

// ❌ 不要在 ThreadLocal 使用后忘记清理
try {
    RequestInfo info = RequestInfo.REQUEST_CONTEXT.get();
    // 使用 info
} finally {
    // RequestLogAop 会自动清理,无需手动清理
}

常见问题

Q: 如何禁用请求日志功能?

A: 有多种方式可以禁用:

方式1:配置文件(推荐)

yaml
krismile:
  web:
    request-log:
      enabled: false

方式2:启动参数

bash
java -jar app.jar --krismile.web.request-log.enabled=false

方式3:环境变量

bash
export KRISMILE_WEB_REQUEST_LOG_ENABLED=false
java -jar app.jar

💡 注意:禁用后所有请求日志(包括自定义链式处理器)都不会执行。

Q: 如何记录请求 Body 和响应 Body?

A: 在自定义链式处理器中实现:

java
@Component
public class RequestBodyLogChain implements RequestInfoChainExecute {
    
    @Override
    public void beforeExecute(HttpServletRequest request, RequestInfo info) {
        // 读取请求 Body(需要使用 RequestBodyFilter)
        String body = HttpRequestUtils.getRequestBody(request);
        log.info("请求 Body: {}", body);
    }
    
    @Override
    public void afterExecute(HttpServletRequest request, 
                            RequestInfo info, ResponseInfo resp) {
        // 记录响应结果
        log.info("响应 Body: {}", JSON.toJSONString(resp.getResult()));
    }
}

Q: userId 和 userName 如何自动填充?

A: 在自定义链式处理器中从 Token 解析用户信息:

java
@Component
public class UserInfoChain implements RequestInfoChainExecute, Ordered {
    
    @Autowired
    private JwtService jwtService;
    
    @Override
    public void beforeExecute(HttpServletRequest request, RequestInfo info) {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            UserContext user = jwtService.parseToken(token.substring(7));
            info.setUserId(user.getId());
            info.setUserName(user.getName());
        }
    }
    
    @Override
    public int getOrder() {
        return 50; // 优先级高于默认日志处理器
    }
}

Q: 如何排除所有 GET 请求?

A: 在自定义 AOP 中实现:

java
@Aspect
@Component
public class CustomRequestLogFilter {
    
    @Around("@within(org.springframework.web.bind.annotation.RestController)")
    public Object filter(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = getCurrentRequest();
        
        // 排除所有 GET 请求
        if ("GET".equals(request.getMethod())) {
            // 手动清理 ThreadLocal
            RequestInfo.REQUEST_CONTEXT.remove();
        }
        
        return point.proceed();
    }
}

Q: 多个链式处理器的执行顺序是什么?

A: 通过实现 Ordered 接口控制顺序,数字越小优先级越高:

java
// 优先级: 100 < 200 < 300
@Override
public int getOrder() {
    return 100; // 最先执行
}

执行流程:

  1. 所有 beforeExecute() 按 order 从小到大执行
  2. 执行 Controller 方法
  3. 所有 afterExecute() 按 order 从小到大执行

💡 提示:框架默认的 DefaultRequestLogChainExecute 的 order 为 100。

Released under the Apache-2.0 License