开放平台 SDK 设计思路

大家好,我是霸戈!上次写完 “淘宝” 开放平台接口设计思路后,有不少粉丝就提到什么时候会出下一篇,所以今天打算写一篇开放平台SDK设计的一些思路,同时我也会做一些实践,一步步把我的设计思路进行落地。

在写这篇文章之前,我个人因为工作的原因使用过淘宝、京东开放平台的一些SDk,也学到不少设计思路,这次准备将我的思路落地成可以使用的SDK,大概的设计会分为三个模块,分别为:

  • 数据传输模块:主要用于传输请求数据,本文使用http协议传输数据
  • 序列化模块:用户序列化和反序列化数据,SDK是给客户使用应该尽可能的去适配客户的意愿,如客户首先的序列化方式为JSON那,作为使用方肯定是会希望SDK能支持JSON序列化和反序列化的
  • 应用模块:主要是协调数据传输、序列化之间的工作,同时对数据做一些校验签名操作

基本功能

数据传输

数据传输协议使用较为通用的http协议,一开始是想是否做一个通用的设计来支持其他的传输方式,如支持socket保持长链接,后来想想没必要过度设计,如果要做到支持不同的传输协议,那么就得做很大一部分适配的编码,实事上可能支持http后以后都会保持不变,没有这个必要!

目前有很多开源的http工具,如httpclientokhttp还有URLConnection的方式等,选择哪一种呢? 好的方法是自己抽象出来一个接口,然后提供一个默认的实现类就好,这样就能保持一定的灵活性。

/**
 * http协议接口
 */
public interface HttpClient {

    /**
     * 执行http post请求
     * @param url
     * @param data
     * @param headers
     * @return
     */
    String post(String url, byte[] data, Map<String, List<String>> headers)  throws HttpClientException;
  
}
复制代码

HttpClient只定义了一个post方法,满足需求即可(偷懒需求~),大部分开放平台的接口都是post请求方式的,没必要把所有的http特性都给定义出来,一个post就是干!

接下来就用HttpURLConnection发起http请求吧:

package com.javaobj.sdk.http.impl;

public class DefaultHttpClient implements HttpClient {


    public String post(String url, byte[] data, Map<String, List<String>> headers) throws HttpClientException {

        URL targetUrl = null;
        try {
            targetUrl = new URL(url);
        } catch (MalformedURLException e) {
            throw new HttpClientException("访问的url链接不正确");
        }
        try {
            HttpURLConnection  connection = (HttpURLConnection) targetUrl.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");

            //设置讲求头
            if(headers != null){
                Map<String, List<String>> tmp = new HashMap<String, List<String>>(headers);
                for(Map.Entry<String, List<String>> entry : tmp.entrySet()){
                    for(String value: entry.getValue()){
                        connection.setRequestProperty(entry.getKey(), value);
                    }
                }
            }
            connection.connect();
            //发送请求数据
            OutputStream out = connection.getOutputStream();
            out.write(data);
            out.close();

            //读取请求响应
            InputStream in = connection.getInputStream();
            ByteOutputStream body = new ByteOutputStream();
            int ch;
            while ((ch = in.read()) != -1){
                body.write(ch);
            }
            in.close();

            String responseBody = new String(body.getBytes());
            int statusCode = connection.getResponseCode();
            boolean is2xxSuccessful = statusCode >= 200 && statusCode <= 299;
            boolean is3xxSuccessful = statusCode >= 300 && statusCode <= 399;
            //判断请求是否成功
            if (!(is2xxSuccessful || is3xxSuccessful)) {
                throw new HttpStatusClientException(statusCode, connection.getResponseMessage(), responseBody);
            }
            return responseBody;
        } catch (IOException e) {
            throw new HttpClientException(e);
        }
    }
}
复制代码

以上就实现了一个简单的http客户端了, 基本满足需求。

序列化和反序列化

序列化和反序列化就是将数据在Java实体之间转换,为了设计的灵活一点,此处面向接口编程,分别为序列化和反序列化定义一个接口:

  • Serializer
  • Deserializer

同时使用jackson库做了一个简单的实现:

JacksonSerializer

public class JacksonSerializer implements Serializer {

