SpringBoot文件上传

364 阅读11分钟

在网页中,经常可以看见上传文件的情况,那么上传文件的前后端是怎么实现的呢?琢磨了一番,总结了一点,供大家学习参考。

1. 前端表单页面

一般来说,大家都习惯于使用表单来实现文件的上传,也有其他方法,没有过于深入探讨。

 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>文件上传</title>
 </head>
 <body>
     <div>
         <!-- 若上传文件,必须将表单的enctype属性设置为multipart/form-data -->
         <form method="post" action="/upload" enctype="multipart/form-data">
             <span>请选择需要上传的文件:</span>
             <input type="file" name="imgFile"/>
             <br>
             <input type="submit" value="upload"/>
         </form>
     </div>
 </body>
 </html>

当需要上传文件时,需要将form的enctype属性改为multipart/form-data,否则不能实现文件的上传,不妨来测一下,若不设置该属性会怎么?不设置其实为application/x-www-form-urlencoded,这是enctype的默认值。

image-20240407163710852

可以看到,上传的时候其实只是将该文件的名字作为字符串传递给后端,那这肯定会报错,而改为multipart/form-data是什么样子呢?

image-20240407164134751

从请求标头就可以看见这里所设置的属性值了,那后面这个又是什么呢?这个其实就是每一个input标签的分隔符,看下面一张图就清楚了。

image-20240407164440183

image-20240407164336351

可以看出,这里的数据形式与上面的就不一致了,若是使用firefox可以看到数据的内容,而谷歌浏览器是将其省略了。

<input type="file" name="imgFile"/>指定type为file类型,则该输入标签为文件类型。

image-20240407165706224

注意:

这里后端使用Spring Boot框架,需要将该HTML文件放在resources/static目录下。

2. 上传文件至本地存储

后端接收到前端传过来的数据,是保存在一个临时文件夹中的,请放心,这个临时文件在结束程序时是会自动删除的。

在Controller类中,使用MultipartFile类提供的API方法,可以把临时文件转存到本地磁盘目录下,这也就是本地存储。

MultipartFile 常见方法:

  • String getOriginalFilename(); // 获取原始文件名
  • void transferTo(File dest); // 将接收的文件转存到磁盘文件中
  • long getSize(); // 获取文件的大小,单位:字节
  • byte[] getBytes(); // 获取文件内容的字节数组
  • InputStream getInputStream(); // 获取接收到的文件内容的输入流
 @RestController
 public class UploadFileController {
 ​
     @PostMapping("/upload")
     public String upLoadFile(MultipartFile imgFile) throws IOException {
         // 获取原始文件名
         String originalFilename = imgFile.getOriginalFilename();
 ​
         // 将文件存储在服务器的磁盘目录(本地目录)
         imgFile.transferTo(new File("E:/images/" + originalFilename));
 ​
         return originalFilename + "上传成功!";
     }
 }

注意:

这里的函数参数需要同上面表单的name名字一样,即:

image-20240407203944719

这里就产生疑问了,如果表单项的名字和方法中形参名不一致,该怎么办?

 public String upLoadFile(MultipartFile file) // file形参名和请求参数名imgFile不一致

使用@RequestParam注解进行参数绑定即可:

 public String upLoadFile(@RequestParam("imgFile") MultipartFile file) 

然后若上传相同文件名的文件,之前的文件就会被覆盖,这里可以使用UUID来生成唯一的文件名,来替换originalFilename。

进行测试:

浏览器直接访问http://localhost:8080/upload.html,就可访问到上面的静态文件中的upload.html。

进行上传操作,

image-20240407202908253

上传成功:

image-20240407202926584

然后来看本地是否有该文件:

image-20240407203019498

这样就已经成功了。

有的可能会发现,诶,为什么我失败了,控制台输出了如下语句:

image-20240407203245313

