首页 > 其他分享 >Spring Boot实现文件上传的两种方式

Spring Boot实现文件上传的两种方式

时间:2023-09-28 13:56:30浏览次数:30  
标签:文件 请求 Spring Boot upload part 上传

最近的一个小项目里使用到了文件上传、下载功能,今天我打算梳理一下文件上传所涉及的技术及实现。 内容主要包括两部分,如何通过纯 Servlet 的形式进行文件上传、保存(不通过 Spring 框架);另一部分是如何在 Spring Web MVC 中进行文件上传。

01-从 HTTP 协议角度分析文件上传

HTTP 协议传输文件一般都遵循 RFC 1867 规范,即客户端通过 POST 请求,Context-Type 为 "multipart/form-data"。 前端提交页面一般为:

<form method="post" action="${user_upload_service_url}" enctype="multipart/form-data">
    Choose a file: <input type="file" name="image" accept="image/*" />
    <input type="submit" value="Upload" />
</form>

通过 Wireshark 对 POST 请求进行抓包,发现发送的请求格式为:

POST /upload HTTP/1.1
Host: localhost:8080
Content-Length: 197624
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynIbwtdWznj6QLu52

First boundary: ------WebKitFormBoundarynIbwtdWznj6QLu52
Encapsulated multipart part:  (image/png)
    Content-Disposition: form-data; name="image"; filename="Snipaste_2023-01-05_13-35-11.png"
    Content-Type: image/png
    Portable Network Graphics
Boundary: ------WebKitFormBoundarynIbwtdWznj6QLu52
Encapsulated multipart part:  (image/png)
    Content-Disposition: form-data; name="image"; filename="Snipaste_2023-01-05_13-35-12.png"
    Content-Type: image/png
    Portable Network Graphics
Last boundary: ------WebKitFormBoundarynIbwtdWznj6QLu52--

对上述过程有了基本的理解后,就可以动手来写上传功能(本文以图片为例,当然你也可以实现支持上传其他类型的文件的版本)。 接下来我会展示两种实现文件上传功能的代码,第一种是使用纯 Servlet API 实现,不依赖 Spring 框架,当你的程序是一个简单的基于 Servlet 的应用时,可以参考这种方式。 第二种,借助了 Spring 提供的 MultipartFile 以及 MultipartResolver 实现的文件上传。

02-Servlet 处理上传请求

首先,需要先实现一个 Servlet。

@MultipartConfig(fileSizeThreshold = 5 * 1024 * 1024,
        maxFileSize = 1024 * 1024 * 5,
        maxRequestSize = 1024 * 1024 * 5)
@WebServlet(name = "MultipartServlet", urlPatterns = "/servlet-upload")
public class MultipartServlet extends HttpServlet {

    private File uploadDir = null;

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        // 检查存储文件的路径是否存在,若不存在,则创建一个
        String uploadPath = System.getProperty("user.dir") + File.separator + "uploads";
        uploadDir = new File(uploadPath);
        if (!uploadDir.exists()) {
            uploadDir.mkdir();
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 第一节中介绍过,文件上传是通过 POST 方法完成的,所以这里我们要重写 doPost 方法
        try {
            final Collection<Part> parts = req.getParts();   // 从请求中获取 multipart 内容
            for (Part part : parts) {
                if (part.getSize() <= 0) {                  // 判断上传的内容是否空文件
                    System.out.println("part is empty, skip it!");
                    continue;
                }
                String fileName = getFileName(part);       // 从请求中获取文件的名
                // or
                //final String fileName = part.getSubmittedFileName();

                // fileName 是前端提供的,并不十分可靠
                // 后端应该自己生成一个文件名
                fileName = genNewFileName(fileName);

                String uploadedFilePath = uploadDir + File.separator + fileName;
                part.write(uploadedFilePath);   // 存储到指定目录
                System.out.println("saved to " + uploadedFilePath);
                resp.getWriter().write("saved to "  + uploadedFilePath);
            }
        } catch (ServletException se) {
            // request is not of type multipart/form-data
        }

        resp.setStatus(HttpServletResponse.SC_OK);
        resp.getWriter().flush();
        resp.getWriter().close();
    }

    private String getFileName(Part part) {
        for (String s : part.getHeader("Content-Disposition").split(";")) {
            if (s.trim().startsWith("filename")) {
                return s.substring(s.indexOf("=") + 2, s.length() - 1);
            }
        }
        // 默认文件名
        return "foo.txt";
    }
    private String genNewFileName(String filename) {
        String filenameFormat = "%s.%s";
        return String.format(filenameFormat,
                UUID.randomUUID().toString().replace("-", "").substring(8),
                FilenameUtils.getExtension(filename)
        );

    }
}

