服务开发框架

有了前面一些基本概念、基础数据结构作铺垫,咱们就可以开始真正熟悉ablecloud的服务开发框架了。实际上大多数开发者在使用ablecloud框架开发服务时,仅需简单的使用那些基础数据,将精力集中在实际的业务逻辑,快速的完成服务程序的开发/测试/发布。

ACService:自定义后端服务

ablecloud定义了抽象基类ACService,开发者只需要继承该类,并实现各个handler即可。定义如下:

public abstract class ACService {
    // ac是非常重要的ablecloud云框架服务,ablecloud提供了默认实现ACCloud,
    // 开发者可以调用ac的相关接口直接调用ablecloud提供的云服务,后文会有详述
    protected AC ac;

    // 以下信息可用于服务内部追踪问题等用,比如打印到日志中
    protected long developerId;         // 开发者id
    protected String majorDomain;       // 服务的主域名
    protected String subDomain;         // 服务的子域名
    protected int majorVersion;         // 服务的主版本号
    protected int minorVersion;         // 服务的副版本号
    protected int patchVersion;         // 服务的修订版本号

    /**
     * 开发者可根据自身需要,重载该方法,在该方法里做一些初始化工作,框架在启动服务时会调用该函数。
     * 比如,该服务要处理和设备之间的交互消息,需要自定义设备消息的序列化/反序列化器
     * ACDeviceMsgMarshaller,在init函数内将marshaller设置到ac中。
     *
     * @throws Exception
     */
    public void init() throws Exception {}

    /**
     * 处理APP-->Service,Service-->Service之间的交互消息
     * @param req   请求消息体
     * @param resp  响应消息体
     * @throws Exception
     */
    public abstract void handleMsg(ACMsg req, ACMsg resp) throws Exception;

    /**
     * 处理Device-->Service之间的交互消息
     * 如果服务不处理和设备之间的交互消息,则无须重载该方法。
     *
     * 当前,处理设备汇报的消息不做响应。
     *
     * @param context       设备的上下文,其中uid字段为系统填充
     * @param deviceId      设备的逻辑id
     * @param req           请求消息体
     * @throws Exception
     */
    public abstract void handleDeviceMsg(ACContext context, long deviceId, ACDeviceMsg req) throws Exception;

    /**
     * 处理JINDDONG-->Service之间的交互消息,收到Stream点数组,进行设备控制
     *
     * @param context          设备的上下文,其中uid字段为系统填充
     * @param physicalDeviceId 设备的物理id
     * @param req              请求消息体(Stream数组)
     * @param resp             响应消息体
     * @throws Exception
     */
    public void handleJDSetStatusMsg(ACContext context, String physicalDeviceId, List<ACJDMsg> req, ACMsg resp) throws Exception {}

    /**
     * 处理JINDDONG-->Service之间的交互消息,获取设备上所有Stream点
     *
     * @param context          设备的上下文,其中uid字段为系统填充
     * @param physicalDeviceId 设备的物理id
     * @param resp             响应消息体(Stream数组)
     * @throws Exception
     */
    public void handleJDGetStatusMsg(ACContext context, String physicalDeviceId, List<ACJDMsg> resp) throws Exception {}

    /**
     * 内部调用接口,开发者不用关注且不能修改。
     * 设置服务相关的信息,并将全局AC框架传给服务
     * 服务内部可以使用AC框架提供的各种功能,如
     * 帐号管理、设备管理、存储服务等。
     * @param ac
     * @param config
     */
    public final void setEnv(AC ac, ACConfiguration config) {}

    /**
     * 内部调用接口,开发者不用关注且不能修改。
     * @return
     */
    public final AC getAc() {}
}

在上述抽象类中,对开发者来说,总共有三个公共接口,其中init提供了默认实现。如果开发者实现的某一服务不需要和设备直接交互,则直接重载handleDeviceMsg为空实现即可。通常情况下,init也无需重载。因此,开发者可以将精力集中在handleMsg接口的实现中,该接口处理客户端请求,并作出响应。下文会对该抽象类进行详细介绍。

注:通常情况下,开发者只需要重点实现handleMsg即可。

ACCronJob:云端定时任务

ablecloud定义了云端定时任务的抽象基类ACCronJob。开发者需要继承该类,并实现其定义的抽象方法ACCronJob::run,即能完成定时任务的开发。ACCronJob的定义如下:

