简介

阿里云对象存储OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,可提供99.9999999999%(12个9)的数据持久性,99.995%的数据可用性。多种存储类型供选择,全面优化存储成本。

参考资料

阿里 OSS

简介

  • 阿里云对象存储OSS(Object Storage Service)为您提供基于网络的数据存取服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音视频在内的各类数据文件
  • 创建Bucket,并将文件上传至Bucket。上传完成后,将文件(Object)下载至本地或者通过生成签名URL的方式将文件分享给第三方,供其下载或预览

入口

  • 登录阿里云控制台 【产品】-【存储】-【对象存储 OSS】

OSS 入口

开通 OSS

开通 OSS

价格

  • 当前截图时价格,我主要是做一些头像照片的存储,可以选低频的,一个月估计不会到 1 个 G,8 分钱就能解决

产品定价

前置准备

创建 Bucket

  • 地域:这里建议选择和自己服务器节点近的地域
  • 存储类型:选择低频访问(便宜点,这个根据实际情况,如果访问量比较大的话还是选标准)
  • 同城冗余存储:不开通要收钱,OSS 将您的数据以冗余的方式存储在同一区域(Region)的 3 个可用区(Zone)中。提供机房级容灾能力,能提高您的数据可用性
  • 版本控制:选择不开通,这个是针对需要回滚的才选择开通,需要额外收费(我们这里涉及不到)
  • 读写权限尽量选择公共读写

创建Bucket

上传体验

  • 在刚才的Bucket 下新建一个目录用于存放图片(为资源归类)

上传体验

权限控制

  • Access Key:阿里系下有非常多的产品,所有产品的控制权限通过主账户Access Key进行控制,该账户可以访问所有的产品
  • RAM:RAM可以有多个,可单独为RAM配置拥有的产品控制权限,基于安全考虑我们用 RAM

权限控制

创建用户组

  • 通过 RAM 入口进入,创建用户组,然后给用户组分配权限,最后再创建用户给用户分配用户组

创建用户组

用户组分配权限

  • 给刚才创建的用户组指定可以对阿里云的那些产品进行权限控制,这里就用我们的 OSS 相关内容

用户组分配权限

创建用户

  • 刚才已经准备好了用户组,现在需要准备用一个用户,然后把用户的权限指定为刚才的用户组,这样该用户就拥有了 OSS 相关的权限了
  • 创建后记得下载 CSV 文件保存好账户信息

创建用户

分配用户组

  • 这一步的目的是将用户绑定到之前的用户组里面,这样用户就能使用用户组的权限了(也可直接单独给用户分配权限,这里不做记录了,我个人还是推荐以用户组进行维护)

分配用户组

基础配置

导入依赖

  • 导入的版本根据官方给的文档进行导入,最好不要私自修改任意版本号 我尝试的把 sdk 升级到最新的后面 做 Bucket 空间判断的时候有返回错误(可能是版本于其它地方有不匹配的)
  • Gradle导入:implementation("com.aliyun.oss:aliyun-sdk-oss:3.10.2")
  • 如果使用的是Java 9及以上的版本,则需要添加jaxb相关依赖
1
2
3
implementation("javax.xml.bind:jaxb-api:2.3.1")
implementation("javax.activation:activation:1.1.1")
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")

配置 yml

  • 将我们刚才创建用户的 key 配置进来
1
2
3
4
5
6
7
8
# 阿里 OSS 配置
# 阿里 OSS 配置
alioss:
endpoint: yourEndpoint
accessKeyId: yourAccessKeyId
accessKeySecret: yourAccessKeySecret
bucketName: yourBucketName
url: https://${alioss.bucketName}.${alioss.endpoint}/

创建实体

  • 将配置文件中的内容读取到实体中
  • ConfigurationProperties 用于读取 yml 配置文件 prefix = “alioss” 用于指定前置
  • 实体中的属性名需要和 yml 中保持一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.adalucky.utils.alioss.entity;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* @author ada
* @Version JDK17
* @Description 读取 阿里 OSS 的配置到实体中
* @since 2022/4/9 13:13
*/
@ConfigurationProperties(prefix = "alioss")
@Component
@Data
public class OssEntity {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String url;
}

验证

  • 创建一个单元测试类,验证一下实体对象有没有把 yml 中的值赋值到属性上
  • 有可能单元测试读取不到 yml 配置文件,我是直接再单元测试中新建了一个 application.yml 和 main 下面的内容一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.adalucky.utils.alioss;

import com.adalucky.utils.alioss.entity.OssEntity;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class OssEntityTest {
@Autowired
OssEntity ossEntity;

@Test
void ossEntity() {
System.out.println(ossEntity);
}
}

单元测试验证

AIP 控制

  • 参考阿里官方的 OSS 开发手册,里面有非常详细的示例和说明
  • 接口中定义方法,实现类具体实现
  • 实现类需要加上 @Service 注解
  • 实现类也实现了 InitializingBean,实现 afterPropertiesSet 方法,创建这个 Bean 后对参数进行赋值方便后面统一使用