这里面有几个地方需要解释一下;

  • 其一,getFileName 为什么要这么实现?参考第一节给出的 HTTP 报文,发现每个 Part,即两个 boundary 之间的内容,通过 Content-Disposition 给出了内容类型、文件名等信息。 getFileName 中的逻辑就是从这个格式里获得文件名的。 不过,这个文件名是由前端提供的,它其实也可以不提供,所以这个值就不是那么可靠。 所以,在我们将上传文件保存到磁盘上时,最好重新生成一个文件名,这就使 genNewFileName 的动机。
  • 其二,根据 HttpServletRequest 接口的文档,getParts 方法在请求不是 multipart 类型时会抛异常。 而且,Part 的内容有可能是为空的,如果我们不做判断,可能会在服务端创建一个空文件。
  • Servlet 类上的注解,@WebServlet 不再介绍,@MultipartConfig 是对请求、请求中文件大小的限制条件,当请求或文件超过这个限制时会抛对应的异常。

有了上面的定义,我们就可以测试下上传功能了。 
服务启动后,访问 http://localhost:8080/servlet-upload-page 能够得到上传页面。 选择文件,提交后,服务端响应成功,并将新名字传给前端。
 

注:这里 http://localhost:8080/servlet-upload-page 会返回 Thymeleaf 实现的上传界面。

 @GetMapping("/servlet-upload-page")
public String uploadImageByServlet(Model model) {
    model.addAttribute("message", "please choose file to be uploaded");
    return "upload/servlet-upload";
}

界面内容为:

<body>
<h2>Upload Image Example</h2>
<p th:text="${message}" th:if="${message ne null}" class="alert alert-primary"></p>
<form method="post" th:action="@{/servlet-upload}" enctype="multipart/form-data">
    <div class="form-group">
        <input type="file" name="image" accept="image/*" class="form-control-file">
        <input type="file" name="image" accept="image/*" class="form-control-file">
    </div>
    <button type="submit" class="btn btn-primary">Upload image</button>
</form>
<span th:if="${msg != null}" th:text="${msg}"></span>
</body>
</html>

其中,@{/servlet-upload}指向的是@WebServlet(name = "MultipartServlet", urlPatterns = "/servlet-upload")中将 Servlet 注册到的 url。

03-通过 Spring Boot 中的 MultipartFile 处理上传请求


通过 Spring Boot 来实现文件上传功能会更简单,它的自动化配置机制已经做了大部分的工作。 开发人员的工作就是定义一个 Controller,处理文件上传请求就可以了。

@Controller
public class UploadController {

    public static String UPLOAD_DIRECTORY = System.getProperty("user.dir") + File.separator + "uploads";

    @GetMapping("/upload")   // 主要返回文件上传页面
    public String uploadImage(Model model) {
        model.addAttribute("message", "please choose file to be uploaded");
        return "upload/index";
    }
    
    @PostMapping("/upload")  // 处理文件上传 POST 请求
    public String upload(@RequestParam("image")MultipartFile[] files,
                         Model model)
            throws IOException {

        StringBuilder sb = new StringBuilder();
        for (MultipartFile file : files) {
            if (file.getSize() <= 0) {
                continue;
            }
            final String newFileName = save(file);
            final String msg = String.format("uploaded file %s, and new filename is %s%n", file.getOriginalFilename(), newFileName);
            sb.append(msg);
        }
        model.addAttribute("msg", sb.toString());

        return "upload/index";
    }

    private String save(MultipartFile file) throws IOException{
        String newFileName = genNewFileName(file.getOriginalFilename());
        final Path filePath = Paths.get(UPLOAD_DIRECTORY, newFileName);
        Files.write(filePath, file.getBytes());

        System.out.println("file saved to: " + filePath);
        return newFileName;
    }
}

Spring Boot 中,文件上传请求(multipart request)被 StandardServletMultipartResolver 进一步封装为 StandardMultipartHttpServletRequest。 解析原请求的过程与我在前面介绍 Servlet 的方式时基本类似:

private void parseRequest(HttpServletRequest request) {
    try {
        Collection<Part> parts = request.getParts();
        this.multipartParameterNames = new LinkedHashSet<>(parts.size());
        MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
        for (Part part : parts) {  
            String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
            ContentDisposition disposition = ContentDisposition.parse(headerValue);
            String filename = disposition.getFilename();
            if (filename != null) {
                // 把文件添加到 files
                if (filename.startsWith("=?") && filename.endsWith("?=")) {
                    filename = MimeDelegate.decode(filename);
                }
                // part 被封装为 StandardMultipartFile,它是 MultipartFile 的一个实现类
                files.add(part.getName(), new StandardMultipartFile(part, filename));
            }
            else {
                // 把不是文件的属性添加到 multipartParameterNames 中
                this.multipartParameterNames.add(part.getName());
            }
        }
        setMultipartFiles(files);
    }
    catch (Throwable ex) {
        handleParseFailure(ex);
    }
}

