Spring Boot升级后之文件上传坑
SpringBoot又双叒叕出新版本啦,不加班的人们都在用SpringBoot了,What?你还在用xml配置,赶紧拥抱SpringBoot,以后迎娶白富美不是梦,最近一次系统改造需要将18年的老系统改造成SpringBoot,优雅发版,减少加班,走上云时代,拥抱微服务。。。二大,赶紧把这个幻想症患者带走
咳咳咳。。。还是说正事,在改造的过程中遇到通过流文件上传的接口全部挂了,你没听错是全挂了
怎么回事呢,听我娓娓道来,业务代码不外泄,所以来个模拟
一、首先创建一个简单的包含WEB依赖的SpringBoot项目
pom.xml
内容:
jetty容器
<!-- Spring Boot 版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- Spring Boot web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
tomcat容器:
<!-- Spring Boot 版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- Spring Boot web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
二、配置文件上传的文件大小限制
application.properties
配置文件添加:
# 上传文件总的最大值
spring.servlet.multipart.max-request-size=10MB
# 单个文件的最大值
spring.servlet.multipart.max-file-size=10MB
三、单文件上传示例
1、度娘一波,从网上抄了一段代码,项目中肯定是抄的这段,transferTo用的风生水起
package com.foo.bar.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import java.io.File;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@RestController
public class UploadController {
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Dict local(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Dict.create().set("code", 400).set("message", "文件内容为空");
}
String fileName = file.getOriginalFilename();
String rawFileName = StrUtil.subBefore(fileName, ".", true);
String fileType = StrUtil.subAfter(fileName, ".", true);
final String filePath = "/Users/foobar/foo/bar/";
String localFilePath = filePath + rawFileName + "-" + DateUtil.current(false) + "." + fileType;
}
try {
file.transferTo(new File(localFilePath));
} catch (IOException e) {
log.error("【文件上传至本地】失败,绝对路径:{}", localFilePath, e);
return Dict.create().set("code", 500).set("message", "文件上传失败");
}
log.info("【文件上传至本地】绝对路径:{}", localFilePath);
return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", fileName).set("filePath", localFilePath));
}
}
点了一下运行,准备一杯咖啡,喝完就下班,这时你眉头一紧,看到了亮眼的文件上传失败
然后点开了可爱的idea
/private/var/folders/7c/c1m8c2h52mx_pz716jlmmsd80000gn/T/foo/bar/001-1597844615672.jpg (No such file or directory)
[21:43:40.705] ERROR com.foo.bar.controller.UploadController 38 local - 【文件上传至本地】失败,绝对路径:/foo/bar/001-1597844615672.jpg java.io.FileNotFoundException: /private/var/folders/7c/c1m8c2h52mx_pz716jlmmsd80000gn/T/foo/bar/001-1597844615672.jpg (No such file or directory)
at java.io.FileOutputStream.open0(FileOutputStream.java) ~[?:1.8.0_211]
at java.io.FileOutputStream.open(FileOutputStream.java:270) ~[?:1.8.0_211]
at java.io.FileOutputStream.<init>(FileOutputStream.java:213) ~[?:1.8.0_211]
at java.io.FileOutputStream.<init>(FileOutputStream.java:162) ~[?:1.8.0_211]
at org.eclipse.jetty.util.MultiPartInputStreamParser$MultiPart.write(MultiPartInputStreamParser.java:329) ~[jetty-util-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.transferTo(StandardMultipartHttpServletRequest.java:256) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at com.foo.bar.controller.UploadController.local(UploadController.java:36) ~[classes/:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_211]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_211]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_211]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_211]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:523) ~[jakarta.servlet-api-4.0.3.jar:4.0.3]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[jakarta.servlet-api-4.0.3.jar:4.0.3]
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:763) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1631) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:226) ~[websocket-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1618) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:124) ~[druid-1.1.23.jar:1.1.23]
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1618) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at com.foo.bar.filter.LogFilter.doFilterInternal(LogFilter.java:97) ~[classes/:?]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1618) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1618) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1618) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1618) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:549) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:602) ~[jetty-security-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1610) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ContextHandler.__doHandle(ContextHandler.java:1369) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:42020) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:489) ~[jetty-servlet-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1580) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1284) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.Server.handle(Server.java:501) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:556) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:273) ~[jetty-server-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311) ~[jetty-io-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103) ~[jetty-io-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104) ~[jetty-io-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336) ~[jetty-util-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313) ~[jetty-util-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171) ~[jetty-util-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce(EatWhatYouKill.java:135) ~[jetty-util-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806) [jetty-util-9.4.29.v20200521.jar:9.4.29.v20200521]
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938) [jetty-util-9.4.29.v20200521.jar:9.4.29.v20200521]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_211]
[21:43:40.719] INFO com.foo.bar.filter.LogFilter 107 doFilterInternal - 请求URL:/upload, total time:9480 ms, responseCode:200, requestBody:file=001.jpg(1470037 byte), responseBody:{"code":500,"message":"文件上传失败"}
2020-08-19 21:52:12 JRebel: Reloading class 'com.foo.bar.controller.UploadController'.
你一点也不慌,再一次打开了度娘
多年code经验告诉你这是路径问题,然后你找到了这个
解决使用Spring Boot、Multipartfile上传文件路径错误问题
目录不存在,我就自己建一个,没毛病,一行代码的事
final String filePath = "/foo/bar/"; String localFilePath = filePath + rawFileName + "-" + DateUtil.current(false) + "." + fileType; final File dirPath = new File(filePath); if(!dirPath.exists()){ dirPath.mkdirs(); }
热部署一波,喝了一口咖啡,在postman上优雅地点了send
你惊讶的发现,报错信息一点没变,你开始怀疑热部署工具是不是坏了,暴躁的重启了server.又点了一下send.你开始惶恐了,依然是这个错误。喝了一口咖啡,回忆往昔,这个上传方法,你已用了无数次,为何这一次不行了呢,是不是因为springboot版本的问题,你再一次点开了度娘,运气真不错,又找到了一个解决方案。
springboot升级导致文件上传自动配置/tmp目录问题解决
接着你开启了debug大法,在这行代码中陷入了沉思,你不敢相信自己的眼睛,
MultiPartInputStreamParser已经被标记为过时了
沉思并不能解决问题,加上了
spring.servlet.multipart.location: /foo/bar
再试一次。。。
很遗憾还是报错了,只是这次错误有点稍微不一样,没有赶紧又去掉了
[22:52:31.957] INFO org.springframework.boot.StartupInfoLogger 61 logStarted - Started BarApplication in 4.417 seconds (JVM running for 8.069)
[22:54:24.754] ERROR com.foo.bar.controller.UploadController 35 local - 【文件上传至本地】失败,绝对路径:/foo/bar/001-1597848801337.jpg java.io.FileNotFoundException: /foo/bar/foo/bar/001-1597848801337.jpg (No such file or directory)
你发现度娘找到解决方案屁用没有,内心一度崩溃。。。关上电脑,今夜不加班。架构改造完,业务代码凉凉,现在知道为啥不让你动老系统了吧,知道错了吧。
第二天醒来,灵光一闪,求人不如求己,开始了一边阅读源码,一边debug
一个
再一个
你似乎看到了一道光,为什么不用下面这个 重载的方法呢,度娘了一圈,你发现竟然没有人用,你似乎要做第一个吃螃蟹的人了
加上了这行载入史册的代码:
file.transferTo(Paths.get(localFilePath));
成功了!
package com.foo.bar.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@RestController
public class UploadController {
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Dict local(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Dict.create().set("code", 400).set("message", "文件内容为空");
}
String fileName = file.getOriginalFilename();
String rawFileName = StrUtil.subBefore(fileName, ".", true);
String fileType = StrUtil.subAfter(fileName, ".", true);
final String filePath = "/Users/foobar/foo/bar/";
String localFilePath = filePath + rawFileName + "-" + DateUtil.current(false) + "." + fileType;
final File dirPath = new File(filePath);
if(!dirPath.exists()){
dirPath.mkdirs();
}
try {
file.transferTo(Paths.get(localFilePath));
} catch (IOException e) {
log.error("【文件上传至本地】失败,绝对路径:{}", localFilePath, e);
return Dict.create().set("code", 500).set("message", "文件上传失败");
}
log.info("【文件上传至本地】绝对路径:{}", localFilePath);
return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", fileName).set("filePath", localFilePath));
}
}
可不可以不实用transferTo方法,度娘大法开启,点开了第一个
MultipartFile转File
File file = new File(path);
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file);
这两个不就是一样的吗,So,还要用transferTo吗,这么坑比的方法,升级了Springboot版本就不能用了
@Override
public void transferTo(Path dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest));
}
彻底换掉这个坑比的transferTo,自己掌控流与文件,感觉真好
正确的文件上传方式:
1.流转文件
2.流转byte[]
package com.foo.bar.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import java.io.File;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@RestController
public class UploadController {
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Dict local(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Dict.create().set("code", 400).set("message", "文件内容为空");
}
String fileName = file.getOriginalFilename();
String rawFileName = StrUtil.subBefore(fileName, ".", true);
String fileType = StrUtil.subAfter(fileName, ".", true);
String destFilePath = "/Users/foobar/foo/bar/" + rawFileName + "-" + DateUtil.current(false) + "." + fileType;
final File destFile = new File(destFilePath);
if(!destFile.getParentFile().exists()){
destFile.getParentFile().mkdirs();
}
try {
FileUtils.copyInputStreamToFile(file.getInputStream(), destFile);
} catch (IOException e) {
log.error("【文件上传至本地】失败,绝对路径:{}", destFilePath, e);
return Dict.create().set("code", 500).set("message", "文件上传失败");
}
log.info("【文件上传至本地】绝对路径:{}", destFilePath);
return Dict.create().set("code", 200).set("message", "上传成功").set("data", Dict.create().set("fileName", fileName).set("filePath", destFilePath));
}
}
友情忠告,没事别动架构,别升级,别升级