过滤器和拦截器实现Http请求日志记录

过滤器和拦截器实现请求出入参日志记录

前言

项目中记录请求日志是重要的非业务功能,记录请求出入参是常见的方式。一方面,项目本身提供的Http接口需要记录外部的访问记录。另一方面,项目调用第三方服务的Http接口也需要记录请求日志。

请求日志实现方案

方案一:请求日志可以使用AOP实现,给Controller层定义一个切面,统一记录出入参即可。这种方式在我的上一篇博文已经介绍。
方案二:Http请求在项目中本身可以通过过滤器或拦截器处理,而AOP切面自定义成本较高。一个针对Controller层方法的切面并不能保障请求日志的完整性,还要有针对ControllerAdvice层方法的切面。如果项目中调用了多方服务的Http接口,那么需要定义更多的AOP切面。所以利用Tomcat官方或Spring官方提供的过滤器和拦截器处理请求日志也是不错的方案。

过滤器实现记录项目本身Http接口的请求日志

问题:过滤器中记录请求出入参日志,绕不开获取请求流和响应流。众所周知,请求流和响应流只能获取一次。请求处理前,在过滤器中获取请求流,则Spring MVC无法获取RequestBody。请求处理后,在过滤器中获取响应流,则前端无法再次获取ResponseBody。
解决方案:利用Spring提供的ContentCachingRequestWrapper(请求内容缓存类)、ContentCachingResponseWrapper(响应内容缓存类)解决请求流和响应流只能获取一次的问题。这两个类分别是HttpServletRequestWrapper、HttpServletResponseWrapper的子类,他们分别实现了HttpServletRequest、HttpServletResponse接口。
使用:ContentCachingRequestWrapper和ContentCachingResponseWrapper提供了getContentAsByteArray()方法,这个方法可以获取请求流或响应流。但需要注意的是,这个方法也只能调用一次,相当于只缓存了一份,用了就没了。
原理

流对象去向
原请求流Spring MVC获取
缓存请求流过滤器获取
原响应流前端获取
缓存响应流过滤器获取
过滤器定义
import cn.hutool.core.date.StopWatch;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;

@Slf4j
public class LogRequestFilter extends HttpFilter {

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response,
                            FilterChain filterChain) throws IOException, ServletException {
        // 转换为请求缓存对象和响应缓存对象
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
        // 过滤器放行并计时
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        super.doFilter(requestWrapper, responseWrapper, filterChain);
        stopWatch.stop();
        // 获取需要记录的信息
        String ip = requestWrapper.getRemoteHost();
        String path = requestWrapper.getRequestURI();
        byte[] body = requestWrapper.getContentAsByteArray();
        String bodyStr = new String(body, Charset.forName("UTF-8"));
        byte[] resp = responseWrapper.getContentAsByteArray();
        String respStr = new String(resp, Charset.forName("UTF-8"));
        //注意,获取缓存响应流之后默认会同时清楚原响应流,前端将获取不到ResponseBody,要将内容复制回响应体
        responseWrapper.copyBodyToResponse();
        // 记录请求日志
        log.info("request->ip:{},path:{},body:{},exec:{}ms,resp:{}.",
                ip, path, bodyStr, stopWatch.getLastTaskTimeMillis(), respStr);
    }
}
过滤器配置
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<LogMDCFilter> logFilterRegistration() {
        FilterRegistrationBean<LogMDCFilter> registration = new FilterRegistrationBean<>();
        // 注入过滤器
        registration.setFilter(new LogMDCFilter());
        // 拦截规则
        registration.addUrlPatterns("/*");
        // 过滤器名称
        registration.setName("logMDCFilter");
        // 过滤器顺序
        registration.setOrder(0);
        return registration;
    }

    @Bean
    public FilterRegistrationBean<LogRequestFilter> cachingFilterRegistration() {
        FilterRegistrationBean<LogRequestFilter> registration = new FilterRegistrationBean<>();
        // 注入过滤器
        registration.setFilter(new LogRequestFilter());
        // 拦截规则
        registration.addUrlPatterns("/*");
        // 过滤器名称
        registration.setName("logRequestFilter");
        // 过滤器顺序
        registration.setOrder(1);
        return registration;
    }
}

