目前大家一般不会把二进制文件直接放在应用服务器上,而是存在“对象存储”的方案中,例如亚马逊的 AWS,阿里云的 OSS、Cloudflare R2 等。AWS 为最早的始作俑者,因此其 S3 协议也近乎标准化,各大厂商的对象存储方案都实现该协议。基本上各家都有提供方便的 SDK 可以用快速调用 S3 服务,而其中的原理就难度来说并不是太复杂,笔者一时“手痒”就想打造自己的 S3 客户端。该组件主打的就是一个轻量级,不超过十个类,基本覆盖日常 OSS 的操作。
当前该组件支持 S3 协议中以下的操作:
- 列出桶 listBucket
- 创建、删除桶 createBucket/deleteBucket
- 上传文件、获取文件、删除文件 putObject/getObject/deleteObject
其他比较少用到的操作,暂且不实现了。
当前支持的厂商:阿里云 OSS、网易云 NOS、Cloudflare R2,其中 Cloudflare R2 是基于 SigV4 协议的。
安装
该组件源码在:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-backend/aj-playground/aj-s3client。
Maven 坐标:
<dependency>
<groupId>com.ajaxjs</groupId>
<artifactId>aj-s3client</artifactId>
<version>1.0</version>
</dependency>
使用方法
第一步是设置好 OSS 各项参数,详见配置对象 Config
:
/**
* 配置
*/
@Data
public class Config {
/**
* 访问 API
*/
private String endPoint;
/**
* 访问 Key
*/
private String accessKey;
/**
* 访问密钥
*/
private String secretKey;
/**
* 存储桶名称
*/
private String bucketName;
/**
* 签名中的标识,每个厂商不同
*/
private String remark;
}
这个 POJO 你通过 Spring 注入到各个厂商实现就可以了。如果不用 Spring,最简单的就是这样子的:
OSS 各种调用 API,详见接口S3Client
,一般懂 Java 的朋友都会,就不详细介绍了。
package com.ajaxjs.s3client;
import java.util.Map;
/**
* S3 客户端
*/
public interface S3Client {
/**
* 列出存储桶中的所有对象
*
* @return XML List
*/
String listBucket();
/**
* 列出存储桶中的所有对象
*
* @return XML List Map
*/
Map<String, String> listBucketXml();
/**
* 创建一个存储桶(Bucket)
*
* @param bucketName 存储桶的名称,必须全局唯一
* @return true=操作成功
*/
boolean createBucket(String bucketName);
/**
* 删除一个存储桶(Bucket)
*
* @param bucketName 存储桶的名称
* @return true=操作成功
*/
boolean deleteBucket(String bucketName);
/**
* 将字节数据上传到指定的存储桶中
*
* @param bucketName 存储桶的名称
* @param objectName 对象(文件)在存储桶中的名称
* @param fileBytes 要上传的文件的字节数据
* @return true=操作成功
*/
boolean putObject(String bucketName, String objectName, byte[] fileBytes);
/**
* 将字节数据上传到指定的存储桶中
*
* @param objectName 对象(文件)在存储桶中的名称
* @param fileBytes 要上传的文件的字节数据
* @return true=操作成功
*/
boolean putObject(String objectName, byte[] fileBytes);
/**
* 将字节数据上传到指定的存储桶中
*
* @param bucketName 存储桶的名称
* @param objectName 对象(文件)在存储桶中的名称
* @return true=操作成功
*/
boolean getObject(String bucketName, String objectName);
/**
* 将字节数据上传到指定的存储桶中
*
* @param objectName 对象(文件)在存储桶中的名称
* @return true=操作成功
*/
boolean getObject(String objectName);
/**
* 删除指定的文件
*
* @param bucketName 存储桶的名称
* @param objectName 要删除的文件名称
* @return true=操作成功
*/
boolean deleteObject(String bucketName, String objectName);
/**
* 删除指定的文件
*
* @param objectName 要删除的文件名称
* @return true=操作成功
*/
boolean deleteObject(String objectName);
}
单元测试的配置文件application.yml
没有随着 VCS 提交,其内容如下:
S3Storage:
Nso:
accessKey: xx
accessSecret: xx
api: nos-eastchina1.126.net
bucket: xx
Oss:
accessKeyId: xx
secretAccessKey: xx
endpoint: oss-cn-beijing.aliyuncs.com
bucket: xx
LocalStorage: # 本地保存
absoluteSavePath: c:\temp\ # 若有此值,保存这个绝对路径上
开发感受
这类客户端的实现过程中,感受如下几点:
- 仍离不开 HTTP 通讯。所谓 OSS,本质围绕着文件服务进行:文件上传、文件删除、文件下载,比较特殊一点的就是“存储桶 Bucket”的概念,这一切与服务器之间沟通仍然通过 HTTP API 进行,故所以这客户端要封装好 HttpClient 这样子组件并进行 API 通讯的调用。Cloudflare R2 官方提供 Postman 直接示例,对于我再现 Java HTTP 请求很方便
- 既然是协议的通讯,如何身份认证是个问题。S3 协议认证方式是每次的 HTTP 请求头加入
Authorization
字段,如何生成这个合法的签名值是相对复杂的过程,包括各种参数设定和加密,占了代码很大的篇幅。S3 签名有两个版本 SigV2、SigV4,本组件都支持。 - 虽然同为 S3 协议,但各厂商实现的产品均有不同程度上的出入,于是在代码设计上采用子类继承了来实现这种关系:
BaseS3Client
–>BaseS3ClientSigV2/BaseS3ClientSigV4
–>各个厂商的实现
- 实现过程感觉最困难的是签名生成,稍有不慎,生成的签名不合法则出现校验不通过的情况。SigV2 签名的生成方式还是比较简单的,类似阿里云文档里面的介绍:但是 SigV4 则复杂得多,于是我找了一个很好开源实现 aws-v4-signer-java,零依赖,我简化了不少代码并将其重构了。另外,阿里云的文档也介绍得很清楚,详见这里和源码。
- 单元测试依然很重要,尤其写组件的时候,或者重构的时候,很多时候要把问题复现出来,单测必不可少,也能提高开发的效率。