一、需求分析
我们提供了标准的 OpenAPI
,实现了第三方 创建应用 、重置密钥 、数据加解密 、数据签名 等功能,但客户需要自行按照我们基于 HTTP 标准的 OpenAPI 文档来进行对接,略显麻烦。
于是我们考虑出一套 Java SDK,让客户可以快速对接,减少对接成本。
本文为了篇幅,部分代码的细节省略了,部分注释代码也比较随意,不太满足 JavaDoc 规范。
如果感兴趣可以查看我们开源项目内的所有源代码: github.com/HammCn/AirP...
二、SDK设计
首先,为了客户的兼容问题,我们使用 Java8 作为支持语言版本。
2.1 加解密部分
因为后端使用了可配置的 加密选项,所以我们先提供一个加密选项的枚举:
2.1.1 加密选项
java
public enum AirArithmetic {
AES,
RSA,
NO
}
2.1.2 加解密工具类
同时,我们需要提供对应的加解密Util类:
AES
java
public class AirAes {
// 一些 Getter 和 Setter 以及属性
public final String encrypt(String source) {
//...
}
public final String decrypt(String content) {
//...
}
public static AirAes create() {
return new AirAes();
}
}
RSA
java
public class AirRsa {
// 一些 Getter 和 Setter 以及属性
public final String encrypt(String sourceContent) {
// 公钥加密
}
public final String decrypt(String encryptedContent) {
// 公钥解密
}
}
RSA
加解密模式下,私钥存在于我方服务器,客户侧只需要存储公钥即可,所以上面省略了私钥加解密的部分代码。
2.2 参数配置
我们提供了 AirClient
类,用于实现客户端的创建,同时,我们提供了 AirConfig
类,用于配置 AirClient
的参数。
2.2.1 客户端配置
java
public class AirConfig {
// 一些 Getter 和 Setter
private String gateway = AirConstant.GATEWAY_PRODUCTION;
private String appKey;
private String appSecret;
private AirArithmetic arithmetic = AirArithmetic.AES;
private String publicKey;
public static AirConfig create() {
return new AirConfig();
}
}
这些配置都是从我们后端应用创建之后获取的:
2.2.2 客户端
我们需要一个客户端来 调用加解密 、调用签名 、发起并解析响应数据 等。
java
public class AirClient {
private AirClient() {
}
private AirConfig config;
public final <REQ extends AbstractRequest<RES>, RES extends AbstractResponse<RES>> RES request(REQ request) {
return decrypt(sendRequest(request), request.getResponseClass());
}
public final <RES extends AbstractResponse<RES>> RES decrypt(String content, Class<RES> targetClass) {
content = decrypt(content);
if (Objects.isNull(content)) {
return null;
}
try {
RES res = targetClass.newInstance();
return res.parseData(content);
} catch (InstantiationException | IllegalAccessException exception) {
AirDebug.show("创建对象失败", exception.getMessage());
throw new AirException(exception.getMessage());
}
}
public final String decrypt(String content) {
if (Objects.isNull(content)) {
return null;
}
switch (config.getArithmetic()) {
case RSA:
content = AirRsa.create().setPublicKey(config.getAppSecret()).decrypt(content);
break;
case AES:
content = AirAes.create().setKey(config.getAppSecret()).decrypt(content);
break;
default:
}
return content;
}
public final <REQ extends AbstractRequest<RES>, RES extends AbstractResponse<RES>> String encrypt(REQ request) {
if (Objects.isNull(request)) {
return null;
}
return encrypt(AirJson.toString(request));
}
public final String encrypt(String content) {
if (Objects.isNull(content)) {
return null;
}
switch (config.getArithmetic()) {
case RSA:
content = AirRsa.create().setPublicKey(config.getPublicKey()).encrypt(content);
break;
case AES:
content = AirAes.create().setKey(config.getAppSecret()).encrypt(content);
break;
default:
}
return content;
}
public static AirClient create(AirConfig config) {
if (Objects.isNull(config)) {
throw new IllegalArgumentException("无效的AirConfig配置");
}
AirClient client = new AirClient();
client.config = config;
return client;
}
private <RES extends AbstractResponse<RES>, REQ extends AbstractRequest<RES>> String sendRequest(REQ request) {
AirRequest airRequest = new AirRequest()
.setAppKey(config.getAppKey())
.setContent(encrypt(request));
// 使用密钥将请求体签名
airRequest.sign(config.getAppSecret());
final String body = AirJson.toString(airRequest);
final String url = config.getGateway() + request.getApiUrl();
String response = AirHttp.post(url, body);
AirJson<?> airJson = AirJson.parse(response, AirJson.class);
if (AirErrorCode.SUCCESS.getCode() != airJson.getCode()) {
throw new AirException(airJson.getCode(), airJson.getMessage());
}
return airJson.getData();
}
}
有了上面的客户端之后,我们就可以通过 AirConfig
参数实例来初始化一个 AirClient
的客户端,然后就可以发起请求了,但在此之前,我们还需要把请求和响应的基类设计一下:
2.3 请求和响应基类
请求基类包括了一些公共的请求参数、网关、加解密方式的参数信息;
业务抽象请求类是整个业务请求类的抽象类,其中包含了请求的 ApiUrl
而响应我们提供了一个抽象类,具体业务的响应类可以自行实现数据的解析方法。
2.3.1 请求基类
java
public class AirRequest {
// AppKey
private String appKey;
// 版本
private int version = 10000;
// 时间戳
private long timestamp = System.currentTimeMillis();
// 加密后的内容
private String content;
// Nonce 防重放
private String nonce = AirRandom.randomString();
// 签名字符串
private String signature;
/**
* <h2>签名</h2>
*
* @param appSecret AppSecret
*/
public final void sign(String appSecret) {
String[] strings = new String[]{appSecret, getAppKey(), String.valueOf(getVersion()), String.valueOf(getTimestamp()), getNonce(), getContent()};
final String source = String.join("", strings);
this.signature = DigestUtils.sha1Hex(source);
}
}
上面的请求基类中包含了 AppKey
Version
Timestamp
Nonce
Signature
等公共参数,其中 Signature
是通过 AppSecret
等参数进行签名,以实现请求的合法性校验。
2.3.2 抽象业务请求类
抽象业务请求类是所有业务请求类的基类,其中包含了一个 getApiUrl()
抽象方法,该方法返回的是请求的 API
地址,例如:/open/user/login
java
public abstract class AbstractRequest<R extends AbstractResponse<R>> {
/**
* <h2>API地址</h2>
*
* @return API地址
*/
protected abstract String getApiUrl();
/**
* <h2>获取响应类</h2>
*
* @return 类
*/
Class<R> getResponseClass() {
//noinspection unchecked
return (Class<R>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
}
2.3.3 抽象业务响应类
抽象业务响应类是所有业务响应类的基类,其中包含了一个 parseData(String content)
抽象方法,该方法返回的是响应数据的解析结果,例如:
java
public abstract class AbstractResponse<R extends AbstractResponse<R>> {
/**
* <h2>解析数据</h2>
*
* @param data 解密后的data数据
* @return 解析后的数据
*/
public abstract R parseData(String data);
}
2.4 编写个示例业务
有了上述的一些支持,接下来我们就可以具体的给 OpenApi
做业务的 SDK 封装了,例如我们接下来写一个 获取用户列表 、修改用户信息 的示例。
2.4.1 获取用户列表
我们只需要定义获取用户列表的请求和响应类即可完成 SDK 的包装:
2.4.1.1 请求类
我们支持了一个 nickname
的模糊搜索。
java
public class UserListRequest extends AbstractRequest<UserListResponse> {
@Override
protected String getApiUrl() {
return "user/getList";
}
private String nickname;
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
}
2.4.1.2 响应类
java
public class UserListResponse extends AbstractResponse<UserListResponse> {
private List<User> list;
public List<User> getList() {
return list;
}
public UserListResponse setList(List<User> list) {
this.list = list;
return this;
}
@Override
public UserListResponse parseData(String data) {
return this.setList(AirJson.parseList(data, User[].class));
}
public static class User {
private Long id;
private String nickname;
private Integer age;
}
}
好的,请求和响应类都写好了,那么我们接下来写个调用方的测试代码:
2.4.1.3 调用方测试代码
java
AirConfig config = AirConfig.create()
.setAppKey("")
.setAppSecret("")
.setPublicKey("")
.setArithmetic(AirArithmetic.AES)
.setGateway(AirConstant.GATEWAY_LOCAL);
AirClient client = AirClient.create(config);
UserListRequest userListRequest = new UserListRequest();
userListRequest.setNickname("Hamm");
UserListResponse response = client.request(userListRequest);
可以看到,客户在调用SDK的时候就比较方便了,只需要通过 AirConfig
来初始化一个 AirClient
之后,然后通过这个客户端示例直接发起一个实例化后的业务请求示例即可。
2.4.3 修改用户信息示例
我们再来写一个返回的值不是数组,而是对象的封装示例吧:
2.4.3.1 请求类
java
public class ModifyUserInfoRequest extends AbstractRequest<ModifyUserInfoResponse> {
@Override
protected String getApiUrl() {
return "open/user/update";
}
private Long id;
private String name;
private Integer age;
}
2.4.3.2 响应类
java
public class ModifyUserInfoResponse extends AbstractResponse<ModifyUserInfoResponse> {
private ModifyUserInfoResponse.User data;
public ModifyUserInfoResponse.User getData() {
return data;
}
public ModifyUserInfoResponse setData(ModifyUserInfoResponse.User data) {
this.data = data;
return this;
}
@Override
public ModifyUserInfoResponse parseData(String data) {
return this.setData(AirJson.parse(data, ModifyUserInfoResponse.User.class));
}
public static class User {
private Long id;
private String nickname;
private Integer age;
}
}
很简单,我们每增加一个 API ,都只需要提供一个请求类和一个响应类即可。然后我们将请求和响应的属性封装好 get/set
方法即可。
2.4.3.4 调用示例
客户侧需要调用修改用户信息时就贼简单了:
java
AirConfig config = AirConfig.create()
.setAppKey("")
.setAppSecret("")
.setPublicKey("")
.setArithmetic(AirArithmetic.AES)
.setGateway(AirConstant.GATEWAY_LOCAL);
AirClient client = AirClient.create(config);
ModifyUserInfoRequest modifyUserInfoRequest = new ModifyUserInfoRequest();
modifyUserInfoRequest.setId(1L);
modifyUserInfoRequest.setName("Hamm");
modifyUserInfoRequest.setAge(18);
ModifyUserInfoResponse response = client.request(modifyUserInfoRequest);
当然,你也可以给 set
方法使用链式调用返回,方便客户侧链式调用:
java
ModifyUserInfoRequest modifyUserInfoRequest = new ModifyUserInfoRequest()
.setId(1L)
.setName("Hamm")
.setAge(18);
2.5 封装原则
封装 SDK 的目的,是方便该语言下的客户侧对接尽可能的方便,无需关心细节,只需要传入对应的业务参数即可。
所有的封装原则,都是以调用方便为准,且如果有异常,需要明确的将异常抛出给调用方。
三、总结
这次我们分享了我们在日常工作中封装 SDK 的经验和心得,数据结构都是以我们之前业务系统的,当然你可能会根据你自己的业务系统数据结构进行一些调整。
本文所有的源代码都开源在Github: 欢迎查阅并Star~
以上就是今天的分享,Bye.