请求日志
✨ 自动记录 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 数据模型
核心字段
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
threadId | String | 线程 ID | 1 |
threadName | String | 线程名称 | http-nio-8080-exec-1 |
uri | String | 请求 URI | /api/user/login |
clientIp | String | 客户端真实 IP | 192.168.1.100 |
userAgent | String | User-Agent | Mozilla/5.0... |
os | String | 操作系统 | Windows 10 |
browser | String | 浏览器 | Chrome 120 |
methodType | String | 请求方法类型 | POST |
fullMethodName | String | 完整方法名 | com.example.UserController.login() |
requestMethodArgs | Object[] | 方法参数 | [LoginRequest(...)] |
userId | String | 用户 ID | 1001 (需自定义扩展) |
userName | String | 用户名称 | 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.enabled | boolean | true | 是否启用请求日志 AOP |
krismile.web.request-log.debug-print-header-names | String[] | ["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; // 最先执行
}执行流程:
- 所有
beforeExecute()按 order 从小到大执行 - 执行 Controller 方法
- 所有
afterExecute()按 order 从小到大执行
💡 提示:框架默认的 DefaultRequestLogChainExecute 的 order 为 100。