拦截器实现记录第三方服务Http接口的请求日志

注:本文依赖Apche httpclient的RestTemplate实现远程第三方服务的Http接口。

拦截器定义
import cn.hutool.core.date.StopWatch;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

@Slf4j
public class LogHttpClientInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        // 手动执行HttpClient请求
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ClientHttpResponse response = execution.execute(request, body);
        stopWatch.stop();
        // 获取HttpClient请求的请求体和响应体
        Charset charset = StandardCharsets.UTF_8;
        String bodyStr = new String(body, charset);
        String respStr = IOUtils.toString(response.getBody(), charset);
        // 记录HttpClient日志
        log.info("client->path:{},body:{},exec:{}ms,resp:{}.",
                request.getURI(), bodyStr, stopWatch.getLastTaskTimeMillis(), respStr);
        return response;
    }
}
RestTemplate配置类
import com.google.common.collect.Lists;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.Charset;
import java.util.List;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate ChoiceRestTemplate() {
        // boot中可使用RestTemplateBuilder.build创建
        RestTemplate restTemplate = new RestTemplate();
        // StringHttpMessageConverter 默认使用ISO-8859-1编码,此处修改为UTF-8
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        for (HttpMessageConverter<?> converter : messageConverters) {
            if (converter instanceof StringHttpMessageConverter) {
                ((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
            }
        }
        // Interceptors 添加写的 Interceptors
        restTemplate.setInterceptors(Lists.newArrayList(new LogHttpClientInterceptor()));
        // 配置请求工厂 此处替换为BufferingClientHttpRequestFactory
        restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new HttpComponentsClientHttpRequestFactory()));
        return restTemplate;
    }
}

参考:
【RestTemplate】统一记录RestTemplate的调用日志
https://blog.csdn.net/hkk666123/article/details/116282274
Spring MVC系列(16)-使用HttpServletRequestWrapper 解决流只能读取一次的问题
https://yunyanchengyu.blog.csdn.net/article/details/122102362
SpringMVC requestBody和responseBody重复获取
https://blog.csdn.net/huangliuyu00/article/details/120517645

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud Gateway 提供了一种简单的方式来实现请求日志记录,可以使用自定义的 GatewayFilter 实现这一功能。下面是一个示例代码,可以在请求进入网关时打印请求信息: ```java @Component public class RequestLoggingFilter implements GatewayFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); log.info("Request URI: {}", request.getURI()); log.info("Request method: {}", request.getMethod()); log.info("Request headers: {}", request.getHeaders()); return chain.filter(exchange); } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } } ``` 在上述示例中,我们创建了一个自定义的 GatewayFilter 实现类 RequestLoggingFilter,它实现了 GatewayFilter 接口和 Ordered 接口。在 filter 方法中,我们获取了请求对象 ServerHttpRequest,并打印了请求 URI、请求方法和请求头信息。最后,我们通过 chain.filter(exchange) 将请求传递给下一个过滤器。 为了确保 RequestLoggingFilter 在所有其他过滤器之前执行,我们将其 getOrder 方法的返回值设置为 Ordered.LOWEST_PRECEDENCE。 要启用 RequestLoggingFilter,只需将其添加到 Spring Cloud Gateway 的路由配置中,如下所示: ```java @Configuration public class GatewayConfig { @Bean public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r.path("/api/**") .filters(f -> f.filter(new RequestLoggingFilter())) .uri("http://localhost:8080")) .build(); } } ``` 在上述示例中,我们将 RequestLoggingFilter 添加到了路由配置中,并指定了要拦截的路径和要转发到的目标 URI。现在,当请求进入网关时,我们就可以看到请求信息被打印到日志中了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值