    private final  ObjectMapper objectMapper = new ObjectMapper();

    public JacksonSerializer() {
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
    }

    public byte[] serialize(Object obj) {
        try {
            return objectMapper.writeValueAsBytes(obj);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException(e);
        }
    }

}

复制代码

JacksonDeserializer

public class JacksonDeserializer implements Deserializer {

    private final ObjectMapper objectMapper = new ObjectMapper();

    public JacksonDeserializer() {
        objectMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
    }

    public <T> T deserialize(byte[] data, Class<T> clz) {
        try {
            return objectMapper.readValue(data, clz);
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }
}
复制代码

这样一个简单的序列化和反序列化工具就做好了,这样基本能满足需求,但是一般的开放平台都有format参数,它的值可能jsonxml等等,为了正好的整合序列化和反序列化, 我又创建了一个SerializationAdapter接口,继承SerializerDeserializer

public interface SerializationAdapter extends Serializer, Deserializer{

    /**
     * 判断是否支持数据格式
     * @return
     */
    boolean support(String format);
}
复制代码

添加SerializationAdapter.support方法用来判断它是否支持指定的数据格式,如适配json数据格式,创建一个实现类JSONSerializationAdapter

JSONSerializationAdapter

public class JSONSerializationAdapter implements SerializationAdapter {
    
    private final Serializer serializer;
    private final Deserializer deserializer;
    
    public JSONSerializationAdapter(Serializer serializer, Deserializer deserializer) {
        this.serializer = serializer;
        this.deserializer = deserializer;
    }
    
   public JSONSerializationAdapter() {
        this(new JacksonSerializer(), new JacksonDeserializer());
    }

    
    @Override
    public boolean support(String format) {
        return format != null && Objects.equals(format.toLowerCase(), "json");
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clz) {
        return deserializer.deserialize(data, clz);
    }

    @Override
    public byte[] serialize(Object obj) {
        return serializer.serialize(obj);
    }
}
复制代码

这样就比较灵活了, 假如说老板在后面偷偷的来一句,这个要是能支持xml就完美了,这个需求很简单! 假如我们使用JAXB来实现xml的序列化,那么先写好序列化和反序列化器:

JAXBSerializer

public class JAXBSerializer implements Serializer {
    @Override
    public byte[] serialize(Object obj) {
        ByteOutputStream outputStream = new ByteOutputStream();
        JAXB.marshal(obj, outputStream);
        return outputStream.getBytes();
    }
}
复制代码

JAXBDeserializer

public class JAXBDeserializer implements Deserializer {
    @Override
    public <T> T deserialize(byte[] data, Class<T> clz) {
        //trim去掉不需要的换行
        return JAXB.unmarshal(new StringReader(new String(data).trim()), clz);

    }
}
复制代码

XMLSerializationAdapter

public class XMLSerializationAdapter implements SerializationAdapter {

    private final Serializer serializer;
    private final Deserializer deserializer;

    public XMLSerializationAdapter(Serializer serializer, Deserializer deserializer) {
        this.serializer = serializer;
        this.deserializer = deserializer;
    }

    public XMLSerializationAdapter() {
        this(new JAXBSerializer(), new JAXBDeserializer());
    }

    @Override
    public boolean support(String format) {
        return format != null && Objects.equals(format.toLowerCase(), "xml");
    }

    @Override
    public <T> T deserialize(byte[] data, Class<T> clz) {
        return deserializer.deserialize(data, clz);
    }

    @Override
    public byte[] serialize(Object obj) {
        return serializer.serialize(obj);
    }
}
复制代码

同理其他数据格式也可以自定定义,就不多做阐述了。

如何使用?

使用也是非常简单的, 简单的写个测试方法测试下:

  public class SerializationTest {