这是因为上传的文件比较大(超出了1M),就会出现了这个问题。因为在SpringBoot中,文件上传时默认单个文件最大大小为1M,那么如果需要上传大文件,可以在application.yml进行如下配置:

 spring:
   servlet:
     multipart:
       max-file-size: 10MB         # 配置单个文件最大上传大小
       max-request-size: 100MB     # 配置单个请求最大上传大小(一次请求可以上传多个文件)

根据自己要求进行设置就可以了,然后再次上传就不会报错。

本地存储也会存在一些问题,会增加服务端的压力,当数据比较多时,需要大量的存储空间,且容量也不好扩增。

现如今使用更多的是云服务器,如:阿里云、腾讯云等。

3. 上传文件至云服务存储

3.1 阿里云对象存储

阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商 。

image-20221229093412464

云服务指的就是通过互联网对外提供的各种各样的服务,比如像:语音服务、短信服务、邮件服务、视频直播服务、文字识别服务、对象存储服务等等。

当我们在项目开发时需要用到某个或某些服务,就不需要自己来开发了,可以直接使用阿里云提供好的这些现成服务就可以了。比如:在项目开发当中,我们要实现一个短信发送的功能,如果我们项目组自己实现,将会非常繁琐,因为你需要和各个运营商进行对接。而此时阿里云完成了和三大运营商对接,并对外提供了一个短信服务。我们项目组只需要调用阿里云提供的短信服务,就可以很方便的来发送短信了。这样就降低了我们项目的开发难度,同时也提高了项目的开发效率。(大白话:别人帮我们实现好了功能,我们只要调用即可)

云服务提供商给我们提供的软件服务通常是需要收取一部分费用的。

阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

image-20220904200642064

在我们使用了阿里云OSS对象存储服务之后,我们的项目当中如果涉及到文件上传这样的业务,在前端进行文件上传并请求到服务端时,在服务器本地磁盘当中就不需要再来存储文件了。我们直接将接收到的文件上传到oss,由oss帮我们存储和管理,同时阿里云的oss存储服务还保障了我们所存储内容的安全可靠。

image-20221229095709505

那我们学习使用这类云服务,我们主要学习什么呢?其实我们主要学习的是如何在项目当中来使用云服务完成具体的业务功能。而无论使用什么样的云服务,阿里云也好,腾讯云、华为云也罢,在使用第三方的服务时,操作的思路都是一样的。

image-20221229093911113

SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。

简单说,sdk中包含了我们使用第三方云服务时所需要的依赖,以及一些示例代码。我们可以参照sdk所提供的示例代码就可以完成入门程序。

第三方服务使用的通用思路,我们做一个简单介绍之后,接下来我们就来介绍一下我们当前要使用的阿里云oss对象存储服务具体的使用步骤。

image-20221229112451120

Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。

下面我们根据之前介绍的使用步骤,完成准备工作:

  1. 注册阿里云账户(注册完成后需要实名认证)
  2. 注册完账号之后,就可以登录阿里云

image-20220904201839857

  1. 通过控制台找到对象存储OSS服务

image-20220904201932884

如果是第一次访问,还需要开通对象存储服务OSS

image-20220904202537579

image-20220904202618423

  1. 开通OSS服务之后,就可以进入到阿里云对象存储的控制台(OSS管理控制台界面)。

image-20240407210532351

  1. 点击左侧的 "Bucket列表",创建一个Bucket

image-20240407210655406

image-20240407210840736

这里设置公共读,否则后续使用链接访问时,没有权限。

3.2 JAVA调用阿里云oss

阿里云oss对象存储服务的准备工作我们已经完成了,接下来我们就来参照官方所提供的sdk示例来编写入门程序。

官方参考文档

首先我们需要来打开阿里云OSS的官方文档,在官方文档中找到 SDK 的示例代码:

image-20221229121848524

image-20221229122046597

image-20221229144342148

image-20221229160827124

