概述

  • 请求日志几乎是所有大型企业级项目的必要的模块,请求日志对于我们来说后期在项目运行上线一段时间用于排除异常、请求分流处理、限制流量等。请求日志一般都会记录请求参数、请求地址、请求状态(Status Code)、SessionId、请求方法方式(Method)、请求时间、客户端IP地址、请求返回内容、耗时等等。如果你得系统还有其他个性化的配置,也可以完成记录。
    记录请求参数时,由于servlet.getInputStream的数据只能读取一次,因此需要先把数据缓存下来,构造返回流,保证之后的Controller可以正常读取到请求体的数据。

方案思路

  1. 封装HttpServletRequest请求类,改类在构造方法中将请求的输入流中的数据缓存了起来,保证之后的处理可以重复读取输入流中的数据。
  2. 实现过滤器,把上步封装的请求类传下去,保证Controller可以正常读取输入流中的数据。
  3. 添加拦截器,读取输入流中的数据。
  4. 读取返回参数。

封装HttpServletRequest请求

package com.example.demo.intercept;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

/**
 * @author 
 * @date 封装HttpServletRequest请求
 */
public class RequestWrapper extends HttpServletRequestWrapper {

    private final String body;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesBody = -1;
                while ((bytesBody = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesBody);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException e) {

        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        body = stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    public String getBody() {
        return this.body;
    }
}

把可重复读请求体通过过滤器往下传

防止请求流读取一次后就没有了,之后的不管是过滤器、拦截器、处理器都是读的已经缓存好的数据,实现可重复读。

package com.example.demo.intercept;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @author 
 * @date 防止请求流读取一次后就没有了
 */
@Component
@WebFilter(urlPatterns = "/**")
public class RecordChannelFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest request = null;
        if (servletRequest instanceof HttpServletRequest) {
            request = new RequestWrapper((HttpServletRequest) servletRequest);
        }
        if (request ==null){
            //防止流读取一次就没有了,将流传递下去
            filterChain.doFilter(servletRequest,servletResponse);
        }else {
            filterChain.doFilter(request,servletResponse);
        }
    }

    @Override
    public void destroy() {

    }
}

记录入参日志

实现入参记录拦截器

通过拦截器的方式实现用户入参记录。

package com.example.demo.intercept;

import com.alibaba.fastjson.JSONObject;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import io.jsonwebtoken.Claims;

import javax.annotation.Resource;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 
 * @date 记录用户操作记录入参
 */
@Component
public class OperationLogInterceptor implements HandlerInterceptor {
    /**
     * Jwt secert串,需要与加密token的秘钥一致
     */
    public static final String JWT_SECERT = "23142d7a9s7d66970ad07d8sa";

    /**
     * 需要记录的接口URL
     */
    private static List<String> pathList = new ArrayList<>();

    static {
        pathList.add("/mdms/model");
    }

    @Resource
    private FunctionDOMapper functionDOMapper;//菜单动能sql

    @Resource
    private UserOperationHistoryDOMapper historyDOMapper;//操作日志记录表

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String servletPath = "" + request.getServletPath();
        String method = request.getMethod();
        pathList.forEach(path -> {
            if (servletPath.contains(path)){
                Cookie[] cookies = request.getCookies();
                if (cookies != null) {
                    for (Cookie cookie : cookies) {
                        //获取token在请求中
                        if (cookie.getName().equals("_qjt_ac_")) {
                            String token = cookie.getValue();
                            /**解密token**/
                            byte[] encodeKey = Base64.decode(JWT_SECERT);
                            Claims claims = null;
                            try {
                                SecretKeySpec keySpec = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
                                claims = Jwts.parser().setSigningKey(keySpec).parseClaimsJws(token).getBody();
                            } catch (Exception e) {
                                return;
                            }
                            //用户账号
                            String account = claims.getSubject();
                            //查询URL在功能表中的功能
                            functionDOMapper.selectOne(servletPath, method);
                            //获取入参
                            RequestWrapper requestWrapper = null;
                            if (request instanceof HttpServletRequest) {
                                requestWrapper = new RequestWrapper(request);
                            }
                            Map<String,Object> map = new HashMap<>();
                            map.put("parameter", JSONObject.parse(requestWrapper.getBody()));
                            historyDOMapper.insert(map);//将操作信息入库
                        }
                    }
                } 
            }
        });
        return true;
    }
}

