相关文章推荐
绅士的咖啡  ·  Get started with ...·  5 月前    · 
阳刚的数据线  ·  Spring ...·  1 年前    · 
听话的牛腩  ·  CefSharp 使用 ...·  1 年前    · 
·  阅读

MultipartFile大家想必不陌生,在SpringMVC的控制器方法中,我们可以通过MultipartFile自动注入上传的文件。我们从一个小案例引入,深入了解下MultipartFile

1、一个小问题

此问题来自真实案例,大家可以先想想当我们通过生产者端 /producer/produce 上传文件时,消费者端会输出什么

1.1、整体结构

我们定义了以下结构:

  • 生产者,将文件上传至生产者,经过处理后再发送到消费者
  • 消费者,消费生产者发送来的文件
  • 远程调用client

  • 提供消费者的远程调用服务
  • 1.2、生产者端

    我们简单定义了一个上传功能,如下所示

    @RestController
    public class ProducerController {
        @Autowired
        private ConsumerFeignClient consumerFeignClient;
        @PostMapping("/producer/produce")
        public String produce(@RequestBody MultipartFile video) {
            System.out.println("video:"+video);
            //经过一系列操作,将视频转为图片...
            MultipartFile photo=video;
            consumerFeignClient.consume(photo);
            return "video";
    

    1.3、消费者端

    在消费者端我们直接输出photo

    @RestController
    public class ConsumerController {
        @PostMapping("/consumer/consume")
        public String consume(@RequestBody MultipartFile photo) {
            System.out.println("photo:"+photo);
            return "photo";
    

    1.4、远程调用消费者端

    这里仅仅定义了远程调用接口,以提供给生产者调用

    @FeignClient("service-consumer")
    @Repository
    public interface ConsumerFeignClient {
        @PostMapping(value = "/consumer/consume",consumes = "multipart/form-data")
        public String consume(@RequestBody MultipartFile photo);
    

    1.5、问题

    消费者端会输出什么?

    问题的答案放在文章的末尾。

    2、MultipartFile接口

    MultipartFile其实是一个接口,其中定义提供了上传文件的各方面信息。话不多说,直接上源码。通过注释的方式解读一下各个方法的意义。

    public interface MultipartFile extends InputStreamSource {
        //获取文件的名字,这里指的是post表单里面定义的名字
        String getName();
        //获取文件的原名字,这里指的是本地文件真正的名字
        @Nullable
        String getOriginalFilename();
        //文件的类型
        @Nullable
        String getContentType();
        //文件是否为空
        boolean isEmpty();
        //文件的大小
        long getSize();
        //获取文件的byte数组
        byte[] getBytes() throws IOException;
        //以流的方式获取文件
        InputStream getInputStream() throws IOException;
        //将其转为Resource类型,可以将其视为文件资源
        default Resource getResource() {
            return new MultipartFileResource(this);
        //将其转换为文件
        void transferTo(File var1) throws IOException, IllegalStateException;
        //通过提供文件路径的方式将其转换为文件
        default void transferTo(Path dest) throws IOException, IllegalStateException {
            FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest));
    

    其中关于提供文件基础信息的方法为:

  • String getName();
  • String getOriginalFilename();
  • String getContentType();
  • byte[] getBytes() throws IOException; 或 InputStream getInputStream() throws IOException;
  • 这些方法描述一个文件所必须的几要素,只要有这些方法就可以获得一个文件了,其他几个方法为方便我们操作的API

    3、自己写一个MultipartFile子类

    参考org.springframework.mock.web包下MockMultipartFile的实现,我写出了MultipartFileDto。

    只需要根据接口中定义的规范,提供所需的要素即可

    public class MultipartFileDto implements MultipartFile {
        public MultipartFileDto() {
            super();
        //用于储存post表单里的文件名
        private String name;
        //用于储存文件原名
        private String originalFilename;
        //用于储存文件类型
        private String contentType;
        //用于储存文件数据
        private byte[] content;
        public MultipartFileDto(String name, byte[] content) {
            this(name, "", null, content);
        public MultipartFileDto(String name, InputStream contentStream) throws IOException {
            this(name, "", null, FileCopyUtils.copyToByteArray(contentStream));
        public MultipartFileDto(String name, String originalFilename, String contentType, byte[] content) {
            this.name = name;
            this.originalFilename = (originalFilename != null ? originalFilename : "");
            this.contentType = contentType;
            this.content = (content != null ? content : new byte[0]);
        public MultipartFileDto(String name, String originalFilename, String contentType, InputStream contentStream)
                throws IOException {
            this(name, originalFilename, contentType, FileCopyUtils.copyToByteArray(contentStream));
        @Override
        public String getName() {
            return this.name;
        @Override
        public String getOriginalFilename() {
            return this.originalFilename;
        @Override
        public String getContentType() {
            return this.contentType;
        @Override
        public boolean isEmpty() {
            return (this.content.length == 0);
        @Override
        public long getSize() {
            return this.content.length;
        @Override
        public byte[] getBytes() throws IOException {
            return this.content;
        @Override
        public InputStream getInputStream() throws IOException {
            return new ByteArrayInputStream(this.content);
        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            FileCopyUtils.copy(this.content, dest);
    

    4、答案与正确实践

    4.1、答案

    通过swagger上传文件进行测试,结果如下所示

    生产者端:

    消费者端:

    可以看到,消费者端并没有获取到生产者提供的文件。

    这是因为虽然生产者将video赋予了photo变量,但实质上只是指针的改变,photo文件中通过getName()方法获得的文件名还是 “video”,自然不会自动注入了

    4.2、正确实践

    在消费者端我们再提供一个正确的控制器方法,利用到了上文自己定义的MultipartFileDto,将文件名改成了“photo”,使得消费者端可以成功注入。

    @PostMapping("/producer/produceRight")
    public String produceRight(@RequestBody MultipartFile video) throws IOException {
        System.out.println("video:"+video);
        //经过一系列操作..
        MultipartFile photo=
                new MultipartFileDto("photo","video.mp4","text/plain", video.getInputStream());
        consumerFeignClient.consume(photo);
        return "video";
    

    提供swagger进行测试,消费者端正确地获得了文件:

    本文通过阅读MultipartFile接口的源码,了解了其对外提供的功能,并且自己定义了可以实现其功能的子类,从而解决了远程调用时文件获取异常的问题。

    通过MultipartFile接口阅读,我们可以得到简单的结论:

    接口的定义是为了定义规范,方便使用者编写实现类与调用。而一个接口所需要向外提供的API是根据实际生产需求而定的,就如同MultipartFile接口,其核心是提供文件信息,对其进行描述。源码并没有想象中的那么晦涩,了解其核心需求能更好地帮助我们理解作者的编写意图与思路

    分类:
    后端
  •