Spring Boot项目请求日志打印

发布时间 2023-11-27 15:50:44作者: -西门-

Spring Boot项目请求日志打印

接口请求日志打印效果如图,基本符合中小型项目所需

直接上代码

  • 本代码中使用了hutool的工具包,需要先导入依赖
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>
  • 先来一个json payload处理类
import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import org.springframework.http.MediaType;

import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Map;

@Getter
public class JsonPayload {
    private final String payload;
    private final Map<String, String> headers;

    public JsonPayload(String payload) {
        this.payload = payload;
        this.headers = Collections.emptyMap();
    }

    public JsonPayload(String payload, Map<String, String> headers) {
        this.payload = payload;
        this.headers = headers;
    }

    public static boolean isText(MediaType mediaType) {
        return mediaType.isCompatibleWith(MediaType.TEXT_HTML) || mediaType.isCompatibleWith(MediaType.TEXT_PLAIN);
    }

    public static boolean isJson(MediaType mediaType) {
        return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
    }

    public static boolean isText(String contentType) {
        if (StrUtil.isBlank(contentType)) {
            return false;
        }
        try {
            return isText(MediaType.parseMediaType(contentType));
        } catch (Exception e) {
            return false;
        }
    }

    public static boolean isJson(String contentType) {
        if (StrUtil.isBlank(contentType)) {
            return false;
        }
        try {
            return isJson(MediaType.parseMediaType(contentType));
        } catch (Exception e) {
            return false;
        }
    }

    public static String toString(byte[] data, String characterEncoding) {
        if (data.length > 0) {
            Charset charset = characterEncoding == null ? Charset.defaultCharset() : Charset.forName(characterEncoding);
            return new String(data, charset);
        } else {
            return null;
        }
    }
}

  • 再来一个json字符串的处理类
import java.util.Map;

public class JsonStringBuilder {
    private static final char OBJ_START = '{';
    private static final char OBJ_END = '}';
    private static final char FIELD_SPLIT = ',';
    private static final char VALUE_SPLIT = ':';
    private static final String NULL_VALUE = "null";
    public static final char WRAP = '"';
    private static final char ESCAPE_PREFIX = '\\';

    private final static int[] OUTPUT_ESCAPES;

    static {
        int[] table = new int[128];
        for (int i = 0; i < 32; ++i) {
            table[i] = -1;
        }
        table['"'] = '"';
        table['\\'] = '\\';
        table[0x08] = 'b';
        table[0x09] = 't';
        table[0x0C] = 'f';
        table[0x0A] = 'n';
        table[0x0D] = 'r';
        OUTPUT_ESCAPES = table;
    }

    private final StringBuilder stringBuilder;

    public JsonStringBuilder() {
        this(512);
    }

    public JsonStringBuilder(int capacity) {
        this.stringBuilder = new StringBuilder(capacity);
    }

    public JsonStringBuilder startObject() {
        this.stringBuilder.append(OBJ_START);
        return this;
    }

    public JsonStringBuilder endObject() {
        this._removeSplit();
        this.stringBuilder.append(OBJ_END);
        this.stringBuilder.append(FIELD_SPLIT);
        return this;
    }

    public JsonStringBuilder startObject(String name) {
        return _field(name).startObject();
    }

    public JsonStringBuilder field(String name, String value) {
        if (value == null) {
            return nullField(name);
        }
        _field(name);
        this.stringBuilder.append(WRAP);
        for (char c : value.toCharArray()) {
            if (c <= 0x7F && OUTPUT_ESCAPES[c] != 0) {
                this.stringBuilder.append(ESCAPE_PREFIX);
                this.stringBuilder.append(c);
            } else {
                this.stringBuilder.append(c);
            }
        }
        this.stringBuilder.append(WRAP);
        this.stringBuilder.append(FIELD_SPLIT);
        return this;
    }

    public JsonStringBuilder field(String name, Integer value) {
        if (value == null) {
            return nullField(name);
        }
        return fieldRawValue(name, Integer.toString(value));
    }

    public JsonStringBuilder fieldRawValue(String name, String value) {
        if (value == null) {
            return nullField(name);
        }
        _field(name);
        this.stringBuilder.append(value);
        this.stringBuilder.append(FIELD_SPLIT);
        return this;
    }

    public JsonStringBuilder field(String name, Map<String, String> value) {
        if (value == null) {
            return nullField(name);
        }
        startObject(name);
        for (Map.Entry<String, String> entry : value.entrySet()) {
            field(entry.getKey(), entry.getValue());
        }
        endObject();
        return this;
    }


    public JsonStringBuilder nullField(String name) {
        return fieldRawValue(name, NULL_VALUE);
    }

