Spring Boot 3.x Filter实战:记录请求日志

上一篇:Spring Boot 3.x Web单元测试最佳实践

下一篇:Spring Boot 3.x Web MVC实战:实现流缓存的request

前面我们在《Spring Boot 3.x Rest API最佳实践之统一响应结构》中学习响应的统一拦截处理,顺带完成了响应结果的记录;而对于请求内容咱们也必须进行日志记录,以确保排查问题时有据可循。为此,本小节咱们利用filter组件来实现这一需求。如果觉得对你有帮助,记得点赞收藏,关注小卷,后续更精彩!

文章目录

filter开发

因为统一请求日志记录属于基础功能,我们在 com.juan.demo.common基础包下来创建,以便后续可以将其抽出来做成一个common子模块进行维护。

这里我们从OncePerRequestFilter继承,这样可以确保一个请求内部无论经过多少次转发,始终会被拦截一次。

java 复制代码
package com.juan.demo.common.web.filter;

import ...

@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("============ {}:{}", request.getMethod(), request.getRequestURL());

        filterChain.doFilter(request, response);
    }
}

这里我们先做最简单的实现:仅打印下请求方法和请求路径,然后继续将请求放行。

filter配置

com.juan.demo.common.web包下创建一个@Configuration修饰的配置类,在内部通过@Bean修饰的方法来创建相关bean,包括了filter本身的bean和用于filter bean注册和配置的FilterRegistrationBean类型的bean

java 复制代码
package com.juan.demo.common.web;

import ...

@Configuration
public class WebConfig {

    @Bean
    public RequestLogFilter requestLogFilter() {
        // new一个filter bean实例
        return new RequestLogFilter();
    }

    @Bean
    public FilterRegistrationBean<RequestLogFilter> requestLogFilterRegistrationBean() {
        FilterRegistrationBean<RequestLogFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(requestLogFilter());
        registrationBean.setName("requestLogFilter");
        registrationBean.addUrlPatterns("/*"); // todo 暂时对所有的请求拦截
        registrationBean.setOrder(1);
        return registrationBean;
    }

}

运行先前开发的web单元测试用例,看到请求被RequestLogFilter拦截到了:

实现logParams方法

这里主要考察request.getParameterMap()方法的使用。注意,它返回的value是一个字符串数组,一个参数key可以对应多个value,在输出日志时,用了org.apache.commons:commons-lang3依赖中的StringUtils工具类的join(...)方法实现数组内容的拼接。

java 复制代码
package com.juan.demo.common.web.filter;

import ...

@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("============ {}:{}", request.getMethod(), request.getRequestURL());

        logParams(request);

        filterChain.doFilter(request, response);
    }

    private void logParams(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        int size = parameterMap.size();
        if (size == 0) return;
        StringBuilder paramBuilder = new StringBuilder();
        Set<String> paramNames = parameterMap.keySet();
        int index = 0;
        for (String paramName : paramNames) {
            paramBuilder.append(paramName).append("=").append(StringUtils.join(parameterMap.get(paramName), ","));
            if (index < size - 1) {
                paramBuilder.append(", ");
            }
            index++;
        }
        log.info("============ params:{}", paramBuilder);
    }

}

测试结果:

同样,客户端提交的formData形式的数据,也能被拦截输出:

实现logRequestBody方法

实现logRequestBody方法,逻辑是:判断请求内容类型为json格式时,就借助objectMapperreadTree方法,传入request.getInputStream()方法获取的流信息,从而读取到树形的JsonNode信息,再序列化为json信息。

java 复制代码
package com.juan.demo.common.web.filter;

import ...

@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {
    @Resource
    private ObjectMapper objectMapper;
    
    @Override
    protected void doFilterInternal(...) ... {
        ...
        logRequestBody(request);
        filterChain.doFilter(request, response);
    }
    ...
    @SneakyThrows
    private void logRequestBody(HttpServletRequest request) {
        if (checkContentType(request, MediaType.APPLICATION_JSON_VALUE)) {
            log.info("============ body:{}", objectMapper.readTree(request.getInputStream()));
        }
    }

    private boolean checkContentType(HttpServletRequest request, String contentType) {
        return StringUtils.startsWith(request.getContentType(), contentType);
    }
}

测试下添加购物车,发现报了400http状态码,并提示错误信息:

再看看日志,并没有抛出异常堆栈信息:

完善CustomErrorAttributes异常输出

CustomErrorAttributes中加一段逻辑:

再测试就发现控制台抛出的错误了:

body信息确实打印到日志了,但是spring web模块在controller层进行请求信息转换成对象时,框架内部会再次调用request.getInputStream()就报出这样的错误了。

存在的问题:流不可重复读

很显然,这里无法两次调用request.getInputStream(),存在着流不可重复读的问题。下一小节我们将巧妙的通过流信息缓存来解决这个问题,大家加油!

相关推荐
Sam_Deep_Thinking11 小时前
Spring Boot 的启动原理是什么?
java·spring boot·后端
屋外雨大,惊蛰出没11 小时前
深入浅出Spring Boot
java·spring boot·ioc·aop
协享科技12 小时前
Spring Boot 与 Go 双服务架构实践:从单体拆分到通信设计
java·人工智能·spring boot·后端·架构·golang·ai编程
小林敲代码778814 小时前
记录一下IDEA中很多变量变色的方案
java·开发语言·spring boot·idea
Flittly14 小时前
【AgentScope Java新手村系列】(3)工具系统
java·spring boot·spring
Flittly15 小时前
【AgentScope Java新手村系列】(2)第一个Agent-基础对话
java·spring boot·spring·ai
小二·15 小时前
Spring Boot 3 + Vue 3 全栈开发实战
vue.js·spring boot·后端
码农飞哥16 小时前
Spring Boot 多角色权限隔离实战:接口层+路由层+UI层三层防御,杜绝生产数据泄露
spring boot·状态模式·架构设计·系统设计·权限控制
SuperArc199916 小时前
SpringBoot+Slf4j+Log4j2+mybatis 日志整合
spring boot·mybatis·log4j2·slf4j·日志整合
lfwh17 小时前
探针程序技术解析:基于 Spring Boot 非 Web 模式的云服务监控告警系统
前端·spring boot·后端