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

相关推荐
李白的粉5 小时前
基于springboot的在线教育系统
java·spring boot·毕业设计·课程设计·在线教育系统·源代码
小马爱打代码6 小时前
SpringBoot原生实现分布式MapReduce计算
spring boot·分布式·mapreduce
iuyou️6 小时前
Spring Boot知识点详解
java·spring boot·后端
一弓虽6 小时前
SpringBoot 学习
java·spring boot·后端·学习
来自星星的猫教授8 小时前
spring,spring boot, spring cloud三者区别
spring boot·spring·spring cloud
乌夷9 小时前
使用spring boot vue 上传mp4转码为dash并播放
vue.js·spring boot·dash
A阳俊yi11 小时前
Spring Boot日志配置
java·spring boot·后端
苹果酱056711 小时前
2020-06-23 暑期学习日更计划(机器学习入门之路(资源汇总)+概率论)
java·vue.js·spring boot·mysql·课程设计
斜月11 小时前
一个服务预约系统该如何设计?
spring boot·后端
Java水解12 小时前
线程池详解:在SpringBoot中的最佳实践
spring boot·后端