    public String build() {
        this._removeSplit();
        return this.stringBuilder.toString();
    }

    private void _removeSplit() {
        int last = this.stringBuilder.length() - 1;
        if (this.stringBuilder.charAt(last) == FIELD_SPLIT) {
            this.stringBuilder.deleteCharAt(last);
        }
    }

    private JsonStringBuilder _field(String name) {
        this.stringBuilder.append(WRAP);
        this.stringBuilder.append(name);
        this.stringBuilder.append(WRAP);
        this.stringBuilder.append(VALUE_SPLIT);
        return this;
    }


    public static byte[] compress(byte[] data) {
        int offset = 0;
        int length = data.length;
        final byte[] output = new byte[length];
        int counter = 0;
        for (int index = 0; index < length; index++) {
            byte b = data[index];
            if (b == 0x08) {
                continue;
            } else if (b == 0x09) {
                continue;
            } else if (b == 0x0C) {
                continue;
            } else if (b == 0x0A) {
                continue;
            } else if (b == 0x0D) {
                continue;
            } else {
                if (b == 0x22) {
                    counter++;
                }
                if (counter % 2 == 0) {
                    if (b == 0x20) {
                        continue;
                    }
                }
                output[offset] = b;
                offset++;
            }
        }
        if (offset == length) {
            return output;
        } else {
            byte[] dest = new byte[offset];
            System.arraycopy(output, 0, dest, 0, offset);
            return dest;
        }
    }

    public static void escape(String s, StringBuffer sb) {
        if (s == null) return;
        final int len = s.length();
        for (int i = 0; i < len; i++) {
            char ch = s.charAt(i);
            switch (ch) {
                case '"':
                    sb.append("\\\"");
                    break;
                case '\\':
                    sb.append("\\\\");
                    break;
                case '\b':
                    sb.append("\\b");
                    break;
                case '\f':
                    sb.append("\\f");
                    break;
                case '\n':
                    sb.append("\\n");
                    break;
                case '\r':
                    sb.append("\\r");
                    break;
                case '\t':
                    sb.append("\\t");
                    break;
                case '/':
                    sb.append("\\/");
                    break;
                default:
                    //Reference: http://www.unicode.org/versions/Unicode5.1.0/
                    if ((ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u007F' && ch <= '\u009F') || (ch >= '\u2000' && ch <= '\u20FF')) {
                        String ss = Integer.toHexString(ch);
                        sb.append("\\u");
                        for (int k = 0; k < 4 - ss.length(); k++) {
                            sb.append('0');
                        }
                        sb.append(ss.toUpperCase());
                    } else {
                        sb.append(ch);
                    }
            }
        }//for
    }
}
  • request请求体处理类
import lombok.Getter;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.ServletInputStream;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

@Getter
public class LogRequest extends JsonPayload {
    private final String method;
    private final String remoteAddr;
    private final String requestURI;
    private final Map<String, String> parameters;

    private static final byte[] EMPTY = new byte[0];

    public LogRequest(ContentCachingRequestWrapper request) {
        super(getPayload(request, request.getCharacterEncoding(), request.getContentType()), getHeaders(request));
        this.method = request.getMethod();
        this.remoteAddr = request.getRemoteAddr();
        this.requestURI = request.getRequestURI();
        this.parameters = getRequestParam(request);
    }

    public static Map<String, String> getHeaders(ContentCachingRequestWrapper request) {
        LinkedHashMap<String, String> singleValueMap = new LinkedHashMap<>();
        for (Enumeration<String> names = request.getHeaderNames(); names.hasMoreElements(); ) {
            String headerName = names.nextElement();
            singleValueMap.put(headerName, request.getHeader(headerName));
        }
        return singleValueMap;
    }

    private static Map<String, String> getRequestParam(ContentCachingRequestWrapper request) {
        Map<String, String[]> parameters = request.getParameterMap();
        LinkedHashMap<String, String> singleValueMap = new LinkedHashMap<>(parameters.size());
        parameters.forEach((key, valueList) -> singleValueMap.put(key, valueList == null ? null : valueList[0]));
        return singleValueMap;
    }

    private static String getPayload(ContentCachingRequestWrapper request, String characterEncoding, String contentType) {
        if (!isJson(contentType)) {
            return null;
        }
        return toString(getPayload(request), characterEncoding);
    }

    private static byte[] getPayload(ContentCachingRequestWrapper request) {
        try {
            ServletInputStream is = request.getRequest().getInputStream();
            byte[] rawData;
            if (is.isFinished()) {
                rawData = request.getContentAsByteArray();
            } else {
                rawData = StreamUtils.copyToByteArray(request.getInputStream());
            }
            return JsonStringBuilder.compress(rawData);
        } catch (Exception e) {
            return EMPTY;
        }
    }
}
  • response响应体处理类
import lombok.Getter;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

@Getter
public final class LogResponse extends JsonPayload {
    private final int status;

