文章目录
在实际的项目开发中,文件的上传和下载可以说是最常见的功能之一,例如图片、视频、音频等的上传和下载等。本文章将探讨在SpringMVC中如何将文件上传到服务器。
实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
- 直接将图片保存到服务器的磁盘目录中
- 优点:开发便捷,成本低
- 缺点:
- 磁盘空间有限,扩容困难
- 无法直接访问
- 不安全,磁盘损坏,文件将会丢失
- 使用分布式文件系统进行存储
- 优点:容易实现扩容
- 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)
- 使用第三方的存储服务(例如OSS)
- 优点:开发简单,拥有强大功能,免维护(阿里云、华为云、腾讯云等)
- 缺点:付费
- 直接将图片保存到服务器的磁盘目录中
1. 前端表单
在SpringMVC中实现文件上传工作,前端表单需做如下操作:
form表单的method属性必须设置为
post
,因为上传的文件可能会比较大。form表单的enctype(编码方式)必须设置为:
multipart/form-data
。至少提供一个type属性为file的input输入框。
<form action="/upload" method="post" enctype="multipart/form-data"> 姓名: <input type="text" name="username"><br> 年龄: <input type="text" name="age"><br> 头像: <input type="file" name="image"><br> <input type="submit" value="提交"> </form>
2. 阿里云OSS
2.1 什么是阿里云OSS
阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。
在我们使用了阿里云OSS对象存储服务之后,我们的项目当中如果涉及到文件上传这样的业务,在前端进行文件上传并请求到服务端时,在服务器本地磁盘当中就不需要再来存储文件了。我们直接将接收到的文件上传到oss,由 oss帮我们存储和管理,同时阿里云的oss存储服务还保障了我们所存储内容的安全可靠。
2.2 实现阿里云OSS
2.2.1 准备工作
登录阿里云官网,注册阿里云账号:阿里云-计算,为了无法计算的价值。
通过控制台开通对象存储OSS服务。
在控制台左侧的Bucket列表,创建一个Bucket。
从右上角头像获取AccessKeyId和AccessKeySecret。
在新创建的Bucket下方获取Endpoint(地域节点)。
2.2.2 SpringBoot集成阿里云OSS
引入依赖:
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.17.4</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> <!-- no more than 2.3.3--> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.3</version> </dependency>
在配置文件中配置OSS相关信息:
alioss: endpoint: https://oss-cn-beijing.aliyuncs.com access-key-id: xxxxxxxxxxx access-key-secret: xxxxxxxxx bucket-name: test-tlias-1
@Data @Component @ConfigurationProperties(prefix = "aliyun.oss") public class AliOSSProperties { //节点 private String endpoint; //身份ID private String accessKeyId ; //身份密钥 private String accessKeySecret ; //存储空间 private String bucketName; }
在我们添加上注解后,会发现idea窗口上面出现一个红色警告:这个警告提示是告知我们还需要引入一个依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency>
阿里云OSS工具类:
@Component public class AliOSSUtil { @Autowired private AliOSSProperties aliOSSProperties; public String upload(MultipartFile multipartFile) throws IOException { //1.获取上传的文件的输入流 InputStream inputStream = multipartFile.getInputStream(); //2.避免文件覆盖 String originalFilename = multipartFile.getOriginalFilename(); String fileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf(".")); //3.上传文件到 OSS OSS ossClient = new OSSClientBuilder().build(aliOSSProperties.getEndpoint(), aliOSSProperties.getAccessKeyId(), aliOSSProperties.getAccessKeySecret()); ossClient.putObject(aliOSSProperties.getBucketName(), fileName, inputStream); //4.文件访问路径 String url = aliOSSProperties.getEndpoint().split("//")[0] + "//" + aliOSSProperties.getBucketName() + "." + aliOSSProperties.getEndpoint().split("//")[1] + "/" + fileName; //5.关闭ossClient ossClient.shutdown(); return url; } }
文件上传控制器:
@RestController public class TestController { @Autowired private AliOSSUtil aliOSSUtils; @PostMapping("/upload") public Result upload(MultipartFile image) throws IOException { //调用阿里云OSS工具类,将上传上来的文件存入阿里云 String url = aliOSSUtils.upload(image); //将图片上传完成后的url返回,用于浏览器回显展示 return Result.success(url); } }
3. 使用SpringCloud Alibaba快捷操作
之前我们是自己创建OSS对象来进行操作,实际上还可以整合SpringCloud Alibaba来更加方便地使用OSS。打开官方文档,找到OSS相关,在SpringBoot项目中引入OSS的starter。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-oss</artifactId> </dependency>
然后在yml配置文件中配置endpoint、accessKeyId、accessKeySecret。
server: port: 30000 spring: cloud: alicloud: oss: endpoint: oss-cn-hangzhou.aliyuncs.com access-key: ... secret-key: ...
在测试代码中无需创建ossClient,只需要将其注入进来即可使用。
@RunWith(SpringRunner.class) @SpringBootTest public class GulimallThirdpartyApplicationTests { // 注入ossClient @Autowired private OSS ossClient; @Test public void testUploadFile() throws FileNotFoundException { // 上传文件流。 InputStream inputStream = new FileInputStream("C:\\Users\\baobao\\Desktop\\guojia.jpg"); ossClient.putObject("gulimall-baobao", "guojia222.jpg", inputStream); // 关闭OSSClient ossClient.shutdown(); } }
4. 前端直传
如果我们用之前的方式上传文件到OSS,那么后端需要先接收前端表单上传的文件,然后再通过SDK将文件上传到OSS。但是实际开发中不推荐这么做,因为这种方式使得文件流先从客户端到后端应用服务器,然后再由服务器转给OSS,相当于多了1次中转,十分浪费服务器带宽,当很多用户都在上传的时候对后端应用服务器的带宽以及处理压力很大。
我们可以采取的优化方式是:在浏览器提交上传Policy请求给服务器,服务器返回上传Policy和签名,将上传地址等重要信息告诉浏览器,这样就可以由浏览器在客户端直接完成上传到oss的操作,分担了服务器压力。
新建一个OssController,编写生成Policy和前面的方法,参考阿里云官方SDK文档。
@RestController
public class OssController {
// 注入ossClient
@Autowired
private OSS ossClient;
// 从配置文件中获取accessKey、endpoint等必要信息
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.access-key}")
private String accessKey;
@Value("${spring.cloud.alicloud.secret-key}")
private String secretKey;
@Value("${spring.cloud.alicloud.bucket-name}")
private String bucketName;
@RequestMapping("oss/policy")
public Map<String, String> policy() {
// 用户上传文件时指定的前缀,指定当前日期
String date = DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDate.now());
String dir = date + "/";
// host的格式为 bucketname.endpoint
String host = "https://" + bucketName + "." + endpoint;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
Map<String, String> respMap = new LinkedHashMap<>();
respMap.put("accessid", accessKey);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
// respMap.put("expire", formatISO8601Date(expiration));
return respMap;
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
return null;
} finally {
ossClient.shutdown();
}
}
}
然后注意由于我们自定义了bucketName也从yml配置文件中取,所以要在yml中定义:
server:
port: 30000
spring:
cloud:
alicloud:
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
access-key: ...
secret-key: ...
bucket-name: gulimall-baobao # 配置bucket-name
5. 临时URL访问文件
之前我们对Bucket的读写权限设置为公共读,这样只要获取了文件的url,就可以在浏览器随意访问了,安全性不好。
一般建议是将Bucket的读写权限设置为私有,此时再通过文件url访问将会提示没有权限。
需要在url上附带一些参数,用这个附带参数的临时url才能访问到文件,而临时url过期后就无法再访问了。我们进入oss控制台,点击文件的详情就会显示临时url。
分析一下这个url的结构:
https://gulimall-baobao.oss-cn-hangzhou.aliyuncs.com/guojia.jpg?Expires=1616766718&OSSAccessKeyId=TMP.3KjvuWJovuqLjNppoo3D3jvDbZFbrfovBR39zhHkdmzrm6M9mdwJAkbc1Ffdhn77nyVWTi2PATz97t5zSKr2TDhoKiE1SY&Signature=jAVkArqRd8K2jFswLy9%2BzH62Emk%3D
可以看出其携带了3个参数:
- Expires:临时url的过期时间,默认5分钟
- OSSAccessKeyId:临时的accessKey
- Signature:签名
我们每点击一次详情,都会生成一个新的临时url,过期时间为点击后的5分钟,临时的accessKey都相同,但是签名不同。可以推测生成签名的参数中有过期时间
在我们自己的应用中,公共读的方式下,后端数据库只要保存文件上传后的url即可,前端需要展示图片的时候只要向后端请求获取图片url并交给图片组件即可。然而改成私有以后,前端如何向后端请求临时url并显示图片文件呢?此时后端接收到前端访问图片的请求,并获取数据库中图片的url后,不能直接返回给前端,需要先解析出图片文件在Bucket中的路径,然后根据bucketName、临时url超时时间、要访问文件在Bucket中的路径这3个参数生成一个临时的url返回给前端:
@Component
public class AliyunOssService {
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "oss-cn-hangzhou.aliyuncs.com";
// 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录RAM控制台创建RAM账号。
String accessKeyId = "...";
String accessKeySecret = "...";
String bucketName = "gulimall-baobao";
// objectName即文件在Bucket中的路径
public String getTempUrl(String objectName){
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 设置URL过期时间为1小时。
Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
// 生成以GET方法访问的签名URL,访客可以直接通过浏览器访问相关内容。其中objectName即文件在Bucket中的路径
URL url = ossClient.generatePresignedUrl(bucketName, objectName, expiration);
// 关闭OSSClient。
ossClient.shutdown();
return url.toString();
}
}
@SpringBootTest
class AliyunOssDemoApplicationTests {
@Autowired
private AliyunOssService aliyunOssService;
@Test
void contextLoads() {
String tempUrl = aliyunOssService.getTempUrl("guojia.jpg");
System.out.println(tempUrl);
}
}
测试生成的临时url如下,生成的规则是http://bucketName.endPoint/要访问的文件在Bucket中的路径,后面携带超时时间、临时的accessKey、签名等参数。
如果要访问的文件位于Bucket中的某个文件夹下,获取临时url时传入的文件路径参数只需要带上文件夹即可,注意最外层文件夹前面不能带/
参考博客:阿里云对象存储Java-SDK实战