Skip to content

Commit da5e162

Browse files
Time-wCharles7c
authored andcommitted
feat(core): 新增请求响应可重复读流处理并优化日志模块
增加访问日志打印处理:包括参数打印、过滤敏感参数和超长参数配置
1 parent 1903520 commit da5e162

File tree

20 files changed

+811
-101
lines changed

20 files changed

+811
-101
lines changed

continew-starter-core/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
<artifactId>spring-boot-configuration-processor</artifactId>
2424
</dependency>
2525

26+
<!-- servlet包 -->
27+
<dependency>
28+
<groupId>jakarta.servlet</groupId>
29+
<artifactId>jakarta.servlet-api</artifactId>
30+
</dependency>
31+
2632
<!-- Hibernate Validator -->
2733
<dependency>
2834
<groupId>org.hibernate.validator</groupId>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
* <p>
4+
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.gnu.org/licenses/lgpl.html
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.starter.core.wrapper;
18+
19+
import cn.hutool.core.io.IoUtil;
20+
import jakarta.servlet.ReadListener;
21+
import jakarta.servlet.ServletInputStream;
22+
import jakarta.servlet.http.HttpServletRequest;
23+
import jakarta.servlet.http.HttpServletRequestWrapper;
24+
25+
import java.io.BufferedReader;
26+
import java.io.ByteArrayInputStream;
27+
import java.io.IOException;
28+
import java.io.InputStreamReader;
29+
import java.nio.charset.StandardCharsets;
30+
31+
/**
32+
* 可重复读取请求体的包装器
33+
* 支持文件流直接透传,非文件流可重复读取
34+
*
35+
* @author echo
36+
* @since 2025/03/25 11:11
37+
**/
38+
public class RepeatReadRequestWrapper extends HttpServletRequestWrapper {
39+
40+
private byte[] cachedBody;
41+
private final HttpServletRequest originalRequest;
42+
43+
public RepeatReadRequestWrapper(HttpServletRequest request) throws IOException {
44+
super(request);
45+
this.originalRequest = request;
46+
47+
// 判断是否为文件上传请求
48+
if (!isMultipartContent(request)) {
49+
this.cachedBody = IoUtil.readBytes(request.getInputStream(), false);
50+
}
51+
}
52+
53+
/**
54+
* 检查是否为文件上传请求
55+
*/
56+
private boolean isMultipartContent(HttpServletRequest request) {
57+
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/");
58+
}
59+
60+
@Override
61+
public ServletInputStream getInputStream() throws IOException {
62+
// 如果是文件上传,直接返回原始输入流
63+
if (isMultipartContent(originalRequest)) {
64+
return originalRequest.getInputStream();
65+
}
66+
67+
// 非文件上传,返回可重复读取的输入流
68+
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
69+
70+
return new ServletInputStream() {
71+
@Override
72+
public boolean isFinished() {
73+
return byteArrayInputStream.available() == 0;
74+
}
75+
76+
@Override
77+
public boolean isReady() {
78+
return true;
79+
}
80+
81+
@Override
82+
public void setReadListener(ReadListener readListener) {
83+
// 非阻塞I/O,这里可以根据需要实现
84+
}
85+
86+
@Override
87+
public int read() {
88+
return byteArrayInputStream.read();
89+
}
90+
};
91+
}
92+
93+
@Override
94+
public BufferedReader getReader() throws IOException {
95+
// 如果是文件上传,直接返回原始Reader
96+
if (isMultipartContent(originalRequest)) {
97+
return originalRequest.getReader();
98+
}
99+
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(cachedBody), StandardCharsets.UTF_8));
100+
}
101+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
3+
* <p>
4+
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.gnu.org/licenses/lgpl.html
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package top.continew.starter.core.wrapper;
18+
19+
import jakarta.servlet.ServletOutputStream;
20+
import jakarta.servlet.WriteListener;
21+
import jakarta.servlet.http.HttpServletResponse;
22+
import jakarta.servlet.http.HttpServletResponseWrapper;
23+
24+
import java.io.ByteArrayOutputStream;
25+
import java.io.IOException;
26+
import java.io.PrintWriter;
27+
import java.nio.charset.StandardCharsets;
28+
29+
/**
30+
* 可重复读取响应内容的包装器
31+
* 支持缓存响应内容,便于日志记录和后续处理 (不缓存SSE)
32+
*
33+
* @author echo
34+
* @since 2025/03/25 11:11
35+
**/
36+
public class RepeatReadResponseWrapper extends HttpServletResponseWrapper {
37+
38+
private final ByteArrayOutputStream cachedOutputStream = new ByteArrayOutputStream();
39+
private final PrintWriter writer = new PrintWriter(cachedOutputStream, true);
40+
41+
// 是否为流式响应
42+
private boolean isStreamingResponse = false;
43+
44+
public RepeatReadResponseWrapper(HttpServletResponse response) {
45+
super(response);
46+
checkStreamingResponse();
47+
}
48+
49+
@Override
50+
public void setContentType(String type) {
51+
super.setContentType(type);
52+
// 根据 Content-Type 判断是否为流式响应
53+
if (type != null) {
54+
String lowerType = type.toLowerCase();
55+
isStreamingResponse = lowerType.contains("text/event-stream");
56+
}
57+
}
58+
59+
private void checkStreamingResponse() {
60+
String contentType = getContentType();
61+
if (contentType != null) {
62+
String lowerType = contentType.toLowerCase();
63+
isStreamingResponse = lowerType.contains("text/event-stream");
64+
}
65+
}
66+
67+
@Override
68+
public ServletOutputStream getOutputStream() throws IOException {
69+
checkStreamingResponse();
70+
if (isStreamingResponse) {
71+
// 对于 SSE 流式响应,直接返回原始响应流,不做额外处理
72+
return super.getOutputStream();
73+
}
74+
return new ServletOutputStream() {
75+
@Override
76+
public boolean isReady() {
77+
return true;
78+
}
79+
80+
@Override
81+
public void setWriteListener(WriteListener writeListener) {
82+
}
83+
84+
@Override
85+
public void write(int b) throws IOException {
86+
cachedOutputStream.write(b);
87+
}
88+
89+
@Override
90+
public void write(byte[] b) throws IOException {
91+
cachedOutputStream.write(b);
92+
}
93+
94+
@Override
95+
public void write(byte[] b, int off, int len) throws IOException {
96+
cachedOutputStream.write(b, off, len);
97+
}
98+
};
99+
}
100+
101+
@Override
102+
public PrintWriter getWriter() throws IOException {
103+
checkStreamingResponse();
104+
if (isStreamingResponse) {
105+
// 对于 SSE 流式响应,直接返回原始响应写入器,不做额外处理
106+
return super.getWriter();
107+
}
108+
return writer;
109+
}
110+
111+
public String getResponseContent() {
112+
if (!isStreamingResponse) {
113+
writer.flush();
114+
return cachedOutputStream.toString(StandardCharsets.UTF_8);
115+
}
116+
return null;
117+
}
118+
119+
public void copyBodyToResponse() throws IOException {
120+
if (!isStreamingResponse && cachedOutputStream.size() > 0) {
121+
getResponse().getOutputStream().write(cachedOutputStream.toByteArray());
122+
}
123+
}
124+
125+
public boolean isStreamingResponse() {
126+
return isStreamingResponse;
127+
}
128+
}

