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(),存在着流不可重复读的问题。下一小节我们将巧妙的通过流信息缓存来解决这个问题,大家加油!

相关推荐
isolusion1 小时前
Springboot的创建方式
java·spring boot·后端
Yvemil71 小时前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
星河梦瑾3 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
计算机学长felix4 小时前
基于SpringBoot的“交流互动系统”的设计与实现(源码+数据库+文档+PPT)
spring boot·毕业设计
.生产的驴4 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲4 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
撒呼呼4 小时前
# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)
数据库·spring boot·spring·mvc·springboot
因我你好久不见4 小时前
springboot java ffmpeg 视频压缩、提取视频帧图片、获取视频分辨率
java·spring boot·ffmpeg
Yvemil75 小时前
《开启微服务之旅:Spring Boot Web开发》(二)
前端·spring boot·微服务
计算机学长felix5 小时前
基于SpringBoot的“旅游管理系统”的设计与实现(源码+数据库+文档+PPT)
spring boot·毕业设计