创建 Bucket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.adalucky.utils.alioss.service.impl;

import com.adalucky.handler.BusinessException;
import com.adalucky.response.Result;
import com.adalucky.response.ResultCode;
import com.adalucky.utils.alioss.entity.OssEntity;
import com.adalucky.utils.alioss.service.OssService;
import com.adalucky.utils.logger.Slf4j;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.CreateBucketRequest;
import com.aliyun.oss.model.StorageClass;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

/**
* @author ada
* @Version JDK17
* @Description
* @since 2022/4/9 15:11
*/
@Service("OssServiceImpl")
public class OssServiceImpl implements OssService, InitializingBean {
@Autowired
OssEntity ossEntity;
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String url;

/**
* 实现 InitializingBean 的方法,在创建初始化 Ali0ssServiceImpl 对象后需要做的事
*
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
endpoint = ossEntity.getEndpoint();
accessKeyId = ossEntity.getAccessKeyId();
accessKeySecret = ossEntity.getAccessKeySecret();
bucketName = ossEntity.getBucketName();
url = ossEntity.getUrl();
}

/**
* 创建 Bucket 存储空间
*
* @return
*/
@Override
public Result createBucket() {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 判断存储空间bucket是否存在。如果返回值为true,则存储空间存在,如果返回值为false,则存储空间不存在。
if (ossClient.doesBucketExist(bucketName)) {
Slf4j.log().error("Bucket【" + bucketName + "】已存在,无法重复创建");
throw new BusinessException(
ResultCode.BUCKET_ALREADY_EXIST.getCode(),
ResultCode.BUCKET_ALREADY_EXIST.getMessage()
);
} else {
//设置为低频后->创建->关闭->返回 ok
CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
createBucketRequest.setStorageClass(StorageClass.IA);
ossClient.createBucket(createBucketRequest);
ossClient.shutdown();
return Result.ok().data("bucketName", bucketName).data("StorageClass",StorageClass.IA);
}
}
}

上传文件

配置

  • 上传文件的时候有大小限制需要调整一下,不然SpringBoot默认的是单个文件 1MB,整体是 10MB
1
2
3
4
5
6
# 上传文件大小限制:单个 50MB 整体 100MB 
spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 100MB

实现类

  • OssService 接口中先定义方法
  • 实现类 url 用于返回给前端
  • url 是在 yml 中变量组合起来的 url: https://${alioss.bucketName}.${alioss.endpoint}/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.adalucky.utils.alioss.service.impl;

import com.adalucky.handler.BusinessException;
import com.adalucky.response.Result;
import com.adalucky.response.ResultCode;
import com.adalucky.utils.alioss.entity.OssEntity;
import com.adalucky.utils.alioss.service.OssService;
import com.adalucky.utils.logger.Slf4j;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.CreateBucketRequest;
import com.aliyun.oss.model.StorageClass;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
* @author ada
* @Version JDK17
* @Description
* @since 2022/4/9 15:11
*/
@Service("OssServiceImpl")
public class OssServiceImpl implements OssService, InitializingBean {
@Autowired
OssEntity ossEntity;
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String url;

/**
* 实现 InitializingBean 的方法,在创建初始化 Ali0ssServiceImpl 对象后需要做的事
*
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
endpoint = ossEntity.getEndpoint();
accessKeyId = ossEntity.getAccessKeyId();
accessKeySecret = ossEntity.getAccessKeySecret();
bucketName = ossEntity.getBucketName();
url = ossEntity.getUrl();
}

/**
* 上传文件
*
* @param file 上传的文件对象
* @return
*/
@Override
public Result upload(MultipartFile file) {
String objectName = setObjectName(file);
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
//上传文件
ossClient.putObject(bucketName, objectName, file.getInputStream());
} catch (IOException e) {
Slf4j.log().error("上传的文件流获取异常");
e.printStackTrace();
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return Result.ok().message("上传成功").data("url", url+objectName);
}

String setObjectName(MultipartFile file) {
String[] split = file.getContentType().split("/");
String directory = split[0];
return directory + "/" + file.getOriginalFilename();
}
}

Controller

  • 前端控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.adalucky.utils.alioss.controller;

import com.adalucky.response.Result;
import com.adalucky.utils.alioss.service.OssService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
* @author ada
* @Version JDK17
* @Description OSS 前端控制器
* @since 2022/4/9 20:46
*/
@RestController
@RequestMapping("/oss")
@Api(tags = "OSS")
public class OssController {
@Autowired
OssService ossService;

@ApiOperation(value = "上传文件", notes = "上传文件到阿里云 OSS 上")
@PostMapping("/upload")
Result upload(MultipartFile file) {
return ossService.upload(file);
}
}