    public LogResponse(ContentCachingResponseWrapper response) {
        super(getPayload(response, response.getCharacterEncoding(), response.getContentType()), getHeaders(response));
        this.status = response.getStatus();
    }

    public static Map<String, String> getHeaders(ContentCachingResponseWrapper response) {
        LinkedHashMap<String, String> singleValueMap = new LinkedHashMap<>();
        Collection<String> names = response.getHeaderNames();
        for (String headerName : names) {
            singleValueMap.put(headerName, response.getHeader(headerName));
        }
        return singleValueMap;
    }

    private static String getPayload(ContentCachingResponseWrapper response, String characterEncoding, String contentType) {
        if (!isJson(contentType)) {
            if (isText(contentType)) {
                StringBuffer sb = new StringBuffer();
                sb.append(JsonStringBuilder.WRAP);
                JsonStringBuilder.escape(toString(response.getContentAsByteArray(), characterEncoding), sb);
                sb.append(JsonStringBuilder.WRAP);
                return sb.toString();
            } else {
                return null;
            }
        }
        return toString(response.getContentAsByteArray(), characterEncoding);
    }
}
  • filter拦截器处理类
import cn.hutool.json.JSONUtil;
import com.ximen.cocktailserver.utils.JsonStringBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

@Component
@Slf4j(topic = "request.log")
public final class LogFilter extends OncePerRequestFilter {

    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 白名单的路径不做处理
        if (writeList(String.valueOf(request.getRequestURI()))) {
            filterChain.doFilter(request, response);
            return;
        }
        long cost = 0;
        HttpServletRequest requestToUse = request;
        if (!isAsyncDispatch(request) && !(request instanceof ContentCachingRequestWrapper)) {
            requestToUse = new ContentCachingRequestWrapper(request);
        }
        HttpServletResponse responseToUse = response;
        if (!isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) {
            responseToUse = new ContentCachingResponseWrapper(response);
        }
        try {
            StopWatch watch = new StopWatch();
            watch.start();
            filterChain.doFilter(requestToUse, responseToUse);
            watch.stop();
            cost = watch.getTotalTimeMillis();
        } finally {
            if (!isAsyncStarted(requestToUse)) {
                LogRequest logRequest = new LogRequest((ContentCachingRequestWrapper) requestToUse);
                LogResponse logResponse = new LogResponse((ContentCachingResponseWrapper) responseToUse);
                boolean isJson = JSONUtil.isTypeJSON(logResponse.getPayload());
                // 根据自己的情况,判断什么时候需要打印日志,我是把错误的请求日志才打印出来
                if (logResponse.getStatus() != 200 || (isJson && JSONUtil.parseObj(logResponse.getPayload()).getInt("code", 1001) != 200)) {
                    log.info(logFormatter(logRequest, logResponse, cost));
                }
                ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(responseToUse, ContentCachingResponseWrapper.class);
                if (wrapper != null) {
                    wrapper.copyBodyToResponse();
                }
            }
        }
    }

    private String logFormatter(LogRequest request, LogResponse response, long spendTime) {
        JsonStringBuilder jsonBuilder = new JsonStringBuilder(2000);
        jsonBuilder.startObject()
                .field("timestamp", LocalDateTime.now().atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_ZONED_DATE_TIME))
                .field("uri", request.getRequestURI())
                .field("method", request.getMethod())
                .field("refer", request.getRemoteAddr())
                .field("status", response.getStatus())
                .field("time", spendTime + "ms")
                .startObject("request")
                .fieldRawValue("body", request.getPayload())
                .field("param", request.getParameters())
                .field("header", request.getHeaders())
                .endObject()
                .fieldRawValue("response", response.getPayload())
                .endObject();
        return jsonBuilder.build();
    }

    private boolean writeList(String path) {
        return path.equals("/") ||
                path.startsWith("/actuator") ||
                path.startsWith("/plumelog") ||
                path.startsWith("/doc.html") ||
                path.startsWith("/swagger-resources") ||
                path.startsWith("/v2/api-docs") ||
                path.equals("/swagger-ui.html") ||
                path.startsWith("/webjars");
    }

}

白名单这块自己根据情况去加吧,equals就是某个全路径,startsWith就是以什么开始的路径。(这里的路径都指的是除去localhost端口号那些,看代码应该就能知道)

  • 如果日志打印出来的编码有问题,可以设置下yml配置文件
server:
  servlet:
    encoding:
      charset: UTF-8
      force-response: true