    @Test
    public void testSerialization(){
        String format = "xml";
        List<SerializationAdapter> adapters = new ArrayList<>();
        adapters.add(new JSONSerializationAdapter());
        adapters.add(new XMLSerializationAdapter());

        Person person = new Person();
        person.setName("霸戈");
        person.setAge(18);

        for(SerializationAdapter adapter : adapters){
            if (adapter.support(format)) {
                byte[] data = adapter.serialize(person);
                System.out.println(format + "序列化后的数据:" + new String(data));
                Person person2 = adapter.deserialize(data, Person.class);
                System.out.println(format + "反序列化后的数据:" + person2);
            }
        }
    }
}
复制代码

以上测试代码,只需要改变format变量的值就能在jsonxml格式之间转换。

JSON格式输出
json序列化后的数据:{"name":"霸戈","age":18}
json反序列化后的数据:Person{name='霸戈', age=18}
复制代码
XML格式输出
xml序列化后的数据:<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<person>
    <age>18</age>
    <name>霸戈</name>
</person>
                                                                      
xml反序列化后的数据:Person{name='霸戈', age=18}
复制代码

API客户端

剩下的工具就是编写一个简单的API客户端,发起http请求、处理响应数据。

public interface SdkClient {

    /**
     * 执行API请求
     * @param req
     * @param <T>
     * @return
     */
    <T extends AbstractResponse> T execute(AbstractRequest<T> req);
    
}
复制代码

因为SDK都是给他人使用,尽可能的方便,入参(AbstractRequest),和出参(AbstractResponse)都用实体类表示。

AbstractRequest

public abstract class AbstractRequest<T extends AbstractResponse> {

    /**
     * 请求方法名称
     * @return
     */
    public abstract String getMethod();


    /**
     * 接口请求参数
     * @return
     */
    public abstract Map<String, Object> getApiParams();

    /**
     * 请求响应实体
     * @return
     */
    public abstract Class<T> getResponseClass();
}
复制代码

AbstractResponse


public class AbstractResponse {
    
    private String code;
    private String msg;
    
    public boolean isSuccess(){
        return Objects.equals(code, "0");
    }
}
复制代码

DefaultClient

一个简单的API客户端

public class DefaultClient implements SdkClient {

    private SerializationAdapter serializationAdapter = new JSONSerializationAdapter();
    private HttpClient httpClient = new DefaultHttpClient();

    /**
     * 请求地址
     */
    private final String endpoint;
    /**
     * 应用id
     */
    private final String appid;
    /**
     * 应用密钥
     */
    private final String appsecret;

    public DefaultClient(String endpoint, String appid, String appsecret) {
        this.endpoint = endpoint;
        this.appid = appid;
        this.appsecret = appsecret;
    }

    @Override
    public <T extends AbstractResponse> T execute(AbstractRequest<T> req) {
        Map<String, Object> params = req.getApiParams();
        String timestamp = System.currentTimeMillis() + "";

        //参数排序
        StringBuilder paramsStr = new StringBuilder();
        Map<String, Object> treeMap = new TreeMap<>(params);
        for(Map.Entry<String, Object> entry : treeMap.entrySet()){
            paramsStr.append(entry.getKey()).append(entry.getValue());
        }

        //计算签名
        //应用密钥 + 应用id + 请求方法名称 + api版本号 + 请求参数 + 应用密钥
        String sign = Md5Util.md5(appsecret + appid + req.getMethod() + req.getVersion()  + paramsStr.toString() + appsecret );

        String url = endpoint + "?"
                + "method=" + req.getMethod()
                + "&version=" + req.getVersion()
                + "&timestamp" + timestamp
                + "&sign" + sign
                + "&appid" + appid;

        Map<String, List<String>> headers = new HashMap<>();
        headers.put("Content-type", Collections.singletonList("application/json"));

        byte[] body = serializationAdapter.serialize(req.getApiParams());
        String response = httpClient.post(url, body, headers);
        return serializationAdapter.deserialize(response.getBytes(), req.getResponseClass());
    }
}
复制代码

总结

至此一个简单的开放平台SDK就基本成型了,为了适配大多数客户的开放环境,应当注意以下几点:

  • 尽量使用低版本jdk开发,如1.5、1.6,覆盖绝大多数Java运行/开发环境
  • 减少第三方依赖,避免版本冲突
  • 出参入参都使用java实体类,避免客户使用时参数名称、类型不正确

还有些注意点没想到的,大家阔以评论区留言,一起学习交流。

猜你喜欢

转载自juejin.im/post/7016671980493799455