public abstract class ACCronJob {
    // ac是非常重要的ablecloud云框架服务,ablecloud提供了默认实现ACCloud,
    // 开发者可以调用ac的相关接口直接调用ablecloud提供的云服务,后文会有详述
    protected AC ac;

    // 以下信息可用于任务内部追踪问题等用,比如打印到日志中等。
    protected long developerId;         // 开发者id
    protected String majorDomain;       // 服务的主域名
    protected String subDomain;         // 服务的子域名
    protected int majorVersion;         // 服务的主版本号
    protected int minorVersion;         // 服务的副版本号
    protected int patchVersion;         // 服务的修订版本号

    /**
     * 内部调用接口,开发者不用关注且不能修改。
     * 设置服务相关的信息,并将全局AC框架传给服务
     * 服务内部可以使用AC框架提供的各种功能,如
     * 帐号管理、设备管理、存储服务等
     * @param ac
     * @param config
     */
    public final void setEnv(AC ac, ACConfiguration config) {
        this.ac = ac;
        this.developerId = config.getDeveloperId();
        this.majorDomain = config.getServiceMajorDomain();
        this.subDomain = config.getServiceSubDomain();
        this.majorVersion = config.getServiceMajorVersion();
        this.minorVersion = config.getServiceMinorVersion();
        this.patchVersion = config.getServicePatchVersion();
    }

    /**
     * 内部调用接口,开发者不用关注且不能修改。
     * @return AC对象。
     */
    public final AC getAc() {
        return ac;
    }

    /**
     * 定时任务的执行函数。
     * @return 返回任务的结束后,进程退出时所使用的状态码。
     * @throws Exception
     */
    public abstract int run() throws Exception;
}

上述抽象类共定义了三个公共方法:ACCronJob::setEnv,ACCronJob::getAC,以及ACCronJob::run。其中,ACCronJob::run是定时任务的执行函数,要求开发者提供具体实现。

AC

在介绍ACService和ACCronJob的时候提到过重要的成员变量ac,ac实际上是ablecloud对抽象服务框架AC的具体实现,其实现过程对开发者透明。通过AC,开发者可以根据需要获取一系列内嵌服务的功能接口。AC的定义如下:

public abstract class AC {
    protected ACConfiguration config;
    protected ACDeviceMsgMarshaller deviceMsgMarshaller;

    /**
     * 初始化框架
     * @param config    配置信息
     */
    public void init(ACConfiguration config) {}

    /**
     * 构建一个开发者上下文
     * @return
     */
    public ACContext newContext() {}

    /**
     * 构建一个用户上下文
     * @param userId
     * @return
     */
    public ACContext newContext(long userId) {}

    /**
     * 则用于创建数据分类/清空数据等操作。
     * 用于测试之用。
     *
     * @return
     */
    public abstract ACStoreForTest storeForTest(ACContext context);

    /**
     * 用于对数据分类进行具体的操作,如create/find/delete/update/scan等
     *
     * @param className     要操作的分类名
     * @param context       要进行操作的开发者context
     * @return
     */
    public abstract ACStore store(String className, ACContext context);

    /**
     * 直接往设备发送命令/消息
     * @param subDomain     子域名,比如thermostat
     * @param deviceId      设备逻辑id
     * @param reqMsg        具体的消息内容
     * @param context       如果通过app端发送消息到服务,服务在中继给设备,
     *                      则服务在发送给设备的时候需要带上app端的用户context。
     *                      如果是服务自主发送控制命令给设备,则需要传入开发者的context。
     * @return  设备返回的消息
     * @throws Exception
     */
    public abstract ACDeviceMsg sendToDevice(String subDomain, long deviceId,
                                             ACDeviceMsg reqMsg, ACContext context) throws Exception;

    /**
     * 往某一服务发送命令/消息
     * @param subDomain     子域名,比如thermostat
     * @param service Name          服务名
     * @param version       服务版本
     * @param req           具体的消息内容
     * @return  服务端相应的消息
     * @throws Exception
     */
    public abstract ACMsg sendToService(String subDomain, String serviceName, int version, ACMsg req) throws Exception;