参照官方提供的SDK,改造一下,即可实现文件上传功能:

 import com.aliyun.oss.ClientException;
 import com.aliyun.oss.OSS;
 import com.aliyun.oss.OSSClientBuilder;
 import com.aliyun.oss.OSSException;
 import com.aliyun.oss.model.PutObjectRequest;
 import com.aliyun.oss.model.PutObjectResult;
 ​
 import java.io.FileInputStream;
 import java.io.InputStream;
 ​
 public class AliOssTest {
     public static void main(String[] args) throws Exception {
         // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
         String endpoint = "oss-cn-shanghai.aliyuncs.com";
         
         // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
         String accessKeyId = "xxxxxxxxxx";
         String accessKeySecret = "xxxxxxxxxx";
         
         // 填写Bucket名称,例如examplebucket。
         String bucketName = "spring-boot-learn";
         // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
         String objectName = "xiaogou.jpg";
         // 填写本地文件的完整路径,例如D:\localpath\examplefile.txt。
         // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
         String filePath= "‪E:\Profile\Image\xiaogou.jpg";
 ​
         // 创建OSSClient实例。
         OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
 ​
         try {
             InputStream inputStream = new FileInputStream(filePath);
             // 创建PutObjectRequest对象。
             PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
             // 设置该属性可以返回response。如果不设置,则返回的response为空。
             putObjectRequest.setProcess("true");
             // 创建PutObject请求。
             PutObjectResult result = ossClient.putObject(putObjectRequest);
             // 如果上传成功,则返回200。
             System.out.println(result.getResponse().getStatusCode());
         } catch (OSSException oe) {
             System.out.println("Caught an OSSException, which means your request made it to OSS, "
                     + "but was rejected with an error response for some reason.");
             System.out.println("Error Message:" + oe.getErrorMessage());
             System.out.println("Error Code:" + oe.getErrorCode());
             System.out.println("Request ID:" + oe.getRequestId());
             System.out.println("Host ID:" + oe.getHostId());
         } catch (ClientException ce) {
             System.out.println("Caught an ClientException, which means the client encountered "
                     + "a serious internal problem while trying to communicate with OSS, "
                     + "such as not being able to access the network.");
             System.out.println("Error Message:" + ce.getMessage());
         } finally {
             if (ossClient != null) {
                 ossClient.shutdown();
             }
         }
     }
 }
 ​

在以上代码中,需要替换的内容为:

  • accessKeyId:阿里云账号AccessKey
  • accessKeySecret:阿里云账号AccessKey对应的秘钥
  • bucketName:Bucket名称
  • objectName:对象名称,在Bucket中存储的对象的名称
  • filePath:文件路径

AccessKey :

image-20240407211814644

运行以上程序后,会把本地的文件上传到阿里云OSS服务器上:

image-20240407212103085

3.3 优化阿里云oss模型

上述简单使用了java调用阿里云oss进行文件上传,现在需要集成到我们自己的代码中,当然最"简单粗暴"的方法就是直接将参上述代码中复制到自己的代码中即可以使用,但是这样总归不好维护,故应进行一些优化处理,下面就娓娓道来。

依赖导入:

 <!-- 阿里云OSS -->
 <dependency>
     <groupId>com.aliyun.oss</groupId>
     <artifactId>aliyun-sdk-oss</artifactId>
     <version>3.15.1</version>
 </dependency>
 ​
 <!-- 以下是如果使用的是Java 9及以上的版本,则需要添加JAXB相关依赖 -->
 <dependency>
     <groupId>javax.xml.bind</groupId>
     <artifactId>jaxb-api</artifactId>
     <version>2.3.1</version>
 </dependency>
 <dependency>
     <groupId>javax.activation</groupId>
     <artifactId>activation</artifactId>
     <version>1.1.1</version>
 </dependency>
 <dependency>
     <groupId>org.glassfish.jaxb</groupId>
     <artifactId>jaxb-runtime</artifactId>
     <version>2.3.3</version>
 </dependency>

这里根据官方文档来就好。

封装官网SDK提供的方法,后续需要使用时,可以直接调用就行:

