ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MultipartFile重复读取难题:三种方案彻底解决Spring Boot文件上传流只能读一次的问题

MultipartFile重复读取难题:三种方案彻底解决Spring Boot文件上传流只能读一次的问题 能不能在同一个请求里把一个 MultipartFile 连续读两遍我最早遇到这个问题是在做一个导入功能的时候。需求本身很简单先读 Excel 校验模板格式校验通过之后再正式解析数据入库。第一版代码写得也很快校验逻辑读一次流数据解析又读一次流本地自测一切正常结果一到联调环境就翻车第二段代码拿到的InputStream读出来的内容全是空的文件大小对不上个别时候直接抛FileNotFoundException。后来排查了一圈才发现问题根本不在数据库也不在逻辑层而是我把MultipartFile当成普通数据对象来用了。它本质上是包装了临时文件或内存缓冲的输入流流这个东西读完一次就到底了不会自己倒带。这个问题在 Spring Boot 文件上传场景里特别常见尤其是涉及文件内容校验、异步处理、日志记录、以及多个业务环节都要访问文件内容的时候。这篇文章就是来把所有方案摊开讲清楚的。核心會覆盖三种主流做法getBytes()缓存到内存、transferTo()落临时文件、手动缓存InputStream副本。同时会把各种方案背后的原理、内存开销、适用场景、以及我踩过的那些坑都写在里面。无论你是刚接触 Spring Boot 文件上传的新手还是已经在生产环境里写过类似功能的开发者这篇文章应该都能给你一些可以参考的实践思路。1. 先搞清楚到底什么时候才需要重复读取很多人在写文件上传接口的时候根本不会意识到 MultipartFile 存在重复读取的问题。因为最常规的用法就是直接拿到MultipartFile调用一次transferTo()把文件保存到服务器指定目录然后就结束了整个过程只读取一次文件内容完全不会踩到流关闭或流耗尽的坑。但实际业务一旦复杂起来情况就不一样了。我在项目里遇到过的典型场景大概有这几类先校验后入库比如导入 Excel 时要先解析模板、校验表头、校验必填列、校验数据类型全部通过后再解析数据行去批量入库。校验阶段读了一次流正式解析阶段又要再读一次内容。这就是最典型的重复读取需求。内容安全扫描上传的文件要先扫毒或做内容检测检测通过后再保存到 OSS 或者本地磁盘。检测进程读取一次文件存储逻辑又要再读一次文件。请求日志留痕部分涉及敏感操作的接口要求把上传文件的内容摘要比如 MD5或文件本身记录到日志系统里方便事后审计。文件解析和日志记录各需要一份内容。异步任务处理接口接收文件后立即返回响应丢到线程池里异步处理文件内容。如果直接把MultipartFile对象传给异步线程很容易出现请求线程结束、临时文件被清理后异步线程啥也读不到的尴尬。多服务间转发从一个服务接收文件又要转发给另一个服务同时本服务还要基于文件内容做一份统计两边都需要文件内容。如果把这些需求直接叠加到一段代码上就会遇到同一个核心矛盾MultipartFile.getInputStream()返回的流只能从头到尾消费一次。你第一次把流读到末尾第二次再调用getInputStream()虽然会重新返回一个新的流对象但读取结果是空的或者直接触到底层临时文件已经被标记删除的异常。所以要判断自己是不是真需求最好的办法是看文件内容被消费的次数。如果文件内容要被两个以上的业务环节消费那就不存在侥幸不可一定得做重复读取的准备。如果只是一次读取后保存文件那最简单老老实实transferTo()就行完全不需要引入额外复杂度。2. 第一个坑为什么 MultipartFile 第二次就读不到内容了要理解这个问题得稍微深入到 MultipartFile 在 Servlet 规范里的实现机制。MultipartFile 并不是把整个文件内容都堆在 Java 堆内存里的一个普通对象而是对底层实现的一种抽象它背后可能是内存缓冲也可能是磁盘上的临时文件具体由容器的配置和上传文件大小决定。以 Spring Boot 内置的 Tomcat 为例文件上传处理的核心逻辑在org.apache.catalina.connector.Request的解析流程中底层调用了DiskFileItem或FileItemStream。当上传的文件很小时文件内容会直接存在内存里的字节数组缓冲区中当文件大小超过阈值默认是 0但 Spring Boot 中有单独配置比如spring.servlet.multipart.file-size-threshold解析器会把文件内容写入磁盘上的临时文件。无论存在哪里最终都是通过一个输入流来访问文件内容。MultipartFile.getInputStream()拿到的流是一个InputStream实例这个实例内部维护了一个指针记录当前读取的位置。流被消费一次之后指针就到了流末尾再次读取自然拿不到任何新内容。个别实现里第二次getInputStream()可能还会重新创建一个新的流但底层临时文件可能已经被标记清理结果就是读取直接抛异常。我做过的错误示范代码大概是这样的相信很多同学也写过类似的PostMapping(/import) public String importFile(RequestParam(file) MultipartFile file) throws IOException { // 第一步校验文件大小和扩展名这里先读了一次文件内容 byte[] headerBytes new byte[8]; try (InputStream is file.getInputStream()) { is.read(headerBytes); if (!isExcelHeader(headerBytes)) { return 文件格式不正确; } } // 第二步真正解析文件内容这里第二次读取发现内容为空 try (InputStream is file.getInputStream()) { ListRow rows ExcelParser.parse(is); // 这里拿到的是空流 // 后续逻辑直接炸了 } return success; }这段代码运行起来的表现会有很强的迷惑性。如果文件小到被容器放在内存里第二次读取得到的流可能是从内存缓冲区重新创建的所以某些时刻能读到内容某些时刻则为空表现不稳定。如果文件较大落到临时文件第二次读取时可能直接报FileNotFoundException因为请求处理结束前临时文件管理机制已经行动了或者流的底层句柄已经失效。理解了这一点后面再做重复读取方案时就清晰了我们的目标就是打破只能读一次的天然限制让文件内容在需要的位置可以被多次访问。三种方案分别从不同层面解决这个问题getBytes()一次性把内容抓进内存你随便怎么用transferTo()把文件内容放到一个独立临时文件里后续任意读取手动缓存InputStream则是把字节流或包装流保存下来复用。3. 方法一getBytes() 把文件字节一次性读进内存这是最简单粗暴的办法也是我最早使用的方案。MultipartFile接口本身就提供了getBytes()方法可以直接返回文件的字节数组。一次调用之后文件内容全部存在你手里的byte[]变量里你可以想读几次读几次想传给谁就传给谁完全不受 InputStream 一次性限制的约束。PostMapping(/import) public String importFile(RequestParam(file) MultipartFile file) throws IOException { byte[] fileBytes file.getBytes(); // 校验阶段从字节数组解析表头 boolean valid validateExcelHeader(fileBytes); if (!valid) { return 文件格式不正确; } // 解析阶段基于字节数组重新创建流 try (InputStream is new ByteArrayInputStream(fileBytes)) { ListRow rows ExcelParser.parse(is); // 业务处理... } return success; }这样改完重复读取的问题就消失了因为fileBytes本身就是一个重复可读的数据源你想读几次都行。ByteArrayInputStream是一个可以反复调reset()的输入流每次new ByteArrayInputStream(fileBytes)相当于从零开始读取天然支持多次消费。但这个方法有一个很现实的代价内存。getBytes()会将整个文件的所有字节加载到 JVM 堆内存中。如果上传的是几十 KB 的图片或文本文件一点问题没有但如果上传的是 100MB 的视频这一下子就会占掉 100MB 堆内存在高并发场景下对 JVM 的压力非常大甚至触发频繁 GC严重时导致内存溢出。所以我个人对这个方案的定位很明确只适合小文件。多大的文件算小我的经验阈值是 1MB 以内最多别超过 5MB。处理 5MB 以上的文件如果又要求重复读取建议直接考虑第二种方案。另外有一个细节要注意getBytes()底层依然会调用getInputStream()并读取全部内容所以它并不会比流读取更快或更高效它做的事情本质上就是把流里所有字节读出来放到堆内存里。在调用getBytes()之后原本的 MultipartFile 内容已经被消费过一次了如果后续你还想调用transferTo()来保存文件实际上transferTo()可能会失效因为底层临时文件或缓冲区可能已经被清理。所以一旦走了getBytes()后续对文件内容的访问应当全部基于这个字节数组不要再依赖原 MultipartFile 了。对于需要频繁复用文件内容、且文件大小可控的场景我还习惯封装一个小工具类把文件快照的概念做进去避免散落各处直接操作裸字节public class FileSnapshot { private final byte[] content; private final String originalFilename; private final String contentType; public FileSnapshot(MultipartFile file) throws IOException { this.content file.getBytes(); this.originalFilename file.getOriginalFilename(); this.contentType file.getContentType(); } public InputStream newInputStream() { return new ByteArrayInputStream(content); } public byte[] getContent() { return content; } public long getSize() { return content.length; } // getter... }这样在业务代码里参数接收 MultipartFile 之后第一步就生成FileSnapshot后面的校验、解析、日志记录全部依赖这个快照对象再也不会出现某个环节偷偷把源文件流读完导致后面环节拿到空流的局面。这个封装习惯我在多个项目里都用了实战下来非常稳定。4. 方法二transferTo() 落临时文件大文件场景的正路如果文件比较大动辄几十 MB 甚至上百 MB用getBytes()硬扛内存是不现实的。这种时候我一般直接换思路不跟 MultipartFile 的流死磕而是先把上传的文件落盘到一个临时文件后续所有环节都基于磁盘上的临时文件去读取规避 JVM 内存压力和输入流一次性限制两个问题。MultipartFile接口提供了transferTo(File dest)方法可以将上传文件直接写到目标文件中。大多数场景下大家用这个方法是为了把上传文件保存到业务指定的目录比如某个 uploads 文件夹。但用在重复读取场景下我们的目标文件并不是最终存储路径而是先落一个临时文件作为内容的中转站。PostMapping(/import) public String importFile(RequestParam(file) MultipartFile file) throws IOException { // 1. 创建临时文件注意一定要指定后缀名 String originalFilename file.getOriginalFilename(); String suffix originalFilename ! null originalFilename.contains(.) ? originalFilename.substring(originalFilename.lastIndexOf(.)) : .tmp; File tempFile File.createTempFile(upload_, suffix); try { // 2. 把上传文件落到临时文件 file.transferTo(tempFile); // 3. 基于临时文件做校验和解析想读几次读几次 boolean valid validateExcelHeader(tempFile); if (!valid) { return 文件格式不正确; } try (InputStream is new FileInputStream(tempFile)) { ListRow rows ExcelParser.parse(is); // 业务处理... } // 4. 如果需要最终存储到业务目录可以再复制 File targetFile new File(/data/uploads/ UUID.randomUUID() suffix); Files.copy(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); return success; } finally { // 5. 清理临时文件 tempFile.delete(); } }这段代码里有几个细节值得展开说明。第一File.createTempFile创建出来的临时文件默认位于系统临时目录比如 Linux 上的/tmp后续被操作系统定期清理。但在代码里还是要做好自我清理因为上传高峰时临时文件数量可能很多只依赖系统清理会积累大量垃圾文件。用finally块手动删除是最稳妥的。第二创建临时文件时一定要保留文件后缀。很多 Excel 解析库是基于文件扩展名或 POI 的WorkbookFactory.create(InputStream)来识别文档类型的如果没有.xlsx后缀某些库会误判为其他格式导致解析失败。我自己就因为这个原因踩过坑后来养成习惯从getOriginalFilename()里提取后缀拼到临时文件名里。第三file.transferTo(tempFile)在某些情况下并不等同于流复制。Spring 的StandardMultipartFile.transferTo()底层有一些优化路径可能会直接复用临时文件而不是重新读写一次。但大家不要依赖这种内部优化把它当成一次文件写入操作来看待最安全。这个方案最大的优势是内存友好。整个文件内容都在磁盘上JVM 堆内存不会被大文件拖垮而且基于临时文件可以反复创建新的FileInputStream天然支持无限次读取。缺点也很明显一次额外的磁盘 I/O 写入和后续的磁盘文件读取整体耗时比纯内存操作要慢一些。不过对于大文件场景这点磁盘开销是可以接受的。异步场景下这个方案几乎是最佳选择。很多时候我们不想让接口在前台等太久文件接收好之后就返回处理中然后丢给后台线程慢慢解析。直接传 MultipartFile 到异步线程是非常危险的因为容器在请求处理结束后可能会清理临时文件异步线程再读就是一场灾难。正确处理方式是接口里先transferTo()落临时文件然后把临时文件的路径传给异步线程异步线程基于文件路径自行读取。这样请求生命周期和异步任务生命周期完全解耦互不干扰。PostMapping(/async/import) public String asyncImport(RequestParam(file) MultipartFile file) throws IOException { File tempFile File.createTempFile(async_upload_, .xlsx); file.transferTo(tempFile); // 把临时文件路径交给异步线程 asyncImportService.process(tempFile.getAbsolutePath()); return 任务已提交; } // 异步任务内部 Service public class AsyncImportService { Async public void process(String filePath) { File tempFile new File(filePath); try { // 基于文件路径读取、解析、入库 // 需要读几次就读几次 } finally { tempFile.delete(); // 处理完清理临时文件 } } }关于临时文件的清理还有一点要提。有些同学为了省事在创建临时文件后调用tempFile.deleteOnExit()寄希望于 JVM 退出时自动删除。但deleteOnExit只保证 JVM 正常退出时删除如果应用持续运行临时文件会成为注销不了的垃圾一直留在磁盘上。高频率上传场景下几万个临时文件堆积下来非常难看。正确方式是在业务逻辑的finally块中主动删除或者在异步任务完成后删除别指望deleteOnExit兜底。5. 方法三缓存 InputStream 副本为 Filter 和拦截器场景留条后路前两种方案覆盖了大多数业务需求但还有一种比较隐蔽的场景文件内容不是在 Controller 里被重复读取而是在 Filter、Interceptor、AOP 切面这类横切环节中被读取然后 Controller 或者其他下游又要再读一次。举个例子我做过一个需求为了审计安全项目里需要记录所有上传文件的关键信息我写了一个过滤器解析上传的 JSON 或表单参数把每一个文件的 MD5 记录下来。但问题来了过滤器里一旦调用了file.getInputStream()读取内容算 MD5等到 Controller 里再调file.getInputStream()去解析或者保存文件时底层的输入流已经消费过一次了内容直接读不到。这就是典型的跨层读取问题。这时候就需要在更靠前的入口把文件内容缓存下来。Servlet 3.0 提供的HttpServletRequest包装器机制就是干这个用的。我们可以写一个自定义的ContentCachingRequestWrapper在请求进入 Filter 时包装原始请求将输入流的内容缓冲起来后续无论是 Filter 还是 Controller 再获取输入流拿到的都是缓冲后的副本。先看一个最简单的实现自定义包装类的示例public class RepeatedlyReadRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public RepeatedlyReadRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 读取原始请求体的全部字节 this.body request.getInputStream().readAllBytes(); } Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream byteArrayInputStream new ByteArrayInputStream(body); return new ServletInputStream() { Override public int read() { return byteArrayInputStream.read(); } Override public boolean isFinished() { return byteArrayInputStream.available() 0; } Override public boolean isReady() { return true; } Override public void setReadListener(ReadListener listener) { // 简单实现可以留空同步读取下不需要真正监听 } }; } Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } }然后在 Filter 中包装原始 requestComponent public class MultipartContentCachingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest httpServletRequest POST.equalsIgnoreCase(httpServletRequest.getMethod()) httpServletRequest.getContentType() ! null httpServletRequest.getContentType().startsWith(multipart/form-data)) { // 用包装类替换原请求 RepeatedlyReadRequestWrapper wrapper new RepeatedlyReadRequestWrapper(httpServletRequest); chain.doFilter(wrapper, response); } else { chain.doFilter(request, response); } } }这样包装之后所有下游代码获取getInputStream()时拿到的都是ByteArrayInputStream的副本数据可以反复读取。但注意这个方案解决的是请求体级别的重复读取对于multipart/form-data来说请求体里包含的是整个表单数据包括文件二进制内容。如果你在 Filter 里直接读整个 body那么在 Controller 里 Spring MVC 还能不能正常解析 MultipartFile 就取决于容器兼容性了。来说实话这个方案在这篇文章的边界内属于重型方案因为大多数情况下你不需要跨过滤器重复读取文件只需要在单个接口内部重复读取。而且实现不好容易把 Multipart 解析逻辑搞坏。我用过但没有大面积推广只在确实需要全局拦截文件内容、做统一哈希或审计时才会引入。真到这一步建议结合CommonsMultipartResolver或者 Spring 的StandardServletMultipartResolver一起评估确保包装后的请求仍能正确进入 Spring MVC 的文件解析流程。如果你只是在前端传入的 MultipartFile 对象本身上做文章有一个更轻量的变体思路在 Controller 入口处就把文件内容复制成一个自定义的ByteArrayMultipartFile后续所有业务环节都使用这个新对象。public class ByteArrayMultipartFile implements MultipartFile { private final byte[] content; private final String name; private final String originalFilename; private final String contentType; public ByteArrayMultipartFile(MultipartFile file) throws IOException { this.content file.getBytes(); this.name file.getName(); this.originalFilename file.getOriginalFilename(); this.contentType file.getContentType(); } Override public String getName() { return name; } Override public String getOriginalFilename() { return originalFilename; } Override public String getContentType() { return contentType; } Override public boolean isEmpty() { return content null || content.length 0; } Override public long getSize() { return content.length; } Override public byte[] getBytes() throws IOException { return content; } Override public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(content); } Override public void transferTo(File dest) throws IOException, IllegalStateException { Files.write(dest.toPath(), content); } }这个做法的好处是后续代码完全无感知还是照着 MultipartFile 的 API 来写调getInputStream()可以拿多次调getBytes()也没问题。它的本质和方法一相同内存开销差不多等于把方法一封装成了接口兼容的形式。如果公司内部有公共组件库把这个类放进去配合静态工厂方法团队里所有文件上传接口要复用内容时直接ByteArrayMultipartFile.of(file)一下就行省事也统一。6. 三种方案横向对比直接告诉你该怎么选方案都放在眼前最后还是要落地到选型。我根据自己的真实使用经验把三者做了个对比核心思路内存开销磁盘开销异步支持实现复杂度适用文件大小getBytes() 缓存字节数组高全部文件内容进 JVM 堆无好但需注意对象传递低5MB 以内transferTo() 落临时文件低一次完整写入 读取极好推荐路径传递中大文件无压力缓存 InputStream 副本 / 自定义包装视实现而定整体偏高无一般中高中小文件我自己在实际项目里的选择逻辑大致是这样一个判断链文件小于 1MB且只在同一线程内重复读取 → 首选getBytes()代码最简洁逻辑最直白。文件大于 5MB不需要立即返回结果可以异步处理 → 首选transferTo()写临时文件把文件路径丢给后台任务。文件大小在 1MB 到 5MB 之间看并发量。并发低用getBytes()问题不大并发高比如每秒几十个上传请求建议还是走临时文件避免 JVM 堆被大对象撑爆。需求是多层 Filter / 拦截器要读取文件内容且各层都要独立获取 → 考虑自定义包装请求但务必做好兼容性验证。公司内部要统一团队规范希望全项目里大家都按同一套模式来 → 封装ByteArrayMultipartFile公共组件轻量且接口兼容。另外不管是哪种方案我都会建议在进入业务逻辑之前先完成文件基础信息的前置判断比如file.isEmpty()、file.getSize() maxSize这类校验这样可以尽早拦截异常输入避免后续为无效文件做徒劳的无用功。这些判断放在任何重复读取方案之前都不会冲突。7. 除了重复读取文件上传接口还有这几个隐藏雷区文章标题写的是 MultipartFile 重复读取但既然把文件上传话题摊开了一些关联的坑我也想多写几句。这些坑往往不会在几天内爆发但一旦触雷排查过程比重复读取问题要痛苦得多。第一是上传上限配置。Spring Boot 中默认的单文件大小限制是 1MB如果业务需要上传较大的文件必须在配置文件中显式调大spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB file-size-threshold: 2MB location: /data/tmp/upload这里的max-file-size指的是单个文件的大小上限max-request-size是整个请求体的大小上限。因为一个请求可能同时携带多个文件和其他表单字段所以max-request-size一般要大于max-file-size。file-size-threshold表示文件超过多少字节后从内存写入磁盘临时文件默认是 0也就是所有文件都直接落盘不经过内存缓存。location则是临时目录路径注意确保这个目录存在且应用有读写权限。这些参数配置不对最常见的表现是文件稍微大一点就被 Spring MVC 直接抛异常报错信息如MaxUploadSizeExceededException。这个异常在全局异常处理器中最好单独捕获返回友好的提示信息不要直接返回 500。否则前端看到的就是服务器内部错误这种没头没尾的提示对排查问题没有帮助。第二是文件类型校验不能只看 Content-Type。getContentType()返回的值是由客户端发送的 Content-Type 头决定的完全可以被伪造。你选择性地相信它就等同于让客户端替你决定安全策略。更可靠的做法是读文件头部字节做 Magic Number 校验或者结合扩展名白名单一起判断。比如 Excel 文件的 xlsx 格式文件头一般是504B0304即 PK 开头的 ZIP 格式通过读取前几个字节可以初步判断是否真的符合预期格式。第三是上传目录的权限与可执行性。如果文件最终保存在应用服务器本地务必确认存放目录不能是 Web 容器的可执行目录目录权限也要收紧避免上传的脚本文件被谁意外访问到。生产环境下更推荐把文件存到 OSS、S3 这类对象存储里应用侧只保留临时文件处理完立即清理这样既能减轻本地磁盘压力也能降低文件被直接访问的风险。第四是包名差异。Spring Boot 2.x 系列里MultipartFile 相关的类都在javax.servlet包下从 Spring Boot 3.x 开始Servlet 规范从javax.servlet迁到了jakarta.servlet。如果你在网上找的示例代码来自 Spring Boot 2.x直接复制到 Spring Boot 3.x 的项目里第一步 import 就会报错。两个版本对外的 API 接口基本一致就是包名前缀不同。解决办法很简单根据自己项目的 Spring Boot 版本把 import 路径统一改成javax.servlet或jakarta.servlet。开发中如果出现ClassNotFoundException: javax.servlet.http.HttpServletRequest或NoClassDefFoundError: jakarta.servlet.http.Part之类的报错基本就是包名混用了。8. 关于文件缓存的一次实际排错经历留着当对照案例吧说了这么多方法论最后放一个我最近处理过的真实事故当作对整个话题的综合验证。有个同事负责的一个数据同步模块问题是这样的上传的 Excel 文件在本地开发环境跑得好好的部署到测试服务器后同样的操作经常出现数据不全或者偶发的Read timed out。一开始大家都怀疑是服务器文件权限问题也有人说是 POI 解析超大 Excel 的性能问题排查了一整天无果。后来我跟进这个模块的代码发现了一个关键线索Controller 里先做了FileValidator.validate(file)这个校验方法内部会new FileInputStream去读取上传的内容做格式预检然后解析器又拿着同一个 MultipartFile 的getInputStream()去解析数据。在本地开发环境里文件小、速度快、内存充足Tomcat 的临时文件处理机制可能还没有触达清理逻辑所以跑起来就碰巧成功了。但测试服务器的文件上传阈值配置和并发量不同导致第二次读取时底层临时文件早就处于不可读状态。解决方案就是我前面讲的第二种方案接口入口处先transferTo()落临时文件然后把临时文件路径传给校验器和解析器。修改后代码结构变得非常明确接收 MultipartFile。transferTo()写临时文件。校验器读临时文件 - 校验通过。解析器读临时文件 - 数据入库。finally块删除临时文件。返回处理结果。上线后这个问题彻底消失。事后我在项目代码里加了一条流水线化的文件处理工具类把接收 - 落临时文件 - 处理 - 清理这套模板固化下来后续新接的文件上传需求都走同一套基础逻辑编码效率和稳定性都明显提升。这个案例给我的教训是MultipartFile 的可重复读取并不是理论问题而是真实存在的工程问题。很多本地能跑通的代码到了生产环境就会暴露出时序和资源管理上的短板。如果你在处理文件上传时也遇到过时好时坏偶发异常的诡异问题第一时间检查一下是不是有多个环节都在读同一个 MultipartFile 流大概率就是这个原因。思路清晰之后剩下的就是选一套适合自己的方案把它固化成团队规范坑自然就填上了。
返回列表