From 2e487a42e237b00b9b8001c1167404efce5a1829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8?= Date: Thu, 13 Feb 2025 12:33:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9F=BA=E7=A1=80=E6=A8=A1=E5=9D=97):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=91=BD=E4=BB=A4=E6=A8=A1=E5=BC=8F=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20(#607)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 可使用 CommandSupportManagerProviders相关方法来执行服务命令进行解耦. --- .../annotation/command/CommandService.java | 64 ++++++++ .../CommandSupportManagerProvider.java | 102 +++++++++++++ .../CommandSupportManagerProviders.java | 121 +++++++++++++++ ...ompositeCommandSupportManagerProvider.java | 37 +++++ .../command/InternalSdkServices.java | 43 ++++++ .../CommandServiceEndpointRegister.java | 83 +++++++++++ .../SpringBeanCommandSupportProvider.java | 138 ++++++++++++++++++ .../configuration/CommonConfiguration.java | 7 + .../community/web/CommandInfoController.java | 63 ++++++++ 9 files changed, 658 insertions(+) create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/annotation/command/CommandService.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CommandSupportManagerProvider.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CommandSupportManagerProviders.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CompositeCommandSupportManagerProvider.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/InternalSdkServices.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/CommandServiceEndpointRegister.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/SpringBeanCommandSupportProvider.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/CommandInfoController.java diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/annotation/command/CommandService.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/annotation/command/CommandService.java new file mode 100644 index 00000000..af6ec337 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/annotation/command/CommandService.java @@ -0,0 +1,64 @@ +package org.jetlinks.community.annotation.command; + + +import org.jetlinks.core.annotation.command.CommandHandler; +import org.springframework.stereotype.Indexed; + +import java.lang.annotation.*; + +/** + * 标记一个类为命令服务支持端点,用于对外提供命令支持 + *
{@code
+ *
+ *  @CommandService("myService")
+ *  public class MyCommandService{
+ *
+ *  }
+ *
+ * }
+ * + * @author zhouhao + * @since 1.2.3 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Indexed +public @interface CommandService { + + /** + * 服务标识 + * + * @return 服务标识 + */ + String id(); + + /** + * 服务名称 + * + * @return 服务名称 + */ + String name(); + + /** + * 服务描述 + * + * @return 服务描述 + */ + String[] description() default {}; + + + /** + * 是否根据注解扫描注册服务 + * + * @return 是否注册服务 + */ + boolean autoRegistered() default true; + + /** + * 命令定义,用于声明支持的命令 + * + * @return 命令定义 + */ + CommandHandler[] commands() default {}; +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CommandSupportManagerProvider.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CommandSupportManagerProvider.java new file mode 100644 index 00000000..32a2c093 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CommandSupportManagerProvider.java @@ -0,0 +1,102 @@ +package org.jetlinks.community.command; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.core.command.CommandSupport; +import org.jetlinks.core.utils.SerializeUtils; +import org.jetlinks.community.spi.Provider; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.Map; + +/** + * 命令支持提供者,用于针对多个基于命令模式的可选模块依赖时的解耦. + *

+ * + * @author zhouhao + * @see org.jetlinks.sdk.server.SdkServices + * @see InternalSdkServices + * @since 2.1 + */ +public interface CommandSupportManagerProvider { + /** + * 所有支持的提供商 + */ + Provider supports = Provider.create(CommandSupportManagerProvider.class); + + /** + * 命令服务提供商标识 + * + * @return 唯一标识 + */ + String getProvider(); + + /** + * 获取命令支持,不同的命令管理支持多种命令支持,可能通过id进行区分,具体规则由对应服务实 现 + * + * @param id 命令ID标识 + * @param options 拓展配置 + * @return CommandSupport + * @see CommandSupportManagerProviders#getCommandSupport(String, Map) + */ + Mono getCommandSupport(String id, Map options); + + /** + * 获取所有支持的信息 + * + * @return id + */ + default Flux getSupportInfo() { + return Flux.empty(); + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor(staticName = "of") + @Setter + class CommandSupportInfo implements Externalizable { + private String id; + private String name; + private String description; + + public CommandSupportInfo copy() { + return FastBeanCopier.copy(this, new CommandSupportInfo()); + } + + /** + * @param serviceId serviceId + * @return this + * @see CommandSupportManagerProviders#getCommandSupport(String) + */ + public CommandSupportInfo appendService(String serviceId) { + if (this.id == null) { + this.id = serviceId; + } else { + this.id = serviceId + ":" + id; + } + return this; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + SerializeUtils.writeNullableUTF(id, out); + SerializeUtils.writeNullableUTF(name, out); + SerializeUtils.writeNullableUTF(description, out); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + id = SerializeUtils.readNullableUTF(in); + name = SerializeUtils.readNullableUTF(in); + description = SerializeUtils.readNullableUTF(in); + } + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CommandSupportManagerProviders.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CommandSupportManagerProviders.java new file mode 100644 index 00000000..4885e771 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CommandSupportManagerProviders.java @@ -0,0 +1,121 @@ +package org.jetlinks.community.command; + +import org.jetlinks.core.command.CommandSupport; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +/** + * 命令支持管理提供商工具类,用于提供对{@link CommandSupportManagerProvider}相关通用操作. + * + * @author zhouhao + * @see CommandSupportManagerProvider + * @see CommandSupportManagerProviders#getCommandSupport(String, Map) + * @since 2.2 + */ +public class CommandSupportManagerProviders { + + + /** + * 根据服务ID获取CommandSupport. + *

{@code
+     *
+     *  CommandSupportManagerProviders
+     *      .getCommandSupport("deviceService:device",Collections.emptyMap())
+     *
+     * }
+ * + * @param serviceId serviceId 服务名 + * @return CommandSupport + * @see InternalSdkServices + * @see org.jetlinks.sdk.server.SdkServices + */ + public static Mono getCommandSupport(String serviceId) { + return getCommandSupport(serviceId, Collections.emptyMap()); + } + + /** + * 根据服务ID和支持ID获取CommandSupport. + * + * @param serviceId 服务ID + * @param supportId 支持ID + * @return CommandSupport + */ + public static Mono getCommandSupport(String serviceId, String supportId) { + return getProviderNow(serviceId) + .getCommandSupport(supportId, Collections.emptyMap()) + .cast(CommandSupport.class); + } + + /** + * 根据服务ID获取CommandSupport. + *
{@code
+     *
+     *  CommandSupportManagerProviders
+     *      .getCommandSupport("deviceService:device",Collections.emptyMap())
+     *
+     * }
+ * + * @param serviceId serviceId 服务名 + * @param options options + * @return CommandSupport + * @see InternalSdkServices + * @see org.jetlinks.sdk.server.SdkServices + */ + public static Mono getCommandSupport(String serviceId, + Map options) { + //fast path + CommandSupportManagerProvider provider = CommandSupportManagerProvider + .supports + .get(serviceId) + .orElse(null); + if (provider != null) { + return provider + .getCommandSupport(serviceId, options) + .cast(CommandSupport.class); + } + String supportId = serviceId; + // deviceService:product + if (serviceId.contains(":")) { + String[] arr = serviceId.split(":", 2); + serviceId = arr[0]; + supportId = arr[1]; + } + String finalServiceId = serviceId; + String finalSupportId = supportId; + return Mono.defer(() -> getProviderNow(finalServiceId).getCommandSupport(finalSupportId, options)); + } + + /** + * 注册命令支持 + * + * @param provider {@link CommandSupportManagerProvider#getProvider()} + */ + public static void register(CommandSupportManagerProvider provider) { + CommandSupportManagerProvider.supports.register(provider.getProvider(), provider); + } + + /** + * 获取命令支持 + * + * @param provider {@link CommandSupportManagerProvider#getProvider()} + * @return Optional + */ + public static Optional getProvider(String provider) { + return CommandSupportManagerProvider.supports.get(provider); + } + + /** + * 获取命令支持,如果不存在则抛出异常{@link UnsupportedOperationException} + * + * @param provider provider {@link CommandSupportManagerProvider#getProvider()} + * @return CommandSupportManagerProvider + */ + public static CommandSupportManagerProvider getProviderNow(String provider) { + return CommandSupportManagerProvider.supports.getNow(provider); + } + + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CompositeCommandSupportManagerProvider.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CompositeCommandSupportManagerProvider.java new file mode 100644 index 00000000..d2a0f583 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CompositeCommandSupportManagerProvider.java @@ -0,0 +1,37 @@ +package org.jetlinks.community.command; + +import lombok.AllArgsConstructor; +import org.jetlinks.core.command.CommandSupport; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +@AllArgsConstructor +public class CompositeCommandSupportManagerProvider implements CommandSupportManagerProvider { + private final List providers; + + @Override + public String getProvider() { + return providers.get(0).getProvider(); + } + + @Override + public Mono getCommandSupport(String id, Map options) { + + return Flux + .fromIterable(providers) + .flatMap(provider -> provider.getCommandSupport(id, options)) + .take(1) + .singleOrEmpty(); + } + + @Override + public Flux getSupportInfo() { + return Flux + .fromIterable(providers) + .flatMap(CommandSupportManagerProvider::getSupportInfo) + .distinct(CommandSupportInfo::getId); + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/InternalSdkServices.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/InternalSdkServices.java new file mode 100644 index 00000000..002a56e2 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/InternalSdkServices.java @@ -0,0 +1,43 @@ +package org.jetlinks.community.command; + +/** + * 平台内部的一些服务定义 + * + * @author zhouhao + * @see org.jetlinks.sdk.server.SdkServices + * @since 2.2 + */ +public interface InternalSdkServices { + + /** + * 网络组件服务 + */ + String networkService = "networkService"; + + /** + * 设备接入网关服务 + */ + String deviceGatewayService = "deviceGatewayService"; + + + /** + * 采集器服务 + */ + String collectorService = "collectorService"; + + /** + * 规则服务 + */ + String ruleService = "ruleService"; + + + /** + * 插件服务 + */ + String pluginService = "pluginService"; + + /** + * 基础服务 + */ + String commonService = "commonService"; +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/CommandServiceEndpointRegister.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/CommandServiceEndpointRegister.java new file mode 100644 index 00000000..aeacf60b --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/CommandServiceEndpointRegister.java @@ -0,0 +1,83 @@ +package org.jetlinks.community.command.register; + +import lombok.extern.slf4j.Slf4j; +import org.jetlinks.community.annotation.command.CommandService; +import org.jetlinks.community.command.CommandSupportManagerProvider; +import org.jetlinks.community.command.CommandSupportManagerProviders; +import org.jetlinks.community.command.CompositeCommandSupportManagerProvider; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +public class CommandServiceEndpointRegister implements ApplicationContextAware, SmartInitializingSingleton { + + private ApplicationContext context; + + @Override + public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException { + this.context = applicationContext; + } + + @Override + public void afterSingletonsInstantiated() { + Map beans = context.getBeansWithAnnotation(CommandService.class); + + //静态Provider + Map> statics = context + .getBeanProvider(CommandSupportManagerProvider.class) + .stream() + .collect(Collectors.groupingBy(CommandSupportManagerProvider::getProvider)); + + Map providers = new HashMap<>(); + + for (Object value : beans.values()) { + CommandService endpoint = + AnnotatedElementUtils.findMergedAnnotation(ClassUtils.getUserClass(value), CommandService.class); + if (endpoint == null || !endpoint.autoRegistered()) { + continue; + } + String id = endpoint.id(); + String support = id; + if (id.contains(":")) { + support = id.substring(id.indexOf(":") + 1); + id = id.substring(0, id.indexOf(":")); + } + + SpringBeanCommandSupportProvider provider = providers + .computeIfAbsent(id, SpringBeanCommandSupportProvider::new); + log.debug("register command support:{} -> {}", endpoint.id(), value); + provider.register(support, endpoint, value); + } + + for (SpringBeanCommandSupportProvider value : providers.values()) { + if (value.isEmpty()) { + continue; + } + //合并静态Provider + List provider = statics.remove(value.getProvider()); + if (provider != null) { + provider.forEach(value::register); + } + + CommandSupportManagerProviders.register(value); + } + for (List value : statics.values()) { + if (value.size() == 1) { + CommandSupportManagerProviders.register(value.get(0)); + } else { + CommandSupportManagerProviders.register(new CompositeCommandSupportManagerProvider(value)); + } + } + } + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/SpringBeanCommandSupportProvider.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/SpringBeanCommandSupportProvider.java new file mode 100644 index 00000000..66666025 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/SpringBeanCommandSupportProvider.java @@ -0,0 +1,138 @@ +package org.jetlinks.community.command.register; + +import com.google.common.collect.Lists; +import lombok.Getter; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.core.command.CommandSupport; +import org.jetlinks.core.command.CompositeCommandSupport; +import org.jetlinks.community.annotation.command.CommandService; +import org.jetlinks.community.command.CommandSupportManagerProvider; +import org.jetlinks.supports.command.JavaBeanCommandSupport; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.*; + +class SpringBeanCommandSupportProvider implements CommandSupportManagerProvider { + + private final String provider; + private final Map commandSupports = new HashMap<>(); + + private final List statics = new ArrayList<>(); + + public SpringBeanCommandSupportProvider(String provider) { + this.provider = provider; + } + + void register(CommandSupportManagerProvider provider) { + statics.add(provider); + } + + void register(String support, CommandService annotation, Object bean) { + Objects.requireNonNull(annotation, "endpoint"); + Objects.requireNonNull(bean, "bean"); + + SpringBeanCommandSupport commandSupport = new SpringBeanCommandSupport(annotation, bean); + if (commandSupport.isEmpty()) { + return; + } + //相同support合并成一个 + commandSupports + .computeIfAbsent(support, id -> new CompositeSpringBeanCommandSupport(id,provider)) + .register(commandSupport); + } + + boolean isEmpty() { + return commandSupports.isEmpty(); + } + + @Override + public String getProvider() { + return provider; + } + + @Override + public Mono getCommandSupport(String id, Map options) { + CommandSupport support = commandSupports.get(StringUtils.hasText(id) ? id : provider); + if (support != null) { + return Mono.just(support); + } + if (statics.isEmpty()) { + return Mono.empty(); + } + return Flux + .fromIterable(statics) + .flatMap(provider -> provider.getCommandSupport(id, options)) + .take(1) + .singleOrEmpty(); + } + + @Override + public Flux getSupportInfo() { + if (statics.isEmpty()) { + return Flux + .fromIterable(commandSupports.values()) + .flatMapIterable(CompositeSpringBeanCommandSupport::getInfo); + } + return Flux + .concat( + Flux + .fromIterable(commandSupports.values()) + .flatMapIterable(CompositeSpringBeanCommandSupport::getInfo), + Flux.fromIterable(statics) + .flatMap(CommandSupportManagerProvider::getSupportInfo) + ) + .distinct(info -> { + String id = info.getId(); + return String.valueOf(id); + }); + } + + static class CompositeSpringBeanCommandSupport extends CompositeCommandSupport { + private final String id; + private final String provider; + + public CompositeSpringBeanCommandSupport(String id,String provider) { + super(); + this.id = id; + this.provider = provider; + } + + public List getInfo() { + return Lists + .transform( + getSupports(), + support -> { + SpringBeanCommandSupport commandSupport = support.unwrap(SpringBeanCommandSupport.class); + //兼容为null + String _id = id.equals(provider) ? null : id; + return CommandSupportInfo.of( + _id, + LocaleUtils.resolveMessage(commandSupport.annotation.name(), commandSupport.annotation.name()), + String.join("", commandSupport.annotation.description()) + ); + }); + } + + @Override + public String toString() { + return getSupports().toString(); + } + } + + @Getter + static class SpringBeanCommandSupport extends JavaBeanCommandSupport { + private final CommandService annotation; + + boolean isEmpty() { + return handlers.isEmpty(); + } + + public SpringBeanCommandSupport(CommandService annotation, Object target) { + super(target); + this.annotation = annotation; + } + + } +} \ No newline at end of file diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java index b125c2ab..f71836a8 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java @@ -14,6 +14,7 @@ import org.hswebframework.web.dict.EnumDict; import org.hswebframework.web.dict.defaults.DefaultItemDefine; import org.jetlinks.community.Interval; import org.jetlinks.community.JvmErrorException; +import org.jetlinks.community.command.register.CommandServiceEndpointRegister; import org.jetlinks.community.config.ConfigManager; import org.jetlinks.community.config.ConfigScopeCustomizer; import org.jetlinks.community.config.ConfigScopeProperties; @@ -250,6 +251,12 @@ public class CommonConfiguration { return referenceManager; } + @Bean + public CommandServiceEndpointRegister commandServiceEndpointRegister() { + return new CommandServiceEndpointRegister(); + } + + @Configuration @ConditionalOnClass(ReactiveRedisOperations.class) static class DefaultUserBindServiceConfiguration { diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/CommandInfoController.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/CommandInfoController.java new file mode 100644 index 00000000..9e7e5ed8 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/CommandInfoController.java @@ -0,0 +1,63 @@ +package org.jetlinks.community.web; + +import io.swagger.v3.oas.annotations.Hidden; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.hswebframework.web.authorization.annotation.Authorize; +import org.jetlinks.core.command.CommandSupport; +import org.jetlinks.core.metadata.FunctionMetadata; +import org.jetlinks.community.command.CommandSupportManagerProvider; +import org.jetlinks.community.command.CommandSupportManagerProviders; +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; + +/** + * 获取平台内部命令信息接口 + * + * @author zhouhao + * @since 2.2 + */ +@RestController +@RequestMapping("/command-supports") +@Hidden +@AllArgsConstructor +public class CommandInfoController { + + @GetMapping("/services") + @SneakyThrows + @Authorize + public Flux getServices() { + return Flux + .fromIterable(CommandSupportManagerProvider.supports.getAll()) + .flatMap(provider -> provider + .getSupportInfo() + .map(s -> s.copy().appendService(provider.getProvider())) + .defaultIfEmpty( + CommandSupportManagerProvider + .CommandSupportInfo + .of(provider.getProvider(), null, null))); + } + + @GetMapping("/service/{serviceId}/commands") + @SneakyThrows + @Authorize + public Flux getServiceCommands(@PathVariable String serviceId) { + return CommandSupportManagerProviders + .getCommandSupport(serviceId) + .flatMapMany(CommandSupport::getCommandMetadata); + } + + @GetMapping("/service/{serviceId}/exists") + @SneakyThrows + @Authorize + public Mono getServiceCommandSupport(@PathVariable String serviceId) { + return CommandSupportManagerProviders + .getCommandSupport(serviceId) + .hasElement() + .onErrorResume(err -> Mono.just(false)); + } +}