AliOssUtil.java

 package com.example.utils;
 ​
 import com.aliyun.oss.OSS;
 import com.aliyun.oss.OSSClientBuilder;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import org.springframework.web.multipart.MultipartFile;
 ​
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.UUID;
 ​
 @Data
 @AllArgsConstructor
 public class AliOssUtil {
 ​
     private String endpoint;
     private String accessKeyId;
     private String accessKeySecret;
     private String bucketName;
 ​
     /**
      * 文件上传
      *
      * @param file 上传的文件对象
      * @return 该文件的链接
      */
     public String upload(MultipartFile file) throws IOException {
         // 获取上传的文件的输入流
         InputStream inputStream = file.getInputStream();
         // 获取上传文件的原始名字
         String originalFilename = file.getOriginalFilename();
         // 避免上传的文件覆盖 使用UUID创建新的名字
         String fileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
 ​
         // 创建OSSClient实例
         OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
         // 创建PutObject请求
         ossClient.putObject(bucketName, fileName, inputStream);
 ​
         // 文件访问路径规则 https://BucketName.Endpoint/ObjectName
         String stringBuilder = "https://" + bucketName + "." + endpoint + "/" + originalFilename;
 ​
         // 关闭ossClient
         ossClient.shutdown();
         return stringBuilder;
     }
 }

细心的可能会发现,我这里并没有填入endpointaccessKeyId等参数,只是设置了私有属性,这里大家可以直接赋值就行,我只是为了更加的"优雅",将这些参数设置到了配置文件中,若不想这么麻烦,直接在这里填入对应的数据就好。

我设置到配置文件中,然后就需要就需要代码去读取对应的数据,我又创建了一个AliOssProperties文件,去获取数据。

application.yml:

 # 阿里云oss 注意使用自己对应的数据 endpoint也可以在bucket的概览中获取
 alioss:
   endpoint: oss-cn-beijing.aliyuncs.com
   access-key-id: xxxxxxxxxxxxxx
   access-key-secret: xxxxxxxxxxx
   bucket-name: spring-boot-learn

AliOssProperties.java

 package com.example.properties;
 ​
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 ​
 @Component
 @ConfigurationProperties(prefix = "alioss")
 @Data
 public class AliOssProperties {
     private String endpoint;
     private String accessKeyId;
     private String accessKeySecret;
     private String bucketName;
 ​
 }

这里使用了Spring Boot框架,可以将其实例化Bean对象并交给IOC容器托管,故创建了一个配置类,后续使用时直接注入就好。

AliOssConfiguration.java

 package com.example.config;
 ​
 import com.example.properties.AliOssProperties;
 import com.example.utils.AliOssUtil;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 ​
 /**
  * 配置类,用于创建AliossUtil对象
  */
 @Configuration
 public class AliOssConfiguration {
 ​
     @Bean
     @ConditionalOnMissingBean
     public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
         return new AliOssUtil(aliOssProperties.getEndpoint(),
                 aliOssProperties.getAccessKeyId(),
                 aliOssProperties.getAccessKeySecret(),
                 aliOssProperties.getBucketName());
     }
 }

这些工做完之后,回到我们的Controller类中,修改其中的业务代码:

 @RestController
 public class UploadFileController {
 ​
     @Autowired
     private AliOssUtil aliOssUtil;
 ​
     @PostMapping("/upload")
     public String upLoadFile(MultipartFile imgFile) throws IOException {
        // 将文件存储在阿里云OSS对象存储中 返回为图片的url链接
        String fileUrl = aliOssUtil.upload(imgFile);
        System.out.println("文件上传到:" + fileUrl);
 ​
        return originalFilename + "上传成功!";
     }
 }

再进行测试:

image-20240407215529314

image-20240407215556452

image-20240407215624659

就上传成功了!

image-20240407220043498

该方法返回了文件的url,可以直接访问该链接就可以获取到文件了!

整个项目的结构为: