OSS即“OpenStorageService”,概念上没啥新意,就是本地存储搬到阿里云平台上了,单个存储对象大小可以达到5G,看了下阿里的OSS教程java版本,
使用原生js和servlet实现,这基本就能看不能用,为了做点通用的,我们来玩一把OSS,抓下时髦的尾巴。
准备:
Idea2019.03/Gradle6.0.1/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/Dubbo2.7.5/Mysql8.0.11/Vue2.5/OSS
难度: 新手--战士--老兵--大师
目标:
1.Vue前端+Java后端结合实现OSS图片对象存储
步骤:
为了遇见各种问题,同时保持时效性,我尽量使用最新的软件版本。代码地址:https://github.com/xiexiaobiao/vehicle-shop-admin
1 OSS简介
- 略
阿里云官网资料太多,请君移步,我就不搬运了。
2 分析原理
很多时候,我们上传文件都是前端把待传数据先给后端应用服务器,然后由后端Server再分类存储到比如Mysql,Mongo等等,由于现代的前端功能已十分强大,
当然可以直接由前端上传文件直接到OSS,后端Server只需要告诉上传到哪里以及如何上传即可,这样可以降低后端负载, 流程如下:
- 用户每次需要上传时,web端先向应用服务器请求上传Policy(就是OSS存储地址和目录、账号密码信息、签名、过期信息等)和 回调(上传过程和结果信息反馈给需要的服务器,做展示/记录等用,户上传完文件后,不会直接得到返回结果,而是先通知应用服务器,再把结果转达给用户)。
- 应用服务器返回给web端上传Policy和回调设置。
- web端直接向OSS发送文件上传请求,并上传文件。
- OSS根据用户的回调设置,发送回调请求给应用服务器。
- 应用服务器返回响应给OSS。
- OSS将应用服务器返回的内容返回给web端。
这里有个模式问题,其实如果把OSS相关的Policy信息直接写在前端代码里,然后直接上传文件,也可以的,只是安全性会让人心慌,算是方案一,
如果仍然由后端提供Policy而不要回调信息,直接返回信息给web端就是方案二,上面的方式就是方案三了,我这里以方案三说事。
3 看前端代码
使用elementUI的upload组件,具体可以见elementUI的官方组件介绍,了解各属性含义:src/components/Upload/singleUpload.vue:
<el-upload ref="upload" :action="useOss?ossUploadUrl:minioUploadUrl" :limit="1" :data="useOss?dataObj:null" list-type="picture" :multiple="false" :file-list="fileList" :auto-upload= "autoUpload" :show-file-list="true" :before-upload="beforeUpload" :on-remove="handleRemove" :on-success="handleUploadSuccess" :on-preview="handlePreview"> <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <!--以下为如果手动触发上传的按钮--> <!-- <el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>--> <div slot="tip" class="el-upload__tip">只能上传一张jpg/png文件,且不超过10MB</div> </el-upload>
上面代码的核心要点:
- 并通过:auto-upload设置为自动提交,即只要选择好文件后就自动提交,当然也可以先选好文件,再通过按钮触发提交,见我注释掉的部分。
- :before-upload="beforeUpload" 即真实开始上传文件前做的工作:
beforeUpload(file) { let _self = this; if(!this.useOss){ //不使用oss不需要获取策略 returntrue; } returnnewPromise((resolve, reject) => { policy().then(response => { // 返回的对象,多一层data封装,故写为response.data.data _self.dataObj.policy = response.data.data.policy; _self.dataObj.signature = response.data.data.signature; _self.dataObj.ossaccessKeyId = response.data.data.accessKeyId; _self.dataObj.key = response.data.data.dir + '/'+this.fileNameUUID +'${filename}'; _self.dataObj.dir = response.data.data.dir; _self.dataObj.host = response.data.data.host; // _self.dataObj.callback = response.data.data.callback; resolve(true) }).catch(err => { console.log(err) reject(false) }) }) },
以上通过axios向后端发异步请求,policy()方法我放在其他文件中了,就是通过axios的GET访问去后端获取Policy,然后再赋值到当前的存储变量上,
这样就取得了Policy信息,注意_self.dataObj.key这里的 '/' 不能放 '${filename}' 前,否则oss存储时会自动增加一层目录目录!!只要以上正确,基本就能自动完成上传了。
4 看后端代码
OSS专用的依赖:
compile group: 'com.aliyun.oss', name: 'aliyun-sdk-oss', version: '3.8.1' compile group: 'com.aliyun', name: 'aliyun-java-sdk-core', version: '4.5.0' compile group: 'com.aliyun', name: 'aliyun-java-sdk-sts', version: '3.0.1'
com.biao.shop.stock.impl.AliyunOssServiceImpl
, 即上面的前端请求Policy的处理对象,Controller我就不展示了,就是响应前端REST,然后调用这个服务:
@Service publicclass AliyunOssServiceImpl implements AliyunOssService { privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(AliyunOssServiceImpl.class); @Value("${aliyun.oss.policy.expire}") privateint ALIYUN_OSS_EXPIRE; @Value("${aliyun.oss.maxSize}") privateint ALIYUN_OSS_MAX_SIZE; @Value("${aliyun.oss.callback}") private String ALIYUN_OSS_CALLBACK; @Value("${aliyun.oss.bucketName}") private String ALIYUN_OSS_BUCKET_NAME; @Value("${aliyun.oss.endpoint}") private String ALIYUN_OSS_ENDPOINT; @Value("${aliyun.oss.dir.prefix}") private String ALIYUN_OSS_DIR_PREFIX; private OSSClient ossClient; @Autowired public AliyunOssServiceImpl(OSSClient ossClient) { this.ossClient = ossClient; } @Override public ObjectResponse<AliyunPolicy> policy() { ObjectResponse<AliyunPolicy> result = new ObjectResponse<>(); // 存储目录,按天区分 DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd"); String dir = ALIYUN_OSS_DIR_PREFIX + LocalDateTime.now().toLocalDate().format(dtf); // 签名有效期 long expireEndTime = System.currentTimeMillis() + ALIYUN_OSS_EXPIRE * 1000; Date expiration = new Date(expireEndTime); // 文件大小 long maxSize = ALIYUN_OSS_MAX_SIZE * 1024 * 1024; // 回调 AliyunCallbackParam callback = new AliyunCallbackParam(); callback.setCallbackUrl(ALIYUN_OSS_CALLBACK); callback.setCallbackBody("filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}"); callback.setCallbackBodyType("application/x-www-form-urlencoded"); // 提交节点 String action = "http://" + ALIYUN_OSS_BUCKET_NAME + "." + ALIYUN_OSS_ENDPOINT; try { PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, maxSize); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8); String policy = BinaryUtil.toBase64String(binaryData); String signature = ossClient.calculatePostSignature(postPolicy); // 对"callback"属性进行base64编码编码 String callbackData = BinaryUtil.toBase64String( JacksonUtil.convertToJson(callback).getBytes(StandardCharsets.UTF_8)); // 返回结果 AliyunPolicy aliyunPolicy = new AliyunPolicy(); aliyunPolicy.setAccessKeyId(ossClient.getCredentialsProvider().getCredentials().getAccessKeyId()); aliyunPolicy.setPolicy(policy); aliyunPolicy.setSignature(signature); aliyunPolicy.setExpire(String.valueOf(expireEndTime)); aliyunPolicy.setDir(dir); aliyunPolicy.setCallback(callbackData); aliyunPolicy.setHost(action); result.setCode(RespStatusEnum.SUCCESS.getCode()); result.setMessage(RespStatusEnum.SUCCESS.getMessage()); result.setData(aliyunPolicy); } catch (Exception e) { LOGGER.error("signature failed !!", e); } return result; } }
以上我做了个响应的对象封装,ObjectResponse{code,message,data}
,关于OSS的账号密码信息,我都放在配置文件统一管理;
注意OSSClient要通过Configuration类中生成一下。可以看到以上设置了一系列的OSS存储政策(Policy):存储目录,按天区分;签名有效期;文件大小限制;
回调地址;提交上传的OSS地址等等。
5 再看前端代码:
src/components/Upload/singleUpload.vue:
这是处理每次上传文件完成之后的逻辑,如果设置为可以上传多个文件,那么这个会执行多次。因为OSS存储,如果同目录下文件同名就直接覆盖,
它不会询问大家意见的!所以这里有个小技巧:我通过getFileNameUUID自动生成个随机前缀,绑定到一个本地变量,每次上传成功,都刷新一下,
再混入url和key值,perfect!!
handleUploadSuccess(res, file) { this.showFileList = true; this.fileList.pop(); let url = this.dataObj.host + '/' + this.dataObj.dir + '/' + this.fileNameUUID+ file.name; if(!this.useOss){ //不使用oss直接获取图片路径 url = res.data.url; } this.fileList.push({name: this.fileNameUUID + file.name, url: url}); // 每次上传完成,更新fileNameUUID this.getFileNameUUID(); this.emitInput(this.fileList[0].url); }, /*随机生成文件前缀,防止同名覆盖*/ getFileNameUUID(){ this.fileNameUUID = (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(0,4); },
效果展示,上传中,可以看到进度:
完成后,可以直接删除,或者查看大图,如果做的完美一点,点删除,要触发OSS删除事件,我代码省略没写啦。
当然,还有些其他组件,如其他一些封装类Bean,我就不细说啦,直接看我代码即可!
@Data publicclass AliyunPolicy { private String AccessKeyId; private String host; private String policy; private String signature; private String expire; private String callback; private String dir; }
后记:
1,我买了一套阿里的云产品:ECS 弹性计算服务器,RDS云关系DB,OSS对象存储,把我自己开发的一套前后端都部署上了,
体验就是真羡慕有钱人,起步价的机器比如2G内存,上5个微服务基本就跑满了!在想尽了各种省内存办法又没辙的情形下,我
只能又拉了一台ECS,才架起了网关,注册中心,内存消耗记录:Nacos 0.4G,zookeeper 0.1G,微服务模块5个:authority 200M,
customer 200M,stock 200M,order 200M,business 200M,其他Nginx 100M,souladmin 0.2G,soulbootstrap 0.2G,另外就是
传说Jetty比Tomcat省内存,反正我没看出来,估计是服务数量级不够。
2,如果公司money多,还是推荐上这些云服务的,至少不会上演最近的微盟删库但跑路不成功的事件,云服务自动有各种容灾备份
机制,如异地备,定期备,不亦乐乎?
3,关于callback没细说,具体看代码吧,代码是最最好的老师。
全文完!
推荐阅读:
- 1 Dubbo学习系列之十七(微服务Soul网关)
- 2 Docker部署RocketMQ
- 3 流式计算(五)-Flink 计算模型
- 4 流式计算(四)-Flink Stream API 篇二
- 5 流式计算(三)-Flink Stream 篇一