likes
comments
collection

深入源码!详解MultipartFile

作者站长头像
站长
· 阅读数 8

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

1、一个小问题

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

1.1、整体结构

深入源码!详解MultipartFile

我们定义了以下结构:

  • 服务端

    • 生产者,将文件上传至生产者,经过处理后再发送到消费者
    • 消费者,消费生产者发送来的文件
  • 远程调用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上传文件进行测试,结果如下所示

生产者端:

深入源码!详解MultipartFile 消费者端:

深入源码!详解MultipartFile

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

这是因为虽然生产者将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

5、小结

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

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

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