zhouhao 2 năm trước cách đây
mục cha
commit
c7f4b19ed1
100 tập tin đã thay đổi với 5173 bổ sung951 xóa
  1. 13 0
      jetlinks-components/common-component/pom.xml
  2. 1 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/ConfigMetadataConstants.java
  3. 12 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyConstants.java
  4. 35 12
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyMetadataConstants.java
  5. 64 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyMetric.java
  6. 10 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/TimerIterable.java
  7. 391 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/TimerSpec.java
  8. 1 1
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/Version.java
  9. 27 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java
  10. 26 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceInfo.java
  11. 70 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceManager.java
  12. 36 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceProvider.java
  13. 30 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferencedException.java
  14. 26 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DefaultDataReferenceManager.java
  15. 75 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/ClassPathJsonResourceProvider.java
  16. 37 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/DefaultResourceManager.java
  17. 16 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/Resource.java
  18. 13 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/ResourceManager.java
  19. 15 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/ResourceProvider.java
  20. 34 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/SimpleResource.java
  21. 42 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/initialize/PermissionResourceProvider.java
  22. 51 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/strategy/StaticStrategyManager.java
  23. 17 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/strategy/Strategy.java
  24. 12 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/strategy/StrategyManager.java
  25. 123 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java
  26. 277 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ReactorUtils.java
  27. 37 0
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/SystemResourcesController.java
  28. 0 6
      jetlinks-components/configure-component/src/main/resources/META-INF/spring.factories
  29. 5 0
      jetlinks-components/configure-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  30. 5 0
      jetlinks-components/dashboard-component/pom.xml
  31. 0 81
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/JvmCpuMeasurementProvider.java
  32. 0 132
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/JvmMemoryMeasurementProvider.java
  33. 3 2
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/MonitorObjectDefinition.java
  34. 0 79
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/SystemCpuMeasurementProvider.java
  35. 0 128
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/SystemMemoryMeasurementProvider.java
  36. 18 7
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/SystemMonitor.java
  37. 36 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/CpuInfo.java
  38. 41 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/DiskInfo.java
  39. 73 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/MemoryInfo.java
  40. 13 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/MonitorInfo.java
  41. 15 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/MonitorUtils.java
  42. 34 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/SystemInfo.java
  43. 148 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/SystemMonitorMeasurementProvider.java
  44. 14 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/SystemMonitorService.java
  45. 66 0
      jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/SystemMonitorServiceImpl.java
  46. 0 20
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/ElasticRestClient.java
  47. 37 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchThingDataConfiguration.java
  48. 0 1
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/DefaultElasticSearchIndexManager.java
  49. 0 1
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/timeseries/ElasticSearchTimeSeriesManager.java
  50. 2 0
      jetlinks-components/elasticsearch-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  51. 0 12
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGateway.java
  52. 403 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayHelper.java
  53. 63 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayManager.java
  54. 33 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/GatewayConfiguration.java
  55. 57 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/ChildDeviceGatewayProvider.java
  56. 101 58
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DefaultDeviceGatewayManager.java
  57. 8 2
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProperties.java
  58. 2 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayPropertiesManager.java
  59. 55 3
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProvider.java
  60. 34 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProviders.java
  61. 1 0
      jetlinks-components/gateway-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  62. 64 0
      jetlinks-components/network-component/http-component/pom.xml
  63. 79 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/DefaultHttpRequestMessage.java
  64. 39 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/DefaultHttpResponseMessage.java
  65. 39 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/VertxWebUtils.java
  66. 103 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/HttpDeviceSession.java
  67. 206 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/HttpServerDeviceGateway.java
  68. 117 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/HttpServerDeviceGatewayProvider.java
  69. 102 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/HttpServerExchangeMessage.java
  70. 97 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/UnknownHttpDeviceSession.java
  71. 130 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/HttpExchange.java
  72. 122 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/HttpRequest.java
  73. 74 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/HttpResponse.java
  74. 49 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/HttpServer.java
  75. 140 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/DefaultHttpServerProvider.java
  76. 54 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/HttpServerConfig.java
  77. 385 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/VertxHttpExchange.java
  78. 167 0
      jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/VertxHttpServer.java
  79. 28 0
      jetlinks-components/network-component/http-component/src/test/resources/client.csr
  80. BIN
      jetlinks-components/network-component/http-component/src/test/resources/client.p12
  81. 46 0
      jetlinks-components/network-component/http-component/src/test/resources/client.pem
  82. 1 1
      jetlinks-components/network-component/mqtt-component/src/test/resources/create.sh
  83. 5 0
      jetlinks-components/network-component/http-component/src/test/resources/ec_private.pem
  84. 4 0
      jetlinks-components/network-component/http-component/src/test/resources/ec_public.pem
  85. BIN
      jetlinks-components/network-component/http-component/src/test/resources/keyStore.jks
  86. 1 0
      jetlinks-components/network-component/http-component/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
  87. 28 0
      jetlinks-components/network-component/http-component/src/test/resources/server.csr
  88. BIN
      jetlinks-components/network-component/http-component/src/test/resources/server.p12
  89. 46 0
      jetlinks-components/network-component/http-component/src/test/resources/server.pem
  90. BIN
      jetlinks-components/network-component/http-component/src/test/resources/trustStore.jks
  91. BIN
      jetlinks-components/network-component/http-component/src/test/resources/trustStore.p12
  92. 54 0
      jetlinks-components/network-component/http-component/src/test/resources/trustStore.pem
  93. 29 4
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClient.java
  94. 45 7
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClientProperties.java
  95. 80 61
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClientProvider.java
  96. 81 52
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/VertxMqttClient.java
  97. 0 39
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskConfiguration.java
  98. 0 131
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskExecutorProvider.java
  99. 0 111
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttRuleDataCodec.java
  100. 0 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttTopics.java

+ 13 - 0
jetlinks-components/common-component/pom.xml

@@ -60,5 +60,18 @@
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>com.cronutils</groupId>
+            <artifactId>cron-utils</artifactId>
+            <version>9.2.0</version>
+            <scope>compile</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.glassfish</groupId>
+                    <artifactId>javax.el</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
     </dependencies>
 </project>

+ 1 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/ConfigMetadataConstants.java

@@ -18,5 +18,6 @@ public interface ConfigMetadataConstants {
     ConfigKey<Boolean> allowInput = ConfigKey.of("allowInput", "允许输入", Boolean.TYPE);
     ConfigKey<Boolean> required = ConfigKey.of("required", "是否必填", Boolean.TYPE);
 
+    ConfigKey<String> format = ConfigKey.of("format", "格式", String.class);
 
 }

+ 12 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyConstants.java

@@ -17,10 +17,22 @@ public interface PropertyConstants {
     Key<String> orgId = Key.of("orgId");
 
     Key<String> deviceName = Key.of("deviceName");
+    //产品名称
+    Key<String> productName = Key.of("productName");
 
     Key<String> productId = Key.of("productId");
     Key<String> uid = Key.of("_uid");
+    //设备创建者
+    Key<String> creatorId = Key.of("creatorId");
 
+    //设备接入网关ID
+    Key<String> accessId = Key.of("accessId");
+
+    /**
+     * 设备接入方式
+     * @see org.jetlinks.community.gateway.supports.DeviceGatewayProvider#getId
+     */
+    Key<String> accessProvider = Key.of("accessProvider");
 
     @SuppressWarnings("all")
     static <T> Optional<T> getFromMap(ConfigKey<T> key, Map<String, Object> map) {

+ 35 - 12
jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyMetadataConstants.java

@@ -1,16 +1,21 @@
 package org.jetlinks.community;
 
+import org.jetlinks.community.utils.ConverterUtils;
 import org.jetlinks.core.message.DeviceMessage;
 import org.jetlinks.core.message.HeaderKey;
 import org.jetlinks.core.metadata.PropertyMetadata;
 import org.jetlinks.reactor.ql.utils.CastUtils;
 
+import java.util.*;
+
 public interface PropertyMetadataConstants {
 
+
     /**
      * 属性来源
      */
     interface Source {
+
         //数据来源
         String id = "source";
 
@@ -45,18 +50,6 @@ public interface PropertyMetadataConstants {
                            .orElse(false);
         }
 
-        /**
-         * 判断属性是否为规则
-         *
-         * @param metadata 物模型
-         * @return 是否规则
-         */
-        static boolean isRule(PropertyMetadata metadata) {
-            return  metadata
-                .getExpand(id)
-                .map(rule::equals)
-                .orElse(false);
-        }
     }
 
     /**
@@ -97,4 +90,34 @@ public interface PropertyMetadataConstants {
                 .orElse(true);
         }
     }
+
+    interface Metrics {
+        String id = "metrics";
+
+
+        static Map<String,Object> metricsToExpands(List<PropertyMetric> metrics) {
+            return Collections.singletonMap(id, metrics);
+        }
+
+        static List<PropertyMetric> getMetrics(PropertyMetadata metadata) {
+            return metadata
+                .getExpand(id)
+                .map(obj -> ConverterUtils.convertToList(obj, PropertyMetric::of))
+                .orElseGet(Collections::emptyList);
+        }
+
+        static Optional<PropertyMetric> getMetric(PropertyMetadata metadata, String metric) {
+            return metadata
+                .getExpand(id)
+                .map(obj -> {
+                    for (PropertyMetric propertyMetric : ConverterUtils.convertToList(obj, PropertyMetric::of)) {
+                        if(Objects.equals(metric, propertyMetric.getId())){
+                            return propertyMetric;
+                        }
+                    }
+                    return null;
+                });
+        }
+
+    }
 }

+ 64 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyMetric.java

@@ -0,0 +1,64 @@
+package org.jetlinks.community;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.community.utils.ConverterUtils;
+import org.springframework.util.StringUtils;
+
+import javax.validation.constraints.NotBlank;
+import java.util.Map;
+import java.util.function.Function;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PropertyMetric {
+    @Schema(description = "指标ID")
+    @NotBlank
+    private String id;
+
+    @Schema(description = "名称")
+    @NotBlank
+    private String name;
+
+    @Schema(description = "值,范围值使用逗号分隔")
+    private Object value;
+
+    @Schema(description = "是否为范围值")
+    private boolean range;
+
+    @Schema(description = "其他拓展配置")
+    private Map<String, Object> expands;
+
+    public Object castValue() {
+        if (value == null) {
+            return null;
+        }
+        if (range) {
+            return ConverterUtils.tryConvertToList(value, Function.identity());
+        }
+        return value;
+    }
+
+    public PropertyMetric merge(PropertyMetric another) {
+        if (!StringUtils.hasText(this.name)) {
+            this.setValue(another.value);
+        }
+        return this;
+    }
+
+    public static PropertyMetric of(String id, String name, Object value) {
+        PropertyMetric metric = new PropertyMetric();
+        metric.setId(id);
+        metric.setName(name);
+        metric.setValue(value);
+        return metric;
+    }
+
+    public static PropertyMetric of(Object mapMetric) {
+        return FastBeanCopier.copy(mapMetric, new PropertyMetric());
+    }
+}

+ 10 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/TimerIterable.java

@@ -0,0 +1,10 @@
+package org.jetlinks.community;
+
+import java.time.ZonedDateTime;
+import java.util.Iterator;
+
+public interface TimerIterable {
+
+    Iterator<ZonedDateTime> iterator(ZonedDateTime baseTime);
+
+}

+ 391 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/TimerSpec.java

@@ -0,0 +1,391 @@
+package org.jetlinks.community;
+
+import com.cronutils.builder.CronBuilder;
+import com.cronutils.model.Cron;
+import com.cronutils.model.definition.CronConstraintsFactory;
+import com.cronutils.model.definition.CronDefinition;
+import com.cronutils.model.definition.CronDefinitionBuilder;
+import com.cronutils.model.field.expression.FieldExpression;
+import com.cronutils.model.field.expression.FieldExpressionFactory;
+import com.cronutils.model.time.ExecutionTime;
+import com.cronutils.parser.CronParser;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.util.Assert;
+
+import javax.validation.ValidationException;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+@Getter
+@Setter
+public class TimerSpec implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "触发方式")
+    @NotNull
+    private Trigger trigger;
+
+    //Cron表达式
+    @Schema(description = "触发方式为[cron]时不能为空")
+    private String cron;
+
+    @Schema(description = "执行的时间.为空则表示每天,触发方式为[week]则为1-7,触发方式为[month]时则为1-31")
+    private Set<Integer> when;
+
+    @Schema(description = "执行模式,一次还是周期执行")
+    private ExecuteMod mod;
+
+    @Schema(description = "执行模式为[period]时不能为空")
+    private Period period;
+
+    @Schema(description = "执行模式为[once]时不能为空")
+    private Once once;
+
+    public static TimerSpec cron(String cron) {
+        TimerSpec spec = new TimerSpec();
+        spec.cron = cron;
+        spec.trigger = Trigger.cron;
+        return spec;
+    }
+
+    public Predicate<LocalDateTime> createRangeFilter() {
+        if (CollectionUtils.isEmpty(when)) {
+            return ignore -> true;
+        }
+        if (trigger == Trigger.week) {
+            return date -> when.contains(date.getDayOfWeek().getValue());
+        } else if (trigger == Trigger.month) {
+            return date -> when.contains(date.getDayOfMonth());
+        }
+        return ignore -> true;
+    }
+
+    public Predicate<LocalDateTime> createTimeFilter() {
+        Predicate<LocalDateTime> range = createRangeFilter();
+        //周期执行指定了to,表示只在时间范围段内执行
+        if (mod == ExecuteMod.period) {
+            LocalTime to = period.toLocalTime();
+            LocalTime from = period.fromLocalTime();
+            Predicate<LocalDateTime> predicate
+                = time -> time.toLocalTime().compareTo(from) >= 0;
+            if (to != null) {
+                predicate = predicate.and(time -> time.toLocalTime().compareTo(to) <= 0);
+            }
+            return predicate.and(range);
+        }
+        return range;
+    }
+
+    public String toCronExpression() {
+        return toCron().asString();
+    }
+
+    private static CronDefinition quartz() {
+        return CronDefinitionBuilder
+            .defineCron()
+            .withSeconds()
+            .withValidRange(0, 59)
+            .and()
+            .withMinutes()
+            .withValidRange(0, 59)
+            .and()
+            .withHours()
+            .withValidRange(0, 23)
+            .and()
+            .withDayOfMonth()
+            .withValidRange(1, 31)
+            .supportsL()
+            .supportsW()
+            .supportsLW()
+            .supportsQuestionMark()
+            .and()
+            .withMonth()
+            .withValidRange(1, 12)
+            .and()
+            .withDayOfWeek()
+            .withValidRange(1, 7)
+            .withMondayDoWValue(1)
+            .supportsHash()
+            .supportsL()
+            .supportsQuestionMark()
+            .and()
+            .withYear()
+            .withValidRange(1970, 2099)
+            .withStrictRange()
+            .optional()
+            .and()
+            .withCronValidation(CronConstraintsFactory.ensureEitherDayOfWeekOrDayOfMonth())
+            .instance();
+    }
+
+    public Cron toCron() {
+        CronDefinition definition = quartz();
+        if (trigger == Trigger.cron || trigger == null) {
+            Assert.hasText(cron, "error.scene_rule_timer_cron_cannot_be_empty");
+            return new CronParser(definition).parse(cron).validate();
+        }
+
+        CronBuilder builder = CronBuilder.cron(definition);
+        builder.withYear(FieldExpression.always());
+        builder.withMonth(FieldExpression.always());
+
+        FieldExpression range;
+        if (CollectionUtils.isNotEmpty(when)) {
+            FieldExpression expr = null;
+            for (Integer integer : when) {
+                if (expr == null) {
+                    expr = FieldExpressionFactory.on(integer);
+                } else {
+                    expr = expr.and(FieldExpressionFactory.on(integer));
+                }
+            }
+            range = expr;
+        } else {
+            range = FieldExpressionFactory.questionMark();
+        }
+
+        if (trigger == Trigger.week) {
+            builder.withDoM(FieldExpressionFactory.questionMark())
+                   .withDoW(range);
+        } else if (trigger == Trigger.month) {
+            builder.withDoM(range)
+                   .withDoW(FieldExpressionFactory.questionMark());
+        }
+
+        //执行一次
+        if (mod == ExecuteMod.once) {
+            LocalTime time = once.localTime();
+            builder.withHour(FieldExpressionFactory.on(time.getHour()));
+            builder.withMinute(FieldExpressionFactory.on(time.getMinute()));
+            builder.withSecond(FieldExpressionFactory.on(time.getSecond()));
+        }
+        //周期执行
+        if (mod == ExecuteMod.period) {
+            LocalTime time = period.fromLocalTime();
+            PeriodUnit unit = period.unit;
+            if (unit == PeriodUnit.hours) {
+                builder.withHour(FieldExpressionFactory.every(FieldExpressionFactory.on(time.getHour()), period.every))
+                       .withMinute(FieldExpressionFactory.on(time.getMinute()))
+                       .withSecond(FieldExpressionFactory.on(time.getSecond()));
+            } else if (unit == PeriodUnit.minutes) {
+                builder
+                    .withHour(FieldExpressionFactory.always())
+                    .withMinute(FieldExpressionFactory.every(FieldExpressionFactory.on(time.getMinute()), period.every))
+                    .withSecond(FieldExpressionFactory.on(time.getSecond()));
+            } else if (unit == PeriodUnit.seconds) {
+                builder
+                    .withHour(FieldExpressionFactory.always())
+                    .withMinute(FieldExpressionFactory.always())
+                    .withSecond(FieldExpressionFactory.every(FieldExpressionFactory.on(time.getSecond()), period.every));
+            }
+        }
+        return builder.instance().validate();
+    }
+
+    public void validate() {
+        if (trigger == null) {
+            Assert.hasText(cron, "error.scene_rule_timer_cron_cannot_be_empty");
+        }
+        try {
+            toCronExpression();
+        } catch (Throwable e) {
+            throw new ValidationException("error.cron_format_error");
+        }
+
+    }
+
+    @Getter
+    @Setter
+    @AllArgsConstructor(staticName = "of")
+    @NoArgsConstructor
+    public static class Once {
+        //时间点
+        @Schema(description = "时间点.格式:[hh:mm],或者[hh:mm:ss]")
+        @NotBlank
+        private String time;
+
+        public LocalTime localTime() {
+            return parsTime(time);
+        }
+    }
+
+    @Getter
+    @Setter
+    public static class Period {
+        //周期执行的时间区间
+        @Schema(description = "执行时间范围从.格式:[hh:mm],或者[hh:mm:ss]")
+        private String from;
+        @Schema(description = "执行时间范围止.格式:[hh:mm],或者[hh:mm:ss]")
+        private String to;
+
+        @Schema(description = "周期值,如:每[every][unit]执行一次")
+        private int every;
+
+        @Schema(description = "周期执行单位")
+        private PeriodUnit unit;
+
+        public LocalTime fromLocalTime() {
+            return parsTime(from);
+        }
+
+        public LocalTime toLocalTime() {
+            return parsTime(to);
+        }
+
+    }
+
+    private static LocalTime parsTime(String time) {
+        return LocalTime.parse(time);
+    }
+
+    /**
+     * 创建一个下一次执行时间间隔构造器,通过构造器来获取基准时间间隔
+     * <pre>{@code
+     *
+     *   Function<ZonedDateTime, Duration> builder = nextDurationBuilder();
+     *
+     *   Duration duration =  builder.apply(ZonedDateTime.now());
+     *
+     * }</pre>
+     *
+     * @return 构造器
+     */
+    public Function<ZonedDateTime, Duration> nextDurationBuilder() {
+        Function<ZonedDateTime, ZonedDateTime> nextTime = nextTimeBuilder();
+        return time -> Duration.between(time, nextTime.apply(time));
+    }
+
+    /**
+     * 创建一个时间构造器,通过构造器来获取下一次时间
+     * <pre>{@code
+     *
+     *   Function<ZonedDateTime, ZonedDateTime> builder = nextTimeBuilder();
+     *
+     *   ZonedDateTime nextTime =  builder.apply(ZonedDateTime.now());
+     *
+     * }</pre>
+     *
+     * @return 构造器
+     */
+    public Function<ZonedDateTime, ZonedDateTime> nextTimeBuilder() {
+        TimerIterable it = iterable();
+        return time -> it.iterator(time).next();
+    }
+
+    static int MAX_IT_TIMES = 10000;
+
+    private TimerIterable cronIterable() {
+        Cron cron = this.toCron();
+        ExecutionTime executionTime = ExecutionTime.forCron(cron);
+        Predicate<LocalDateTime> filter = createTimeFilter();
+        return baseTime -> new Iterator<ZonedDateTime>() {
+            ZonedDateTime current = baseTime;
+
+            @Override
+            public boolean hasNext() {
+                return current != null;
+            }
+
+            @Override
+            public ZonedDateTime next() {
+                if (!hasNext()) {
+                    throw new NoSuchElementException();
+                }
+                ZonedDateTime dateTime = current;
+                int i = 0;
+                do {
+                    dateTime = executionTime
+                        .nextExecution(dateTime)
+                        .orElse(null);
+                    if (dateTime == null) {
+                        i++;
+                        continue;
+                    }
+                    if (filter.test(dateTime.toLocalDateTime())) {
+                        break;
+                    }
+                } while (i < MAX_IT_TIMES);
+                return current = dateTime;
+            }
+        };
+    }
+
+    private TimerIterable periodIterable() {
+        Assert.notNull(period, "period can not be null");
+        Predicate<LocalDateTime> filter = createTimeFilter();
+
+        Duration duration = Duration.of(period.every, period.unit.temporal);
+        LocalTime time = period.fromLocalTime();
+        return baseTime -> new Iterator<ZonedDateTime>() {
+            ZonedDateTime current = baseTime;
+
+            @Override
+            public boolean hasNext() {
+                return true;
+            }
+
+            @Override
+            public ZonedDateTime next() {
+                ZonedDateTime dateTime = current;
+                int max = MAX_IT_TIMES;
+                do {
+                    dateTime = dateTime.plus(duration);
+                    if (filter.test(dateTime.toLocalDateTime())) {
+                        break;
+                    }
+                    max--;
+                } while (max > 0);
+
+                return current = dateTime;
+            }
+        };
+    }
+
+    public TimerIterable iterable() {
+        return mod == ExecuteMod.period ? periodIterable() : cronIterable();
+    }
+
+    public List<ZonedDateTime> getNextExecuteTimes(ZonedDateTime from, long times) {
+        List<ZonedDateTime> timeList = new ArrayList<>((int) times);
+        Iterator<ZonedDateTime> it = iterable().iterator(from);
+        for (long i = 0; i < times; i++) {
+            timeList.add(it.next());
+        }
+        return timeList;
+    }
+
+    public enum Trigger {
+        week,
+        month,
+        cron
+    }
+
+    public enum ExecuteMod {
+        period,
+        once
+    }
+
+    @AllArgsConstructor
+    public enum PeriodUnit {
+        seconds(ChronoUnit.SECONDS),
+        minutes(ChronoUnit.MINUTES),
+        hours(ChronoUnit.HOURS);
+        private final TemporalUnit temporal;
+
+    }
+}

+ 1 - 1
jetlinks-components/common-component/src/main/java/org/jetlinks/community/Version.java

@@ -8,6 +8,6 @@ public class Version {
 
     private final String edition = "community";
 
-    private final String version = "1.12.0-SNAPSHOT";
+    private final String version = "2.0.0-SNAPSHOT";
 
 }

+ 27 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java

@@ -12,7 +12,15 @@ import org.jetlinks.community.config.ConfigScopeCustomizer;
 import org.jetlinks.community.config.ConfigScopeProperties;
 import org.jetlinks.community.config.SimpleConfigManager;
 import org.jetlinks.community.config.entity.ConfigEntity;
+import org.jetlinks.community.reference.DataReferenceManager;
+import org.jetlinks.community.reference.DataReferenceProvider;
+import org.jetlinks.community.reference.DefaultDataReferenceManager;
+import org.jetlinks.community.resource.DefaultResourceManager;
+import org.jetlinks.community.resource.ResourceManager;
+import org.jetlinks.community.resource.ResourceProvider;
+import org.jetlinks.community.resource.initialize.PermissionResourceProvider;
 import org.jetlinks.community.utils.TimeUtils;
+import org.jetlinks.core.rpc.RpcManager;
 import org.jetlinks.reactor.ql.feature.Feature;
 import org.jetlinks.reactor.ql.supports.DefaultReactorQLMetadata;
 import org.jetlinks.reactor.ql.utils.CastUtils;
@@ -136,5 +144,24 @@ public class CommonConfiguration {
         return configManager;
     }
 
+    @Bean
+    public PermissionResourceProvider permissionResourceProvider(){
+        return new PermissionResourceProvider();
+    }
+
+    @Bean
+    public ResourceManager resourceManager(ObjectProvider<ResourceProvider> providers) {
+        DefaultResourceManager manager = new DefaultResourceManager();
+        providers.forEach(manager::addProvider);
+        return manager;
+    }
 
+    @Bean
+    public DataReferenceManager dataReferenceManager(ObjectProvider<DataReferenceProvider> provider) {
+        DefaultDataReferenceManager referenceManager = new DefaultDataReferenceManager();
+
+        provider.forEach(referenceManager::addStrategy);
+
+        return referenceManager;
+    }
 }

+ 26 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceInfo.java

@@ -0,0 +1,26 @@
+package org.jetlinks.community.reference;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+@Getter
+@Setter
+@AllArgsConstructor(staticName = "of")
+@NoArgsConstructor
+public class DataReferenceInfo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private String dataId;
+
+    private String referenceType;
+
+    private String referenceId;
+
+    private String referenceName;
+
+}

+ 70 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceManager.java

@@ -0,0 +1,70 @@
+package org.jetlinks.community.reference;
+
+import org.apache.commons.collections4.CollectionUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * 数据引用管理器,用于获取、判断指定的数据是否被其他地方所引用.
+ * <p>
+ * 可以通过实现接口: {@link DataReferenceProvider}来定义数据引用者.
+ * <p>
+ * 使用场景: 在对一些数据进行删除操作时,可以通过判断引用关系来阻止删除.
+ * 比如: 删除网络组件时,如果网络组件已经被其他地方使用,则不能删除.
+ *
+ * @author zhouhao
+ * @see DataReferenceProvider
+ * @since 2.0
+ */
+public interface DataReferenceManager {
+
+    //数据类型: 设备接入网关
+    String TYPE_DEVICE_GATEWAY = "device-gateway";
+    //数据类型: 网络组件
+    String TYPE_NETWORK = "network";
+    //数据类型:关系配置
+    String TYPE_RELATION = "relation";
+
+    /**
+     * 判断指定数据类型的数据是否已经被其他地方所引用
+     *
+     * @param dataType 数据类型
+     * @param dataId   数据ID
+     * @return 是否已经被引用
+     */
+    Mono<Boolean> isReferenced(String dataType, String dataId);
+
+    /**
+     * 获取指定类型数据的引用关系
+     *
+     * @param dataType 数据类型
+     * @param dataId   数据ID
+     * @return 引用信息
+     */
+    Flux<DataReferenceInfo> getReferences(String dataType, String dataId);
+
+    /**
+     * 获取指定类型数据的全部引用关系
+     *
+     * @param dataType 数据类型
+     * @return 引用信息
+     */
+    Flux<DataReferenceInfo> getReferences(String dataType);
+
+
+    /**
+     * 断言数据没有被引用,如果存在引用,则抛出异常: {@link DataReferencedException}
+     *
+     * @param dataType 数据类型
+     * @param dataId   数据ID
+     * @return void
+     */
+    default Mono<Void> assertNotReferenced(String dataType, String dataId) {
+        return this
+            .getReferences(dataType, dataId)
+            .collectList()
+            .filter(CollectionUtils::isNotEmpty)
+            .flatMap(list -> Mono.error(new DataReferencedException(dataType, dataId, list)));
+    }
+
+}

