使用过滤器打印Rest接口日志

以下所有内容均可在springboot-demo中找到。

前面介绍了通过切面打印日志的方法,其实定义一个注解@ControllLog专门处理Controller层的方法就可以了。 但是有时候有的人就是懒得连一个注解都不愿意加。想要统一的处理Rest接口,就得想办法在过滤器中获得入参和返回值。 由于java的输入流只能读一次,输出流只能写一次,首先还要继承HttpServletRequestWrapper重写读取入参body的方式, 和继承HttpServletResponseWrapper重写读取返回值body的方式。否则,前者会到导致InputStream为了打印日志已被读取过, controller层接受不到body里的入参;后者会导致OutputStream为了打印日志已被写入过,前端接收不到body里的返回值了。

BodyRequestWrapper

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

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

@Slf4j
public class BodyRequestWrapper extends HttpServletRequestWrapper {
    @Getter
    private String body=null;

    public BodyRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        if (request.getInputStream() != null) {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
                StringBuilder stringBuilder = new StringBuilder();
                char[] charBuffer = new char[128];
                int bytesRead;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
                body = stringBuilder.toString();
            } catch (Exception ex) {
                log.error("获取请求体body异常:", ex);
                throw ex;
            }
        }
    }

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

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

            @Override
            public void setReadListener(ReadListener readListener) {

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

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

BodyResponseWrapper

import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;

@Slf4j
public class BodyResponseWrapper extends HttpServletResponseWrapper {
    private final ByteArrayOutputStream output;

    public BodyResponseWrapper(HttpServletResponse response) {
        super(response);
        output = new ByteArrayOutputStream();
    }

    /**
     */
    @Override
    public ServletOutputStream getOutputStream() {
         return new ServletOutputStream() {
            @Override
            public void write(int b) {
                output.write(b);
            }

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

            @Override
            public void setWriteListener(WriteListener writeListener) {
            }
        };
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return new PrintWriter(new OutputStreamWriter(this.getOutputStream()));
    }

    /**
     * 获取返回值
     */
    public String getBody() throws UnsupportedEncodingException {
        return output.toString();
    }
}

过滤器

import com.zph.programmer.springboot.servlet.BodyRequestWrapper;
import com.zph.programmer.springboot.servlet.BodyResponseWrapper;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;


@Slf4j
public class HttpServletWrapperFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("init filter 打印REST接口日志过滤器");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        BodyRequestWrapper requestWrapper = null;
        HttpServletRequest httpServletRequest = null;
        BodyResponseWrapper responseWrapper = null;
        HttpServletResponse httpServletResponse = null;
        if (request instanceof HttpServletRequest) {
            httpServletRequest = (HttpServletRequest) request;
            requestWrapper = new BodyRequestWrapper(httpServletRequest);
        }
        if (response instanceof HttpServletResponse) {
            httpServletResponse = (HttpServletResponse) response;
            responseWrapper = new BodyResponseWrapper(httpServletResponse);
        }
        if (httpServletRequest != null) {
            StringBuilder requestParamsBuilder = new StringBuilder();
            if (httpServletRequest.getQueryString() != null) {
                //针对param入参
                requestParamsBuilder.append(URLDecoder.decode(httpServletRequest.getQueryString(), StandardCharsets.UTF_8));
            } else {
                //针对body入参
                requestParamsBuilder.append(requestWrapper.getBody());
            }
            String sb = "\n********************************* " + LocalDateTime.now().toString() + "\n" +
                    "ServerAddress :  " + httpServletRequest.getScheme() + "://" + httpServletRequest.getServerName() + ":" + httpServletRequest.getServerPort()
                                        + httpServletRequest.getRequestURI() +" "+httpServletRequest.getMethod()+ "\n" +
                    "ContentType   :  " + httpServletRequest.getContentType() + "\n" +
                    "RequestParams :  " + requestParamsBuilder.toString() + "\n" +
                    "********************************* Request Start\n";
            log.info(sb);
        }

        chain.doFilter(requestWrapper != null ? requestWrapper : request,
                responseWrapper != null ? responseWrapper : response);

        if (httpServletResponse != null) {
            String body = responseWrapper.getBody();
            String sb = "\n********************************* " + LocalDateTime.now().toString() + "\n" +
                    "status        :  " + httpServletResponse.getStatus() + "\n" +
                    "ContentType   :  " + httpServletResponse.getContentType() + "\n" +
                    "Response      :  " + body + "\n" +
                    "********************************* Response End\n";
            log.info(sb);
            //重新 writer 返回值,重要!!!
            response.getOutputStream().write(body.getBytes());
        }

    }

    @Override
    public void destroy() {

    }
}

最后再在配置类里将过滤器注册就行了。

/**
     * 注册过滤器
     * @return
     */
    @Bean
    public FilterRegistrationBean<HttpServletWrapperFilter> filterRegistry() {
        FilterRegistrationBean<HttpServletWrapperFilter> frBean = new FilterRegistrationBean<>();
        frBean.setFilter(new HttpServletWrapperFilter());
        frBean.setOrder(1);//多个过滤器时指定过滤器的执行顺序
        frBean.addUrlPatterns("/*");
        return frBean;
    }

日志效果

2021-02-11 17:25:20 |INFO  |http-nio-8080-exec-1 |HttpServletWrapperFilter.java:52 |com.zph.programmer.springboot.filter.HttpServletWrapperFilter |
********************************* 2021-02-11T17:25:20.502694700
ServerAddress :  http://127.0.0.1:8080/test/testCache POST
ContentType   :  application/json
RequestParams :  {
    "key":"test-cache",
    "value":"测试缓存"
}
********************************* Request Start

2021-02-11 17:25:20 |INFO  |http-nio-8080-exec-1 |HttpServletWrapperFilter.java:65 |com.zph.programmer.springboot.filter.HttpServletWrapperFilter |
********************************* 2021-02-11T17:25:20.697581400
status        :  200
ContentType   :  application/json
Response      :  {"code":200,"status":"success","message":null,"moreInfo":null,"data":"OK"}
********************************* Response End

2021-02-11 17:25:22 |INFO  |http-nio-8080-exec-2 |HttpServletWrapperFilter.java:52 |com.zph.programmer.springboot.filter.HttpServletWrapperFilter |
********************************* 2021-02-11T17:25:22.679441100
ServerAddress :  http://127.0.0.1:8080/test/testCache GET
ContentType   :  null
RequestParams :  key=test-cache
********************************* Request Start

2021-02-11 17:25:22 |INFO  |http-nio-8080-exec-2 |HttpServletWrapperFilter.java:65 |com.zph.programmer.springboot.filter.HttpServletWrapperFilter |
********************************* 2021-02-11T17:25:22.688437400
status        :  200
ContentType   :  application/json
Response      :  {"code":200,"status":"success","message":null,"moreInfo":null,"data":"测试缓存"}
********************************* Response End

其它

为什么要费这么大劲打印日志呢? 我觉得规整的日志绝对能更好得帮助排查问题,而且将接口日志存进数据库可以做更多事情, 比如记录系统的运行,监控负载攻击,统计调用次数,哪些功能常用,哪些功能不常用; 加上时间的话还可以统计接口的调用时长,哪些接口需要优化; 如果接入了用户,还可以从记录用户行为,刻画用户画像等等。 总之,规范的日志必不可少。

留下评论