注册拦截器

package com.example.demo.config;

import com.example.demo.intercept.OperationLogInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 
 * @date 注册拦截器
 */
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public HandlerInterceptor getOperationLogInterceptor() {
        return new OperationLogInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(getOperationLogInterceptor()).addPathPatterns("/**").excludePathPatterns();
    }
}

记录返参日志

package com.example.demo.intercept;

import com.alibaba.fastjson.JSONObject;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.annotation.Resource;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.ws.Response;
import java.util.*;

/**
 * @author 
 * @date 记录用户操作日志返参
 */
@ControllerAdvice(basePackages = "项目包")
public class GetResponseBody implements ResponseBodyAdvice<Object> {

    /**
     * Jwt secert串,需要与加密token的秘钥一致
     */
    public static final String JWT_SECERT = "23142d7a9s7d66970ad07d8sa";

    /**
     * 需要记录的接口URL
     */
    private static List<String> pathList = new ArrayList<>();

    static {
        pathList.add("/mdms/model");
    }

    @Resource
    private FunctionDOMapper functionDOMapper;//菜单动能sql
    @Resource
    private UserOperationHistoryDOMapper historyDOMapper;//操作日志记录表

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return false;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        String path = serverHttpRequest.getURI().getPath();
        String methodValue = serverHttpRequest.getMethodValue();
        pathList.forEach(serverPath -> {
            if (path.contains(serverPath)) {
                HashMap<String, String> cookieMap = new HashMap<>();
                HttpHeaders headers = serverHttpRequest.getHeaders();
                List<String> cookieList = headers.get("cookie");
                if (CollectionUtils.isEmpty(cookieList)) {
                    return;
                }
                String replaceAll = cookieList.get(0).replaceAll(";", "").replaceAll(";", "");
                String[] split = replaceAll.split(";");
                for (String cookie : split) {
                    String[] param = cookie.split("=");
                    cookieMap.put(param[0], param[1]);
                }

                String token = cookieMap.get("_qjt_ac_");
                byte[] encodeKey = Base64.decode(JWT_SECERT);
                Claims claims = null;
                try {
                    SecretKeySpec keySpec = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
                    claims = Jwts.parser().setSigningKey(keySpec).parseClaimsJws(token).getBody();
                } catch (Exception e) {
                    return;
                }
                //用户账号
                String account = claims.getSubject();
                //查询URL在功能表中的功能
                functionDOMapper.selectOne(servletPath, method);
                //获取返参
                List<Object> list = historyDOMapper.select("功能表参数", account);
                list.sort((Object1,Object2)->Object2.getTime().compareTo(Object1.getTime()));//将查询到的操作记录按时间降序排列
                Object history = list.get(0);
                if (body instanceof Response) {
                    Response response = (Response) body;
                    JSONObject jsonObject = JSONObject.parseObject(history.getParam());
                    jsonObject.put("message",response.getMessage());
                    jsonObject.put("body",response.getData());
                    history.setParam(jsonObject.toString());
                    history.setDes(response.getMessage());
                }
                historyDOMapper.updateByPrimaryKeySelective(history);//将操作信息更新
            }
        });
        return body;
    }
}
参考

SpringBoot 整合ApiBoot Logging 实现监控打印接口的请求日志Java 拦截器、过滤器实现记录用户的操作记录,并保存入参,反参第八章:使用拦截器记录你的SpringBoot的请求日志springboot项目添加拦截器,打印方法参数日志springboot通过拦截器打印入口日志