+ 36 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceProvider.java

@@ -0,0 +1,36 @@
+package org.jetlinks.community.reference;
+
+import org.jetlinks.community.strategy.Strategy;
+import reactor.core.publisher.Flux;
+
+/**
+ * 数据引用提供商,用于提供获取对应类型的引用信息
+ *
+ * @author zhouhao
+ * @since 2.0
+ */
+public interface DataReferenceProvider extends Strategy {
+
+    /**
+     * 引用的数据类型
+     *
+     * @see DataReferenceManager
+     */
+    @Override
+    String getId();
+
+    /**
+     * 获取数据引用信息
+     *
+     * @param dataId 数据ID
+     * @return 引用信息
+     */
+    Flux<DataReferenceInfo> getReference(String dataId);
+
+    /**
+     * 获取全部数据引用信息
+     *
+     * @return 引用信息
+     */
+    Flux<DataReferenceInfo> getReferences();
+}

+ 30 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferencedException.java

@@ -0,0 +1,30 @@
+package org.jetlinks.community.reference;
+
+import lombok.Getter;
+import org.hswebframework.web.exception.I18nSupportException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.util.List;
+
+@Getter
+@ResponseStatus(HttpStatus.BAD_REQUEST)
+public class DataReferencedException extends I18nSupportException {
+
+    private final String dataType;
+    private final String dataId;
+
+    private final List<DataReferenceInfo> referenceList;
+
+
+    public DataReferencedException(String dataType,
+                                   String dataId,
+                                   List<DataReferenceInfo> referenceList) {
+        this.dataType = dataType;
+        this.dataId = dataId;
+        this.referenceList = referenceList;
+
+        super.setI18nCode("error.data.referenced");
+    }
+
+}

+ 26 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DefaultDataReferenceManager.java

@@ -0,0 +1,26 @@
+package org.jetlinks.community.reference;
+
+import org.jetlinks.community.strategy.StaticStrategyManager;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class DefaultDataReferenceManager extends StaticStrategyManager<DataReferenceProvider> implements DataReferenceManager {
+
+    @Override
+    public Mono<Boolean> isReferenced(String dataType, String dataId) {
+        return this
+            .getReferences(dataType, dataId)
+            .hasElements();
+    }
+
+    @Override
+    public Flux<DataReferenceInfo> getReferences(String dataType, String dataId) {
+        return doWithFlux(dataType, provider -> provider.getReference(dataId));
+    }
+
+    @Override
+    public Flux<DataReferenceInfo> getReferences(String dataType) {
+        return doWithFlux(dataType, DataReferenceProvider::getReferences);
+    }
+}

+ 75 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/ClassPathJsonResourceProvider.java

@@ -0,0 +1,75 @@
+package org.jetlinks.community.resource;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.StreamUtils;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+@Slf4j
+public abstract class ClassPathJsonResourceProvider implements ResourceProvider {
+
+    @Getter
+    private final String type;
+    private final String filePath;
+    private static final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
+    private List<Resource> cache;
+
+    public ClassPathJsonResourceProvider(String type, String filePath) {
+        this.type = type;
+        this.filePath = filePath;
+    }
+
+    @Override
+    public final Flux<Resource> getResources() {
+        return Flux.fromIterable(cache == null ? cache = read() : cache);
+    }
+
+    @Override
+    public final Flux<Resource> getResources(Collection<String> id) {
+        Set<String> filter = new HashSet<>(id);
+        return getResources()
+            .filter(res -> filter.contains(res.getId()));
+    }
+
+
+    private List<Resource> read() {
+        List<Resource> resources = new ArrayList<>();
+        try {
+            log.debug("start load {} resource [{}]", type, filePath);
+            for (org.springframework.core.io.Resource resource : resourcePatternResolver.getResources(filePath)) {
+                log.debug("loading {} resource {}", type, resource);
+                try (InputStream inputStream = resource.getInputStream()) {
+                    int index = 0;
+                    for (JSONObject json : JSON.parseArray(StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8), JSONObject.class)) {
+                        index++;
+                        String id = getResourceId(json);
+                        if (StringUtils.hasText(id)) {
+                            resources.add(SimpleResource.of(id, type, json.toJSONString()));
+                        } else {
+                            log.warn("{} resource [{}] id (index:{}) is empty : {}", type, resource, index, json);
+                        }
+                    }
+                } catch (Throwable err) {
+                    log.debug("load {} resource {} error", type, resource, err);
+                }
+            }
+        } catch (Throwable e) {
+            log.warn("load {} resource [{}] error", type, filePath, e);
+            return Collections.emptyList();
+        }
+        return resources;
+    }
+
+    protected String getResourceId(JSONObject data) {
+        return data.getString("id");
+    }
+}

+ 37 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/DefaultResourceManager.java

@@ -0,0 +1,37 @@
+package org.jetlinks.community.resource;
+
+import org.springframework.util.CollectionUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class DefaultResourceManager implements ResourceManager {
+    private final Map<String, ResourceProvider> providers = new ConcurrentHashMap<>();
+
+    public DefaultResourceManager() {
+    }
+
+    public void addProvider(ResourceProvider provider) {
+        providers.put(provider.getType(), provider);
+    }
+
+    @Override
+    public Flux<Resource> getResources(String type) {
+        return getResources(type, null);
+    }
+
+    @Override
+    public Flux<Resource> getResources(String type, Collection<String> id) {
+        return this
+            .getProvider(type)
+            .flatMapMany(provider -> CollectionUtils.isEmpty(id) ? provider.getResources() : provider.getResources(id));
+    }
+
+    private Mono<ResourceProvider> getProvider(String type) {
+        return Mono.justOrEmpty(providers.get(type));
+    }
+
+}

+ 16 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/Resource.java

@@ -0,0 +1,16 @@
+package org.jetlinks.community.resource;
+
+import java.lang.reflect.Type;
+
+public interface Resource {
+
+    String getId();
+
+    String getType();
+
+    <T> T as(Class<T> type);
+
+    <T> T as(Type type);
+
+    String asString();
+}

+ 13 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/ResourceManager.java

@@ -0,0 +1,13 @@
+package org.jetlinks.community.resource;
+
+import reactor.core.publisher.Flux;
+
+import java.util.Collection;
+
+public interface ResourceManager {
+
+    Flux<Resource> getResources(String type);
+
+    Flux<Resource> getResources(String type, Collection<String> id);
+
+}

+ 15 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/ResourceProvider.java

@@ -0,0 +1,15 @@
+package org.jetlinks.community.resource;
+
+import reactor.core.publisher.Flux;
+
+import java.util.Collection;
+
+public interface ResourceProvider {
+
+    String getType();
+
+    Flux<Resource> getResources();
+
+    Flux<Resource> getResources(Collection<String> id);
+
+}

+ 34 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/SimpleResource.java

@@ -0,0 +1,34 @@
+package org.jetlinks.community.resource;
+
+import com.alibaba.fastjson.JSON;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.lang.reflect.Type;
+
+@AllArgsConstructor(staticName = "of")
+@NoArgsConstructor
+@Getter
+@Setter
+public class SimpleResource implements Resource {
+    private String id;
+    private String type;
+    private String resource;
+
+    @Override
+    public <T> T as(Class<T> type) {
+        return JSON.parseObject(resource, type);
+    }
+
+    @Override
+    public <T> T as(Type type) {
+        return JSON.parseObject(resource, type);
+    }
+
+    @Override
+    public String asString() {
+        return resource;
+    }
+}

+ 42 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/initialize/PermissionResourceProvider.java

@@ -0,0 +1,42 @@
+package org.jetlinks.community.resource.initialize;
+
+import com.alibaba.fastjson.JSON;
+import org.hswebframework.web.authorization.define.AuthorizeDefinitionInitializedEvent;
+import org.hswebframework.web.authorization.define.MergedAuthorizeDefinition;
+import org.jetlinks.community.resource.Resource;
+import org.jetlinks.community.resource.ResourceProvider;
+import org.jetlinks.community.resource.SimpleResource;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+import java.util.Collection;
+
+public class PermissionResourceProvider implements ResourceProvider {
+    public static final String type = "permission";
+
+    private final MergedAuthorizeDefinition definition = new MergedAuthorizeDefinition();
+
+    @Override
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public Flux<Resource> getResources() {
+        return Flux
+            .fromIterable(definition.getResources())
+            .map(def -> SimpleResource.of(def.getId(), getType(), JSON.toJSONString(def)));
+    }
+
+    @Override
+    public Flux<Resource> getResources(Collection<String> id) {
+        return getResources()
+            .filter(resource -> id == null || id.contains(resource.getId()));
+    }
+
+    @EventListener
+    public void handleInitializedEvent(AuthorizeDefinitionInitializedEvent event) {
+        definition.merge(event.getAllDefinition());
+    }
+}

+ 51 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/strategy/StaticStrategyManager.java

@@ -0,0 +1,51 @@
+package org.jetlinks.community.strategy;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+public class StaticStrategyManager<S
+    extends Strategy>
+    implements StrategyManager<S> {
+
+    private final Map<String, Mono<S>> strategies = new ConcurrentHashMap<>();
+
+    public void addStrategy(S strategy) {
+        this.addStrategy(strategy.getId(), Mono.just(strategy));
+    }
+
+    public void addStrategy(String strategyId, Mono<S> providerMono) {
+        strategies.put(strategyId, providerMono);
+    }
+
+    @Override
+    public final Mono<S> getStrategy(String strategyId) {
+        return strategies.getOrDefault(strategyId, Mono.empty());
+    }
+
+    @Override
+    public final Flux<S> getStrategies() {
+        return Flux.concat(strategies.values());
+    }
+
+    protected final <T> Mono<T> doWithMono(String strategy, Function<S, Mono<T>> executor) {
+        return this
+            .getStrategy(strategy)
+            .switchIfEmpty(onStrategyNotFound(strategy))
+            .flatMap(executor);
+    }
+
+    protected final <T> Flux<T> doWithFlux(String strategy, Function<S, Flux<T>> executor) {
+        return this
+            .getStrategy(strategy)
+            .switchIfEmpty(onStrategyNotFound(strategy))
+            .flatMapMany(executor);
+    }
+
+    protected <T> Mono<T> onStrategyNotFound(String strategy){
+        return Mono.empty();
+    }
+}

+ 17 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/strategy/Strategy.java

@@ -0,0 +1,17 @@
+package org.jetlinks.community.strategy;
+
+/**
+ * 策略接口定义
+ *
+ * @author zhouhao
+ * @since 2.0
+ */
+public interface Strategy {
+
+    /**
+     * @return 策略ID
+     */
+    String getId();
+
+
+}

+ 12 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/strategy/StrategyManager.java

@@ -0,0 +1,12 @@
+package org.jetlinks.community.strategy;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface StrategyManager<S extends Strategy> {
+
+    Mono<S> getStrategy(String provider);
+
+    Flux<S> getStrategies();
+
+}

+ 123 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java

@@ -0,0 +1,123 @@
+package org.jetlinks.community.topic;
+
+import lombok.Generated;
+import org.jetlinks.core.utils.StringBuilderUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public interface Topics {
+
+    static String creator(String creatorId, String topic) {
+        return StringBuilderUtils.buildString(creatorId, topic, Topics::creator);
+    }
+
+    static void creator(String creatorId, String topic, StringBuilder builder) {
+        builder.append("/user/").append(creatorId);
+        if (topic.charAt(0) != '/') {
+            builder.append('/');
+        }
+        builder.append(topic);
+    }
+
+    /**
+     * 根据绑定信息构造topic
+     *
+     * @param bindings 绑定信息
+     * @param topic    topic
+     * @return topic
+     */
+    static List<String> bindings(List<Map<String, Object>> bindings, String topic) {
+        List<String> topics = new ArrayList<>(bindings.size());
+        for (Map<String, Object> binding : bindings) {
+            topics.add(binding(String.valueOf(binding.get("type")), String.valueOf(binding.get("id")), topic));
+        }
+        return topics;
+    }
+
+    static void binding(String type, String id, String topic, StringBuilder builder) {
+        builder.append('/')
+               .append(type)
+               .append('/')
+               .append(id);
+        if (topic.charAt(0) != '/') {
+            builder.append('/');
+        }
+        builder.append(topic);
+    }
+
+    static String binding(String type, String id, String topic) {
+        return StringBuilderUtils.buildString(type, id, topic, Topics::binding);
+    }
+
+    @Deprecated
+    static String tenantMember(String memberId, String topic) {
+        if (!topic.startsWith("/")) {
+            topic = "/" + topic;
+        }
+        return String.join("", "/member/", memberId, topic);
+    }
+
+    @Deprecated
+    static List<String> tenantMembers(List<String> members, String topic) {
+        return members
+            .stream()
+            .map(id -> tenantMember(id, topic))
+            .collect(Collectors.toList());
+    }
+
+    String allDeviceRegisterEvent = "/_sys/registry-device/*/register";
+    String allDeviceUnRegisterEvent = "/_sys/registry-device/*/unregister";
+    String allDeviceMetadataChangedEvent = "/_sys/registry-device/*/metadata";
+
+    @Generated
+    static String deviceRegisterEvent(String deviceId) {
+        return registryDeviceEvent(deviceId, "register");
+    }
+
+    @Generated
+    static String deviceUnRegisterEvent(String deviceId) {
+        return registryDeviceEvent(deviceId, "unregister");
+    }
+
+    @Generated
+    static String deviceMetadataChangedEvent(String deviceId) {
+        return registryDeviceEvent(deviceId, "metadata");
+    }
+
+    String allProductRegisterEvent = "/_sys/registry-product/*/register";
+    String allProductUnRegisterEvent = "/_sys/registry-product/*/unregister";
+    String allProductMetadataChangedEvent = "/_sys/registry-product/*/metadata";
+
+    @Generated
+    static String productRegisterEvent(String deviceId) {
+        return registryProductEvent(deviceId, "register");
+    }
+
+    @Generated
+    static String productUnRegisterEvent(String deviceId) {
+        return registryProductEvent(deviceId, "unregister");
+    }
+
+    @Generated
+    static String productMetadataChangedEvent(String deviceId) {
+        return registryProductEvent(deviceId, "metadata");
+    }
+
+
+    static String registryDeviceEvent(String deviceId, String event) {
+        return "/_sys/registry-device/" + deviceId + "/" + event;
+    }
+
+    static String registryProductEvent(String deviceId, String event) {
+        return "/_sys/registry-product/" + deviceId + "/" + event;
+    }
+
+    static String alarm(String targetType, String targetId, String alarmId) {
+        //  /alarm/{targetType}/{targetId}/{alarmId}/record
+        return String.join("", "/alarm/", targetType, "/", targetId, "/", alarmId, "/record");
+    }
+}

+ 277 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ReactorUtils.java

@@ -0,0 +1,277 @@
+package org.jetlinks.community.utils;
+
+import lombok.Getter;
+import org.hswebframework.ezorm.core.param.Term;
+import org.hswebframework.ezorm.rdb.executor.SqlRequest;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.AbstractTermsFragmentBuilder;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.core.metadata.Jsonable;
+import org.jetlinks.core.metadata.types.*;
+import org.jetlinks.core.utils.FluxUtils;
+import org.jetlinks.core.utils.Reactors;
+import org.jetlinks.reactor.ql.ReactorQL;
+import org.jetlinks.reactor.ql.ReactorQLContext;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * 响应式相关工具类
+ *
+ * @author zhouhao
+ * @since 2.0
+ */
+public class ReactorUtils {
+
+
+    public static <T> Function<Flux<T>, Flux<T>> limit(Long pageIndex, Long pageSize) {
+        if (pageIndex == null || pageSize == null) {
+            return Function.identity();
+        }
+        return flux -> flux.skip(pageIndex & pageSize).take(pageSize);
+    }
+
+    /**
+     * 构造有效期内去重的Flux
+     *
+     * <pre>
+     *    flux.as(ReactorUtils.distinct(MyData::getId,Duration.ofSeconds(30)))
+     * </pre>
+     *
+     * @param keySelector 去重的key
+     * @param duration    有效期
+     * @param <T>         泛型
+     * @return 去重构造器
+     */
+    public static <T> Function<Flux<T>, Flux<T>> distinct(Function<T, ?> keySelector, Duration duration) {
+        return FluxUtils.distinct(keySelector, duration);
+    }
+
+
+    public static final Function<Object, Mono<Boolean>> alwaysTrue = ignore -> Reactors.ALWAYS_TRUE;
+
+
+    /**
+     * 使用 {@link Term}来构造一个异步过滤器,请缓存过滤器函数使用,不要每次构建.
+     * <p>
+     * 在判断时会尝试把对象转为Map,
+     *
+     * <pre>{@code
+     *
+     * flux
+     *  .filter(createFilter(TermExpressionParser.parse("age gt 1 and name like '%张%'")))
+     *  .flatMap(this::handleData)
+     *  ...
+     *
+     * }</pre>
+     *
+     * @param terms 条件对象
+     * @param <T>   对象泛型
+     * @return 过滤器函数
+     */
+    @SuppressWarnings("all")
+    public static <T> Function<T, Mono<Boolean>> createFilter(List<Term> filter) {
+        return createFilter(filter, t -> {
+            if (t instanceof Map) {
+                return ((Map<String, Object>) t);
+            }
+            if (t instanceof Jsonable) {
+                return ((Jsonable) t).toJson();
+            }
+            return FastBeanCopier.copy(t, new HashMap<>());
+        });
+    }
+
+    /**
+     * 使用 {@link Term}来构造一个异步过滤器,请缓存过滤器函数使用,不要每次构建
+     *
+     * <pre>{@code
+     *
+     * flux
+     *  .filter(createFilter(TermExpressionParser.parse("age gt 1 and name like '%张%'"),Data::toMap))
+     *  .flatMap(this::handleData)
+     *  ...
+     *
+     * }</pre>
+     *
+     * @param terms     条件对象
+     * @param converter 转换器,用于将对象转为map,更有利于进行条件判断
+     * @param <T>       对象泛型
+     * @return 过滤器函数
+     */
+    @SuppressWarnings("all")
+    public static <T> Function<T, Mono<Boolean>> createFilter(List<Term> terms,
+                                                              Function<T, Map<String, Object>> converter) {
+
+        return createFilter(terms, converter, (arg, data) -> arg);
+    }
+
+    @SuppressWarnings("all")
+    public static <T> Function<T, Mono<Boolean>> createFilter(List<Term> terms,
+                                                              Function<T, Map<String, Object>> converter,
+                                                              BiFunction<Object, Map<String, Object>, Object> bindConverter) {
+        if (CollectionUtils.isEmpty(terms)) {
+            return (Function<T, Mono<Boolean>>) alwaysTrue;
+        }
+
+        SqlFragments fragments = termBuilder.createTermFragments(null, terms);
+        if (fragments.isEmpty()) {
+            return (Function<T, Mono<Boolean>>) alwaysTrue;
+        }
+
+        SqlRequest request = fragments.toRequest();
+
+        String sql = "select 1 from dual where " + request.getSql();
+        String nativeSql = request.toNativeSql();
+        try {
+            ReactorQL ql = ReactorQL.builder().sql(sql).build();
+            Object[] parameters = request.getParameters();
+            return new Function<T, Mono<Boolean>>() {
+                @Override
+                public String toString() {
+                    return nativeSql;
+                }
+
+                @Override
+                public Mono<Boolean> apply(T data) {
+                    Map<String, Object> mapValue = converter.apply(data);
+                    ReactorQLContext context = ReactorQLContext.ofDatasource(ignore -> Flux.just(mapValue));
+                    for (Object parameter : parameters) {
+                        context.bind(bindConverter.apply(parameter, mapValue));
+                    }
+                    return ql
+                        .start(context)
+                        .hasElements();
+                }
+            };
+        } catch (Throwable e) {
+            throw new IllegalArgumentException("error.create_connector_filter_error", e);
+        }
+    }
+
+    static final TermBuilder termBuilder = new TermBuilder();
+
+    static class TermBuilder extends AbstractTermsFragmentBuilder<Object> {
+
+        @Override
+        public SqlFragments createTermFragments(Object parameter, List<Term> terms) {
+            return super.createTermFragments(parameter, terms);
+        }
+
+        @Override
+        protected SqlFragments createTermFragments(Object trigger, Term term) {
+            String termType = StringUtils.hasText(term.getTermType()) ? term.getTermType() : "eq";
+            switch (termType) {
+                case "is":
+                case "=":
+                    termType = "eq";
+                    break;
+                case ">":
+                    termType = "gt";
+                    break;
+                case ">=":
+                    termType = "gte";
+                    break;
+                case "<":
+                    termType = "lt";
+                    break;
+                case "<=":
+                    termType = "lte";
+                    break;
+                case "!=":
+                case "<>":
+                    termType = "neq";
+                    break;
+            }
+            try {
+                TermTypeSupport support = TermTypeSupport.valueOf(termType);
+                return support.createSql("this['" + term.getColumn() + "']", term.getValue());
+            } catch (Throwable e) {
+                throw new IllegalArgumentException("unsupported termType " + term.getTermType(), e);
+            }
+        }
+    }
+
+    @Getter
+    enum TermTypeSupport {
+
+        eq("等于", "eq"),
+        neq("不等于", "neq"),
+
+        gt("大于", "gt", DateTimeType.ID, IntType.ID, FloatType.ID, DoubleType.ID),
+        gte("大于等于", "gte", DateTimeType.ID, IntType.ID, FloatType.ID, DoubleType.ID),
+        lt("小于", "lt", DateTimeType.ID, IntType.ID, FloatType.ID, DoubleType.ID),
+        lte("小于等于", "lte", DateTimeType.ID, IntType.ID, FloatType.ID, DoubleType.ID),
+
+        btw("在...之间", "btw", DateTimeType.ID, IntType.ID, FloatType.ID, DoubleType.ID) {
+            @Override
+            protected Object convertValue(Object val) {
+                return val;
+            }
+        },
+        nbtw("不在...之间", "nbtw", DateTimeType.ID, IntType.ID, FloatType.ID, DoubleType.ID) {
+            @Override
+            protected Object convertValue(Object val) {
+                return val;
+            }
+        },
+        in("在...之中", "in", StringType.ID, IntType.ID, FloatType.ID, DoubleType.ID) {
+            @Override
+            protected Object convertValue(Object val) {
+                return val;
+            }
+        },
+        nin("不在...之中", "not in", StringType.ID, IntType.ID, FloatType.ID, DoubleType.ID) {
+            @Override
+            protected Object convertValue(Object val) {
+                return val;
+            }
+        },
+
+        like("包含字符", "str_like", StringType.ID),
+        nlike("不包含字符", "not str_like", StringType.ID),
+
+        ;
+
+        private final String text;
+        private final Set<String> supportTypes;
+        private final String function;
+
+        TermTypeSupport(String text, String function, String... supportTypes) {
+            this.text = text;
+            this.function = function;
+            this.supportTypes = new HashSet<>(Arrays.asList(supportTypes));
+        }
+
+        protected Object convertValue(Object val) {
+
+            return val;
+        }
+
+        public final SqlFragments createSql(String column, Object value) {
+            PrepareSqlFragments fragments = PrepareSqlFragments.of();
+            fragments.addSql(function + "(", column, ",");
+            if (value instanceof NativeSql) {
+                fragments
+                    .addSql(((NativeSql) value).getSql())
+                    .addParameter(((NativeSql) value).getParameters());
+            } else {
+                fragments.addSql("?")
+                         .addParameter(convertValue(value));
+            }
+            fragments.addSql(")");
+            return fragments;
+        }
+
+    }
+
+}

+ 37 - 0
jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/SystemResourcesController.java

@@ -0,0 +1,37 @@
+package org.jetlinks.community.web;
+
+import io.swagger.v3.oas.annotations.Hidden;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.hswebframework.web.authorization.Authentication;
+import org.hswebframework.web.authorization.exception.UnAuthorizedException;
+import org.jetlinks.community.resource.Resource;
+import org.jetlinks.community.resource.ResourceManager;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/system/resources")
+@Hidden
+@AllArgsConstructor
+public class SystemResourcesController {
+
+    private final ResourceManager resourceManager;
+
+    @GetMapping("/{type}")
+    @SneakyThrows
+    public Flux<String> getResources(@PathVariable String type) {
+        return Authentication
+            .currentReactive()
+            .filter(auth -> "admin".equals(auth.getUser().getUsername()))
+            .switchIfEmpty(Mono.error(UnAuthorizedException::new))
+            .flatMapMany(auth -> resourceManager.getResources(type))
+            .map(Resource::asString);
+    }
+
+
+}

+ 0 - 6
jetlinks-components/configure-component/src/main/resources/META-INF/spring.factories

@@ -1,6 +0,0 @@
-org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-org.jetlinks.community.configure.cluster.ClusterConfiguration,\
-org.jetlinks.community.configure.doc.SpringDocCustomizerConfiguration,\
-org.jetlinks.community.configure.trace.TraceConfiguration,\
-org.jetlinks.community.configure.device.DeviceClusterConfiguration,\
-org.jetlinks.community.configure.redis.RedisSerializationConfiguration

+ 5 - 0
jetlinks-components/configure-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,5 @@
+org.jetlinks.community.configure.cluster.ClusterConfiguration
+org.jetlinks.community.configure.doc.SpringDocCustomizerConfiguration
+org.jetlinks.community.configure.trace.TraceConfiguration
+org.jetlinks.community.configure.device.DeviceClusterConfiguration
+org.jetlinks.community.configure.redis.RedisSerializationConfiguration

+ 5 - 0
jetlinks-components/dashboard-component/pom.xml

@@ -29,6 +29,11 @@
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.github.oshi</groupId>
+            <artifactId>oshi-core</artifactId>
+            <version>6.2.2</version>
+        </dependency>
 
     </dependencies>
 

+ 0 - 81
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/JvmCpuMeasurementProvider.java

@@ -1,81 +0,0 @@
-package org.jetlinks.community.dashboard.measurements;
-
-import org.hswebframework.utils.time.DateFormatter;
-import org.jetlinks.community.dashboard.*;
-import org.jetlinks.community.dashboard.supports.StaticMeasurement;
-import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider;
-import org.jetlinks.core.metadata.ConfigMetadata;
-import org.jetlinks.core.metadata.DataType;
-import org.jetlinks.core.metadata.types.DoubleType;
-import org.jetlinks.core.metadata.unit.UnifyUnit;
-import org.springframework.stereotype.Component;
-import reactor.core.publisher.Flux;
-
-import java.math.BigDecimal;
-import java.time.Duration;
-import java.util.Date;
-
-import static java.math.BigDecimal.ROUND_HALF_UP;
-
-/**
- * 实时CPU 使用率监控
- * <pre>
- *     /dashboard/systemMonitor/cpu/usage/realTime
- * </pre>
- *
- * @author zhouhao
- */
-@Component
-public class JvmCpuMeasurementProvider
-    extends StaticMeasurementProvider {
-
-    public JvmCpuMeasurementProvider() {
-        super(DefaultDashboardDefinition.jvmMonitor, MonitorObjectDefinition.cpu);
-        addMeasurement(cpuUseAgeMeasurement);
-    }
-
-    static DataType type = new DoubleType().scale(1).min(0).max(100).unit(UnifyUnit.percent);
-
-    static StaticMeasurement cpuUseAgeMeasurement = new StaticMeasurement(CommonMeasurementDefinition.usage)
-        .addDimension(new CpuRealTimeMeasurementDimension());
-
-
-    static class CpuRealTimeMeasurementDimension implements MeasurementDimension {
-
-        @Override
-        public DimensionDefinition getDefinition() {
-            return CommonDimensionDefinition.realTime;
-        }
-
-        @Override
-        public DataType getValueType() {
-            return type;
-        }
-
-        @Override
-        public ConfigMetadata getParams() {
-            return null;
-        }
-
-        @Override
-        public boolean isRealTime() {
-            return true;
-        }
-
-        @Override
-        public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
-            //每秒获取系统CPU使用率
-            return Flux.concat(
-                Flux.just(1),
-                Flux.interval(Duration.ofSeconds(1)))
-                .map(t -> SimpleMeasurementValue.of(BigDecimal
-                        .valueOf(SystemMonitor.jvmCpuUsage.getValue())
-                        .setScale(1, ROUND_HALF_UP),
-                    DateFormatter.toString(new Date(), "HH:mm:ss"),
-                    System.currentTimeMillis()))
-                .cast(MeasurementValue.class);
-        }
-
-    }
-
-}