图片无法预览

  • 针对上面上传的图片无法进行预览,每次访问连接都是下载图片,主要有两种方式进行解决:①绑定一个自己的域名 ②上传的时候指定文件的类型 Content-Type
  • 这里为了方便直接绑定了自己的域名(如果没有域名的只能用第二种方式了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	//如果想要实现图片预览的效果,一定要设置以下几点  1.设置文件的ACL(权限)  要么是公共读,要么是公共读写  2.一定要设置文本类型(image/jpg)
ObjectMetadata objectMetadata = new ObjectMetadata();
//设置公共读权限
objectMetadata.setObjectAcl(CannedAccessControlList.PublicRead);
//设置文件类型
objectMetadata.setContentType(getcontentType(fileType));
//put到 Bucket
ossClient.putObject(bucketName, fileName, inputStream, objectMetadata);

/**
* Description: 判断OSS服务文件上传时文件的contentType
*
* @param FilenameExtension 文件后缀
* @return String
*/
public static String getcontentType(String FilenameExtension) {
if (FilenameExtension.equalsIgnoreCase(".bmp")) {
return "image/bmp";
}
if (FilenameExtension.equalsIgnoreCase(".gif")) {
return "image/gif";
}
if (FilenameExtension.equalsIgnoreCase(".jpeg") ||
FilenameExtension.equalsIgnoreCase(".jpg") ||
FilenameExtension.equalsIgnoreCase(".png")) {
return "image/jpg";
}
if (FilenameExtension.equalsIgnoreCase(".html")) {
return "text/html";
}
if (FilenameExtension.equalsIgnoreCase(".txt")) {
return "text/plain";
}
if (FilenameExtension.equalsIgnoreCase(".vsd")) {
return "application/vnd.visio";
}
if (FilenameExtension.equalsIgnoreCase(".pptx") ||
FilenameExtension.equalsIgnoreCase(".ppt")) {
return "application/vnd.ms-powerpoint";
}
if (FilenameExtension.equalsIgnoreCase(".docx") ||
FilenameExtension.equalsIgnoreCase(".doc")) {
return "application/msword";
}
if (FilenameExtension.equalsIgnoreCase(".xml")) {
return "text/xml";
}
return "image/jpg";
}

绑定域名

  • 新增域名并自动添加到 CNAME 映射

新增域名


  • Bucket 开启自定义域名(只需要对其中一个文件开启后全局生效)
  • 开启自定义域名后可以看到就会多了类型 image 这样就可以通过 url 进行预览了,而不是下载

开启自定义域名

SSL 证书

  • 阿里云有免费的证书一年 20 个
  • 先免费购买 -> 创建 -> 申请 -> 域名解析 -> 证书托管

效果


  • 免费购买

购买证书


  • 创建+申请

创建并申请证书


  • 证书域名解析

证书域名解析


  • 证书托管

证书托管

下载文件

  • 我自己其实并没事去实现,因为当前我的项目还没涉及到 OSS 对象需要下载,先做个示例记录下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 下载文件
*
* @param fileName
* @throws IOException
*/
@Override
public void download(String fileName) throws IOException {
// <yourObjectName>从OSS下载文件时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
String objectName = fileName;

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

// 调用ossClient.getObject返回一个OSSObject实例,该实例包含文件内容及文件元信息。
OSSObject ossObject = ossClient.getObject(bucketName, objectName);
// 调用ossObject.getObjectContent获取文件输入流,可读取此输入流获取其内容。
InputStream content = ossObject.getObjectContent();
if (content != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(content));
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
System.out.println("\n" + line);
}
// 数据读取完成后,获取的流必须关闭,否则会造成连接泄漏,导致请求无连接可用,程序无法正常工作。
content.close();
}
// 关闭OSSClient。
ossClient.shutdown();
}

列举文件

  • 我自己其实并没事去实现,因为当前我的项目还没涉及到 OSS 对象需要列举展示,先做个示例记录下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 列举文件
*/
@Override
public void listFile() {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

// ossClient.listObjects返回ObjectListing实例,包含此次listObject请求的返回结果。
ObjectListing objectListing = ossClient.listObjects(bucketName);
// objectListing.getObjectSummaries获取所有文件的描述信息。
for (OSSObjectSummary objectSummary : objectListing.getObjectSummaries()) {
System.out.println(" - " + objectSummary.getKey() + " " +
"(size = " + objectSummary.getSize() + ")");
}

// 关闭OSSClient。
ossClient.shutdown();
}

删除文件

  • 前端控制器
1
2
3
4
5
@ApiOperation(value = "删除文件", notes = "根据文件名删除 Bucket 内的文件")
@DeleteMapping("/delete")
Result delete(@RequestParam String fileName) {
return ossService.deleteFile(fileName);
}
  • 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 删除文件
*
* @param fileName 需要删除的文件
* @return 封装的 Result
*/
@Override
public Result deleteFile(String fileName) {
//文件名为空的话抛出自定义异常
if (fileName.isBlank()) {
throw new BusinessException(
ResultCode.PARAM_IS_BLANK.getCode(),
ResultCode.PARAM_IS_BLANK.getMessage()
);
} else {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 删除文件。
ossClient.deleteObject(bucketName, fileName);
} catch (OSSException e) {
Slf4j.log().error("确认文件名是否存在");
e.printStackTrace();
} finally {
ossClient.shutdown();
}
return Result.ok().message("删除文件成功").data("fileName", fileName);
}
}