为了第三方快速对接,我们设计了这套基于Java的SDK方案

一、需求分析

我们提供了标准的 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~

github.com/HammCn/AirP...

以上就是今天的分享,Bye.

相关推荐
云初@4 分钟前
IDEA实现javaweb用户登录(增删改查)
java·ide·intellij-idea
代码小鑫8 分钟前
【开源】A059-基于SpringBoot的社区养老服务系统的设计与实现
java·开发语言·数据库·spring boot·后端
机器之心20 分钟前
质量超越o1,成本仅4%,UCSD张怡颖教授团队开源生成式AI工作流自动优化器
人工智能·后端
paopaokaka_luck34 分钟前
[384]基于springboot的药品管理系统
java·spring boot·后端
老马啸西风1 小时前
Neo4j APOC-01-图数据库 apoc 插件介绍
java
次次皮1 小时前
【方案三】JAVA中使用ocr(Umi-OCR)
java·ocr
九转成圣1 小时前
详解日志格式配置:XML 与 Spring Boot 配置文件格式
xml·spring boot·后端
疯一样的码农1 小时前
如何使用Apache HttpClient发送带有HTML表单数据的POST请求
java·apache·httpclient
Allen Bright1 小时前
使用 Apache Commons IO 实现文件读写
java·开发语言·apache