+ 0 - 132
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/JvmMemoryMeasurementProvider.java

@@ -1,132 +0,0 @@
-package org.jetlinks.community.dashboard.measurements;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.hswebframework.utils.time.DateFormatter;
-import org.jetlinks.community.dashboard.*;
-import org.jetlinks.community.dashboard.supports.StaticMeasurement;
-import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider;
-import org.jetlinks.core.metadata.ConfigMetadata;
-import org.jetlinks.core.metadata.DataType;
-import org.jetlinks.core.metadata.SimplePropertyMetadata;
-import org.jetlinks.core.metadata.types.DoubleType;
-import org.jetlinks.core.metadata.types.LongType;
-import org.jetlinks.core.metadata.types.ObjectType;
-import org.springframework.stereotype.Component;
-import reactor.core.publisher.Flux;
-
-import java.lang.management.ManagementFactory;
-import java.lang.management.MemoryMXBean;
-import java.lang.management.MemoryUsage;
-import java.math.BigDecimal;
-import java.time.Duration;
-import java.util.Date;
-
-import static java.math.BigDecimal.ROUND_HALF_UP;
-
-/**
- * 实时内存使用率监控
- * <pre>
- *     /dashboard/jvmMonitor/memory/info/realTime
- * </pre>
- *
- * @author zhouhao
- */
-@Component
-public class JvmMemoryMeasurementProvider extends StaticMeasurementProvider {
-    public JvmMemoryMeasurementProvider() {
-        super(DefaultDashboardDefinition.jvmMonitor, MonitorObjectDefinition.memory);
-        addMeasurement(jvmMemoryInfo);
-    }
-
-    static ObjectType type = new ObjectType();
-
-    static {
-        {
-            SimplePropertyMetadata metadata = new SimplePropertyMetadata();
-            metadata.setId("max");
-            metadata.setName("最大值");
-            metadata.setValueType(new LongType());
-            type.addPropertyMetadata(metadata);
-        }
-
-        {
-            SimplePropertyMetadata metadata = new SimplePropertyMetadata();
-            metadata.setId("used");
-            metadata.setName("已使用");
-            metadata.setValueType(new LongType());
-            type.addPropertyMetadata(metadata);
-        }
-
-        {
-            SimplePropertyMetadata metadata = new SimplePropertyMetadata();
-            metadata.setId("usage");
-            metadata.setName("使用率");
-            metadata.setValueType(new DoubleType());
-            type.addPropertyMetadata(metadata);
-        }
-
-    }
-
-    static StaticMeasurement jvmMemoryInfo = new StaticMeasurement(CommonMeasurementDefinition.info)
-        .addDimension(new JvmMemoryInfoDimension());
-
-    static class JvmMemoryInfoDimension implements MeasurementDimension {
-
-        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
-
-        @Override
-        public DimensionDefinition getDefinition() {
-            return CommonDimensionDefinition.realTime;
-        }
-
-        @Override
-        public DataType getValueType() {
-            return type;
-        }
-
-        @Override
-        public ConfigMetadata getParams() {
-            return null;
-        }
-
-        @Override
-        public boolean isRealTime() {
-            return true;
-        }
-
-        @Override
-        public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
-            return Flux.concat(
-                Flux.just(MemoryInfo.of(memoryMXBean.getHeapMemoryUsage())),
-                Flux.interval(Duration.ofSeconds(1))
-                    .map(t -> MemoryInfo.of(memoryMXBean.getHeapMemoryUsage()))
-                    .windowUntilChanged(MemoryInfo::getUsage)
-                    .flatMap(Flux::last))
-                .map(val -> SimpleMeasurementValue.of(val,
-                    DateFormatter.toString(new Date(), "HH:mm:ss"),
-                    System.currentTimeMillis()))
-                .cast(MeasurementValue.class);
-        }
-
-    }
-
-    @Getter
-    @Setter
-    public static class MemoryInfo {
-        private long max;
-
-        private long used;
-
-        private double usage;
-
-        public static MemoryInfo of(MemoryUsage usage) {
-            MemoryInfo info = new MemoryInfo();
-            info.max = (usage.getMax()) / 1000 / 1000;
-            info.used = usage.getUsed() / 1000 / 1000;
-            info.usage = BigDecimal.valueOf(((double) usage.getUsed() / usage.getMax()) * 100D).setScale(2, ROUND_HALF_UP)
-                .doubleValue();
-            return info;
-        }
-    }
-}

+ 3 - 2
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/MonitorObjectDefinition.java

@@ -9,9 +9,10 @@ import org.jetlinks.community.dashboard.ObjectDefinition;
 public enum MonitorObjectDefinition implements ObjectDefinition {
 
     cpu("CPU"),
-    memory("内存");
+    memory("内存"),
+    stats("运行状态");
 
-    private String name;
+    private final String name;
 
     @Override
     public String getId() {

+ 0 - 79
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/SystemCpuMeasurementProvider.java

@@ -1,79 +0,0 @@
-package org.jetlinks.community.dashboard.measurements;
-
-import org.hswebframework.utils.time.DateFormatter;
-import org.jetlinks.community.dashboard.*;
-import org.jetlinks.community.dashboard.supports.StaticMeasurement;
-import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider;
-import org.jetlinks.core.metadata.ConfigMetadata;
-import org.jetlinks.core.metadata.DataType;
-import org.jetlinks.core.metadata.types.DoubleType;
-import org.jetlinks.core.metadata.unit.UnifyUnit;
-import org.springframework.stereotype.Component;
-import reactor.core.publisher.Flux;
-
-import java.math.BigDecimal;
-import java.time.Duration;
-import java.util.Date;
-
-import static java.math.BigDecimal.ROUND_HALF_UP;
-
-/**
- * 实时CPU 使用率监控
- * <pre>
- *     /dashboard/systemMonitor/cpu/usage/realTime
- * </pre>
- *
- * @author zhouhao
- */
-@Component
-public class SystemCpuMeasurementProvider
-    extends StaticMeasurementProvider {
-
-    public SystemCpuMeasurementProvider() {
-        super(DefaultDashboardDefinition.systemMonitor, MonitorObjectDefinition.cpu);
-        addMeasurement(cpuUseAgeMeasurement);
-    }
-
-    static DataType type = new DoubleType().scale(1).min(0).max(100).unit(UnifyUnit.percent);
-
-    static StaticMeasurement cpuUseAgeMeasurement = new StaticMeasurement(CommonMeasurementDefinition.usage)
-        .addDimension(new CpuRealTimeMeasurementDimension());
-
-
-    static class CpuRealTimeMeasurementDimension implements MeasurementDimension {
-
-        @Override
-        public DimensionDefinition getDefinition() {
-            return CommonDimensionDefinition.realTime;
-        }
-
-        @Override
-        public DataType getValueType() {
-            return type;
-        }
-
-        @Override
-        public ConfigMetadata getParams() {
-            return null;
-        }
-
-        @Override
-        public boolean isRealTime() {
-            return true;
-        }
-
-        @Override
-        public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
-            //每秒获取系统CPU使用率
-            return Flux.interval(Duration.ofSeconds(1))
-                .map(t -> SimpleMeasurementValue.of(BigDecimal
-                        .valueOf(SystemMonitor.systemCpuUsage.getValue())
-                        .setScale(1, ROUND_HALF_UP),
-                    DateFormatter.toString(new Date(), "HH:mm:ss"),
-                    System.currentTimeMillis()))
-                .cast(MeasurementValue.class);
-        }
-
-    }
-
-}

+ 0 - 128
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/SystemMemoryMeasurementProvider.java

@@ -1,128 +0,0 @@
-package org.jetlinks.community.dashboard.measurements;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.hswebframework.utils.time.DateFormatter;
-import org.jetlinks.community.dashboard.*;
-import org.jetlinks.community.dashboard.supports.StaticMeasurement;
-import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider;
-import org.jetlinks.core.metadata.ConfigMetadata;
-import org.jetlinks.core.metadata.DataType;
-import org.jetlinks.core.metadata.SimplePropertyMetadata;
-import org.jetlinks.core.metadata.types.DoubleType;
-import org.jetlinks.core.metadata.types.LongType;
-import org.jetlinks.core.metadata.types.ObjectType;
-import org.springframework.stereotype.Component;
-import reactor.core.publisher.Flux;
-
-import java.math.BigDecimal;
-import java.time.Duration;
-import java.util.Date;
-
-import static java.math.BigDecimal.ROUND_HALF_UP;
-
-/**
- * 实时内存使用率监控
- * <pre>
- *     /dashboard/systemMonitor/memory/info/realTime
- * </pre>
- *
- * @author zhouhao
- */
-@Component
-public class SystemMemoryMeasurementProvider extends StaticMeasurementProvider {
-    public SystemMemoryMeasurementProvider() {
-        super(DefaultDashboardDefinition.systemMonitor, MonitorObjectDefinition.memory);
-        addMeasurement(systemMemoryInfo);
-    }
-
-    static ObjectType type = new ObjectType();
-
-    static {
-        {
-            SimplePropertyMetadata metadata = new SimplePropertyMetadata();
-            metadata.setId("max");
-            metadata.setName("最大值");
-            metadata.setValueType(new LongType());
-            type.addPropertyMetadata(metadata);
-        }
-
-        {
-            SimplePropertyMetadata metadata = new SimplePropertyMetadata();
-            metadata.setId("used");
-            metadata.setName("已使用");
-            metadata.setValueType(new LongType());
-            type.addPropertyMetadata(metadata);
-        }
-
-        {
-            SimplePropertyMetadata metadata = new SimplePropertyMetadata();
-            metadata.setId("usage");
-            metadata.setName("使用率");
-            metadata.setValueType(new DoubleType());
-            type.addPropertyMetadata(metadata);
-        }
-
-    }
-
-    static StaticMeasurement systemMemoryInfo = new StaticMeasurement(CommonMeasurementDefinition.info)
-        .addDimension(new JvmMemoryInfoDimension());
-
-    static class JvmMemoryInfoDimension implements MeasurementDimension {
-
-        @Override
-        public DimensionDefinition getDefinition() {
-            return CommonDimensionDefinition.realTime;
-        }
-
-        @Override
-        public DataType getValueType() {
-            return type;
-        }
-
-        @Override
-        public ConfigMetadata getParams() {
-            return null;
-        }
-
-        @Override
-        public boolean isRealTime() {
-            return true;
-        }
-
-        @Override
-        public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
-            return Flux.concat(
-                Flux.just(MemoryInfo.of()),
-                Flux.interval(Duration.ofSeconds(1))
-                    .map(t -> MemoryInfo.of())
-                    .windowUntilChanged(MemoryInfo::getUsage)
-                    .flatMap(Flux::last))
-                .map(val -> SimpleMeasurementValue.of(val,
-                    DateFormatter.toString(new Date(), "HH:mm:ss"),
-                    System.currentTimeMillis()))
-                .cast(MeasurementValue.class);
-        }
-
-    }
-
-    @Getter
-    @Setter
-    public static class MemoryInfo {
-        private long max;
-
-        private long used;
-
-        private double usage;
-
-        public static MemoryInfo of() {
-            MemoryInfo info = new MemoryInfo();
-            long total = (long) SystemMonitor.totalSystemMemory.getValue();
-
-            info.max = total;
-            info.used = (long) (total-  SystemMonitor.freeSystemMemory.getValue());
-            info.usage = BigDecimal.valueOf(((double) info.getUsed() / info.getMax()) * 100D).setScale(2, ROUND_HALF_UP).doubleValue();
-            return info;
-        }
-    }
-}

+ 18 - 7
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/SystemMonitor.java

@@ -3,6 +3,8 @@ package org.jetlinks.community.dashboard.measurements;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.SneakyThrows;
+import oshi.SystemInfo;
+import oshi.hardware.GlobalMemory;
 
 import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
@@ -25,21 +27,22 @@ public enum SystemMonitor {
     maxOpenFileCount("最大打开文件数"),
     ;
 
-    private String text;
+    private final String text;
 
     public double getValue() {
         return getValue(name());
     }
 
-    static OperatingSystemMXBean osMxBean = ManagementFactory.getOperatingSystemMXBean();
-    private static Map<String, Callable<Double>> items = new HashMap<>();
+    static final OperatingSystemMXBean osMxBean = ManagementFactory.getOperatingSystemMXBean();
+
+    private final static Map<String, Callable<Double>> items = new HashMap<>();
 
     private static final List<String> OPERATING_SYSTEM_BEAN_CLASS_NAMES = Arrays.asList(
         "com.sun.management.OperatingSystemMXBean", // HotSpot
         "com.ibm.lang.management.OperatingSystemMXBean" // J9
     );
 
-    private static Callable<Double> zero = () -> 0D;
+    private static final Callable<Double> zero = () -> 0D;
 
     private static Class<?> mxBeanClass;
 
@@ -47,7 +50,7 @@ public enum SystemMonitor {
         try {
             Method method = mxBeanClass.getMethod(methodName);
             items.put(item, () -> mapping.apply(((Number) method.invoke(osMxBean)).doubleValue()));
-        } catch (Exception e) {
+        } catch (Exception ignore) {
 
         }
     }
@@ -59,12 +62,17 @@ public enum SystemMonitor {
             } catch (Exception ignore) {
             }
         }
+        GlobalMemory memory = new oshi.SystemInfo()
+            .getHardware()
+            .getMemory();
+        {
+            items.put(freeSystemMemory.name(),()->memory.getAvailable()/ 1024 / 1024D);
+            items.put(totalSystemMemory.name(),()->memory.getTotal()/ 1024 / 1024D);
+        }
         try {
             if (mxBeanClass != null) {
                 register(systemCpuUsage.name(), "getSystemCpuLoad", usage -> usage * 100D);
                 register(jvmCpuUsage.name(), "getProcessCpuLoad", usage -> usage * 100D);
-                register(freeSystemMemory.name(), "getFreePhysicalMemorySize", val -> val / 1024 / 1024);
-                register(totalSystemMemory.name(), "getTotalPhysicalMemorySize", val -> val / 1024 / 1024);
                 register("virtualMemory", "getCommittedVirtualMemorySize", val -> val / 1024 / 1024);
                 register(openFileCount.name(), "getOpenFileDescriptorCount", Function.identity());
                 register(maxOpenFileCount.name(), "getMaxFileDescriptorCount", Function.identity());
@@ -74,6 +82,9 @@ public enum SystemMonitor {
         }
     }
 
+    public double value() {
+        return SystemMonitor.getValue(this.name());
+    }
 
     @SneakyThrows
     public static double getValue(String id) {

+ 36 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/CpuInfo.java

@@ -0,0 +1,36 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+@Getter
+@Setter
+@ToString
+@AllArgsConstructor
+@NoArgsConstructor
+public class CpuInfo implements MonitorInfo<CpuInfo> {
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "JVM进程CPU使用率,0-100")
+    private float jvmUsage;
+
+    @Schema(description = "系统CPU使用率,0-100")
+    private float systemUsage;
+
+    @Override
+    public CpuInfo add(CpuInfo info) {
+        return new CpuInfo(
+            MonitorUtils.round(info.jvmUsage + this.jvmUsage),
+            MonitorUtils.round(info.systemUsage + this.systemUsage)
+        );
+    }
+
+    @Override
+    public CpuInfo division(int num) {
+        return new CpuInfo(
+            MonitorUtils.round(this.jvmUsage / num),
+            MonitorUtils.round(this.systemUsage / num)
+        );
+    }
+
+}

+ 41 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/DiskInfo.java

@@ -0,0 +1,41 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+@Getter
+@Setter
+@ToString
+@AllArgsConstructor
+@NoArgsConstructor
+public class DiskInfo implements MonitorInfo<DiskInfo> {
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "磁盘总容量,单位MB")
+    private long total;
+
+    @Schema(description = "磁盘可用容量,单位MB")
+    private long free;
+
+    public float getUsage() {
+        return MonitorUtils.round(
+            ((total - free) / (float) total) * 100
+        );
+    }
+
+    @Override
+    public DiskInfo add(DiskInfo info) {
+        return new DiskInfo(
+            info.total + this.total,
+            info.free + this.free
+        );
+    }
+
+    @Override
+    public DiskInfo division(int num) {
+        return new DiskInfo(
+            this.total / num,
+            this.free / num
+        );
+    }
+}

+ 73 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/MemoryInfo.java

@@ -0,0 +1,73 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+@Getter
+@Setter
+@ToString
+@AllArgsConstructor
+@NoArgsConstructor
+public class MemoryInfo implements MonitorInfo<MemoryInfo> {
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "JVM堆总内存,单位MB")
+    private long jvmHeapTotal;
+
+    @Schema(description = "JVM堆可用内存,单位MB")
+    private long jvmHeapFree;
+
+    @Schema(description = "JVM堆外总内存,单位MB")
+    private long jvmNonHeapTotal;
+
+    @Schema(description = "JVM堆外可用内存,单位MB")
+    private long jvmNonHeapFree;
+
+    @Schema(description = "系统总内存,单位MB")
+    private long systemTotal;
+
+    @Schema(description = "系统可用内存,单位MB")
+    private long systemFree;
+
+    public float getJvmHeapUsage() {
+        return MonitorUtils.round(
+            ((jvmHeapTotal - jvmHeapFree) / (float) jvmHeapTotal)*100
+        );
+    }
+
+    public float getJvmNonHeapUsage() {
+        return MonitorUtils.round(
+            ((jvmNonHeapTotal - jvmNonHeapFree) / (float) jvmNonHeapTotal)*100
+        );
+    }
+
+    public float getSystemUsage() {
+        return MonitorUtils.round(
+            ((systemTotal - systemFree) / (float) systemTotal)*100
+        );
+    }
+
+    @Override
+    public MemoryInfo add(MemoryInfo info) {
+        return new MemoryInfo(
+            info.jvmHeapTotal + this.jvmHeapTotal,
+            info.jvmHeapFree + this.jvmHeapFree,
+            info.jvmNonHeapTotal + this.jvmNonHeapTotal,
+            info.jvmNonHeapFree + this.jvmNonHeapFree,
+            info.systemTotal + this.systemTotal,
+            info.systemFree + this.systemFree
+        );
+    }
+
+    @Override
+    public MemoryInfo division(int num) {
+        return new MemoryInfo(
+            this.jvmHeapTotal / num,
+            this.jvmHeapFree / num,
+            this.jvmNonHeapTotal / num,
+            this.jvmNonHeapFree / num,
+            this.systemTotal / num,
+            this.systemFree / num
+        );
+    }
+}

+ 13 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/MonitorInfo.java

@@ -0,0 +1,13 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import java.io.Serializable;
+
+public interface MonitorInfo<T extends MonitorInfo<T>> extends Serializable {
+
+
+    T add(T info);
+
+
+    T division(int num);
+
+}

+ 15 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/MonitorUtils.java

@@ -0,0 +1,15 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+class MonitorUtils {
+
+     static float round(float val) {
+        return BigDecimal
+            .valueOf(val)
+            .setScale(1, RoundingMode.HALF_UP)
+            .floatValue();
+    }
+
+}

+ 34 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/SystemInfo.java

@@ -0,0 +1,34 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import lombok.*;
+
+@Getter
+@Setter
+@AllArgsConstructor(staticName = "of")
+@NoArgsConstructor
+@ToString
+public class SystemInfo implements MonitorInfo<SystemInfo> {
+
+    private CpuInfo cpu;
+    private MemoryInfo memory;
+    private DiskInfo disk;
+
+    @Override
+    public SystemInfo add(SystemInfo info) {
+        return new SystemInfo(
+            this.cpu.add(info.cpu),
+            this.memory.add(info.memory),
+            this.disk.add(info.disk)
+        );
+    }
+
+    @Override
+    public SystemInfo division(int num) {
+
+        return new SystemInfo(
+            this.cpu.division(num),
+            this.memory.division(num),
+            this.disk.division(num)
+        );
+    }
+}

+ 148 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/SystemMonitorMeasurementProvider.java

@@ -0,0 +1,148 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.community.dashboard.*;
+import org.jetlinks.community.dashboard.measurements.MonitorObjectDefinition;
+import org.jetlinks.community.dashboard.supports.StaticMeasurement;
+import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.core.metadata.DataType;
+import org.jetlinks.core.metadata.DefaultConfigMetadata;
+import org.jetlinks.core.metadata.types.EnumType;
+import org.jetlinks.core.metadata.types.ObjectType;
+import org.jetlinks.core.metadata.types.StringType;
+import org.reactivestreams.Publisher;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <h1>系统监控支持</h1>
+ * <p>
+ * 支持获取cpu,内存,磁盘信息.支持实时监控
+ * <p>
+ * <h2>实时数据</h2>
+ * 通过websocket(topic)或者sse(url)请求:
+ * <p>
+ * /dashboard/systemMonitor/stats/info/realTime
+ *
+ * <p>
+ * <h3>参数:</h3>
+ * <ul>
+ * <li>type: memory,cpu,disk,其他则为全部信息</li>
+ * <li>clusterNodeId: 指定获取集群节点的信息,不支持则返回所有节点的监控信息</li>
+ * <li>agg: 在没有指定clusterNodeId时有效,设置为avg表示所有集群节点的平均值,sum为总和.</li>
+ * </ul>
+ *
+ * <h3>响应结构:</h3>
+ * <p>
+ * 类型不同结构不同,memory: {@link MemoryInfo},cpu:{@link CpuInfo},disk:{@link DiskInfo},all:{@link SystemInfo}
+ * <p>
+ *  🌟: 企业版支持集群监控以及历史记录
+ *
+ * @author zhouhao
+ * @since 2.0
+ */
+@Component
+public class SystemMonitorMeasurementProvider extends StaticMeasurementProvider {
+
+    private final SystemMonitorService monitorService = new SystemMonitorServiceImpl();
+
+
+    public SystemMonitorMeasurementProvider() {
+        super(DefaultDashboardDefinition.systemMonitor, MonitorObjectDefinition.stats);
+
+        addMeasurement(new StaticMeasurement(CommonMeasurementDefinition.info)
+                           .addDimension(new RealTimeDimension())
+        );
+
+
+    }
+
+
+    private void putTo(String prefix, MonitorInfo<?> source, Map<String, Object> target) {
+        Map<String, Object> data = FastBeanCopier.copy(source, new HashMap<>());
+        data.forEach((key, value) -> {
+            char[] keyChars = key.toCharArray();
+            keyChars[0] = Character.toUpperCase(keyChars[0]);
+            target.put(prefix + new String(keyChars), value);
+        });
+    }
+
+
+    //实时监控
+    class RealTimeDimension implements MeasurementDimension {
+
+        @Override
+        public DimensionDefinition getDefinition() {
+            return CommonDimensionDefinition.realTime;
+        }
+
+        @Override
+        public DataType getValueType() {
+            return new ObjectType();
+        }
+
+        @Override
+        public ConfigMetadata getParams() {
+
+            return new DefaultConfigMetadata()
+                .add("serverNodeId", "服务节点ID", StringType.GLOBAL)
+                .add("interval", "更新频率", StringType.GLOBAL)
+                .add("type", "指标类型", new EnumType()
+                    .addElement(EnumType.Element.of("all", "全部"))
+                    .addElement(EnumType.Element.of("cpu", "CPU"))
+                    .addElement(EnumType.Element.of("memory", "内存"))
+                    .addElement(EnumType.Element.of("disk", "硬盘"))
+                );
+        }
+
+        @Override
+        public boolean isRealTime() {
+            return true;
+        }
+
+        @Override
+        @SuppressWarnings("all")
+        public Publisher<? extends MeasurementValue> getValue(MeasurementParameter parameter) {
+            Duration interval = parameter.getDuration("interval", Duration.ofSeconds(1));
+            String type = parameter.getString("type", "all");
+
+            return Flux
+                .concat(
+                    info(monitorService, type),
+                    Flux
+                        .interval(interval)
+                        .flatMap(ignore -> info(monitorService, type))
+                )
+                .map(info -> SimpleMeasurementValue.of(info, System.currentTimeMillis()));
+        }
+
+        private Mono<? extends MonitorInfo<?>> info(SystemMonitorService service, String type) {
+            Mono<? extends MonitorInfo<?>> data;
+            switch (type) {
+                case "cpu":
+                    data = service.cpu();
+                    break;
+                case "memory":
+                    data = service.memory();
+                    break;
+                case "disk":
+                    data = service.disk();
+                    break;
+                default:
+                    data = service.system();
+                    break;
+            }
+            return data
+                .onErrorResume(err -> Mono.empty());
+        }
+
+
+    }
+
+}

+ 14 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/SystemMonitorService.java

@@ -0,0 +1,14 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import reactor.core.publisher.Mono;
+
+public interface SystemMonitorService {
+
+    Mono<SystemInfo> system();
+
+    Mono<MemoryInfo> memory();
+
+    Mono<CpuInfo> cpu();
+
+    Mono<DiskInfo> disk();
+}

+ 66 - 0
jetlinks-components/dashboard-component/src/main/java/org/jetlinks/community/dashboard/measurements/sys/SystemMonitorServiceImpl.java

@@ -0,0 +1,66 @@
+package org.jetlinks.community.dashboard.measurements.sys;
+
+import org.jetlinks.community.dashboard.measurements.SystemMonitor;
+import reactor.core.publisher.Mono;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.management.MemoryUsage;
+
+public class SystemMonitorServiceImpl implements SystemMonitorService {
+
+    private final static MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
+
+    private final static int MB = 1024 * 1024;
+
+    @Override
+    public Mono<SystemInfo> system() {
+        return Mono
+            .zip(
+                cpu(),
+                memory(),
+                disk()
+            )
+            .map(tp3 -> SystemInfo.of(tp3.getT1(), tp3.getT2(), tp3.getT3()));
+    }
+
+    @Override
+    public Mono<DiskInfo> disk() {
+        long total = 0, usable = 0;
+        for (File file : File.listRoots()) {
+            total += file.getTotalSpace();
+            usable += file.getUsableSpace();
+        }
+        DiskInfo diskInfo = new DiskInfo();
+        diskInfo.setTotal(total / MB);
+        diskInfo.setFree(usable / MB);
+        return Mono.just(diskInfo);
+    }
+
+    public Mono<MemoryInfo> memory() {
+        MemoryInfo info = new MemoryInfo();
+        info.setSystemTotal((long) SystemMonitor.totalSystemMemory.value());
+        info.setSystemFree((long) SystemMonitor.freeSystemMemory.value());
+
+        MemoryUsage heap = memoryMXBean.getHeapMemoryUsage();
+        MemoryUsage nonHeap = memoryMXBean.getNonHeapMemoryUsage();
+        long nonHeapMax = (nonHeap.getMax() > 0 ? nonHeap.getMax() / MB : info.getSystemTotal());
+
+        info.setJvmHeapFree((heap.getMax() - heap.getUsed()) / MB);
+        info.setJvmHeapTotal(heap.getMax() / MB);
+
+        info.setJvmNonHeapFree(nonHeapMax - nonHeap.getUsed() / MB);
+        info.setJvmNonHeapTotal(nonHeapMax);
+        return Mono.just(info);
+    }
+
+    public Mono<CpuInfo> cpu() {
+        CpuInfo info = new CpuInfo();
+
+        info.setSystemUsage(MonitorUtils.round((float) (SystemMonitor.systemCpuUsage.value())));
+        info.setJvmUsage(MonitorUtils.round((float) (SystemMonitor.jvmCpuUsage.value())));
+
+        return Mono.just(info);
+    }
+}

