diff --git a/yudao-module-iot/pom.xml b/yudao-module-iot/pom.xml index e5833a3fae..325f81a24b 100644 --- a/yudao-module-iot/pom.xml +++ b/yudao-module-iot/pom.xml @@ -11,6 +11,7 @@ yudao-module-iot-api yudao-module-iot-biz yudao-module-iot-net-components + yudao-module-iot-script 4.0.0 diff --git a/yudao-module-iot/yudao-module-iot-api/pom.xml b/yudao-module-iot/yudao-module-iot-api/pom.xml index 4a31c9bf55..ef65715aae 100644 --- a/yudao-module-iot/yudao-module-iot-api/pom.xml +++ b/yudao-module-iot/yudao-module-iot-api/pom.xml @@ -37,10 +37,10 @@ provided - - org.pf4j - pf4j-spring - + + + + diff --git a/yudao-module-iot/yudao-module-iot-biz/pom.xml b/yudao-module-iot/yudao-module-iot-biz/pom.xml index a5f66ceee1..fe8e34ec38 100644 --- a/yudao-module-iot/yudao-module-iot-biz/pom.xml +++ b/yudao-module-iot/yudao-module-iot-biz/pom.xml @@ -69,13 +69,6 @@ yudao-spring-boot-starter-excel - - - - - - - org.apache.rocketmq @@ -93,11 +86,6 @@ true - - - - - org.apache.groovy @@ -145,6 +133,15 @@ yudao-module-iot-net-component-emqx ${revision} + + + + cn.iocoder.boot + yudao-module-iot-script + ${revision} + + + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductScriptController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductScriptController.java index 7e95ea2e0e..ca8666d730 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductScriptController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductScriptController.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.*; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO; +import cn.iocoder.yudao.module.iot.script.example.ProductScriptSamples; import cn.iocoder.yudao.module.iot.service.product.IotProductScriptService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -28,6 +29,9 @@ public class IotProductScriptController { @Resource private IotProductScriptService productScriptService; + @Resource + private ProductScriptSamples scriptSamples; + @PostMapping("/create") @Operation(summary = "创建产品脚本") @PreAuthorize("@ss.hasPermission('iot:product-script:create')") @@ -96,4 +100,26 @@ public class IotProductScriptController { productScriptService.updateProductScriptStatus(updateStatusReqVO.getId(), updateStatusReqVO.getStatus()); return success(true); } + + @GetMapping("/sample") + @Operation(summary = "获取示例脚本") + @Parameter(name = "type", description = "脚本类型(1=属性解析, 2=事件解析, 3=命令编码)", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('iot:product-script:query')") + public CommonResult getSampleScript(@RequestParam("type") Integer type) { + String sample; + switch (type) { + case 1: + sample = scriptSamples.getPropertyParserSample(); + break; + case 2: + sample = scriptSamples.getEventParserSample(); + break; + case 3: + sample = scriptSamples.getCommandEncoderSample(); + break; + default: + sample = "// 不支持的脚本类型"; + } + return success(sample); + } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductScriptServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductScriptServiceImpl.java index 7b225195f7..803e0047e9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductScriptServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductScriptServiceImpl.java @@ -9,13 +9,18 @@ import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProduct import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO; import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductScriptMapper; +import cn.iocoder.yudao.module.iot.script.context.DeviceScriptContext; +import cn.iocoder.yudao.module.iot.script.service.ScriptService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS; @@ -38,8 +43,8 @@ public class IotProductScriptServiceImpl implements IotProductScriptService { @Resource private IotProductService productService; -// @Resource -// private ScriptService scriptService; + @Resource + private ScriptService scriptService; @Override public Long createProductScript(IotProductScriptSaveReqVO createReqVO) { @@ -116,90 +121,103 @@ public class IotProductScriptServiceImpl implements IotProductScriptService { @Override public IotProductScriptTestRespVO testProductScript(IotProductScriptTestReqVO testReqVO) { -// long startTime = System.currentTimeMillis(); -// -// try { -// // 验证产品是否存在 -// validateProductExists(testReqVO.getProductId()); -// -// // 根据ID获取已保存的脚本(如果有) -// IotProductScriptDO existingScript = null; -// if (testReqVO.getId() != null) { -// existingScript = getProductScript(testReqVO.getId()); -// } -// -// // 创建测试上下文 -// PluginScriptContext context = new PluginScriptContext(); -// IotProductDO product = productService.getProduct(testReqVO.getProductId()); -// -// // 设置设备上下文(使用产品信息,没有具体设备) -// context.withDeviceContext(product.getProductKey(), null); -// -// // 设置输入参数 -// Map params = new HashMap<>(); -// params.put("input", testReqVO.getTestInput()); -// params.put("productKey", product.getProductKey()); -// params.put("scriptType", testReqVO.getScriptType()); -// -// // 根据脚本类型设置特定参数 -// switch (testReqVO.getScriptType()) { -// case 1: // PROPERTY_PARSER -// params.put("method", "property"); -// break; -// case 2: // EVENT_PARSER -// params.put("method", "event"); -// params.put("identifier", "default"); -// break; -// case 3: // COMMAND_ENCODER -// params.put("method", "command"); -// break; -// default: -// // 默认不添加额外参数 -// } -// -// // 添加所有参数到上下文 -// for (Map.Entry entry : params.entrySet()) { -// context.setParameter(entry.getKey(), entry.getValue()); -// } -// -// // 执行脚本 -// Object result = scriptService.executeScript( -// testReqVO.getScriptLanguage(), -// testReqVO.getScriptContent(), -// context); -// -// // 更新测试结果(如果是已保存的脚本) -// if (existingScript != null) { -// IotProductScriptDO updateObj = new IotProductScriptDO(); -// updateObj.setId(existingScript.getId()); -// updateObj.setLastTestTime(LocalDateTime.now()); -// updateObj.setLastTestResult(1); // 1表示成功 -// productScriptMapper.updateById(updateObj); -// } -// -// long executionTime = System.currentTimeMillis() - startTime; -// return IotProductScriptTestRespVO.success(result, executionTime); -// -// } catch (Exception e) { -// log.error("[testProductScript][测试脚本异常]", e); -// -// // 如果是已保存的脚本,更新测试失败状态 -// if (testReqVO.getId() != null) { -// try { -// IotProductScriptDO updateObj = new IotProductScriptDO(); -// updateObj.setId(testReqVO.getId()); -// updateObj.setLastTestTime(LocalDateTime.now()); -// updateObj.setLastTestResult(0); // 0表示失败 -// productScriptMapper.updateById(updateObj); -// } catch (Exception ex) { -// log.error("[testProductScript][更新脚本测试结果异常]", ex); -// } -// } -// -// long executionTime = System.currentTimeMillis() - startTime; -// return IotProductScriptTestRespVO.error(e.getMessage(), executionTime); -// } - return null; + long startTime = System.currentTimeMillis(); + + try { + // 验证产品是否存在 + validateProductExists(testReqVO.getProductId()); + + // 根据ID获取已保存的脚本(如果有) + IotProductScriptDO existingScript = null; + if (testReqVO.getId() != null) { + existingScript = getProductScript(testReqVO.getId()); + } + + // 创建测试上下文 + IotProductDO product = productService.getProduct(testReqVO.getProductId()); + DeviceScriptContext context = new DeviceScriptContext(); + + // 设置设备上下文(使用产品信息,测试时无具体设备) + context.withDeviceInfo(product.getProductKey(), null); + + // 设置输入参数 + Map params = new HashMap<>(); + params.put("input", testReqVO.getTestInput()); + params.put("productKey", product.getProductKey()); + params.put("scriptType", testReqVO.getScriptType()); + + // 根据脚本类型设置特定参数 + switch (testReqVO.getScriptType()) { + case 1: // PROPERTY_PARSER + params.put("method", "property"); + // 添加一些模拟的属性数据 + Map properties = new HashMap<>(); + properties.put("temp", 25.5); + properties.put("humidity", 60); + context.withProperties(properties); + break; + case 2: // EVENT_PARSER + params.put("method", "event"); + params.put("identifier", "default"); + // 添加事件数据 + Map eventParams = new HashMap<>(); + eventParams.put("timestamp", System.currentTimeMillis()); + params.put("eventParams", eventParams); + break; + case 3: // COMMAND_ENCODER + params.put("method", "command"); + // 添加命令参数 + Map cmdParams = new HashMap<>(); + cmdParams.put("cmdName", "setValue"); + cmdParams.put("cmdValue", 100); + params.put("cmdParams", cmdParams); + break; + default: + // 默认不添加额外参数 + } + + // 添加所有参数到上下文 + for (Map.Entry entry : params.entrySet()) { + context.setParameter(entry.getKey(), entry.getValue()); + } + + // 执行脚本 + Object result = scriptService.executeScript( + testReqVO.getScriptLanguage(), + testReqVO.getScriptContent(), + context); + + // 更新测试结果(如果是已保存的脚本) + if (existingScript != null) { + IotProductScriptDO updateObj = new IotProductScriptDO(); + updateObj.setId(existingScript.getId()); + updateObj.setLastTestTime(LocalDateTime.now()); + updateObj.setLastTestResult(1); // 1表示成功 + productScriptMapper.updateById(updateObj); + } + + long executionTime = System.currentTimeMillis() - startTime; + return IotProductScriptTestRespVO.success(result, executionTime); + + } catch (Exception e) { + log.error("[testProductScript][测试脚本异常]", e); + + // 如果是已保存的脚本,更新测试失败状态 + if (testReqVO.getId() != null) { + try { + IotProductScriptDO updateObj = new IotProductScriptDO(); + updateObj.setId(testReqVO.getId()); + updateObj.setLastTestTime(LocalDateTime.now()); + updateObj.setLastTestResult(0); // 0表示失败 + productScriptMapper.updateById(updateObj); + } catch (Exception ex) { + log.error("[testProductScript][更新脚本测试结果异常]", ex); + } + } + + long executionTime = System.currentTimeMillis() - startTime; + return IotProductScriptTestRespVO.error(e.getMessage(), executionTime); + } } @Override diff --git a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java index efd6cc0943..6364f5c72d 100644 --- a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java +++ b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java @@ -3,10 +3,8 @@ package cn.iocoder.yudao.module.iot.net.component.core.upstream; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Resource; - /** * 设备数据 Upstream 上行客户端 *

diff --git a/yudao-module-iot/yudao-module-iot-script/pom.xml b/yudao-module-iot/yudao-module-iot-script/pom.xml new file mode 100644 index 0000000000..8b46914a2d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/pom.xml @@ -0,0 +1,93 @@ + + + + yudao-module-iot + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-module-iot-script + jar + + ${project.artifactId} + IoT 脚本模块,提供 JavaScript 引擎解析等功能 + + + + + cn.iocoder.boot + yudao-module-iot-api + ${revision} + + + + + org.springframework + spring-context + + + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + true + + + org.slf4j + slf4j-api + + + + + org.graalvm.sdk + graal-sdk + 22.3.0 + + + org.graalvm.js + js + 22.3.0 + + + org.graalvm.js + js-scriptengine + 22.3.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + ${revision} + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + + diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/ScriptExample.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/ScriptExample.java new file mode 100644 index 0000000000..7a90251836 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/ScriptExample.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.module.iot.script; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext; +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; +import cn.iocoder.yudao.module.iot.script.service.ScriptService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 脚本使用示例类 + */ +@Slf4j +@Component +public class ScriptExample { + + @Autowired + private ScriptService scriptService; + + /** + * 执行简单的 JavaScript 脚本 + * + * @return 执行结果 + */ + public Object executeSimpleScript() { + // 简单的脚本内容 + String script = "var result = a + b; result;"; + + // 创建参数 + Map params = MapUtil.newHashMap(); + params.put("a", 10); + params.put("b", 20); + + // 执行脚本 + return scriptService.executeJavaScript(script, params); + } + + /** + * 执行包含函数的 JavaScript 脚本 + * + * @return 执行结果 + */ + public Object executeScriptWithFunction() { + // 包含函数的脚本内容 + String script = "function calc(x, y) { return x * y; } calc(a, b);"; + + // 创建上下文 + ScriptContext context = new DefaultScriptContext(); + context.setParameter("a", 5); + context.setParameter("b", 6); + + // 执行脚本 + return scriptService.executeJavaScript(script, context); + } + + /** + * 执行包含工具类使用的脚本 + * + * @return 执行结果 + */ + public Object executeScriptWithUtils() { + // 使用工具类的脚本内容 + String script = "var data = {name: 'test', value: 123}; utils.toJson(data);"; + + // 执行脚本 + return scriptService.executeJavaScript(script, MapUtil.newHashMap()); + } + + /** + * 执行包含日志输出的脚本 + * + * @return 执行结果 + */ + public Object executeScriptWithLogging() { + // 包含日志输出的脚本内容 + String script = "log.info('脚本开始执行...'); " + + "var result = a + b; " + + "log.info('计算结果: ' + result); " + + "result;"; + + // 创建参数 + Map params = MapUtil.newHashMap(); + params.put("a", 100); + params.put("b", 200); + + // 执行脚本 + return scriptService.executeJavaScript(script, params); + } + + /** + * 演示脚本安全性验证 + * + * @return 是否安全 + */ + public boolean validateScriptSecurity() { + // 安全的脚本 + String safeScript = "var x = 10; var y = 20; x + y;"; + boolean safeResult = scriptService.validateScript("js", safeScript); + + // 不安全的脚本 + String unsafeScript = "java.lang.System.exit(0);"; + boolean unsafeResult = scriptService.validateScript("js", unsafeScript); + + log.info("安全脚本验证结果: {}", safeResult); + log.info("不安全脚本验证结果: {}", unsafeResult); + + return safeResult && !unsafeResult; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/config/ScriptConfiguration.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/config/ScriptConfiguration.java new file mode 100644 index 0000000000..8339b217f2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/config/ScriptConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.script.config; + +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * 脚本模块配置类 + */ +@Configuration +public class ScriptConfiguration { + + /** + * 创建脚本引擎工厂 + * + * @return 脚本引擎工厂 + */ + @Bean + @Primary + public ScriptEngineFactory scriptEngineFactory() { + return new ScriptEngineFactory(); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DefaultScriptContext.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DefaultScriptContext.java new file mode 100644 index 0000000000..a75a354307 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DefaultScriptContext.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.script.context; + +import cn.hutool.core.map.MapUtil; + +import java.util.Map; + +/** + * 默认脚本上下文实现 + */ +public class DefaultScriptContext implements ScriptContext { + + /** + * 上下文参数 + */ + private final Map parameters = MapUtil.newHashMap(); + + /** + * 上下文函数 + */ + private final Map functions = MapUtil.newHashMap(); + + @Override + public Map getParameters() { + return parameters; + } + + @Override + public Map getFunctions() { + return functions; + } + + @Override + public void setParameter(String key, Object value) { + parameters.put(key, value); + } + + @Override + public Object getParameter(String key) { + return parameters.get(key); + } + + @Override + public void registerFunction(String name, Object function) { + functions.put(name, function); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DeviceScriptContext.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DeviceScriptContext.java new file mode 100644 index 0000000000..1518736b55 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DeviceScriptContext.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.iot.script.context; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * 设备脚本上下文,提供设备相关的上下文信息 + */ +@Slf4j +public class DeviceScriptContext extends DefaultScriptContext { + + /** + * 产品 Key + */ + @Getter + private String productKey; + + /** + * 设备名称 + */ + @Getter + private String deviceName; + + /** + * 设备属性数据缓存 + */ + private Map properties; + + /** + * 使用产品 Key 和设备名称初始化上下文 + * + * @param productKey 产品 Key + * @param deviceName 设备名称,可以为 null + * @return 当前上下文实例,用于链式调用 + */ + public DeviceScriptContext withDeviceInfo(String productKey, String deviceName) { + this.productKey = productKey; + this.deviceName = deviceName; + + // 添加到参数中,便于脚本访问 + setParameter("productKey", productKey); + if (StrUtil.isNotEmpty(deviceName)) { + setParameter("deviceName", deviceName); + } + return this; + } + + /** + * 设置设备属性数据 + * + * @param properties 属性数据 + * @return 当前上下文实例,用于链式调用 + */ + public DeviceScriptContext withProperties(Map properties) { + this.properties = properties; + if (MapUtil.isNotEmpty(properties)) { + setParameter("properties", properties); + } + return this; + } + + /** + * 获取设备属性值 + * + * @param key 属性标识符 + * @return 属性值 + */ + public Object getProperty(String key) { + if (MapUtil.isEmpty(properties)) { + return null; + } + return properties.get(key); + } + + /** + * 设置设备属性值 + * + * @param key 属性标识符 + * @param value 属性值 + */ + public void setProperty(String key, Object value) { + if (this.properties == null) { + this.properties = MapUtil.newHashMap(); + setParameter("properties", this.properties); + } + this.properties.put(key, value); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/ScriptContext.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/ScriptContext.java new file mode 100644 index 0000000000..d18644e822 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/ScriptContext.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.iot.script.context; + +import java.util.Map; + +/** + * 脚本上下文接口,定义脚本执行所需的上下文环境 + */ +public interface ScriptContext { + + /** + * 获取上下文参数 + * + * @return 上下文参数 + */ + Map getParameters(); + + /** + * 获取上下文函数 + * + * @return 上下文函数 + */ + Map getFunctions(); + + /** + * 设置上下文参数 + * + * @param key 参数名 + * @param value 参数值 + */ + void setParameter(String key, Object value); + + /** + * 获取上下文参数 + * + * @param key 参数名 + * @return 参数值 + */ + Object getParameter(String key); + + /** + * 注册函数 + * + * @param name 函数名称 + * @param function 函数对象 + */ + void registerFunction(String name, Object function); +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/AbstractScriptEngine.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/AbstractScriptEngine.java new file mode 100644 index 0000000000..b69aced139 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/AbstractScriptEngine.java @@ -0,0 +1,49 @@ +package cn.iocoder.yudao.module.iot.script.engine; + +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox; +import lombok.extern.slf4j.Slf4j; + +/** + * 抽象脚本引擎,提供脚本引擎的基本框架 + */ +@Slf4j +public abstract class AbstractScriptEngine implements ScriptEngine { + + /** + * 脚本沙箱,用于提供安全执行环境 + */ + protected final ScriptSandbox sandbox; + + /** + * 构造函数 + * + * @param sandbox 脚本沙箱 + */ + protected AbstractScriptEngine(ScriptSandbox sandbox) { + this.sandbox = sandbox; + } + + @Override + public Object execute(String script, ScriptContext context) { + try { + // 执行前验证脚本安全性 + sandbox.validate(script); + // 执行脚本 + return doExecute(script, context); + } catch (Exception e) { + log.error("执行脚本出错:{}", e.getMessage(), e); + throw new RuntimeException("脚本执行失败:" + e.getMessage(), e); + } + } + + /** + * 执行脚本的具体实现 + * + * @param script 脚本内容 + * @param context 脚本上下文 + * @return 脚本执行结果 + * @throws Exception 执行异常 + */ + protected abstract Object doExecute(String script, ScriptContext context) throws Exception; +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/JsScriptEngine.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/JsScriptEngine.java new file mode 100644 index 0000000000..222c56eb5a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/JsScriptEngine.java @@ -0,0 +1,343 @@ +package cn.iocoder.yudao.module.iot.script.engine; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox; +import cn.iocoder.yudao.module.iot.script.util.ScriptUtils; +import lombok.extern.slf4j.Slf4j; +import org.graalvm.polyglot.*; + +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * JavaScript 脚本引擎实现,基于 GraalJS Context API + */ +@Slf4j +public class JsScriptEngine extends AbstractScriptEngine implements AutoCloseable { + + /** + * JavaScript 引擎类型 + */ + public static final String TYPE = "js"; + + /** + * 脚本语言类型 + */ + private static final String LANGUAGE_ID = "js"; + + /** + * GraalJS 上下文 + */ + private final Context context; + + /** + * 脚本源代码缓存 + */ + private final Map sourceCache = new ConcurrentHashMap<>(); + + /** + * 脚本缓存的最大数量 + */ + private static final int MAX_CACHE_SIZE = 1000; + + /** + * 构造函数 + * + * @param sandbox JavaScript 沙箱 + */ + public JsScriptEngine(ScriptSandbox sandbox) { + super(sandbox); + + // 创建安全的主机访问配置 + HostAccess hostAccess = HostAccess.newBuilder() + .allowPublicAccess(true) // 允许访问公共方法和字段 + .allowArrayAccess(true) // 允许数组访问 + .allowListAccess(true) // 允许 List 访问 + .allowMapAccess(true) // 允许 Map 访问 + .build(); + + // 创建隔离的临时目录路径 + Path tempDirectory = Path.of(System.getProperty("java.io.tmpdir"), "graaljs-" + IdUtil.fastSimpleUUID()); + + // 初始化 GraalJS 上下文 + this.context = Context.newBuilder(LANGUAGE_ID) + .allowHostAccess(hostAccess) // 使用安全的主机访问配置 + .allowHostClassLookup(className -> false) // 禁止查找 Java 类 + .allowIO(false) // 禁止文件 IO + .allowNativeAccess(false) // 禁止本地访问 + .allowCreateThread(false) // 禁止创建线程 + .allowEnvironmentAccess(org.graalvm.polyglot.EnvironmentAccess.NONE) // 禁止环境变量访问 + .allowExperimentalOptions(false) // 禁止实验性选项 + .option("js.ecmascript-version", "2021") // 使用最新的 ECMAScript 标准 + .option("js.foreign-object-prototype", "false") // 禁用外部对象原型 + .option("js.nashorn-compat", "false") // 关闭 Nashorn 兼容模式以获得更好性能 + .build(); + } + + @Override + protected Object doExecute(String script, ScriptContext context) throws Exception { + if (StrUtil.isBlank(script)) { + return null; + } + + try { + // 绑定上下文变量 + bindContextVariables(context); + + // 从缓存获取或创建脚本源 + Source source = getOrCreateSource(script); + + // 执行脚本并捕获结果,添加超时控制 + Value result; + Thread executionThread = Thread.currentThread(); + Thread watchdogThread = new Thread(() -> { + try { + // 等待 5 秒 + TimeUnit.SECONDS.sleep(5); + // 如果执行线程还在运行,中断它 + if (executionThread.isAlive()) { + log.warn("脚本执行超时,强制中断"); + executionThread.interrupt(); + } + } catch (InterruptedException ignored) { + // 忽略中断 + } + }); + + watchdogThread.setDaemon(true); + watchdogThread.start(); + + try { + result = this.context.eval(source); + } finally { + watchdogThread.interrupt(); // 确保看门狗线程停止 + } + + // 转换结果为 Java 对象 + return convertResultToJava(result); + } catch (PolyglotException e) { + handleScriptException(e, script); + throw e; + } + } + + /** + * 绑定上下文变量 + * + * @param context 脚本上下文 + */ + private void bindContextVariables(ScriptContext context) { + Value bindings = this.context.getBindings(LANGUAGE_ID); + + // 添加上下文参数 + if (MapUtil.isNotEmpty(context.getParameters())) { + context.getParameters().forEach(bindings::putMember); + } + + // 添加上下文函数 + if (MapUtil.isNotEmpty(context.getFunctions())) { + context.getFunctions().forEach(bindings::putMember); + } + + // 添加工具类 + bindings.putMember("utils", ScriptUtils.getInstance()); + + // 添加日志对象 + bindings.putMember("log", log); + + // 添加控制台输出(限制并重定向到日志) + AtomicReference consoleBuffer = new AtomicReference<>(new StringBuilder()); + + Value console = this.context.eval(LANGUAGE_ID, "({\n" + + " log: function(msg) { _consoleLog(msg, 'INFO'); },\n" + + " info: function(msg) { _consoleLog(msg, 'INFO'); },\n" + + " warn: function(msg) { _consoleLog(msg, 'WARN'); },\n" + + " error: function(msg) { _consoleLog(msg, 'ERROR'); }\n" + + "})"); + + bindings.putMember("console", console); + + bindings.putMember("_consoleLog", (java.util.function.BiConsumer) (message, level) -> { + String formattedMsg = String.valueOf(message); + switch (level) { + case "INFO": + log.info("Script console: {}", formattedMsg); + break; + case "WARN": + log.warn("Script console: {}", formattedMsg); + break; + case "ERROR": + log.error("Script console: {}", formattedMsg); + break; + default: + log.info("Script console: {}", formattedMsg); + } + + // 将输出添加到缓冲区 + StringBuilder buffer = consoleBuffer.get(); + if (buffer.length() > 10000) { + buffer = new StringBuilder(); + consoleBuffer.set(buffer); + } + buffer.append(formattedMsg).append("\n"); + }); + } + + /** + * 从缓存中获取或创建脚本源 + * + * @param script 脚本内容 + * @return 脚本源 + */ + private Source getOrCreateSource(String script) { + // 如果缓存太大,清理部分缓存 + if (sourceCache.size() > MAX_CACHE_SIZE) { + int itemsToRemove = (int) (MAX_CACHE_SIZE * 0.2); // 清理 20% 的缓存 + sourceCache.keySet().stream() + .limit(itemsToRemove) + .toList() + .forEach(sourceCache::remove); + } + + // 使用脚本的哈希码作为缓存键 + String cacheKey = String.valueOf(script.hashCode()); + + return sourceCache.computeIfAbsent(cacheKey, key -> { + try { + return Source.newBuilder(LANGUAGE_ID, script, "script-" + key + ".js").cached(true).build(); + } catch (Exception e) { + log.error("创建脚本源失败: {}", e.getMessage(), e); + throw new RuntimeException("创建脚本源失败: " + e.getMessage(), e); + } + }); + } + + /** + * 将 GraalJS 结果转换为 Java 对象 + * + * @param result GraalJS 执行结果 + * @return Java 对象 + */ + private Object convertResultToJava(Value result) { + if (result == null || result.isNull()) { + return null; + } + + if (result.isString()) { + return result.asString(); + } + + if (result.isNumber()) { + if (result.fitsInInt()) { + return result.asInt(); + } else if (result.fitsInLong()) { + return result.asLong(); + } else if (result.fitsInFloat()) { + return result.asFloat(); + } else if (result.fitsInDouble()) { + return result.asDouble(); + } + } + + if (result.isBoolean()) { + return result.asBoolean(); + } + + if (result.hasArrayElements()) { + int size = (int) result.getArraySize(); + Object[] array = new Object[size]; + for (int i = 0; i < size; i++) { + array[i] = convertResultToJava(result.getArrayElement(i)); + } + return array; + } + + if (result.hasMembers()) { + Map map = MapUtil.newHashMap(); + for (String key : result.getMemberKeys()) { + map.put(key, convertResultToJava(result.getMember(key))); + } + return map; + } + + if (result.isHostObject()) { + return result.asHostObject(); + } + + // 默认情况下尝试转换为字符串 + return result.toString(); + } + + /** + * 处理脚本执行异常 + * + * @param e 多语言异常 + * @param script 原始脚本 + */ + private void handleScriptException(PolyglotException e, String script) { + if (e.isCancelled()) { + log.error("脚本执行被取消,可能超出资源限制"); + } else if (e.isHostException()) { + Throwable hostException = e.asHostException(); + log.error("脚本执行时发生 Java 异常: {}", hostException.getMessage(), hostException); + } else if (e.isGuestException()) { + if (e.getSourceLocation() != null) { + log.error("脚本执行错误: {} 位于行 {},列 {}", + e.getMessage(), + e.getSourceLocation().getStartLine(), + e.getSourceLocation().getStartColumn()); + + // 尝试显示错误位置上下文 + try { + String[] lines = script.split("\n"); + int lineNumber = e.getSourceLocation().getStartLine(); + if (lineNumber > 0 && lineNumber <= lines.length) { + int contextStart = Math.max(1, lineNumber - 2); + int contextEnd = Math.min(lines.length, lineNumber + 2); + + StringBuilder context = new StringBuilder(); + for (int i = contextStart; i <= contextEnd; i++) { + if (i == lineNumber) { + context.append("> "); // 标记错误行 + } else { + context.append(" "); + } + context.append(i).append(": ").append(lines[i - 1]).append("\n"); + } + log.error("脚本上下文:\n{}", context); + } + } catch (Exception ignored) { + // 忽略上下文显示失败 + } + } else { + log.error("脚本执行错误: {}", e.getMessage()); + } + } else { + log.error("脚本执行时发生未知错误: {}", e.getMessage(), e); + } + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public void close() { + try { + // 清除脚本缓存 + sourceCache.clear(); + + // 关闭 GraalJS 上下文,释放资源 + context.close(true); + } catch (Exception e) { + log.warn("关闭 GraalJS 引擎时发生错误: {}", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngine.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngine.java new file mode 100644 index 0000000000..7786aea4d5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngine.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.script.engine; + +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; + +/** + * 脚本引擎接口,定义脚本执行的核心功能 + */ +public interface ScriptEngine { + + /** + * 执行脚本 + * + * @param script 脚本内容 + * @param context 脚本上下文 + * @return 脚本执行结果 + */ + Object execute(String script, ScriptContext context); + + /** + * 获取脚本引擎类型 + * + * @return 脚本引擎类型 + */ + String getType(); +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngineFactory.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngineFactory.java new file mode 100644 index 0000000000..25cdc85c7c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngineFactory.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.iot.script.engine; + +import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 脚本引擎工厂,用于创建和缓存不同类型的脚本引擎,支持资源生命周期管理 + */ +@Slf4j +@Component +public class ScriptEngineFactory implements DisposableBean { + + /** + * 脚本引擎缓存 + */ + private final Map engines = new ConcurrentHashMap<>(); + + /** + * 获取脚本引擎 + * + * @param type 脚本类型 + * @return 脚本引擎 + */ + public ScriptEngine getEngine(String type) { + // 从缓存中获取引擎 + return engines.computeIfAbsent(type, this::createEngine); + } + + /** + * 创建脚本引擎 + * + * @param type 脚本类型 + * @return 脚本引擎 + */ + private ScriptEngine createEngine(String type) { + try { + if (JsScriptEngine.TYPE.equals(type)) { + log.info("创建 GraalJS 脚本引擎"); + return new JsScriptEngine(new JsSandbox()); + } + + log.warn("不支持的脚本类型: {}", type); + return null; + } catch (Exception e) { + log.error("创建脚本引擎 [{}] 失败: {}", type, e.getMessage(), e); + throw new RuntimeException("创建脚本引擎失败: " + e.getMessage(), e); + } + } + + /** + * 释放指定类型的引擎资源 + * + * @param type 脚本类型 + */ + public void releaseEngine(String type) { + ScriptEngine engine = engines.remove(type); + if (engine instanceof AutoCloseable) { + try { + ((AutoCloseable) engine).close(); + log.info("已释放脚本引擎资源: {}", type); + } catch (Exception e) { + log.warn("释放脚本引擎 [{}] 资源时发生错误: {}", type, e.getMessage()); + } + } + } + + /** + * 清理所有引擎资源 + */ + public void releaseAllEngines() { + engines.keySet().forEach(this::releaseEngine); + log.info("已清理所有脚本引擎资源"); + } + + @Override + public void destroy() { + // 应用关闭时,释放所有引擎资源 + log.info("应用关闭,释放所有脚本引擎资源..."); + releaseAllEngines(); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/GraalJsExample.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/GraalJsExample.java new file mode 100644 index 0000000000..445b4410be --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/GraalJsExample.java @@ -0,0 +1,208 @@ +package cn.iocoder.yudao.module.iot.script.example; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext; +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; +import cn.iocoder.yudao.module.iot.script.service.ScriptService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * GraalJS 脚本引擎示例 + *

+ * 展示了如何使用 GraalJS 脚本引擎的各种功能 + */ +@Slf4j +@Component +public class GraalJsExample { + + @Autowired + private ScriptService scriptService; + + /** + * 执行简单的 JavaScript 脚本 + * + * @return 执行结果 + */ + public Object executeSimpleScript() { + // 简单的脚本内容 + String script = "var result = a + b; result;"; + + // 创建参数 + Map params = MapUtil.newHashMap(); + params.put("a", 10); + params.put("b", 20); + + // 执行脚本 + return scriptService.executeJavaScript(script, params); + } + + /** + * 执行现代 JavaScript 语法(ES6+) + * + * @return 执行结果 + */ + public Object executeModernJavaScript() { + // 使用现代 JavaScript 语法 + String script = "// 使用箭头函数\n" + + "const add = (a, b) => a + b;\n" + + "\n" + + "// 使用解构赋值\n" + + "const {c, d} = params;\n" + + "\n" + + "// 使用模板字符串\n" + + "const result = `计算结果: ${add(c, d)}`;\n" + + "\n" + + "// 使用可选链操作符\n" + + "const value = params?.e?.value ?? 'default';\n" + + "\n" + + "// 返回一个对象\n" + + "({\n" + + " sum: add(c, d),\n" + + " message: result,\n" + + " defaultValue: value\n" + + "})"; + + // 创建参数 + Map params = MapUtil.newHashMap(); + params.put("params", MapUtil.builder() + .put("c", 30) + .put("d", 40) + .build()); + + // 执行脚本 + return scriptService.executeJavaScript(script, params); + } + + /** + * 执行带错误处理的脚本 + * + * @return 执行结果 + */ + public Object executeWithErrorHandling() { + // 包含错误处理的脚本 + String script = "try {\n" + + " // 故意制造错误\n" + + " if (!nonExistentVar) {\n" + + " throw new Error('手动抛出的错误');\n" + + " }\n" + + "} catch (error) {\n" + + " console.error('捕获到错误: ' + error.message);\n" + + " return { success: false, error: error.message };\n" + + "}\n" + + "\n" + + "return { success: true, data: 'No error' };"; + + // 执行脚本 + return scriptService.executeJavaScript(script, MapUtil.newHashMap()); + } + + /** + * 演示超时控制 + * + * @return 执行结果 + */ + public Object executeWithTimeout() { + // 这个脚本会导致无限循环 + String script = "// 无限循环\n" + + "var counter = 0;\n" + + "while(true) {\n" + + " counter++;\n" + + " if (counter % 1000000 === 0) {\n" + + " console.log('Still running: ' + counter);\n" + + " }\n" + + "}\n" + + "return counter;"; + + // 使用 CompletableFuture 和超时控制 + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + return scriptService.executeJavaScript(script, MapUtil.newHashMap()); + } catch (Exception e) { + log.error("脚本执行失败: {}", e.getMessage()); + return "执行失败: " + e.getMessage(); + } + }); + + try { + // 等待结果,最多 10 秒 + return future.get(10, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException e) { + return "执行异常: " + e.getMessage(); + } catch (TimeoutException e) { + future.cancel(true); + return "执行超时,已强制终止"; + } + } + + /** + * 演示 JSON 处理 + * + * @return 执行结果 + */ + public Object executeJsonProcessing() { + // JSON 处理示例 + String script = "// 解析传入的 JSON\n" + + "var data = JSON.parse(jsonString);\n" + + "\n" + + "// 处理 JSON 数据\n" + + "var result = {\n" + + " name: data.name.toUpperCase(),\n" + + " age: data.age + 1,\n" + + " address: data.address || 'Unknown',\n" + + " tags: [...data.tags, 'processed'],\n" + + " timestamp: Date.now()\n" + + "};\n" + + "\n" + + "// 转换回 JSON 字符串\n" + + "JSON.stringify(result);"; + + // 创建上下文 + ScriptContext context = new DefaultScriptContext(); + context.setParameter("jsonString", + "{\"name\":\"test user\",\"age\":25,\"tags\":[\"tag1\",\"tag2\"]}"); + + // 执行脚本 + return scriptService.executeJavaScript(script, context); + } + + /** + * 演示数据转换 + * + * @return 执行结果 + */ + public Object executeDataConversion() { + // 数据转换和处理示例 + String script = "// 使用 utils 工具类进行数据转换\n" + + "var stringValue = utils.isEmpty(input) ? \"默认值\" : input;\n" + + "var numberValue = utils.convert(stringValue, \"number\");\n" + + "\n" + + "// 创建一个复杂数据结构\n" + + "var result = {\n" + + " original: input,\n" + + " stringValue: stringValue,\n" + + " numberValue: numberValue,\n" + + " booleanValue: Boolean(numberValue),\n" + + " isValid: utils.isNotEmpty(input)\n" + + "};\n" + + "\n" + + "// 记录处理结果\n" + + "log.info(\"处理结果: \" + utils.toJson(result));\n" + + "\n" + + "return result;"; + + // 创建参数 + Map params = MapUtil.newHashMap(); + params.put("input", "42"); + + // 执行脚本 + return scriptService.executeJavaScript(script, params); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/ProductScriptSamples.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/ProductScriptSamples.java new file mode 100644 index 0000000000..d091565b8b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/ProductScriptSamples.java @@ -0,0 +1,174 @@ +package cn.iocoder.yudao.module.iot.script.example; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 产品脚本示例类,提供各种类型的产品脚本示例代码 + */ +@Slf4j +@Component +public class ProductScriptSamples { + + /** + * 获取属性解析脚本示例 + * + * @return 属性解析脚本示例代码 + */ + public String getPropertyParserSample() { + return "/**\n" + + " * 属性上报数据解析脚本示例\n" + + " * @param input 设备上报的原始数据\n" + + " * @param productKey 产品标识\n" + + " * @param method 方法类型,固定为 property\n" + + " * @param properties 当前设备的属性数据\n" + + " * @return 解析后的属性数据\n" + + " */\n" + + "function parseProperty(input, productKey) {\n" + + " // 记录日志\n" + + " console.log('开始解析属性数据: ' + input);\n" + + " \n" + + " try {\n" + + " // 假设上报的是 JSON 字符串\n" + + " var data = JSON.parse(input);\n" + + " \n" + + " // 构建属性数据结构\n" + + " var result = {\n" + + " // 属性上报的时间戳,毫秒级\n" + + " timestamp: data.timestamp || Date.now(),\n" + + " // 属性数据\n" + + " params: {}\n" + + " };\n" + + " \n" + + " // 处理属性值\n" + + " if (data.temperature) {\n" + + " result.params.temperature = parseFloat(data.temperature);\n" + + " }\n" + + " \n" + + " if (data.humidity) {\n" + + " result.params.humidity = parseFloat(data.humidity);\n" + + " }\n" + + " \n" + + " console.log('属性解析结果: ' + JSON.stringify(result));\n" + + " return result;\n" + + " } catch (error) {\n" + + " console.error('解析属性数据失败: ' + error.message);\n" + + " throw new Error('解析失败: ' + error.message);\n" + + " }\n" + + "};\n" + + "\n" + + "// 执行解析\n" + + "parseProperty(input, productKey);"; + } + + /** + * 获取事件解析脚本示例 + * + * @return 事件解析脚本示例代码 + */ + public String getEventParserSample() { + return "/**\n" + + " * 事件数据解析脚本示例\n" + + " * @param input 设备上报的原始数据\n" + + " * @param productKey 产品标识\n" + + " * @param method 方法类型,固定为 event\n" + + " * @param identifier 事件标识符\n" + + " * @return 解析后的事件数据\n" + + " */\n" + + "function parseEvent(input, productKey, identifier) {\n" + + " // 记录日志\n" + + " console.log('开始解析事件数据: ' + input);\n" + + " console.log('事件标识符: ' + identifier);\n" + + " \n" + + " try {\n" + + " // 假设上报的是 JSON 字符串\n" + + " var data = JSON.parse(input);\n" + + " \n" + + " // 构建事件数据结构\n" + + " var result = {\n" + + " // 事件标识符\n" + + " identifier: identifier || 'alert',\n" + + " // 事件上报的时间戳,毫秒级\n" + + " timestamp: data.timestamp || Date.now(),\n" + + " // 事件参数\n" + + " params: {}\n" + + " };\n" + + " \n" + + " // 根据不同事件类型处理参数\n" + + " if (result.identifier === 'alert') {\n" + + " result.params.level = data.level || 'info';\n" + + " result.params.message = data.message || '';\n" + + " } else if (result.identifier === 'error') {\n" + + " result.params.code = data.code || 0;\n" + + " result.params.message = data.message || '';\n" + + " }\n" + + " \n" + + " console.log('事件解析结果: ' + JSON.stringify(result));\n" + + " return result;\n" + + " } catch (error) {\n" + + " console.error('解析事件数据失败: ' + error.message);\n" + + " throw new Error('解析失败: ' + error.message);\n" + + " }\n" + + "};\n" + + "\n" + + "// 执行解析\n" + + "parseEvent(input, productKey, identifier);"; + } + + /** + * 获取命令编码脚本示例 + * + * @return 命令编码脚本示例代码 + */ + public String getCommandEncoderSample() { + return "/**\n" + + " * 命令数据编码脚本示例\n" + + " * @param input 平台下发的命令数据\n" + + " * @param productKey 产品标识\n" + + " * @param method 方法类型,固定为 command\n" + + " * @param cmdParams 命令参数\n" + + " * @return 编码后的命令数据\n" + + " */\n" + + "function encodeCommand(input, productKey, cmdParams) {\n" + + " // 记录日志\n" + + " console.log('开始编码命令数据: ' + input);\n" + + " console.log('命令参数: ' + JSON.stringify(cmdParams));\n" + + " \n" + + " try {\n" + + " // 输入可能是 JSON 字符串或对象\n" + + " var data = typeof input === 'string' ? JSON.parse(input) : input;\n" + + " \n" + + " // 获取命令名称和值\n" + + " var cmdName = cmdParams.cmdName || '';\n" + + " var cmdValue = cmdParams.cmdValue;\n" + + " \n" + + " // 构建设备可识别的命令格式\n" + + " var result = {\n" + + " cmd: cmdName,\n" + + " value: cmdValue,\n" + + " timestamp: Date.now()\n" + + " };\n" + + " \n" + + " // 根据不同命令类型构建参数\n" + + " if (cmdName === 'setValue') {\n" + + " // 无需额外处理\n" + + " } else if (cmdName === 'control') {\n" + + " result.mode = data.mode || 'auto';\n" + + " result.action = data.action || 'start';\n" + + " }\n" + + " \n" + + " // 转换为设备能识别的格式(此处以 JSON 字符串为例)\n" + + " var encodedResult = JSON.stringify(result);\n" + + " \n" + + " console.log('命令编码结果: ' + encodedResult);\n" + + " return encodedResult;\n" + + " } catch (error) {\n" + + " console.error('编码命令数据失败: ' + error.message);\n" + + " throw new Error('编码失败: ' + error.message);\n" + + " }\n" + + "};\n" + + "\n" + + "// 执行编码\n" + + "encodeCommand(input, productKey, cmdParams);"; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/package-info.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/package-info.java new file mode 100644 index 0000000000..0ec0c14e07 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/package-info.java @@ -0,0 +1,4 @@ +/** + * IoT 脚本模块,提供脚本引擎、执行环境和沙箱功能,支持 JavaScript 脚本的执行 + */ +package cn.iocoder.yudao.module.iot.script; diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/JsSandbox.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/JsSandbox.java new file mode 100644 index 0000000000..299f152c7f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/JsSandbox.java @@ -0,0 +1,329 @@ +package cn.iocoder.yudao.module.iot.script.sandbox; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * JavaScript 沙箱实现,提供脚本安全性验证 + */ +@Slf4j +public class JsSandbox implements ScriptSandbox { + + /** + * JavaScript 沙箱类型 + */ + public static final String TYPE = "js"; + + /** + * 不安全的关键字列表 + */ + private final List unsafeKeywords = new ArrayList<>(); + + /** + * 可能导致高资源消耗的关键字 + */ + private final List highResourceKeywords = new ArrayList<>(); + + /** + * 不安全的包/类访问模式 + */ + private final List unsafePatterns = new ArrayList<>(); + + /** + * 递归或循环嵌套深度检测模式 + */ + private final List recursionPatterns = new ArrayList<>(); + + /** + * 允许的脚本最大长度(字节) + */ + private static final int MAX_SCRIPT_LENGTH = 100 * 1024; // 100KB + + /** + * 脚本安全验证超时时间(毫秒) + */ + private static final long VALIDATION_TIMEOUT = 1000; // 1秒 + + /** + * 构造函数,初始化不安全的关键字和模式 + */ + public JsSandbox() { + // 初始化 Java 相关的不安全关键字 + Arrays.asList( + "java.lang.System", + "java.io", + "java.net", + "java.nio", + "java.security", + "java.rmi", + "java.lang.reflect", + "java.sql", + "javax.sql", + "javax.naming", + "javax.script", + "javax.tools", + "org.omg", + "org.graalvm.polyglot", + "sun.", + "javafx.", + "Packages.", + "com.sun.", + "com.oracle.").forEach(unsafeKeywords::add); + + // GraalJS 特有的不安全关键字 + Arrays.asList( + "Polyglot.import", + "Polyglot.eval", + "Java.type", + "allowHostAccess", + "allowNativeAccess", + "allowIO", + "allowHostClassLoading", + "allowAllAccess", + "allowExperimentalOptions", + "Context.Builder", + "Context.create", + "Context.getCurrent", + "Context.newBuilder", + "__proto__", + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "__noSuchMethod__", + "constructor.constructor", + "Object.constructor").forEach(unsafeKeywords::add); + + // 可能导致高资源消耗的关键字 + Arrays.asList( + "while(true)", + "for(;;)", + "do{", + "BigInt", + "Promise.all", + "setTimeout", + "setInterval", + "new Array(", + "Array(", + "new ArrayBuffer(", + ".repeat(", + ".forEach(", + ".map(", + ".reduce(").forEach(highResourceKeywords::add); + + // 初始化不安全的模式 + // 系统访问和进程执行 + unsafePatterns.add(Pattern.compile("java\\.lang\\.Runtime")); + unsafePatterns.add(Pattern.compile("java\\.lang\\.ProcessBuilder")); + unsafePatterns.add(Pattern.compile("java\\.lang\\.reflect")); + + // 特殊对象和操作 + unsafePatterns.add(Pattern.compile("Packages")); + unsafePatterns.add(Pattern.compile("JavaImporter")); + unsafePatterns.add(Pattern.compile("load\\s*\\(")); + unsafePatterns.add(Pattern.compile("loadWithNewGlobal\\s*\\(")); + unsafePatterns.add(Pattern.compile("exit\\s*\\(")); + unsafePatterns.add(Pattern.compile("quit\\s*\\(")); + unsafePatterns.add(Pattern.compile("eval\\s*\\(")); + + // GraalJS 特有的不安全模式 + unsafePatterns.add(Pattern.compile("Polyglot\\.")); + unsafePatterns.add(Pattern.compile("Java\\.type\\s*\\(")); + unsafePatterns.add(Pattern.compile("Context\\.")); + unsafePatterns.add(Pattern.compile("Engine\\.")); + + // 原型污染检测 + unsafePatterns.add(Pattern.compile("(?:Object|Array|String|Number|Boolean|Function|RegExp|Date)\\.prototype")); + unsafePatterns.add(Pattern.compile("\\['constructor'\\]")); + unsafePatterns.add(Pattern.compile("\\[\"constructor\"\\]")); + unsafePatterns.add(Pattern.compile("\\['__proto__'\\]")); + unsafePatterns.add(Pattern.compile("\\[\"__proto__\"\\]")); + + // 检测可能导致无限递归或循环的模式 + recursionPatterns.add(Pattern.compile("for\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*for\\s*\\(")); // 嵌套循环 + recursionPatterns.add(Pattern.compile("while\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*while\\s*\\(")); // 嵌套 while + recursionPatterns.add(Pattern.compile("function\\s+[a-zA-Z0-9_$]+\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*\\1\\s*\\(")); // 递归函数调用 + } + + @Override + public boolean validate(String script) { + if (StrUtil.isBlank(script)) { + return true; + } + + // 检查脚本长度 + if (script.length() > MAX_SCRIPT_LENGTH) { + log.warn("脚本长度超过限制: {} > {}", script.length(), MAX_SCRIPT_LENGTH); + return false; + } + + // 使用超时机制进行验证 + final boolean[] result = {true}; + Thread validationThread = new Thread(() -> { + // 检查不安全的关键字 + if (containsUnsafeKeywords(script)) { + result[0] = false; + return; + } + + // 检查不安全的模式 + if (matchesUnsafePatterns(script)) { + result[0] = false; + return; + } + + // 检查可能导致高资源消耗的构造 + if (containsHighResourcePatterns(script)) { + log.warn("脚本包含可能导致高资源消耗的构造,需要注意"); + // 不直接拒绝,而是记录警告 + } + + // 分析脚本复杂度 + analyzeScriptComplexity(script); + }); + + validationThread.start(); + try { + validationThread.join(VALIDATION_TIMEOUT); + if (validationThread.isAlive()) { + validationThread.interrupt(); + log.warn("脚本安全验证超时"); + return false; + } + } catch (InterruptedException e) { + log.warn("脚本安全验证被中断"); + return false; + } + + return result[0]; + } + + @Override + public String getType() { + return TYPE; + } + + /** + * 检查脚本是否包含不安全的关键字 + * + * @param script 脚本内容 + * @return 是否包含不安全的关键字 + */ + private boolean containsUnsafeKeywords(String script) { + if (CollUtil.isEmpty(unsafeKeywords)) { + return false; + } + + for (String keyword : unsafeKeywords) { + if (script.contains(keyword)) { + log.warn("脚本包含不安全的关键字: {}", keyword); + return true; + } + } + return false; + } + + /** + * 检查脚本是否匹配不安全的模式 + * + * @param script 脚本内容 + * @return 是否匹配不安全的模式 + */ + private boolean matchesUnsafePatterns(String script) { + if (CollUtil.isEmpty(unsafePatterns)) { + return false; + } + + for (Pattern pattern : unsafePatterns) { + Matcher matcher = pattern.matcher(script); + if (matcher.find()) { + log.warn("脚本匹配到不安全的模式: {}", pattern.pattern()); + return true; + } + } + return false; + } + + /** + * 检查脚本是否包含可能导致高资源消耗的模式 + * + * @param script 脚本内容 + * @return 是否包含高资源消耗模式 + */ + private boolean containsHighResourcePatterns(String script) { + if (CollUtil.isEmpty(highResourceKeywords)) { + return false; + } + + boolean result = false; + for (String pattern : highResourceKeywords) { + if (script.contains(pattern)) { + log.warn("脚本包含高资源消耗模式: {}", pattern); + result = true; + } + } + + // 还要检查递归或嵌套循环模式 + for (Pattern pattern : recursionPatterns) { + Matcher matcher = pattern.matcher(script); + if (matcher.find()) { + log.warn("脚本包含嵌套循环或递归调用: {}", pattern.pattern()); + result = true; + } + } + + return result; + } + + /** + * 分析脚本复杂度 + * + * @param script 脚本内容 + */ + private void analyzeScriptComplexity(String script) { + // 计算循环和条件语句的数量 + int forCount = countOccurrences(script, "for("); + forCount += countOccurrences(script, "for ("); + + int whileCount = countOccurrences(script, "while("); + whileCount += countOccurrences(script, "while ("); + + int doWhileCount = countOccurrences(script, "do{"); + doWhileCount += countOccurrences(script, "do {"); + + int funcCount = countOccurrences(script, "function"); + + // 记录复杂度评估 + if (forCount + whileCount + doWhileCount > 10) { + log.warn("脚本循环结构过多: for={}, while={}, do-while={}", forCount, whileCount, doWhileCount); + } + + if (funcCount > 20) { + log.warn("脚本函数定义过多: {}", funcCount); + } + } + + /** + * 计算字符串出现次数 + * + * @param source 源字符串 + * @param substring 子字符串 + * @return 出现次数 + */ + private int countOccurrences(String source, String substring) { + int count = 0; + int index = 0; + while ((index = source.indexOf(substring, index)) != -1) { + count++; + index += substring.length(); + } + return count; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/ScriptSandbox.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/ScriptSandbox.java new file mode 100644 index 0000000000..3064bec393 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/ScriptSandbox.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.script.sandbox; + +/** + * 脚本沙箱接口,提供脚本安全性验证 + */ +public interface ScriptSandbox { + + /** + * 验证脚本内容是否安全 + * + * @param script 脚本内容 + * @return 脚本是否安全 + */ + boolean validate(String script); + + /** + * 获取沙箱类型 + * + * @return 沙箱类型 + */ + String getType(); +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptService.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptService.java new file mode 100644 index 0000000000..1988e5c151 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptService.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.iot.script.service; + +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; + +import java.util.Map; + +/** + * 脚本服务接口,定义脚本执行的核心功能 + */ +public interface ScriptService { + + /** + * 执行脚本 + * + * @param scriptType 脚本类型(如 js、groovy 等) + * @param script 脚本内容 + * @param context 脚本上下文 + * @return 脚本执行结果 + */ + Object executeScript(String scriptType, String script, ScriptContext context); + + /** + * 执行脚本 + * + * @param scriptType 脚本类型(如 js、groovy 等) + * @param script 脚本内容 + * @param params 脚本参数 + * @return 脚本执行结果 + */ + Object executeScript(String scriptType, String script, Map params); + + /** + * 执行 JavaScript 脚本 + * + * @param script 脚本内容 + * @param context 脚本上下文 + * @return 脚本执行结果 + */ + Object executeJavaScript(String script, ScriptContext context); + + /** + * 执行 JavaScript 脚本 + * + * @param script 脚本内容 + * @param params 脚本参数 + * @return 脚本执行结果 + */ + Object executeJavaScript(String script, Map params); + + /** + * 验证脚本内容是否安全 + * + * @param scriptType 脚本类型 + * @param script 脚本内容 + * @return 脚本是否安全 + */ + boolean validateScript(String scriptType, String script); +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptServiceImpl.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptServiceImpl.java new file mode 100644 index 0000000000..b21136affb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptServiceImpl.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.module.iot.script.service; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext; +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; +import cn.iocoder.yudao.module.iot.script.engine.JsScriptEngine; +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngine; +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory; +import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox; +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * 脚本服务实现类 + */ +@Slf4j +@Service +public class ScriptServiceImpl implements ScriptService { + + @Autowired + private ScriptEngineFactory engineFactory; + + @Override + public Object executeScript(String scriptType, String script, ScriptContext context) { + if (StrUtil.isBlank(scriptType) || StrUtil.isBlank(script)) { + return null; + } + + ScriptEngine engine = engineFactory.getEngine(scriptType); + if (engine == null) { + log.error("找不到脚本引擎: {}", scriptType); + throw new RuntimeException("不支持的脚本类型: " + scriptType); + } + + try { + return engine.execute(script, context); + } catch (Exception e) { + log.error("执行脚本失败: {}", e.getMessage(), e); + throw new RuntimeException("执行脚本失败: " + e.getMessage(), e); + } + } + + @Override + public Object executeScript(String scriptType, String script, Map params) { + ScriptContext context = createContext(params); + return executeScript(scriptType, script, context); + } + + @Override + public Object executeJavaScript(String script, ScriptContext context) { + return executeScript(JsScriptEngine.TYPE, script, context); + } + + @Override + public Object executeJavaScript(String script, Map params) { + return executeScript(JsScriptEngine.TYPE, script, params); + } + + @Override + public boolean validateScript(String scriptType, String script) { + if (StrUtil.isBlank(scriptType) || StrUtil.isBlank(script)) { + return true; + } + + ScriptSandbox sandbox = getSandbox(scriptType); + if (sandbox == null) { + log.warn("找不到对应的脚本沙箱: {}", scriptType); + return false; + } + + try { + return sandbox.validate(script); + } catch (Exception e) { + log.error("验证脚本安全性失败: {}", e.getMessage(), e); + return false; + } + } + + /** + * 根据脚本类型获取对应的沙箱实现 + * + * @param scriptType 脚本类型 + * @return 沙箱实现 + */ + private ScriptSandbox getSandbox(String scriptType) { + if (JsScriptEngine.TYPE.equals(scriptType)) { + return new JsSandbox(); + } + return null; + } + + /** + * 根据参数创建脚本上下文 + * + * @param params 参数 + * @return 脚本上下文 + */ + private ScriptContext createContext(Map params) { + ScriptContext context = new DefaultScriptContext(); + if (MapUtil.isNotEmpty(params)) { + params.forEach(context::setParameter); + } + return context; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/util/ScriptUtils.java b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/util/ScriptUtils.java new file mode 100644 index 0000000000..acf51115f1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/util/ScriptUtils.java @@ -0,0 +1,158 @@ +package cn.iocoder.yudao.module.iot.script.util; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * 脚本工具类,提供给脚本执行环境使用的工具方法 + */ +@Slf4j +public class ScriptUtils { + + /** + * 单例实例 + */ + private static final ScriptUtils INSTANCE = new ScriptUtils(); + + /** + * 获取单例实例 + * + * @return 工具类实例 + */ + public static ScriptUtils getInstance() { + return INSTANCE; + } + + private ScriptUtils() { + // 私有构造函数 + } + + /** + * 字符串是否为空 + * + * @param str 字符串 + * @return 是否为空 + */ + public boolean isEmpty(String str) { + return StrUtil.isEmpty(str); + } + + /** + * 字符串是否不为空 + * + * @param str 字符串 + * @return 是否不为空 + */ + public boolean isNotEmpty(String str) { + return StrUtil.isNotEmpty(str); + } + + /** + * 将对象转为 JSON 字符串 + * + * @param obj 对象 + * @return JSON 字符串 + */ + public String toJson(Object obj) { + return JSONUtil.toJsonStr(obj); + } + + /** + * 将 JSON 字符串转为 Map + * + * @param json JSON 字符串 + * @return Map 对象 + */ + public Map parseJson(String json) { + if (StrUtil.isEmpty(json)) { + return MapUtil.newHashMap(); + } + try { + return JSONUtil.toBean(json, Map.class); + } catch (Exception e) { + log.error("JSON 解析失败: {}", json, e); + return MapUtil.newHashMap(); + } + } + + /** + * 类型转换 + * + * @param value 值 + * @param type 目标类型 + * @param 泛型 + * @return 转换后的值 + */ + public T convert(Object value, Class type) { + return Convert.convert(type, value); + } + + /** + * 从 Map 中获取值 + * + * @param map Map 对象 + * @param key 键 + * @return 值 + */ + public Object get(Map map, String key) { + return MapUtil.get(map, key, Object.class); + } + + /** + * 从 Map 中获取字符串 + * + * @param map Map 对象 + * @param key 键 + * @return 字符串值 + */ + public String getString(Map map, String key) { + return MapUtil.getStr(map, key); + } + + /** + * 从 Map 中获取整数 + * + * @param map Map 对象 + * @param key 键 + * @return 整数值 + */ + public Integer getInt(Map map, String key) { + return MapUtil.getInt(map, key); + } + + /** + * 从 Map 中获取布尔值 + * + * @param map Map 对象 + * @param key 键 + * @return 布尔值 + */ + public Boolean getBool(Map map, String key) { + return MapUtil.getBool(map, key); + } + + /** + * 从 Map 中获取双精度浮点数 + * + * @param map Map 对象 + * @param key 键 + * @return 双精度浮点数值 + */ + public Double getDouble(Map map, String key) { + return MapUtil.getDouble(map, key); + } + + /** + * 获取当前时间戳(毫秒) + * + * @return 时间戳 + */ + public long currentTimeMillis() { + return System.currentTimeMillis(); + } +} \ No newline at end of file