background

The production system of The client of Party A requires early warning of safety risks and quick traceability of safety events, so a set of log management specifications should be made.

The system that we access is required to output corresponding logs in strict accordance with the provided format for important scenarios such as user login, registration, and password change.

In the follow-up, log information on our system was collected through fileBeat interconnection.

In simple terms, the application system uniformly prints corresponding logs when processing interface requests.

Problem description

Mature and common unified log printing scheme is to use AOP technology, custom annotation, use Around notification @around on the aspect, intercept requests, get the Controller class method input and output parameters.

However, the interface used in the business scenario was implemented in the following way:

@RequestMapping(value = "/auth", method = { RequestMethod.POST, RequestMethod.GET, RequestMethod.OPTIONS })
public void auth(HttpServletRequest req, HttpServletResponse resp) {
   authService.auth(req, resp);
}
Copy the code

Drop the pass parameters directly into HttpServletRequest.

Returns parameters, again using HttpServletResponse output.

public void printResult(HttpServletRequest req, HttpServletResponse resp,
      String action, int code, String msg, Object result) {
   PrintWriter p = null;
   Ret ret = new Ret();
   Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss")
         .serializeNulls()
         .create();
   try {
      p = resp.getWriter();
      ret.setRspCode(code);
      ret.setRspDesc(msg);
      ret.setData(result);

      p.write(gson.toJson(ret));
      return;
   } catch (Exception e) {
      logger.error(e.getMessage());
   } finally{ p.flush(); p.close(); }}Copy the code

Unlike usual practice, the specific input and output parameters, with object encapsulation, directly on the method can be.

If we want to intercept the request for passing parameters in advance, we can use methods such as request.getParameter() to get the parameters. However, in a specific interface business process, when using methods such as Request.getParameter (), the parameters passed in cannot be obtained.

Because streams can only be read once.

So the question is: How can Request and Response be read repeatedly?

The solution

Using methods like Request.getParameter (), you end up calling the getInputStream method.

Needs to be rewritten HttpServletRequestWrapper wrapper classes, in the calling getInputStream method, the flow data and written to the cache.

To obtain the parameters, read the cache data directly.

This allows the contents of the Request to be read multiple times.

The implementation code

Encapsulates the request

Custom class ContentCachingRequestWrapper

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/ * * * * rewrite HttpServletRequestWrapper * *@Author: linzengrui
 * @Date: 2021/11/22 15:33 the * /
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    public ContentCachingRequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder sb = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))){
            String line = "";
            while((line = reader.readLine()) ! =null) { sb.append(line); }}catch (IOException e) {
            e.printStackTrace();
        }
        body = sb.toString().getBytes(StandardCharsets.UTF_8);
    }

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

    @Override
    public ServletInputStream getInputStream(a) throws IOException {

        final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public boolean isFinished(a) {
                return false;
            }

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

            @Override
            public void setReadListener(ReadListener readListener) {}@Override
            public int read(a) throws IOException {
                returninputStream.read(); }}; }public byte[] getBody() {
        returnbody; }}Copy the code

Wrap the response

Custom class ContentCachingResponseWrapper


import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;

/ * * * * rewrite HttpServletResponseWrapper * *@Author: linzengrui
 * @Date: 2021/11/22 19:45 * /
public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {

    private ByteArrayOutputStream byteArrayOutputStream;
    private ServletOutputStream servletOutputStream;
    private PrintWriter printWriter;

    public ContentCachingResponseWrapper(HttpServletResponse response) {
        super(response);
        byteArrayOutputStream = new ByteArrayOutputStream();
        servletOutputStream = new ServletOutputStream() {
            @Override
            public boolean isReady(a) {
                return false;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {}@Override
            public void write(int b) throws IOException { byteArrayOutputStream.write(b); }}; printWriter =new PrintWriter(byteArrayOutputStream);
    }

    @Override
    public PrintWriter getWriter(a) {
        return printWriter;
    }

    @Override
    public ServletOutputStream getOutputStream(a) throws IOException {
        return servletOutputStream;
    }

    public byte[] toByteArray() {
        returnbyteArrayOutputStream.toByteArray(); }}Copy the code

Filter Intercepts requests

The interceptor LogFilter uses the wrapper class encapsulated above to get the pass parameter.

@Slf4j
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        / / to use rewrite HttpServletRequestWrapper custom wrapper classes
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest)request);
        / / to use rewrite HttpServletResponseWrapper custom wrapper classes
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

        // The fetch stream method can only be executed once
        try(ServletOutputStream outputStream = response.getOutputStream()){
            // Get the pass parameter
            String requestParamJson = new String(requestWrapper.getBody());
            log.info("requestParamJson --> {}", requestParamJson);

            // Specific methods to execute the process
            chain.doFilter(requestWrapper, responseWrapper);

            // After the fetch stream operation is triggered, data can be fetched from the cache multiple times
            String respDataJson = new String(responseWrapper.toByteArray());
            log.info("respDataJson <-- {}", respDataJson);

            // TODO writes logs
            

            // The content needs to be rewritten, otherwise the stream will have no output
            outputStream.write(respDataJson.getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        }catch(Exception e){ e.printStackTrace(); }}@Override
    public void destroy(a) {}}Copy the code

The SpringBoot boot class adds an annotation @ServletComponentScan

Note: The bootstrap class is annotated @ServletComponentScan to identify the Filter injected above.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan
@SpringBootApplication
public class SpringBootApplication {

	public static void main(String[] args) { SpringApplication.run(SpringBootApplication.class, args); }}Copy the code

For the specific business interface, the original logic remains unchanged and the input parameters can still be obtained.