+ 0 - 20
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/ElasticRestClient.java

@@ -1,20 +0,0 @@
-package org.jetlinks.community.elastic.search;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import org.elasticsearch.client.RestHighLevelClient;
-
-/**
- * @author bsetfeng
- * @since 1.0
- **/
-@AllArgsConstructor
-@NoArgsConstructor
-@Getter
-public class ElasticRestClient {
-
-    private RestHighLevelClient queryClient;
-
-    private RestHighLevelClient writeClient;
-}

+ 37 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchThingDataConfiguration.java

@@ -0,0 +1,37 @@
+package org.jetlinks.community.elastic.search.configuration;
+
+import org.jetlinks.core.things.ThingsRegistry;
+import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager;
+import org.jetlinks.community.elastic.search.service.AggregationService;
+import org.jetlinks.community.elastic.search.service.ElasticSearchService;
+import org.jetlinks.community.elastic.search.things.ElasticSearchColumnModeStrategy;
+import org.jetlinks.community.elastic.search.things.ElasticSearchRowModeStrategy;
+import org.jetlinks.community.things.data.ThingsDataRepositoryStrategy;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(ThingsDataRepositoryStrategy.class)
+public class ElasticSearchThingDataConfiguration {
+
+    @Bean
+    public ElasticSearchColumnModeStrategy elasticSearchColumnModThingDataPolicy(
+        ThingsRegistry registry,
+        ElasticSearchService searchService,
+        AggregationService aggregationService,
+        ElasticSearchIndexManager indexManager) {
+
+        return new ElasticSearchColumnModeStrategy(registry, searchService, aggregationService, indexManager);
+    }
+
+    @Bean
+    public ElasticSearchRowModeStrategy elasticSearchRowModThingDataPolicy(
+        ThingsRegistry registry,
+        ElasticSearchService searchService,
+        AggregationService aggregationService,
+        ElasticSearchIndexManager indexManager) {
+
+        return new ElasticSearchRowModeStrategy(registry, searchService, aggregationService, indexManager);
+    }
+}

+ 0 - 1
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/DefaultElasticSearchIndexManager.java

@@ -12,7 +12,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentSkipListMap;
 
-@Component
 @ConfigurationProperties(prefix = "elasticsearch.index")
 public class DefaultElasticSearchIndexManager implements ElasticSearchIndexManager {
 

+ 0 - 1
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/timeseries/ElasticSearchTimeSeriesManager.java

@@ -23,7 +23,6 @@ import java.util.concurrent.ConcurrentHashMap;
  * @author zhouhao
  * @since 1.0
  **/
-@Service
 @Slf4j
 public class ElasticSearchTimeSeriesManager implements TimeSeriesManager {
 

+ 2 - 0
jetlinks-components/elasticsearch-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,2 @@
+org.jetlinks.community.elastic.search.configuration.ElasticSearchConfiguration
+org.jetlinks.community.elastic.search.configuration.ElasticSearchThingDataConfiguration

+ 0 - 12
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGateway.java

@@ -23,18 +23,6 @@ public interface DeviceGateway {
      */
     String getId();
 
-    /**
-     * @return 传输协议
-     * @see org.jetlinks.core.message.codec.DefaultTransport
-     */
-    Transport getTransport();
-
-    /**
-     * @return 网络类型
-     * @see org.jetlinks.community.network.DefaultNetworkType
-     */
-    NetworkType getNetworkType();
-
     /**
      * 订阅来自设备到消息,关闭网关时不会结束流.
      *

+ 403 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayHelper.java

@@ -0,0 +1,403 @@
+package org.jetlinks.community.gateway;
+
+import lombok.AllArgsConstructor;
+import org.jetlinks.core.device.DeviceConfigKey;
+import org.jetlinks.core.device.DeviceOperator;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.device.session.DeviceSessionManager;
+import org.jetlinks.core.message.*;
+import org.jetlinks.core.message.state.DeviceStateCheckMessage;
+import org.jetlinks.core.message.state.DeviceStateCheckMessageReply;
+import org.jetlinks.core.server.session.ChildrenDeviceSession;
+import org.jetlinks.core.server.session.DeviceSession;
+import org.jetlinks.core.server.session.KeepOnlineSession;
+import org.jetlinks.core.server.session.LostDeviceSession;
+import org.jetlinks.community.PropertyConstants;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Mono;
+import reactor.util.context.Context;
+
+import java.time.Duration;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * 设备网关消息处理,会话管理工具类,用于统一封装对设备消息和会话的处理逻辑
+ *
+ * @author zhouhao
+ * @see DeviceRegistry
+ * @see DecodedClientMessageHandler
+ * @since 1.5
+ */
+@AllArgsConstructor
+public class DeviceGatewayHelper {
+
+    private final DeviceRegistry registry;
+    private final DeviceSessionManager sessionManager;
+    private final DecodedClientMessageHandler messageHandler;
+
+    public static Consumer<DeviceSession> applySessionKeepaliveTimeout(DeviceMessage msg, Supplier<Duration> timeoutSupplier) {
+        return session -> {
+            Integer timeout = msg.getHeaderOrElse(Headers.keepOnlineTimeoutSeconds, () -> null);
+            if (null != timeout) {
+                session.setKeepAliveTimeout(Duration.ofSeconds(timeout));
+            } else {
+                Duration defaultTimeout = timeoutSupplier.get();
+                if (null != defaultTimeout) {
+                    session.setKeepAliveTimeout(defaultTimeout);
+                }
+            }
+        };
+    }
+
+    public Mono<DeviceOperator> handleDeviceMessage(DeviceMessage message,
+                                                    Function<DeviceOperator, DeviceSession> sessionBuilder) {
+
+        return handleDeviceMessage(message,
+                                   sessionBuilder,
+                                   (ignore) -> {
+                                   },
+                                   () -> {
+                                   });
+    }
+
+    /**
+     * 处理设备消息
+     *
+     * @param message                设备消息
+     * @param sessionBuilder         会话构造器,在会话不存在时,创建会话
+     * @param sessionConsumer        会话自定义回调,处理会话时用来自定义会话,比如重置连接信息
+     * @param deviceNotFoundCallback 设备不存在的监听器回调
+     * @return 设备操作接口
+     */
+    public Mono<DeviceOperator> handleDeviceMessage(DeviceMessage message,
+                                                    Function<DeviceOperator, DeviceSession> sessionBuilder,
+                                                    Consumer<DeviceSession> sessionConsumer,
+                                                    Runnable deviceNotFoundCallback) {
+
+        return handleDeviceMessage(message, sessionBuilder, sessionConsumer, () -> Mono.fromRunnable(deviceNotFoundCallback));
+    }
+
+    private Mono<Void> handleChildrenDeviceMessage(String deviceId, DeviceMessage children) {
+        //设备状态检查,断开设备连接的消息都忽略
+        //这些消息属于状态管理,通常是用来自定义子设备状态的,所以这些消息都忽略处理会话
+        if (deviceId == null
+            || children instanceof DeviceStateCheckMessage
+            || children instanceof DeviceStateCheckMessageReply
+            || children instanceof DisconnectDeviceMessage
+            || children instanceof DisconnectDeviceMessageReply
+            || children.getHeaderOrDefault(Headers.ignoreSession)) {
+            return Mono.empty();
+        }
+        //子设备回复失败的也忽略
+        if (children instanceof DeviceMessageReply) {
+            DeviceMessageReply reply = ((DeviceMessageReply) children);
+            if (!reply.isSuccess()) {
+                return Mono.empty();
+            }
+        }
+        String childrenId = children.getDeviceId();
+
+        //子设备离线或者注销
+        if (children instanceof DeviceOfflineMessage || children instanceof DeviceUnRegisterMessage) {
+            //注销会话,这里子设备可能会收到多次离线消息
+            //注销会话一次离线,消息网关转发子设备消息一次
+            return sessionManager
+                .remove(childrenId, removeSessionOnlyLocal(children))
+                .doOnNext(total -> {
+                    if (total > 0) {
+                        children.addHeader(Headers.ignore, true);
+                    }
+                })
+                .then();
+        } else {
+            //子设备上线
+            if (children instanceof DeviceOnlineMessage) {
+                children.addHeader(Headers.ignore, true);
+            }
+            //子设备会话处理
+            Mono<DeviceSession> sessionHandler = sessionManager
+                .getSession(deviceId)
+                .flatMap(parentSession -> this
+                    .createOrUpdateSession(childrenId,
+                                           children,
+                                           child -> Mono.just(new ChildrenDeviceSession(childrenId, parentSession, child)),
+                                           Mono::empty));
+
+
+            //子设备注册
+            if (isDoRegister(children)) {
+                return this
+                    .getDeviceForRegister(children.getDeviceId())
+                    .flatMap(device -> device
+                        //没有配置状态自管理才自动上线
+                        .getSelfConfig(DeviceConfigKey.selfManageState)
+                        .defaultIfEmpty(false)
+                        .filter(Boolean.FALSE::equals))
+                    .flatMap(ignore -> sessionHandler)
+                    .then();
+            }
+            return sessionHandler.then();
+        }
+    }
+
+    public Mono<DeviceOperator> handleDeviceMessage(DeviceMessage message,
+                                                    Function<DeviceOperator, Mono<DeviceSession>> sessionBuilder,
+                                                    Function<DeviceSession, Mono<Void>> sessionConsumer,
+                                                    Supplier<Mono<DeviceOperator>> deviceNotFoundCallback) {
+        String deviceId = message.getDeviceId();
+        if (!StringUtils.hasText(deviceId)) {
+            return Mono.empty();
+        }
+        Mono<DeviceOperator> then = null;
+        boolean doHandle = true;
+        //子设备消息
+        if (message instanceof ChildDeviceMessage) {
+            DeviceMessage childrenMessage = (DeviceMessage) ((ChildDeviceMessage) message).getChildDeviceMessage();
+            then = handleChildrenDeviceMessage(deviceId, childrenMessage)
+                .then(registry.getDevice(deviceId));
+        }
+        //子设备消息回复
+        else if (message instanceof ChildDeviceMessageReply) {
+            DeviceMessage childrenMessage = (DeviceMessage) ((ChildDeviceMessageReply) message).getChildDeviceMessage();
+            then = handleChildrenDeviceMessage(deviceId, childrenMessage)
+                .then(registry.getDevice(deviceId));
+        }
+        //设备离线消息
+        else if (message instanceof DeviceOfflineMessage) {
+            return sessionManager
+                .remove(deviceId, removeSessionOnlyLocal(message))
+                .flatMap(l -> {
+                    if (l == 0) {
+                        return registry
+                            .getDevice(deviceId)
+                            .flatMap(device -> messageHandler.handleMessage(device, message));
+                    }
+                    return Mono.empty();
+                })
+                .then(registry.getDevice(deviceId))
+                .contextWrite(Context.of(DeviceMessage.class, message));
+        }
+        //设备上线消息,不发送到messageHandler,防止设备上线存在重复消息
+        else if (message instanceof DeviceOnlineMessage) {
+            doHandle = message
+                .getHeader(Headers.force)
+                .orElse(false);
+        }
+
+        //忽略会话管理,比如一个设备存在多种接入方式时,其中一种接入方式收到的消息设置忽略会话来防止会话冲突
+        if (message.getHeaderOrDefault(Headers.ignoreSession)) {
+            return registry
+                .getDevice(deviceId)
+                .flatMap(device -> {
+                    if (!isDoRegister(message)) {
+                        return messageHandler
+                            .handleMessage(device, message)
+                            .thenReturn(device);
+                    }
+                    return Mono.just(device);
+                });
+
+        }
+
+        if (then == null) {
+            then = registry.getDevice(deviceId);
+        }
+
+        if (doHandle) {
+            then = messageHandler
+                .handleMessage(null, message)
+                .then(then);
+        }
+
+        return this
+            .createOrUpdateSession(deviceId, message, sessionBuilder, deviceNotFoundCallback)
+            .flatMap(sessionConsumer)
+            .then(then)
+            .contextWrite(Context.of(DeviceMessage.class, message));
+
+    }
+
+    private Mono<DeviceSession> createOrUpdateSession(String deviceId,
+                                                      DeviceMessage message,
+                                                      Function<DeviceOperator, Mono<DeviceSession>> sessionBuilder,
+                                                      Supplier<Mono<DeviceOperator>> deviceNotFoundCallback) {
+        return sessionManager
+            .getSession(deviceId, false)
+            .filterWhen(DeviceSession::isAliveAsync)
+            .map(old -> {
+                //需要更新会话时才进行更新
+                if (needUpdateSession(old, message)) {
+                    return sessionManager
+                        .compute(deviceId, null, session -> updateSession(session, message, sessionBuilder));
+                }
+                applySessionKeepaliveTimeout(message, old);
+                old.keepAlive();
+                return Mono.just(old);
+            })
+            //会话不存在则尝试创建或者更新
+            .defaultIfEmpty(Mono.defer(() -> sessionManager
+                .compute(deviceId,
+                         createNewSession(
+                             deviceId,
+                             message,
+                             sessionBuilder,
+                             () -> {
+                                 //设备注册
+                                 if (isDoRegister(message)) {
+                                     return messageHandler
+                                         .handleMessage(null, message)
+                                         //延迟2秒后尝试重新获取设备并上线
+                                         .then(Mono.delay(Duration.ofSeconds(2)))
+                                         .then(registry.getDevice(deviceId));
+                                 }
+                                 if (deviceNotFoundCallback != null) {
+                                     return deviceNotFoundCallback.get();
+                                 }
+                                 return Mono.empty();
+                             }),
+                         session -> updateSession(session, message, sessionBuilder))))
+            .flatMap(Function.identity());
+    }
+
+    private Mono<DeviceOperator> getDeviceForRegister(String deviceId) {
+        return registry
+            .getDevice(deviceId)
+            .switchIfEmpty(Mono.defer(() -> Mono
+                //延迟2秒,因为自动注册是异步的,收到消息后并不能保证马上可以注册成功.
+                .delay(Duration.ofSeconds(2))
+                .then(registry.getDevice(deviceId))));
+    }
+
+    private Mono<DeviceSession> createNewSession(String deviceId,
+                                                 DeviceMessage message,
+                                                 Function<DeviceOperator, Mono<DeviceSession>> sessionBuilder,
+                                                 Supplier<Mono<DeviceOperator>> deviceNotFoundCallback) {
+        return registry
+            .getDevice(deviceId)
+            .switchIfEmpty(Mono.defer(deviceNotFoundCallback))
+            .flatMap(device -> sessionBuilder
+                .apply(device)
+                .map(newSession -> {
+                    //保持在线,在低功率设备上,可能无法保持长连接,通过keepOnline的header来标识让设备保持在线
+                    if (message.getHeader(Headers.keepOnline).orElse(false)) {
+                        int timeout = message.getHeaderOrDefault(Headers.keepOnlineTimeoutSeconds);
+                        newSession = new KeepOnlineSession(newSession, Duration.ofSeconds(timeout));
+                    }
+                    return newSession;
+                }));
+    }
+
+    private Mono<DeviceSession> updateSession(DeviceSession session,
+                                              DeviceMessage message,
+                                              Function<DeviceOperator, Mono<DeviceSession>> sessionBuilder) {
+
+        return session
+            .isAliveAsync()
+            .flatMap(alive -> {
+                //设备会话存活才更新
+                if (alive) {
+                    return updateSession0(session, message, sessionBuilder);
+                }
+                //创建新的会话
+                return createNewSession(message.getDeviceId(), message, sessionBuilder, Mono::empty);
+            });
+    }
+
+    private Mono<DeviceSession> updateSession0(DeviceSession session,
+                                               DeviceMessage message,
+                                               Function<DeviceOperator, Mono<DeviceSession>> sessionBuilder) {
+        Mono<DeviceSession> after = null;
+        //消息中指定保持在线,并且之前的会话不是保持在线,则需要替换之前的会话
+        if (isNewKeeOnline(session, message)) {
+            Integer timeoutSeconds = message.getHeaderOrDefault(Headers.keepOnlineTimeoutSeconds);
+            //替换session
+            session = new KeepOnlineSession(session, Duration.ofSeconds(timeoutSeconds));
+        }
+        //KeepOnline的连接丢失时(服务重启等操作),设备上线后替换丢失的会话,让其能恢复下行能力。
+        if (isKeeOnlineLost(session)) {
+            Integer timeoutSeconds = message.getHeaderOrDefault(Headers.keepOnlineTimeoutSeconds);
+            after = sessionBuilder
+                .apply(session.getOperator())
+                .map(newSession -> new KeepOnlineSession(newSession, Duration.ofSeconds(timeoutSeconds)));
+        }
+        applySessionKeepaliveTimeout(message, session);
+        session.keepAlive();
+        return after == null
+            ? Mono.just(session)
+            : after;
+    }
+
+    private static void applySessionKeepaliveTimeout(DeviceMessage msg, DeviceSession session) {
+        Integer timeout = msg.getHeaderOrElse(Headers.keepOnlineTimeoutSeconds, () -> null);
+        if (null != timeout) {
+            session.setKeepAliveTimeout(Duration.ofSeconds(timeout));
+        }
+    }
+
+    //是否只移除当前节点中的会话,默认false,表示下行则移除整个集群的会话.
+    //设置addHeader("clearAllSession",false); 表示只移除本地会话
+    private boolean removeSessionOnlyLocal(DeviceMessage message) {
+        return message
+            .getHeader(Headers.clearAllSession)
+            .map(val -> !val)
+            .orElse(false);
+    }
+
+    //判断是否需要更新会话
+    private static boolean needUpdateSession(DeviceSession session, DeviceMessage message) {
+        return isNewKeeOnline(session, message) || isKeeOnlineLost(session);
+    }
+
+    //判断是否为新的保持在线消息
+    private static boolean isNewKeeOnline(DeviceSession session, DeviceMessage message) {
+        return message.getHeader(Headers.keepOnline).orElse(false) && !(session instanceof KeepOnlineSession);
+    }
+
+    //判断保持在线的会话是否以及丢失(服务重启后可能出现)
+    private static boolean isKeeOnlineLost(DeviceSession session) {
+        if (!session.isWrapFrom(KeepOnlineSession.class)) {
+            return false;
+        }
+        return session.isWrapFrom(LostDeviceSession.class)
+            || !session.unwrap(KeepOnlineSession.class).getParent().isAlive();
+    }
+
+    //判断是否为设备注册
+    private static boolean isDoRegister(DeviceMessage message) {
+        return message instanceof DeviceRegisterMessage
+            && message.getHeader(PropertyConstants.deviceName).isPresent()
+            && message.getHeader(PropertyConstants.productId).isPresent();
+    }
+
+
+    /**
+     * 处理设备消息
+     *
+     * @param message                设备消息
+     * @param sessionBuilder         会话构造器,在会话不存在时,创建会话
+     * @param sessionConsumer        会话自定义回调,处理会话时用来自定义会话,比如重置连接信息
+     * @param deviceNotFoundCallback 设备不存在的监听器回调
+     * @return 设备操作接口
+     */
+    public Mono<DeviceOperator> handleDeviceMessage(DeviceMessage message,
+                                                    Function<DeviceOperator, DeviceSession> sessionBuilder,
+                                                    Consumer<DeviceSession> sessionConsumer,
+                                                    Supplier<Mono<DeviceOperator>> deviceNotFoundCallback) {
+        return this
+            .handleDeviceMessage(
+                message,
+                device -> Mono.justOrEmpty(sessionBuilder.apply(device)),
+                session -> {
+                    sessionConsumer.accept(session);
+                    return Mono.empty();
+                },
+                deviceNotFoundCallback
+            );
+
+    }
+
+
+}

+ 63 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayManager.java

@@ -1,15 +1,78 @@
 package org.jetlinks.community.gateway;
 
 import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
+import org.jetlinks.community.network.channel.ChannelInfo;
+import org.jetlinks.community.network.channel.ChannelProvider;
 import reactor.core.publisher.Mono;
 
 import java.util.List;
+import java.util.Optional;
 
+/**
+ * 设备接入网关管理器,统一管理设备接入网关等相关信息
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
 public interface DeviceGatewayManager {
 
+    /**
+     * 获取接入网关
+     *
+     * @param id ID
+     * @return 接入网关
+     */
     Mono<DeviceGateway> getGateway(String id);
 
+    /**
+     * 重新加载网关
+     *
+     * @param gatewayId 网关ID
+     * @return void
+     * @since 2.0
+     */
+    Mono<Void> reload(String gatewayId);
+
+    /**
+     * 停止网关
+     *
+     * @param gatewayId 网关ID
+     * @return void
+     */
     Mono<Void> shutdown(String gatewayId);
 
+    /**
+     * 启动网关
+     *
+     * @param id 网关ID
+     * @return void
+     */
+    Mono<Void> start(String id);
+
+    /**
+     * 获取接入网关通道信息,通道中包含接入地址等信息
+     *
+     * @param channel   通道标识,如 network
+     * @param channelId 通道ID
+     * @return 通道信息
+     * @see ChannelProvider#getChannel()
+     * @see 2.0
+     */
+    Mono<ChannelInfo> getChannel(String channel, String channelId);
+
+    /**
+     * 获取全部的设备接入网关提供商
+     *
+     * @return 设备接入网关提供商
+     * @see DeviceGatewayProvider
+     */
     List<DeviceGatewayProvider> getProviders();
+
+    /**
+     * 根据接入提供商标识获取设备接入提供商接口
+     *
+     * @param provider 提供商标识
+     * @return DeviceGatewayProvider
+     */
+    Optional<DeviceGatewayProvider> getProvider(String provider);
 }

+ 33 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/GatewayConfiguration.java

@@ -0,0 +1,33 @@
+package org.jetlinks.community.gateway;
+
+import org.jetlinks.community.gateway.supports.*;
+import org.jetlinks.community.network.channel.ChannelProvider;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration
+@ConditionalOnClass(ChannelProvider.class)
+public class GatewayConfiguration {
+
+
+    @Bean
+    @ConditionalOnBean(DeviceGatewayPropertiesManager.class)
+    public DeviceGatewayManager deviceGatewayManager(ObjectProvider<ChannelProvider> channelProviders,
+                                                     ObjectProvider<DeviceGatewayProvider> gatewayProviders,
+                                                     DeviceGatewayPropertiesManager propertiesManager) {
+        DefaultDeviceGatewayManager gatewayManager=new DefaultDeviceGatewayManager(propertiesManager);
+        channelProviders.forEach(gatewayManager::addChannelProvider);
+        gatewayProviders.forEach(gatewayManager::addGatewayProvider);
+        gatewayProviders.forEach(DeviceGatewayProviders::register);
+        return gatewayManager;
+    }
+
+    @Bean
+    public ChildDeviceGatewayProvider childDeviceGatewayProvider(){
+        return new ChildDeviceGatewayProvider();
+    }
+
+}

+ 57 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/ChildDeviceGatewayProvider.java

@@ -0,0 +1,57 @@
+package org.jetlinks.community.gateway.supports;
+
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.community.gateway.AbstractDeviceGateway;
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.network.NetworkType;
+import reactor.core.publisher.Mono;
+
+public class ChildDeviceGatewayProvider implements DeviceGatewayProvider {
+
+    @Override
+    public String getId() {
+        return "child-device";
+    }
+
+    @Override
+    public String getName() {
+        return "网关子设备接入";
+    }
+
+    @Override
+    public String getDescription() {
+        return "通过其他网关设备来接入子设备";
+    }
+
+    @Override
+    public String getChannel() {
+        return "child-device";
+    }
+
+
+    public Transport getTransport() {
+        return Transport.of("Gateway");
+    }
+
+    @Override
+    public Mono<? extends DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties) {
+        return Mono.just(new ChildDeviceGateway(properties.getId()));
+    }
+
+    static class ChildDeviceGateway extends AbstractDeviceGateway {
+
+        public ChildDeviceGateway(String id) {
+            super(id);
+        }
+
+        @Override
+        protected Mono<Void> doShutdown() {
+            return Mono.empty();
+        }
+
+        @Override
+        protected Mono<Void> doStartup() {
+            return Mono.empty();
+        }
+    }
+}

+ 101 - 58
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DefaultDeviceGatewayManager.java

@@ -1,103 +1,146 @@
 package org.jetlinks.community.gateway.supports;
 
+import lombok.extern.slf4j.Slf4j;
 import org.jetlinks.community.gateway.DeviceGateway;
 import org.jetlinks.community.gateway.DeviceGatewayManager;
-import org.springframework.beans.BeansException;
-import org.springframework.beans.factory.config.BeanPostProcessor;
-import org.springframework.stereotype.Component;
+import org.jetlinks.community.network.channel.ChannelInfo;
+import org.jetlinks.community.network.channel.ChannelProvider;
+import org.jetlinks.core.cache.ReactiveCacheContainer;
+import org.springframework.util.StringUtils;
 import reactor.core.publisher.Mono;
 
-import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
 
-/**
- * 设备网关管理器
- * <p>
- * TCP   UDP   MQTT  CoAP
- *
- * @author zhouhao
- */
-@Component
-public class DefaultDeviceGatewayManager implements DeviceGatewayManager, BeanPostProcessor {
+@Slf4j
+public class DefaultDeviceGatewayManager implements DeviceGatewayManager {
 
     private final DeviceGatewayPropertiesManager propertiesManager;
 
-    /**
-     * TCP MQTT的设备网关服务提供者
-     */
     private final Map<String, DeviceGatewayProvider> providers = new ConcurrentHashMap<>();
 
-    /**
-     * 启动状态的设备网关
-     */
-    private final Map<String, DeviceGateway> store = new ConcurrentHashMap<>();
+    private final ReactiveCacheContainer<String, DeviceGateway> store = ReactiveCacheContainer.create();
+
+    private final Map<String, ChannelProvider> channels = new ConcurrentHashMap<>();
+
+    public void addChannelProvider(ChannelProvider provider) {
+        channels.put(provider.getChannel(), provider);
+    }
+
+    public void addGatewayProvider(DeviceGatewayProvider provider) {
+        providers.put(provider.getId(), provider);
+    }
 
     public DefaultDeviceGatewayManager(DeviceGatewayPropertiesManager propertiesManager) {
         this.propertiesManager = propertiesManager;
     }
 
-    /**
-     * 获取设备网关,有则返回,没有就创建返回
-     *
-     * @param id 网关ID
-     * @return 设备网关
-     */
     private Mono<DeviceGateway> doGetGateway(String id) {
-        if (store.containsKey(id)) {
-            return Mono.just(store.get(id));
+        if (null == id) {
+            return Mono.empty();
         }
+        return store
+            .computeIfAbsent(id, this::createGateway);
+    }
 
-        // 数据库查 DeviceGatewayEntity 转换成 DeviceGatewayProperties
-        // BeanMap中找provider 找不到就是不支持
-        // 创建设备网关
-        // double check 防止重复创建
+
+    protected Mono<DeviceGateway> createGateway(String id) {
         return propertiesManager
             .getProperties(id)
             .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("网关配置[" + id + "]不存在")))
-            .flatMap(properties -> Mono
-                .justOrEmpty(providers.get(properties.getProvider()))
-                .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("不支持的网络服务[" + properties.getProvider() + "]")))
-                .flatMap(provider -> provider
-                    .createDeviceGateway(properties)
-                    .flatMap(gateway -> {
-                        if (store.containsKey(id)) {
-                            return gateway
-                                .shutdown()
-                                .thenReturn(store.get(id));
-                        }
-                        store.put(id, gateway);
-                        return Mono.justOrEmpty(gateway);
-                    })));
+            .flatMap(properties -> getProviderNow(properties.getProvider()).createDeviceGateway(properties));
+
+    }
+
+    public Mono<Void> doShutdown(String gatewayId) {
+        return Mono.justOrEmpty(store.remove(gatewayId))
+                   .flatMap(DeviceGateway::shutdown)
+                   .doOnSuccess(nil -> log.debug("shutdown device gateway {}", gatewayId))
+                   .doOnError(err -> log.error("shutdown device gateway {} error", gatewayId, err));
     }
 
     @Override
     public Mono<Void> shutdown(String gatewayId) {
-        return Mono.justOrEmpty(store.remove(gatewayId))
-            .flatMap(DeviceGateway::shutdown);
+        return doShutdown(gatewayId);
+    }
+
+    public Mono<Void> doStart(String id) {
+        return this
+            .getGateway(id)
+            .flatMap(DeviceGateway::startup)
+            .doOnSuccess(nil -> log.debug("started device gateway {}", id))
+            .doOnError(err -> log.error("start device gateway {} error", id, err));
+    }
+
+    @Override
+    public Mono<Void> start(String gatewayId) {
+        return this
+            .doStart(gatewayId);
     }
 
     @Override
     public Mono<DeviceGateway> getGateway(String id) {
-        return Mono
-            .justOrEmpty(store.get(id))
-            .switchIfEmpty(doGetGateway(id));
+        return doGetGateway(id);
+    }
+
+    @Override
+    public Mono<Void> reload(String gatewayId) {
+        return this
+            .doReload(gatewayId);
+    }
+
+    private Mono<Void> doReload(String gatewayId) {
+        return propertiesManager
+            .getProperties(gatewayId)
+            .flatMap(prop -> {
+
+                DeviceGatewayProvider provider = this.getProviderNow(prop.getProvider());
+                return store
+                    .compute(gatewayId, (id, gateway) -> {
+                        if (gateway != null) {
+                            log.debug("reload device gateway {} {}:{}", prop.getName(), prop.getProvider(), prop.getId());
+                            return provider
+                                .reloadDeviceGateway(gateway, prop)
+                                .cast(DeviceGateway.class);
+                        }
+                        log.debug("create device gateway {} {}:{}", prop.getName(), prop.getProvider(), prop.getId());
+                        return provider
+                            .createDeviceGateway(prop)
+                            .flatMap(newer -> newer.startup().thenReturn(newer));
+                    });
+            })
+            .then();
     }
 
     @Override
     public List<DeviceGatewayProvider> getProviders() {
-        return new ArrayList<>(providers.values());
+        return providers
+            .values()
+            .stream()
+            .sorted(Comparator.comparingInt(DeviceGatewayProvider::getOrder))
+            .collect(Collectors.toList());
     }
 
     @Override
