springboot项目添加请求url及请求入参日志
前言:
在开发的过程中若没有请求日志是比较难排查定位bug是在请求那个接口时抛出的异常。springboot默认是不会把请求日志打印出来的,这就需要我们自己手动写拦截器打印请求日志。下面介绍打印请求日志及请求入参步骤。
目录
方式一:只打印请求日志
一,创建拦截器
二,注册拦截器
三,yml文件中配置日志级别
四,演示
方式二:打印请求日志和请求入参
一,创建拦截器
1.1脱敏工具类:
二,自定义可重复读流
三,yml文件中配置日志级别
四,演示
方式一:只打印请求日志
一,创建拦截器
@Component
public class LogUrlInterceptor implements HandlerInterceptor {private static final Logger log = LoggerFactory.getLogger(LogUrlInterceptor.class);@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler){if (handler instanceof HandlerMethod) {// 请求url日志log.info(">>> {} {}", request.getMethod(), request.getRequestURL());}return true;}}
二,注册拦截器
如果你的项目中已有配置类实现了 implements WebMvcConfigurer类,那么就直接在那个类里面添加注册即可。
直接添加以下代码:
@Autowiredprivate LogUrlInterceptor logUrlInterceptor; @Overridepublic void addInterceptors(InterceptorRegistry registry){registry.addInterceptor(logUrlInterceptor).addPathPatterns("/**");//添加这一行即可,其他的不变....}
如果你的项目中没有实现WebMvcConfigurer类,那么就将以下以下代码直接复制粘贴到你的项目中去:
package com.example.demo.config;import com.example.demo.interceptor.LoggingInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate LoggingInterceptor loggingInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loggingInterceptor).addPathPatterns("/**"); // 拦截所有请求}
}
三,yml文件中配置日志级别
此配置已精确到类中,不必担心影响到其他日志行为,对项目零入侵
logging:level:com.zqd.framework.interceptor.LogUrlInterceptor: info #你自己的全量路径类名
四,演示
重启项目,自己随便请求一个接口,便可在控制台日志中出现请求接口的url
如果只是想要打印请求url日志,到这里就已经实现了。
下面为拓展部分,继续介绍打印请求入参的方式
方式二:打印请求日志和请求入参
一,创建拦截器
@Component
public class LogUrlInterceptor implements HandlerInterceptor {private static final Logger log = LoggerFactory.getLogger(LogUrlInterceptor.class);@Value("${spring.profiles.active}")private String env;@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) {if (handler instanceof HandlerMethod) {// 请求url日志log.info(">>> {} {}", request.getMethod(), request.getRequestURL());StringBuilder sb = new StringBuilder(128);// 1. GET 参数if (StringUtils.hasText(request.getQueryString())) {//铭感入参脱敏String query = desensitize(request.getQueryString());sb.append('?').append(query);}// 2. POST/PUT/DELETE 的 json/form 文本if (request instanceof CacheBodyHttpServletRequest) {String body = ((CacheBodyHttpServletRequest) request).getBody();if (StringUtils.hasText(body)) {//铭感入参脱敏body = desensitize(body);// 防太长sb.append(" | body=").append(body, 0, Math.min(body.length(), 500));}}log.info(">>> {}", sb);}return true;}/*** 正式环境敏感参数脱敏*/private String desensitize(String raw) {//正式环境脱敏if(StrUtil.equals("prod", env)){//手机号脱敏return DesensitizedUtil.desensitizePhone(raw);}return raw;}
以上代码做了根据环境进行铭感入参数据脱敏,比如手机号,银行卡号,身份证号,这些数据是不能明文传输的,否者会泄露用户隐私。在测试/开发环境中为了方便调试可以明文打印,但是在正式环境中需要脱敏,所以以上代码会根据是否是正式环境进行脱敏。而你还需要一个脱敏工具类,
以下是脱敏工具类,你复制粘贴即可:
1.1脱敏工具类:
import java.util.regex.Pattern;/*** 脱敏工具类** @author ruoyi*/
public class DesensitizedUtil
{/* ================ 预编译正则,避免每次 new ================ */private static final Pattern MOBILE_PATTERN =Pattern.compile("(?<![0-9])(1[3-9]\\d)\\d{4}(\\d{4})(?![0-9])");/** 18 位身份证:前 4 + 中间 10 位掩码 + 后 4 */private static final Pattern ID_CARD_PATTERN =Pattern.compile("(?<![0-9])(\\d{4})\\d{10}(\\d{4})(?![0-9Xx])");/** 银行卡(10~30 位):前 6 + 中间 * + 后 4 */private static final Pattern BANK_CARD_PATTERN =Pattern.compile("(?<![0-9])(\\d{6})\\d+(\\d{4})(?![0-9])");/*** 包含手机号JSON串脱敏*/public static String desensitizePhone(String raw) {if (raw == null || raw.isEmpty()) {return raw;}// 手机号return MOBILE_PATTERN.matcher(raw).replaceAll("$1****$2");}/*** 包含身份证JSON串脱敏*/public static String desensitizeIdCard(String raw) {if (raw == null || raw.isEmpty()) {return raw;}// 身份证return ID_CARD_PATTERN.matcher(raw).replaceAll("$1**********$2");}/*** 包含银行卡JSON串脱敏*/public static String desensitizeBankCard(String raw) {if (raw == null || raw.isEmpty()) {return raw;}// 银行卡号return BANK_CARD_PATTERN.matcher(raw).replaceAll("$1****$2");}}
二,自定义可重复读流
为什么需要自定义可重复读流?因为post方式请求的时候,请求体入参是以流的形式传输的,流request.getInputStream()
只能读一次 “流只能读一次”这条铁律,上面为了打印日志读完就被“消耗”了,后面 controller 会拿不到 body → 400/500。所以我们需要自定义可重复读流
import org.springframework.util.StreamUtils;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;public class CacheBodyHttpServletRequest extends HttpServletRequestWrapper {private final byte[] cachedBody;public CacheBodyHttpServletRequest(HttpServletRequest request) throws IOException {super(request);cachedBody = StreamUtils.copyToByteArray(request.getInputStream());}@Overridepublic ServletInputStream getInputStream() {ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);return new ServletInputStream() {@Override public boolean isFinished() { return byteArrayInputStream.available() == 0; }@Override public boolean isReady() { return true; }@Override public void setReadListener(ReadListener listener) {}@Override public int read() { return byteArrayInputStream.read(); }};}@Overridepublic BufferedReader getReader() throws UnsupportedEncodingException {return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));}public String getBody() {return new String(cachedBody, java.nio.charset.StandardCharsets.UTF_8);}
}
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;@Component
public class CacheBodyFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request;if (HttpMethod.POST.matches(req.getMethod()) ||HttpMethod.PUT.matches(req.getMethod()) ||HttpMethod.DELETE.matches(req.getMethod())) {req = new CacheBodyHttpServletRequest(req);}chain.doFilter(req, response);}}
三,yml文件中配置日志级别
logging:level:com.zqd.framework.interceptor.LogUrlInterceptor: info #你自己的全量路径类名
四,演示
重启项目随便请求一个接口,可以看到请求体入参也打印出来了:
到此结束!!!