mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-09 20:57:23 +08:00
feat(core): 新增请求响应可重复读流处理并优化日志模块
增加访问日志打印处理:包括参数打印、过滤敏感参数和超长参数配置
This commit is contained in:
@@ -23,6 +23,12 @@
|
|||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- servlet包 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.servlet</groupId>
|
||||||
|
<artifactId>jakarta.servlet-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Hibernate Validator -->
|
<!-- Hibernate Validator -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hibernate.validator</groupId>
|
<groupId>org.hibernate.validator</groupId>
|
||||||
|
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||||
|
* <p>
|
||||||
|
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* <p>
|
||||||
|
* http://www.gnu.org/licenses/lgpl.html
|
||||||
|
* <p>
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package top.continew.starter.core.wrapper;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import jakarta.servlet.ReadListener;
|
||||||
|
import jakarta.servlet.ServletInputStream;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可重复读取请求体的包装器
|
||||||
|
* 支持文件流直接透传,非文件流可重复读取
|
||||||
|
*
|
||||||
|
* @author echo
|
||||||
|
* @since 2025/03/25 11:11
|
||||||
|
**/
|
||||||
|
public class RepeatReadRequestWrapper extends HttpServletRequestWrapper {
|
||||||
|
|
||||||
|
private byte[] cachedBody;
|
||||||
|
private final HttpServletRequest originalRequest;
|
||||||
|
|
||||||
|
public RepeatReadRequestWrapper(HttpServletRequest request) throws IOException {
|
||||||
|
super(request);
|
||||||
|
this.originalRequest = request;
|
||||||
|
|
||||||
|
// 判断是否为文件上传请求
|
||||||
|
if (!isMultipartContent(request)) {
|
||||||
|
this.cachedBody = IoUtil.readBytes(request.getInputStream(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为文件上传请求
|
||||||
|
*/
|
||||||
|
private boolean isMultipartContent(HttpServletRequest request) {
|
||||||
|
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServletInputStream getInputStream() throws IOException {
|
||||||
|
// 如果是文件上传,直接返回原始输入流
|
||||||
|
if (isMultipartContent(originalRequest)) {
|
||||||
|
return originalRequest.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非文件上传,返回可重复读取的输入流
|
||||||
|
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
|
||||||
|
|
||||||
|
return new ServletInputStream() {
|
||||||
|
@Override
|
||||||
|
public boolean isFinished() {
|
||||||
|
return byteArrayInputStream.available() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReadListener(ReadListener readListener) {
|
||||||
|
// 非阻塞I/O,这里可以根据需要实现
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() {
|
||||||
|
return byteArrayInputStream.read();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedReader getReader() throws IOException {
|
||||||
|
// 如果是文件上传,直接返回原始Reader
|
||||||
|
if (isMultipartContent(originalRequest)) {
|
||||||
|
return originalRequest.getReader();
|
||||||
|
}
|
||||||
|
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(cachedBody), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||||
|
* <p>
|
||||||
|
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* <p>
|
||||||
|
* http://www.gnu.org/licenses/lgpl.html
|
||||||
|
* <p>
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package top.continew.starter.core.wrapper;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletOutputStream;
|
||||||
|
import jakarta.servlet.WriteListener;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletResponseWrapper;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可重复读取响应内容的包装器
|
||||||
|
* 支持缓存响应内容,便于日志记录和后续处理 (不缓存SSE)
|
||||||
|
*
|
||||||
|
* @author echo
|
||||||
|
* @since 2025/03/25 11:11
|
||||||
|
**/
|
||||||
|
public class RepeatReadResponseWrapper extends HttpServletResponseWrapper {
|
||||||
|
|
||||||
|
private final ByteArrayOutputStream cachedOutputStream = new ByteArrayOutputStream();
|
||||||
|
private final PrintWriter writer = new PrintWriter(cachedOutputStream, true);
|
||||||
|
|
||||||
|
// 是否为流式响应
|
||||||
|
private boolean isStreamingResponse = false;
|
||||||
|
|
||||||
|
public RepeatReadResponseWrapper(HttpServletResponse response) {
|
||||||
|
super(response);
|
||||||
|
checkStreamingResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentType(String type) {
|
||||||
|
super.setContentType(type);
|
||||||
|
// 根据 Content-Type 判断是否为流式响应
|
||||||
|
if (type != null) {
|
||||||
|
String lowerType = type.toLowerCase();
|
||||||
|
isStreamingResponse = lowerType.contains("text/event-stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkStreamingResponse() {
|
||||||
|
String contentType = getContentType();
|
||||||
|
if (contentType != null) {
|
||||||
|
String lowerType = contentType.toLowerCase();
|
||||||
|
isStreamingResponse = lowerType.contains("text/event-stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServletOutputStream getOutputStream() throws IOException {
|
||||||
|
checkStreamingResponse();
|
||||||
|
if (isStreamingResponse) {
|
||||||
|
// 对于 SSE 流式响应,直接返回原始响应流,不做额外处理
|
||||||
|
return super.getOutputStream();
|
||||||
|
}
|
||||||
|
return new ServletOutputStream() {
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWriteListener(WriteListener writeListener) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(int b) throws IOException {
|
||||||
|
cachedOutputStream.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b) throws IOException {
|
||||||
|
cachedOutputStream.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
cachedOutputStream.write(b, off, len);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrintWriter getWriter() throws IOException {
|
||||||
|
checkStreamingResponse();
|
||||||
|
if (isStreamingResponse) {
|
||||||
|
// 对于 SSE 流式响应,直接返回原始响应写入器,不做额外处理
|
||||||
|
return super.getWriter();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResponseContent() {
|
||||||
|
if (!isStreamingResponse) {
|
||||||
|
writer.flush();
|
||||||
|
return cachedOutputStream.toString(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyBodyToResponse() throws IOException {
|
||||||
|
if (!isStreamingResponse && cachedOutputStream.size() > 0) {
|
||||||
|
getResponse().getOutputStream().write(cachedOutputStream.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStreamingResponse() {
|
||||||
|
return isStreamingResponse;
|
||||||
|
}
|
||||||
|
}
|
@@ -26,9 +26,12 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
|
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
|
||||||
|
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
|
||||||
|
import top.continew.starter.log.model.AccessLogContext;
|
||||||
import top.continew.starter.log.model.LogProperties;
|
import top.continew.starter.log.model.LogProperties;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,9 +46,11 @@ public class AccessLogAspect {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(AccessLogAspect.class);
|
private static final Logger log = LoggerFactory.getLogger(AccessLogAspect.class);
|
||||||
private final LogProperties logProperties;
|
private final LogProperties logProperties;
|
||||||
|
private final LogHandler logHandler;
|
||||||
|
|
||||||
public AccessLogAspect(LogProperties logProperties) {
|
public AccessLogAspect(LogProperties logProperties, LogHandler logHandler) {
|
||||||
this.logProperties = logProperties;
|
this.logProperties = logProperties;
|
||||||
|
this.logHandler = logHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,19 +113,18 @@ public class AccessLogAspect {
|
|||||||
HttpServletRequest request = attributes.getRequest();
|
HttpServletRequest request = attributes.getRequest();
|
||||||
HttpServletResponse response = attributes.getResponse();
|
HttpServletResponse response = attributes.getResponse();
|
||||||
try {
|
try {
|
||||||
// 打印请求日志
|
logHandler.processAccessLogStartReq(AccessLogContext.builder()
|
||||||
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
|
.startTime(startTime)
|
||||||
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
|
.request(new RecordableServletHttpRequest(request))
|
||||||
}
|
.properties(logProperties)
|
||||||
|
.build());
|
||||||
return joinPoint.proceed();
|
return joinPoint.proceed();
|
||||||
} finally {
|
} finally {
|
||||||
Instant endTime = Instant.now();
|
Instant endTime = Instant.now();
|
||||||
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
|
logHandler.processAccessLogEndReq(AccessLogContext.builder()
|
||||||
Duration timeTaken = Duration.between(startTime, endTime);
|
.endTime(endTime)
|
||||||
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), response != null
|
.response(new RecordableServletHttpResponse(response, response.getStatus()))
|
||||||
? response.getStatus()
|
.build());
|
||||||
: "N/A", timeTaken.toMillis());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,7 @@ import org.springframework.web.context.request.RequestContextHolder;
|
|||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import top.continew.starter.log.annotation.Log;
|
import top.continew.starter.log.annotation.Log;
|
||||||
import top.continew.starter.log.dao.LogDao;
|
import top.continew.starter.log.dao.LogDao;
|
||||||
import top.continew.starter.log.LogHandler;
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
import top.continew.starter.log.model.LogProperties;
|
import top.continew.starter.log.model.LogProperties;
|
||||||
import top.continew.starter.log.model.LogRecord;
|
import top.continew.starter.log.model.LogRecord;
|
||||||
import top.continew.starter.web.util.SpringWebUtils;
|
import top.continew.starter.web.util.SpringWebUtils;
|
||||||
|
@@ -29,9 +29,9 @@ import top.continew.starter.log.aspect.AccessLogAspect;
|
|||||||
import top.continew.starter.log.aspect.LogAspect;
|
import top.continew.starter.log.aspect.LogAspect;
|
||||||
import top.continew.starter.log.dao.LogDao;
|
import top.continew.starter.log.dao.LogDao;
|
||||||
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
|
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
|
||||||
|
import top.continew.starter.log.filter.LogFilter;
|
||||||
import top.continew.starter.log.handler.AopLogHandler;
|
import top.continew.starter.log.handler.AopLogHandler;
|
||||||
import top.continew.starter.log.LogFilter;
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
import top.continew.starter.log.LogHandler;
|
|
||||||
import top.continew.starter.log.model.LogProperties;
|
import top.continew.starter.log.model.LogProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,9 +49,11 @@ public class LogAutoConfiguration {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(LogAutoConfiguration.class);
|
private static final Logger log = LoggerFactory.getLogger(LogAutoConfiguration.class);
|
||||||
private final LogProperties logProperties;
|
private final LogProperties logProperties;
|
||||||
|
private final LogHandler logHandler;
|
||||||
|
|
||||||
public LogAutoConfiguration(LogProperties logProperties) {
|
public LogAutoConfiguration(LogProperties logProperties, LogHandler logHandler) {
|
||||||
this.logProperties = logProperties;
|
this.logProperties = logProperties;
|
||||||
|
this.logHandler = logHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,13 +68,12 @@ public class LogAutoConfiguration {
|
|||||||
/**
|
/**
|
||||||
* 日志切面
|
* 日志切面
|
||||||
*
|
*
|
||||||
* @param logHandler 日志处理器
|
|
||||||
* @param logDao 日志持久层接口
|
* @param logDao 日志持久层接口
|
||||||
* @return {@link LogAspect }
|
* @return {@link LogAspect }
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public LogAspect logAspect(LogHandler logHandler, LogDao logDao) {
|
public LogAspect logAspect(LogDao logDao) {
|
||||||
return new LogAspect(logProperties, logHandler, logDao);
|
return new LogAspect(logProperties, logHandler, logDao);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ public class LogAutoConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public AccessLogAspect accessLogAspect() {
|
public AccessLogAspect accessLogAspect() {
|
||||||
return new AccessLogAspect(logProperties);
|
return new AccessLogAspect(logProperties, logHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package top.continew.starter.log.handler;
|
package top.continew.starter.log.handler;
|
||||||
|
|
||||||
import top.continew.starter.log.AbstractLogHandler;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志处理器-AOP 版实现
|
* 日志处理器-AOP 版实现
|
||||||
*
|
*
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package top.continew.starter.log;
|
package top.continew.starter.log.filter;
|
||||||
|
|
||||||
import cn.hutool.extra.spring.SpringUtil;
|
import cn.hutool.extra.spring.SpringUtil;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
@@ -25,15 +25,13 @@ import org.springframework.boot.autoconfigure.web.ServerProperties;
|
|||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
import top.continew.starter.core.wrapper.RepeatReadRequestWrapper;
|
||||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
import top.continew.starter.core.wrapper.RepeatReadResponseWrapper;
|
||||||
import org.springframework.web.util.WebUtils;
|
|
||||||
import top.continew.starter.log.model.LogProperties;
|
import top.continew.starter.log.model.LogProperties;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志过滤器
|
* 日志过滤器
|
||||||
@@ -67,20 +65,24 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
|
|||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isMatch = logProperties.isMatch(request.getRequestURI());
|
boolean isMatch = logProperties.isMatch(request.getRequestURI());
|
||||||
// 包装输入流,可重复读取
|
|
||||||
if (!isMatch && this.isRequestWrapper(request)) {
|
// 处理可重复读取的请求
|
||||||
request = new ContentCachingRequestWrapper(request);
|
HttpServletRequest requestWrapper = (isMatch || !this.isRequestWrapper(request))
|
||||||
}
|
? request
|
||||||
// 包装输出流,可重复读取
|
: new RepeatReadRequestWrapper(request);
|
||||||
boolean isResponseWrapper = !isMatch && this.isResponseWrapper(response);
|
|
||||||
if (isResponseWrapper) {
|
// 处理可重复读取的响应
|
||||||
response = new ContentCachingResponseWrapper(response);
|
HttpServletResponse responseWrapper = (isMatch || !this.isResponseWrapper(response))
|
||||||
}
|
? response
|
||||||
filterChain.doFilter(request, response);
|
: new RepeatReadResponseWrapper(response);
|
||||||
// 更新响应(不操作这一步,会导致接口响应空白)
|
|
||||||
if (isResponseWrapper) {
|
filterChain.doFilter(requestWrapper, responseWrapper);
|
||||||
this.updateResponse(response);
|
|
||||||
|
// 如果响应被包装了,复制缓存数据到原始响应
|
||||||
|
if (responseWrapper instanceof RepeatReadResponseWrapper wrappedResponse) {
|
||||||
|
wrappedResponse.copyBodyToResponse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +123,7 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
|
|||||||
* @return true:是;false:否
|
* @return true:是;false:否
|
||||||
*/
|
*/
|
||||||
private boolean isRequestWrapper(HttpServletRequest request) {
|
private boolean isRequestWrapper(HttpServletRequest request) {
|
||||||
return !(request instanceof ContentCachingRequestWrapper);
|
return !(request instanceof RepeatReadRequestWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,18 +133,6 @@ public class LogFilter extends OncePerRequestFilter implements Ordered {
|
|||||||
* @return true:是;false:否
|
* @return true:是;false:否
|
||||||
*/
|
*/
|
||||||
private boolean isResponseWrapper(HttpServletResponse response) {
|
private boolean isResponseWrapper(HttpServletResponse response) {
|
||||||
return !(response instanceof ContentCachingResponseWrapper);
|
return !(response instanceof RepeatReadResponseWrapper);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新响应
|
|
||||||
*
|
|
||||||
* @param response 响应对象
|
|
||||||
* @throws IOException /
|
|
||||||
*/
|
|
||||||
private void updateResponse(HttpServletResponse response) throws IOException {
|
|
||||||
ContentCachingResponseWrapper responseWrapper = WebUtils
|
|
||||||
.getNativeResponse(response, ContentCachingResponseWrapper.class);
|
|
||||||
Objects.requireNonNull(responseWrapper).copyBodyToResponse();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -14,21 +14,31 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package top.continew.starter.log;
|
package top.continew.starter.log.handler;
|
||||||
|
|
||||||
import cn.hutool.core.annotation.AnnotationUtil;
|
import cn.hutool.core.annotation.AnnotationUtil;
|
||||||
import cn.hutool.core.text.CharSequenceUtil;
|
import cn.hutool.core.text.CharSequenceUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import top.continew.starter.log.annotation.Log;
|
import top.continew.starter.log.annotation.Log;
|
||||||
import top.continew.starter.log.enums.Include;
|
import top.continew.starter.log.enums.Include;
|
||||||
|
import top.continew.starter.log.http.RecordableHttpRequest;
|
||||||
|
import top.continew.starter.log.http.RecordableHttpResponse;
|
||||||
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
|
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
|
||||||
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
|
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
|
||||||
|
import top.continew.starter.log.model.AccessLogContext;
|
||||||
|
import top.continew.starter.log.model.AccessLogProperties;
|
||||||
import top.continew.starter.log.model.LogRecord;
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
import top.continew.starter.log.util.AccessLogUtils;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -41,6 +51,9 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
public abstract class AbstractLogHandler implements LogHandler {
|
public abstract class AbstractLogHandler implements LogHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AbstractLogHandler.class);
|
||||||
|
private final TransmittableThreadLocal<AccessLogContext> logContextThread = new TransmittableThreadLocal<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LogRecord.Started start(Instant startTime, HttpServletRequest request) {
|
public LogRecord.Started start(Instant startTime, HttpServletRequest request) {
|
||||||
return LogRecord.start(startTime, new RecordableServletHttpRequest(request));
|
return LogRecord.start(startTime, new RecordableServletHttpRequest(request));
|
||||||
@@ -156,4 +169,37 @@ public abstract class AbstractLogHandler implements LogHandler {
|
|||||||
includes.removeAll(Set.of(excludeArr));
|
includes.removeAll(Set.of(excludeArr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAccessLogStartReq(AccessLogContext accessLogContext) {
|
||||||
|
AccessLogProperties properties = accessLogContext.getProperties().getAccessLog();
|
||||||
|
// 是否需要打印 规则: 是否打印开关 或 放行路径
|
||||||
|
if (!properties.isPrint() || accessLogContext.getProperties()
|
||||||
|
.getAccessLog()
|
||||||
|
.isMatch(accessLogContext.getRequest().getPath())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 构建上下文
|
||||||
|
logContextThread.set(accessLogContext);
|
||||||
|
RecordableHttpRequest request = accessLogContext.getRequest();
|
||||||
|
String path = request.getPath();
|
||||||
|
String param = AccessLogUtils.getParam(request, properties);
|
||||||
|
log.info(param != null ? "[Start] [{}] {} {}" : "[Start] [{}] {}", request.getMethod(), path, param);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAccessLogEndReq(AccessLogContext accessLogContext) {
|
||||||
|
AccessLogContext logContext = logContextThread.get();
|
||||||
|
if (ObjectUtil.isNotEmpty(logContext)) {
|
||||||
|
try {
|
||||||
|
RecordableHttpRequest request = logContext.getRequest();
|
||||||
|
RecordableHttpResponse response = accessLogContext.getResponse();
|
||||||
|
Duration timeTaken = Duration.between(logContext.getStartTime(), accessLogContext.getEndTime());
|
||||||
|
log.info("[End] [{}] {} {} {}ms", request.getMethod(), request.getPath(), response
|
||||||
|
.getStatus(), timeTaken.toMillis());
|
||||||
|
} finally {
|
||||||
|
logContextThread.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -14,11 +14,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package top.continew.starter.log;
|
package top.continew.starter.log.handler;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import top.continew.starter.log.enums.Include;
|
import top.continew.starter.log.enums.Include;
|
||||||
|
import top.continew.starter.log.model.AccessLogContext;
|
||||||
import top.continew.starter.log.model.LogRecord;
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -97,4 +98,18 @@ public interface LogHandler {
|
|||||||
* @return 日志包含信息
|
* @return 日志包含信息
|
||||||
*/
|
*/
|
||||||
Set<Include> getIncludes(Set<Include> includes, Method targetMethod, Class<?> targetClass);
|
Set<Include> getIncludes(Set<Include> includes, Method targetMethod, Class<?> targetClass);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理访问日志开始请求
|
||||||
|
*
|
||||||
|
* @param accessLogContext 访问日志上下文
|
||||||
|
*/
|
||||||
|
void processAccessLogStartReq(AccessLogContext accessLogContext);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理访问日志 结束请求
|
||||||
|
*
|
||||||
|
* @param accessLogContext 访问日志上下文
|
||||||
|
*/
|
||||||
|
void processAccessLogEndReq(AccessLogContext accessLogContext);
|
||||||
}
|
}
|
@@ -71,4 +71,11 @@ public interface RecordableHttpRequest {
|
|||||||
* @return 请求参数
|
* @return 请求参数
|
||||||
*/
|
*/
|
||||||
Map<String, Object> getParam();
|
Map<String, Object> getParam();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取路径 - 格式 /system/dept
|
||||||
|
*
|
||||||
|
* @return {@link String }
|
||||||
|
*/
|
||||||
|
String getPath();
|
||||||
}
|
}
|
||||||
|
@@ -17,13 +17,10 @@
|
|||||||
package top.continew.starter.log.http.servlet;
|
package top.continew.starter.log.http.servlet;
|
||||||
|
|
||||||
import cn.hutool.core.text.CharSequenceUtil;
|
import cn.hutool.core.text.CharSequenceUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
|
||||||
import org.springframework.web.util.UriUtils;
|
import org.springframework.web.util.UriUtils;
|
||||||
import org.springframework.web.util.WebUtils;
|
|
||||||
import top.continew.starter.core.constant.StringConstants;
|
import top.continew.starter.core.constant.StringConstants;
|
||||||
import top.continew.starter.log.http.RecordableHttpRequest;
|
import top.continew.starter.log.http.RecordableHttpRequest;
|
||||||
|
|
||||||
@@ -80,20 +77,21 @@ public final class RecordableServletHttpRequest implements RecordableHttpRequest
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getBody() {
|
public String getBody() {
|
||||||
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
|
String body = JakartaServletUtil.getBody(request);
|
||||||
if (null != wrapper) {
|
|
||||||
String body = StrUtil.utf8Str(wrapper.getContentAsByteArray());
|
|
||||||
return JSONUtil.isTypeJSON(body) ? body : null;
|
return JSONUtil.isTypeJSON(body) ? body : null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getParam() {
|
public Map<String, Object> getParam() {
|
||||||
String body = this.getBody();
|
String body = this.getBody();
|
||||||
return CharSequenceUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)
|
return CharSequenceUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)
|
||||||
? JSONUtil.toBean(body, Map.class)
|
? JSONUtil.toBean(body, Map.class)
|
||||||
: Collections.unmodifiableMap(request.getParameterMap());
|
: Collections.unmodifiableMap(JakartaServletUtil.getParamMap(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPath() {
|
||||||
|
return request.getRequestURI();
|
||||||
}
|
}
|
||||||
|
|
||||||
private StringBuilder appendQueryString(String queryString) {
|
private StringBuilder appendQueryString(String queryString) {
|
||||||
|
@@ -17,11 +17,9 @@
|
|||||||
package top.continew.starter.log.http.servlet;
|
package top.continew.starter.log.http.servlet;
|
||||||
|
|
||||||
import cn.hutool.core.text.CharSequenceUtil;
|
import cn.hutool.core.text.CharSequenceUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
import top.continew.starter.core.wrapper.RepeatReadResponseWrapper;
|
||||||
import org.springframework.web.util.WebUtils;
|
|
||||||
import top.continew.starter.log.http.RecordableHttpResponse;
|
import top.continew.starter.log.http.RecordableHttpResponse;
|
||||||
import top.continew.starter.web.util.ServletUtils;
|
import top.continew.starter.web.util.ServletUtils;
|
||||||
|
|
||||||
@@ -56,10 +54,8 @@ public final class RecordableServletHttpResponse implements RecordableHttpRespon
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getBody() {
|
public String getBody() {
|
||||||
ContentCachingResponseWrapper wrapper = WebUtils
|
if (response instanceof RepeatReadResponseWrapper wrapper && !wrapper.isStreamingResponse()) {
|
||||||
.getNativeResponse(response, ContentCachingResponseWrapper.class);
|
String body = wrapper.getResponseContent();
|
||||||
if (null != wrapper) {
|
|
||||||
String body = StrUtil.utf8Str(wrapper.getContentAsByteArray());
|
|
||||||
return JSONUtil.isTypeJSON(body) ? body : null;
|
return JSONUtil.isTypeJSON(body) ? body : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||||
|
* <p>
|
||||||
|
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* <p>
|
||||||
|
* http://www.gnu.org/licenses/lgpl.html
|
||||||
|
* <p>
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package top.continew.starter.log.model;
|
||||||
|
|
||||||
|
import top.continew.starter.log.http.RecordableHttpRequest;
|
||||||
|
import top.continew.starter.log.http.RecordableHttpResponse;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问日志上下文
|
||||||
|
*
|
||||||
|
* @author echo
|
||||||
|
* @since 2.8.3
|
||||||
|
*/
|
||||||
|
public class AccessLogContext {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始时间
|
||||||
|
*/
|
||||||
|
private final Instant startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束时间
|
||||||
|
*/
|
||||||
|
private Instant endTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求信息
|
||||||
|
*/
|
||||||
|
private final RecordableHttpRequest request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应信息
|
||||||
|
*/
|
||||||
|
private final RecordableHttpResponse response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置信息
|
||||||
|
*/
|
||||||
|
private final LogProperties properties;
|
||||||
|
|
||||||
|
private AccessLogContext(Builder builder) {
|
||||||
|
this.startTime = builder.startTime;
|
||||||
|
this.endTime = builder.endTime;
|
||||||
|
this.request = builder.request;
|
||||||
|
this.response = builder.response;
|
||||||
|
this.properties = builder.properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getStartTime() {
|
||||||
|
return startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getEndTime() {
|
||||||
|
return endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecordableHttpRequest getRequest() {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecordableHttpResponse getResponse() {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogProperties getProperties() {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndTime(Instant endTime) {
|
||||||
|
this.endTime = endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private Instant startTime;
|
||||||
|
private Instant endTime;
|
||||||
|
private RecordableHttpRequest request;
|
||||||
|
private RecordableHttpResponse response;
|
||||||
|
private LogProperties properties;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder startTime(Instant startTime) {
|
||||||
|
this.startTime = startTime;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder endTime(Instant endTime) {
|
||||||
|
this.endTime = endTime;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder request(RecordableHttpRequest request) {
|
||||||
|
this.request = request;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder response(RecordableHttpResponse response) {
|
||||||
|
this.response = response;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder properties(LogProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessLogContext build() {
|
||||||
|
return new AccessLogContext(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||||
|
* <p>
|
||||||
|
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* <p>
|
||||||
|
* http://www.gnu.org/licenses/lgpl.html
|
||||||
|
* <p>
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package top.continew.starter.log.model;
|
||||||
|
|
||||||
|
import top.continew.starter.web.util.SpringWebUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问日志输出配置
|
||||||
|
*
|
||||||
|
* @author echo
|
||||||
|
* @since 2.8.3
|
||||||
|
*/
|
||||||
|
public class AccessLogProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否打印日志,开启后可打印访问日志(类似于 Nginx access log)
|
||||||
|
* <p>
|
||||||
|
* 不记录日志也支持开启打印访问日志
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private boolean isPrint = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 放行路由
|
||||||
|
*/
|
||||||
|
private List<String> excludePatterns = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否记录请求参数(body/query/form)
|
||||||
|
* <p>开启后会在日志中输出请求参数</p>
|
||||||
|
*/
|
||||||
|
private boolean isReqParams = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否自动截断超长参数值(如 base64、大文本)
|
||||||
|
* <p>开启后会对超过指定长度的参数值进行截断处理</p>
|
||||||
|
*/
|
||||||
|
private boolean truncateLongParams = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超长参数检测阈值(单位:字符)
|
||||||
|
* <p>当参数值长度超过此值时,触发截断规则</p>
|
||||||
|
* <p>默认:2000</p>
|
||||||
|
*/
|
||||||
|
private int ultraLongParamThreshold = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超长参数最大展示长度(单位:字符)
|
||||||
|
* <p>当参数超过ultraLongParamThreshold时,强制截断到此长度</p>
|
||||||
|
* <p>默认:50</p>
|
||||||
|
*/
|
||||||
|
private int ultraLongParamMaxLength = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截断后追加的后缀符号(如配置 "..." 会让截断内容更直观)
|
||||||
|
* <p>建议配置 3-5 个非占宽字符,默认为空不追加</p>
|
||||||
|
*/
|
||||||
|
private String truncateSuffix = "...";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否过滤敏感参数
|
||||||
|
* <p>开启后会对敏感参数进行过滤,默认不过滤</p>
|
||||||
|
*/
|
||||||
|
private boolean isSensitiveParams = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 敏感参数字段列表(如:password,token,idCard)
|
||||||
|
* <p>支持精确匹配(区分大小写)</p>
|
||||||
|
* <p>示例值:password,oldPassword</p>
|
||||||
|
*/
|
||||||
|
private List<String> sensitiveParamList = new ArrayList<>();
|
||||||
|
|
||||||
|
public boolean isPrint() {
|
||||||
|
return isPrint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrint(boolean print) {
|
||||||
|
isPrint = print;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getExcludePatterns() {
|
||||||
|
return excludePatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExcludePatterns(List<String> excludePatterns) {
|
||||||
|
this.excludePatterns = excludePatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReqParams() {
|
||||||
|
return isReqParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReqParams(boolean reqParams) {
|
||||||
|
isReqParams = reqParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTruncateLongParams() {
|
||||||
|
return truncateLongParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTruncateLongParams(boolean truncateLongParams) {
|
||||||
|
this.truncateLongParams = truncateLongParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUltraLongParamThreshold() {
|
||||||
|
return ultraLongParamThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUltraLongParamThreshold(int ultraLongParamThreshold) {
|
||||||
|
this.ultraLongParamThreshold = ultraLongParamThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUltraLongParamMaxLength() {
|
||||||
|
return ultraLongParamMaxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUltraLongParamMaxLength(int ultraLongParamMaxLength) {
|
||||||
|
this.ultraLongParamMaxLength = ultraLongParamMaxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTruncateSuffix() {
|
||||||
|
return truncateSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTruncateSuffix(String truncateSuffix) {
|
||||||
|
this.truncateSuffix = truncateSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSensitiveParams() {
|
||||||
|
return isSensitiveParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSensitiveParams(boolean sensitiveParams) {
|
||||||
|
isSensitiveParams = sensitiveParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getSensitiveParamList() {
|
||||||
|
return sensitiveParamList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSensitiveParamList(List<String> sensitiveParamList) {
|
||||||
|
this.sensitiveParamList = sensitiveParamList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否匹配放行路由
|
||||||
|
*
|
||||||
|
* @param uri 请求 URI
|
||||||
|
* @return 是否匹配
|
||||||
|
*/
|
||||||
|
public boolean isMatch(String uri) {
|
||||||
|
return this.getExcludePatterns().stream().anyMatch(pattern -> SpringWebUtils.isMatch(uri, pattern));
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,7 @@
|
|||||||
package top.continew.starter.log.model;
|
package top.continew.starter.log.model;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||||
import top.continew.starter.core.constant.PropertiesConstants;
|
import top.continew.starter.core.constant.PropertiesConstants;
|
||||||
import top.continew.starter.log.enums.Include;
|
import top.continew.starter.log.enums.Include;
|
||||||
import top.continew.starter.web.util.SpringWebUtils;
|
import top.continew.starter.web.util.SpringWebUtils;
|
||||||
@@ -40,12 +41,10 @@ public class LogProperties {
|
|||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否打印日志,开启后可打印访问日志(类似于 Nginx access log)
|
* 访问日志配置
|
||||||
* <p>
|
|
||||||
* 不记录日志也支持开启打印访问日志
|
|
||||||
* </p>
|
|
||||||
*/
|
*/
|
||||||
private Boolean isPrint = false;
|
@NestedConfigurationProperty
|
||||||
|
private AccessLogProperties accessLog = new AccessLogProperties();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 包含信息
|
* 包含信息
|
||||||
@@ -65,14 +64,6 @@ public class LogProperties {
|
|||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getIsPrint() {
|
|
||||||
return isPrint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIsPrint(Boolean print) {
|
|
||||||
isPrint = print;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<Include> getIncludes() {
|
public Set<Include> getIncludes() {
|
||||||
return includes;
|
return includes;
|
||||||
}
|
}
|
||||||
@@ -89,6 +80,14 @@ public class LogProperties {
|
|||||||
this.excludePatterns = excludePatterns;
|
this.excludePatterns = excludePatterns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AccessLogProperties getAccessLog() {
|
||||||
|
return accessLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessLog(AccessLogProperties accessLog) {
|
||||||
|
this.accessLog = accessLog;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否匹配放行路由
|
* 是否匹配放行路由
|
||||||
*
|
*
|
||||||
|
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||||
|
* <p>
|
||||||
|
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* <p>
|
||||||
|
* http://www.gnu.org/licenses/lgpl.html
|
||||||
|
* <p>
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package top.continew.starter.log.util;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import top.continew.starter.log.http.RecordableHttpRequest;
|
||||||
|
import top.continew.starter.log.model.AccessLogProperties;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author echo
|
||||||
|
* @since 2025/03/25 19:16
|
||||||
|
**/
|
||||||
|
public class AccessLogUtils {
|
||||||
|
|
||||||
|
public AccessLogUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取参数信息
|
||||||
|
*
|
||||||
|
* @param request 请求
|
||||||
|
* @param properties 属性
|
||||||
|
* @return {@link String }
|
||||||
|
*/
|
||||||
|
public static String getParam(RecordableHttpRequest request, AccessLogProperties properties) {
|
||||||
|
// 是否需要输出参数
|
||||||
|
if (!properties.isReqParams()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数为空返回空
|
||||||
|
Map<String, Object> params = request.getParam();
|
||||||
|
if (ObjectUtil.isEmpty(params) || params.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否需要对特定入参脱敏
|
||||||
|
if (properties.isSensitiveParams()) {
|
||||||
|
params = filterSensitiveParams(params, properties.getSensitiveParamList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否自动截断超长参数值
|
||||||
|
if (properties.isTruncateLongParams()) {
|
||||||
|
params = truncateLongParams(params, properties.getUltraLongParamThreshold(), properties
|
||||||
|
.getUltraLongParamMaxLength(), properties.getTruncateSuffix());
|
||||||
|
}
|
||||||
|
return "param:" + JSONUtil.toJsonStr(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤敏感参数
|
||||||
|
*
|
||||||
|
* @param params 参数 Map
|
||||||
|
* @param sensitiveParams 敏感参数列表
|
||||||
|
* @return 处理后的参数 Map
|
||||||
|
*/
|
||||||
|
private static Map<String, Object> filterSensitiveParams(Map<String, Object> params, List<String> sensitiveParams) {
|
||||||
|
if (params == null || params.isEmpty() || sensitiveParams == null || sensitiveParams.isEmpty()) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> filteredParams = new HashMap<>(params);
|
||||||
|
for (String sensitiveKey : sensitiveParams) {
|
||||||
|
if (filteredParams.containsKey(sensitiveKey)) {
|
||||||
|
filteredParams.put(sensitiveKey, "***");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截断超长参数
|
||||||
|
*
|
||||||
|
* @param params 参数 Map
|
||||||
|
* @param threshold 截断阈值(值长度超过该值才截断)
|
||||||
|
* @param maxLength 最大长度
|
||||||
|
* @param suffix 后缀(如 "...")
|
||||||
|
* @return 处理后的参数 Map
|
||||||
|
*/
|
||||||
|
private static Map<String, Object> truncateLongParams(Map<String, Object> params,
|
||||||
|
int threshold,
|
||||||
|
int maxLength,
|
||||||
|
String suffix) {
|
||||||
|
if (params == null || params.isEmpty()) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> truncatedParams = new HashMap<>(params);
|
||||||
|
for (Map.Entry<String, Object> entry : truncatedParams.entrySet()) {
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof String strValue) {
|
||||||
|
if (strValue.length() > threshold) {
|
||||||
|
entry.setValue(strValue.substring(0, Math.min(strValue.length(), maxLength)) + suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return truncatedParams;
|
||||||
|
}
|
||||||
|
}
|
@@ -30,8 +30,8 @@ import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
|
|||||||
import top.continew.starter.log.dao.LogDao;
|
import top.continew.starter.log.dao.LogDao;
|
||||||
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
|
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
|
||||||
import top.continew.starter.log.handler.InterceptorLogHandler;
|
import top.continew.starter.log.handler.InterceptorLogHandler;
|
||||||
import top.continew.starter.log.LogFilter;
|
import top.continew.starter.log.filter.LogFilter;
|
||||||
import top.continew.starter.log.LogHandler;
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
import top.continew.starter.log.interceptor.LogInterceptor;
|
import top.continew.starter.log.interceptor.LogInterceptor;
|
||||||
import top.continew.starter.log.model.LogProperties;
|
import top.continew.starter.log.model.LogProperties;
|
||||||
|
|
||||||
|
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package top.continew.starter.log.handler;
|
package top.continew.starter.log.handler;
|
||||||
|
|
||||||
import top.continew.starter.log.AbstractLogHandler;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志处理器-拦截器版实现
|
* 日志处理器-拦截器版实现
|
||||||
*
|
*
|
||||||
|
@@ -27,13 +27,15 @@ import org.springframework.lang.NonNull;
|
|||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import top.continew.starter.log.annotation.Log;
|
import top.continew.starter.log.annotation.Log;
|
||||||
|
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
|
||||||
|
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
|
||||||
|
import top.continew.starter.log.model.AccessLogContext;
|
||||||
import top.continew.starter.log.model.LogProperties;
|
import top.continew.starter.log.model.LogProperties;
|
||||||
import top.continew.starter.log.dao.LogDao;
|
import top.continew.starter.log.dao.LogDao;
|
||||||
import top.continew.starter.log.LogHandler;
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
import top.continew.starter.log.model.LogRecord;
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,10 +64,11 @@ public class LogInterceptor implements HandlerInterceptor {
|
|||||||
@NonNull HttpServletResponse response,
|
@NonNull HttpServletResponse response,
|
||||||
@NonNull Object handler) {
|
@NonNull Object handler) {
|
||||||
Instant startTime = Instant.now();
|
Instant startTime = Instant.now();
|
||||||
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
|
logHandler.processAccessLogStartReq(AccessLogContext.builder()
|
||||||
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
|
.startTime(startTime)
|
||||||
timeTtl.set(startTime);
|
.request(new RecordableServletHttpRequest(request))
|
||||||
}
|
.properties(logProperties)
|
||||||
|
.build());
|
||||||
// 开始日志记录
|
// 开始日志记录
|
||||||
if (this.isRequestRecord(handler, request)) {
|
if (this.isRequestRecord(handler, request)) {
|
||||||
LogRecord.Started startedLogRecord = logHandler.start(startTime, request);
|
LogRecord.Started startedLogRecord = logHandler.start(startTime, request);
|
||||||
@@ -81,11 +84,10 @@ public class LogInterceptor implements HandlerInterceptor {
|
|||||||
Exception e) {
|
Exception e) {
|
||||||
try {
|
try {
|
||||||
Instant endTime = Instant.now();
|
Instant endTime = Instant.now();
|
||||||
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
|
logHandler.processAccessLogEndReq(AccessLogContext.builder()
|
||||||
Duration timeTaken = Duration.between(timeTtl.get(), endTime);
|
.endTime(endTime)
|
||||||
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), response
|
.response(new RecordableServletHttpResponse(response, response.getStatus()))
|
||||||
.getStatus(), timeTaken.toMillis());
|
.build());
|
||||||
}
|
|
||||||
LogRecord.Started startedLogRecord = logTtl.get();
|
LogRecord.Started startedLogRecord = logTtl.get();
|
||||||
if (null == startedLogRecord) {
|
if (null == startedLogRecord) {
|
||||||
return;
|
return;
|
||||||
|
Reference in New Issue
Block a user