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
- 一共就这么几个类