Commit 2954445d347f38820da3f93d5a00a9e1ef814459
1 parent
ae96ff4a
【功能新增】IoT: 新增脚本模块,支持 JavaScript 引擎解析,提供示例脚本和安全性验证功能,更新相关依赖和配置。
Showing
24 changed files
with
2125 additions
and
105 deletions
yudao-module-iot/pom.xml
| @@ -11,6 +11,7 @@ | @@ -11,6 +11,7 @@ | ||
| 11 | <module>yudao-module-iot-api</module> | 11 | <module>yudao-module-iot-api</module> |
| 12 | <module>yudao-module-iot-biz</module> | 12 | <module>yudao-module-iot-biz</module> |
| 13 | <module>yudao-module-iot-net-components</module> | 13 | <module>yudao-module-iot-net-components</module> |
| 14 | + <module>yudao-module-iot-script</module> | ||
| 14 | <!-- <module>yudao-module-iot-plugins</module>--> | 15 | <!-- <module>yudao-module-iot-plugins</module>--> |
| 15 | </modules> | 16 | </modules> |
| 16 | <modelVersion>4.0.0</modelVersion> | 17 | <modelVersion>4.0.0</modelVersion> |
yudao-module-iot/yudao-module-iot-api/pom.xml
| @@ -37,10 +37,10 @@ | @@ -37,10 +37,10 @@ | ||
| 37 | <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> | 37 | <scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
| 38 | </dependency> | 38 | </dependency> |
| 39 | 39 | ||
| 40 | - <dependency> | ||
| 41 | - <groupId>org.pf4j</groupId> <!-- PF4J:内置插件机制 --> | ||
| 42 | - <artifactId>pf4j-spring</artifactId> | ||
| 43 | - </dependency> | 40 | + <!-- <dependency>--> |
| 41 | + <!-- <groupId>org.pf4j</groupId> <!– PF4J:内置插件机制 –>--> | ||
| 42 | + <!-- <artifactId>pf4j-spring</artifactId>--> | ||
| 43 | + <!-- </dependency>--> | ||
| 44 | 44 | ||
| 45 | <!-- 参数校验 --> | 45 | <!-- 参数校验 --> |
| 46 | <dependency> | 46 | <dependency> |
yudao-module-iot/yudao-module-iot-biz/pom.xml
| @@ -69,13 +69,6 @@ | @@ -69,13 +69,6 @@ | ||
| 69 | <artifactId>yudao-spring-boot-starter-excel</artifactId> | 69 | <artifactId>yudao-spring-boot-starter-excel</artifactId> |
| 70 | </dependency> | 70 | </dependency> |
| 71 | 71 | ||
| 72 | - <!-- 脚本插件相关 --> | ||
| 73 | - <!-- <dependency>--> | ||
| 74 | - <!-- <groupId>cn.iocoder.boot</groupId>--> | ||
| 75 | - <!-- <artifactId>yudao-module-iot-plugin-script</artifactId>--> | ||
| 76 | - <!-- <version>${revision}</version>--> | ||
| 77 | - <!-- </dependency>--> | ||
| 78 | - | ||
| 79 | <!-- 消息队列相关 --> | 72 | <!-- 消息队列相关 --> |
| 80 | <dependency> | 73 | <dependency> |
| 81 | <groupId>org.apache.rocketmq</groupId> | 74 | <groupId>org.apache.rocketmq</groupId> |
| @@ -93,11 +86,6 @@ | @@ -93,11 +86,6 @@ | ||
| 93 | <optional>true</optional> | 86 | <optional>true</optional> |
| 94 | </dependency> | 87 | </dependency> |
| 95 | 88 | ||
| 96 | - <!-- <dependency>--> | ||
| 97 | - <!-- <groupId>org.pf4j</groupId> <!– PF4J:内置插件机制 –>--> | ||
| 98 | - <!-- <artifactId>pf4j-spring</artifactId>--> | ||
| 99 | - <!-- </dependency>--> | ||
| 100 | - | ||
| 101 | <!-- TODO @芋艿:bom 管理 --> | 89 | <!-- TODO @芋艿:bom 管理 --> |
| 102 | <dependency> | 90 | <dependency> |
| 103 | <groupId>org.apache.groovy</groupId> | 91 | <groupId>org.apache.groovy</groupId> |
| @@ -145,6 +133,15 @@ | @@ -145,6 +133,15 @@ | ||
| 145 | <artifactId>yudao-module-iot-net-component-emqx</artifactId> | 133 | <artifactId>yudao-module-iot-net-component-emqx</artifactId> |
| 146 | <version>${revision}</version> | 134 | <version>${revision}</version> |
| 147 | </dependency> | 135 | </dependency> |
| 136 | + | ||
| 137 | + <!-- 脚本相关 --> | ||
| 138 | + <dependency> | ||
| 139 | + <groupId>cn.iocoder.boot</groupId> | ||
| 140 | + <artifactId>yudao-module-iot-script</artifactId> | ||
| 141 | + <version>${revision}</version> | ||
| 142 | + </dependency> | ||
| 143 | + | ||
| 144 | + | ||
| 148 | </dependencies> | 145 | </dependencies> |
| 149 | 146 | ||
| 150 | </project> | 147 | </project> |
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; | @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; | ||
| 5 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 5 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 6 | import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.*; | 6 | import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.*; |
| 7 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO; | 7 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO; |
| 8 | +import cn.iocoder.yudao.module.iot.script.example.ProductScriptSamples; | ||
| 8 | import cn.iocoder.yudao.module.iot.service.product.IotProductScriptService; | 9 | import cn.iocoder.yudao.module.iot.service.product.IotProductScriptService; |
| 9 | import io.swagger.v3.oas.annotations.Operation; | 10 | import io.swagger.v3.oas.annotations.Operation; |
| 10 | import io.swagger.v3.oas.annotations.Parameter; | 11 | import io.swagger.v3.oas.annotations.Parameter; |
| @@ -28,6 +29,9 @@ public class IotProductScriptController { | @@ -28,6 +29,9 @@ public class IotProductScriptController { | ||
| 28 | @Resource | 29 | @Resource |
| 29 | private IotProductScriptService productScriptService; | 30 | private IotProductScriptService productScriptService; |
| 30 | 31 | ||
| 32 | + @Resource | ||
| 33 | + private ProductScriptSamples scriptSamples; | ||
| 34 | + | ||
| 31 | @PostMapping("/create") | 35 | @PostMapping("/create") |
| 32 | @Operation(summary = "创建产品脚本") | 36 | @Operation(summary = "创建产品脚本") |
| 33 | @PreAuthorize("@ss.hasPermission('iot:product-script:create')") | 37 | @PreAuthorize("@ss.hasPermission('iot:product-script:create')") |
| @@ -96,4 +100,26 @@ public class IotProductScriptController { | @@ -96,4 +100,26 @@ public class IotProductScriptController { | ||
| 96 | productScriptService.updateProductScriptStatus(updateStatusReqVO.getId(), updateStatusReqVO.getStatus()); | 100 | productScriptService.updateProductScriptStatus(updateStatusReqVO.getId(), updateStatusReqVO.getStatus()); |
| 97 | return success(true); | 101 | return success(true); |
| 98 | } | 102 | } |
| 103 | + | ||
| 104 | + @GetMapping("/sample") | ||
| 105 | + @Operation(summary = "获取示例脚本") | ||
| 106 | + @Parameter(name = "type", description = "脚本类型(1=属性解析, 2=事件解析, 3=命令编码)", required = true, example = "1") | ||
| 107 | + @PreAuthorize("@ss.hasPermission('iot:product-script:query')") | ||
| 108 | + public CommonResult<String> getSampleScript(@RequestParam("type") Integer type) { | ||
| 109 | + String sample; | ||
| 110 | + switch (type) { | ||
| 111 | + case 1: | ||
| 112 | + sample = scriptSamples.getPropertyParserSample(); | ||
| 113 | + break; | ||
| 114 | + case 2: | ||
| 115 | + sample = scriptSamples.getEventParserSample(); | ||
| 116 | + break; | ||
| 117 | + case 3: | ||
| 118 | + sample = scriptSamples.getCommandEncoderSample(); | ||
| 119 | + break; | ||
| 120 | + default: | ||
| 121 | + sample = "// 不支持的脚本类型"; | ||
| 122 | + } | ||
| 123 | + return success(sample); | ||
| 124 | + } | ||
| 99 | } | 125 | } |
| 100 | \ No newline at end of file | 126 | \ No newline at end of file |
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 | @@ -9,13 +9,18 @@ import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProduct | ||
| 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | 9 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 10 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO; | 10 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO; |
| 11 | import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductScriptMapper; | 11 | import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductScriptMapper; |
| 12 | +import cn.iocoder.yudao.module.iot.script.context.DeviceScriptContext; | ||
| 13 | +import cn.iocoder.yudao.module.iot.script.service.ScriptService; | ||
| 12 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | 14 | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| 13 | import jakarta.annotation.Resource; | 15 | import jakarta.annotation.Resource; |
| 14 | import lombok.extern.slf4j.Slf4j; | 16 | import lombok.extern.slf4j.Slf4j; |
| 15 | import org.springframework.stereotype.Service; | 17 | import org.springframework.stereotype.Service; |
| 16 | import org.springframework.validation.annotation.Validated; | 18 | import org.springframework.validation.annotation.Validated; |
| 17 | 19 | ||
| 20 | +import java.time.LocalDateTime; | ||
| 21 | +import java.util.HashMap; | ||
| 18 | import java.util.List; | 22 | import java.util.List; |
| 23 | +import java.util.Map; | ||
| 19 | 24 | ||
| 20 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 25 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 21 | import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS; | 26 | import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS; |
| @@ -38,8 +43,8 @@ public class IotProductScriptServiceImpl implements IotProductScriptService { | @@ -38,8 +43,8 @@ public class IotProductScriptServiceImpl implements IotProductScriptService { | ||
| 38 | @Resource | 43 | @Resource |
| 39 | private IotProductService productService; | 44 | private IotProductService productService; |
| 40 | 45 | ||
| 41 | -// @Resource | ||
| 42 | -// private ScriptService scriptService; | 46 | + @Resource |
| 47 | + private ScriptService scriptService; | ||
| 43 | 48 | ||
| 44 | @Override | 49 | @Override |
| 45 | public Long createProductScript(IotProductScriptSaveReqVO createReqVO) { | 50 | public Long createProductScript(IotProductScriptSaveReqVO createReqVO) { |
| @@ -116,90 +121,103 @@ public class IotProductScriptServiceImpl implements IotProductScriptService { | @@ -116,90 +121,103 @@ public class IotProductScriptServiceImpl implements IotProductScriptService { | ||
| 116 | 121 | ||
| 117 | @Override | 122 | @Override |
| 118 | public IotProductScriptTestRespVO testProductScript(IotProductScriptTestReqVO testReqVO) { | 123 | public IotProductScriptTestRespVO testProductScript(IotProductScriptTestReqVO testReqVO) { |
| 119 | -// long startTime = System.currentTimeMillis(); | ||
| 120 | -// | ||
| 121 | -// try { | ||
| 122 | -// // 验证产品是否存在 | ||
| 123 | -// validateProductExists(testReqVO.getProductId()); | ||
| 124 | -// | ||
| 125 | -// // 根据ID获取已保存的脚本(如果有) | ||
| 126 | -// IotProductScriptDO existingScript = null; | ||
| 127 | -// if (testReqVO.getId() != null) { | ||
| 128 | -// existingScript = getProductScript(testReqVO.getId()); | ||
| 129 | -// } | ||
| 130 | -// | ||
| 131 | -// // 创建测试上下文 | ||
| 132 | -// PluginScriptContext context = new PluginScriptContext(); | ||
| 133 | -// IotProductDO product = productService.getProduct(testReqVO.getProductId()); | ||
| 134 | -// | ||
| 135 | -// // 设置设备上下文(使用产品信息,没有具体设备) | ||
| 136 | -// context.withDeviceContext(product.getProductKey(), null); | ||
| 137 | -// | ||
| 138 | -// // 设置输入参数 | ||
| 139 | -// Map<String, Object> params = new HashMap<>(); | ||
| 140 | -// params.put("input", testReqVO.getTestInput()); | ||
| 141 | -// params.put("productKey", product.getProductKey()); | ||
| 142 | -// params.put("scriptType", testReqVO.getScriptType()); | ||
| 143 | -// | ||
| 144 | -// // 根据脚本类型设置特定参数 | ||
| 145 | -// switch (testReqVO.getScriptType()) { | ||
| 146 | -// case 1: // PROPERTY_PARSER | ||
| 147 | -// params.put("method", "property"); | ||
| 148 | -// break; | ||
| 149 | -// case 2: // EVENT_PARSER | ||
| 150 | -// params.put("method", "event"); | ||
| 151 | -// params.put("identifier", "default"); | ||
| 152 | -// break; | ||
| 153 | -// case 3: // COMMAND_ENCODER | ||
| 154 | -// params.put("method", "command"); | ||
| 155 | -// break; | ||
| 156 | -// default: | ||
| 157 | -// // 默认不添加额外参数 | ||
| 158 | -// } | ||
| 159 | -// | ||
| 160 | -// // 添加所有参数到上下文 | ||
| 161 | -// for (Map.Entry<String, Object> entry : params.entrySet()) { | ||
| 162 | -// context.setParameter(entry.getKey(), entry.getValue()); | ||
| 163 | -// } | ||
| 164 | -// | ||
| 165 | -// // 执行脚本 | ||
| 166 | -// Object result = scriptService.executeScript( | ||
| 167 | -// testReqVO.getScriptLanguage(), | ||
| 168 | -// testReqVO.getScriptContent(), | ||
| 169 | -// context); | ||
| 170 | -// | ||
| 171 | -// // 更新测试结果(如果是已保存的脚本) | ||
| 172 | -// if (existingScript != null) { | ||
| 173 | -// IotProductScriptDO updateObj = new IotProductScriptDO(); | ||
| 174 | -// updateObj.setId(existingScript.getId()); | ||
| 175 | -// updateObj.setLastTestTime(LocalDateTime.now()); | ||
| 176 | -// updateObj.setLastTestResult(1); // 1表示成功 | ||
| 177 | -// productScriptMapper.updateById(updateObj); | ||
| 178 | -// } | ||
| 179 | -// | ||
| 180 | -// long executionTime = System.currentTimeMillis() - startTime; | ||
| 181 | -// return IotProductScriptTestRespVO.success(result, executionTime); | ||
| 182 | -// | ||
| 183 | -// } catch (Exception e) { | ||
| 184 | -// log.error("[testProductScript][测试脚本异常]", e); | ||
| 185 | -// | ||
| 186 | -// // 如果是已保存的脚本,更新测试失败状态 | ||
| 187 | -// if (testReqVO.getId() != null) { | ||
| 188 | -// try { | ||
| 189 | -// IotProductScriptDO updateObj = new IotProductScriptDO(); | ||
| 190 | -// updateObj.setId(testReqVO.getId()); | ||
| 191 | -// updateObj.setLastTestTime(LocalDateTime.now()); | ||
| 192 | -// updateObj.setLastTestResult(0); // 0表示失败 | ||
| 193 | -// productScriptMapper.updateById(updateObj); | ||
| 194 | -// } catch (Exception ex) { | ||
| 195 | -// log.error("[testProductScript][更新脚本测试结果异常]", ex); | ||
| 196 | -// } | ||
| 197 | -// } | ||
| 198 | -// | ||
| 199 | -// long executionTime = System.currentTimeMillis() - startTime; | ||
| 200 | -// return IotProductScriptTestRespVO.error(e.getMessage(), executionTime); | ||
| 201 | -// } | ||
| 202 | - return null; | 124 | + long startTime = System.currentTimeMillis(); |
| 125 | + | ||
| 126 | + try { | ||
| 127 | + // 验证产品是否存在 | ||
| 128 | + validateProductExists(testReqVO.getProductId()); | ||
| 129 | + | ||
| 130 | + // 根据ID获取已保存的脚本(如果有) | ||
| 131 | + IotProductScriptDO existingScript = null; | ||
| 132 | + if (testReqVO.getId() != null) { | ||
| 133 | + existingScript = getProductScript(testReqVO.getId()); | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + // 创建测试上下文 | ||
| 137 | + IotProductDO product = productService.getProduct(testReqVO.getProductId()); | ||
| 138 | + DeviceScriptContext context = new DeviceScriptContext(); | ||
| 139 | + | ||
| 140 | + // 设置设备上下文(使用产品信息,测试时无具体设备) | ||
| 141 | + context.withDeviceInfo(product.getProductKey(), null); | ||
| 142 | + | ||
| 143 | + // 设置输入参数 | ||
| 144 | + Map<String, Object> params = new HashMap<>(); | ||
| 145 | + params.put("input", testReqVO.getTestInput()); | ||
| 146 | + params.put("productKey", product.getProductKey()); | ||
| 147 | + params.put("scriptType", testReqVO.getScriptType()); | ||
| 148 | + | ||
| 149 | + // 根据脚本类型设置特定参数 | ||
| 150 | + switch (testReqVO.getScriptType()) { | ||
| 151 | + case 1: // PROPERTY_PARSER | ||
| 152 | + params.put("method", "property"); | ||
| 153 | + // 添加一些模拟的属性数据 | ||
| 154 | + Map<String, Object> properties = new HashMap<>(); | ||
| 155 | + properties.put("temp", 25.5); | ||
| 156 | + properties.put("humidity", 60); | ||
| 157 | + context.withProperties(properties); | ||
| 158 | + break; | ||
| 159 | + case 2: // EVENT_PARSER | ||
| 160 | + params.put("method", "event"); | ||
| 161 | + params.put("identifier", "default"); | ||
| 162 | + // 添加事件数据 | ||
| 163 | + Map<String, Object> eventParams = new HashMap<>(); | ||
| 164 | + eventParams.put("timestamp", System.currentTimeMillis()); | ||
| 165 | + params.put("eventParams", eventParams); | ||
| 166 | + break; | ||
| 167 | + case 3: // COMMAND_ENCODER | ||
| 168 | + params.put("method", "command"); | ||
| 169 | + // 添加命令参数 | ||
| 170 | + Map<String, Object> cmdParams = new HashMap<>(); | ||
| 171 | + cmdParams.put("cmdName", "setValue"); | ||
| 172 | + cmdParams.put("cmdValue", 100); | ||
| 173 | + params.put("cmdParams", cmdParams); | ||
| 174 | + break; | ||
| 175 | + default: | ||
| 176 | + // 默认不添加额外参数 | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + // 添加所有参数到上下文 | ||
| 180 | + for (Map.Entry<String, Object> entry : params.entrySet()) { | ||
| 181 | + context.setParameter(entry.getKey(), entry.getValue()); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + // 执行脚本 | ||
| 185 | + Object result = scriptService.executeScript( | ||
| 186 | + testReqVO.getScriptLanguage(), | ||
| 187 | + testReqVO.getScriptContent(), | ||
| 188 | + context); | ||
| 189 | + | ||
| 190 | + // 更新测试结果(如果是已保存的脚本) | ||
| 191 | + if (existingScript != null) { | ||
| 192 | + IotProductScriptDO updateObj = new IotProductScriptDO(); | ||
| 193 | + updateObj.setId(existingScript.getId()); | ||
| 194 | + updateObj.setLastTestTime(LocalDateTime.now()); | ||
| 195 | + updateObj.setLastTestResult(1); // 1表示成功 | ||
| 196 | + productScriptMapper.updateById(updateObj); | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + long executionTime = System.currentTimeMillis() - startTime; | ||
| 200 | + return IotProductScriptTestRespVO.success(result, executionTime); | ||
| 201 | + | ||
| 202 | + } catch (Exception e) { | ||
| 203 | + log.error("[testProductScript][测试脚本异常]", e); | ||
| 204 | + | ||
| 205 | + // 如果是已保存的脚本,更新测试失败状态 | ||
| 206 | + if (testReqVO.getId() != null) { | ||
| 207 | + try { | ||
| 208 | + IotProductScriptDO updateObj = new IotProductScriptDO(); | ||
| 209 | + updateObj.setId(testReqVO.getId()); | ||
| 210 | + updateObj.setLastTestTime(LocalDateTime.now()); | ||
| 211 | + updateObj.setLastTestResult(0); // 0表示失败 | ||
| 212 | + productScriptMapper.updateById(updateObj); | ||
| 213 | + } catch (Exception ex) { | ||
| 214 | + log.error("[testProductScript][更新脚本测试结果异常]", ex); | ||
| 215 | + } | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + long executionTime = System.currentTimeMillis() - startTime; | ||
| 219 | + return IotProductScriptTestRespVO.error(e.getMessage(), executionTime); | ||
| 220 | + } | ||
| 203 | } | 221 | } |
| 204 | 222 | ||
| 205 | @Override | 223 | @Override |
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; | @@ -3,10 +3,8 @@ package cn.iocoder.yudao.module.iot.net.component.core.upstream; | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; | 4 | import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; |
| 5 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; | 5 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; |
| 6 | +import jakarta.annotation.Resource; | ||
| 6 | import lombok.extern.slf4j.Slf4j; | 7 | import lombok.extern.slf4j.Slf4j; |
| 7 | - | ||
| 8 | -import javax.annotation.Resource; | ||
| 9 | - | ||
| 10 | /** | 8 | /** |
| 11 | * 设备数据 Upstream 上行客户端 | 9 | * 设备数据 Upstream 上行客户端 |
| 12 | * <p> | 10 | * <p> |
yudao-module-iot/yudao-module-iot-script/pom.xml
0 → 100644
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | +<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
| 3 | + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 4 | + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| 5 | + <parent> | ||
| 6 | + <artifactId>yudao-module-iot</artifactId> | ||
| 7 | + <groupId>cn.iocoder.boot</groupId> | ||
| 8 | + <version>${revision}</version> | ||
| 9 | + </parent> | ||
| 10 | + <modelVersion>4.0.0</modelVersion> | ||
| 11 | + <artifactId>yudao-module-iot-script</artifactId> | ||
| 12 | + <packaging>jar</packaging> | ||
| 13 | + | ||
| 14 | + <name>${project.artifactId}</name> | ||
| 15 | + <description>IoT 脚本模块,提供 JavaScript 引擎解析等功能</description> | ||
| 16 | + | ||
| 17 | + <dependencies> | ||
| 18 | + <!-- 引入公共模块 --> | ||
| 19 | + <dependency> | ||
| 20 | + <groupId>cn.iocoder.boot</groupId> | ||
| 21 | + <artifactId>yudao-module-iot-api</artifactId> | ||
| 22 | + <version>${revision}</version> | ||
| 23 | + </dependency> | ||
| 24 | + | ||
| 25 | + <!-- Spring 相关依赖 --> | ||
| 26 | + <dependency> | ||
| 27 | + <groupId>org.springframework</groupId> | ||
| 28 | + <artifactId>spring-context</artifactId> | ||
| 29 | + </dependency> | ||
| 30 | + | ||
| 31 | + <!-- 工具类相关 --> | ||
| 32 | + <dependency> | ||
| 33 | + <groupId>cn.hutool</groupId> | ||
| 34 | + <artifactId>hutool-all</artifactId> | ||
| 35 | + </dependency> | ||
| 36 | + <dependency> | ||
| 37 | + <groupId>org.projectlombok</groupId> | ||
| 38 | + <artifactId>lombok</artifactId> | ||
| 39 | + <optional>true</optional> | ||
| 40 | + </dependency> | ||
| 41 | + <dependency> | ||
| 42 | + <groupId>org.slf4j</groupId> | ||
| 43 | + <artifactId>slf4j-api</artifactId> | ||
| 44 | + </dependency> | ||
| 45 | + | ||
| 46 | + <!-- JavaScript 引擎 - 使用 GraalJS 替代 Nashorn --> | ||
| 47 | + <dependency> | ||
| 48 | + <groupId>org.graalvm.sdk</groupId> | ||
| 49 | + <artifactId>graal-sdk</artifactId> | ||
| 50 | + <version>22.3.0</version> | ||
| 51 | + </dependency> | ||
| 52 | + <dependency> | ||
| 53 | + <groupId>org.graalvm.js</groupId> | ||
| 54 | + <artifactId>js</artifactId> | ||
| 55 | + <version>22.3.0</version> | ||
| 56 | + </dependency> | ||
| 57 | + <dependency> | ||
| 58 | + <groupId>org.graalvm.js</groupId> | ||
| 59 | + <artifactId>js-scriptengine</artifactId> | ||
| 60 | + <version>22.3.0</version> | ||
| 61 | + </dependency> | ||
| 62 | + | ||
| 63 | + <!-- 测试相关 --> | ||
| 64 | + <dependency> | ||
| 65 | + <groupId>org.springframework.boot</groupId> | ||
| 66 | + <artifactId>spring-boot-starter-test</artifactId> | ||
| 67 | + <scope>test</scope> | ||
| 68 | + <exclusions> | ||
| 69 | + <exclusion> | ||
| 70 | + <groupId>org.junit.vintage</groupId> | ||
| 71 | + <artifactId>junit-vintage-engine</artifactId> | ||
| 72 | + </exclusion> | ||
| 73 | + </exclusions> | ||
| 74 | + </dependency> | ||
| 75 | + <dependency> | ||
| 76 | + <groupId>cn.iocoder.boot</groupId> | ||
| 77 | + <artifactId>yudao-spring-boot-starter-test</artifactId> | ||
| 78 | + <version>${revision}</version> | ||
| 79 | + <scope>test</scope> | ||
| 80 | + </dependency> | ||
| 81 | + <dependency> | ||
| 82 | + <groupId>org.mockito</groupId> | ||
| 83 | + <artifactId>mockito-core</artifactId> | ||
| 84 | + <scope>test</scope> | ||
| 85 | + </dependency> | ||
| 86 | + <dependency> | ||
| 87 | + <groupId>org.mockito</groupId> | ||
| 88 | + <artifactId>mockito-junit-jupiter</artifactId> | ||
| 89 | + <scope>test</scope> | ||
| 90 | + </dependency> | ||
| 91 | + </dependencies> | ||
| 92 | + | ||
| 93 | +</project> |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/ScriptExample.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.map.MapUtil; | ||
| 4 | +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext; | ||
| 5 | +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; | ||
| 6 | +import cn.iocoder.yudao.module.iot.script.service.ScriptService; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 9 | +import org.springframework.stereotype.Component; | ||
| 10 | + | ||
| 11 | +import java.util.Map; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * 脚本使用示例类 | ||
| 15 | + */ | ||
| 16 | +@Slf4j | ||
| 17 | +@Component | ||
| 18 | +public class ScriptExample { | ||
| 19 | + | ||
| 20 | + @Autowired | ||
| 21 | + private ScriptService scriptService; | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 执行简单的 JavaScript 脚本 | ||
| 25 | + * | ||
| 26 | + * @return 执行结果 | ||
| 27 | + */ | ||
| 28 | + public Object executeSimpleScript() { | ||
| 29 | + // 简单的脚本内容 | ||
| 30 | + String script = "var result = a + b; result;"; | ||
| 31 | + | ||
| 32 | + // 创建参数 | ||
| 33 | + Map<String, Object> params = MapUtil.newHashMap(); | ||
| 34 | + params.put("a", 10); | ||
| 35 | + params.put("b", 20); | ||
| 36 | + | ||
| 37 | + // 执行脚本 | ||
| 38 | + return scriptService.executeJavaScript(script, params); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 执行包含函数的 JavaScript 脚本 | ||
| 43 | + * | ||
| 44 | + * @return 执行结果 | ||
| 45 | + */ | ||
| 46 | + public Object executeScriptWithFunction() { | ||
| 47 | + // 包含函数的脚本内容 | ||
| 48 | + String script = "function calc(x, y) { return x * y; } calc(a, b);"; | ||
| 49 | + | ||
| 50 | + // 创建上下文 | ||
| 51 | + ScriptContext context = new DefaultScriptContext(); | ||
| 52 | + context.setParameter("a", 5); | ||
| 53 | + context.setParameter("b", 6); | ||
| 54 | + | ||
| 55 | + // 执行脚本 | ||
| 56 | + return scriptService.executeJavaScript(script, context); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + /** | ||
| 60 | + * 执行包含工具类使用的脚本 | ||
| 61 | + * | ||
| 62 | + * @return 执行结果 | ||
| 63 | + */ | ||
| 64 | + public Object executeScriptWithUtils() { | ||
| 65 | + // 使用工具类的脚本内容 | ||
| 66 | + String script = "var data = {name: 'test', value: 123}; utils.toJson(data);"; | ||
| 67 | + | ||
| 68 | + // 执行脚本 | ||
| 69 | + return scriptService.executeJavaScript(script, MapUtil.newHashMap()); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * 执行包含日志输出的脚本 | ||
| 74 | + * | ||
| 75 | + * @return 执行结果 | ||
| 76 | + */ | ||
| 77 | + public Object executeScriptWithLogging() { | ||
| 78 | + // 包含日志输出的脚本内容 | ||
| 79 | + String script = "log.info('脚本开始执行...'); " + | ||
| 80 | + "var result = a + b; " + | ||
| 81 | + "log.info('计算结果: ' + result); " + | ||
| 82 | + "result;"; | ||
| 83 | + | ||
| 84 | + // 创建参数 | ||
| 85 | + Map<String, Object> params = MapUtil.newHashMap(); | ||
| 86 | + params.put("a", 100); | ||
| 87 | + params.put("b", 200); | ||
| 88 | + | ||
| 89 | + // 执行脚本 | ||
| 90 | + return scriptService.executeJavaScript(script, params); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + /** | ||
| 94 | + * 演示脚本安全性验证 | ||
| 95 | + * | ||
| 96 | + * @return 是否安全 | ||
| 97 | + */ | ||
| 98 | + public boolean validateScriptSecurity() { | ||
| 99 | + // 安全的脚本 | ||
| 100 | + String safeScript = "var x = 10; var y = 20; x + y;"; | ||
| 101 | + boolean safeResult = scriptService.validateScript("js", safeScript); | ||
| 102 | + | ||
| 103 | + // 不安全的脚本 | ||
| 104 | + String unsafeScript = "java.lang.System.exit(0);"; | ||
| 105 | + boolean unsafeResult = scriptService.validateScript("js", unsafeScript); | ||
| 106 | + | ||
| 107 | + log.info("安全脚本验证结果: {}", safeResult); | ||
| 108 | + log.info("不安全脚本验证结果: {}", unsafeResult); | ||
| 109 | + | ||
| 110 | + return safeResult && !unsafeResult; | ||
| 111 | + } | ||
| 112 | +} | ||
| 0 | \ No newline at end of file | 113 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/config/ScriptConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.config; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory; | ||
| 4 | +import org.springframework.context.annotation.Bean; | ||
| 5 | +import org.springframework.context.annotation.Configuration; | ||
| 6 | +import org.springframework.context.annotation.Primary; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 脚本模块配置类 | ||
| 10 | + */ | ||
| 11 | +@Configuration | ||
| 12 | +public class ScriptConfiguration { | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * 创建脚本引擎工厂 | ||
| 16 | + * | ||
| 17 | + * @return 脚本引擎工厂 | ||
| 18 | + */ | ||
| 19 | + @Bean | ||
| 20 | + @Primary | ||
| 21 | + public ScriptEngineFactory scriptEngineFactory() { | ||
| 22 | + return new ScriptEngineFactory(); | ||
| 23 | + } | ||
| 24 | +} | ||
| 0 | \ No newline at end of file | 25 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DefaultScriptContext.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.context; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.map.MapUtil; | ||
| 4 | + | ||
| 5 | +import java.util.Map; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 默认脚本上下文实现 | ||
| 9 | + */ | ||
| 10 | +public class DefaultScriptContext implements ScriptContext { | ||
| 11 | + | ||
| 12 | + /** | ||
| 13 | + * 上下文参数 | ||
| 14 | + */ | ||
| 15 | + private final Map<String, Object> parameters = MapUtil.newHashMap(); | ||
| 16 | + | ||
| 17 | + /** | ||
| 18 | + * 上下文函数 | ||
| 19 | + */ | ||
| 20 | + private final Map<String, Object> functions = MapUtil.newHashMap(); | ||
| 21 | + | ||
| 22 | + @Override | ||
| 23 | + public Map<String, Object> getParameters() { | ||
| 24 | + return parameters; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + @Override | ||
| 28 | + public Map<String, Object> getFunctions() { | ||
| 29 | + return functions; | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + @Override | ||
| 33 | + public void setParameter(String key, Object value) { | ||
| 34 | + parameters.put(key, value); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + @Override | ||
| 38 | + public Object getParameter(String key) { | ||
| 39 | + return parameters.get(key); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + @Override | ||
| 43 | + public void registerFunction(String name, Object function) { | ||
| 44 | + functions.put(name, function); | ||
| 45 | + } | ||
| 46 | +} | ||
| 0 | \ No newline at end of file | 47 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/DeviceScriptContext.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.context; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.map.MapUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import lombok.Getter; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | + | ||
| 8 | +import java.util.Map; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 设备脚本上下文,提供设备相关的上下文信息 | ||
| 12 | + */ | ||
| 13 | +@Slf4j | ||
| 14 | +public class DeviceScriptContext extends DefaultScriptContext { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 产品 Key | ||
| 18 | + */ | ||
| 19 | + @Getter | ||
| 20 | + private String productKey; | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 设备名称 | ||
| 24 | + */ | ||
| 25 | + @Getter | ||
| 26 | + private String deviceName; | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 设备属性数据缓存 | ||
| 30 | + */ | ||
| 31 | + private Map<String, Object> properties; | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 使用产品 Key 和设备名称初始化上下文 | ||
| 35 | + * | ||
| 36 | + * @param productKey 产品 Key | ||
| 37 | + * @param deviceName 设备名称,可以为 null | ||
| 38 | + * @return 当前上下文实例,用于链式调用 | ||
| 39 | + */ | ||
| 40 | + public DeviceScriptContext withDeviceInfo(String productKey, String deviceName) { | ||
| 41 | + this.productKey = productKey; | ||
| 42 | + this.deviceName = deviceName; | ||
| 43 | + | ||
| 44 | + // 添加到参数中,便于脚本访问 | ||
| 45 | + setParameter("productKey", productKey); | ||
| 46 | + if (StrUtil.isNotEmpty(deviceName)) { | ||
| 47 | + setParameter("deviceName", deviceName); | ||
| 48 | + } | ||
| 49 | + return this; | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * 设置设备属性数据 | ||
| 54 | + * | ||
| 55 | + * @param properties 属性数据 | ||
| 56 | + * @return 当前上下文实例,用于链式调用 | ||
| 57 | + */ | ||
| 58 | + public DeviceScriptContext withProperties(Map<String, Object> properties) { | ||
| 59 | + this.properties = properties; | ||
| 60 | + if (MapUtil.isNotEmpty(properties)) { | ||
| 61 | + setParameter("properties", properties); | ||
| 62 | + } | ||
| 63 | + return this; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * 获取设备属性值 | ||
| 68 | + * | ||
| 69 | + * @param key 属性标识符 | ||
| 70 | + * @return 属性值 | ||
| 71 | + */ | ||
| 72 | + public Object getProperty(String key) { | ||
| 73 | + if (MapUtil.isEmpty(properties)) { | ||
| 74 | + return null; | ||
| 75 | + } | ||
| 76 | + return properties.get(key); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + /** | ||
| 80 | + * 设置设备属性值 | ||
| 81 | + * | ||
| 82 | + * @param key 属性标识符 | ||
| 83 | + * @param value 属性值 | ||
| 84 | + */ | ||
| 85 | + public void setProperty(String key, Object value) { | ||
| 86 | + if (this.properties == null) { | ||
| 87 | + this.properties = MapUtil.newHashMap(); | ||
| 88 | + setParameter("properties", this.properties); | ||
| 89 | + } | ||
| 90 | + this.properties.put(key, value); | ||
| 91 | + } | ||
| 92 | +} | ||
| 0 | \ No newline at end of file | 93 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/context/ScriptContext.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.context; | ||
| 2 | + | ||
| 3 | +import java.util.Map; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 脚本上下文接口,定义脚本执行所需的上下文环境 | ||
| 7 | + */ | ||
| 8 | +public interface ScriptContext { | ||
| 9 | + | ||
| 10 | + /** | ||
| 11 | + * 获取上下文参数 | ||
| 12 | + * | ||
| 13 | + * @return 上下文参数 | ||
| 14 | + */ | ||
| 15 | + Map<String, Object> getParameters(); | ||
| 16 | + | ||
| 17 | + /** | ||
| 18 | + * 获取上下文函数 | ||
| 19 | + * | ||
| 20 | + * @return 上下文函数 | ||
| 21 | + */ | ||
| 22 | + Map<String, Object> getFunctions(); | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 设置上下文参数 | ||
| 26 | + * | ||
| 27 | + * @param key 参数名 | ||
| 28 | + * @param value 参数值 | ||
| 29 | + */ | ||
| 30 | + void setParameter(String key, Object value); | ||
| 31 | + | ||
| 32 | + /** | ||
| 33 | + * 获取上下文参数 | ||
| 34 | + * | ||
| 35 | + * @param key 参数名 | ||
| 36 | + * @return 参数值 | ||
| 37 | + */ | ||
| 38 | + Object getParameter(String key); | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 注册函数 | ||
| 42 | + * | ||
| 43 | + * @param name 函数名称 | ||
| 44 | + * @param function 函数对象 | ||
| 45 | + */ | ||
| 46 | + void registerFunction(String name, Object function); | ||
| 47 | +} | ||
| 0 | \ No newline at end of file | 48 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/AbstractScriptEngine.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.engine; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; | ||
| 4 | +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox; | ||
| 5 | +import lombok.extern.slf4j.Slf4j; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 抽象脚本引擎,提供脚本引擎的基本框架 | ||
| 9 | + */ | ||
| 10 | +@Slf4j | ||
| 11 | +public abstract class AbstractScriptEngine implements ScriptEngine { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 脚本沙箱,用于提供安全执行环境 | ||
| 15 | + */ | ||
| 16 | + protected final ScriptSandbox sandbox; | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 构造函数 | ||
| 20 | + * | ||
| 21 | + * @param sandbox 脚本沙箱 | ||
| 22 | + */ | ||
| 23 | + protected AbstractScriptEngine(ScriptSandbox sandbox) { | ||
| 24 | + this.sandbox = sandbox; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + @Override | ||
| 28 | + public Object execute(String script, ScriptContext context) { | ||
| 29 | + try { | ||
| 30 | + // 执行前验证脚本安全性 | ||
| 31 | + sandbox.validate(script); | ||
| 32 | + // 执行脚本 | ||
| 33 | + return doExecute(script, context); | ||
| 34 | + } catch (Exception e) { | ||
| 35 | + log.error("执行脚本出错:{}", e.getMessage(), e); | ||
| 36 | + throw new RuntimeException("脚本执行失败:" + e.getMessage(), e); | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 执行脚本的具体实现 | ||
| 42 | + * | ||
| 43 | + * @param script 脚本内容 | ||
| 44 | + * @param context 脚本上下文 | ||
| 45 | + * @return 脚本执行结果 | ||
| 46 | + * @throws Exception 执行异常 | ||
| 47 | + */ | ||
| 48 | + protected abstract Object doExecute(String script, ScriptContext context) throws Exception; | ||
| 49 | +} | ||
| 0 | \ No newline at end of file | 50 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/JsScriptEngine.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.engine; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.map.MapUtil; | ||
| 4 | +import cn.hutool.core.util.IdUtil; | ||
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 6 | +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; | ||
| 7 | +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox; | ||
| 8 | +import cn.iocoder.yudao.module.iot.script.util.ScriptUtils; | ||
| 9 | +import lombok.extern.slf4j.Slf4j; | ||
| 10 | +import org.graalvm.polyglot.*; | ||
| 11 | + | ||
| 12 | +import java.nio.file.Path; | ||
| 13 | +import java.util.Map; | ||
| 14 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 15 | +import java.util.concurrent.TimeUnit; | ||
| 16 | +import java.util.concurrent.atomic.AtomicReference; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * JavaScript 脚本引擎实现,基于 GraalJS Context API | ||
| 20 | + */ | ||
| 21 | +@Slf4j | ||
| 22 | +public class JsScriptEngine extends AbstractScriptEngine implements AutoCloseable { | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * JavaScript 引擎类型 | ||
| 26 | + */ | ||
| 27 | + public static final String TYPE = "js"; | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 脚本语言类型 | ||
| 31 | + */ | ||
| 32 | + private static final String LANGUAGE_ID = "js"; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * GraalJS 上下文 | ||
| 36 | + */ | ||
| 37 | + private final Context context; | ||
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * 脚本源代码缓存 | ||
| 41 | + */ | ||
| 42 | + private final Map<String, Source> sourceCache = new ConcurrentHashMap<>(); | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 脚本缓存的最大数量 | ||
| 46 | + */ | ||
| 47 | + private static final int MAX_CACHE_SIZE = 1000; | ||
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * 构造函数 | ||
| 51 | + * | ||
| 52 | + * @param sandbox JavaScript 沙箱 | ||
| 53 | + */ | ||
| 54 | + public JsScriptEngine(ScriptSandbox sandbox) { | ||
| 55 | + super(sandbox); | ||
| 56 | + | ||
| 57 | + // 创建安全的主机访问配置 | ||
| 58 | + HostAccess hostAccess = HostAccess.newBuilder() | ||
| 59 | + .allowPublicAccess(true) // 允许访问公共方法和字段 | ||
| 60 | + .allowArrayAccess(true) // 允许数组访问 | ||
| 61 | + .allowListAccess(true) // 允许 List 访问 | ||
| 62 | + .allowMapAccess(true) // 允许 Map 访问 | ||
| 63 | + .build(); | ||
| 64 | + | ||
| 65 | + // 创建隔离的临时目录路径 | ||
| 66 | + Path tempDirectory = Path.of(System.getProperty("java.io.tmpdir"), "graaljs-" + IdUtil.fastSimpleUUID()); | ||
| 67 | + | ||
| 68 | + // 初始化 GraalJS 上下文 | ||
| 69 | + this.context = Context.newBuilder(LANGUAGE_ID) | ||
| 70 | + .allowHostAccess(hostAccess) // 使用安全的主机访问配置 | ||
| 71 | + .allowHostClassLookup(className -> false) // 禁止查找 Java 类 | ||
| 72 | + .allowIO(false) // 禁止文件 IO | ||
| 73 | + .allowNativeAccess(false) // 禁止本地访问 | ||
| 74 | + .allowCreateThread(false) // 禁止创建线程 | ||
| 75 | + .allowEnvironmentAccess(org.graalvm.polyglot.EnvironmentAccess.NONE) // 禁止环境变量访问 | ||
| 76 | + .allowExperimentalOptions(false) // 禁止实验性选项 | ||
| 77 | + .option("js.ecmascript-version", "2021") // 使用最新的 ECMAScript 标准 | ||
| 78 | + .option("js.foreign-object-prototype", "false") // 禁用外部对象原型 | ||
| 79 | + .option("js.nashorn-compat", "false") // 关闭 Nashorn 兼容模式以获得更好性能 | ||
| 80 | + .build(); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + @Override | ||
| 84 | + protected Object doExecute(String script, ScriptContext context) throws Exception { | ||
| 85 | + if (StrUtil.isBlank(script)) { | ||
| 86 | + return null; | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + try { | ||
| 90 | + // 绑定上下文变量 | ||
| 91 | + bindContextVariables(context); | ||
| 92 | + | ||
| 93 | + // 从缓存获取或创建脚本源 | ||
| 94 | + Source source = getOrCreateSource(script); | ||
| 95 | + | ||
| 96 | + // 执行脚本并捕获结果,添加超时控制 | ||
| 97 | + Value result; | ||
| 98 | + Thread executionThread = Thread.currentThread(); | ||
| 99 | + Thread watchdogThread = new Thread(() -> { | ||
| 100 | + try { | ||
| 101 | + // 等待 5 秒 | ||
| 102 | + TimeUnit.SECONDS.sleep(5); | ||
| 103 | + // 如果执行线程还在运行,中断它 | ||
| 104 | + if (executionThread.isAlive()) { | ||
| 105 | + log.warn("脚本执行超时,强制中断"); | ||
| 106 | + executionThread.interrupt(); | ||
| 107 | + } | ||
| 108 | + } catch (InterruptedException ignored) { | ||
| 109 | + // 忽略中断 | ||
| 110 | + } | ||
| 111 | + }); | ||
| 112 | + | ||
| 113 | + watchdogThread.setDaemon(true); | ||
| 114 | + watchdogThread.start(); | ||
| 115 | + | ||
| 116 | + try { | ||
| 117 | + result = this.context.eval(source); | ||
| 118 | + } finally { | ||
| 119 | + watchdogThread.interrupt(); // 确保看门狗线程停止 | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + // 转换结果为 Java 对象 | ||
| 123 | + return convertResultToJava(result); | ||
| 124 | + } catch (PolyglotException e) { | ||
| 125 | + handleScriptException(e, script); | ||
| 126 | + throw e; | ||
| 127 | + } | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + /** | ||
| 131 | + * 绑定上下文变量 | ||
| 132 | + * | ||
| 133 | + * @param context 脚本上下文 | ||
| 134 | + */ | ||
| 135 | + private void bindContextVariables(ScriptContext context) { | ||
| 136 | + Value bindings = this.context.getBindings(LANGUAGE_ID); | ||
| 137 | + | ||
| 138 | + // 添加上下文参数 | ||
| 139 | + if (MapUtil.isNotEmpty(context.getParameters())) { | ||
| 140 | + context.getParameters().forEach(bindings::putMember); | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + // 添加上下文函数 | ||
| 144 | + if (MapUtil.isNotEmpty(context.getFunctions())) { | ||
| 145 | + context.getFunctions().forEach(bindings::putMember); | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + // 添加工具类 | ||
| 149 | + bindings.putMember("utils", ScriptUtils.getInstance()); | ||
| 150 | + | ||
| 151 | + // 添加日志对象 | ||
| 152 | + bindings.putMember("log", log); | ||
| 153 | + | ||
| 154 | + // 添加控制台输出(限制并重定向到日志) | ||
| 155 | + AtomicReference<StringBuilder> consoleBuffer = new AtomicReference<>(new StringBuilder()); | ||
| 156 | + | ||
| 157 | + Value console = this.context.eval(LANGUAGE_ID, "({\n" + | ||
| 158 | + " log: function(msg) { _consoleLog(msg, 'INFO'); },\n" + | ||
| 159 | + " info: function(msg) { _consoleLog(msg, 'INFO'); },\n" + | ||
| 160 | + " warn: function(msg) { _consoleLog(msg, 'WARN'); },\n" + | ||
| 161 | + " error: function(msg) { _consoleLog(msg, 'ERROR'); }\n" + | ||
| 162 | + "})"); | ||
| 163 | + | ||
| 164 | + bindings.putMember("console", console); | ||
| 165 | + | ||
| 166 | + bindings.putMember("_consoleLog", (java.util.function.BiConsumer<String, String>) (message, level) -> { | ||
| 167 | + String formattedMsg = String.valueOf(message); | ||
| 168 | + switch (level) { | ||
| 169 | + case "INFO": | ||
| 170 | + log.info("Script console: {}", formattedMsg); | ||
| 171 | + break; | ||
| 172 | + case "WARN": | ||
| 173 | + log.warn("Script console: {}", formattedMsg); | ||
| 174 | + break; | ||
| 175 | + case "ERROR": | ||
| 176 | + log.error("Script console: {}", formattedMsg); | ||
| 177 | + break; | ||
| 178 | + default: | ||
| 179 | + log.info("Script console: {}", formattedMsg); | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + // 将输出添加到缓冲区 | ||
| 183 | + StringBuilder buffer = consoleBuffer.get(); | ||
| 184 | + if (buffer.length() > 10000) { | ||
| 185 | + buffer = new StringBuilder(); | ||
| 186 | + consoleBuffer.set(buffer); | ||
| 187 | + } | ||
| 188 | + buffer.append(formattedMsg).append("\n"); | ||
| 189 | + }); | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + /** | ||
| 193 | + * 从缓存中获取或创建脚本源 | ||
| 194 | + * | ||
| 195 | + * @param script 脚本内容 | ||
| 196 | + * @return 脚本源 | ||
| 197 | + */ | ||
| 198 | + private Source getOrCreateSource(String script) { | ||
| 199 | + // 如果缓存太大,清理部分缓存 | ||
| 200 | + if (sourceCache.size() > MAX_CACHE_SIZE) { | ||
| 201 | + int itemsToRemove = (int) (MAX_CACHE_SIZE * 0.2); // 清理 20% 的缓存 | ||
| 202 | + sourceCache.keySet().stream() | ||
| 203 | + .limit(itemsToRemove) | ||
| 204 | + .toList() | ||
| 205 | + .forEach(sourceCache::remove); | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + // 使用脚本的哈希码作为缓存键 | ||
| 209 | + String cacheKey = String.valueOf(script.hashCode()); | ||
| 210 | + | ||
| 211 | + return sourceCache.computeIfAbsent(cacheKey, key -> { | ||
| 212 | + try { | ||
| 213 | + return Source.newBuilder(LANGUAGE_ID, script, "script-" + key + ".js").cached(true).build(); | ||
| 214 | + } catch (Exception e) { | ||
| 215 | + log.error("创建脚本源失败: {}", e.getMessage(), e); | ||
| 216 | + throw new RuntimeException("创建脚本源失败: " + e.getMessage(), e); | ||
| 217 | + } | ||
| 218 | + }); | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + /** | ||
| 222 | + * 将 GraalJS 结果转换为 Java 对象 | ||
| 223 | + * | ||
| 224 | + * @param result GraalJS 执行结果 | ||
| 225 | + * @return Java 对象 | ||
| 226 | + */ | ||
| 227 | + private Object convertResultToJava(Value result) { | ||
| 228 | + if (result == null || result.isNull()) { | ||
| 229 | + return null; | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + if (result.isString()) { | ||
| 233 | + return result.asString(); | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + if (result.isNumber()) { | ||
| 237 | + if (result.fitsInInt()) { | ||
| 238 | + return result.asInt(); | ||
| 239 | + } else if (result.fitsInLong()) { | ||
| 240 | + return result.asLong(); | ||
| 241 | + } else if (result.fitsInFloat()) { | ||
| 242 | + return result.asFloat(); | ||
| 243 | + } else if (result.fitsInDouble()) { | ||
| 244 | + return result.asDouble(); | ||
| 245 | + } | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + if (result.isBoolean()) { | ||
| 249 | + return result.asBoolean(); | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + if (result.hasArrayElements()) { | ||
| 253 | + int size = (int) result.getArraySize(); | ||
| 254 | + Object[] array = new Object[size]; | ||
| 255 | + for (int i = 0; i < size; i++) { | ||
| 256 | + array[i] = convertResultToJava(result.getArrayElement(i)); | ||
| 257 | + } | ||
| 258 | + return array; | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | + if (result.hasMembers()) { | ||
| 262 | + Map<String, Object> map = MapUtil.newHashMap(); | ||
| 263 | + for (String key : result.getMemberKeys()) { | ||
| 264 | + map.put(key, convertResultToJava(result.getMember(key))); | ||
| 265 | + } | ||
| 266 | + return map; | ||
| 267 | + } | ||
| 268 | + | ||
| 269 | + if (result.isHostObject()) { | ||
| 270 | + return result.asHostObject(); | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + // 默认情况下尝试转换为字符串 | ||
| 274 | + return result.toString(); | ||
| 275 | + } | ||
| 276 | + | ||
| 277 | + /** | ||
| 278 | + * 处理脚本执行异常 | ||
| 279 | + * | ||
| 280 | + * @param e 多语言异常 | ||
| 281 | + * @param script 原始脚本 | ||
| 282 | + */ | ||
| 283 | + private void handleScriptException(PolyglotException e, String script) { | ||
| 284 | + if (e.isCancelled()) { | ||
| 285 | + log.error("脚本执行被取消,可能超出资源限制"); | ||
| 286 | + } else if (e.isHostException()) { | ||
| 287 | + Throwable hostException = e.asHostException(); | ||
| 288 | + log.error("脚本执行时发生 Java 异常: {}", hostException.getMessage(), hostException); | ||
| 289 | + } else if (e.isGuestException()) { | ||
| 290 | + if (e.getSourceLocation() != null) { | ||
| 291 | + log.error("脚本执行错误: {} 位于行 {},列 {}", | ||
| 292 | + e.getMessage(), | ||
| 293 | + e.getSourceLocation().getStartLine(), | ||
| 294 | + e.getSourceLocation().getStartColumn()); | ||
| 295 | + | ||
| 296 | + // 尝试显示错误位置上下文 | ||
| 297 | + try { | ||
| 298 | + String[] lines = script.split("\n"); | ||
| 299 | + int lineNumber = e.getSourceLocation().getStartLine(); | ||
| 300 | + if (lineNumber > 0 && lineNumber <= lines.length) { | ||
| 301 | + int contextStart = Math.max(1, lineNumber - 2); | ||
| 302 | + int contextEnd = Math.min(lines.length, lineNumber + 2); | ||
| 303 | + | ||
| 304 | + StringBuilder context = new StringBuilder(); | ||
| 305 | + for (int i = contextStart; i <= contextEnd; i++) { | ||
| 306 | + if (i == lineNumber) { | ||
| 307 | + context.append("> "); // 标记错误行 | ||
| 308 | + } else { | ||
| 309 | + context.append(" "); | ||
| 310 | + } | ||
| 311 | + context.append(i).append(": ").append(lines[i - 1]).append("\n"); | ||
| 312 | + } | ||
| 313 | + log.error("脚本上下文:\n{}", context); | ||
| 314 | + } | ||
| 315 | + } catch (Exception ignored) { | ||
| 316 | + // 忽略上下文显示失败 | ||
| 317 | + } | ||
| 318 | + } else { | ||
| 319 | + log.error("脚本执行错误: {}", e.getMessage()); | ||
| 320 | + } | ||
| 321 | + } else { | ||
| 322 | + log.error("脚本执行时发生未知错误: {}", e.getMessage(), e); | ||
| 323 | + } | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + @Override | ||
| 327 | + public String getType() { | ||
| 328 | + return TYPE; | ||
| 329 | + } | ||
| 330 | + | ||
| 331 | + @Override | ||
| 332 | + public void close() { | ||
| 333 | + try { | ||
| 334 | + // 清除脚本缓存 | ||
| 335 | + sourceCache.clear(); | ||
| 336 | + | ||
| 337 | + // 关闭 GraalJS 上下文,释放资源 | ||
| 338 | + context.close(true); | ||
| 339 | + } catch (Exception e) { | ||
| 340 | + log.warn("关闭 GraalJS 引擎时发生错误: {}", e.getMessage()); | ||
| 341 | + } | ||
| 342 | + } | ||
| 343 | +} | ||
| 0 | \ No newline at end of file | 344 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngine.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.engine; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 脚本引擎接口,定义脚本执行的核心功能 | ||
| 7 | + */ | ||
| 8 | +public interface ScriptEngine { | ||
| 9 | + | ||
| 10 | + /** | ||
| 11 | + * 执行脚本 | ||
| 12 | + * | ||
| 13 | + * @param script 脚本内容 | ||
| 14 | + * @param context 脚本上下文 | ||
| 15 | + * @return 脚本执行结果 | ||
| 16 | + */ | ||
| 17 | + Object execute(String script, ScriptContext context); | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 获取脚本引擎类型 | ||
| 21 | + * | ||
| 22 | + * @return 脚本引擎类型 | ||
| 23 | + */ | ||
| 24 | + String getType(); | ||
| 25 | +} | ||
| 0 | \ No newline at end of file | 26 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/engine/ScriptEngineFactory.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.engine; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.beans.factory.DisposableBean; | ||
| 6 | +import org.springframework.stereotype.Component; | ||
| 7 | + | ||
| 8 | +import java.util.Map; | ||
| 9 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 脚本引擎工厂,用于创建和缓存不同类型的脚本引擎,支持资源生命周期管理 | ||
| 13 | + */ | ||
| 14 | +@Slf4j | ||
| 15 | +@Component | ||
| 16 | +public class ScriptEngineFactory implements DisposableBean { | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 脚本引擎缓存 | ||
| 20 | + */ | ||
| 21 | + private final Map<String, ScriptEngine> engines = new ConcurrentHashMap<>(); | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 获取脚本引擎 | ||
| 25 | + * | ||
| 26 | + * @param type 脚本类型 | ||
| 27 | + * @return 脚本引擎 | ||
| 28 | + */ | ||
| 29 | + public ScriptEngine getEngine(String type) { | ||
| 30 | + // 从缓存中获取引擎 | ||
| 31 | + return engines.computeIfAbsent(type, this::createEngine); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 创建脚本引擎 | ||
| 36 | + * | ||
| 37 | + * @param type 脚本类型 | ||
| 38 | + * @return 脚本引擎 | ||
| 39 | + */ | ||
| 40 | + private ScriptEngine createEngine(String type) { | ||
| 41 | + try { | ||
| 42 | + if (JsScriptEngine.TYPE.equals(type)) { | ||
| 43 | + log.info("创建 GraalJS 脚本引擎"); | ||
| 44 | + return new JsScriptEngine(new JsSandbox()); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + log.warn("不支持的脚本类型: {}", type); | ||
| 48 | + return null; | ||
| 49 | + } catch (Exception e) { | ||
| 50 | + log.error("创建脚本引擎 [{}] 失败: {}", type, e.getMessage(), e); | ||
| 51 | + throw new RuntimeException("创建脚本引擎失败: " + e.getMessage(), e); | ||
| 52 | + } | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 释放指定类型的引擎资源 | ||
| 57 | + * | ||
| 58 | + * @param type 脚本类型 | ||
| 59 | + */ | ||
| 60 | + public void releaseEngine(String type) { | ||
| 61 | + ScriptEngine engine = engines.remove(type); | ||
| 62 | + if (engine instanceof AutoCloseable) { | ||
| 63 | + try { | ||
| 64 | + ((AutoCloseable) engine).close(); | ||
| 65 | + log.info("已释放脚本引擎资源: {}", type); | ||
| 66 | + } catch (Exception e) { | ||
| 67 | + log.warn("释放脚本引擎 [{}] 资源时发生错误: {}", type, e.getMessage()); | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * 清理所有引擎资源 | ||
| 74 | + */ | ||
| 75 | + public void releaseAllEngines() { | ||
| 76 | + engines.keySet().forEach(this::releaseEngine); | ||
| 77 | + log.info("已清理所有脚本引擎资源"); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + @Override | ||
| 81 | + public void destroy() { | ||
| 82 | + // 应用关闭时,释放所有引擎资源 | ||
| 83 | + log.info("应用关闭,释放所有脚本引擎资源..."); | ||
| 84 | + releaseAllEngines(); | ||
| 85 | + } | ||
| 86 | +} | ||
| 0 | \ No newline at end of file | 87 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/GraalJsExample.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.example; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.map.MapUtil; | ||
| 4 | +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext; | ||
| 5 | +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; | ||
| 6 | +import cn.iocoder.yudao.module.iot.script.service.ScriptService; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 9 | +import org.springframework.stereotype.Component; | ||
| 10 | + | ||
| 11 | +import java.util.Map; | ||
| 12 | +import java.util.concurrent.CompletableFuture; | ||
| 13 | +import java.util.concurrent.ExecutionException; | ||
| 14 | +import java.util.concurrent.TimeUnit; | ||
| 15 | +import java.util.concurrent.TimeoutException; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * GraalJS 脚本引擎示例 | ||
| 19 | + * <p> | ||
| 20 | + * 展示了如何使用 GraalJS 脚本引擎的各种功能 | ||
| 21 | + */ | ||
| 22 | +@Slf4j | ||
| 23 | +@Component | ||
| 24 | +public class GraalJsExample { | ||
| 25 | + | ||
| 26 | + @Autowired | ||
| 27 | + private ScriptService scriptService; | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 执行简单的 JavaScript 脚本 | ||
| 31 | + * | ||
| 32 | + * @return 执行结果 | ||
| 33 | + */ | ||
| 34 | + public Object executeSimpleScript() { | ||
| 35 | + // 简单的脚本内容 | ||
| 36 | + String script = "var result = a + b; result;"; | ||
| 37 | + | ||
| 38 | + // 创建参数 | ||
| 39 | + Map<String, Object> params = MapUtil.newHashMap(); | ||
| 40 | + params.put("a", 10); | ||
| 41 | + params.put("b", 20); | ||
| 42 | + | ||
| 43 | + // 执行脚本 | ||
| 44 | + return scriptService.executeJavaScript(script, params); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + /** | ||
| 48 | + * 执行现代 JavaScript 语法(ES6+) | ||
| 49 | + * | ||
| 50 | + * @return 执行结果 | ||
| 51 | + */ | ||
| 52 | + public Object executeModernJavaScript() { | ||
| 53 | + // 使用现代 JavaScript 语法 | ||
| 54 | + String script = "// 使用箭头函数\n" + | ||
| 55 | + "const add = (a, b) => a + b;\n" + | ||
| 56 | + "\n" + | ||
| 57 | + "// 使用解构赋值\n" + | ||
| 58 | + "const {c, d} = params;\n" + | ||
| 59 | + "\n" + | ||
| 60 | + "// 使用模板字符串\n" + | ||
| 61 | + "const result = `计算结果: ${add(c, d)}`;\n" + | ||
| 62 | + "\n" + | ||
| 63 | + "// 使用可选链操作符\n" + | ||
| 64 | + "const value = params?.e?.value ?? 'default';\n" + | ||
| 65 | + "\n" + | ||
| 66 | + "// 返回一个对象\n" + | ||
| 67 | + "({\n" + | ||
| 68 | + " sum: add(c, d),\n" + | ||
| 69 | + " message: result,\n" + | ||
| 70 | + " defaultValue: value\n" + | ||
| 71 | + "})"; | ||
| 72 | + | ||
| 73 | + // 创建参数 | ||
| 74 | + Map<String, Object> params = MapUtil.newHashMap(); | ||
| 75 | + params.put("params", MapUtil.builder() | ||
| 76 | + .put("c", 30) | ||
| 77 | + .put("d", 40) | ||
| 78 | + .build()); | ||
| 79 | + | ||
| 80 | + // 执行脚本 | ||
| 81 | + return scriptService.executeJavaScript(script, params); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 执行带错误处理的脚本 | ||
| 86 | + * | ||
| 87 | + * @return 执行结果 | ||
| 88 | + */ | ||
| 89 | + public Object executeWithErrorHandling() { | ||
| 90 | + // 包含错误处理的脚本 | ||
| 91 | + String script = "try {\n" + | ||
| 92 | + " // 故意制造错误\n" + | ||
| 93 | + " if (!nonExistentVar) {\n" + | ||
| 94 | + " throw new Error('手动抛出的错误');\n" + | ||
| 95 | + " }\n" + | ||
| 96 | + "} catch (error) {\n" + | ||
| 97 | + " console.error('捕获到错误: ' + error.message);\n" + | ||
| 98 | + " return { success: false, error: error.message };\n" + | ||
| 99 | + "}\n" + | ||
| 100 | + "\n" + | ||
| 101 | + "return { success: true, data: 'No error' };"; | ||
| 102 | + | ||
| 103 | + // 执行脚本 | ||
| 104 | + return scriptService.executeJavaScript(script, MapUtil.newHashMap()); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + /** | ||
| 108 | + * 演示超时控制 | ||
| 109 | + * | ||
| 110 | + * @return 执行结果 | ||
| 111 | + */ | ||
| 112 | + public Object executeWithTimeout() { | ||
| 113 | + // 这个脚本会导致无限循环 | ||
| 114 | + String script = "// 无限循环\n" + | ||
| 115 | + "var counter = 0;\n" + | ||
| 116 | + "while(true) {\n" + | ||
| 117 | + " counter++;\n" + | ||
| 118 | + " if (counter % 1000000 === 0) {\n" + | ||
| 119 | + " console.log('Still running: ' + counter);\n" + | ||
| 120 | + " }\n" + | ||
| 121 | + "}\n" + | ||
| 122 | + "return counter;"; | ||
| 123 | + | ||
| 124 | + // 使用 CompletableFuture 和超时控制 | ||
| 125 | + CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> { | ||
| 126 | + try { | ||
| 127 | + return scriptService.executeJavaScript(script, MapUtil.newHashMap()); | ||
| 128 | + } catch (Exception e) { | ||
| 129 | + log.error("脚本执行失败: {}", e.getMessage()); | ||
| 130 | + return "执行失败: " + e.getMessage(); | ||
| 131 | + } | ||
| 132 | + }); | ||
| 133 | + | ||
| 134 | + try { | ||
| 135 | + // 等待结果,最多 10 秒 | ||
| 136 | + return future.get(10, TimeUnit.SECONDS); | ||
| 137 | + } catch (InterruptedException | ExecutionException e) { | ||
| 138 | + return "执行异常: " + e.getMessage(); | ||
| 139 | + } catch (TimeoutException e) { | ||
| 140 | + future.cancel(true); | ||
| 141 | + return "执行超时,已强制终止"; | ||
| 142 | + } | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + /** | ||
| 146 | + * 演示 JSON 处理 | ||
| 147 | + * | ||
| 148 | + * @return 执行结果 | ||
| 149 | + */ | ||
| 150 | + public Object executeJsonProcessing() { | ||
| 151 | + // JSON 处理示例 | ||
| 152 | + String script = "// 解析传入的 JSON\n" + | ||
| 153 | + "var data = JSON.parse(jsonString);\n" + | ||
| 154 | + "\n" + | ||
| 155 | + "// 处理 JSON 数据\n" + | ||
| 156 | + "var result = {\n" + | ||
| 157 | + " name: data.name.toUpperCase(),\n" + | ||
| 158 | + " age: data.age + 1,\n" + | ||
| 159 | + " address: data.address || 'Unknown',\n" + | ||
| 160 | + " tags: [...data.tags, 'processed'],\n" + | ||
| 161 | + " timestamp: Date.now()\n" + | ||
| 162 | + "};\n" + | ||
| 163 | + "\n" + | ||
| 164 | + "// 转换回 JSON 字符串\n" + | ||
| 165 | + "JSON.stringify(result);"; | ||
| 166 | + | ||
| 167 | + // 创建上下文 | ||
| 168 | + ScriptContext context = new DefaultScriptContext(); | ||
| 169 | + context.setParameter("jsonString", | ||
| 170 | + "{\"name\":\"test user\",\"age\":25,\"tags\":[\"tag1\",\"tag2\"]}"); | ||
| 171 | + | ||
| 172 | + // 执行脚本 | ||
| 173 | + return scriptService.executeJavaScript(script, context); | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + /** | ||
| 177 | + * 演示数据转换 | ||
| 178 | + * | ||
| 179 | + * @return 执行结果 | ||
| 180 | + */ | ||
| 181 | + public Object executeDataConversion() { | ||
| 182 | + // 数据转换和处理示例 | ||
| 183 | + String script = "// 使用 utils 工具类进行数据转换\n" + | ||
| 184 | + "var stringValue = utils.isEmpty(input) ? \"默认值\" : input;\n" + | ||
| 185 | + "var numberValue = utils.convert(stringValue, \"number\");\n" + | ||
| 186 | + "\n" + | ||
| 187 | + "// 创建一个复杂数据结构\n" + | ||
| 188 | + "var result = {\n" + | ||
| 189 | + " original: input,\n" + | ||
| 190 | + " stringValue: stringValue,\n" + | ||
| 191 | + " numberValue: numberValue,\n" + | ||
| 192 | + " booleanValue: Boolean(numberValue),\n" + | ||
| 193 | + " isValid: utils.isNotEmpty(input)\n" + | ||
| 194 | + "};\n" + | ||
| 195 | + "\n" + | ||
| 196 | + "// 记录处理结果\n" + | ||
| 197 | + "log.info(\"处理结果: \" + utils.toJson(result));\n" + | ||
| 198 | + "\n" + | ||
| 199 | + "return result;"; | ||
| 200 | + | ||
| 201 | + // 创建参数 | ||
| 202 | + Map<String, Object> params = MapUtil.newHashMap(); | ||
| 203 | + params.put("input", "42"); | ||
| 204 | + | ||
| 205 | + // 执行脚本 | ||
| 206 | + return scriptService.executeJavaScript(script, params); | ||
| 207 | + } | ||
| 208 | +} | ||
| 0 | \ No newline at end of file | 209 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/example/ProductScriptSamples.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.example; | ||
| 2 | + | ||
| 3 | +import lombok.extern.slf4j.Slf4j; | ||
| 4 | +import org.springframework.stereotype.Component; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * 产品脚本示例类,提供各种类型的产品脚本示例代码 | ||
| 8 | + */ | ||
| 9 | +@Slf4j | ||
| 10 | +@Component | ||
| 11 | +public class ProductScriptSamples { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 获取属性解析脚本示例 | ||
| 15 | + * | ||
| 16 | + * @return 属性解析脚本示例代码 | ||
| 17 | + */ | ||
| 18 | + public String getPropertyParserSample() { | ||
| 19 | + return "/**\n" + | ||
| 20 | + " * 属性上报数据解析脚本示例\n" + | ||
| 21 | + " * @param input 设备上报的原始数据\n" + | ||
| 22 | + " * @param productKey 产品标识\n" + | ||
| 23 | + " * @param method 方法类型,固定为 property\n" + | ||
| 24 | + " * @param properties 当前设备的属性数据\n" + | ||
| 25 | + " * @return 解析后的属性数据\n" + | ||
| 26 | + " */\n" + | ||
| 27 | + "function parseProperty(input, productKey) {\n" + | ||
| 28 | + " // 记录日志\n" + | ||
| 29 | + " console.log('开始解析属性数据: ' + input);\n" + | ||
| 30 | + " \n" + | ||
| 31 | + " try {\n" + | ||
| 32 | + " // 假设上报的是 JSON 字符串\n" + | ||
| 33 | + " var data = JSON.parse(input);\n" + | ||
| 34 | + " \n" + | ||
| 35 | + " // 构建属性数据结构\n" + | ||
| 36 | + " var result = {\n" + | ||
| 37 | + " // 属性上报的时间戳,毫秒级\n" + | ||
| 38 | + " timestamp: data.timestamp || Date.now(),\n" + | ||
| 39 | + " // 属性数据\n" + | ||
| 40 | + " params: {}\n" + | ||
| 41 | + " };\n" + | ||
| 42 | + " \n" + | ||
| 43 | + " // 处理属性值\n" + | ||
| 44 | + " if (data.temperature) {\n" + | ||
| 45 | + " result.params.temperature = parseFloat(data.temperature);\n" + | ||
| 46 | + " }\n" + | ||
| 47 | + " \n" + | ||
| 48 | + " if (data.humidity) {\n" + | ||
| 49 | + " result.params.humidity = parseFloat(data.humidity);\n" + | ||
| 50 | + " }\n" + | ||
| 51 | + " \n" + | ||
| 52 | + " console.log('属性解析结果: ' + JSON.stringify(result));\n" + | ||
| 53 | + " return result;\n" + | ||
| 54 | + " } catch (error) {\n" + | ||
| 55 | + " console.error('解析属性数据失败: ' + error.message);\n" + | ||
| 56 | + " throw new Error('解析失败: ' + error.message);\n" + | ||
| 57 | + " }\n" + | ||
| 58 | + "};\n" + | ||
| 59 | + "\n" + | ||
| 60 | + "// 执行解析\n" + | ||
| 61 | + "parseProperty(input, productKey);"; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + /** | ||
| 65 | + * 获取事件解析脚本示例 | ||
| 66 | + * | ||
| 67 | + * @return 事件解析脚本示例代码 | ||
| 68 | + */ | ||
| 69 | + public String getEventParserSample() { | ||
| 70 | + return "/**\n" + | ||
| 71 | + " * 事件数据解析脚本示例\n" + | ||
| 72 | + " * @param input 设备上报的原始数据\n" + | ||
| 73 | + " * @param productKey 产品标识\n" + | ||
| 74 | + " * @param method 方法类型,固定为 event\n" + | ||
| 75 | + " * @param identifier 事件标识符\n" + | ||
| 76 | + " * @return 解析后的事件数据\n" + | ||
| 77 | + " */\n" + | ||
| 78 | + "function parseEvent(input, productKey, identifier) {\n" + | ||
| 79 | + " // 记录日志\n" + | ||
| 80 | + " console.log('开始解析事件数据: ' + input);\n" + | ||
| 81 | + " console.log('事件标识符: ' + identifier);\n" + | ||
| 82 | + " \n" + | ||
| 83 | + " try {\n" + | ||
| 84 | + " // 假设上报的是 JSON 字符串\n" + | ||
| 85 | + " var data = JSON.parse(input);\n" + | ||
| 86 | + " \n" + | ||
| 87 | + " // 构建事件数据结构\n" + | ||
| 88 | + " var result = {\n" + | ||
| 89 | + " // 事件标识符\n" + | ||
| 90 | + " identifier: identifier || 'alert',\n" + | ||
| 91 | + " // 事件上报的时间戳,毫秒级\n" + | ||
| 92 | + " timestamp: data.timestamp || Date.now(),\n" + | ||
| 93 | + " // 事件参数\n" + | ||
| 94 | + " params: {}\n" + | ||
| 95 | + " };\n" + | ||
| 96 | + " \n" + | ||
| 97 | + " // 根据不同事件类型处理参数\n" + | ||
| 98 | + " if (result.identifier === 'alert') {\n" + | ||
| 99 | + " result.params.level = data.level || 'info';\n" + | ||
| 100 | + " result.params.message = data.message || '';\n" + | ||
| 101 | + " } else if (result.identifier === 'error') {\n" + | ||
| 102 | + " result.params.code = data.code || 0;\n" + | ||
| 103 | + " result.params.message = data.message || '';\n" + | ||
| 104 | + " }\n" + | ||
| 105 | + " \n" + | ||
| 106 | + " console.log('事件解析结果: ' + JSON.stringify(result));\n" + | ||
| 107 | + " return result;\n" + | ||
| 108 | + " } catch (error) {\n" + | ||
| 109 | + " console.error('解析事件数据失败: ' + error.message);\n" + | ||
| 110 | + " throw new Error('解析失败: ' + error.message);\n" + | ||
| 111 | + " }\n" + | ||
| 112 | + "};\n" + | ||
| 113 | + "\n" + | ||
| 114 | + "// 执行解析\n" + | ||
| 115 | + "parseEvent(input, productKey, identifier);"; | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + /** | ||
| 119 | + * 获取命令编码脚本示例 | ||
| 120 | + * | ||
| 121 | + * @return 命令编码脚本示例代码 | ||
| 122 | + */ | ||
| 123 | + public String getCommandEncoderSample() { | ||
| 124 | + return "/**\n" + | ||
| 125 | + " * 命令数据编码脚本示例\n" + | ||
| 126 | + " * @param input 平台下发的命令数据\n" + | ||
| 127 | + " * @param productKey 产品标识\n" + | ||
| 128 | + " * @param method 方法类型,固定为 command\n" + | ||
| 129 | + " * @param cmdParams 命令参数\n" + | ||
| 130 | + " * @return 编码后的命令数据\n" + | ||
| 131 | + " */\n" + | ||
| 132 | + "function encodeCommand(input, productKey, cmdParams) {\n" + | ||
| 133 | + " // 记录日志\n" + | ||
| 134 | + " console.log('开始编码命令数据: ' + input);\n" + | ||
| 135 | + " console.log('命令参数: ' + JSON.stringify(cmdParams));\n" + | ||
| 136 | + " \n" + | ||
| 137 | + " try {\n" + | ||
| 138 | + " // 输入可能是 JSON 字符串或对象\n" + | ||
| 139 | + " var data = typeof input === 'string' ? JSON.parse(input) : input;\n" + | ||
| 140 | + " \n" + | ||
| 141 | + " // 获取命令名称和值\n" + | ||
| 142 | + " var cmdName = cmdParams.cmdName || '';\n" + | ||
| 143 | + " var cmdValue = cmdParams.cmdValue;\n" + | ||
| 144 | + " \n" + | ||
| 145 | + " // 构建设备可识别的命令格式\n" + | ||
| 146 | + " var result = {\n" + | ||
| 147 | + " cmd: cmdName,\n" + | ||
| 148 | + " value: cmdValue,\n" + | ||
| 149 | + " timestamp: Date.now()\n" + | ||
| 150 | + " };\n" + | ||
| 151 | + " \n" + | ||
| 152 | + " // 根据不同命令类型构建参数\n" + | ||
| 153 | + " if (cmdName === 'setValue') {\n" + | ||
| 154 | + " // 无需额外处理\n" + | ||
| 155 | + " } else if (cmdName === 'control') {\n" + | ||
| 156 | + " result.mode = data.mode || 'auto';\n" + | ||
| 157 | + " result.action = data.action || 'start';\n" + | ||
| 158 | + " }\n" + | ||
| 159 | + " \n" + | ||
| 160 | + " // 转换为设备能识别的格式(此处以 JSON 字符串为例)\n" + | ||
| 161 | + " var encodedResult = JSON.stringify(result);\n" + | ||
| 162 | + " \n" + | ||
| 163 | + " console.log('命令编码结果: ' + encodedResult);\n" + | ||
| 164 | + " return encodedResult;\n" + | ||
| 165 | + " } catch (error) {\n" + | ||
| 166 | + " console.error('编码命令数据失败: ' + error.message);\n" + | ||
| 167 | + " throw new Error('编码失败: ' + error.message);\n" + | ||
| 168 | + " }\n" + | ||
| 169 | + "};\n" + | ||
| 170 | + "\n" + | ||
| 171 | + "// 执行编码\n" + | ||
| 172 | + "encodeCommand(input, productKey, cmdParams);"; | ||
| 173 | + } | ||
| 174 | +} | ||
| 0 | \ No newline at end of file | 175 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/package-info.java
0 → 100644
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/JsSandbox.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.sandbox; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import lombok.extern.slf4j.Slf4j; | ||
| 6 | + | ||
| 7 | +import java.util.ArrayList; | ||
| 8 | +import java.util.Arrays; | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.regex.Matcher; | ||
| 11 | +import java.util.regex.Pattern; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * JavaScript 沙箱实现,提供脚本安全性验证 | ||
| 15 | + */ | ||
| 16 | +@Slf4j | ||
| 17 | +public class JsSandbox implements ScriptSandbox { | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * JavaScript 沙箱类型 | ||
| 21 | + */ | ||
| 22 | + public static final String TYPE = "js"; | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 不安全的关键字列表 | ||
| 26 | + */ | ||
| 27 | + private final List<String> unsafeKeywords = new ArrayList<>(); | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 可能导致高资源消耗的关键字 | ||
| 31 | + */ | ||
| 32 | + private final List<String> highResourceKeywords = new ArrayList<>(); | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 不安全的包/类访问模式 | ||
| 36 | + */ | ||
| 37 | + private final List<Pattern> unsafePatterns = new ArrayList<>(); | ||
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * 递归或循环嵌套深度检测模式 | ||
| 41 | + */ | ||
| 42 | + private final List<Pattern> recursionPatterns = new ArrayList<>(); | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 允许的脚本最大长度(字节) | ||
| 46 | + */ | ||
| 47 | + private static final int MAX_SCRIPT_LENGTH = 100 * 1024; // 100KB | ||
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * 脚本安全验证超时时间(毫秒) | ||
| 51 | + */ | ||
| 52 | + private static final long VALIDATION_TIMEOUT = 1000; // 1秒 | ||
| 53 | + | ||
| 54 | + /** | ||
| 55 | + * 构造函数,初始化不安全的关键字和模式 | ||
| 56 | + */ | ||
| 57 | + public JsSandbox() { | ||
| 58 | + // 初始化 Java 相关的不安全关键字 | ||
| 59 | + Arrays.asList( | ||
| 60 | + "java.lang.System", | ||
| 61 | + "java.io", | ||
| 62 | + "java.net", | ||
| 63 | + "java.nio", | ||
| 64 | + "java.security", | ||
| 65 | + "java.rmi", | ||
| 66 | + "java.lang.reflect", | ||
| 67 | + "java.sql", | ||
| 68 | + "javax.sql", | ||
| 69 | + "javax.naming", | ||
| 70 | + "javax.script", | ||
| 71 | + "javax.tools", | ||
| 72 | + "org.omg", | ||
| 73 | + "org.graalvm.polyglot", | ||
| 74 | + "sun.", | ||
| 75 | + "javafx.", | ||
| 76 | + "Packages.", | ||
| 77 | + "com.sun.", | ||
| 78 | + "com.oracle.").forEach(unsafeKeywords::add); | ||
| 79 | + | ||
| 80 | + // GraalJS 特有的不安全关键字 | ||
| 81 | + Arrays.asList( | ||
| 82 | + "Polyglot.import", | ||
| 83 | + "Polyglot.eval", | ||
| 84 | + "Java.type", | ||
| 85 | + "allowHostAccess", | ||
| 86 | + "allowNativeAccess", | ||
| 87 | + "allowIO", | ||
| 88 | + "allowHostClassLoading", | ||
| 89 | + "allowAllAccess", | ||
| 90 | + "allowExperimentalOptions", | ||
| 91 | + "Context.Builder", | ||
| 92 | + "Context.create", | ||
| 93 | + "Context.getCurrent", | ||
| 94 | + "Context.newBuilder", | ||
| 95 | + "__proto__", | ||
| 96 | + "__defineGetter__", | ||
| 97 | + "__defineSetter__", | ||
| 98 | + "__lookupGetter__", | ||
| 99 | + "__lookupSetter__", | ||
| 100 | + "__noSuchMethod__", | ||
| 101 | + "constructor.constructor", | ||
| 102 | + "Object.constructor").forEach(unsafeKeywords::add); | ||
| 103 | + | ||
| 104 | + // 可能导致高资源消耗的关键字 | ||
| 105 | + Arrays.asList( | ||
| 106 | + "while(true)", | ||
| 107 | + "for(;;)", | ||
| 108 | + "do{", | ||
| 109 | + "BigInt", | ||
| 110 | + "Promise.all", | ||
| 111 | + "setTimeout", | ||
| 112 | + "setInterval", | ||
| 113 | + "new Array(", | ||
| 114 | + "Array(", | ||
| 115 | + "new ArrayBuffer(", | ||
| 116 | + ".repeat(", | ||
| 117 | + ".forEach(", | ||
| 118 | + ".map(", | ||
| 119 | + ".reduce(").forEach(highResourceKeywords::add); | ||
| 120 | + | ||
| 121 | + // 初始化不安全的模式 | ||
| 122 | + // 系统访问和进程执行 | ||
| 123 | + unsafePatterns.add(Pattern.compile("java\\.lang\\.Runtime")); | ||
| 124 | + unsafePatterns.add(Pattern.compile("java\\.lang\\.ProcessBuilder")); | ||
| 125 | + unsafePatterns.add(Pattern.compile("java\\.lang\\.reflect")); | ||
| 126 | + | ||
| 127 | + // 特殊对象和操作 | ||
| 128 | + unsafePatterns.add(Pattern.compile("Packages")); | ||
| 129 | + unsafePatterns.add(Pattern.compile("JavaImporter")); | ||
| 130 | + unsafePatterns.add(Pattern.compile("load\\s*\\(")); | ||
| 131 | + unsafePatterns.add(Pattern.compile("loadWithNewGlobal\\s*\\(")); | ||
| 132 | + unsafePatterns.add(Pattern.compile("exit\\s*\\(")); | ||
| 133 | + unsafePatterns.add(Pattern.compile("quit\\s*\\(")); | ||
| 134 | + unsafePatterns.add(Pattern.compile("eval\\s*\\(")); | ||
| 135 | + | ||
| 136 | + // GraalJS 特有的不安全模式 | ||
| 137 | + unsafePatterns.add(Pattern.compile("Polyglot\\.")); | ||
| 138 | + unsafePatterns.add(Pattern.compile("Java\\.type\\s*\\(")); | ||
| 139 | + unsafePatterns.add(Pattern.compile("Context\\.")); | ||
| 140 | + unsafePatterns.add(Pattern.compile("Engine\\.")); | ||
| 141 | + | ||
| 142 | + // 原型污染检测 | ||
| 143 | + unsafePatterns.add(Pattern.compile("(?:Object|Array|String|Number|Boolean|Function|RegExp|Date)\\.prototype")); | ||
| 144 | + unsafePatterns.add(Pattern.compile("\\['constructor'\\]")); | ||
| 145 | + unsafePatterns.add(Pattern.compile("\\[\"constructor\"\\]")); | ||
| 146 | + unsafePatterns.add(Pattern.compile("\\['__proto__'\\]")); | ||
| 147 | + unsafePatterns.add(Pattern.compile("\\[\"__proto__\"\\]")); | ||
| 148 | + | ||
| 149 | + // 检测可能导致无限递归或循环的模式 | ||
| 150 | + recursionPatterns.add(Pattern.compile("for\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*for\\s*\\(")); // 嵌套循环 | ||
| 151 | + recursionPatterns.add(Pattern.compile("while\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*while\\s*\\(")); // 嵌套 while | ||
| 152 | + recursionPatterns.add(Pattern.compile("function\\s+[a-zA-Z0-9_$]+\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*\\1\\s*\\(")); // 递归函数调用 | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + @Override | ||
| 156 | + public boolean validate(String script) { | ||
| 157 | + if (StrUtil.isBlank(script)) { | ||
| 158 | + return true; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + // 检查脚本长度 | ||
| 162 | + if (script.length() > MAX_SCRIPT_LENGTH) { | ||
| 163 | + log.warn("脚本长度超过限制: {} > {}", script.length(), MAX_SCRIPT_LENGTH); | ||
| 164 | + return false; | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + // 使用超时机制进行验证 | ||
| 168 | + final boolean[] result = {true}; | ||
| 169 | + Thread validationThread = new Thread(() -> { | ||
| 170 | + // 检查不安全的关键字 | ||
| 171 | + if (containsUnsafeKeywords(script)) { | ||
| 172 | + result[0] = false; | ||
| 173 | + return; | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + // 检查不安全的模式 | ||
| 177 | + if (matchesUnsafePatterns(script)) { | ||
| 178 | + result[0] = false; | ||
| 179 | + return; | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + // 检查可能导致高资源消耗的构造 | ||
| 183 | + if (containsHighResourcePatterns(script)) { | ||
| 184 | + log.warn("脚本包含可能导致高资源消耗的构造,需要注意"); | ||
| 185 | + // 不直接拒绝,而是记录警告 | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + // 分析脚本复杂度 | ||
| 189 | + analyzeScriptComplexity(script); | ||
| 190 | + }); | ||
| 191 | + | ||
| 192 | + validationThread.start(); | ||
| 193 | + try { | ||
| 194 | + validationThread.join(VALIDATION_TIMEOUT); | ||
| 195 | + if (validationThread.isAlive()) { | ||
| 196 | + validationThread.interrupt(); | ||
| 197 | + log.warn("脚本安全验证超时"); | ||
| 198 | + return false; | ||
| 199 | + } | ||
| 200 | + } catch (InterruptedException e) { | ||
| 201 | + log.warn("脚本安全验证被中断"); | ||
| 202 | + return false; | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + return result[0]; | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + @Override | ||
| 209 | + public String getType() { | ||
| 210 | + return TYPE; | ||
| 211 | + } | ||
| 212 | + | ||
| 213 | + /** | ||
| 214 | + * 检查脚本是否包含不安全的关键字 | ||
| 215 | + * | ||
| 216 | + * @param script 脚本内容 | ||
| 217 | + * @return 是否包含不安全的关键字 | ||
| 218 | + */ | ||
| 219 | + private boolean containsUnsafeKeywords(String script) { | ||
| 220 | + if (CollUtil.isEmpty(unsafeKeywords)) { | ||
| 221 | + return false; | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + for (String keyword : unsafeKeywords) { | ||
| 225 | + if (script.contains(keyword)) { | ||
| 226 | + log.warn("脚本包含不安全的关键字: {}", keyword); | ||
| 227 | + return true; | ||
| 228 | + } | ||
| 229 | + } | ||
| 230 | + return false; | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + /** | ||
| 234 | + * 检查脚本是否匹配不安全的模式 | ||
| 235 | + * | ||
| 236 | + * @param script 脚本内容 | ||
| 237 | + * @return 是否匹配不安全的模式 | ||
| 238 | + */ | ||
| 239 | + private boolean matchesUnsafePatterns(String script) { | ||
| 240 | + if (CollUtil.isEmpty(unsafePatterns)) { | ||
| 241 | + return false; | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + for (Pattern pattern : unsafePatterns) { | ||
| 245 | + Matcher matcher = pattern.matcher(script); | ||
| 246 | + if (matcher.find()) { | ||
| 247 | + log.warn("脚本匹配到不安全的模式: {}", pattern.pattern()); | ||
| 248 | + return true; | ||
| 249 | + } | ||
| 250 | + } | ||
| 251 | + return false; | ||
| 252 | + } | ||
| 253 | + | ||
| 254 | + /** | ||
| 255 | + * 检查脚本是否包含可能导致高资源消耗的模式 | ||
| 256 | + * | ||
| 257 | + * @param script 脚本内容 | ||
| 258 | + * @return 是否包含高资源消耗模式 | ||
| 259 | + */ | ||
| 260 | + private boolean containsHighResourcePatterns(String script) { | ||
| 261 | + if (CollUtil.isEmpty(highResourceKeywords)) { | ||
| 262 | + return false; | ||
| 263 | + } | ||
| 264 | + | ||
| 265 | + boolean result = false; | ||
| 266 | + for (String pattern : highResourceKeywords) { | ||
| 267 | + if (script.contains(pattern)) { | ||
| 268 | + log.warn("脚本包含高资源消耗模式: {}", pattern); | ||
| 269 | + result = true; | ||
| 270 | + } | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + // 还要检查递归或嵌套循环模式 | ||
| 274 | + for (Pattern pattern : recursionPatterns) { | ||
| 275 | + Matcher matcher = pattern.matcher(script); | ||
| 276 | + if (matcher.find()) { | ||
| 277 | + log.warn("脚本包含嵌套循环或递归调用: {}", pattern.pattern()); | ||
| 278 | + result = true; | ||
| 279 | + } | ||
| 280 | + } | ||
| 281 | + | ||
| 282 | + return result; | ||
| 283 | + } | ||
| 284 | + | ||
| 285 | + /** | ||
| 286 | + * 分析脚本复杂度 | ||
| 287 | + * | ||
| 288 | + * @param script 脚本内容 | ||
| 289 | + */ | ||
| 290 | + private void analyzeScriptComplexity(String script) { | ||
| 291 | + // 计算循环和条件语句的数量 | ||
| 292 | + int forCount = countOccurrences(script, "for("); | ||
| 293 | + forCount += countOccurrences(script, "for ("); | ||
| 294 | + | ||
| 295 | + int whileCount = countOccurrences(script, "while("); | ||
| 296 | + whileCount += countOccurrences(script, "while ("); | ||
| 297 | + | ||
| 298 | + int doWhileCount = countOccurrences(script, "do{"); | ||
| 299 | + doWhileCount += countOccurrences(script, "do {"); | ||
| 300 | + | ||
| 301 | + int funcCount = countOccurrences(script, "function"); | ||
| 302 | + | ||
| 303 | + // 记录复杂度评估 | ||
| 304 | + if (forCount + whileCount + doWhileCount > 10) { | ||
| 305 | + log.warn("脚本循环结构过多: for={}, while={}, do-while={}", forCount, whileCount, doWhileCount); | ||
| 306 | + } | ||
| 307 | + | ||
| 308 | + if (funcCount > 20) { | ||
| 309 | + log.warn("脚本函数定义过多: {}", funcCount); | ||
| 310 | + } | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + /** | ||
| 314 | + * 计算字符串出现次数 | ||
| 315 | + * | ||
| 316 | + * @param source 源字符串 | ||
| 317 | + * @param substring 子字符串 | ||
| 318 | + * @return 出现次数 | ||
| 319 | + */ | ||
| 320 | + private int countOccurrences(String source, String substring) { | ||
| 321 | + int count = 0; | ||
| 322 | + int index = 0; | ||
| 323 | + while ((index = source.indexOf(substring, index)) != -1) { | ||
| 324 | + count++; | ||
| 325 | + index += substring.length(); | ||
| 326 | + } | ||
| 327 | + return count; | ||
| 328 | + } | ||
| 329 | +} | ||
| 0 | \ No newline at end of file | 330 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/sandbox/ScriptSandbox.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.sandbox; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * 脚本沙箱接口,提供脚本安全性验证 | ||
| 5 | + */ | ||
| 6 | +public interface ScriptSandbox { | ||
| 7 | + | ||
| 8 | + /** | ||
| 9 | + * 验证脚本内容是否安全 | ||
| 10 | + * | ||
| 11 | + * @param script 脚本内容 | ||
| 12 | + * @return 脚本是否安全 | ||
| 13 | + */ | ||
| 14 | + boolean validate(String script); | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 获取沙箱类型 | ||
| 18 | + * | ||
| 19 | + * @return 沙箱类型 | ||
| 20 | + */ | ||
| 21 | + String getType(); | ||
| 22 | +} | ||
| 0 | \ No newline at end of file | 23 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.service; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; | ||
| 4 | + | ||
| 5 | +import java.util.Map; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 脚本服务接口,定义脚本执行的核心功能 | ||
| 9 | + */ | ||
| 10 | +public interface ScriptService { | ||
| 11 | + | ||
| 12 | + /** | ||
| 13 | + * 执行脚本 | ||
| 14 | + * | ||
| 15 | + * @param scriptType 脚本类型(如 js、groovy 等) | ||
| 16 | + * @param script 脚本内容 | ||
| 17 | + * @param context 脚本上下文 | ||
| 18 | + * @return 脚本执行结果 | ||
| 19 | + */ | ||
| 20 | + Object executeScript(String scriptType, String script, ScriptContext context); | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 执行脚本 | ||
| 24 | + * | ||
| 25 | + * @param scriptType 脚本类型(如 js、groovy 等) | ||
| 26 | + * @param script 脚本内容 | ||
| 27 | + * @param params 脚本参数 | ||
| 28 | + * @return 脚本执行结果 | ||
| 29 | + */ | ||
| 30 | + Object executeScript(String scriptType, String script, Map<String, Object> params); | ||
| 31 | + | ||
| 32 | + /** | ||
| 33 | + * 执行 JavaScript 脚本 | ||
| 34 | + * | ||
| 35 | + * @param script 脚本内容 | ||
| 36 | + * @param context 脚本上下文 | ||
| 37 | + * @return 脚本执行结果 | ||
| 38 | + */ | ||
| 39 | + Object executeJavaScript(String script, ScriptContext context); | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 执行 JavaScript 脚本 | ||
| 43 | + * | ||
| 44 | + * @param script 脚本内容 | ||
| 45 | + * @param params 脚本参数 | ||
| 46 | + * @return 脚本执行结果 | ||
| 47 | + */ | ||
| 48 | + Object executeJavaScript(String script, Map<String, Object> params); | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 验证脚本内容是否安全 | ||
| 52 | + * | ||
| 53 | + * @param scriptType 脚本类型 | ||
| 54 | + * @param script 脚本内容 | ||
| 55 | + * @return 脚本是否安全 | ||
| 56 | + */ | ||
| 57 | + boolean validateScript(String scriptType, String script); | ||
| 58 | +} | ||
| 0 | \ No newline at end of file | 59 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/service/ScriptServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.service; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.map.MapUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext; | ||
| 6 | +import cn.iocoder.yudao.module.iot.script.context.ScriptContext; | ||
| 7 | +import cn.iocoder.yudao.module.iot.script.engine.JsScriptEngine; | ||
| 8 | +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngine; | ||
| 9 | +import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory; | ||
| 10 | +import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox; | ||
| 11 | +import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox; | ||
| 12 | +import lombok.extern.slf4j.Slf4j; | ||
| 13 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 14 | +import org.springframework.stereotype.Service; | ||
| 15 | + | ||
| 16 | +import java.util.Map; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * 脚本服务实现类 | ||
| 20 | + */ | ||
| 21 | +@Slf4j | ||
| 22 | +@Service | ||
| 23 | +public class ScriptServiceImpl implements ScriptService { | ||
| 24 | + | ||
| 25 | + @Autowired | ||
| 26 | + private ScriptEngineFactory engineFactory; | ||
| 27 | + | ||
| 28 | + @Override | ||
| 29 | + public Object executeScript(String scriptType, String script, ScriptContext context) { | ||
| 30 | + if (StrUtil.isBlank(scriptType) || StrUtil.isBlank(script)) { | ||
| 31 | + return null; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + ScriptEngine engine = engineFactory.getEngine(scriptType); | ||
| 35 | + if (engine == null) { | ||
| 36 | + log.error("找不到脚本引擎: {}", scriptType); | ||
| 37 | + throw new RuntimeException("不支持的脚本类型: " + scriptType); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + try { | ||
| 41 | + return engine.execute(script, context); | ||
| 42 | + } catch (Exception e) { | ||
| 43 | + log.error("执行脚本失败: {}", e.getMessage(), e); | ||
| 44 | + throw new RuntimeException("执行脚本失败: " + e.getMessage(), e); | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + @Override | ||
| 49 | + public Object executeScript(String scriptType, String script, Map<String, Object> params) { | ||
| 50 | + ScriptContext context = createContext(params); | ||
| 51 | + return executeScript(scriptType, script, context); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + @Override | ||
| 55 | + public Object executeJavaScript(String script, ScriptContext context) { | ||
| 56 | + return executeScript(JsScriptEngine.TYPE, script, context); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + @Override | ||
| 60 | + public Object executeJavaScript(String script, Map<String, Object> params) { | ||
| 61 | + return executeScript(JsScriptEngine.TYPE, script, params); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + @Override | ||
| 65 | + public boolean validateScript(String scriptType, String script) { | ||
| 66 | + if (StrUtil.isBlank(scriptType) || StrUtil.isBlank(script)) { | ||
| 67 | + return true; | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + ScriptSandbox sandbox = getSandbox(scriptType); | ||
| 71 | + if (sandbox == null) { | ||
| 72 | + log.warn("找不到对应的脚本沙箱: {}", scriptType); | ||
| 73 | + return false; | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + try { | ||
| 77 | + return sandbox.validate(script); | ||
| 78 | + } catch (Exception e) { | ||
| 79 | + log.error("验证脚本安全性失败: {}", e.getMessage(), e); | ||
| 80 | + return false; | ||
| 81 | + } | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 根据脚本类型获取对应的沙箱实现 | ||
| 86 | + * | ||
| 87 | + * @param scriptType 脚本类型 | ||
| 88 | + * @return 沙箱实现 | ||
| 89 | + */ | ||
| 90 | + private ScriptSandbox getSandbox(String scriptType) { | ||
| 91 | + if (JsScriptEngine.TYPE.equals(scriptType)) { | ||
| 92 | + return new JsSandbox(); | ||
| 93 | + } | ||
| 94 | + return null; | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * 根据参数创建脚本上下文 | ||
| 99 | + * | ||
| 100 | + * @param params 参数 | ||
| 101 | + * @return 脚本上下文 | ||
| 102 | + */ | ||
| 103 | + private ScriptContext createContext(Map<String, Object> params) { | ||
| 104 | + ScriptContext context = new DefaultScriptContext(); | ||
| 105 | + if (MapUtil.isNotEmpty(params)) { | ||
| 106 | + params.forEach(context::setParameter); | ||
| 107 | + } | ||
| 108 | + return context; | ||
| 109 | + } | ||
| 110 | +} | ||
| 0 | \ No newline at end of file | 111 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-script/src/main/java/cn/iocoder/yudao/module/iot/script/util/ScriptUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.script.util; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.convert.Convert; | ||
| 4 | +import cn.hutool.core.map.MapUtil; | ||
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 6 | +import cn.hutool.json.JSONUtil; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | + | ||
| 9 | +import java.util.Map; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 脚本工具类,提供给脚本执行环境使用的工具方法 | ||
| 13 | + */ | ||
| 14 | +@Slf4j | ||
| 15 | +public class ScriptUtils { | ||
| 16 | + | ||
| 17 | + /** | ||
| 18 | + * 单例实例 | ||
| 19 | + */ | ||
| 20 | + private static final ScriptUtils INSTANCE = new ScriptUtils(); | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 获取单例实例 | ||
| 24 | + * | ||
| 25 | + * @return 工具类实例 | ||
| 26 | + */ | ||
| 27 | + public static ScriptUtils getInstance() { | ||
| 28 | + return INSTANCE; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + private ScriptUtils() { | ||
| 32 | + // 私有构造函数 | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * 字符串是否为空 | ||
| 37 | + * | ||
| 38 | + * @param str 字符串 | ||
| 39 | + * @return 是否为空 | ||
| 40 | + */ | ||
| 41 | + public boolean isEmpty(String str) { | ||
| 42 | + return StrUtil.isEmpty(str); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 字符串是否不为空 | ||
| 47 | + * | ||
| 48 | + * @param str 字符串 | ||
| 49 | + * @return 是否不为空 | ||
| 50 | + */ | ||
| 51 | + public boolean isNotEmpty(String str) { | ||
| 52 | + return StrUtil.isNotEmpty(str); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 将对象转为 JSON 字符串 | ||
| 57 | + * | ||
| 58 | + * @param obj 对象 | ||
| 59 | + * @return JSON 字符串 | ||
| 60 | + */ | ||
| 61 | + public String toJson(Object obj) { | ||
| 62 | + return JSONUtil.toJsonStr(obj); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + /** | ||
| 66 | + * 将 JSON 字符串转为 Map | ||
| 67 | + * | ||
| 68 | + * @param json JSON 字符串 | ||
| 69 | + * @return Map 对象 | ||
| 70 | + */ | ||
| 71 | + public Map<String, Object> parseJson(String json) { | ||
| 72 | + if (StrUtil.isEmpty(json)) { | ||
| 73 | + return MapUtil.newHashMap(); | ||
| 74 | + } | ||
| 75 | + try { | ||
| 76 | + return JSONUtil.toBean(json, Map.class); | ||
| 77 | + } catch (Exception e) { | ||
| 78 | + log.error("JSON 解析失败: {}", json, e); | ||
| 79 | + return MapUtil.newHashMap(); | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + /** | ||
| 84 | + * 类型转换 | ||
| 85 | + * | ||
| 86 | + * @param value 值 | ||
| 87 | + * @param type 目标类型 | ||
| 88 | + * @param <T> 泛型 | ||
| 89 | + * @return 转换后的值 | ||
| 90 | + */ | ||
| 91 | + public <T> T convert(Object value, Class<T> type) { | ||
| 92 | + return Convert.convert(type, value); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + /** | ||
| 96 | + * 从 Map 中获取值 | ||
| 97 | + * | ||
| 98 | + * @param map Map 对象 | ||
| 99 | + * @param key 键 | ||
| 100 | + * @return 值 | ||
| 101 | + */ | ||
| 102 | + public Object get(Map<String, Object> map, String key) { | ||
| 103 | + return MapUtil.get(map, key, Object.class); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + /** | ||
| 107 | + * 从 Map 中获取字符串 | ||
| 108 | + * | ||
| 109 | + * @param map Map 对象 | ||
| 110 | + * @param key 键 | ||
| 111 | + * @return 字符串值 | ||
| 112 | + */ | ||
| 113 | + public String getString(Map<String, Object> map, String key) { | ||
| 114 | + return MapUtil.getStr(map, key); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + /** | ||
| 118 | + * 从 Map 中获取整数 | ||
| 119 | + * | ||
| 120 | + * @param map Map 对象 | ||
| 121 | + * @param key 键 | ||
| 122 | + * @return 整数值 | ||
| 123 | + */ | ||
| 124 | + public Integer getInt(Map<String, Object> map, String key) { | ||
| 125 | + return MapUtil.getInt(map, key); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + /** | ||
| 129 | + * 从 Map 中获取布尔值 | ||
| 130 | + * | ||
| 131 | + * @param map Map 对象 | ||
| 132 | + * @param key 键 | ||
| 133 | + * @return 布尔值 | ||
| 134 | + */ | ||
| 135 | + public Boolean getBool(Map<String, Object> map, String key) { | ||
| 136 | + return MapUtil.getBool(map, key); | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + /** | ||
| 140 | + * 从 Map 中获取双精度浮点数 | ||
| 141 | + * | ||
| 142 | + * @param map Map 对象 | ||
| 143 | + * @param key 键 | ||
| 144 | + * @return 双精度浮点数值 | ||
| 145 | + */ | ||
| 146 | + public Double getDouble(Map<String, Object> map, String key) { | ||
| 147 | + return MapUtil.getDouble(map, key); | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + /** | ||
| 151 | + * 获取当前时间戳(毫秒) | ||
| 152 | + * | ||
| 153 | + * @return 时间戳 | ||
| 154 | + */ | ||
| 155 | + public long currentTimeMillis() { | ||
| 156 | + return System.currentTimeMillis(); | ||
| 157 | + } | ||
| 158 | +} | ||
| 0 | \ No newline at end of file | 159 | \ No newline at end of file |