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格式时,就借助objectMapper的readTree方法,传入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);
    }
}

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

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

完善CustomErrorAttributes异常输出

在CustomErrorAttributes中加一段逻辑:

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

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

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

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

相关推荐
谢亮_vipxieliang8 小时前
Spring 事务失效的常见场景
java·开发语言·数据库·spring boot
专业程序开发源8 小时前
springboot简历管理系统81389-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
专业程序开发源9 小时前
springboot社区养老系统44071-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·php·课程设计
余槐i9 小时前
数据没回滚也不报错:@Transactional 自调用失效的 3 种复现与修复
spring boot·mysql·多线程·spring 事务
小蒜学长10 小时前
基于SpringBoot的佳新超市管理系统设计与实现系统(代码+数据库+LW)
java·数据库·spring boot·后端·佳新超市管理系统
vx_Biye_Design12 小时前
springboot高校选课系统82776-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express
专业程序开发源14 小时前
SSM校园拍摄交流服务平台36936-计算机课程设计、毕业设计
java·spring boot·后端·python·elasticsearch·php·课程设计
AI人工智能+电脑小能手15 小时前
Java 企业级工具类全家桶-74-JSON序列化工具类
spring boot·jackson·jdk 17·objectmapper·java工具类·json序列化·dto互转
ZealSinger16 小时前
Boot4升级PostConstruct不跑了
java·spring boot·spring·升级迁移
专业程序开发源17 小时前
django新闻推荐系统70655-计算机课程设计、毕业设计
java·javascript·spring boot·后端·python·django·课程设计