-    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
-        if (bean instanceof DeviceGatewayProvider) {
-            DeviceGatewayProvider provider = ((DeviceGatewayProvider) bean);
-            providers.put(provider.getId(), provider);
-        }
-        return bean;
+    public Optional<DeviceGatewayProvider> getProvider(String provider) {
+        return Optional.ofNullable(providers.get(provider));
+    }
+
+    public DeviceGatewayProvider getProviderNow(String provider) {
+        return DeviceGatewayProviders.getProviderNow(provider);
     }
 
+    @Override
+    public Mono<ChannelInfo> getChannel(String channel, String channelId) {
+        if (!StringUtils.hasText(channel) || !StringUtils.hasText(channel)) {
+            return Mono.empty();
+        }
+        return Mono.justOrEmpty(channels.get(channel))
+                   .flatMap(provider -> provider.getChannelInfo(channelId));
+    }
 
 }

+ 8 - 2
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProperties.java

@@ -21,11 +21,17 @@ public class DeviceGatewayProperties implements ValueObject {
 
     private String id;
 
+    private String name;
+
+    private String description;
+
     private String provider;
 
-    private String networkId;
+    private String channelId;
+
+    private String protocol;
 
-    private Map<String, Object> configuration = new HashMap<>();
+    private Map<String,Object> configuration=new HashMap<>();
 
     @Override
     public Map<String, Object> values() {

+ 2 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayPropertiesManager.java

@@ -1,5 +1,6 @@
 package org.jetlinks.community.gateway.supports;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 /**
@@ -17,5 +18,6 @@ public interface DeviceGatewayPropertiesManager {
      */
     Mono<DeviceGatewayProperties> getProperties(String id);
 
+    Flux<DeviceGatewayProperties> getPropertiesByChannel(String channel);
 
 }

+ 55 - 3
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProvider.java

@@ -1,5 +1,7 @@
 package org.jetlinks.community.gateway.supports;
 
+import org.jetlinks.core.Wrapper;
+import org.jetlinks.core.message.codec.Transport;
 import org.jetlinks.community.gateway.DeviceGateway;
 import org.jetlinks.community.network.NetworkType;
 import reactor.core.publisher.Mono;
@@ -12,14 +14,64 @@ import reactor.core.publisher.Mono;
  * @see DeviceGateway
  * @since 1.0
  */
-public interface DeviceGatewayProvider {
+public interface DeviceGatewayProvider extends Wrapper {
 
+    String CHANNEL_NETWORK = "network";
+
+    /**
+     * @return 唯一标识
+     */
     String getId();
 
+    /**
+     * @return 名称
+     */
     String getName();
 
-    NetworkType getNetworkType();
+    /**
+     * @return 接入说明
+     */
+    default String getDescription() {
+        return null;
+    }
+
+    /**
+     * 接入通道,如: network,modbus
+     *
+     * @return 通道
+     */
+    default String getChannel() {
+        return CHANNEL_NETWORK;
+    }
+
+    /**
+     * @return 排序。从小到大排序
+     */
+    default int getOrder() {
+        return Integer.MAX_VALUE;
+    }
+
+    /**
+     * @return 传输协议
+     */
+    Transport getTransport();
 
-    Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties);
+    /**
+     * 使用配置信息创建设备网关
+     *
+     * @param properties 配置
+     * @return void
+     */
+    Mono<? extends DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties);
 
+    /**
+     * 重新加载网关
+     *
+     * @param gateway    网关
+     * @param properties 配置信息
+     * @return void
+     */
+    default Mono<? extends DeviceGateway> reloadDeviceGateway(DeviceGateway gateway, DeviceGatewayProperties properties) {
+        return Mono.just(gateway);
+    }
 }

+ 34 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProviders.java

@@ -0,0 +1,34 @@
+package org.jetlinks.community.gateway.supports;
+
+import org.hswebframework.web.exception.I18nSupportException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class DeviceGatewayProviders {
+    private static final Map<String, DeviceGatewayProvider> providers = new ConcurrentHashMap<>();
+
+    public static void register(DeviceGatewayProvider provider) {
+        providers.put(provider.getId(), provider);
+    }
+
+    public static Optional<DeviceGatewayProvider> getProvider(String provider) {
+        return Optional.ofNullable(providers.get(provider));
+    }
+
+    public static DeviceGatewayProvider getProviderNow(String provider) {
+        DeviceGatewayProvider gatewayProvider = providers.get(provider);
+        if (null == gatewayProvider) {
+            throw new I18nSupportException("error.unsupported_device_gateway_provider", provider);
+        }
+        return gatewayProvider;
+    }
+
+    public static List<DeviceGatewayProvider> getAll() {
+        return new ArrayList<>(providers.values());
+    }
+
+}

+ 1 - 0
jetlinks-components/gateway-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+org.jetlinks.community.gateway.GatewayConfiguration

+ 64 - 0
jetlinks-components/network-component/http-component/pom.xml

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>network-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>2.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>http-component</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webflux</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-core</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>network-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.vertx</groupId>
+            <artifactId>vertx-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.vertx</groupId>
+            <artifactId>vertx-web-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.vertx</groupId>
+            <artifactId>vertx-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.projectreactor.netty</groupId>
+            <artifactId>reactor-netty</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>gateway-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>net.bytebuddy</groupId>
+            <artifactId>byte-buddy</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>net.bytebuddy</groupId>
+            <artifactId>byte-buddy-agent</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 79 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/DefaultHttpRequestMessage.java

@@ -0,0 +1,79 @@
+package org.jetlinks.community.network.http;
+
+import com.alibaba.fastjson.JSON;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.jetlinks.core.message.codec.http.Header;
+import org.jetlinks.core.message.codec.http.HttpRequestMessage;
+import org.jetlinks.core.message.codec.http.HttpUtils;
+import org.jetlinks.core.message.codec.http.MultiPart;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+
+import java.util.*;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Generated
+public class DefaultHttpRequestMessage implements HttpRequestMessage {
+
+    //消息体
+    private ByteBuf payload;
+
+    private String url;
+
+    //请求方法
+    private HttpMethod method;
+
+    //请求头
+    private List<Header> headers = new ArrayList<>();
+
+    //参数
+    private Map<String, String> queryParameters = new HashMap<>();
+
+    //请求类型
+    private MediaType contentType;
+
+    private MultiPart multiPart;
+
+    @Override
+    public Optional<MultiPart> multiPart() {
+        return Optional
+            .ofNullable(multiPart)
+            .filter(part -> part.getParts().size() > 0);
+    }
+
+    public void setBody(Object body) {
+        if (body instanceof ByteBuf) {
+            setPayload(((ByteBuf) body));
+        } else if (body instanceof String) {
+            setPayload(Unpooled.wrappedBuffer(((String) body).getBytes()));
+        } else if (body instanceof byte[]) {
+            setPayload(Unpooled.wrappedBuffer(((byte[]) body)));
+        } else if (MediaType.APPLICATION_JSON.includes(getContentType())) {
+            setPayload(Unpooled.wrappedBuffer(JSON.toJSONBytes(body)));
+        } else if (MediaType.APPLICATION_FORM_URLENCODED.includes(getContentType()) && body instanceof Map) {
+            setPayload(Unpooled.wrappedBuffer(HttpUtils.createEncodedUrlParams(((Map<?, ?>) body)).getBytes()));
+        } else if (body != null) {
+            setPayload(Unpooled.wrappedBuffer(JSON.toJSONBytes(body)));
+        } else {
+            setPayload(Unpooled.EMPTY_BUFFER);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return print();
+    }
+}

+ 39 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/DefaultHttpResponseMessage.java

@@ -0,0 +1,39 @@
+package org.jetlinks.community.network.http;
+
+import io.netty.buffer.ByteBuf;
+import lombok.AllArgsConstructor;
+import lombok.Generated;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.jetlinks.core.message.codec.http.Header;
+import org.jetlinks.core.message.codec.http.HttpResponseMessage;
+import org.springframework.http.MediaType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Generated
+public class DefaultHttpResponseMessage implements HttpResponseMessage {
+
+    private int status;
+
+    private MediaType contentType;
+
+    private List<Header> headers = new ArrayList<>();
+
+    private ByteBuf payload;
+
+    @Override
+    public String toString() {
+        return print();
+    }
+}

+ 39 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/VertxWebUtils.java

@@ -0,0 +1,39 @@
+package org.jetlinks.community.network.http;
+
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SocketAddress;
+import org.springframework.util.StringUtils;
+
+import java.util.Optional;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public class VertxWebUtils {
+
+    static final String[] ipHeaders = {
+        "X-Forwarded-For",
+        "X-Real-IP",
+        "Proxy-Client-IP",
+        "WL-Proxy-Client-IP"
+    };
+
+    /**
+     * 获取请求客户端的真实ip地址
+     *
+     * @param request 请求对象
+     * @return ip地址
+     */
+    public static String getIpAddr(HttpServerRequest request) {
+        for (String ipHeader : ipHeaders) {
+            String ip = request.getHeader(ipHeader);
+            if (!StringUtils.isEmpty(ip) && !ip.contains("unknown")) {
+                return ip;
+            }
+        }
+        return Optional.ofNullable(request.remoteAddress())
+            .map(SocketAddress::host)
+            .orElse("unknown");
+    }
+}

+ 103 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/HttpDeviceSession.java

@@ -0,0 +1,103 @@
+package org.jetlinks.community.network.http.device;
+
+import org.jetlinks.core.device.DeviceOperator;
+import org.jetlinks.core.message.codec.DefaultTransport;
+import org.jetlinks.core.message.codec.EncodedMessage;
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.core.server.session.DeviceSession;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nullable;
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.Optional;
+
+/**
+ * Http 设备会话
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+class HttpDeviceSession implements DeviceSession {
+
+    private final DeviceOperator operator;
+
+    private final InetSocketAddress address;
+
+    private long lastPingTime = System.currentTimeMillis();
+
+    //默认永不超时
+    private long keepAliveTimeOutMs = -1;
+
+    public HttpDeviceSession(DeviceOperator deviceOperator, InetSocketAddress address) {
+        this.operator = deviceOperator;
+        this.address = address;
+    }
+
+    @Override
+    public String getId() {
+        return operator.getDeviceId();
+    }
+
+    @Override
+    public String getDeviceId() {
+        return operator.getDeviceId();
+    }
+
+    @Nullable
+    @Override
+    public DeviceOperator getOperator() {
+        return operator;
+    }
+
+    @Override
+    public long lastPingTime() {
+        return lastPingTime;
+    }
+
+    @Override
+    public long connectTime() {
+        return lastPingTime;
+    }
+
+    @Override
+    public Mono<Boolean> send(EncodedMessage encodedMessage) {
+        return Mono.just(false);
+    }
+
+    @Override
+    public Transport getTransport() {
+        return DefaultTransport.HTTP;
+    }
+
+    @Override
+    public Optional<InetSocketAddress> getClientAddress() {
+        return Optional.ofNullable(address);
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public void setKeepAliveTimeout(Duration timeout) {
+        keepAliveTimeOutMs = timeout.toMillis();
+    }
+
+    @Override
+    public void ping() {
+        lastPingTime = System.currentTimeMillis();
+    }
+
+    @Override
+    public boolean isAlive() {
+        return keepAliveTimeOutMs <= 0
+            || System.currentTimeMillis() - lastPingTime < keepAliveTimeOutMs;
+    }
+
+    @Override
+    public void onClose(Runnable call) {
+
+    }
+}

+ 206 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/HttpServerDeviceGateway.java

@@ -0,0 +1,206 @@
+package org.jetlinks.community.network.http.device;
+
+import lombok.AllArgsConstructor;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.hswebframework.web.logger.ReactiveLogger;
+import org.jetlinks.core.ProtocolSupport;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.device.session.DeviceSessionManager;
+import org.jetlinks.core.message.DeviceMessage;
+import org.jetlinks.core.message.codec.DefaultTransport;
+import org.jetlinks.core.message.codec.FromDeviceMessageContext;
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.core.route.HttpRoute;
+import org.jetlinks.core.trace.MonoTracer;
+import org.jetlinks.core.utils.TopicUtils;
+import org.jetlinks.community.gateway.AbstractDeviceGateway;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.http.server.HttpExchange;
+import org.jetlinks.community.network.http.server.HttpServer;
+import org.jetlinks.community.gateway.DeviceGatewayHelper;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import reactor.core.Disposable;
+import reactor.core.publisher.Mono;
+
+import java.net.InetSocketAddress;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Http 服务设备网关,使用指定的协议包,将网络组件中Http服务的请求处理为设备消息
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+@Slf4j
+public class HttpServerDeviceGateway extends AbstractDeviceGateway {
+
+    final HttpServer httpServer;
+
+    @Setter
+    Mono<ProtocolSupport> protocol;
+
+    private final DeviceRegistry registry;
+
+    private final DeviceGatewayHelper helper;
+
+    private final Map<RouteKey, Disposable> handlers = new ConcurrentHashMap<>();
+
+
+    public HttpServerDeviceGateway(String id,
+                                   HttpServer server,
+                                   Mono<ProtocolSupport> protocol,
+                                   DeviceSessionManager sessionManager,
+                                   DeviceRegistry registry,
+                                   DecodedClientMessageHandler messageHandler) {
+        super(id);
+        this.httpServer = server;
+        this.registry = registry;
+        this.helper = new DeviceGatewayHelper(registry, sessionManager, messageHandler);
+        this.protocol = protocol;
+    }
+
+
+    public Transport getTransport() {
+        return DefaultTransport.HTTP;
+    }
+
+    private Disposable handleRequest(HttpMethod method, String url) {
+        return httpServer
+            .handleRequest(method, url)
+            .filterWhen(exchange -> {
+                if (!isStarted()) {
+                    return Mono
+                        .defer(() -> exchange
+                            .error(HttpStatus.SERVICE_UNAVAILABLE)
+                            .thenReturn(false))
+                        .onErrorReturn(false);
+                }
+                return Mono.just(true);
+            })
+            .flatMap(this::handleHttpRequest, Integer.MAX_VALUE)
+            .subscribe();
+    }
+
+    private Mono<Void> handleHttpRequest(HttpExchange exchange) {
+        return protocol
+            .flatMap(protocol -> exchange
+                .toExchangeMessage()
+                .flatMap(httpMessage -> {
+                    if (log.isDebugEnabled()) {
+                        log.debug("收到HTTP请求\n{}", httpMessage);
+                    }
+                    InetSocketAddress address = exchange.request().getClientAddress();
+                    UnknownHttpDeviceSession session = new UnknownHttpDeviceSession(exchange);
+                    //调用协议执行解码
+                    return protocol
+                        .getMessageCodec(getTransport())
+                        .flatMapMany(codec -> codec.decode(FromDeviceMessageContext.of(session, httpMessage, registry)))
+                        .cast(DeviceMessage.class)
+                        .flatMap(deviceMessage -> {
+                            monitor.receivedMessage();
+                            return helper
+                                .handleDeviceMessage(deviceMessage,
+                                                     device -> new HttpDeviceSession(device, address),
+                                                     ignore -> {
+                                                     },
+                                                     () -> {
+                                                         log.warn("无法从HTTP消息中获取设备信息:\n{}\n\n设备消息:{}", httpMessage, deviceMessage);
+                                                         return exchange
+                                                             .error(HttpStatus.NOT_FOUND)
+                                                             .then(Mono.empty());
+                                                     });
+                        })
+                        .then(Mono.defer(() -> {
+                            //如果协议包里没有回复,那就响应200
+                            if (!exchange.isClosed()) {
+                                return exchange.ok();
+                            }
+                            return Mono.empty();
+                        }))
+                        .onErrorResume(err -> {
+                            log.error("处理http请求失败:\n{}", httpMessage, err);
+                            return response500Error(exchange, err);
+                        })
+                        .then();
+                }))
+            .as(MonoTracer.create("http-device-gateway/" + getId() + exchange.request().getPath()))
+            .onErrorResume((error) -> {
+                log.error(error.getMessage(), error);
+                return response500Error(exchange, error);
+            });
+    }
+
+    private void doReloadRoute(List<HttpRoute> routes) {
+
+        Map<RouteKey, Disposable> readyToRemove = new HashMap<>(handlers);
+        for (HttpRoute route : routes) {
+            for (HttpMethod httpMethod : route.getMethod()) {
+                String addr = TopicUtils
+                    .convertToMqttTopic(route.getAddress())
+                    .replace("+", "*")
+                    .replace("#", "**");
+
+                RouteKey key = RouteKey.of(httpMethod, addr);
+                readyToRemove.remove(key);
+                handlers.computeIfAbsent(key, _key -> handleRequest(_key.method, _key.url));
+            }
+        }
+        //取消处理被移除的url信息
+        for (Disposable value : readyToRemove.values()) {
+            value.dispose();
+        }
+
+    }
+
+    final Mono<Void> reload() {
+
+        return protocol
+            .flatMap(support -> support
+                .getRoutes(DefaultTransport.HTTP)
+                .filter(HttpRoute.class::isInstance)
+                .cast(HttpRoute.class)
+                .collectList()
+                .doOnEach(ReactiveLogger
+                              .onNext(routes -> {
+                                  //协议包里没有配置Http url信息
+                                  if (CollectionUtils.isEmpty(routes)) {
+                                      log.warn("The protocol [{}] is not configured with url information", support.getId());
+                                  }
+                              }))
+                .doOnNext(this::doReloadRoute))
+            .then();
+    }
+
+
+    @AllArgsConstructor(staticName = "of")
+    private static class RouteKey {
+        private HttpMethod method;
+        private String url;
+    }
+
+    private Mono<Void> response500Error(HttpExchange exchange, Throwable err) {
+        return exchange.error(HttpStatus.INTERNAL_SERVER_ERROR, err);
+    }
+
+    @Override
+    protected Mono<Void> doStartup() {
+        return reload();
+    }
+
+    @Override
+    protected Mono<Void> doShutdown() {
+        for (Disposable value : handlers.values()) {
+            value.dispose();
+        }
+        handlers.clear();
+        return Mono.empty();
+    }
+}

+ 117 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/HttpServerDeviceGatewayProvider.java

@@ -0,0 +1,117 @@
+package org.jetlinks.community.network.http.device;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.core.ProtocolSupports;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.device.session.DeviceSessionManager;
+import org.jetlinks.core.message.codec.DefaultTransport;
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.gateway.supports.DeviceGatewayProperties;
+import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkManager;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.http.server.HttpServer;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.Objects;
+
+/**
+ * HTTP 服务设备网关提供商
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+@Component
+@Slf4j
+public class HttpServerDeviceGatewayProvider implements DeviceGatewayProvider {
+    private final NetworkManager networkManager;
+
+    private final DeviceRegistry registry;
+
+    private final DeviceSessionManager sessionManager;
+
+    private final DecodedClientMessageHandler clientMessageHandler;
+
+    private final ProtocolSupports protocolSupports;
+
+    public HttpServerDeviceGatewayProvider(NetworkManager networkManager,
+                                           DeviceRegistry registry,
+                                           DeviceSessionManager sessionManager,
+                                           DecodedClientMessageHandler clientMessageHandler,
+                                           ProtocolSupports protocolSupports) {
+        this.networkManager = networkManager;
+        this.registry = registry;
+        this.sessionManager = sessionManager;
+        this.clientMessageHandler = clientMessageHandler;
+        this.protocolSupports = protocolSupports;
+    }
+
+    @Override
+    public String getId() {
+        return "http-server-gateway";
+    }
+
+    @Override
+    public String getName() {
+        return "HTTP 推送接入";
+    }
+
+    @Override
+    public Transport getTransport() {
+        return DefaultTransport.HTTP;
+    }
+
+    public NetworkType getNetworkType() {
+        return DefaultNetworkType.HTTP_SERVER;
+    }
+
+    @Override
+    public Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties) {
+        return networkManager
+            .<HttpServer>getNetwork(getNetworkType(), properties.getChannelId())
+            .map(server -> {
+                String protocol = properties.getProtocol();
+                return new HttpServerDeviceGateway(properties.getId(),
+                                                   server,
+                                                   Mono.defer(()->protocolSupports.getProtocol(protocol)),
+                                                   sessionManager,
+                                                   registry,
+                                                   clientMessageHandler);
+            });
+    }
+
+    @Override
+    public Mono<? extends DeviceGateway> reloadDeviceGateway(DeviceGateway gateway,
+                                                             DeviceGatewayProperties properties) {
+        HttpServerDeviceGateway deviceGateway = ((HttpServerDeviceGateway) gateway);
+
+        String networkId = properties.getChannelId();
+        //网络组件发生了变化
+        if(!Objects.equals(networkId, deviceGateway.httpServer.getId())){
+            return gateway
+                .shutdown()
+                .then(this
+                          .createDeviceGateway(properties)
+                          .flatMap(gate -> gate.startup().thenReturn(gate)));
+        }
+        //更新协议包
+        deviceGateway.setProtocol(protocolSupports.getProtocol(properties.getProtocol()));
+        return deviceGateway
+            .reload()
+            .thenReturn(deviceGateway);
+    }
+
+    @Getter
+    @Setter
+    public static class RouteConfig {
+        private String url;
+
+        private String protocol;
+    }
+}

+ 102 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/HttpServerExchangeMessage.java

@@ -0,0 +1,102 @@
+package org.jetlinks.community.network.http.device;
+
+import io.netty.buffer.ByteBuf;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.core.message.codec.http.Header;
+import org.jetlinks.core.message.codec.http.HttpExchangeMessage;
+import org.jetlinks.core.message.codec.http.HttpResponseMessage;
+import org.jetlinks.core.message.codec.http.MultiPart;
+import org.jetlinks.community.network.http.server.HttpExchange;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Slf4j
+public class HttpServerExchangeMessage implements HttpExchangeMessage {
+
+
+    AtomicReference<Boolean> responded = new AtomicReference<>(false);
+
+    MultiPart multiPart;
+
+    public HttpServerExchangeMessage(HttpExchange exchange,
+                                     ByteBuf payload,
+                                     MultiPart multiPart) {
+        this.exchange = exchange;
+        this.payload = payload;
+        this.multiPart = multiPart;
+    }
+
+    private final HttpExchange exchange;
+    private final ByteBuf payload;
+
+    @Nonnull
+    @Override
+    public Mono<Void> response(@Nonnull HttpResponseMessage message) {
+        return Mono
+            .defer(() -> {
+                if (!responded.getAndSet(true) && !exchange.isClosed()) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("响应HTTP请求:\n{}", message.print());
+                    }
+                    return exchange.response(message);
+                }
+                return Mono.empty();
+            });
+    }
+
+    @Override
+    public Optional<MultiPart> multiPart() {
+        return Optional
+            .ofNullable(multiPart)
+            .filter(part -> part.getParts().size() > 0);
+    }
+
+    @Nonnull
+    @Override
+    public String getUrl() {
+        return exchange.request().getUrl();
+    }
+
+    @Nonnull
+    @Override
+    public HttpMethod getMethod() {
+        return exchange.request().getMethod();
+    }
+
+    @Nullable
+    @Override
+    public MediaType getContentType() {
+        return exchange.request().getContentType();
+    }
+
+    @Nonnull
+    @Override
+    public List<Header> getHeaders() {
+        return exchange.request().getHeaders();
+    }
+
+    @Nullable
+    @Override
+    public Map<String, String> getQueryParameters() {
+        return exchange.request().getQueryParameters();
+    }
+
+    @Nonnull
+    @Override
+    public ByteBuf getPayload() {
+        return payload;
+    }
+
+    @Override
+    public String toString() {
+        return print();
+    }
+}

+ 97 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/device/UnknownHttpDeviceSession.java

@@ -0,0 +1,97 @@
+package org.jetlinks.community.network.http.device;
+
+import org.jetlinks.core.device.DeviceOperator;
+import org.jetlinks.core.message.codec.DefaultTransport;
+import org.jetlinks.core.message.codec.EncodedMessage;
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.core.server.session.DeviceSession;
+import org.jetlinks.community.network.http.server.HttpExchange;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nullable;
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.Optional;
+
+class UnknownHttpDeviceSession implements DeviceSession {
+
+    private final HttpExchange exchange;
+
+    public UnknownHttpDeviceSession(HttpExchange exchange) {
+        this.exchange = exchange;
+
+    }
+
+    private Duration timeout;
+
+    @Override
+    public String getId() {
+        return "unknown";
+    }
+
+    @Override
+    public String getDeviceId() {
+        return "unknown";
+    }
+
+    @Nullable
+    @Override
+    public DeviceOperator getOperator() {
+        return null;
+    }
+
+    @Override
+    public long lastPingTime() {
+        return 0;
+    }
+
+    @Override
+    public long connectTime() {
+        return 0;
+    }
+
+    @Override
+    public Mono<Boolean> send(EncodedMessage encodedMessage) {
+        return Mono.empty();
+    }
+
+    @Override
+    public Transport getTransport() {
+        return DefaultTransport.HTTP;
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public void ping() {
+
+    }
+
+    @Override
+    public boolean isAlive() {
+        return true;
+    }
+
+    @Override
+    public void onClose(Runnable call) {
+
+    }
+
+    @Override
+    public void setKeepAliveTimeout(Duration timeout) {
+        this.timeout = timeout;
+    }
+
+    @Override
+    public Duration getKeepAliveTimeout() {
+        return timeout;
+    }
+
+    @Override
+    public Optional<InetSocketAddress> getClientAddress() {
+        return Optional.of(exchange.request().getClientAddress());
+    }
+}

+ 130 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/HttpExchange.java

@@ -0,0 +1,130 @@
+package org.jetlinks.community.network.http.server;
+
+import io.netty.buffer.Unpooled;
+import org.apache.commons.collections.CollectionUtils;
+import org.jetlinks.community.network.http.device.HttpServerExchangeMessage;
+import org.jetlinks.core.message.codec.http.HttpExchangeMessage;
+import org.jetlinks.core.message.codec.http.HttpResponseMessage;
+import org.jetlinks.core.message.codec.http.SimpleHttpResponseMessage;
+import org.jetlinks.core.trace.TraceHolder;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import reactor.core.publisher.Mono;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * HTTP交换接口,支持获取请求和发送响应
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+public interface HttpExchange {
+
+    /**
+     * @return 请求ID
+     */
+    String requestId();
+
+    /**
+     * @return 时间戳
+     */
+    long timestamp();
+
+    /**
+     * @return 请求接口
+     */
+    HttpRequest request();
+
+    /**
+     * @return 响应接口
+     */
+    HttpResponse response();
+
+    /**
+     * @return 是否已经完成响应
+     */
+    boolean isClosed();
+
+    /**
+     * 响应错误
+     *
+     * @param status 状态码
+     * @return void
+     */
+    default Mono<Void> error(@NotNull HttpStatus status) {
+        return response(status, "{\"message\":\"" + status.getReasonPhrase() + "\"}");
+    }
+
+    /**
+     * 响应成功
+     *
+     * @return void
+     */
+    default Mono<Void> ok() {
+        return response(HttpStatus.OK, "{\"message\":\"OK\"}");
+    }
+
+    /**
+     * 响应指定当状态码和响应头
+     *
+     * @param status 状态码
+     * @param body   响应体
+     * @return void
+     */
+    default Mono<Void> response(@NotNull HttpStatus status, @NotNull String body) {
+        return this.response(SimpleHttpResponseMessage
+                                 .builder()
+                                 .contentType(MediaType.APPLICATION_JSON)
+                                 .status(status.value())
+                                 .body(body.getBytes())
+                                 .build());
+    }
+
+    /**
+     * 根据异常信息来响应错误
+     *
+     * @param status 响应码
+     * @param body   异常信息
+     * @return void
+     */
+    default Mono<Void> error(@NotNull HttpStatus status, @NotNull Throwable body) {
+        return response(status, body.getMessage() == null ? body.getClass().getSimpleName() : body.getMessage());
+    }
+
+    /**
+     * 根据HttpResponseMessage进行响应
+     *
+     * @param message HttpResponseMessage
+     * @return void
+     */
+    default Mono<Void> response(HttpResponseMessage message) {
+        HttpResponse response = response();
+        response.status(message.getStatus());
+        if (CollectionUtils.isNotEmpty(message.getHeaders())) {
+            message.getHeaders().forEach(response::header);
+        }
+        response.contentType(message.getContentType());
+        return TraceHolder
+            .writeContextTo(response, HttpResponse::header)
+            .then(response.writeAndEnd(message.getPayload()))
+            ;
+    }
+
+    /**
+     * 转换为 HttpExchangeMessage
+     *
+     * @return HttpExchangeMessage
+     * @see HttpExchangeMessage
+     */
+    default Mono<HttpExchangeMessage> toExchangeMessage() {
+        return Mono
+            .zip(
+                request()
+                    .getBody()
+                    .defaultIfEmpty(Unpooled.EMPTY_BUFFER),
+                request().multiPart(),
+                (body, part) -> new HttpServerExchangeMessage(this, body, part)
+            );
+    }
+}