continew-starter-log/continew-starter-log-aop/src/main/java/top/continew/starter/log/aspect/AccessLogAspect.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
import org.slf4j.LoggerFactory;
2727
import org.springframework.web.context.request.RequestContextHolder;
2828
import org.springframework.web.context.request.ServletRequestAttributes;
29+
import top.continew.starter.log.handler.LogHandler;
30+
import top.continew.starter.log.http.servlet.RecordableServletHttpRequest;
31+
import top.continew.starter.log.http.servlet.RecordableServletHttpResponse;
32+
import top.continew.starter.log.model.AccessLogContext;
2933
import top.continew.starter.log.model.LogProperties;
3034

31-
import java.time.Duration;
3235
import java.time.Instant;
3336

3437
/**
@@ -43,9 +46,11 @@ public class AccessLogAspect {
4346

4447
private static final Logger log = LoggerFactory.getLogger(AccessLogAspect.class);
4548
private final LogProperties logProperties;
49+
private final LogHandler logHandler;
4650

47-
public AccessLogAspect(LogProperties logProperties) {
51+
public AccessLogAspect(LogProperties logProperties, LogHandler logHandler) {
4852
this.logProperties = logProperties;
53+
this.logHandler = logHandler;
4954
}
5055

5156
/**
@@ -108,19 +113,18 @@ public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
108113
HttpServletRequest request = attributes.getRequest();
109114
HttpServletResponse response = attributes.getResponse();
110115
try {
111-
// 打印请求日志
112-
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
113-
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
114-
}
116+
logHandler.processAccessLogStartReq(AccessLogContext.builder()
117+
.startTime(startTime)
118+
.request(new RecordableServletHttpRequest(request))
119+
.properties(logProperties)
120+
.build());
115121
return joinPoint.proceed();
116122
} finally {
117123
Instant endTime = Instant.now();
118-
if (Boolean.TRUE.equals(logProperties.getIsPrint())) {
119-
Duration timeTaken = Duration.between(startTime, endTime);
120-
log.info("[{}] {} {} {}ms", request.getMethod(), request.getRequestURI(), response != null
121-
? response.getStatus()
122-
: "N/A", timeTaken.toMillis());
123-
}
124+
logHandler.processAccessLogEndReq(AccessLogContext.builder()
125+
.endTime(endTime)
126+
.response(new RecordableServletHttpResponse(response, response.getStatus()))
127+
.build());
124128
}
125129
}
126130
}

continew-starter-log/continew-starter-log-aop/src/main/java/top/continew/starter/log/aspect/LogAspect.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import org.springframework.web.context.request.ServletRequestAttributes;
3333
import top.continew.starter.log.annotation.Log;
3434
import top.continew.starter.log.dao.LogDao;
35-
import top.continew.starter.log.LogHandler;
35+
import top.continew.starter.log.handler.LogHandler;
3636
import top.continew.starter.log.model.LogProperties;
3737
import top.continew.starter.log.model.LogRecord;
3838
import top.continew.starter.web.util.SpringWebUtils;

continew-starter-log/continew-starter-log-aop/src/main/java/top/continew/starter/log/autoconfigure/LogAutoConfiguration.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
import top.continew.starter.log.aspect.LogAspect;
3030
import top.continew.starter.log.dao.LogDao;
3131
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
32+
import top.continew.starter.log.filter.LogFilter;
3233
import top.continew.starter.log.handler.AopLogHandler;
33-
import top.continew.starter.log.LogFilter;
34-
import top.continew.starter.log.LogHandler;
34+
import top.continew.starter.log.handler.LogHandler;
3535
import top.continew.starter.log.model.LogProperties;
3636

3737
/**
@@ -49,9 +49,11 @@ public class LogAutoConfiguration {
4949

5050
private static final Logger log = LoggerFactory.getLogger(LogAutoConfiguration.class);
5151
private final LogProperties logProperties;
52+
private final LogHandler logHandler;
5253

53-
public LogAutoConfiguration(LogProperties logProperties) {
54+
public LogAutoConfiguration(LogProperties logProperties, LogHandler logHandler) {
5455
this.logProperties = logProperties;
56+
this.logHandler = logHandler;
5557
}
5658

5759
/**
@@ -66,13 +68,12 @@ public LogFilter logFilter() {
6668
/**
6769
* 日志切面
6870
*
69-
* @param logHandler 日志处理器
70-
* @param logDao 日志持久层接口
71+
* @param logDao 日志持久层接口
7172
* @return {@link LogAspect }
7273
*/
7374
@Bean
7475
@ConditionalOnMissingBean
75-
public LogAspect logAspect(LogHandler logHandler, LogDao logDao) {
76+
public LogAspect logAspect(LogDao logDao) {
7677
return new LogAspect(logProperties, logHandler, logDao);
7778
}
7879

@@ -84,7 +85,7 @@ public LogAspect logAspect(LogHandler logHandler, LogDao logDao) {
8485
@Bean
8586
@ConditionalOnMissingBean
8687
public AccessLogAspect accessLogAspect() {
87-
return new AccessLogAspect(logProperties);
88+
return new AccessLogAspect(logProperties, logHandler);
8889
}
8990

9091
/**

continew-starter-log/continew-starter-log-aop/src/main/java/top/continew/starter/log/handler/AopLogHandler.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package top.continew.starter.log.handler;
1818

19-
import top.continew.starter.log.AbstractLogHandler;
20-
2119
/**
2220
* 日志处理器-AOP 版实现
2321
*

0 commit comments

Comments
 (0)