环境:SpringBoot3.2.5
1. 简介
API接口记录请求相关日志是确保系统安全性、可维护性以及性能监控的重要措施之一。通过记录API请求的详细信息,包括请求时间、来源IP地址、请求方法(GET、POST等)、请求参数、响应状态码及响应时间等,开发者能够有效追踪错误源头、分析用户行为模式、优化系统性能,并保障数据安全。
接下来,我将介绍七种API接口请求日志的记录方式:
图片
准备接口
@RestController
@RequestMapping("/api")
public class ApiController {
private final static Logger logger = LoggerFactory.getLogger(ApiController.class) ;
@GetMapping("/query/{category}")
public ResponseEntity<Object> query(@PathVariable String category, @RequestParam String keyword) {
logger.info("查询数据, 分类: {}, 关键词: {}", category, keyword) ;
return ResponseEntity.ok("success") ;
}
}
接下来的示例我们都将通过该接口进行测试。
2. 实战案例
2.1 Filter过滤
通过Filter方式可以拦截并记录HTTP请求和响应的组件。通过Filter技术,开发人员可以在请求到达目标资源之前或响应返回客户端之前进行日志记录,从而实现访问控制和请求预处理等功能。
如果你考虑使用Filter,那么非常幸运你不需要自己去实现,Spring MVC给我们提供了此功能的实现,如下类图:
图片
这里我们可以直接注册CommonRequestLoggingFilter过滤器。
@Bean
FilterRegistrationBean<Filter> loggingFilter() {
FilterRegistrationBean<Filter> reg = new FilterRegistrationBean<>() ;
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter() ;
// 是否记录客户端信息
filter.setIncludeClientInfo(true) ;
// 记录请求header数据
filter.setIncludeHeaders(true) ;
// 记录请求body内容
filter.setIncludePayload(true) ;
// 是否记录查询字符串
filter.setIncludeQueryString(true) ;
reg.setFilter(filter) ;
reg.addUrlPatterns("/api/*") ;
return reg ;
}
这还不够,还需要配合下面日志级别配置:
logging:
level:
'[org.springframework.web.filter]': debug
接下来,访问/api/query/book?keyword=java接口
图片
成功记录了请求的相关数据。
2.2 HandlerInterceptor记录日志
HandlerInterceptor用于拦截请求处理也是非常的实用。它允许开发者在请求到达控制器之前、之后或发生异常时执行特定逻辑,从而实现对请求处理的全面监控和日志记录。
定义拦截器
@Component
public class LoggingRequestInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LoggingRequestInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
long startTime = Instant.now().toEpochMilli();
String time = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) ;
logger.info("Request URL::" + request.getRequestURL().toString() + ":: StartTime=" + time);
request.setAttribute("startTime", startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
long startTime = (Long) request.getAttribute("startTime");
logger.info("Request URL::" + request.getRequestURL().toString() + ":: TimeTaken="
+ (Instant.now().toEpochMilli() - startTime));
}
}
注册拦截器
@Component
public class InterceptorConfig implements WebMvcConfigurer {
private final LoggingRequestInterceptor loggingRequestInterceptor ;
public InterceptorConfig(LoggingRequestInterceptor loggingRequestInterceptor) {
this.loggingRequestInterceptor = loggingRequestInterceptor;
}
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingRequestInterceptor).addPathPatterns("/api/**") ;
}
}
请求API接口
2.3 使用AOP技术
通过AOP记录日志是一种强大的技术,它允许开发者在不修改业务逻辑代码的情况下,将日志记录等横切关注点与业务逻辑分离。AOP能够自动拦截方法调用,并记录方法执行前后的信息,为系统提供全面的日志支持。
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Log {
/**模块*/
String module() default "" ;
/**具体操作说明*/
String desc() default "" ;
}
定义切面
@Component
@Aspect
public class LogAspect {
@Pointcut("@annotation(log)")
private void recordLog(Log log) {}
@Around("recordLog(log)")
public Object logAround(ProceedingJoinPoint pjp, Log log) throws Throwable {
String module = log.module();
String desc = log.desc();
long uid = System.nanoTime() ;
String threadName = Thread.currentThread().getName();
System.err.printf("%s - 【%d】 模块: %s, 操作: %s, 请求参数: %s%n", threadName, uid, module, desc, Arrays.toString(pjp.getArgs())) ;
Object ret = pjp.proceed() ;
System.err.printf("%s - 【%d】 返回值: %s%n", threadName, uid, ret) ;
return ret ;
}
}
修改Controller接口
@Log(module = "综合查询", desc = "查询商品信息")
@GetMapping("/query/{category}")
public ResponseEntity<Object> query(@PathVariable String category, @RequestParam String keyword)
请求API接口
图片
2.4 基于Servlet请求事件机制
默认情况下,当一个请求到达后,Spring MVC底层会发布一个事件ServletRequestHandledEvent,我们只需要监听该事件也是可以获取到详细的请求/响应信息。
自定义事件监听器
@Component
public class RequestEventListener implements ApplicationListener<ServletRequestHandledEvent> {
private static final Logger logger = LoggerFactory.getLogger(RequestEventListener.class) ;
@Override
public void onApplicationEvent(ServletRequestHandledEvent event) {
logger.info("请求信息: {}", event) ;
}
}
请求API接口
图片
你可以通过ServletRequestHandledEvent事件对象获取具体的明细信息
图片
2.5 使用第三方Logbook组件
Logbook是一个可扩展的Java库,能够为不同的客户端和服务器端技术实现完整的请求和响应日志记录。它允许开发者记录应用程序接收或发送的任何HTTP流量。
引入依赖
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>3.10.0</version>
</dependency>
配置文件
logging:
level:
'[org.zalando.logbook.Logbook]': TRACE
请求API接口
图片
2.6 自定义HandlerMethod
如果你仅仅是针对Controller接口进行日志记录处理,那么强烈推荐此种方式。
2.7 三方API接口调用
通常我们会使用RestTemplate/RestClient进行第三方接口的调用;如果要记录此种情况的日志信息,那么我们需要自定义ClientHttpRequestInterceptor拦截器。
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
logRequest(request, body) ;
ClientHttpResponse response = execution.execute(request, body);
logResponse(response);
return response;
}
private void logRequest(HttpRequest request, byte[] body) throws IOException {
if (log.isDebugEnabled()) {
log.debug("===========================request begin================================================");
log.debug("URI : {}", request.getURI());
log.debug("Method : {}", request.getMethod());
log.debug("Headers : {}", request.getHeaders());
log.debug("Request body: {}", new String(body, "UTF-8"));
log.debug("==========================request end================================================");
}
}
private void logResponse(ClientHttpResponse response) throws IOException {
if (log.isDebugEnabled()) {
log.debug("============================response begin==========================================");
log.debug("Status code : {}", response.getStatusCode());
log.debug("Status text : {}", response.getStatusText());
log.debug("Headers : {}", response.getHeaders());
log.debug("Response body: {}", StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()));
log.debug("=======================response end=================================================");
}
}
}
RestTemplate配置拦截器
@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
RestTemplate restTemplate = builder
.additionalInterceptors(
new LoggingClientHttpRequestInterceptor())
.build() ;
return restTemplate ;
}
日志打印情况如下:
图片
如果使用的RestClient,那么可以同一个拦截器,配置如下:
@Bean
RestClient restClient() {
return RestClient
.builder()
.requestInterceptor(
new LoggingClientHttpRequestInterceptor())
.build() ;
}