+ 122 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/HttpRequest.java

@@ -0,0 +1,122 @@
+package org.jetlinks.community.network.http.server;
+
+import io.netty.buffer.ByteBuf;
+import org.jetlinks.core.message.codec.http.Header;
+import org.jetlinks.core.message.codec.http.HttpRequestMessage;
+import org.jetlinks.core.message.codec.http.MultiPart;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.util.MultiValueMap;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * HTTP请求接口,用于定义HTTP请求
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+public interface HttpRequest {
+
+    /**
+     * 请求的URL地址,如: http://127.0.0.1/test
+     *
+     * @return 请求的URL地址
+     */
+    String getUrl();
+
+    /**
+     * 请求路径,如: /test
+     *
+     * @return 请求路径
+     */
+    String getPath();
+
+    /**
+     * @return 远程客户端IP地址
+     */
+    String getRemoteIp();
+
+    /**
+     * 获取远程客户端真实地址,通过解析HTTP请求头中的: X-Real-IP,X-Forwarded-For,Proxy-Client-IP信息,
+     *
+     * @return 远程客户端真实地址
+     */
+    String getRealIp();
+
+    /**
+     * 获取远程套接字地址,包含ip和端口信息
+     *
+     * @return 远程套接字地址
+     */
+    InetSocketAddress getClientAddress();
+
+    /**
+     * @return 请求方法
+     */
+    HttpMethod getMethod();
+
+    /**
+     * @return 请求体类型
+     */
+    MediaType getContentType();
+
+    /**
+     * 根据key获取查询参数(查询参数为url中的参数,如: /query?name=test)
+     *
+     * @param key 参数key
+     * @return 参数值
+     */
+    Optional<String> getQueryParameter(String key);
+
+    /**
+     * 获取全部的查询参数(查询参数为url中的参数,如: /query?name=test)
+     *
+     * @return 参数值
+     */
+    Map<String, String> getQueryParameters();
+
+    /**
+     * 获取POST请求参数,当contentType为<code>application/x-www-form-urlencoded</code>时,
+     * 通过此方法获取参数信息
+     *
+     * @return 请求参数
+     */
+    Map<String, String> getRequestParam();
+
+    /**
+     * 获取请求体
+     *
+     * @return 请求体
+     */
+    Mono<ByteBuf> getBody();
+
+    /**
+     * 获取请求头信息
+     *
+     * @return 请求头列表
+     */
+    List<Header> getHeaders();
+
+    /**
+     * 根据key获取对应当请求头
+     *
+     * @param key key
+     * @return 请求头
+     */
+    Optional<Header> getHeader(String key);
+
+    /**
+     * 将当前对象转为{@link HttpRequestMessage}
+     *
+     * @return HttpRequestMessage
+     */
+    Mono<HttpRequestMessage> toMessage();
+
+    Mono<MultiPart> multiPart();
+}

+ 74 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/HttpResponse.java

@@ -0,0 +1,74 @@
+package org.jetlinks.community.network.http.server;
+
+import io.netty.buffer.ByteBuf;
+import org.jetlinks.core.message.codec.http.Header;
+import org.springframework.http.MediaType;
+import reactor.core.publisher.Mono;
+
+/**
+ * HTTP响应信息
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+public interface HttpResponse {
+
+    /**
+     * 设置响应状态码
+     *
+     * @param status 状态吗
+     * @return this
+     */
+    HttpResponse status(int status);
+
+    /**
+     * 设置响应类型
+     *
+     * @param mediaType 媒体类型
+     * @return this
+     */
+    HttpResponse contentType(MediaType mediaType);
+
+    /**
+     * 设置响应头
+     *
+     * @param header 响应头
+     * @return this
+     */
+    HttpResponse header(Header header);
+
+    /**
+     * 设置响应头
+     *
+     * @param header key
+     * @param value  value
+     * @return this
+     */
+    HttpResponse header(String header, String value);
+
+    /**
+     * 写出数据
+     *
+     * @param buffer ByteBuf
+     * @return void
+     */
+    Mono<Void> write(ByteBuf buffer);
+
+    /**
+     * 完成响应
+     *
+     * @return void
+     */
+    Mono<Void> end();
+
+    /**
+     * 响应数据然后结束
+     *
+     * @param buffer ByteBuf
+     * @return void
+     */
+    default Mono<Void> writeAndEnd(ByteBuf buffer) {
+        return write(buffer)
+            .then(end());
+    }
+}

+ 49 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/HttpServer.java

@@ -0,0 +1,49 @@
+package org.jetlinks.community.network.http.server;
+
+import org.jetlinks.community.network.ServerNetwork;
+import org.springframework.http.HttpMethod;
+import reactor.core.publisher.Flux;
+
+import java.util.Locale;
+
+/**
+ * HTTP 服务网络组件接口
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+public interface HttpServer extends ServerNetwork {
+
+    /**
+     * 监听所有请求
+     *
+     * @return HttpExchange
+     */
+    Flux<HttpExchange> handleRequest();
+
+    /**
+     * 根据请求方法和url监听请求.
+     * <p>
+     * URL支持通配符:
+     * <pre>
+     *   /device/* 匹配/device/下1级的请求,如: /device/1
+     *
+     *   /device/** 匹配/device/下N级的请求,如: /device/1/2/3
+     *
+     * </pre>
+     *
+     * @param method     请求方法: {@link org.springframework.http.HttpMethod}
+     * @param urlPattern url
+     * @return HttpExchange
+     */
+    Flux<HttpExchange> handleRequest(String method, String... urlPattern);
+
+    default Flux<HttpExchange> handleRequest(HttpMethod method, String... urlPattern) {
+        return handleRequest(method.name().toLowerCase(Locale.ROOT), urlPattern);
+    }
+
+    /**
+     * 停止服务
+     */
+    void shutdown();
+}

+ 140 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/DefaultHttpServerProvider.java

@@ -0,0 +1,140 @@
+package org.jetlinks.community.network.http.server.vertx;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerOptions;
+import lombok.Generated;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.jetlinks.community.network.*;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.core.metadata.DefaultConfigMetadata;
+import org.jetlinks.core.metadata.types.BooleanType;
+import org.jetlinks.core.metadata.types.IntType;
+import org.jetlinks.core.metadata.types.ObjectType;
+import org.jetlinks.core.metadata.types.StringType;
+import org.jetlinks.community.network.security.CertificateManager;
+import org.jetlinks.community.network.security.VertxKeyCertTrustOptions;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 使用Vertx实现HTTP服务
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+@Component
+@Slf4j
+public class DefaultHttpServerProvider implements NetworkProvider<HttpServerConfig> {
+
+    private final CertificateManager certificateManager;
+
+    private final Vertx vertx;
+
+    public DefaultHttpServerProvider(CertificateManager certificateManager, Vertx vertx) {
+        this.certificateManager = certificateManager;
+        this.vertx = vertx;
+    }
+
+    @Nonnull
+    @Override
+    @Generated
+    public NetworkType getType() {
+        return DefaultNetworkType.HTTP_SERVER;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Network> createNetwork(@Nonnull HttpServerConfig config) {
+        VertxHttpServer server = new VertxHttpServer(config);
+        return initServer(server, config);
+    }
+
+    @Override
+    public Mono<Network> reload(@Nonnull Network network, @Nonnull HttpServerConfig config) {
+        VertxHttpServer server = ((VertxHttpServer) network);
+        return initServer(server, config);
+    }
+
+    protected HttpServer createHttpServer(HttpServerOptions options) {
+        return vertx.createHttpServer(options);
+    }
+
+    @Nullable
+    @Override
+    @Generated
+    public ConfigMetadata getConfigMetadata() {
+        return new DefaultConfigMetadata()
+            .add("id", "id", "", new StringType())
+            .add("host", "本地地址", "", new StringType())
+            .add("port", "本地端口", "", new IntType())
+            .add("publicHost", "公网地址", "", new StringType())
+            .add("publicPort", "公网端口", "", new IntType())
+            .add("certId", "证书id", "", new StringType())
+            .add("secure", "开启TSL", "", new BooleanType())
+            .add("httpHeaders", "请求头", "", new ObjectType());
+    }
+
+    @Nonnull
+    @Override
+    public Mono<HttpServerConfig> createConfig(@Nonnull NetworkProperties properties) {
+        return Mono.defer(() -> {
+            HttpServerConfig config = FastBeanCopier.copy(properties.getConfigurations(), new HttpServerConfig());
+            config.setId(properties.getId());
+            config.validate();
+            return Mono.just(config);
+        })
+            .as(LocaleUtils::transform);
+    }
+
+    private Mono<Network> initServer(VertxHttpServer server, HttpServerConfig config) {
+        int numberOfInstance = Math.max(1, config.getInstance());
+        List<HttpServer> instances = new ArrayList<>(numberOfInstance);
+        return convert(config)
+            .map(options -> {
+                //利用多线程处理请求
+                for (int i = 0; i < numberOfInstance; i++) {
+                    instances.add(createHttpServer(options));
+                }
+                server.setBindAddress(new InetSocketAddress(config.getHost(), config.getPort()));
+                server.setHttpServers(instances);
+                for (HttpServer httpServer : instances) {
+                    httpServer.listen(result -> {
+                        if (result.succeeded()) {
+                            log.debug("startup http server on [{}]", server.getBindAddress());
+                        } else {
+                            server.setLastError(result.cause().getMessage());
+                            log.warn("startup http server on [{}] failed", server.getBindAddress(), result.cause());
+                        }
+                    });
+                }
+                return server;
+            });
+    }
+
+
+    private Mono<HttpServerOptions> convert(HttpServerConfig config) {
+        HttpServerOptions options = new HttpServerOptions();
+        options.setHandle100ContinueAutomatically(true);
+        options.setHost(config.getHost());
+        options.setPort(config.getPort());
+        if (config.isSecure()) {
+            options.setSsl(true);
+            return certificateManager
+                .getCertificate(config.getCertId())
+                .map(VertxKeyCertTrustOptions::new)
+                .doOnNext(options::setKeyCertOptions)
+                .doOnNext(options::setTrustOptions)
+                .thenReturn(options);
+        }
+        return Mono.just(options);
+    }
+}

+ 54 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/HttpServerConfig.java

@@ -0,0 +1,54 @@
+package org.jetlinks.community.network.http.server.vertx;
+
+import io.vertx.core.http.HttpServerOptions;
+import lombok.*;
+import org.jetlinks.community.network.AbstractServerNetworkConfig;
+import org.jetlinks.community.network.resource.NetworkTransport;
+import org.jetlinks.community.network.AbstractServerNetworkConfig;
+import org.jetlinks.community.network.resource.NetworkTransport;
+import org.springframework.util.StringUtils;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * HTTP服务配置
+ *
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class HttpServerConfig extends AbstractServerNetworkConfig {
+
+    /**
+     * 服务实例数量(线程数)
+     */
+    private int instance = Math.max(4, Runtime.getRuntime().availableProcessors());
+
+    /**
+     * 固定响应头信息
+     */
+    private Map<String, String> httpHeaders;
+
+    public Map<String, String> getHttpHeaders() {
+        return nullMapHandle(httpHeaders);
+    }
+
+    private Map<String, String> nullMapHandle(Map<String, String> map) {
+        return map == null ? Collections.emptyMap() : map;
+    }
+
+    @Override
+    public NetworkTransport getTransport() {
+        return NetworkTransport.TCP;
+    }
+
+    @Override
+    public String getSchema() {
+        return "http";
+    }
+}

+ 385 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/VertxHttpExchange.java

@@ -0,0 +1,385 @@
+package org.jetlinks.community.network.http.server.vertx;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.vertx.core.MultiMap;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.http.HttpServerResponse;
+import io.vertx.core.net.SocketAddress;
+import lombok.Generated;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.network.http.DefaultHttpRequestMessage;
+import org.jetlinks.community.network.http.VertxWebUtils;
+import org.jetlinks.community.network.http.server.HttpExchange;
+import org.jetlinks.community.network.http.server.HttpRequest;
+import org.jetlinks.community.network.http.server.HttpResponse;
+import org.jetlinks.core.message.codec.http.*;
+import org.jetlinks.core.utils.Reactors;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.Sinks;
+
+import java.net.InetSocketAddress;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 默认HTTP交换消息
+ *
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@Slf4j
+public class VertxHttpExchange implements HttpExchange, HttpResponse, HttpRequest {
+
+    static final AtomicReferenceFieldUpdater<VertxHttpExchange, Boolean> ALREADY_RESPONSE = AtomicReferenceFieldUpdater
+        .newUpdater(VertxHttpExchange.class, Boolean.class, "alreadyResponse");
+
+    static final MultiPart emptyPart = MultiPart.of(Collections.emptyList());
+
+    private final HttpServerRequest httpServerRequest;
+
+    private final HttpServerResponse response;
+
+    private final Mono<ByteBuf> body;
+
+    private final String requestId;
+
+    private MultiPart multiPart;
+
+    private volatile Boolean alreadyResponse = false;
+
+    public VertxHttpExchange(HttpServerRequest httpServerRequest,
+                             HttpServerConfig config) {
+
+        this.httpServerRequest = httpServerRequest;
+        this.response = httpServerRequest.response();
+        this.requestId = UUID.randomUUID().toString();
+        config.getHttpHeaders().forEach(response::putHeader);
+
+        if (httpServerRequest.method() == HttpMethod.GET) {
+            body = Mono.just(Unpooled.EMPTY_BUFFER);
+        } else {
+            log.debug("create multi part");
+            if (MultiPart.isMultiPart(getContentType())) {
+
+                Sinks.Many<ByteBuf> sink = Reactors.createMany(false);
+
+                this.httpServerRequest
+                    .handler(buf -> sink.emitNext(buf.getByteBuf(), (s, result) -> {
+                        if (result != Sinks.EmitResult.OK) {
+                            response
+                                .putHeader("X-Server-Error", result.toString())
+                                .setStatusCode(500)
+                                .end();
+                        }
+                        return false;
+                    }))
+                    .endHandler(ignore -> sink.tryEmitComplete());
+
+                body = MultiPart
+                    .parse(getSpringHttpHeaders(), sink.asFlux())
+                    .doOnNext(this::setMultiPart)
+                    .thenReturn(Unpooled.EMPTY_BUFFER)
+                    .cache();
+            } else {
+                body = Mono
+                    .<ByteBuf>create(sink -> {
+                        if (this.httpServerRequest.isEnded()) {
+                            sink.success();
+                        } else {
+                            this.httpServerRequest
+                                .bodyHandler(buffer -> {
+                                    sink.success(buffer.getByteBuf());
+                                });
+                        }
+                    })
+                    .cache();
+            }
+            body
+                .doOnError(err -> {
+                    response
+                        .putHeader("X-Server-Error", err.getMessage())
+                        .setStatusCode(500)
+                        .end();
+                })
+                .subscribe();
+        }
+    }
+
+    @Override
+    @Generated
+    public String requestId() {
+        return requestId;
+    }
+
+    @Override
+    @Generated
+    public long timestamp() {
+        return System.currentTimeMillis();
+    }
+
+    @Override
+    @Generated
+    public HttpRequest request() {
+        return this;
+    }
+
+
+    @Override
+    @Generated
+    public HttpResponse response() {
+        return this;
+    }
+
+    @Override
+    @Generated
+    public boolean isClosed() {
+        return response.closed() || response.ended();
+    }
+
+    @Override
+    @Generated
+    public HttpResponse status(int status) {
+        response.setStatusCode(status);
+        return this;
+    }
+
+    static Map<String, String> convertRequestParam(MultiMap multiMap) {
+        return multiMap
+            .entries()
+            .stream()
+            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> String.join(",", a, b)));
+    }
+
+    static List<Header> convertHeader(MultiMap multiMap) {
+        return multiMap
+            .names()
+            .stream()
+            .map(name -> {
+                Header header = new Header();
+                header.setName(name);
+                header.setValue(multiMap.getAll(name).toArray(new String[0]));
+                return header;
+            })
+            .collect(Collectors.toList())
+            ;
+    }
+
+    private org.springframework.http.HttpMethod convertMethodType(HttpMethod method) {
+        for (org.springframework.http.HttpMethod httpMethod : org.springframework.http.HttpMethod.values()) {
+            if (httpMethod.toString().equals(method.toString())) {
+                return httpMethod;
+            }
+        }
+        throw new UnsupportedOperationException("不支持的HttpMethod类型: " + method);
+    }
+
+    private void setResponseDefaultLength(int length) {
+        if (!isClosed()) {
+            response.putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(length));
+        }
+    }
+
+    @Override
+    public HttpResponse contentType(MediaType mediaType) {
+        if (null != mediaType && !isClosed()) {
+            response.putHeader(HttpHeaders.CONTENT_TYPE, mediaType.toString());
+        }
+        return this;
+    }
+
+    @Override
+    public HttpResponse header(Header header) {
+        if (null != header && !isClosed()) {
+            response.putHeader(header.getName(), Arrays.<String>asList(header.getValue()));
+        }
+        return this;
+    }
+
+    @Override
+    public HttpResponse header(String header, String value) {
+        if (header != null && value != null && !isClosed()) {
+            response.putHeader(header, value);
+        }
+        return this;
+    }
+
+    @Override
+    public Mono<Void> write(ByteBuf buffer) {
+        if (isClosed()) {
+            return Mono.empty();
+        }
+        return Mono
+            .<Void>create(sink -> {
+                Buffer buf = Buffer.buffer(buffer);
+                setResponseDefaultLength(buf.length());
+                response.write(buf, v -> sink.success());
+            });
+    }
+
+    @Override
+    public Mono<Void> end() {
+        if (isClosed()) {
+            return Mono.empty();
+        }
+        ALREADY_RESPONSE.set(this, true);
+        return Mono
+            .<Void>create(sink -> {
+                if (response.ended()) {
+                    sink.success();
+                    return;
+                }
+                response.end(v -> sink.success());
+            });
+    }
+
+    @Override
+    public Mono<Void> response(HttpResponseMessage message) {
+        if (ALREADY_RESPONSE.compareAndSet(this, false, true)) {
+            return HttpExchange.super.response(message);
+        } else {
+            if (log.isInfoEnabled()) {
+                log.info("http already response,discard message: {}", message.print());
+            }
+            return Mono.empty();
+        }
+    }
+
+    @Override
+    @Generated
+    public String getUrl() {
+        return httpServerRequest.path();
+    }
+
+    @Override
+    @Generated
+    public String getPath() {
+        return httpServerRequest.path();
+    }
+
+    @Override
+    @Generated
+    public String getRemoteIp() {
+        return httpServerRequest.remoteAddress().host();
+    }
+
+    @Override
+    @Generated
+    public String getRealIp() {
+        return VertxWebUtils.getIpAddr(httpServerRequest);
+    }
+
+    @Override
+    public InetSocketAddress getClientAddress() {
+        SocketAddress address = httpServerRequest.remoteAddress();
+        if (null == address) {
+            return null;
+        }
+        return new InetSocketAddress(getRealIp(), address.port());
+    }
+
+    @Override
+    public MediaType getContentType() {
+        String contentType = httpServerRequest.getHeader(HttpHeaders.CONTENT_TYPE);
+        if (StringUtils.hasText(contentType)) {
+            return MediaType.parseMediaType(contentType);
+        } else {
+            return MediaType.APPLICATION_FORM_URLENCODED;
+        }
+    }
+
+    @Override
+    public Optional<String> getQueryParameter(String key) {
+        return Optional.ofNullable(httpServerRequest.getParam(key));
+    }
+
+    @Override
+    public Map<String, String> getQueryParameters() {
+        Map<String, String> params = new HashMap<>();
+
+        MultiMap map = httpServerRequest.params();
+
+        for (String name : map.names()) {
+            params.put(name, String.join(",", map.getAll(name)));
+        }
+
+        return params;
+    }
+
+    @Override
+    @Generated
+    public Map<String, String> getRequestParam() {
+        return convertRequestParam(httpServerRequest.formAttributes());
+    }
+
+    @Override
+    @Generated
+    public Mono<ByteBuf> getBody() {
+        return body;
+    }
+
+    @Override
+    @Generated
+    public org.springframework.http.HttpMethod getMethod() {
+        return convertMethodType(httpServerRequest.method());
+    }
+
+    @Override
+    @Generated
+    public List<Header> getHeaders() {
+        return convertHeader(httpServerRequest.headers());
+    }
+
+    private HttpHeaders getSpringHttpHeaders() {
+        MultiMap map = httpServerRequest.headers();
+        HttpHeaders headers = new HttpHeaders();
+        for (String name : map.names()) {
+            headers.addAll(name, map.getAll(name));
+        }
+        return headers;
+    }
+
+    @Override
+    public Optional<Header> getHeader(String key) {
+        return Optional.ofNullable(
+            getHeaders()
+                .stream()
+                .collect(Collectors.toMap(Header::getName, Function.identity()))
+                .get(key));
+    }
+
+    @Override
+    public Mono<HttpRequestMessage> toMessage() {
+        return this.getBody()
+                   .defaultIfEmpty(Unpooled.EMPTY_BUFFER)
+                   .map(byteBuf -> {
+                       DefaultHttpRequestMessage message = new DefaultHttpRequestMessage();
+                       message.setContentType(this.getContentType());
+                       message.setHeaders(this.getHeaders());
+                       message.setMethod(this.getMethod());
+                       message.setPayload(byteBuf);
+                       message.setQueryParameters(this.getQueryParameters());
+                       message.setUrl(this.getUrl());
+                       message.setMultiPart(multiPart);
+                       return message;
+                   });
+    }
+
+    @Override
+    public Mono<MultiPart> multiPart() {
+        return body
+            .then(Mono.fromSupplier(this::getMultiPart))
+            .defaultIfEmpty(emptyPart);
+    }
+}

+ 167 - 0
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/VertxHttpServer.java

