3. 自定义服务开发基础

ThingWorx 平台内置了大量开箱即用的服务,但在真实项目中,设备协议、业务规则和数据口径往往带有强烈的项目特征,仅靠内置服务很难覆盖全部需求。自定义服务(Custom Service)正是为此设计的扩展点:它允许开发者用 Java 编写业务逻辑,通过注解把方法暴露为平台可调用的服务,再由 ThingWorx 的运行时负责参数解析、类型转换、结果封装与调用编排。本节围绕三个核心注解展开,完成一个"设备状态查询服务"的开发与调试闭环。

一、注解驱动的服务模型

ThingWorx Java 扩展的核心思想是"注解即契约"。开发者不需要编写 WSDL、不需要手工注册服务,只要在类和方法上添加正确的注解,平台在扩展包加载时就会扫描并生成对应的服务元数据。三个最关键的注解分工如下:

  • @ThingworxServiceDefinition:标注在方法上,声明"这是一个可被 ThingWorx 调用的服务"。它的 name 属性决定服务在平台中的名称,description、category 等属性用于在 Composer 中展示说明与分组。
  • @ThingworxServiceResult:标注在方法上,声明服务的返回值信息,包括 name(结果名)与 baseType(基础类型,如 STRING、NUMBER、BOOLEAN、JSON、INFOTABLE)。一个服务只能有一个返回值声明。
  • @ThingworxServiceParameter:标注在方法参数上,声明每个输入参数的名称、类型、是否必填以及默认值。参数顺序与注解顺序一致,平台据此完成调用时的参数绑定。

需要特别注意的是,被注解的方法必须是 public,且参数与返回值类型要落在 ThingWorx 支持的类型集合内。基础类型直接映射,复杂结构通常用 InfoTable 或 JSON 字符串承载。方法内部不应直接依赖平台上下文之外的静态状态,否则在并发调用时容易出现难以复现的问题。

Java 自定义服务:注解契约与调用链 Java 扩展类 public class DeviceService { @ThingworxServiceDefinition( name="QueryDeviceStatus") @ThingworxServiceResult( name="result", baseType="JSON") public String query( @ThingworxServiceParameter( name="deviceId", baseType="STRING") String deviceId) { ... } } 注解 = 服务契约 ThingWorx 运行时 ① 扫描注解 → 生成服务元数据 注册到 Thing 的服务列表 ② 接收调用 → 参数绑定 按 name/baseType 做类型转换 ③ 反射调用 Java 方法 执行业务逻辑 ④ 封装返回值 按 baseType 序列化结果 运行时负责契约落地 调用入口 REST API POST /Things/{name}/Services/QueryDeviceStatus Composer 测试面板 填写 deviceId 后点击执行 服务编排 / 订阅 被其它服务或事件触发 统一按服务名调用 加载 调用

二、设备状态查询服务的实现

下面以一个"设备状态查询服务"为例,说明从参数设计到结果返回的完整实现。假设平台中已有一个名为 DeviceThing 的 Thing,其属性 deviceId、onlineStatus、lastHeartbeat 分别记录设备编号、在线状态和最近心跳时间。服务的目标是:输入设备编号,返回该设备当前的状态摘要。

package com.example.ext;

import com.thingworx.types.InfoTable;
import com.thingworx.types.collections.ValueCollection;
import com.thingworx.types.primitives.StringPrimitive;
import com.thingworx.entities.utils.ThingUtilities;
import com.thingworx.metadata.annotations.*;

public class DeviceStatusService {

    @ThingworxServiceDefinition(
        name = "QueryDeviceStatus",
        description = "根据设备编号查询设备在线状态与最近心跳",
        category = "DeviceManagement",
        isAllowOverride = false)
    @ThingworxServiceResult(
        name = "statusResult",
        baseType = "JSON",
        description = "设备状态摘要")
    public String queryDeviceStatus(
        @ThingworxServiceParameter(
            name = "deviceId",
            baseType = "STRING",
            description = "设备唯一编号",
            isRequired = true)
        String deviceId) throws Exception {

        if (deviceId == null || deviceId.trim().isEmpty()) {
            throw new Exception("deviceId 不能为空");
        }

        // 从平台读取目标 Thing 的属性
        String online = (String) ThingUtilities
            .findThing("DeviceThing")
            .getPropertyValue("onlineStatus")
            .getValue();

        String heartbeat = (String) ThingUtilities
            .findThing("DeviceThing")
            .getPropertyValue("lastHeartbeat")
            .getValue();

        // 组装 JSON 结果
        StringBuilder sb = new StringBuilder();
        sb.append("{")
          .append("\"deviceId\":\"").append(deviceId).append("\",")
          .append("\"onlineStatus\":\"").append(online).append("\",")
          .append("\"lastHeartbeat\":\"").append(heartbeat).append("\",")
          .append("\"queryTime\":\"").append(System.currentTimeMillis()).append("\"")
          .append("}");
        return sb.toString();
    }
}

这段代码体现了几个关键约定。第一,@ThingworxServiceDefinition 的 name 是平台侧的服务名,Java 方法名可以不同,但建议保持一致以便维护。第二,@ThingworxServiceResult 的 baseType 设为 JSON,意味着返回值会被平台按 JSON 字符串处理,调用方拿到的是结构化文本而非普通字符串。第三,参数注解中的 isRequired = true 会让平台在调用前做非空校验,减少方法内部的防御性代码。第四,方法抛出异常时,平台会把异常信息包装成调用错误返回,调用方可通过错误码和消息定位问题。

三、编译、部署与调试

Java 扩展不能直接热加载到运行中的平台,需要经过"编译 → 打包 → 导入 → 重启"的流程。典型步骤如下:

  1. 使用与目标平台版本匹配的 ThingWorx Extension SDK 作为编译依赖,避免因 API 版本差异导致 NoSuchMethodError。
  2. 将源码编译为 class 文件,连同 metadata.xml 一起打包为扩展包(通常为 .zip 结构)。metadata.xml 中声明的扩展名、版本号必须与代码中的包路径一致。
  3. 在 Composer 的"导入/导出"中导入扩展包,平台会校验依赖并提示重启。
  4. 重启后,在目标 Thing 的"服务"列表中应能看到 QueryDeviceStatus,说明注解已被正确扫描。
调试阶段建议先用 Composer 的服务测试面板验证参数绑定和返回值格式,确认无误后再接入 REST 或服务编排。这样可以把"注解配置问题"和"调用方问题"分开定位。

调试时常见的三类问题值得留意。其一是服务未出现在列表中,通常是因为扩展包未成功加载、类未被扫描到,或 metadata.xml 与代码包名不匹配。其二是调用时报参数类型错误,多因 baseType 与实际传入值不一致,例如把数字传给了声明为 STRING 的参数。其三是返回结果为空或格式异常,需要检查方法是否真的返回了非空字符串,以及 JSON 拼接是否合法。建议在方法入口和出口各加一行日志,记录入参和返回值,便于在平台日志中回溯。

四、参数与结果的映射要点

注解位置关键属性作用常见取值
方法name平台侧服务名QueryDeviceStatus
方法categoryComposer 中的分组DeviceManagement
方法baseType返回值类型JSON / STRING / INFOTABLE
参数name调用时的参数名deviceId
参数baseType参数类型STRING / NUMBER / BOOLEAN
参数isRequired是否必填true / false

当返回结构较复杂、需要多行多列时,应改用 InfoTable 作为返回类型,通过 ValueCollection 逐行填充字段,而不是把数据拼成 JSON 字符串。这样调用方在服务编排中可以直接按字段名取值,无需二次解析。对于本节的设备状态查询,如果后续要扩展为"批量查询多台设备",返回 InfoTable 会比 JSON 字符串更合适。

扩展包一旦导入,修改 Java 代码后必须重新编译、重新打包并再次导入,且通常需要重启平台才能生效。开发阶段应固定 SDK 版本与平台版本,避免在联调中途升级依赖。

完成上述步骤后,一个可用的自定义服务就具备了完整的调用能力:注解定义了契约,Java 方法实现了逻辑,平台运行时完成了参数绑定与结果封装,调用方通过统一的服务名访问。这套机制是后续实现更复杂业务扩展的基础,也是把项目特有逻辑沉淀到平台侧的标准做法。