文件上传是非常常见的功能,本期要实现的功能是将文件存储到阿里云分布式对象存储OSS中,这样做的好处是随便哪里都可以方便的展示出该图片,并且图片以链接形式在客户端浏览器渲染,流量不会经过后台,降低后台压力,但若对文件有限制性的操作,还是要通过后台返回文件
- 配置阿里云OSS密钥
- 编写OSS上传的简单类
- Springboot集成OSS
- 后端开发上传接口
- 前端开发Vue+Iview+Uploader组件
- 前端渲染上传后的图片
如果没有云存储,可访问阿里云官网,搜索对象存储或者访问下方链接,按需购买
https://common-buy.aliyun.com/?spm=5176.7933691.J_5253785160.2.64e62c47OY978E&commodityCode=oss_rc_dp_cn
对象存储是以桶来进行文件管理,文件都存在一个个的桶里,每个桶可以存放目录和文件,即可以按照文件进行分类存储,可以创建多个桶。
从下图可以看到,一个桶里面可以存储多个文件夹,文件夹里可以存多个文件,桶里面也可以直接存文件
要想将文件上传到OSS中,用的是AK/SK的方式进行认证授权,所以需要先在阿里云OSS中配置安全密钥,
点击头像打开访问控制页面
创建用户
创建访问AccessKey
需要注意的是,创建的时候AppSecret及时复制出来保存,关闭弹窗后,无法再从阿里云页面找到,若不慎丢失,需重新生成
至此,云存储已经具备了,访问的AK/SK也有了,接下来编写客户端上传下载的逻辑。
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
public class OssConfig {
private String accessKeyId;
private String secretAccessKey;
private String endpoint;
private String bucketName;
}
private final DefaultCredentialProvider credentialsProvider;
private final OssConfig ossConfig;
@SneakyThrows
public OssHelper(@Autowired OssConfig ossConfig) {
// 从环境变量中获取访问凭证。运行本代码示例之前,请先配置环境变量。
if (ossConfig == null) {
throw new ServerException("初始化对象存储工具失败,配置信息未读取到");
}
this.ossConfig = ossConfig;
this.credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider(ossConfig.getAccessKeyId(), ossConfig.getSecretAccessKey());
}
OSS支持上传的类型非常多,这里例举几类
public Optional<String> upload(File file, String ossDir, String ossFileName) {
try {
return this.uploadToOss(file, ossDir, ossFileName);
} catch (Exception exception) {
log.error("上传字符串前异常:{}", exception.getMessage());
}
return Optional.empty();
}
public Optional<String> upload(String content, String ossDir, String ossFileName) {
try {
InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
return this.uploadToOss(inputStream, ossDir, ossFileName);
} catch (Exception exception) {
log.error("上传字符串前异常:{}", exception.getMessage());
}
return Optional.empty();
}
public Optional<String> upload(byte[] content, String ossDir, String ossFileName) {
try {
InputStream inputStream = new ByteArrayInputStream(content);
return this.uploadToOss(inputStream, ossDir, ossFileName);
} catch (Exception exception) {
log.error("上传字符串前异常:{}", exception.getMessage());
}
return Optional.empty();
}
public Optional<String> upload(URL url, String ossDir, String ossFileName) {
try {
if (url == null) {
return Optional.empty();
}
InputStream inputStream = url.openStream();
return this.uploadToOss(inputStream, ossDir, ossFileName);
} catch (Exception exception) {
log.error("上传字符串前异常:{}", exception.getMessage());
}
return Optional.empty();
}
private Optional<String> uploadToOss(Object object, String ossDir, String ossFileName) {
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
OSS ossClient = new OSSClientBuilder().build(ossConfig.getEndpoint(), credentialsProvider);
try {
String objectName = getObjectName(ossDir, ossFileName);
PutObjectRequest putObjectRequest = null;
if (object instanceof InputStream) {
putObjectRequest = new PutObjectRequest(ossConfig.getBucketName(), objectName, (InputStream) object);
}
if (object instanceof File) {
putObjectRequest = new PutObjectRequest(ossConfig.getBucketName(), objectName, (InputStream) object);
}
if (putObjectRequest == null) {
return Optional.empty();
}
PutObjectResult result = ossClient.putObject(putObjectRequest);
// 设置签名URL过期时间,单位为毫秒。本示例以设置过期时间为50年为例。
Date expiration = new Date(new Date().getTime() + 50 * 365 * 24 * 3600 * 1000L);
// 生成以GET方法访问的签名URL,访客可以直接通过浏览器访问相关内容。
URL url = ossClient.generatePresignedUrl(ossConfig.getBucketName(), objectName, expiration);
if (url != null) {
return Optional.of(url.toString());
}
return Optional.empty();
} catch (OSSException oe) {
log.error("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
log.error("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.");
log.error("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return Optional.empty();
}
从下图可以看到,成功把字符串hello world
上传到了OSS的certificate目录下,文件名为test.txt
查看OSS桶的对应文件内容,与上传一致
public void download(String ossDir, String ossFileName) {
String objectName = getObjectName(ossDir, ossFileName);
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(ossConfig.getEndpoint(), credentialsProvider);
try {
// 调用ossClient.getObject返回一个OSSObject实例,该实例包含文件内容及文件元信息。
OSSObject ossObject = ossClient.getObject(ossConfig.getBucketName(), 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();
}
} 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();
}
}
}
从OSS中将前面上传的test.txt下载下来打印出内容,与上传时的一致
到了这一步,上传、下载等功能已经具备了,只需要将其与Springboot集成起来,即可在各个业务中使用,为了使用单例模式,这里将其注册为Spring Bean
@Data
@Configuration
@ConfigurationProperties(prefix = "aliyun.oss")
public class OssConfig {
private String accessKeyId;
private String secretAccessKey;
private String endpoint;
private String bucketName;
}
@Slf4j
@Component
public class OssHelper {
private final DefaultCredentialProvider credentialsProvider;
private final OssConfig ossConfig;
@SneakyThrows
public OssHelper(@Autowired OssConfig ossConfig) {
// 从环境变量中获取访问凭证。运行本代码示例之前,请先配置环境变量。
if (ossConfig == null) {
throw new ServerException("初始化对象存储工具失败,配置信息未读取到");
}
this.ossConfig = ossConfig;
this.credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider(ossConfig.getAccessKeyId(), ossConfig.getSecretAccessKey());
}
public Optional<String> upload(File file, String ossDir, String ossFileName) {
.........
}
public Optional<String> upload(String content, String ossDir, String ossFileName) {
...........
}
public Optional<String> upload(byte[] content, String ossDir, String ossFileName) {
............
}
public Optional<String> upload(URL url, String ossDir, String ossFileName) {
............
}
private Optional<String> uploadToOss(Object object, String ossDir, String ossFileName) {
............
}
public void download(String ossDir, String ossFileName) {
............
}
}
这里是直接获取到MultipartFile的字节数组,将其上传到OSStest/cover
目录下,文件名是通过雪花算法生成的id
至此,后端已经具备文件上传的完整功能,接下来编写前端的上传功能
该组件可以支持拖拽和选择上传,功能方便,实现简单
<FormItem label="封面" prop="coverId">
<Upload
ref="upload"
:show-upload-list="false"
:format="['zip','png','jpg','jpeg','pdf','doc','docx','ppt','pptx','xls','xlsx']"
:max-size="5120"
:on-success="handleUploadSuccess"
:on-error="handleUploadFailed"
:on-format-error="handleFormatError"
:on-exceeded-size="handleMaxSize"
:before-upload="handleBeforeUpload"
style="width: 210px;height:50px;"
type="drag"
:action="albumCoverAction"
:headers="uploadHeader">
<div>
<Icon type="ios-cloud-upload" size="30" style="color: #3399ff"></Icon>
<p>点击上传或将图片拖拽到这里</p>
</div>
</Upload>
<RadioGroup v-model="formData.coverId">
<Row style="margin-top: 40px;margin-left: 0px;">
<Col span="4">
<div>
<Card dis-hover style="height:auto;width:210px;">
<p slot="title">自定义封面</p>
<Row>
<Col span="24">
<Radio :label="customCover.id">
<div :class="selectedCover===customCover.id?'album-cover-selected':'album-cover'"
@click="changeCover(customCover)"
style="margin-right: 10px;">
<img :src="customCover.coverUrl"/>
</div>
</Radio>
</Col>
</Row>
</Card>
</div>
</Col>
<Col span="19" offset="1">
<Card dis-hover style="height:auto">
<p slot="title">系统默认封面</p>
<Row>
<Col span="24">
<Radio v-for="(item, index) in $store.state.defaultAlbumCovers" class="cover-default"
:key="index"
:label="item.id">
<div :class="selectedCover===item.id?'album-cover-selected':'album-cover'"
@click="changeCover(item)"
style="margin-right: 5px;">
<img :src="item.coverUrl"/>
</div>
</Radio>
</Col>
</Row>
</Card>
</Col>
</Row>
</RadioGroup>
</FormItem>
上面的上传组件中的文件往哪里上传,以及怎么通过后端的用户登录认证,是通过如下2个值来决定的