@@ -0,0 +1,167 @@
+package org.jetlinks.community.network.http.server.vertx;
+
+import lombok.*;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.core.topic.Topic;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.http.server.HttpExchange;
+import org.jetlinks.community.network.http.server.HttpServer;
+import org.springframework.http.HttpStatus;
+import reactor.core.Disposable;
+import reactor.core.Disposables;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+
+import java.net.InetSocketAddress;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Slf4j
+public class VertxHttpServer implements HttpServer {
+
+    private Collection<io.vertx.core.http.HttpServer> httpServers;
+
+    private HttpServerConfig config;
+
+    private String id;
+
+    private final Topic<FluxSink<HttpExchange>> route = Topic.createRoot();
+
+    @Getter
+    @Setter
+    private String lastError;
+
+    @Setter(AccessLevel.PACKAGE)
+    private InetSocketAddress bindAddress;
+
+    public VertxHttpServer(HttpServerConfig config) {
+        this.config = config;
+        this.id = config.getId();
+    }
+
+    @Override
+    public InetSocketAddress getBindAddress() {
+        return bindAddress;
+    }
+
+    public void setHttpServers(Collection<io.vertx.core.http.HttpServer> httpServers) {
+        if (isAlive()) {
+            shutdown();
+        }
+        this.httpServers = httpServers;
+        for (io.vertx.core.http.HttpServer server : this.httpServers) {
+            server.requestHandler(request -> {
+                request.exceptionHandler(err -> {
+                    log.error(err.getMessage(), err);
+                });
+                VertxHttpExchange exchange = new VertxHttpExchange(request, config);
+
+                String url = exchange.getUrl();
+                if (url.endsWith("/")) {
+                    url = url.substring(0, url.length() - 1);
+                }
+
+                route.findTopic("/" + exchange.request().getMethod().name().toLowerCase() + url)
+                     .flatMapIterable(Topic::getSubscribers)
+                     .doOnNext(sink -> sink.next(exchange))
+                     .switchIfEmpty(Mono.fromRunnable(() -> {
+
+                         log.warn("http server no handler for:[{} {}://{}{}]", request.method(), request.scheme(), request.host(), request.path());
+                         request.response()
+                                .setStatusCode(HttpStatus.NOT_FOUND.value())
+                                .end();
+
+                     }))
+                     .subscribe();
+
+            });
+            server.exceptionHandler(err -> log.error(err.getMessage(), err));
+        }
+    }
+
+    @Override
+    public Flux<HttpExchange> handleRequest() {
+        return handleRequest("*", "/**");
+    }
+
+
+    @Override
+    public Flux<HttpExchange> handleRequest(String method, String... urlPatterns) {
+        return Flux.create(sink -> {
+            Disposable.Composite disposable = Disposables.composite();
+            for (String urlPattern : urlPatterns) {
+                String pattern = Stream
+                    .of(urlPattern.split("/"))
+                    .map(str -> {
+                        //处理路径变量,如: /devices/{id}
+                        if (str.startsWith("{") && str.endsWith("}")) {
+                            return "*";
+                        }
+                        return str;
+                    })
+                    .collect(Collectors.joining("/"));
+                if (pattern.endsWith("/")) {
+                    pattern = pattern.substring(0, pattern.length() - 1);
+                }
+                if (!pattern.startsWith("/")) {
+                    pattern = "/".concat(pattern);
+                }
+                pattern = "/" + method + pattern;
+                log.debug("handle http request : {}", pattern);
+                Topic<FluxSink<HttpExchange>> sub = route.append(pattern);
+                sub.subscribe(sink);
+                disposable.add(() -> sub.unsubscribe(sink));
+            }
+            sink.onDispose(disposable);
+        });
+    }
+
+    @Override
+    @Generated
+    public String getId() {
+        return id;
+    }
+
+    @Override
+    @Generated
+    public NetworkType getType() {
+        return DefaultNetworkType.HTTP_SERVER;
+    }
+
+    @Override
+    public void shutdown() {
+        if (httpServers != null) {
+            for (io.vertx.core.http.HttpServer httpServer : httpServers) {
+                httpServer.close(res -> {
+                    if (res.failed()) {
+                        log.error(res.cause().getMessage(), res.cause());
+                    } else {
+                        log.debug("http server [{}] closed", httpServer.actualPort());
+                    }
+                });
+            }
+            httpServers.clear();
+            httpServers = null;
+        }
+    }
+
+    @Override
+    public boolean isAlive() {
+        return httpServers != null && !httpServers.isEmpty();
+    }
+
+    @Override
+    public boolean isAutoReload() {
+        return false;
+    }
+}

+ 28 - 0
jetlinks-components/network-component/http-component/src/test/resources/client.csr

@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIICKTCCAcygAwIBAgIEFFnz/zAMBggqhkjOPQQDAgUAMGUxCzAJBgNVBAMTAmNh
+MR4wHAYDVQQLExVKZXRMaW5rcyBJb3QgUGxhdGZvcm0xFTATBgNVBAoTDEpldExp
+bmtzIFBybzESMBAGA1UEBxMJQ2hvbmdRaW5nMQswCQYDVQQGEwJDTjAeFw0yMTA5
+MTQwMTAwMjlaFw0zMTA5MTIwMTAwMjlaMGkxDzANBgNVBAMTBmNsaWVudDEeMBwG
+A1UECxMVSmV0TGlua3MgSW90IFBsYXRmb3JtMRUwEwYDVQQKEwxKZXRMaW5rcyBQ
+cm8xEjAQBgNVBAcTCUNob25nUWluZzELMAkGA1UEBhMCQ04wWTATBgcqhkjOPQIB
+BggqhkjOPQMBBwNCAAT75ntA9C3tQDFrw5jvGQb/Gp93O7UXgc+tDAvAF/U2eZNU
+h6s5Ubax4JKdRYbPoa9+Hf4laP3t+6LL9hGsdRjUo2QwYjAfBgNVHSMEGDAWgBT2
+e6y0VWMcroc5FlnU7tbz4AjaIzATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8E
+BAMCA6gwHQYDVR0OBBYEFKQyCSuvjBA5qE1SaDYppqo0lVjHMAwGCCqGSM49BAMC
+BQADSQAwRgIhAP4Hsx9Ar3UuL3GBt8NSMknFrjBBiG38edozAYs4BEM5AiEAmtHy
+802+uSnuK621rGSgSb4XeZAkzFC+8n79vrveMpk=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICGjCCAb2gAwIBAgIECbCsJDAMBggqhkjOPQQDAgUAMF4xDTALBgNVBAMTBHJv
+b3QxFTATBgNVBAsTDElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJv
+MRIwEAYDVQQHEwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMB4XDTIxMDkxNDAxMDAy
+MloXDTMxMDkxMjAxMDAyMlowZTELMAkGA1UEAxMCY2ExHjAcBgNVBAsTFUpldExp
+bmtzIElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJvMRIwEAYDVQQH
+EwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAESEKoHg/XrwP6AS/QdLRbdtTjXOZ5b5SFh7Kd0lWP83MoxpKEOgJzGm0DcMP5
+y+j95Tz+6nR8rk+955NVVuZOpaNgMF4wHwYDVR0jBBgwFoAUPU05HaGbS/CXyapJ
+Z80X2Z8Qt8IwDwYDVR0TBAgwBgEB/wIBADALBgNVHQ8EBAMCAQYwHQYDVR0OBBYE
+FPZ7rLRVYxyuhzkWWdTu1vPgCNojMAwGCCqGSM49BAMCBQADSQAwRgIhAPlu7nZH
+vZBLoLWzNJgK4NDrdemOuIw5PhT83jSeFXGfAiEA9vRtf+TOYgcOAaD4ekMgzHWs
+BNR8Y12G04lyb+Qs2iY=
+-----END CERTIFICATE-----

BIN
jetlinks-components/network-component/http-component/src/test/resources/client.p12


+ 46 - 0
jetlinks-components/network-component/http-component/src/test/resources/client.pem

@@ -0,0 +1,46 @@
+Bag Attributes
+    friendlyName: client
+    localKeyID: 54 69 6D 65 20 31 36 33 31 35 38 31 32 33 32 30 33 34 
+Key Attributes: <No Attributes>
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgraqqYXGjDZta52O9
+Un8XXxkG7v30k4i6nmSwLoiqIXKhRANCAAT75ntA9C3tQDFrw5jvGQb/Gp93O7UX
+gc+tDAvAF/U2eZNUh6s5Ubax4JKdRYbPoa9+Hf4laP3t+6LL9hGsdRjU
+-----END PRIVATE KEY-----
+Bag Attributes
+    friendlyName: client
+    localKeyID: 54 69 6D 65 20 31 36 33 31 35 38 31 32 33 32 30 33 34 
+subject=/CN=client/OU=JetLinks Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+issuer=/CN=ca/OU=JetLinks Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+-----BEGIN CERTIFICATE-----
+MIICKTCCAcygAwIBAgIEFFnz/zAMBggqhkjOPQQDAgUAMGUxCzAJBgNVBAMTAmNh
+MR4wHAYDVQQLExVKZXRMaW5rcyBJb3QgUGxhdGZvcm0xFTATBgNVBAoTDEpldExp
+bmtzIFBybzESMBAGA1UEBxMJQ2hvbmdRaW5nMQswCQYDVQQGEwJDTjAeFw0yMTA5
+MTQwMTAwMjlaFw0zMTA5MTIwMTAwMjlaMGkxDzANBgNVBAMTBmNsaWVudDEeMBwG
+A1UECxMVSmV0TGlua3MgSW90IFBsYXRmb3JtMRUwEwYDVQQKEwxKZXRMaW5rcyBQ
+cm8xEjAQBgNVBAcTCUNob25nUWluZzELMAkGA1UEBhMCQ04wWTATBgcqhkjOPQIB
+BggqhkjOPQMBBwNCAAT75ntA9C3tQDFrw5jvGQb/Gp93O7UXgc+tDAvAF/U2eZNU
+h6s5Ubax4JKdRYbPoa9+Hf4laP3t+6LL9hGsdRjUo2QwYjAfBgNVHSMEGDAWgBT2
+e6y0VWMcroc5FlnU7tbz4AjaIzATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8E
+BAMCA6gwHQYDVR0OBBYEFKQyCSuvjBA5qE1SaDYppqo0lVjHMAwGCCqGSM49BAMC
+BQADSQAwRgIhAP4Hsx9Ar3UuL3GBt8NSMknFrjBBiG38edozAYs4BEM5AiEAmtHy
+802+uSnuK621rGSgSb4XeZAkzFC+8n79vrveMpk=
+-----END CERTIFICATE-----
+Bag Attributes
+    friendlyName: C=CN,L=ChongQing,O=JetLinks Pro,OU=JetLinks Iot Platform,CN=ca
+subject=/CN=ca/OU=JetLinks Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+issuer=/CN=root/OU=Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+-----BEGIN CERTIFICATE-----
+MIICGjCCAb2gAwIBAgIECbCsJDAMBggqhkjOPQQDAgUAMF4xDTALBgNVBAMTBHJv
+b3QxFTATBgNVBAsTDElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJv
+MRIwEAYDVQQHEwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMB4XDTIxMDkxNDAxMDAy
+MloXDTMxMDkxMjAxMDAyMlowZTELMAkGA1UEAxMCY2ExHjAcBgNVBAsTFUpldExp
+bmtzIElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJvMRIwEAYDVQQH
+EwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAESEKoHg/XrwP6AS/QdLRbdtTjXOZ5b5SFh7Kd0lWP83MoxpKEOgJzGm0DcMP5
+y+j95Tz+6nR8rk+955NVVuZOpaNgMF4wHwYDVR0jBBgwFoAUPU05HaGbS/CXyapJ
+Z80X2Z8Qt8IwDwYDVR0TBAgwBgEB/wIBADALBgNVHQ8EBAMCAQYwHQYDVR0OBBYE
+FPZ7rLRVYxyuhzkWWdTu1vPgCNojMAwGCCqGSM49BAMCBQADSQAwRgIhAPlu7nZH
+vZBLoLWzNJgK4NDrdemOuIw5PhT83jSeFXGfAiEA9vRtf+TOYgcOAaD4ekMgzHWs
+BNR8Y12G04lyb+Qs2iY=
+-----END CERTIFICATE-----

+ 1 - 1
jetlinks-components/network-component/mqtt-component/src/test/resources/create.sh

@@ -15,7 +15,7 @@ TRUST_STORE_PEM=trustStore.pem
 CLIENT_KEY_STORE_PEM=client.pem
 SERVER_KEY_STORE_PEM=server.pem
 
-VALIDITY=365
+VALIDITY=3650
 
 create_keys() {
    echo "creating root key and certificate..."

+ 5 - 0
jetlinks-components/network-component/http-component/src/test/resources/ec_private.pem

@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIM1UW2HToEI8HEIDJNEncq8ilmsbupnmtAReC57UTxZroAoGCCqGSM49
+AwEHoUQDQgAEORd8Br8ewxhutgd1xm4epZzCDUq6L3bMijuQbtrAdrDQ9kF7g2IA
+wiVvwn2bXdyh5Liotiqj6t/CJh+av90i6A==
+-----END EC PRIVATE KEY-----

+ 4 - 0
jetlinks-components/network-component/http-component/src/test/resources/ec_public.pem

@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEORd8Br8ewxhutgd1xm4epZzCDUq6
+L3bMijuQbtrAdrDQ9kF7g2IAwiVvwn2bXdyh5Liotiqj6t/CJh+av90i6A==
+-----END PUBLIC KEY-----

BIN
jetlinks-components/network-component/http-component/src/test/resources/keyStore.jks


+ 1 - 0
jetlinks-components/network-component/http-component/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

@@ -0,0 +1 @@
+mock-maker-inline

+ 28 - 0
jetlinks-components/network-component/http-component/src/test/resources/server.csr

@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIICJzCCAcygAwIBAgIEfEP7YDAMBggqhkjOPQQDAgUAMGUxCzAJBgNVBAMTAmNh
+MR4wHAYDVQQLExVKZXRMaW5rcyBJb3QgUGxhdGZvcm0xFTATBgNVBAoTDEpldExp
+bmtzIFBybzESMBAGA1UEBxMJQ2hvbmdRaW5nMQswCQYDVQQGEwJDTjAeFw0yMTA5
+MTQwMTAwMjJaFw0zMTA5MTIwMTAwMjJaMGkxDzANBgNVBAMTBnNlcnZlcjEeMBwG
+A1UECxMVSmV0TGlua3MgSW90IFBsYXRmb3JtMRUwEwYDVQQKEwxKZXRMaW5rcyBQ
+cm8xEjAQBgNVBAcTCUNob25nUWluZzELMAkGA1UEBhMCQ04wWTATBgcqhkjOPQIB
+BggqhkjOPQMBBwNCAAR67PpBTH1IAKEthtA6G2d8LpS4y9OYCObSEgKYdJ1K+d47
+F3M+cKJ2EVaoR5iquHtDsZcRkI75HUzWsBP5W2Blo2QwYjAfBgNVHSMEGDAWgBT2
+e6y0VWMcroc5FlnU7tbz4AjaIzATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8E
+BAMCA6gwHQYDVR0OBBYEFPmr0C3VVNwUL1EPi8bVztEDzKG3MAwGCCqGSM49BAMC
+BQADRwAwRAIgRihtlq1KABZdKQf5J9gXnkbSUS7jIvGo40DxfUW/W0QCIBOMMIRY
+cEhQTO+2OMf7LiJ/3hp9S5bdz9rSd2YkZ7Ax
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICGjCCAb2gAwIBAgIECbCsJDAMBggqhkjOPQQDAgUAMF4xDTALBgNVBAMTBHJv
+b3QxFTATBgNVBAsTDElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJv
+MRIwEAYDVQQHEwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMB4XDTIxMDkxNDAxMDAy
+MloXDTMxMDkxMjAxMDAyMlowZTELMAkGA1UEAxMCY2ExHjAcBgNVBAsTFUpldExp
+bmtzIElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJvMRIwEAYDVQQH
+EwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAESEKoHg/XrwP6AS/QdLRbdtTjXOZ5b5SFh7Kd0lWP83MoxpKEOgJzGm0DcMP5
+y+j95Tz+6nR8rk+955NVVuZOpaNgMF4wHwYDVR0jBBgwFoAUPU05HaGbS/CXyapJ
+Z80X2Z8Qt8IwDwYDVR0TBAgwBgEB/wIBADALBgNVHQ8EBAMCAQYwHQYDVR0OBBYE
+FPZ7rLRVYxyuhzkWWdTu1vPgCNojMAwGCCqGSM49BAMCBQADSQAwRgIhAPlu7nZH
+vZBLoLWzNJgK4NDrdemOuIw5PhT83jSeFXGfAiEA9vRtf+TOYgcOAaD4ekMgzHWs
+BNR8Y12G04lyb+Qs2iY=
+-----END CERTIFICATE-----

BIN
jetlinks-components/network-component/http-component/src/test/resources/server.p12


+ 46 - 0
jetlinks-components/network-component/http-component/src/test/resources/server.pem

@@ -0,0 +1,46 @@
+Bag Attributes
+    friendlyName: server
+    localKeyID: 54 69 6D 65 20 31 36 33 31 35 38 31 32 33 32 34 35 30 
+Key Attributes: <No Attributes>
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNO6FLW/zoyDFUe1G
+IoLwwKpslV7u0iwMbwEP3P0786+hRANCAAR67PpBTH1IAKEthtA6G2d8LpS4y9OY
+CObSEgKYdJ1K+d47F3M+cKJ2EVaoR5iquHtDsZcRkI75HUzWsBP5W2Bl
+-----END PRIVATE KEY-----
+Bag Attributes
+    friendlyName: server
+    localKeyID: 54 69 6D 65 20 31 36 33 31 35 38 31 32 33 32 34 35 30 
+subject=/CN=server/OU=JetLinks Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+issuer=/CN=ca/OU=JetLinks Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+-----BEGIN CERTIFICATE-----
+MIICJzCCAcygAwIBAgIEfEP7YDAMBggqhkjOPQQDAgUAMGUxCzAJBgNVBAMTAmNh
+MR4wHAYDVQQLExVKZXRMaW5rcyBJb3QgUGxhdGZvcm0xFTATBgNVBAoTDEpldExp
+bmtzIFBybzESMBAGA1UEBxMJQ2hvbmdRaW5nMQswCQYDVQQGEwJDTjAeFw0yMTA5
+MTQwMTAwMjJaFw0zMTA5MTIwMTAwMjJaMGkxDzANBgNVBAMTBnNlcnZlcjEeMBwG
+A1UECxMVSmV0TGlua3MgSW90IFBsYXRmb3JtMRUwEwYDVQQKEwxKZXRMaW5rcyBQ
+cm8xEjAQBgNVBAcTCUNob25nUWluZzELMAkGA1UEBhMCQ04wWTATBgcqhkjOPQIB
+BggqhkjOPQMBBwNCAAR67PpBTH1IAKEthtA6G2d8LpS4y9OYCObSEgKYdJ1K+d47
+F3M+cKJ2EVaoR5iquHtDsZcRkI75HUzWsBP5W2Blo2QwYjAfBgNVHSMEGDAWgBT2
+e6y0VWMcroc5FlnU7tbz4AjaIzATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8E
+BAMCA6gwHQYDVR0OBBYEFPmr0C3VVNwUL1EPi8bVztEDzKG3MAwGCCqGSM49BAMC
+BQADRwAwRAIgRihtlq1KABZdKQf5J9gXnkbSUS7jIvGo40DxfUW/W0QCIBOMMIRY
+cEhQTO+2OMf7LiJ/3hp9S5bdz9rSd2YkZ7Ax
+-----END CERTIFICATE-----
+Bag Attributes
+    friendlyName: C=CN,L=ChongQing,O=JetLinks Pro,OU=JetLinks Iot Platform,CN=ca
+subject=/CN=ca/OU=JetLinks Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+issuer=/CN=root/OU=Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+-----BEGIN CERTIFICATE-----
+MIICGjCCAb2gAwIBAgIECbCsJDAMBggqhkjOPQQDAgUAMF4xDTALBgNVBAMTBHJv
+b3QxFTATBgNVBAsTDElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJv
+MRIwEAYDVQQHEwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMB4XDTIxMDkxNDAxMDAy
+MloXDTMxMDkxMjAxMDAyMlowZTELMAkGA1UEAxMCY2ExHjAcBgNVBAsTFUpldExp
+bmtzIElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJvMRIwEAYDVQQH
+EwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAESEKoHg/XrwP6AS/QdLRbdtTjXOZ5b5SFh7Kd0lWP83MoxpKEOgJzGm0DcMP5
+y+j95Tz+6nR8rk+955NVVuZOpaNgMF4wHwYDVR0jBBgwFoAUPU05HaGbS/CXyapJ
+Z80X2Z8Qt8IwDwYDVR0TBAgwBgEB/wIBADALBgNVHQ8EBAMCAQYwHQYDVR0OBBYE
+FPZ7rLRVYxyuhzkWWdTu1vPgCNojMAwGCCqGSM49BAMCBQADSQAwRgIhAPlu7nZH
+vZBLoLWzNJgK4NDrdemOuIw5PhT83jSeFXGfAiEA9vRtf+TOYgcOAaD4ekMgzHWs
+BNR8Y12G04lyb+Qs2iY=
+-----END CERTIFICATE-----

BIN
jetlinks-components/network-component/http-component/src/test/resources/trustStore.jks


BIN
jetlinks-components/network-component/http-component/src/test/resources/trustStore.p12


+ 54 - 0
jetlinks-components/network-component/http-component/src/test/resources/trustStore.pem

@@ -0,0 +1,54 @@
+Bag Attributes
+    friendlyName: ca
+    localKeyID: 54 69 6D 65 20 31 36 33 31 35 38 31 32 33 32 38 36 34 
+subject=/CN=ca/OU=JetLinks Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+issuer=/CN=root/OU=Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+-----BEGIN CERTIFICATE-----
+MIICGjCCAb2gAwIBAgIECbCsJDAMBggqhkjOPQQDAgUAMF4xDTALBgNVBAMTBHJv
+b3QxFTATBgNVBAsTDElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJv
+MRIwEAYDVQQHEwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMB4XDTIxMDkxNDAxMDAy
+MloXDTMxMDkxMjAxMDAyMlowZTELMAkGA1UEAxMCY2ExHjAcBgNVBAsTFUpldExp
+bmtzIElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJvMRIwEAYDVQQH
+EwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAESEKoHg/XrwP6AS/QdLRbdtTjXOZ5b5SFh7Kd0lWP83MoxpKEOgJzGm0DcMP5
+y+j95Tz+6nR8rk+955NVVuZOpaNgMF4wHwYDVR0jBBgwFoAUPU05HaGbS/CXyapJ
+Z80X2Z8Qt8IwDwYDVR0TBAgwBgEB/wIBADALBgNVHQ8EBAMCAQYwHQYDVR0OBBYE
+FPZ7rLRVYxyuhzkWWdTu1vPgCNojMAwGCCqGSM49BAMCBQADSQAwRgIhAPlu7nZH
+vZBLoLWzNJgK4NDrdemOuIw5PhT83jSeFXGfAiEA9vRtf+TOYgcOAaD4ekMgzHWs
+BNR8Y12G04lyb+Qs2iY=
+-----END CERTIFICATE-----
+Bag Attributes
+    friendlyName: C=CN,L=ChongQing,O=JetLinks Pro,OU=Iot Platform,CN=root
+subject=/CN=root/OU=Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+issuer=/CN=root/OU=Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+-----BEGIN CERTIFICATE-----
+MIIB4jCCAYWgAwIBAgIEfN9zGTAMBggqhkjOPQQDAgUAMF4xDTALBgNVBAMTBHJv
+b3QxFTATBgNVBAsTDElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJv
+MRIwEAYDVQQHEwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMB4XDTIxMDkxNDAxMDAy
+MVoXDTMxMDkxMjAxMDAyMVowXjENMAsGA1UEAxMEcm9vdDEVMBMGA1UECxMMSW90
+IFBsYXRmb3JtMRUwEwYDVQQKEwxKZXRMaW5rcyBQcm8xEjAQBgNVBAcTCUNob25n
+UWluZzELMAkGA1UEBhMCQ04wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQXXaFh
+SOMdzg+hqr3aHlhOypIwRbgc1zRfJ9pahb4VhIloTRc5Mt4jGV54mcFlTvvQjzef
+OQh2LQN9EW2uQobboy8wLTAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBQ9TTkdoZtL
+8JfJqklnzRfZnxC3wjAMBggqhkjOPQQDAgUAA0kAMEYCIQC7gZBCAwbGaQLt4lJj
+aOPvdrUgV5OEv39h+KE7wUP7GQIhAMe47HO77gAEDqxscJkC1laS++42Co/5CMgB
+ftPHE2Ne
+-----END CERTIFICATE-----
+Bag Attributes
+    friendlyName: root
+    localKeyID: 54 69 6D 65 20 31 36 33 31 35 38 31 32 33 32 39 38 31 
+subject=/CN=root/OU=Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+issuer=/CN=root/OU=Iot Platform/O=JetLinks Pro/L=ChongQing/C=CN
+-----BEGIN CERTIFICATE-----
+MIIB4jCCAYWgAwIBAgIEfN9zGTAMBggqhkjOPQQDAgUAMF4xDTALBgNVBAMTBHJv
+b3QxFTATBgNVBAsTDElvdCBQbGF0Zm9ybTEVMBMGA1UEChMMSmV0TGlua3MgUHJv
+MRIwEAYDVQQHEwlDaG9uZ1FpbmcxCzAJBgNVBAYTAkNOMB4XDTIxMDkxNDAxMDAy
+MVoXDTMxMDkxMjAxMDAyMVowXjENMAsGA1UEAxMEcm9vdDEVMBMGA1UECxMMSW90
+IFBsYXRmb3JtMRUwEwYDVQQKEwxKZXRMaW5rcyBQcm8xEjAQBgNVBAcTCUNob25n
+UWluZzELMAkGA1UEBhMCQ04wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQXXaFh
+SOMdzg+hqr3aHlhOypIwRbgc1zRfJ9pahb4VhIloTRc5Mt4jGV54mcFlTvvQjzef
+OQh2LQN9EW2uQobboy8wLTAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBQ9TTkdoZtL
+8JfJqklnzRfZnxC3wjAMBggqhkjOPQQDAgUAA0kAMEYCIQC7gZBCAwbGaQLt4lJj
+aOPvdrUgV5OEv39h+KE7wUP7GQIhAMe47HO77gAEDqxscJkC1laS++42Co/5CMgB
+ftPHE2Ne
+-----END CERTIFICATE-----

+ 29 - 4
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClient.java

@@ -1,20 +1,45 @@
 package org.jetlinks.community.network.mqtt.client;
 
-import org.jetlinks.community.network.Network;
 import org.jetlinks.core.message.codec.MqttMessage;
+import org.jetlinks.community.network.Network;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import java.util.List;
 
+/**
+ * MQTT Client
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
 public interface MqttClient extends Network {
 
-    default Flux<MqttMessage> subscribe(List<String> topics){
-        return subscribe(topics,0);
+    /**
+     * 从MQTT Broker订阅Topic
+     *
+     * @param topics topic列表
+     * @return MQTT消息流
+     */
+    default Flux<MqttMessage> subscribe(List<String> topics) {
+        return subscribe(topics, 0);
     }
 
-    Flux<MqttMessage> subscribe(List<String> topics,int qos);
+    /**
+     * 自定义QoS,从MQTT Broker订阅Topic
+     *
+     * @param topics topic列表
+     * @param qos    QoS
+     * @return MQTT消息流
+     */
+    Flux<MqttMessage> subscribe(List<String> topics, int qos);
 
+    /**
+     * 推送MQTT消息到MQTT Broker
+     *
+     * @param message 消息
+     * @return void
+     */
     Mono<Void> publish(MqttMessage message);
 
 }

+ 45 - 7
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClientProperties.java

@@ -1,22 +1,60 @@
 package org.jetlinks.community.network.mqtt.client;
 
-import io.vertx.mqtt.MqttClientOptions;
 import lombok.Getter;
 import lombok.Setter;
+import org.jetlinks.community.network.AbstractClientNetworkConfig;
+import org.jetlinks.community.network.resource.NetworkTransport;
+import org.jetlinks.community.network.AbstractClientNetworkConfig;
+import org.jetlinks.community.network.resource.NetworkTransport;
 
+/**
+ * MQTT Client 配置信息
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
 @Getter
 @Setter
-public class MqttClientProperties {
-    private String id;
+public class MqttClientProperties extends AbstractClientNetworkConfig {
+
+    /**
+     * 客户端ID
+     */
     private String clientId;
-    private String host;
-    private int port;
 
