mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-11 06:57:14 +08:00
refactor(web): 请求响应可重复读流处理由 core 调整到 web 模块
This commit is contained in:
@@ -23,6 +23,12 @@
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- AOP(AspectJ) -->
|
||||
<dependency>
|
||||
<groupId>org.aspectj</groupId>
|
||||
<artifactId>aspectjweaver</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Hibernate Validator -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
@@ -59,12 +65,5 @@
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-http</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Jakarta(原 Javax Servlet) -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -40,45 +40,40 @@ public class PropertiesConstants {
|
||||
public static final String SECURITY = CONTINEW_STARTER + StringConstants.DOT + "security";
|
||||
|
||||
/**
|
||||
* 密码编解码配置
|
||||
* 安全-密码编解码配置
|
||||
*/
|
||||
public static final String SECURITY_PASSWORD = SECURITY + StringConstants.DOT + "password";
|
||||
|
||||
/**
|
||||
* 加/解密配置
|
||||
* 安全-加/解密配置
|
||||
*/
|
||||
public static final String SECURITY_CRYPTO = SECURITY + StringConstants.DOT + "crypto";
|
||||
|
||||
/**
|
||||
* 敏感词配置
|
||||
* 安全-敏感词配置
|
||||
*/
|
||||
public static final String SECURITY_SENSITIVE_WORDS = SECURITY + StringConstants.DOT + "sensitive-words";
|
||||
|
||||
/**
|
||||
* 安全-XSS 配置
|
||||
*/
|
||||
public static final String SECURITY_XSS = SECURITY + StringConstants.DOT + "xss";
|
||||
|
||||
/**
|
||||
* Web 配置
|
||||
*/
|
||||
public static final String WEB = CONTINEW_STARTER + StringConstants.DOT + "web";
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
* Web-跨域配置
|
||||
*/
|
||||
public static final String WEB_CORS = WEB + StringConstants.DOT + "cors";
|
||||
|
||||
/**
|
||||
* 响应配置
|
||||
* Web-响应配置
|
||||
*/
|
||||
public static final String WEB_RESPONSE = WEB + StringConstants.DOT + "response";
|
||||
|
||||
/**
|
||||
* 链路配置
|
||||
*/
|
||||
public static final String WEB_TRACE = WEB + StringConstants.DOT + "trace";
|
||||
|
||||
/**
|
||||
* XSS 配置
|
||||
*/
|
||||
public static final String WEB_XSS = WEB + StringConstants.DOT + "xss";
|
||||
|
||||
/**
|
||||
* 日志配置
|
||||
*/
|
||||
@@ -89,11 +84,6 @@ public class PropertiesConstants {
|
||||
*/
|
||||
public static final String STORAGE = CONTINEW_STARTER + StringConstants.DOT + "storage";
|
||||
|
||||
/**
|
||||
* 本地存储配置
|
||||
*/
|
||||
public static final String STORAGE_LOCAL = STORAGE + StringConstants.DOT + "local";
|
||||
|
||||
/**
|
||||
* 验证码配置
|
||||
*/
|
||||
@@ -144,6 +134,11 @@ public class PropertiesConstants {
|
||||
*/
|
||||
public static final String IDEMPOTENT = CONTINEW_STARTER + StringConstants.DOT + "idempotent";
|
||||
|
||||
/**
|
||||
* 链路追踪配置
|
||||
*/
|
||||
public static final String TRACE = CONTINEW_STARTER + StringConstants.DOT + "trace";
|
||||
|
||||
private PropertiesConstants() {
|
||||
}
|
||||
}
|
||||
|
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* 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 2.10.0
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为文件上传请求
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return 是否为文件上传请求
|
||||
*/
|
||||
private boolean isMultipartContent(HttpServletRequest request) {
|
||||
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/");
|
||||
}
|
||||
}
|
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
* @author Charles7c
|
||||
* @since 2.10.0
|
||||
*/
|
||||
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();
|
||||
// 对于 SSE 流式响应,直接返回原始响应流,不做额外处理
|
||||
if (isStreamingResponse) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的响应内容
|
||||
*
|
||||
* @return 缓存的响应内容
|
||||
*/
|
||||
public String getResponseContent() {
|
||||
if (!isStreamingResponse) {
|
||||
writer.flush();
|
||||
return cachedOutputStream.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将缓存的响应内容复制到原始响应中
|
||||
*
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
public void copyBodyToResponse() throws IOException {
|
||||
if (!isStreamingResponse && cachedOutputStream.size() > 0) {
|
||||
getResponse().getOutputStream().write(cachedOutputStream.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为流式响应
|
||||
*
|
||||
* @return 是否为流式响应
|
||||
*/
|
||||
public boolean isStreamingResponse() {
|
||||
return isStreamingResponse;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user