1、文件上传介绍

也就是说:客户端向服务端发送一些图片、视频、音频等。(上面是前端发送图片、视频、音频时代码的逻辑方式) 

因此,前端通过上面的形式向我们后端发送了图片、视频、音频后,我们后端就需要通过下面的逻辑来进行接收前端传递的图片、视频、音频等信息:

2、文件下载介绍

 3、文件上传/下载代码实现

3.1、文件上传代码实现

第一步:首先先把ElementUI提供的上传组件搞到项目当中去:

 

 上传组件搞到项目中后,我们后端开启服务器,访问demo下的upload.html组件,测试一下是否能成功:

注意:因为我们的项目当中设置的有Filter拦截器,因此别忘记放行该访问update.html的url路径。

然后就能上传图片了,并且上传的图片也会向后端发送一个url请求(其实这都是前端的事情了,向后端访问的地址什么的就看前端怎么搞了):

需要注意一点:

        就是我们知道我们后端等会接收上面客户端请求的图片的时候,是spring框架帮我们简化了很多后端的代码,(只需要我们在表现层中声明一个MultipartFile类型的参数,就可以接收到前端请求的图片或者视频资源了。 这里注意的是这个参数不是随便起的,这个参数要遵循下面框起来的name属性的值,参数要和name属性这个值一致,要不然后台就接收不到前端请求的图片或者视频的url资源

因此后端的逻辑代码如下所示:

package com.Bivin.reggie.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 *      文件上传和下载
 */
@RestController // @Controller和@ResponseBody注解 (表现层bean注解和响应前端数据注解)
@RequestMapping(value = "/common")
@Slf4j
public class CommonController {

    /**
     *
     * *  文件上传  
     *
     * @param file  // 注意:这个参数名一定要保证和前端的那个name的值一致,要不然进不到该上传方法当中来。
     * @return
     */

    @PostMapping(value = "/upload")
    public R upload(MultipartFile file){
        System.out.println("文件上传成功了,确实上传到服务器了哦~ ");

        return new R(fileName); // 把UUID随机生成的照片名称加上后缀名后响应给前端
        // (因为这个照片已经上传到我们D盘中了,把这个照片新名称响应给前端的目的就是以后前端好拿着这个照片的名称
        //   来我们D盘中下载这张照片展示在页面上)
    }

}

当前端上传照片的时候,会发现确实能请求进入到upload方法当中:

注意:我们后端拿到前端上传的照片或者视频之后,需要转存到指定位置,否则本次请求完成后临时文件会自动删除(也就是说如果不指定位置的话,上传的照片文件会自动删除掉)。

指定位置代码如下所示:

package com.Bivin.reggie.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;

/**
 *      文件上传和下载
 */
@RestController // @Controller和@ResponseBody注解 (表现层bean注解和响应前端数据注解)
@RequestMapping(value = "/common")
@Slf4j
public class CommonController {

    /**
     *  文件上传
     *
     *
     * @param file  // 注意:这个参数名一定要保证和前端的那个name的值一致,要不然进不到该上传方法当中来。
     * @return
     */

    @PostMapping(value = "/upload")
    public R upload(MultipartFile file){
        /**
         *  注意:我们后端拿到前端上传的照片或者视频之后,需要转存到指定位置,否则本次请求完成后临时文件会自动删除。
         *      也就是说:我们需要对前端传递的这些东西指定到我们的D盘或者C盘等位置当中,要不指定的话等该方法执行完
         *      之后,客户端上传的这个图片或者视频就自动被删除了。
         *
         *      指定位置: transferTo方法
         */
        try {

            file.transferTo(new File("D:\\hello.jpg")); // 将客户端请求上传的照片指定到我们D盘当中

        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

}

当前端上传照片的时候,会发现照片确实成功上传到我们指定的D盘当中了:

 3.1.1、路径和文件名优化

路径优化:

我们知道我们后端指定的路径在表现层代码当中是写死了,我们能不能通过配置yml属性来把路径写活呢,也就是说想指定到哪个盘就在yml属性当中写一下就行了。 (看yml属性配置笔记【读取yml属性中的数据步骤那里】

做法:通过@Value注解,把yml属性中的数据读到使用@Value注解的属性当中。

 

 会发现上传的图片同样上传到了D盘下:

文件名优化:

上面的指定路径写活了之后,还可以进行再优化其他的东西,那就是文件名,如上面的hello.jpg文件名。

        我们知道文件名是我们自己写的,写了个hello.jpg,这仅仅是一个用户点击上传照片到后端中,如果有很多用户一起点击上传照片到后端的话,难不成我们就用这一个文件名? 那肯定不行啊,不用想第二次该文件名的数据肯定会把第一次的那个数据给覆盖掉,因此我们需要对文件名进行一次优化。

实现思路:就是我们后端先获取到前端上传的照片的原始名,如这个前端上传的这个照片叫做dwadawwaeawtgwa12dwadwa.jpg,那么我们后端就先拿到这个照片的这个名字dwadawwaeawtgwa12dwadwa.jpg,然后通过截取把后缀名.jpg截取下来,然后通过UUID帮我们随机生成的一串字母,然后把这串随机生成的字母再拼接上后缀名.jpg,那么这个文件名就随机生成了,通过截取拼接后缀名的方式,保证了和原始图片的后缀名一致的正确性,又保证了文件名的随机生成,太牛逼了。(因为实际开发当中,前端上传的东西的后缀名不一定都是jpg,还有可能是.mvp,.jpig等一些后缀名,因此这就是我们通过截取后缀名然后拼接到UUID随机生成的一串文件名的原因。即随机生成了一串字母作为文件名,又保证了原始图片的后缀名的正确性)

代码如下所示:

注:假设客户端上传的照片原名就是:a.jpg

package com.Bivin.reggie.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

/**
 *      文件上传和下载
 */
@RestController // @Controller和@ResponseBody注解 (表现层bean注解和响应前端数据注解)
@RequestMapping(value = "/common")
@Slf4j
public class CommonController {

    @Value("${reggie.path}")
    private String basePath;    // 把yml属性配置中的路径读取到basePath属性当中

    /**
     *  文件上传
     *
     *
     * @param file  // 注意:这个参数名一定要保证和前端的那个name的值一致,要不然进不到该上传方法当中来。
     * @return
     */

    @PostMapping(value = "/upload")
    public R upload(MultipartFile file){

        /**
         *  文件名优化
         */

        // 1、先获取前端上传的照片的原始名
        String filename = file.getOriginalFilename();
        //System.out.println(filename);    // a.jpg

        // 2、截取原始照片名的后缀名 .jpg
        String s = filename.substring(filename.lastIndexOf("."));// 从最后一个.开始往后截取。【包括.】
        // System.out.println(s);  // .jpg

        // 3、通过UUID随机生成文件名 (其实就是随机生成的一串字母,我们把生成的字母再加上上面截取的后缀名,那么不就是一个新的原始照片的名字了嘛~)
        String s1 = UUID.randomUUID().toString();//  随机生成文件名
        // System.out.println(s1); // 如随机生成的文件名: 3ee3b9bc-70d8-4b87-b91b-5832714fae82

        String fileName = s1+s; // 为第三步通过UUID随机生成的文件名拼接加上截取到的原始照片名的后缀名
        // System.out.println(fileName);   // 3ee3b9bc-70d8-4b87-b91b-5832714fae82.jpg


        try {

            // 指定照片上传路径
            file.transferTo(new File(basePath+fileName));   // 直接路径+生成的文件名即可。

        } catch (IOException e) {
            e.printStackTrace();
        }


        return null;
    }

}

我们会发现我们在yml属性中指定的D盘路径下,真的随机生成了一个.jpg为后缀名的文件名,并且就是前端上传的照片:

补充一个问题:

        就是我们知道,照片在后端上传的地址路径,我们是在yml属性当中配置的到D盘目录下的:

假定现在有这样一种情况:就是在yml属性当中配置的该路径对应的我们盘里根本就不存在该文件夹:

 

 

因此我们的代码就需要再次优化一下,当后端打算上传照片之前,先判断一下该上传的文件夹是否存在,不存在的话就创建好该文件夹然后再把照片上传到该文件夹当中:

package com.Bivin.reggie.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

/**
 *      文件上传和下载
 */
@RestController // @Controller和@ResponseBody注解 (表现层bean注解和响应前端数据注解)
@RequestMapping(value = "/common")
@Slf4j
public class CommonController {

    @Value("${reggie.path}")
    private String basePath;    // 把yml属性配置中的路径读取到basePath属性当中

    /**
     *  文件上传
     *
     *
     * @param file  // 注意:这个参数名一定要保证和前端的那个name的值一致,要不然进不到该上传方法当中来。
     * @return
     */

    @PostMapping(value = "/upload")
    public R upload(MultipartFile file){

        /**
         *  文件名优化
         */

        // 1、先获取前端上传的照片的原始名
        String filename = file.getOriginalFilename();
        //System.out.println(filename);    // a.jpg

        // 2、截取原始照片名的后缀名 .jpg
        String s = filename.substring(filename.lastIndexOf("."));// 从最后一个.开始往后截取。【包括.】
        // System.out.println(s);  // .jpg

        // 3、通过UUID随机生成文件名 (其实就是随机生成的一串字母,我们把生成的字母再加上上面截取的后缀名,那么不就是一个新的原始照片的名字了嘛~)
        String s1 = UUID.randomUUID().toString();//  随机生成文件名
        // System.out.println(s1); // 如随机生成的文件名: 3ee3b9bc-70d8-4b87-b91b-5832714fae82

        String fileName = s1+s; // 为第三步通过UUID随机生成的文件名拼接加上截取到的原始照片名的后缀名
        // System.out.println(fileName);   // 3ee3b9bc-70d8-4b87-b91b-5832714fae82.jpg


        /**
         *  再次优化: 判断照片上传的路径是否存在,不存在的话先创建好再上传照片
         */

        // 创建一个目录对象
        File file1 = new File(basePath);    // basePath是上面配置了@Value注解的属性,里面封装的是yml属性配置中设置的图片上传路径
        // 判断当前目录是否存在 (也就是说判断yml属性设置的图片上传路径D:\iiiiiiis\是否存在)
        if (! file1.exists()){
            // 如果路径目录不存在,那么就先把路径目录创建出来
            file1.mkdirs(); 
        }
        
        // 如果指定的上传路径不存在的话,通过上面的mkdirs方法把上传的路径创建好了,然后再执行下面的上传照片到该指定的路径下就能成功了。
    
        try {

            // 指定照片上传路径
            file.transferTo(new File(basePath+fileName));   // 直接路径+生成的文件名即可。

        } catch (IOException e) {
            e.printStackTrace();
        }


        return null;
    }

}

最终会发现,本来D盘没有iiiiiiis的目录也被创建出来了,而且照片也上传成功了(到该目录下了,并且文件名就是通过UUID随机生成的): 

 

3.2、文件下载代码实现

什么是文件下载:

         也就是说前端通过向后端传递一个下载图片或者视频的请求,然后后端接收到请求后向我们本机上找到相对应的资源通过io流的形式再响应给前端,前端就相当于下载了该图片或者视频了,也就是说该下载的照片或者视频就可以展示在页面上了。

注:这里客户端请求的name参数资源就是a.jpg图片资源

前端向后端发送的请求下载图片的路径如下所示: 

后端代码如下所示:

  /**
     **  文件下载
     *
     *      前端向我们后台发送的请求路径: /common/download?name=${response.data}
     *                    name=${response.data} 这里演示的其实就是a.jpg图片资源
     *
     *      回忆基础知识:
     *          FileInputStream : 输入流  万能的,任何类型的文件都可以采用这种流来读(图片、视频、文件等)
     *          read方法: 读硬盘数据的方法,该方法当指针到数据的末尾在硬盘中读取不到数据的时候,为-1
     *
     */
    @GetMapping("/download")
    public R download(String name, HttpServletResponse response) throws IOException {   // 参数name用来接收前端发送的name请求资源(该name就是对应的照片的名)

        FileInputStream fis =null;
        ServletOutputStream outputStream =null;


        try {

            
        /**
         * // 1、输入流,通过输入流读取文件内容 (输入流:就是从硬盘【相当于电脑的D盘】中把数据读入到内存【idea上】中)
         */

            fis = new FileInputStream(basePath+name);
            // 假设我们就读yml属性配置的D:\iiiiiiis\路径下的name照片名的资源照片(D:\iiiiiiis\下的资源就相当于在硬盘中,然后读入到我们idea上)

            /**
             *  2、初始化一个byte数组 (如果忘记怎么用的话,看IO流改进循环升级版笔记)
             *      目的:就是把1中读取出来的硬盘上的数据,循环读入到数组当中。
             *
             */
            byte[] bytes = new byte[1024];
            int readCount =0;
            while ((readCount =fis.read(bytes))!=-1){    // read(bytes)方法是用来往bytes数组中一直读输入流读取出来的硬件上的数据的,全部读完的时候为-1

                // 能到这里说明read为-1了,也就意味着硬盘上的数据全部已经读取到了bytes数组当中了,此时的数组当中已经有了硬盘上的数据了。

                /**
                 * // 那么数组中有了硬盘上的数据之后,我们就创建输出流,
                 * // 把数组中读取到的硬盘中的数据(也就是图片资源)通过输出流响应给前端浏览器,然后前端拿到该照片后,在页面上展示该图片
                 *
                 *  实现思路: 通过Response向前端页面响应字节数据 。这也是我们方法中有response参数的原因(忘记的话看Response笔记)
                 *
                 */

                // 1、通过Response对象获取字符输出流
                outputStream = response.getOutputStream();
                // 2、写数据,把上面通过输入流读入到bytes数组中的硬盘中的图片资源写给前端页面
                outputStream.write(bytes);  // 把上面通过输入流读入到bytes数组中的硬盘中的图片资源写给前端页面

                /**
                 *  注意输出流写完之后别忘记刷新一下管道
                 */
                outputStream.flush();

            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        finally {  // 在finally语句块当中确保流一定关闭(因为如果前面的代码出现异常了,但是finally块还是能执行了,所以在finally块中关闭流确保了流的关闭)
            // 关闭流的前提是:流不是空,流是null的时候没必要关闭

            /**
             *  把输入流和输出流别忘记关闭
             *
             *      注意: 这里因为是在finally分块当中关闭输入流和输出流的,因此这也是为什么上面的输入流和输出流要写成如下这种形式:
             *                      FileInputStream fis =null;
             *         ServletOutputStream outputStream =null;
             *         如果不这样把输入流和输出流定义成全局变量的话,那么这里的finally分块中就无法让输入流和输出流关闭流。
             *
             */
            fis.close();
            outputStream.close();


        }
        
        return null;
    }

最终会发现,客户端向后端发送请求下载D盘中的照片的时候,我们后端确实把D盘中对应的照片数据通过流的形式响应给前端页面了:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