+    /**
+     * 用户名
+     */
     private String username;
+
+    /**
+     * 密码
+     */
     private String password;
 
+    /**
+     * 证书ID
+     */
     private String certId;
-    private MqttClientOptions options;
-    private boolean ssl;
 
+    //最大消息长度
+    private int maxMessageSize = 1024*1024;
+
+    //共享订阅前缀
+    private String topicPrefix;
+
+    /**
+     * TSL
+     */
+    private boolean secure;
+
+    @Override
+    public NetworkTransport getTransport() {
+        return NetworkTransport.TCP;
+    }
+
+    @Override
+    public String getSchema() {
+        return isSecure()?"mqtts":"mqtt";
+    }
 }

+ 80 - 61
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClientProvider.java

@@ -1,31 +1,32 @@
 package org.jetlinks.community.network.mqtt.client;
 
-import com.alibaba.fastjson.JSONObject;
 import io.vertx.core.Vertx;
 import io.vertx.mqtt.MqttClient;
 import io.vertx.mqtt.MqttClientOptions;
 import lombok.extern.slf4j.Slf4j;
 import org.hswebframework.web.bean.FastBeanCopier;
-import org.hswebframework.web.utils.ExpressionUtils;
+import org.hswebframework.web.i18n.LocaleUtils;
 import org.jetlinks.community.network.*;
-import org.jetlinks.community.network.security.CertificateManager;
-import org.jetlinks.community.network.security.VertxKeyCertTrustOptions;
 import org.jetlinks.core.metadata.ConfigMetadata;
 import org.jetlinks.core.metadata.DefaultConfigMetadata;
 import org.jetlinks.core.metadata.types.BooleanType;
 import org.jetlinks.core.metadata.types.IntType;
 import org.jetlinks.core.metadata.types.StringType;
+import org.jetlinks.community.network.security.CertificateManager;
+import org.jetlinks.community.network.security.VertxKeyCertTrustOptions;
 import org.springframework.core.env.Environment;
 import org.springframework.stereotype.Component;
 import reactor.core.publisher.Mono;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import java.util.Collections;
-import java.util.Map;
-
-import static org.springframework.util.StringUtils.isEmpty;
 
+/**
+ * MQTT Client 网络组件提供商
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
 @Component
 @Slf4j
 public class MqttClientProvider implements NetworkProvider<MqttClientProperties> {
@@ -41,7 +42,7 @@ public class MqttClientProvider implements NetworkProvider<MqttClientProperties>
                               Environment environment) {
         this.vertx = vertx;
         this.certificateManager = certificateManager;
-        this.environment=environment;
+        this.environment = environment;
     }
 
     @Nonnull
@@ -52,33 +53,41 @@ public class MqttClientProvider implements NetworkProvider<MqttClientProperties>
 
     @Nonnull
     @Override
-    public VertxMqttClient createNetwork(@Nonnull MqttClientProperties properties) {
+    public Mono<Network> createNetwork(@Nonnull MqttClientProperties properties) {
         VertxMqttClient mqttClient = new VertxMqttClient(properties.getId());
-        initMqttClient(mqttClient, properties);
-        return mqttClient;
+        return initMqttClient(mqttClient, properties);
     }
 
     @Override
-    public void reload(@Nonnull Network network, @Nonnull MqttClientProperties properties) {
+    public Mono<Network> reload(@Nonnull Network network, @Nonnull MqttClientProperties properties) {
         VertxMqttClient mqttClient = ((VertxMqttClient) network);
         if (mqttClient.isLoading()) {
-            return;
+            return Mono.just(mqttClient);
         }
-        initMqttClient(mqttClient, properties);
+        return initMqttClient(mqttClient, properties);
     }
 
-    public void initMqttClient(VertxMqttClient mqttClient, MqttClientProperties properties) {
-        mqttClient.setLoading(true);
-        MqttClient client = MqttClient.create(vertx, properties.getOptions());
-        mqttClient.setClient(client);
-        client.connect(properties.getPort(), properties.getHost(), result -> {
-            mqttClient.setLoading(false);
-            if (!result.succeeded()) {
-                log.warn("connect mqtt [{}] error", properties.getId(), result.cause());
-            } else {
-                log.debug("connect mqtt [{}] success", properties.getId());
-            }
-        });
+    public Mono<Network> initMqttClient(VertxMqttClient mqttClient, MqttClientProperties properties) {
+        return convert(properties)
+            .map(options -> {
+                mqttClient.setTopicPrefix(properties.getTopicPrefix());
+                mqttClient.setLoading(true);
+                MqttClient client = MqttClient.create(vertx, options);
+                mqttClient.setClient(client);
+                client.connect(properties.getRemotePort(), properties.getRemoteHost(), result -> {
+                    mqttClient.setLoading(false);
+                    if (!result.succeeded()) {
+                        log.warn("connect mqtt [{}@{}:{}] error",
+                                 properties.getClientId(),
+                                 properties.getRemoteHost(),
+                                 properties.getRemotePort(),
+                                 result.cause());
+                    } else {
+                        log.debug("connect mqtt [{}] success", properties.getId());
+                    }
+                });
+                return mqttClient;
+            });
     }
 
     @Nullable
@@ -86,45 +95,55 @@ public class MqttClientProvider implements NetworkProvider<MqttClientProperties>
     public ConfigMetadata getConfigMetadata() {
         return new DefaultConfigMetadata()
             .add("id", "id", "", new StringType())
-            .add("instance", "服务实例数量(线程数)", "", new IntType())
+            .add("remoteHost", "远程地址", "", new StringType())
+            .add("remotePort", "远程地址", "", new IntType())
             .add("certId", "证书id", "", new StringType())
-            .add("ssl", "是否开启ssl", "", new BooleanType())
-            .add("options.port", "MQTT服务设置", "", new IntType());
+            .add("secure", "开启TSL", "", new BooleanType())
+            .add("clientId", "客户端ID", "", new BooleanType())
+            .add("username", "用户名", "", new BooleanType())
+            .add("password", "密码", "", new BooleanType());
     }
 
     @Nonnull
     @Override
     public Mono<MqttClientProperties> createConfig(@Nonnull NetworkProperties properties) {
-        return Mono.defer(() -> {
-            MqttClientProperties config = FastBeanCopier.copy(properties.getConfigurations(), new MqttClientProperties());
-            config.setId(properties.getId());
-            config.setOptions(new JSONObject(properties.getConfigurations()).toJavaObject(MqttClientOptions.class));
-
-            Map<String, Object> ctx = Collections.singletonMap("env", environment);
-
-            String clientId = ExpressionUtils.analytical(String.valueOf(config.getClientId()), ctx, "spel");
-
-            String username = isEmpty(config.getUsername())
-                ? config.getUsername()
-                : ExpressionUtils.analytical(String.valueOf(config.getUsername()), ctx, "spel");
-
-            String password = isEmpty(config.getPassword())
-                ? config.getPassword()
-                : ExpressionUtils.analytical(String.valueOf(config.getPassword()), ctx, "spel");
-
-            config.getOptions().setClientId(clientId);
-            config.getOptions().setPassword(password);
-            config.getOptions().setUsername(username);
-
-            if (config.isSsl()) {
-                config.getOptions().setSsl(true);
-                return certificateManager.getCertificate(config.getCertId())
-                    .map(VertxKeyCertTrustOptions::new)
-                    .doOnNext(config.getOptions()::setKeyCertOptions)
-                    .doOnNext(config.getOptions()::setTrustOptions)
-                    .thenReturn(config);
-            }
-            return Mono.just(config);
-        });
+        return Mono
+            .defer(() -> {
+                MqttClientProperties config = FastBeanCopier.copy(properties.getConfigurations(), new MqttClientProperties());
+                config.setId(properties.getId());
+                config.validate();
+                return Mono.just(config);
+            })
+            .as(LocaleUtils::transform);
+    }
+
+
+    private Mono<MqttClientOptions> convert(MqttClientProperties config) {
+        MqttClientOptions options = FastBeanCopier.copy(config, MqttClientOptions.class);
+        options.setTcpKeepAlive(true);
+//        options.setReconnectAttempts(10);
+        options.setAutoKeepAlive(true);
+        options.setKeepAliveInterval(180);
+
+        String clientId = String.valueOf(config.getClientId());
+
+        String username = config.getUsername();
+
+        String password = config.getUsername();
+
+        options.setClientId(clientId);
+        options.setPassword(password);
+        options.setUsername(username);
+
+        if (config.isSecure()) {
+            options.setSsl(true);
+            return certificateManager
+                .getCertificate(config.getCertId())
+                .map(VertxKeyCertTrustOptions::new)
+                .doOnNext(options::setKeyCertOptions)
+                .doOnNext(options::setTrustOptions)
+                .thenReturn(options);
+        }
+        return Mono.just(options);
     }
 }

+ 81 - 52
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/VertxMqttClient.java

@@ -3,8 +3,10 @@ package org.jetlinks.community.network.mqtt.client;
 import io.netty.handler.codec.mqtt.MqttQoS;
 import io.vertx.core.buffer.Buffer;
 import lombok.Getter;
+import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.jetlinks.community.network.DefaultNetworkType;
 import org.jetlinks.community.network.NetworkType;
 import org.jetlinks.core.message.codec.MqttMessage;
@@ -23,6 +25,12 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+/**
+ * 使用Vertx,MQTT Client。
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
 @Slf4j
 public class VertxMqttClient implements MqttClient {
 
@@ -37,6 +45,10 @@ public class VertxMqttClient implements MqttClient {
 
     private final List<Runnable> loadSuccessListener = new CopyOnWriteArrayList<>();
 
+    //订阅前缀
+    @Setter
+    private String topicPrefix;
+
     public void setLoading(boolean loading) {
         this.loading = loading;
         if (!loading) {
@@ -65,26 +77,30 @@ public class VertxMqttClient implements MqttClient {
         client
             .closeHandler(nil -> log.debug("mqtt client [{}] closed", id))
             .publishHandler(msg -> {
-                MqttMessage mqttMessage = SimpleMqttMessage
-                    .builder()
-                    .messageId(msg.messageId())
-                    .topic(msg.topicName())
-                    .payload(msg.payload().getByteBuf())
-                    .dup(msg.isDup())
-                    .retain(msg.isRetain())
-                    .qosLevel(msg.qosLevel().value())
-                    .build();
-                log.debug("handle mqtt message \n{}", mqttMessage);
-                subscriber
-                    .findTopic(msg.topicName().replace("#", "**").replace("+", "*"))
-                    .flatMapIterable(Topic::getSubscribers)
-                    .subscribe(sink -> {
-                        try {
-                            sink.getT2().next(mqttMessage);
-                        } catch (Exception e) {
-                            log.error("handle mqtt message error", e);
-                        }
-                    });
+                try {
+                    MqttMessage mqttMessage = SimpleMqttMessage
+                        .builder()
+                        .messageId(msg.messageId())
+                        .topic(msg.topicName())
+                        .payload(msg.payload().getByteBuf())
+                        .dup(msg.isDup())
+                        .retain(msg.isRetain())
+                        .qosLevel(msg.qosLevel().value())
+                        .build();
+                    log.debug("handle mqtt message \n{}", mqttMessage);
+                    subscriber
+                        .findTopic(msg.topicName().replace("#", "**").replace("+", "*"))
+                        .flatMapIterable(Topic::getSubscribers)
+                        .subscribe(sink -> {
+                            try {
+                                sink.getT2().next(mqttMessage);
+                            } catch (Exception e) {
+                                log.error("handle mqtt message error", e);
+                            }
+                        });
+                } catch (Throwable e) {
+                    log.error("handle mqtt message error", e);
+                }
             });
         if (loading) {
             loadSuccessListener.add(this::reSubscribe);
@@ -96,9 +112,10 @@ public class VertxMqttClient implements MqttClient {
 
     private void reSubscribe() {
         subscriber
-            .findTopic("/**")
+            .getAllSubscriber()
             .filter(topic -> topic.getSubscribers().size() > 0)
-            .collectMap(topic -> convertMqttTopic(topic.getSubscribers().iterator().next().getT1()), topic -> topic.getSubscribers().iterator().next().getT3())
+            .collectMap(topic -> getCompleteTopic(convertMqttTopic(topic.getSubscribers().iterator().next().getT1())),
+                        topic -> topic.getSubscribers().iterator().next().getT3())
             .filter(MapUtils::isNotEmpty)
             .subscribe(topics -> {
                 log.debug("subscribe mqtt topic {}", topics);
@@ -113,18 +130,26 @@ public class VertxMqttClient implements MqttClient {
     protected String parseTopic(String topic) {
         //适配emqx共享订阅
         if (topic.startsWith("$share")) {
-            topic= Stream.of(topic.split("/"))
-                .skip(2)
-                .collect(Collectors.joining("/", "/", ""));
+            topic = Stream.of(topic.split("/"))
+                          .skip(2)
+                          .collect(Collectors.joining("/", "/", ""));
         } else if (topic.startsWith("$queue")) {
-            topic= topic.substring(6);
+            topic = topic.substring(6);
         }
-        if(topic.startsWith("//")){
+        if (topic.startsWith("//")) {
             return topic.substring(1);
         }
         return topic;
     }
 
+    //获取完整的topic
+    protected String getCompleteTopic(String topic) {
+        if (StringUtils.isEmpty(topicPrefix)) {
+            return topic;
+        }
+        return topicPrefix.concat(topic);
+    }
+
     @Override
     public Flux<MqttMessage> subscribe(List<String> topics, int qos) {
         return Flux.create(sink -> {
@@ -133,22 +158,24 @@ public class VertxMqttClient implements MqttClient {
 
             for (String topic : topics) {
                 String realTopic = parseTopic(topic);
+                String completeTopic = getCompleteTopic(topic);
 
                 Topic<Tuple3<String, FluxSink<MqttMessage>, Integer>> sinkTopic = subscriber
-                    .append(realTopic.replace("#", "**")
-                                     .replace("+", "*"));
+                    .append(realTopic
+                                .replace("#", "**")
+                                .replace("+", "*"));
 
                 Tuple3<String, FluxSink<MqttMessage>, Integer> topicQos = Tuples.of(topic, sink, qos);
 
                 boolean first = sinkTopic.getSubscribers().size() == 0;
                 sinkTopic.subscribe(topicQos);
                 composite.add(() -> {
-                    if (sinkTopic.unsubscribe(topicQos).size() > 0) {
-                        client.unsubscribe(convertMqttTopic(topic), result -> {
+                    if (sinkTopic.unsubscribe(topicQos).size() > 0 && isAlive()) {
+                        client.unsubscribe(convertMqttTopic(completeTopic), result -> {
                             if (result.succeeded()) {
-                                log.debug("unsubscribe mqtt topic {}", topic);
+                                log.debug("unsubscribe mqtt topic {}", completeTopic);
                             } else {
-                                log.debug("unsubscribe mqtt topic {} error", topic, result.cause());
+                                log.debug("unsubscribe mqtt topic {} error", completeTopic, result.cause());
                             }
                         });
                     }
@@ -156,8 +183,8 @@ public class VertxMqttClient implements MqttClient {
 
                 //首次订阅
                 if (isAlive() && first) {
-                    log.debug("subscribe mqtt topic {}", topic);
-                    client.subscribe(convertMqttTopic(topic), qos, result -> {
+                    log.debug("subscribe mqtt topic {}", completeTopic);
+                    client.subscribe(convertMqttTopic(completeTopic), qos, result -> {
                         if (!result.succeeded()) {
                             sink.error(result.cause());
                         }
@@ -174,31 +201,33 @@ public class VertxMqttClient implements MqttClient {
         return Mono.create((sink) -> {
             Buffer buffer = Buffer.buffer(message.getPayload());
             client.publish(message.getTopic(),
-                buffer,
-                MqttQoS.valueOf(message.getQosLevel()),
-                message.isDup(),
-                message.isRetain(),
-                result -> {
-                    if (result.succeeded()) {
-                        log.info("publish mqtt [{}] message success: {}", client.clientId(), message);
-                        sink.success();
-                    } else {
-                        log.info("publish mqtt [{}] message error : {}", client.clientId(), message, result.cause());
-                        sink.error(result.cause());
-                    }
-                });
+                           buffer,
+                           MqttQoS.valueOf(message.getQosLevel()),
+                           message.isDup(),
+                           message.isRetain(),
+                           result -> {
+                               if (result.succeeded()) {
+                                   log.info("publish mqtt [{}] message success: {}", client.clientId(), message);
+                                   sink.success();
+                               } else {
+                                   log.info("publish mqtt [{}] message error : {}", client.clientId(), message, result.cause());
+                                   sink.error(result.cause());
+                               }
+                           });
         });
     }
 
     @Override
     public Mono<Void> publish(MqttMessage message) {
         if (loading) {
-            return Mono.create(sink ->
-                loadSuccessListener
-                    .add(() -> doPublish(message)
+            return Mono.create(sink -> {
+                loadSuccessListener.add(() -> {
+                    doPublish(message)
                         .doOnSuccess(sink::success)
                         .doOnError(sink::error)
-                        .subscribe()));
+                        .subscribe();
+                });
+            });
         }
         return doPublish(message);
     }

+ 0 - 39
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskConfiguration.java

@@ -1,39 +0,0 @@
-package org.jetlinks.community.network.mqtt.executor;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.hswebframework.web.utils.ExpressionUtils;
-import org.jetlinks.community.network.PubSubType;
-import org.jetlinks.rule.engine.executor.PayloadType;
-import org.springframework.util.Assert;
-
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-@Getter
-@Setter
-public class MqttClientTaskConfiguration {
-
-    private String clientId;
-
-    private PayloadType payloadType = PayloadType.JSON;
-
-    private PubSubType[] clientType;
-
-    private List<String> topics;
-
-    private List<String> topicVariables;
-
-    public List<String> getTopics(Map<String, Object> vars) {
-        return topics.stream()
-                .map(topic -> ExpressionUtils.analytical(topic, vars, "spel")).collect(Collectors.toList());
-    }
-
-    public void validate() {
-        Assert.hasText(clientId, "clientId can not be empty");
-        Assert.notNull(clientType, "clientType can not be null");
-        Assert.notEmpty(topics, "topics can not be empty");
-
-    }
-}

+ 0 - 131
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttClientTaskExecutorProvider.java

@@ -1,131 +0,0 @@
-package org.jetlinks.community.network.mqtt.executor;
-
-import lombok.AllArgsConstructor;
-import org.hswebframework.web.bean.FastBeanCopier;
-import org.hswebframework.web.dict.EnumDict;
-import org.jetlinks.community.network.DefaultNetworkType;
-import org.jetlinks.community.network.NetworkManager;
-import org.jetlinks.community.network.PubSubType;
-import org.jetlinks.community.network.mqtt.client.MqttClient;
-import org.jetlinks.core.message.codec.MqttMessage;
-import org.jetlinks.rule.engine.api.RuleConstants;
-import org.jetlinks.rule.engine.api.RuleData;
-import org.jetlinks.rule.engine.api.RuleDataCodecs;
-import org.jetlinks.rule.engine.api.RuleDataHelper;
-import org.jetlinks.rule.engine.api.task.ExecutionContext;
-import org.jetlinks.rule.engine.api.task.Task;
-import org.jetlinks.rule.engine.api.task.TaskExecutor;
-import org.jetlinks.rule.engine.api.task.TaskExecutorProvider;
-import org.jetlinks.rule.engine.defaults.AbstractTaskExecutor;
-import org.springframework.stereotype.Component;
-import reactor.core.Disposable;
-import reactor.core.Disposables;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-@AllArgsConstructor
-@Component
-public class MqttClientTaskExecutorProvider implements TaskExecutorProvider {
-
-    private final NetworkManager networkManager;
-
-    static {
-        MqttRuleDataCodec.load();
-    }
-
-    @Override
-    public String getExecutor() {
-        return "mqtt-client";
-    }
-
-    protected Flux<MqttMessage> convertMessage(RuleData message, MqttClientTaskConfiguration config) {
-
-        return RuleDataCodecs.getCodec(MqttMessage.class)
-            .map(codec ->
-                codec.decode(message,
-                    config.getPayloadType(),
-                    new MqttTopics(config.getTopics(RuleDataHelper.toContextMap(message))))
-                    .cast(MqttMessage.class))
-            .orElseThrow(() -> new UnsupportedOperationException("unsupported decode message:{}" + message));
-    }
-
-    protected Mono<RuleData> convertMessage(MqttMessage message, MqttClientTaskConfiguration config) {
-
-        return Mono.just(RuleDataCodecs.getCodec(MqttMessage.class)
-            .map(codec -> codec.encode(message, config.getPayloadType(), new TopicVariables(config.getTopicVariables())))
-            .map(RuleData::create)
-            .orElseGet(() -> RuleData.create(message)));
-    }
-
-    @Override
-    public Mono<TaskExecutor> createTask(ExecutionContext context) {
-        return Mono.just(new MqttClientTaskExecutor(context));
-    }
-
-    class MqttClientTaskExecutor extends AbstractTaskExecutor {
-
-        private MqttClientTaskConfiguration config;
-
-        public MqttClientTaskExecutor(ExecutionContext context) {
-            super(context);
-            reload();
-        }
-
-        @Override
-        public String getName() {
-            return "MQTT Client";
-        }
-
-        @Override
-        public void reload() {
-            config = FastBeanCopier.copy(context.getJob().getConfiguration(), new MqttClientTaskConfiguration());
-            config.validate();
-            if (disposable != null) {
-                disposable.dispose();
-            }
-        }
-
-        @Override
-        public void validate() {
-            FastBeanCopier
-                .copy(context.getJob().getConfiguration(), new MqttClientTaskConfiguration())
-                .validate();
-        }
-
-        @Override
-        protected Disposable doStart() {
-            Disposable.Composite disposable = Disposables.composite();
-
-            if (EnumDict.in(PubSubType.producer, config.getClientType())) {
-                disposable.add(context.getInput()
-                    .accept()
-                    .filter((data) -> state == Task.State.running)
-                    .flatMap(data ->
-                        networkManager
-                            .<MqttClient>getNetwork(DefaultNetworkType.MQTT_CLIENT, config.getClientId())
-                            .flatMapMany(client -> convertMessage(data, config)
-                                .flatMap(msg -> client
-                                    .publish(msg)
-                                    .doOnSuccess((v) -> context.getLogger().debug("推送MQTT[{}]消息:{}", client.getId(), msg))
-                                )
-                            ).onErrorContinue((err, e) -> context.onError(err, null).subscribe())
-                    )
-                    .subscribe()
-                );
-            }
-            if (EnumDict.in(PubSubType.consumer, config.getClientType())) {
-                disposable.add(networkManager
-                    .<MqttClient>getNetwork(DefaultNetworkType.MQTT_CLIENT, config.getClientId())
-                    .flatMapMany(client -> client.subscribe(config.getTopics()))
-                    .filter((data) -> state == Task.State.running)
-                    .doOnNext(message -> context.getLogger().info("consume mqtt message:{}", message))
-                    .flatMap(message -> convertMessage(message, config))
-                    .flatMap(ruleData -> context.getOutput().write(Mono.just(ruleData)).thenReturn(ruleData))
-                    .flatMap(ruleData -> context.fireEvent(RuleConstants.Event.result, ruleData).thenReturn(ruleData))
-                    .onErrorContinue((err, e) -> context.onError(err, null).subscribe())
-                    .subscribe());
-            }
-            return disposable;
-        }
-    }
-}

+ 0 - 111
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttRuleDataCodec.java

@@ -1,111 +0,0 @@
-package org.jetlinks.community.network.mqtt.executor;
-
-import io.netty.buffer.ByteBuf;
-import org.apache.commons.collections4.CollectionUtils;
-import org.jetlinks.core.message.codec.MessagePayloadType;
-import org.jetlinks.core.message.codec.MqttMessage;
-import org.jetlinks.core.message.codec.SimpleMqttMessage;
-import org.jetlinks.core.utils.TopicUtils;
-import org.jetlinks.rule.engine.api.RuleData;
-import org.jetlinks.rule.engine.api.RuleDataCodec;
-import org.jetlinks.rule.engine.api.RuleDataCodecs;
-import org.jetlinks.rule.engine.executor.PayloadType;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-public class MqttRuleDataCodec implements RuleDataCodec<MqttMessage> {
-
-    static {
-
-        MqttRuleDataCodec codec = new MqttRuleDataCodec();
-//        EncodedMessageCodec.register(DefaultTransport.MQTT, codec);
-//        EncodedMessageCodec.register(DefaultTransport.MQTT_TLS, codec);
-        RuleDataCodecs.register(MqttMessage.class, codec);
-
-    }
-
-    static void load() {
-
-    }
-
-    @Override
-    public Object encode(MqttMessage message, Feature... features) {
-        Map<String, Object> payload = new HashMap<>();
-        payload.put("topic", message.getTopic());
-        payload.put("will", message.isWill());
-        payload.put("qos", message.getQosLevel());
-        payload.put("dup", message.isDup());
-        payload.put("retain", message.isRetain());
-        PayloadType payloadType = Feature.find(PayloadType.class, features).orElse(PayloadType.JSON);
-        Feature.find(TopicVariables.class, features)
-            .map(TopicVariables::getVariables)
-            .filter(CollectionUtils::isNotEmpty)
-            .flatMap(list -> list.stream()
-                .map(str -> TopicUtils.getPathVariables(str, message.getTopic()))
-                .reduce((m1, m2) -> {
-                    m1.putAll(m2);
-                    return m1;
-                }))
-            .ifPresent(vars -> payload.put("vars", vars));
-
-        payload.put("payloadType", payloadType.name());
-        payload.put("payload", payloadType.read(message.getPayload()));
-        payload.put("clientId", message.getClientId());
-
-
-        return payload;
-    }
-
-    @Override
-    public Flux<MqttMessage> decode(RuleData data, Feature... features) {
-        if (data.getData() instanceof MqttMessage) {
-            return Flux.just(((MqttMessage) data.getData()));
-        }
-        MqttTopics topics = Feature.find(MqttTopics.class, features).orElse(null);
-
-        return data
-            .dataToMap()
-            .filter(map -> map.containsKey("payload"))
-            .flatMap(map -> {
-                if (topics != null && !map.containsKey("topic")) {
-                    return Flux.fromIterable(topics.getTopics())
-                        .flatMap(topic -> {
-                            Map<String, Object> copy = new HashMap<>();
-                            copy.put("topic", topic);
-                            copy.putAll(map);
-                            return Mono.just(copy);
-                        })
-                        .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("topic not set")));
-                }
-                return Flux.just(map);
-            })
-            .map(map -> {
-                PayloadType payloadType = Feature.find(PayloadType.class, features)
-                    .orElseGet(() -> Optional.ofNullable(map.get("payloadType"))
-                        .map(String::valueOf)
-                        .map(PayloadType::valueOf)
-                        .orElse(PayloadType.JSON));
-                Object payload = map.get("payload");
-
-                ByteBuf byteBuf = payloadType.write(payload);
-
-                Integer qos = (Integer) map.get("qos");
-
-                return SimpleMqttMessage
-                    .builder()
-                    .clientId((String) map.get("clientId"))
-                    .topic((String) map.get("topic"))
-                    .dup(Boolean.TRUE.equals(map.get("dup")))
-                    .will(Boolean.TRUE.equals(map.get("will")))
-                    .retain(Boolean.TRUE.equals(map.get("retain")))
-                    .qosLevel(qos == null ? 0 : qos)
-                    .payloadType(MessagePayloadType.valueOf(payloadType.name()))
-                    .payload(byteBuf)
-                    .build();
-            });
-    }
-}

+ 0 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/executor/MqttTopics.java


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác