mirror of
				https://github.com/continew-org/continew-starter.git
				synced 2025-10-31 10:57:15 +08:00 
			
		
		
		
	fix(core): 修复 application/x-www-form-urlencoded 请求体数据无法在 Controller 层获取的问题
This commit is contained in:
		| @@ -16,89 +16,216 @@ | |||||||
|  |  | ||||||
| package top.continew.starter.core.wrapper; | package top.continew.starter.core.wrapper; | ||||||
|  |  | ||||||
| import cn.hutool.core.io.IoUtil; |  | ||||||
| import jakarta.servlet.ReadListener; | import jakarta.servlet.ReadListener; | ||||||
| import jakarta.servlet.ServletInputStream; | import jakarta.servlet.ServletInputStream; | ||||||
|  | import jakarta.servlet.ServletRequest; | ||||||
| import jakarta.servlet.http.HttpServletRequest; | import jakarta.servlet.http.HttpServletRequest; | ||||||
| import jakarta.servlet.http.HttpServletRequestWrapper; | import jakarta.servlet.http.HttpServletRequestWrapper; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
|  | import org.springframework.util.FastByteArrayOutputStream; | ||||||
|  | import org.springframework.util.StreamUtils; | ||||||
|  | import top.continew.starter.core.constant.StringConstants; | ||||||
|  |  | ||||||
| import java.io.BufferedReader; | import java.io.*; | ||||||
| import java.io.ByteArrayInputStream; | import java.net.URLEncoder; | ||||||
| import java.io.IOException; | import java.nio.charset.Charset; | ||||||
| import java.io.InputStreamReader; |  | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Iterator; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 可重复读取请求体的包装器 |  * 可重复读取请求体的包装器 支持文件流直接透传,非文件流可重复读取 | ||||||
|  * 支持文件流直接透传,非文件流可重复读取 |  * <p> | ||||||
|  |  * 虽然这里可以多次读取流里面的数据, 但是建议还是调用getContentAsString()/getCachedContent() 方法, 已经把内容缓存在内存中了。 | ||||||
|  |  * </p> | ||||||
|  * |  * | ||||||
|  * @author echo |  * @author Jasmine | ||||||
|  * @since 2.10.0 |  * @since 2.12.1 | ||||||
|  */ |  */ | ||||||
| public class RepeatReadRequestWrapper extends HttpServletRequestWrapper { | public class RepeatReadRequestWrapper extends HttpServletRequestWrapper { | ||||||
|  |  | ||||||
|     private byte[] cachedBody; |     /** | ||||||
|     private final HttpServletRequest originalRequest; |      * 缓存内容 | ||||||
|  |      */ | ||||||
|  |     private final FastByteArrayOutputStream cachedContent; | ||||||
|  |  | ||||||
|  |     /*** 用于缓存输入流 */ | ||||||
|  |     private ContentCachingInputStream contentCachingInputStream; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 字符编码 | ||||||
|  |      */ | ||||||
|  |     private final String characterEncoding; | ||||||
|  |  | ||||||
|  |     // private BufferedReader reader; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Constructs a request object wrapping the given request. | ||||||
|  |      * | ||||||
|  |      * @param request the {@link HttpServletRequest} to be wrapped. | ||||||
|  |      * @throws IllegalArgumentException if the request is null | ||||||
|  |      */ | ||||||
|     public RepeatReadRequestWrapper(HttpServletRequest request) throws IOException { |     public RepeatReadRequestWrapper(HttpServletRequest request) throws IOException { | ||||||
|         super(request); |         super(request); | ||||||
|         this.originalRequest = request; |         this.characterEncoding = request.getCharacterEncoding() != null | ||||||
|  |             ? request.getCharacterEncoding() | ||||||
|  |             : StandardCharsets.UTF_8.name(); | ||||||
|  |         int contentLength = super.getRequest().getContentLength(); | ||||||
|  |         cachedContent = (contentLength > 0) | ||||||
|  |             ? new FastByteArrayOutputStream(contentLength) | ||||||
|  |             : new FastByteArrayOutputStream(); | ||||||
|         // 判断是否为文件上传请求 |         // 判断是否为文件上传请求 | ||||||
|         if (!isMultipartContent(request)) { |         if (!isMultipartContent(request)) { | ||||||
|             this.cachedBody = IoUtil.readBytes(request.getInputStream(), false); |             if (isFormRequest()) { | ||||||
|  |                 writeRequestParametersToCachedContent(); | ||||||
|  |             } else { | ||||||
|  |                 StreamUtils.copy(request.getInputStream(), cachedContent); | ||||||
|  |             } | ||||||
|  |             contentCachingInputStream = new ContentCachingInputStream(cachedContent.toByteArray()); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public ServletInputStream getInputStream() throws IOException { |     public ServletInputStream getInputStream() throws IOException { | ||||||
|         // 如果是文件上传,直接返回原始输入流 |         // 如果是文件上传,直接返回原始输入流 | ||||||
|         if (isMultipartContent(originalRequest)) { |         if (isMultipartContent(super.getRequest())) { | ||||||
|             return originalRequest.getInputStream(); |             return super.getRequest().getInputStream(); | ||||||
|         } |         } | ||||||
|  |         synchronized (this) { | ||||||
|         // 非文件上传,返回可重复读取的输入流 |             contentCachingInputStream.reset(); | ||||||
|         final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody); |             return contentCachingInputStream; | ||||||
|  |  | ||||||
|         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 |     @Override | ||||||
|     public BufferedReader getReader() throws IOException { |     public BufferedReader getReader() throws IOException { | ||||||
|         // 如果是文件上传,直接返回原始Reader |         // 如果是文件上传,直接返回原始Reader | ||||||
|         if (isMultipartContent(originalRequest)) { |         if (isMultipartContent(super.getRequest())) { | ||||||
|             new BufferedReader(new InputStreamReader(originalRequest.getInputStream(), StandardCharsets.UTF_8)); |             return super.getRequest().getReader(); | ||||||
|         } |         } | ||||||
|         return new BufferedReader(new InputStreamReader(getInputStream())); |  | ||||||
|  |         // BufferedReader不支持多次reset()(除非手动调用 mark() 并控制其生命周期),最安全的方式是每次调用getReader()时基于缓存内容重新创建一个新的BufferedReader实例。 | ||||||
|  |         synchronized (this) { | ||||||
|  |             return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding())); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void writeRequestParametersToCachedContent() { | ||||||
|  |         try { | ||||||
|  |             if (this.cachedContent.size() == 0) { | ||||||
|  |                 Map<String, String[]> form = super.getParameterMap(); | ||||||
|  |                 for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) { | ||||||
|  |                     String name = nameIterator.next(); | ||||||
|  |                     List<String> values = Arrays.asList(form.get(name)); | ||||||
|  |                     for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) { | ||||||
|  |                         String value = valueIterator.next(); | ||||||
|  |                         this.cachedContent.write(URLEncoder.encode(name, characterEncoding).getBytes()); | ||||||
|  |                         if (value != null) { | ||||||
|  |                             this.cachedContent.write(StringConstants.EQUALS.getBytes()); | ||||||
|  |                             this.cachedContent.write(URLEncoder.encode(value, characterEncoding).getBytes()); | ||||||
|  |                             if (valueIterator.hasNext()) { | ||||||
|  |                                 this.cachedContent.write(StringConstants.AMP.getBytes()); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     if (nameIterator.hasNext()) { | ||||||
|  |                         this.cachedContent.write(StringConstants.AMP.getBytes()); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (IOException ex) { | ||||||
|  |             throw new IllegalStateException("Failed to write request parameters to cached content", ex); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String getCharacterEncoding() { | ||||||
|  |         return this.characterEncoding; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getContentAsString() { | ||||||
|  |         return this.cachedContent.toString(Charset.forName(getCharacterEncoding())); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public FastByteArrayOutputStream getCachedContent() { | ||||||
|  |         return cachedContent; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 检查是否为文件上传请求 |      * 判断当前请求是否为 multipart/form-data 类型的文件上传请求。 该类型一般用于表单上传文件的场景,例如 enctype="multipart/form-data"。 | ||||||
|      * |      * | ||||||
|      * @param request 请求对象 |      * @param request 当前 HTTP 请求对象 | ||||||
|      * @return 是否为文件上传请求 |      * @return true 表示为 multipart 文件上传请求;否则为 false | ||||||
|      */ |      */ | ||||||
|     public boolean isMultipartContent(HttpServletRequest request) { |     public boolean isMultipartContent(ServletRequest request) { | ||||||
|         return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/"); |         String contentType = request.getContentType(); | ||||||
|  |         return contentType != null && contentType.toLowerCase().startsWith("multipart/"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean isFormRequest() { | ||||||
|  |         String contentType = getContentType(); | ||||||
|  |         return (contentType != null && contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Body 缓存的ServletInputStream实现 DefaultServerRequestBuilder.BodyInputStream | ||||||
|  |      */ | ||||||
|  |     private static class ContentCachingInputStream extends ServletInputStream { | ||||||
|  |  | ||||||
|  |         private final InputStream delegate; | ||||||
|  |  | ||||||
|  |         public ContentCachingInputStream(byte[] body) { | ||||||
|  |             this.delegate = new ByteArrayInputStream(body); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public boolean isFinished() { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public boolean isReady() { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setReadListener(ReadListener readListener) { | ||||||
|  |             throw new UnsupportedOperationException(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public int read() throws IOException { | ||||||
|  |             return this.delegate.read(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public int read(byte[] b, int off, int len) throws IOException { | ||||||
|  |             return this.delegate.read(b, off, len); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public int read(byte[] b) throws IOException { | ||||||
|  |             return this.delegate.read(b); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public long skip(long n) throws IOException { | ||||||
|  |             return this.delegate.skip(n); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public int available() throws IOException { | ||||||
|  |             return this.delegate.available(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void close() throws IOException { | ||||||
|  |             this.delegate.close(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public synchronized void mark(int readlimit) { | ||||||
|  |             this.delegate.mark(readlimit); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public synchronized void reset() throws IOException { | ||||||
|  |             this.delegate.reset(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public boolean markSupported() { | ||||||
|  |             return this.delegate.markSupported(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 liquor
					liquor