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 字符串承载。方法内部不应直接依赖平台上下文之外的静态状态,否则在并发调用时容易出现难以复现的问题。
二、设备状态查询服务的实现
下面以一个"设备状态查询服务"为例,说明从参数设计到结果返回的完整实现。假设平台中已有一个名为 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 扩展不能直接热加载到运行中的平台,需要经过"编译 → 打包 → 导入 → 重启"的流程。典型步骤如下:
- 使用与目标平台版本匹配的 ThingWorx Extension SDK 作为编译依赖,避免因 API 版本差异导致
NoSuchMethodError。 - 将源码编译为 class 文件,连同
metadata.xml一起打包为扩展包(通常为.zip结构)。metadata.xml中声明的扩展名、版本号必须与代码中的包路径一致。 - 在 Composer 的"导入/导出"中导入扩展包,平台会校验依赖并提示重启。
- 重启后,在目标 Thing 的"服务"列表中应能看到
QueryDeviceStatus,说明注解已被正确扫描。
调试时常见的三类问题值得留意。其一是服务未出现在列表中,通常是因为扩展包未成功加载、类未被扫描到,或 metadata.xml 与代码包名不匹配。其二是调用时报参数类型错误,多因 baseType 与实际传入值不一致,例如把数字传给了声明为 STRING 的参数。其三是返回结果为空或格式异常,需要检查方法是否真的返回了非空字符串,以及 JSON 拼接是否合法。建议在方法入口和出口各加一行日志,记录入参和返回值,便于在平台日志中回溯。
四、参数与结果的映射要点
| 注解位置 | 关键属性 | 作用 | 常见取值 |
|---|---|---|---|
| 方法 | name | 平台侧服务名 | QueryDeviceStatus |
| 方法 | category | Composer 中的分组 | DeviceManagement |
| 方法 | baseType | 返回值类型 | JSON / STRING / INFOTABLE |
| 参数 | name | 调用时的参数名 | deviceId |
| 参数 | baseType | 参数类型 | STRING / NUMBER / BOOLEAN |
| 参数 | isRequired | 是否必填 | true / false |
当返回结构较复杂、需要多行多列时,应改用 InfoTable 作为返回类型,通过 ValueCollection 逐行填充字段,而不是把数据拼成 JSON 字符串。这样调用方在服务编排中可以直接按字段名取值,无需二次解析。对于本节的设备状态查询,如果后续要扩展为"批量查询多台设备",返回 InfoTable 会比 JSON 字符串更合适。
完成上述步骤后,一个可用的自定义服务就具备了完整的调用能力:注解定义了契约,Java 方法实现了逻辑,平台运行时完成了参数绑定与结果封装,调用方通过统一的服务名访问。这套机制是后续实现更复杂业务扩展的基础,也是把项目特有逻辑沉淀到平台侧的标准做法。