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

相关推荐
小码的头发丝、24 分钟前
Spring Boot 注解
java·spring boot
午觉千万别睡过27 分钟前
RuoYI分页不准确问题解决
spring boot
2301_811274311 小时前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
编程重生之路2 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端
politeboy2 小时前
k8s启动springboot容器的时候,显示找不到application.yml文件
java·spring boot·kubernetes
世间万物皆对象8 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
qq_174482857510 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
代码小鑫12 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖12 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
周全全12 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php