通过上面的代码可以了解到,Client 提交的 POST 请求中,上传的文件被封装称 MultipartFile。 所以,我们在 Controller 中的处理方法中,可以通过 @RequestParam 的方式拿到这个文件列表进行处理,就像我们的 UploadController 实现的那样。

04-总结

在今天的文章中,我介绍了文件上传的两种实现方式,从纯 Servlet 实现,到基于 Spring Boot MVC 实现。 并且分析了 Spring Boot 中对 Multipart 请求的封装过程。

 

 

参考文章:http://blog.ncmem.com/wordpress/2023/09/28/spring-boot%e5%ae%9e%e7%8e%b0%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e7%9a%84%e4%b8%a4%e7%a7%8d%e6%96%b9%e5%bc%8f/

欢迎入群一起讨论

 

 

标签:文件,请求,Spring,Boot,upload,part,上传
From: https://www.cnblogs.com/songsu/p/17735568.html

相关文章

  • Spring Framework框架
    SpringFramework框架一、含义:简称为Spring,是一个开源的、综合性的Java应用程序开发框架。它提供了一系列的功能和特性,用于开发企业级的Java应用程序。二、主要模块支持IoC和AOP的容器IoC(InversionofControl,控制反转):一种设计原则1.含义:它指将对象的创建、依赖关系的管理......
  • SpringMVC 的执行流程
    具体流程如下所示:用户发送出请求到前端控制器DispatcherServlet。DispatcherServlet收到请求调用HandlerMapping(处理器映射器)。HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。Dis......
  • Spring boot 处理大文件上传
    在Web上处理大文件上传时,可以使用以下方法来优化和处理大文件的上传:前端处理:在前端使用合适的文件上传库或组件,例如Dropzone.js、FineUploader等,它们提供了更好的用户体验和可靠的上传功能。使用分块上传(ChunkedUpload)技术,将大文件拆分成较小的块进行上传,以便提高上传的可靠性......
  • 最近正在集成SpringBoot与MyBatis-plus,体验感很好啊
    sqlCREATETABLE`class`(`id`int(11)NOTNULLAUTO_INCREMENTCOMMENT'编号',`name`varchar(30)DEFAULTNULLCOMMENT'班级名',`floor`int(3)DEFAULTNULLCOMMENT'楼层',`teacher_id`int(11)DEFAULTNULLCOMMENT'老师......
  • 实战指南,SpringBoot + Mybatis 如何对接多数据源
    本文分享自华为云社区 《实战指南,SpringBoot+Mybatis如何对接多数据源》,作者:战斧。在我们开发一些具有综合功能的项目时,往往会碰到一种情况,需要同时连接多个数据库,这个时候就需要用到多数据源的设计。而Spring与Myabtis其实做了多数据源的适配,只需少许改动即可对接多数据源。......
  • springcloud gateway 获取响应体进行加密操作,byte[]转换String乱码
    记录一下困扰一星期的问题!在全局过滤器中,获取响应体进行加密操作,在拿到byte[]之后转成String,控制台打印出来是乱码,编码也加了UTF-8还是报错。publicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){ServerHttpResponseoriginalResponse=ex......
  • SpringBoot实现文件上传的多种方式
    我们平时在项目开发过程中,会遇到许多的文件上传与下载的需求,今天我们就来梳理一下文件上传的代码实现,基于SpringBoot快速搭建服务,集成文件上传功能,包括传统的文件上传方式,也拓展OSS对象存储方式。项目类型是Maven项目一、引入web依赖<dependency><groupId>org.springframework......
  • 如何保证Spring Boot接口安全的呢?
     在保证SpringBoot接口安全时,我们需要关注的主要方面包括:认证(Authentication)、授权(Authorization)、数据安全性(DataSecurity)、以及防止常见的Web安全威胁。认证(Authentication)在SpringSecurity中,认证是验证用户的过程。通过用户名和密码、OAuth2令牌、JWT(JSONWebTokens......
  • Web-入门-SpringBoot快速入门 创建springboot web项目
    web入门spring官网spring发展到今天已经形成了一种开发生态圈,spring提供了若干个子项目,为每个项目用于完成特定的功能。这些框架都是基于一个基础框架:直接基于SpringFramework基础框架进行开发会有两大难题:1.配置繁琐。2.入门难度大。所以spring家族意识到了这一点,......
  • 使用SpringBoot开发一个POST接口
    SpringBoot项目的分层SpringBoot框架项目一般分为五层:View层:向用户展示页面Controller层:前后端交互层,接收前端请求,调用Service层中的方法,接收Service层返回的数据并将其返回到前端。Service层:存放业务处理的逻辑,以及一些操作数据库的接口Mapper层:也可以成为DAO层,是数......