文章标题说是springBoot 的文件上传与下载,但其实不限于springBoot中使用,主要是了解它的实质是什么。如果只想要获取能用的代码,请直接转到最后;如果更深层次掌握的话,就跟着作者的思路走一遍吧!
目录
文件上传与下载实质上就是 IO流+数据传输嘛。
1、文件的上传
1)数据的获取
一般在浏览器中做文件的上传时,都要整一个这玩意 <input type="file" id="file" name="file"/>,这样浏览器解析的时候就会出现这么一个选择文件的按钮
然后就可以选择文件打开了,打开这个过程发生了什么事,大家可以打印一下获取到的值,应该可以看着这样的信息
具体是浏览器还是依赖操作系统读取的文件数据,这里就不讨论了,反正咱们是获取到了文件的数据流,单位为字节byte。然后就可以通过网络传输数据流到服务器端了。
关于上传的动作一般有两种方式:
1、通过form表单直接提交(同步的还会跳转页面,不好),
2、通过ajax+formData异步提交数据了(异步的,不会跳转页面,较灵活),平时开发是也是第二种较多。
今天咱就主要讨论第二种异步的方式。
2)数据的提交
先放一段ajax提交数据的代码
//文件输入框改变时获取文件内容
$("#headImgInput").change(function () {
//文件内容
var headImgFile = $(this)[0].files[0];
if (headImgFile.size == 0){
alert("请先选择头像!");
return false;
}
var formData = new FormData();
formData.set("uploadHeadImg",headImgFile);
//ajax上传头像
$.ajax({
async:false,
type:"post",
url:"/customer/uploadHeadImg",
data:formData,//文件内容
cache:false,//不缓存
processData: false,// 不处理数据
contentType: false, // 不设置内容类型
success:function (result) {
if (result.code == 200){
$("#headImg").attr("src",result.data);
}else{
alert(result.message);
}
}
});
});
这是一段上传头像的代码,我是把数据封装到formData对象中提交的 ,(PS:formData确实好用,建议上传文件时都使用这种方式)主要注意两个地方 processData: false,// 不处理数据 contentType: false, // 不设置内容类型 ,这两个必须要有才行。
3)数据的解析和处理
当ajax执行后就会将数据通过网络,传输到后台服务器的数据接口中,此时后台的任务就是正确的解析发送的文件数据,这点因为springBoot封装了springMVC,所以可以使用springMVC提供的 MultipartFile uploadHeadImg 这个对象完美的解析文件数据,(要知道以前没有这个MultipartFile 时,解析文件的数据还是很麻烦的,现在传入一个对象就可以搞定)然后这个MultipartFile 就封装了解析到的文件的所有信息,具体的信息大家可以点进去看看。
ok,到这个地方就能在后台操纵文件数据了,(其实就是将文件数据写出到服务器所在的文件系统中,如果有专门的文件服务器的话,可能就写出到文件服务器所在的地方了)操作的方式也有多种,常用的有Apache的FileUtils、springMVC的transter()方法(注意这个方法一定要传入一个绝对路径,否则会报错)、或者获取到文件的字节数组,自己通过缓冲流写出去,其实道理都是一样的。
在写出去之前,可能还会对文件进行:
1、非空判断(null 和 size == 0)
2、文件格式和大小的校验
3、文件重命名(数字时间和uuid等)。
到此文件上传即基本完成了,代码后面再完整放一遍。
可能会遇到的问题
1、上传文件太大问题
原因:springBoot不是内置的tomcat嘛,tomcat默认的上传文件的大小为2MB,到springBoot就是1MB了
打成war包,可以修改tomcat的配置参数
<!--maxPostSize="0":对于post请求的数据不限制大小,解决文件上传大小限制问题,默认为2MB。
connectionTimeout="20000":20秒的有效连接,可以设置的大一点来解决大文件上传 -->
<Connector connectionTimeout="20000"
maxPostSize="0"
port="8080"
protocol="HTTP/1.1"
redirectPort="8443"/>
若是jar包,可以修改springBoot的配置
参看源码配置类 MultipartProperties
springboot2.x
spring.servlet.multipart.max-request-size=50MB //单次请求单个文件的最大大小
spring.servlet.multipart.max-file-size=300MB //单次请求多个文件的最大大小
springboot1.x
spring.http.multipart.max-request-size=50MB
spring.http.multipart.max-file-size=300MB
2、文件临时目录问题
ok,项目做好,打包放到linux上测试文件上传一气呵成,没问题,十分完美。结果一段时间后突然有人告诉你上传文件功能不能用了,你顿时心里产生了十万个为什么,what?不管用了?不会吧?结果你亲自一测,一个系统异常的弹窗出现在你面前,嗯?什么鬼?看了一下日志,还真有错误
org.springframework.web.multipart.MultipartException: Could not parse multipart servlet
request;nested exception is java.io.IOException: The temporary upload location
[/tmp/tomcat.1337767218595042057.80/work/Tomcat/localhost/ROOT] is not valid
说什么某个目录不可用了。
原因就是:当springBoot项目启动时,会创建一个临时目录用来存放上传的文件资源(临时目录就是OS的临时目录),当部署到Linux下,Linux会每隔10天自动清理不使用的临时目录,就导致文件上传不上去。
解决:指定一个目录当成临时目录就可以了
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
String location = System.getProperty("user.dir") + "/../tempUpload";//jar包运行位置上一级创建该文件夹
File tmpFile = new File(location);
if (!tmpFile.exists()) {
tmpFile.mkdirs();
}
factory.setLocation(location);
return factory.createMultipartConfig();
}
3、上传资源后就能立马访问的问题
不知道大家有没有遇到过这种问题,知道springBoot静态资源文件夹的文件可以直接访问,所以就想着把文件上传到这个位置不就行了吗,然后按照 /src/main/resource/static/xxx文件,这个路径上传了,也成功了,但是不能访问,于是重启一下项目,哎可以了,又传了一个还是不能访问,心想这样可不行啊。于是问了一下度娘后,查询到/target/class/static/xxx文件,这个路径可以访问,试了一下成功了也没重启项目,心想这下就没事了吧。结果打成jar包放到服务器又不能访问了(原因:jar包特殊的机制,不允许通过url直接访问内部上传的静态资源,jar包设计之初就不是来存放静态资源的。)解决方法:打成war包,放到tomcat中就行了。
其实上面都是屁话,下面说真正的方法。
其实咱们有三个地方可以存放文件资源
1、类路径的静态资源文件夹下(不推荐,若打成jar包,图片上传后不能访问且资源和代码全都耦合在一个项目里了)
2、放到项目所运行的服务器的文件系统中(推荐,但要做虚拟路径的映射)
3、专门的文件服务器(最好,有条件可以使用)
关于上面三个地方存储路径的获取和返回的访问地址 假设 ip为10.0.113.10 port为8999
1、获取 this.getClass().getClassLoader().getResource("static/img/");
访问 10.0.113.10:8999/img/1.jpg文件
2、可以写死服务器的一个绝对路径或根据java -jar 命令执行的位置的相对路径都可以
这里使用springBoot(其实是springMVC)的虚拟路径映射,因为简单。平时可能Nginx用的比较多。
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//获取文件的存储目录
File file = new File(System.getProperty("user.dir") + "/../sixBookStoreResource");
try {
//判断是windows系统还是linux系统,做不同路径的映射
String osName = System.getProperty("os.name").toLowerCase().substring(0,3);
if (osName.equals("win")){
registry.addResourceHandler("/resource/**")
.addResourceLocations("file:"+file.getCanonicalPath()+"\\");
}else if (osName.equals("lin")){
registry.addResourceHandler("/resource/**")
.addResourceLocations("file:"+file.getCanonicalPath()+"/");
}
} catch (IOException e) {
e.printStackTrace();
}
}
这样假设你在sixBookStoreResource目录下存放了一个2.jpg图片,
访问 10.0.113.10:8999/resource/2.jpg
3、文件服务器就不用多说了,调用它的接口传输文件数据,那个文件服务器肯定会返回一个访问路径的。
关于文件的上传暂时就说到这里,以上讲的都是一些简单的小文件的传输,像大文件或超大文件,应该动态的加一个进度条,或者开启多个线程来快速处理文件资源。
一点优化建议
1、数据库中如何存储文件信息?
前期文件量小,先将文件保存到服务器磁盘上,数据库中保存文件访问路径和磁盘存储路径,并且存储的文件夹按照xxxx年xx月xx日分隔,具体精确到月还是日,就看你的数据量了(存磁盘存储路径是因为,当更换文件时删除旧的文件,节省服务器磁盘空间,如果你磁盘容量无限,当我没说)。文件上传方式先用随表单信息一块提交的方式(防止上传文件后,表单不提交了,造成一些无用的文件,还要想办法清理)。
等后期上传文件量大了再用OSS服务,那个时候就可以只存储访问路径了,并且可以采用先上传文件,再提交表单的方式,反正不用关心磁盘空间问题。
2、文件的下载
下载相对于上传就简单不少,就是将一个数据流从服务器写出到浏览器端,唯一注意的一点就是要设置一下http响应头
//设置强制下载而不是打开
response.setContentType("application/force-download");
response.setHeader("content-disposition", "attachment;filename="+showFileName);
再有就是对于超大文件,如果快速的下载就是个问题了;如何能下到一半暂停,一段时间后再接着下载,也是一个问题。
3、废话少说,放码过来
单文件上传:
js部分
//文件输入框改变时获取文件内容
$("#headImgInput").change(function () {
//文件内容
var headImgFile = $(this)[0].files[0];
if (headImgFile.size == 0){
alert("请先选择头像!");
return false;
}
var formData = new FormData();
//必须要通过formData来构造键值对,否则对应不起来与后台
formData.set("uploadHeadImg",headImgFile);
//ajax上传头像
$.ajax({
async:false,
type:"post",
url:"/customer/uploadHeadImg",
data:formData,//文件内容
cache:false,//不缓存
processData: false,// 不处理数据
contentType: false, // 不设置内容类型
success:function (result) {
if (result.code == 200){
$("#headImg").attr("src",result.data);
}else{
alert(result.message);
}
}
});
console.log(formData);
});
java部分:
/**
* 用户修改头像
* @param uploadHeadImg
* @return
*/
@ResponseBody
@RequestMapping("/uploadHeadImg")
public Result uploadHeadImg(MultipartFile uploadHeadImg) throws IOException {
if (uploadHeadImg == null) {
return Result.fail("头像不能为空!");
}
//验证文件大小和格式
if (!MyFileUtils.checkFile(uploadHeadImg)){
return Result.fail("文件格式不正确或大小应小于50MB!");
}
String originalFilename = uploadHeadImg.getOriginalFilename();
//重命名文件
String renameFile = MyFileUtils.renameFile(originalFilename);
//上传文件
String DBFilePath2 = MyFileUtils.upload(uploadHeadImg.getBytes(), renameFile, filePath, DBFilePath);
//如果上传成功了
if (DBFilePath2 != null) {
Customer customer = customerService.updateHeadImg(getCustomerId(),DBFilePath2);
//更新session和cookie中的信息
updateSessionCookieCustomer(getRequest(),getResponse(),customer);
return Result.success("修改头像成功",DBFilePath2);
}else {
return Result.fail("修改头像失败!");
}
}
工具类中的方法给大家贴一下,太长就不全贴了
private static String[] fileTypeArray = {"jpg","png","jpeg","gif"};
//验证文件大小和格式
public static boolean checkFile(MultipartFile uploadHeadImg){
String originalFilename = uploadHeadImg.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
for (String fileType : fileTypeArray) {
//如果格式正确
if (fileType.equalsIgnoreCase(suffix)){
//验证大小50MB
long fileSize = uploadHeadImg.getSize();
if (fileSize >0 && fileSize <= 1024 * 1024 * 50){
return true;
}
}
}
return false;
}
/**
* 文件重命名为时间+8位随机字符串格式
* @param fileName 需要重命名的文件
* @return
*/
public static String renameFile(String fileName) {
String timeName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String uuidName = UUID.randomUUID().toString().replace("-","").substring(0,8);
return timeName + uuidName + "." + fileName.substring(fileName.lastIndexOf(".") + 1);
}
/**
* 上传
* @param bytesFile 文件的字节码数组
* @param fileName 文件名
* @param filePath 文件要存储的目录
* @param DBFilePath 文件可访问的目录
* @return 文件可访问的路径
*/
public static String upload(byte[] bytesFile,String fileName, String filePath, String DBFilePath){
createDir(filePath);
//将请求的文件数据写出到服务器所在电脑的指定文件中
boolean flag = bytesToFile(bytesFile, filePath + "/" + fileName);
//成功后返回
if (flag){
return DBFilePath + "/"+ fileName;
}
return null;
}
/**
* 确保文件路径存在
* @param path
*/
private static void createDir(String path) {
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
}
/**
* 将文件的字节内容写出到文件
* @param bytes
* @param filePath
* @return
*/
private static boolean bytesToFile(byte[] bytes, final String filePath) {
File file = new File(filePath);
OutputStream os = null;
BufferedOutputStream bos = null;
try {
os = new FileOutputStream(file);
bos = new BufferedOutputStream(os);
//缓冲流写出字节数组
bos.write(bytes);
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != bos) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != os) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
多文件上传
PS:多文件上传和单文件上传一样,只不过前端需要传递多个文件,后台就用MultipartFile[] 数组来接收,然后循环遍历即可。
主要就是前端怎么传递多个文件,才能让MultipartFile[] 接收的到。有两种方式:
1、若是form表单的话,只需要多个<input type="file" name="uploadFiles"/>的name值一样即可。
2、若是ajax+formData的话,formData.append(key,value),多个append方法中的这个key一样即可(formData对象允许多个key重复,不会后一个覆盖前一个)
然后name的值或key和后台MultipartFile[] uploadFiles 属性对应起来即可,或者用@RequestParam("uploadFiles ") 获取也行
js部分:
var formData = new FormData();
//多文件上传,这个key一样就行,会自动的封装成数组,对应后台的MultipartFile[]
formData.append("files",$("#productIcon")[0].files[0]);
formData.append("files",$("#productPic")[0].files[0]);
//异步上传文件
$.ajax({
type : "POST",
url : "/manager/product/save",
data : formData,
cache:false,//不缓存
processData: false,// 不处理数据
contentType: false, // 不设置内容类型
success : function(data) {
}
});
java部分:
@Log("添加产品表")
@ResponseBody
@PostMapping("/save")
public Result<String> save(@RequestParam("files") MultipartFile[] files, ProductDO product) {
for (int i = 0; i < files.length; i++) {
//单文件上传的步骤
}
}
后台这块需要注意一下,因为是多文件,如果要求全部都要上传成功的话,应该有两个循环,第一个对文件的是否为空、格式、大小等做判断;第二个才真正上传文件,避免出现第一个成功了,第二个失败了的情况。
文件下载:
/**
* 文件下载
* @param response HttpServletResponse对象
* @param inputStream 要下载的文件的输入流
* @param showFileName 下载时展示的文件名
* @throws UnsupportedEncodingException
*/
public static void downLoadFile(HttpServletResponse response,InputStream inputStream,String showFileName) throws UnsupportedEncodingException {
//防止文件名中文乱码
showFileName = new String(showFileName.getBytes("utf-8"),"ISO8859-1");
//设置响应头
//设置强制下载不打开
response.setContentType("application/force-download");
response.setHeader("content-disposition", "attachment;filename="+showFileName);
OutputStream outputStream = null;
BufferedOutputStream bos = null;
BufferedInputStream bis = null;
try {
outputStream = response.getOutputStream();
bos = new BufferedOutputStream(outputStream);
bis = new BufferedInputStream(inputStream);
byte[] b = new byte[1024];
int length = 0;
while ((length = bis.read(b)) != -1){
bos.write(b,0,length);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
最后说明:如果恰巧能解决你的问题,那就动一动您的小手,点个赞或者评论一下,让我看到你们,您的点赞或评论将会持续带给我不懈的动力!!!come on baby!Let's go!