一篇解决 springBoot 文件的上传与下载

文章标题说是springBoot 的文件上传与下载,但其实不限于springBoot中使用,主要是了解它的实质是什么。如果只想要获取能用的代码,请直接转到最后;如果更深层次掌握的话,就跟着作者的思路走一遍吧!

目录

1、文件的上传

     1)数据的获取

2)数据的提交

3)数据的解析和处理

可能会遇到的问题

1、上传文件太大问题

2、文件临时目录问题

3、上传资源后就能立马访问的问题

一点优化建议

1、数据库中如何存储文件信息?

2、文件的下载

3、废话少说,放码过来

单文件上传:

多文件上传

文件下载:


文件上传与下载实质上就是 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的FileUtilsspringMVC的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!

 

 

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值