一、需求
在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();// 用户IPString clientIp = request.getRemoteAddr();log.info("clientIp:{}", clientIp);// URLString 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();/*** 接口日志配置*/@Datapublic 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与自定义注解相结合,为指定接口提供了灵活的日志记录能力,并通过配置项支持日志记录的开启与关闭,优化了系统性能。实际生产环境中,建议采用异步方式(如消息队列)处理接口日志。