一、需求
在Spring Boot应用中,实现接口请求日志记录功能,要求能够记录包括请求方法、接口路径及请求参数等核心信息,并提供灵活的开关配置。
二、方案概述
采用AOP(面向切面编程)结合自定义注解的方式实现。
具体步骤如下:
- 创建自定义注解
@ApiLog
,标记需要记录日志的接口。 - 通过AOP实现一个切面,对被
@ApiLog
注解修饰的方法进行前置处理,记录其请求相关信息。 - 提供配置项开关,控制是否开启接口日志记录。
- 推荐使用消息队列(例如RocketMQ)异步处理接口日志,以提升性能,但本示例仅展示简单的日志打印。使用消息队列的方法是:将接口的请求日志发送到消息队列里,由专门的日志记录服务器去处理,比如写入专门的数据库。这样可以减少接口的同步处理的时间,避免客户端等待时间过长,提升总体性能。
三、核心代码
自定义注解:@ApiLog
package com.example.core.log.annotation;
import java.lang.annotation.*;
/**
* 接口日志注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog {
}
切面类:ApiLogAspect
package com.example.core.log.aspect;
import com.example.core.property.BaseFrameworkConfigProperties;
import com.example.core.util.JsonUtil;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
@Aspect
@Order(20)
@Component
public class ApiLogAspect {
@Value("${spring.application.name:}")
private String applicationName;
private final BaseFrameworkConfigProperties properties;
public ApiLogAspect(BaseFrameworkConfigProperties properties) {
this.properties = properties;
}
// 定义一个切点:所有被 ApiLog 注解修饰的方法会织入advice
@Pointcut("@annotation(com.example.core.log.annotation.ApiLog)")
private void pointcut() {
}
// Before表示 advice() 将在目标方法执行前执行
@Before("pointcut()")
public void advice(JoinPoint joinPoint) {
if (!properties.getApiLog().isEnabled()) {
return;
}
log.info("\n-------------------- 接口日志,开始 --------------------");
log.info("applicationName:{}", applicationName);
// 获取请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 用户IP
String clientIp = request.getRemoteAddr();
log.info("clientIp:{}", clientIp);
// URL
String requestURL = request.getRequestURL().toString();
log.info("url:{}", requestURL);
// 请求方法
String requestMethod = request.getMethod();
log.info("requestMethod:{}", requestMethod);
// 接口路径
String path = request.getServletPath();
log.info("path:{}", path);
}
// 控制器方法参数列表
Object[] args = joinPoint.getArgs();
// 获取有效的控制器方法参数列表
List<Object> validArgs = getValidArguments(args);
log.info("args:{}", JsonUtil.toJson(validArgs));
// 方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
log.info("methodSignature:{}", methodSignature);
// 方法参数名称列表
String[] parameterNames = methodSignature.getParameterNames();
log.info("parameterNames:{}", JsonUtil.toJson(parameterNames));
// 获取接口的注解
Operation operation = methodSignature.getMethod().getAnnotation(Operation.class);
if (operation != null) {
// 接口概述
String summary = operation.summary();
log.info("summary:{}", summary);
// 接口描述
String description = operation.description();
log.info("description:{}", description);
}
log.info("\n-------------------- 接口日志,结束 --------------------\n");
}
/**
* 获取有效的控制器方法参数列表
* <p>
* 排除 HttpServletRequest 和 HttpServletResponse 参数。
* <p>
* HttpServletRequest 参数,会阻塞线程,抛出异常 NestedServletException-OutOfMemoryError。
* <p>
* HttpServletResponse 参数,会抛出异常 NestedServletException-StackOverflowError。
*/
private List<Object> getValidArguments(Object[] args) {
return Stream.of(args).filter(this::isValidArgument).collect(Collectors.toList());
}
private Boolean isValidArgument(Object arg) {
return isNotHttpServletRequest(arg) && isNotHttpServletResponse(arg);
}
/**
* 不是 HttpServletRequest
* <p>
* HttpServletRequest 参数,会阻塞线程,会抛出如下异常:
* org.springframework.web.util.NestedServletException:
* Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space
*/
private Boolean isNotHttpServletRequest(Object arg) {
return !(arg instanceof HttpServletRequest);
}
/**
* 不是 HttpServletResponse
* <p>
* HttpServletResponse 参数,会抛出如下异常:
* org.springframework.web.util.NestedServletException:
* Handler dispatch failed; nested exception is java.lang.StackOverflowError
*/
private Boolean isNotHttpServletResponse(Object arg) {
return !(arg instanceof HttpServletResponse);
}
}
日志开关配置
配置类:BaseFrameworkConfigProperties
package com.example.core.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* BaseFramework 配置文件
*
* @author songguanxun
* 2019/08/27 15:40
* @since 1.0.0
*/
@Data
@Component
@ConfigurationProperties(prefix = "base-framework")
public class BaseFrameworkConfigProperties {
/**
* 接口日志配置
*/
private ApiLog apiLog = new ApiLog();
/**
* 接口日志配置
*/
@Data
public static class ApiLog {
/**
* 是否开启接口日志
*/
private boolean enabled = false;
}
}
配置文件:application.yml
# 自定义配置
base-framework:
api-log:
enabled: false
四、测试案例一:查询用户列表
4.1 测试代码
package com.example.web.user.controller;
import com.example.core.log.annotation.ApiLog;
import com.example.core.model.PageQuery;
import com.example.web.model.query.UserQuery;
import com.example.web.model.vo.UserVO;
import com.example.web.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("users")
@Tag(name = "用户管理")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@ApiLog
@GetMapping
@Operation(summary = "查询用户列表", description = "支持通过”姓名“和”手机号码“筛选用户")
public List<UserVO> listUsers(@Valid UserQuery userQuery, PageQuery pageQuery) {
log.info("查询用户列表。userQuery={},pageQuery={}", userQuery, pageQuery);
return userService.listUsers(userQuery);
}
}
package com.example.web.model.query;
import com.example.core.constant.RegexConstant;
import com.example.core.validation.phone.query.MobilePhoneQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springdoc.api.annotations.ParameterObject;
@Data
@ParameterObject
@Schema(name = "用户Query")
public class UserQuery {
@Schema(description = "姓名", example = "张三")
private String name;
@MobilePhoneQuery
@Schema(description = "手机号码", example = "18612345678", pattern = RegexConstant.NUMBERS, maxLength = 11)
private String mobilePhone;
}
package com.example.core.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.FieldNameConstants;
import org.springdoc.api.annotations.ParameterObject;
@Data
@FieldNameConstants
@ParameterObject
@Schema(name = "分页参数Query")
public class PageQuery {
@Schema(description = "当前页码", type = "Integer", defaultValue = "1", example = "1", minimum = "1")
private Integer pageNumber = 1;
@Schema(description = "每 1 页的数据量", type = "Integer", defaultValue = "10", example = "10", minimum = "1", maximum = "100")
private Integer pageSize = 10;
}
4.2 接口调用效果
4.3 控制台日志
五、排除HttpServletRequest和HttpServletResponse参数
测试 HttpServletRequest、HttpServletResponse 和 HttpSession,是否在接口日志处理时堵塞线程或抛出异常?
5.1 原因
获取有效的控制器方法参数列表时,需要排除 HttpServletRequest 和 HttpServletResponse 参数。原因如下:
- 打印 HttpServletRequest 参数,会阻塞线程,抛出异常 NestedServletException-OutOfMemoryError。
- 打印 HttpServletResponse 参数,会抛出异常 NestedServletException-StackOverflowError。
HttpSession能够正常获取并打印日志,不需要特殊处理。
5.2 核心代码示例
下面图片中圈中的部分,就是排除HttpServletRequest和HttpServletResponse参数的核心代码。
5.3 测试代码
package com.example.web.api.log.controller;
import com.example.core.log.annotation.ApiLog;
import com.example.core.model.PageQuery;
import com.example.web.model.query.UserQuery;
import com.example.web.model.vo.UserVO;
import com.example.web.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/log")
@Tag(name = "接口日志")
public class ApiLogController {
private final UserService userService;
public ApiLogController(UserService userService) {
this.userService = userService;
}
@ApiLog
@GetMapping(path = "users")
@Operation(summary = "查询用户列表", description = "测试 HttpServletRequest、HttpServletResponse 和 HttpSession,是否在接口日志处理时堵塞线程或抛出异常")
public List<UserVO> listUsers(@Valid UserQuery userQuery, PageQuery pageQuery,
HttpServletRequest request, HttpServletResponse response, HttpSession session) {
log.info("查询用户列表。userQuery={},pageQuery={}", userQuery, pageQuery);
return userService.listUsers(userQuery);
}
}
5.4 正常调用效果
5.5 打印HttpServletRequest参数,会阻塞线程,抛出异常
测试不排除控制器方法中的HttpServletRequest参数,直接打印的效果
打印HttpServletRequest 参数,会阻塞线程很长一段时间,大约几十秒,然后会抛出如下异常:
org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space
接口阻塞
抛出异常NestedServletException-OutOfMemoryError
接口响应
异常统一处理后,响应给前端,耗时50多秒。
5.6 打印HttpServletResponse参数,会抛出异常
测试不排除控制器方法中的HttpServletResponse参数,直接打印的效果
打印 HttpServletResponse 参数,会抛出如下异常:
org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.StackOverflowError
抛出异常NestedServletException-StackOverflowError
接口响应
异常统一处理后,响应给前端。
六、总结
本文实现了基于Spring Boot的接口请求日志记录方案,通过AOP与自定义注解相结合,为指定接口提供了灵活的日志记录能力,并通过配置项支持日志记录的开启与关闭,优化了系统性能。实际生产环境中,建议采用异步方式(如消息队列)处理接口日志。