    /**
     * 往JD service发送命令/消息,上报设备上的所有Stream点到JINGDONG Service
     *
     * @param context          设备的上下文,其中uid字段为系统填充
     * @param physicalDeviceId 设备的物理id
     * @param req              请求消息体(Stream数组)
     * @return 服务端相应的消息
     * @throws Exception
     */
    public abstract ACMsg sendToJDService(ACContext context, String physicalDeviceId, List<ACJDMsg> req) throws Exception;

    /**
     * 获取设备管理器。开发者在实现自定义服务时,
     * 可以调用ACDeviceMgr提供的各个通用接口
     *
     * @param context   用户的context
     * @return
     */
    public abstract ACDeviceMgr deviceMgr(ACContext context);

    /**
     * 获取用于单元测试的设备管理器,可以创建分组/绑定设备等
     *
     * @param context   用户的context
     * @return
     */
    public abstract ACDeviceMgrForTest deviceMgrForTest(ACContext context);

    /**
     * 获取帐号管理器。开发者组实现自定义服务时,
     * 可以调用ACAccountMgr提供的各个通用接口
     *
     * @param context   开发者的context
     * @return
     */
    public abstract ACAccountMgr accountMgr(ACContext context);

    /**
     * 获取用于单元测试的帐号管理器,可以注册用户等
     *
     * @param context   开发者的context
     * @return
     */
    public abstract ACAccountMgrForTest accountMgrForTest(ACContext context);

    /**
     * 获取通知管理器,可以给用户发送通知消息
     *
     * @param context   开发者的context
     * @return
     */
    public abstract ACNotificationMgr notificationMgr(ACContext context);

    /**
     * 为便于测试,开发者可实现一个服务的桩
     * 在框架中添加一个服务桩
     *
     * @param name  服务名
     * @param stub  服务桩的实现,实际上也是一个ACService
     */
    public abstract void addServiceStub(String name, ACService stub);

    /**
     * 为便于测试,开发者可实现一个设备的桩
     *
     * @param subDomain     设备所属子域
     * @param stub          设备桩
     */
    public abstract void addDeviceStub(String subDomain, ACDeviceStub stub);

    /**
     * 如果服务要处理和设备之间的交互消息,需要实现设备消息序列化/反序列化器
     * 该接口将序列化/反序列化器设置给ac框架
     *
     * @param marshaller    设备消息序列化/反序列化器
     */
    public void setDeviceMsgMarshaller(ACDeviceMsgMarshaller marshaller) {}

    /**
     * 获取设备消息序列化/反序列化器
     * @return
     */
    public ACDeviceMsgMarshaller getDeviceMsgMarshaller() {}

    /**
     * 获取用于单元测试的服务框架ac
     * @param config    单元测试环境构造的config
     * @return
     * @throws Exception
     */
    public static final AC getTestAc(ACConfiguration config) throws Exception {}
}

附录

签名算法

public class ACSigner {
    private static final Logger logger = LoggerFactory.getLogger(ACSigner.class);
    private static final String ENCODING = "UTF-8";
    private static final String HASH = "HmacSHA256";

    public static String genSignString(long developerId, String majorDomain,
                                       String subDomain, String method,
                                       long timestamp, long timeout, String nonce) {
        String stringToSign = String.valueOf(timeout) +
                    String.valueOf(timestamp) +
                    nonce +
                    String.valueOf(developerId) +
                    method +
                    majorDomain +
                    subDomain;
        return stringToSign;
    }

    public static String genSignature(String sk, String stringToSign) {
        String signature = "";

        try {
            String encodedSign = URLEncoder.encode(stringToSign, ENCODING);
            try {
                Mac mac = Mac.getInstance(HASH);
                mac.init(new SecretKeySpec(sk.getBytes(ENCODING), HASH));
                byte[] hashData = mac.doFinal(encodedSign.getBytes(ENCODING));

                StringBuilder sb = new StringBuilder(hashData.length * 2);
                for (byte data : hashData) {
                    String hex = Integer.toHexString(data & 0xFF);
                    if (hex.length() == 1) {
                        // append leading zero
                        sb.append("0");
                    }
                    sb.append(hex);
                }
                signature = sb.toString().toLowerCase();
            } catch (Exception e) {
                logger.warn("sha256 exception for[" + sk + "," + stringToSign + "]. e:", e);
            }
        } catch (UnsupportedEncodingException e) {
            logger.warn("encode error, string[" + stringToSign + "] e:" + e);
        }

        return signature;
    }
}