diff --git a/script/soul.sql b/script/soul.sql
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/soul-admin/src/main/java/org/dromara/soul/admin/controller/SoulClientController.java b/soul-admin/src/main/java/org/dromara/soul/admin/controller/SoulClientController.java
index 5fa23b9cfcda..b0feb8cf9856 100644
--- a/soul-admin/src/main/java/org/dromara/soul/admin/controller/SoulClientController.java
+++ b/soul-admin/src/main/java/org/dromara/soul/admin/controller/SoulClientController.java
@@ -67,7 +67,7 @@ public String registerSpringMvc(@RequestBody final SpringMvcRegisterDTO springMv
public String registerSpringCloud(@RequestBody final SpringCloudRegisterDTO springCloudRegisterDTO) {
return soulClientRegisterService.registerSpringCloud(springCloudRegisterDTO);
}
-
+
/**
* Register dubbo string.
*
@@ -100,4 +100,15 @@ public String registerSofaRpc(@RequestBody final MetaDataDTO metaDataDTO) {
public String registerTarsRpc(@RequestBody final MetaDataDTO metaDataDTO) {
return soulClientRegisterService.registerTars(metaDataDTO);
}
+
+ /**
+ * Register spring mvc string.
+ *
+ * @param grpcMetaDataDTO the spring mvc register dto
+ * @return the string
+ */
+ @PostMapping("/grpc-register")
+ public String registerGrpc(@RequestBody final MetaDataDTO grpcMetaDataDTO) {
+ return soulClientRegisterService.registerGrpc(grpcMetaDataDTO);
+ }
}
diff --git a/soul-admin/src/main/java/org/dromara/soul/admin/service/SoulClientRegisterService.java b/soul-admin/src/main/java/org/dromara/soul/admin/service/SoulClientRegisterService.java
index 74ae4021b407..772533c0fbdc 100644
--- a/soul-admin/src/main/java/org/dromara/soul/admin/service/SoulClientRegisterService.java
+++ b/soul-admin/src/main/java/org/dromara/soul/admin/service/SoulClientRegisterService.java
@@ -25,7 +25,7 @@
* The interface Soul client service.
*/
public interface SoulClientRegisterService {
-
+
/**
* Register http string.
*
@@ -33,7 +33,7 @@ public interface SoulClientRegisterService {
* @return the string
*/
String registerSpringMvc(SpringMvcRegisterDTO springMvcRegisterDTO);
-
+
/**
* Register spring cloud string.
*
@@ -41,7 +41,7 @@ public interface SoulClientRegisterService {
* @return the string
*/
String registerSpringCloud(SpringCloudRegisterDTO springCloudRegisterDTO);
-
+
/**
* Register rpc string.
*
@@ -65,4 +65,12 @@ public interface SoulClientRegisterService {
* @return the string
*/
String registerTars(MetaDataDTO metaDataDTO);
+
+ /**
+ * Register grpc string.
+ *
+ * @param metaDataDTO the meta data dto
+ * @return the string
+ */
+ String registerGrpc(MetaDataDTO metaDataDTO);
}
diff --git a/soul-admin/src/main/java/org/dromara/soul/admin/service/impl/SoulClientRegisterServiceImpl.java b/soul-admin/src/main/java/org/dromara/soul/admin/service/impl/SoulClientRegisterServiceImpl.java
index 22f16674d709..c1ed39332057 100644
--- a/soul-admin/src/main/java/org/dromara/soul/admin/service/impl/SoulClientRegisterServiceImpl.java
+++ b/soul-admin/src/main/java/org/dromara/soul/admin/service/impl/SoulClientRegisterServiceImpl.java
@@ -89,8 +89,6 @@ public class SoulClientRegisterServiceImpl implements SoulClientRegisterService
private final PluginMapper pluginMapper;
-
-
/**
* Instantiates a new Meta data service.
*
@@ -210,6 +208,15 @@ public String registerTars(final MetaDataDTO dto) {
return SoulResultMessage.SUCCESS;
}
+ @Override
+ public String registerGrpc(final MetaDataDTO dto) {
+ MetaDataDO exist = metaDataMapper.findByPath(dto.getPath());
+ saveOrUpdateMetaData(exist, dto);
+ String selectorId = handlerGrpcSelector(dto);
+ handlerGrpcRule(selectorId, dto, exist);
+ return SoulResultMessage.SUCCESS;
+ }
+
private String handlerTarsSelector(final MetaDataDTO metaDataDTO) {
return getString(metaDataDTO);
}
@@ -232,6 +239,17 @@ private void handlerSofaRule(final String selectorId, final MetaDataDTO metaData
}
}
+ private String handlerGrpcSelector(final MetaDataDTO metaDataDTO) {
+ return getString(metaDataDTO);
+ }
+
+ private void handlerGrpcRule(final String selectorId, final MetaDataDTO metaDataDTO, final MetaDataDO exist) {
+ RuleDO existRule = ruleMapper.findByName(metaDataDTO.getPath());
+ if (Objects.isNull(exist) || Objects.isNull(existRule)) {
+ registerRule(selectorId, metaDataDTO.getPath(), metaDataDTO.getRpcType(), metaDataDTO.getRuleName());
+ }
+ }
+
private void saveSpringMvcMetaData(final SpringMvcRegisterDTO dto) {
Timestamp currentTime = new Timestamp(System.currentTimeMillis());
MetaDataDO metaDataDO = MetaDataDO.builder()
@@ -374,6 +392,9 @@ private String registerSelector(final String contextPath, final String rpcType,
} else if (RpcTypeEnum.TARS.getName().equals(rpcType)) {
selectorDTO.setPluginId(getPluginId(PluginEnum.TARS.getName()));
selectorDTO.setHandle(appName);
+ } else if (RpcTypeEnum.GRPC.getName().equals(rpcType)) {
+ selectorDTO.setPluginId(getPluginId(PluginEnum.GRPC.getName()));
+ selectorDTO.setHandle(appName);
} else {
//is divide
DivideUpstream divideUpstream = buildDivideUpstream(uri);
diff --git a/soul-admin/src/main/resources/META-INF/schema.h2.sql b/soul-admin/src/main/resources/META-INF/schema.h2.sql
index 6513464241b9..f3e0ad0cebfb 100644
--- a/soul-admin/src/main/resources/META-INF/schema.h2.sql
+++ b/soul-admin/src/main/resources/META-INF/schema.h2.sql
@@ -261,6 +261,7 @@ INSERT INTO `plugin` (`id`, `name`, `role`, `config`, `enabled`, `date_created`,
INSERT INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('12','resilience4j', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10');
INSERT INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('13', 'tars', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10');
INSERT INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('14', 'context_path', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10');
+INSERT INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('15', 'grpc', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10');
/**default admin user**/
INSERT INTO `dashboard_user` (`id`, `user_name`, `password`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('1','admin','jHcpKkiDbbQh7W7hh8yQSA==', '1', '1', '2018-06-23 15:12:22', '2018-06-23 15:12:23');
diff --git a/soul-admin/src/main/resources/META-INF/schema.sql b/soul-admin/src/main/resources/META-INF/schema.sql
index 2bf4603bd1a8..e38ad66eaf0c 100644
--- a/soul-admin/src/main/resources/META-INF/schema.sql
+++ b/soul-admin/src/main/resources/META-INF/schema.sql
@@ -264,6 +264,7 @@ INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `config`, `enabled`, `date_cr
INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('12','resilience4j', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10');
INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('13', 'tars', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10');
INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('14', 'context_path', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10');
+INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('15', 'grpc', '1','0', '2020-11-09 01:19:10', '2020-11-09 01:19:10');
/**default admin user**/
INSERT IGNORE INTO `dashboard_user` (`id`, `user_name`, `password`, `role`, `enabled`, `date_created`, `date_updated`) VALUES ('1','admin','jHcpKkiDbbQh7W7hh8yQSA==', '1', '1', '2018-06-23 15:12:22', '2018-06-23 15:12:23');
diff --git a/soul-client/pom.xml b/soul-client/pom.xml
index be320d50825e..fb314adbc2dd 100644
--- a/soul-client/pom.xml
+++ b/soul-client/pom.xml
@@ -33,6 +33,7 @@
soul-client-dubbosoul-client-sofasoul-client-tars
+ soul-client-grpc
diff --git a/soul-client/soul-client-grpc/pom.xml b/soul-client/soul-client-grpc/pom.xml
new file mode 100644
index 000000000000..5fbfa93b0c16
--- /dev/null
+++ b/soul-client/soul-client-grpc/pom.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+ soul-client
+ org.dromara
+ 2.2.1
+
+ 4.0.0
+
+ soul-client-grpc
+
+
+ 1.33.1
+
+
+
+
+ org.dromara
+ soul-client-common
+ ${project.version}
+
+
+ org.springframework
+ spring-beans
+ provided
+
+
+ org.springframework
+ spring-context
+ provided
+
+
+ org.springframework
+ spring-core
+ provided
+
+
+ io.grpc
+ grpc-all
+ ${grpc.version}
+ provided
+
+
+
diff --git a/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/GrpcClientBeanPostProcessor.java b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/GrpcClientBeanPostProcessor.java
new file mode 100644
index 000000000000..2a635b3ac8c8
--- /dev/null
+++ b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/GrpcClientBeanPostProcessor.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.client.grpc;
+
+import io.grpc.BindableService;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.client.common.utils.OkHttpTools;
+import org.dromara.soul.client.common.utils.RegisterUtils;
+import org.dromara.soul.client.grpc.common.annotation.SoulGrpcClient;
+import org.dromara.soul.client.grpc.common.config.GrpcConfig;
+import org.dromara.soul.client.grpc.common.dto.MetaDataDTO;
+import org.dromara.soul.common.enums.RpcTypeEnum;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.lang.NonNull;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * The type Soul grpc client bean post processor.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public class GrpcClientBeanPostProcessor implements BeanPostProcessor {
+
+ private final ThreadPoolExecutor executorService;
+
+ private final String url;
+
+ private final GrpcConfig grpcConfig;
+
+ private final String serviceName = "SERVICE_NAME";
+
+ /**
+ * Instantiates a new Soul client bean post processor.
+ *
+ * @param config the soul grpc config
+ */
+ public GrpcClientBeanPostProcessor(final GrpcConfig config) {
+ String contextPath = config.getContextPath();
+ String adminUrl = config.getAdminUrl();
+ Integer port = config.getPort();
+ if (contextPath == null || "".equals(contextPath)
+ || adminUrl == null || "".equals(adminUrl)
+ || port == null) {
+ log.error("grpc param must config contextPath, adminUrl and port");
+ throw new RuntimeException("grpc param must config contextPath, adminUrl and port");
+ }
+ this.grpcConfig = config;
+ url = adminUrl + "/soul-client/grpc-register";
+ executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(@NonNull final Object bean, @NonNull final String beanName) throws BeansException {
+ if (bean instanceof BindableService) {
+ executorService.execute(() -> handler(bean));
+ }
+ return bean;
+ }
+
+ private void handler(final Object serviceBean) {
+ Class> clazz;
+ try {
+ clazz = serviceBean.getClass();
+ } catch (Exception e) {
+ log.error("failed to get grpc target class");
+ return;
+ }
+ Class parent = clazz.getSuperclass();
+ Class classes = parent.getDeclaringClass();
+ String packageName = null;
+ try {
+ Field field = classes.getField(serviceName);
+ field.setAccessible(true);
+ packageName = field.get(null).toString();
+ } catch (Exception e) {
+ log.error(String.format("SERVICE_NAME field not found: %s", classes));
+ return;
+ }
+ if (StringUtils.isEmpty(packageName)) {
+ log.error(String.format("grpc SERVICE_NAME can not found: %s", classes));
+ return;
+ }
+ final Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(clazz);
+ for (Method method : methods) {
+ SoulGrpcClient grpcClient = method.getAnnotation(SoulGrpcClient.class);
+ if (Objects.nonNull(grpcClient)) {
+ RegisterUtils.doRegister(buildJsonParams(packageName, grpcClient, method), url, RpcTypeEnum.GRPC);
+ }
+ }
+ }
+
+ private String buildJsonParams(final String packageName, final SoulGrpcClient soulGrpcClient, final Method method) {
+ String path = grpcConfig.getContextPath() + soulGrpcClient.path();
+ String desc = soulGrpcClient.desc();
+ String configRuleName = soulGrpcClient.ruleName();
+ String ruleName = ("".equals(configRuleName)) ? path : configRuleName;
+ String methodName = method.getName();
+ Class>[] parameterTypesClazz = method.getParameterTypes();
+ String parameterTypes = Arrays.stream(parameterTypesClazz).map(Class::getName)
+ .collect(Collectors.joining(","));
+ MetaDataDTO grpcMetaDataDTO = MetaDataDTO.builder()
+ .appName(String.join(":", grpcConfig.getHost(), grpcConfig.getPort().toString()))
+ .serviceName(packageName)
+ .methodName(methodName)
+ .contextPath(grpcConfig.getContextPath())
+ .path(path)
+ .ruleName(ruleName)
+ .pathDesc(desc)
+ .parameterTypes(parameterTypes)
+ .rpcType("grpc")
+ .rpcExt(buildRpcExt(soulGrpcClient))
+ .enabled(soulGrpcClient.enabled())
+ .build();
+ return OkHttpTools.getInstance().getGson().toJson(grpcMetaDataDTO);
+ }
+
+ private String buildRpcExt(final SoulGrpcClient soulGrpcClient) {
+ MetaDataDTO.RpcExt build = MetaDataDTO.RpcExt.builder()
+ .timeout(soulGrpcClient.timeout())
+ .build();
+ return OkHttpTools.getInstance().getGson().toJson(build);
+
+ }
+}
+
+
diff --git a/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/annotation/SoulGrpcClient.java b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/annotation/SoulGrpcClient.java
new file mode 100644
index 000000000000..5c15bb1dee4b
--- /dev/null
+++ b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/annotation/SoulGrpcClient.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.dromara.soul.client.grpc.common.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The interface Soul client.
+ *
+ * @author zhanglei
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface SoulGrpcClient {
+ /**
+ * Path string.
+ *
+ * @return the string
+ */
+ String path();
+
+ /**
+ * Rule name string.
+ *
+ * @return the string
+ */
+ String ruleName() default "";
+
+ /**
+ * Desc string.
+ *
+ * @return String string
+ */
+ String desc() default "";
+
+ /**
+ * Enabled boolean.
+ *
+ * @return the boolean
+ */
+ boolean enabled() default true;
+
+ /**
+ * Timeout long.
+ *
+ * @return the timeout
+ */
+ int timeout() default -1;
+}
diff --git a/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/config/GrpcConfig.java b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/config/GrpcConfig.java
new file mode 100644
index 000000000000..5bb9e4065cf0
--- /dev/null
+++ b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/config/GrpcConfig.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.dromara.soul.client.grpc.common.config;
+
+import lombok.Data;
+
+/**
+ * grpc config.
+ *
+ * @author zhanglei
+ */
+@Data
+public class GrpcConfig {
+
+ private String host;
+
+ private Integer port;
+
+ private String appName;
+
+ private String adminUrl;
+
+ private String contextPath;
+}
diff --git a/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/dto/MetaDataDTO.java b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/dto/MetaDataDTO.java
new file mode 100644
index 000000000000..4db83d589706
--- /dev/null
+++ b/soul-client/soul-client-grpc/src/main/java/org/dromara/soul/client/grpc/common/dto/MetaDataDTO.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.dromara.soul.client.grpc.common.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * The type Meta data dto.
+ *
+ * @author xiaoyu
+ */
+@Data
+@Builder
+public class MetaDataDTO {
+
+ private String appName;
+
+ private String contextPath;
+
+ private String path;
+
+ private String pathDesc;
+
+ private String rpcType;
+
+ private String serviceName;
+
+ private String methodName;
+
+ private String ruleName;
+
+ private String parameterTypes;
+
+ private String rpcExt;
+
+ private boolean enabled;
+
+ /**
+ * The type Rpc ext.
+ */
+ @Data
+ @Builder
+ public static class RpcExt {
+
+ private String loadbalance;
+
+ private Integer timeout;
+
+ }
+
+}
diff --git a/soul-common/src/main/java/org/dromara/soul/common/constant/Constants.java b/soul-common/src/main/java/org/dromara/soul/common/constant/Constants.java
index 2ed677d115c9..4c2f7241304e 100644
--- a/soul-common/src/main/java/org/dromara/soul/common/constant/Constants.java
+++ b/soul-common/src/main/java/org/dromara/soul/common/constant/Constants.java
@@ -74,6 +74,11 @@ public interface Constants {
*/
String TARS_RPC_RESULT = "tars_rpc_result";
+ /**
+ * The constant GRPC_RPC_RESULT.
+ */
+ String GRPC_RPC_RESULT = "grpc_rpc_result";
+
/**
* The constant TARS_RPC_RESULT_EMPTY.
*/
@@ -124,6 +129,11 @@ public interface Constants {
*/
String TARS_PARAMS = "tars_params";
+ /**
+ * The constant GRPC_PARAMS.
+ */
+ String GRPC_PARAMS = "grpc_params";
+
/**
* The constant DECODE.
*/
diff --git a/soul-common/src/main/java/org/dromara/soul/common/enums/PluginEnum.java b/soul-common/src/main/java/org/dromara/soul/common/enums/PluginEnum.java
index 86bd76e30049..e2247ddc9dcc 100644
--- a/soul-common/src/main/java/org/dromara/soul/common/enums/PluginEnum.java
+++ b/soul-common/src/main/java/org/dromara/soul/common/enums/PluginEnum.java
@@ -111,6 +111,11 @@ public enum PluginEnum {
*/
TARS(60, 0, "tars"),
+ /**
+ * GPRC plugin enum.
+ */
+ GRPC(60, 0, "grpc"),
+
/**
* Monitor plugin enum.
*/
diff --git a/soul-common/src/main/java/org/dromara/soul/common/enums/RpcTypeEnum.java b/soul-common/src/main/java/org/dromara/soul/common/enums/RpcTypeEnum.java
index bcfd1992989d..56de03199969 100644
--- a/soul-common/src/main/java/org/dromara/soul/common/enums/RpcTypeEnum.java
+++ b/soul-common/src/main/java/org/dromara/soul/common/enums/RpcTypeEnum.java
@@ -72,7 +72,7 @@ public enum RpcTypeEnum {
/**
* grpc.
*/
- GRPC("grpc", false);
+ GRPC("grpc", true);
private final String name;
diff --git a/soul-examples/pom.xml b/soul-examples/pom.xml
index 9554055ea55b..2c7da1514c10 100644
--- a/soul-examples/pom.xml
+++ b/soul-examples/pom.xml
@@ -45,6 +45,7 @@
soul-examples-eurekasoul-examples-sofasoul-examples-tars
+ soul-examples-grpc
diff --git a/soul-examples/soul-examples-grpc/pom.xml b/soul-examples/soul-examples-grpc/pom.xml
new file mode 100644
index 000000000000..0eba19d95dc6
--- /dev/null
+++ b/soul-examples/soul-examples-grpc/pom.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+ soul-examples
+ org.dromara
+ 2.1.0
+
+ 4.0.0
+
+ soul-examples-grpc
+
+
+ 1.33.1
+ 3.13.0
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.dromara
+ soul-spring-boot-starter-client-grpc
+ ${soul.version}
+
+
+ guava
+ com.google.guava
+
+
+
+
+
+ io.grpc
+ grpc-all
+ ${grpc.version}
+ provided
+
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.6.2
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ 11
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+
+ com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+ src/main/resources/proto
+
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+
+
+
+
diff --git a/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/SoulTestGrpcApplication.java b/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/SoulTestGrpcApplication.java
new file mode 100644
index 000000000000..54728eb03631
--- /dev/null
+++ b/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/SoulTestGrpcApplication.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.examples.grpc;
+
+import io.grpc.ServerBuilder;
+import io.grpc.protobuf.services.ProtoReflectionService;
+import org.dromara.soul.examples.grpc.echo.EchoServiceImpl;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+import java.io.IOException;
+
+/**
+ * SoulTestTarsApplication.
+ * @author tydhot
+ */
+@SpringBootApplication
+public class SoulTestGrpcApplication {
+
+ /**
+ * main.
+ *
+ * @param args args
+ */
+ public static void main(final String[] args) throws IOException {
+ SpringApplication.run(SoulTestGrpcApplication.class, args);
+ io.grpc.Server server = ServerBuilder
+ .forPort(8080)
+ .addService(new EchoServiceImpl())
+ .addService(ProtoReflectionService.newInstance())
+ .build();
+ server.start();
+ }
+
+}
diff --git a/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/echo/EchoServiceImpl.java b/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/echo/EchoServiceImpl.java
new file mode 100644
index 000000000000..5b73620c1f9a
--- /dev/null
+++ b/soul-examples/soul-examples-grpc/src/main/java/org/dromara/soul/examples/grpc/echo/EchoServiceImpl.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.examples.grpc.echo;
+
+import echo.EchoRequest;
+import echo.EchoResponse;
+import echo.EchoServiceGrpc;
+import echo.Trace;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.stub.StreamObserver;
+import org.dromara.soul.client.grpc.common.annotation.SoulGrpcClient;
+import org.springframework.stereotype.Service;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * @author zhanglei
+ */
+@Service
+public class EchoServiceImpl extends EchoServiceGrpc.EchoServiceImplBase {
+
+ @Override
+ @SoulGrpcClient(path = "/echo")
+ public void echo(EchoRequest request, StreamObserver responseObserver) {
+ System.out.println("Received: " + request.getMessage());
+
+ EchoResponse.Builder response = EchoResponse.newBuilder()
+ .setMessage("ReceivedHELLO")
+ .addTraces(Trace.newBuilder().setHost(getHostname()).build());
+
+ responseObserver.onNext(response.build());
+ responseObserver.onCompleted();
+ }
+
+ private String getHostname() {
+ try {
+ return InetAddress.getLocalHost().getHostName() + "(" + InetAddress.getLocalHost().getHostAddress() + ")";
+ } catch (UnknownHostException e) {
+ return "";
+ }
+ }
+}
diff --git a/soul-examples/soul-examples-grpc/src/main/resources/application.yml b/soul-examples/soul-examples-grpc/src/main/resources/application.yml
new file mode 100644
index 000000000000..973e3fa1c68b
--- /dev/null
+++ b/soul-examples/soul-examples-grpc/src/main/resources/application.yml
@@ -0,0 +1,38 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+server:
+ port: 55290
+ servlet:
+ context-path: /
+ address: 0.0.0.0
+
+spring:
+ application:
+ name: grpc-test
+
+logging:
+ level:
+ root: info
+ org.dromara.soul: debug
+ path: "./logs"
+
+soul:
+ grpc:
+ adminUrl: http://localhost:9095
+ contextPath: /grpc
+ appName: grpc
+ host: 127.0.0.1
+ port: 8080
diff --git a/soul-examples/soul-examples-grpc/src/main/resources/proto/echo.proto b/soul-examples/soul-examples-grpc/src/main/resources/proto/echo.proto
new file mode 100644
index 000000000000..026a31030dff
--- /dev/null
+++ b/soul-examples/soul-examples-grpc/src/main/resources/proto/echo.proto
@@ -0,0 +1,37 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+syntax = "proto3";
+
+package echo;
+
+option java_multiple_files = true;
+
+
+service EchoService {
+ rpc echo (EchoRequest) returns (EchoResponse) {}
+}
+
+message EchoRequest {
+ string message = 1;
+}
+
+message EchoResponse {
+ string message = 1;
+ repeated Trace traces = 2;
+}
+
+message Trace {
+ string host = 1;
+}
diff --git a/soul-plugin/pom.xml b/soul-plugin/pom.xml
index b85ec3a685c4..429e75042ecf 100644
--- a/soul-plugin/pom.xml
+++ b/soul-plugin/pom.xml
@@ -48,6 +48,7 @@
soul-plugin-sofasoul-plugin-tarssoul-plugin-context-path
+ soul-plugin-grpc
diff --git a/soul-plugin/soul-plugin-api/src/main/java/org/dromara/soul/plugin/api/result/SoulResultEnum.java b/soul-plugin/soul-plugin-api/src/main/java/org/dromara/soul/plugin/api/result/SoulResultEnum.java
index 9a6541198655..3aee73da360a 100644
--- a/soul-plugin/soul-plugin-api/src/main/java/org/dromara/soul/plugin/api/result/SoulResultEnum.java
+++ b/soul-plugin/soul-plugin-api/src/main/java/org/dromara/soul/plugin/api/result/SoulResultEnum.java
@@ -79,6 +79,16 @@ public enum SoulResultEnum {
*/
TARS_INVOKE(434, "Tars invoke error!"),
+ /**
+ * Grpc have body param soul result enum.
+ */
+ GRPC_HAVE_BODY_PARAM(435, "grpc must have body param, please enter the JSON format in the body!"),
+
+ /**
+ * Grpc client resultenum.
+ */
+ GRPC_CLIENT_NULL(436, "grpc client is null, please check the context path!"),
+
/**
* full selector type enum.
*/
diff --git a/soul-plugin/soul-plugin-global/src/main/java/org/dromara/soul/plugin/global/DefaultSoulContextBuilder.java b/soul-plugin/soul-plugin-global/src/main/java/org/dromara/soul/plugin/global/DefaultSoulContextBuilder.java
index 960c01824b92..764dcb8f42eb 100644
--- a/soul-plugin/soul-plugin-global/src/main/java/org/dromara/soul/plugin/global/DefaultSoulContextBuilder.java
+++ b/soul-plugin/soul-plugin-global/src/main/java/org/dromara/soul/plugin/global/DefaultSoulContextBuilder.java
@@ -20,6 +20,7 @@
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Optional;
+
import org.apache.commons.lang3.StringUtils;
import org.dromara.soul.common.constant.Constants;
import org.dromara.soul.common.dto.MetaData;
@@ -36,7 +37,7 @@
* @author xiaoyu
*/
public class DefaultSoulContextBuilder implements SoulContextBuilder {
-
+
@Override
public SoulContext build(final ServerWebExchange exchange) {
final ServerHttpRequest request = exchange.getRequest();
@@ -47,7 +48,7 @@ public SoulContext build(final ServerWebExchange exchange) {
}
return transform(request, metaData);
}
-
+
/**
* ServerHttpRequest transform RequestDTO .
*
@@ -71,6 +72,8 @@ private SoulContext transform(final ServerHttpRequest request, final MetaData me
setSoulContextBySofa(soulContext, metaData);
} else if (RpcTypeEnum.TARS.getName().equals(metaData.getRpcType())) {
setSoulContextByTars(soulContext, metaData);
+ } else if (RpcTypeEnum.GRPC.getName().equals(metaData.getRpcType())) {
+ setSoulContextByGrpc(soulContext, metaData);
} else {
setSoulContextByHttp(soulContext, path);
soulContext.setRpcType(RpcTypeEnum.HTTP.getName());
@@ -86,7 +89,7 @@ private SoulContext transform(final ServerHttpRequest request, final MetaData me
Optional.ofNullable(request.getMethod()).ifPresent(httpMethod -> soulContext.setHttpMethod(httpMethod.name()));
return soulContext;
}
-
+
private void setSoulContextByDubbo(final SoulContext soulContext, final MetaData metaData) {
soulContext.setModule(metaData.getAppName());
soulContext.setMethod(metaData.getServiceName());
@@ -107,7 +110,14 @@ private void setSoulContextByTars(final SoulContext soulContext, final MetaData
soulContext.setRpcType(metaData.getRpcType());
soulContext.setContextPath(metaData.getContextPath());
}
-
+
+ private void setSoulContextByGrpc(final SoulContext soulContext, final MetaData metaData) {
+ soulContext.setModule(metaData.getServiceName());
+ soulContext.setMethod(metaData.getMethodName());
+ soulContext.setRpcType(metaData.getRpcType());
+ soulContext.setContextPath(metaData.getContextPath());
+ }
+
private void setSoulContextByHttp(final SoulContext soulContext, final String path) {
String contextPath = "/";
String[] splitList = StringUtils.split(path, "/");
diff --git a/soul-plugin/soul-plugin-grpc/pom.xml b/soul-plugin/soul-plugin-grpc/pom.xml
new file mode 100644
index 000000000000..07767928cff1
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/pom.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+ soul-plugin
+ org.dromara
+ 2.2.1
+
+ 4.0.0
+
+ soul-plugin-grpc
+
+ 1.33.1
+ 3.12.0
+
+
+
+
+ org.dromara
+ soul-plugin-base
+ ${project.version}
+
+
+
+ io.grpc
+ grpc-all
+ ${grpc.version}
+
+
+ com.google.protobuf
+ protobuf-java
+ ${protobuf-java.version}
+
+
+ org.springframework
+ spring-test
+ test
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+
+
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/GrpcPlugin.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/GrpcPlugin.java
new file mode 100644
index 000000000000..88757dbeca45
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/GrpcPlugin.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc;
+
+import io.grpc.CallOptions;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.dromara.soul.common.constant.Constants;
+import org.dromara.soul.common.dto.MetaData;
+import org.dromara.soul.common.dto.RuleData;
+import org.dromara.soul.common.dto.SelectorData;
+import org.dromara.soul.common.enums.PluginEnum;
+import org.dromara.soul.common.enums.ResultEnum;
+import org.dromara.soul.common.enums.RpcTypeEnum;
+import org.dromara.soul.common.exception.SoulException;
+import org.dromara.soul.plugin.api.SoulPluginChain;
+import org.dromara.soul.plugin.api.context.SoulContext;
+import org.dromara.soul.plugin.api.result.SoulResultEnum;
+import org.dromara.soul.plugin.base.AbstractSoulPlugin;
+import org.dromara.soul.plugin.base.utils.SoulResultWrap;
+import org.dromara.soul.plugin.base.utils.WebFluxResultUtils;
+import org.dromara.soul.plugin.grpc.cache.GrpcClientCache;
+import org.dromara.soul.plugin.grpc.client.SoulGrpcClient;
+import org.dromara.soul.plugin.grpc.proto.SoulGrpcResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * The type grpc plugin.
+ *
+ * @author xiaoyu(Myth)
+ */
+@Slf4j
+public class GrpcPlugin extends AbstractSoulPlugin {
+
+ @Override
+ protected Mono doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
+ String body = exchange.getAttribute(Constants.GRPC_PARAMS);
+ SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
+ assert soulContext != null;
+ MetaData metaData = exchange.getAttribute(Constants.META_DATA);
+ if (!checkMetaData(metaData)) {
+ assert metaData != null;
+ log.error(" path is :{}, meta data have error.... {}", soulContext.getPath(), metaData.toString());
+ exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
+ Object error = SoulResultWrap.error(SoulResultEnum.META_DATA_ERROR.getCode(), SoulResultEnum.META_DATA_ERROR.getMsg(), null);
+ return WebFluxResultUtils.result(exchange, error);
+ }
+ if (StringUtils.isNoneBlank(metaData.getParameterTypes()) && StringUtils.isBlank(body)) {
+ exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
+ Object error = SoulResultWrap.error(SoulResultEnum.GRPC_HAVE_BODY_PARAM.getCode(), SoulResultEnum.GRPC_HAVE_BODY_PARAM.getMsg(), null);
+ return WebFluxResultUtils.result(exchange, error);
+ }
+
+ final SoulGrpcClient client = GrpcClientCache.getGrpcClient(metaData.getContextPath());
+ if (Objects.isNull(client)) {
+ exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
+ Object error = SoulResultWrap.error(SoulResultEnum.GRPC_CLIENT_NULL.getCode(), SoulResultEnum.GRPC_CLIENT_NULL.getMsg(), null);
+ return WebFluxResultUtils.result(exchange, error);
+ }
+ CompletableFuture result = client.call(metaData, CallOptions.DEFAULT, body);
+ return Mono.fromFuture(result.thenApply(ret -> {
+ exchange.getAttributes().put(Constants.GRPC_RPC_RESULT, ret.getResult());
+ exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.SUCCESS.getName());
+ return ret;
+ })).onErrorMap(SoulException::new).then(chain.execute(exchange));
+ }
+
+ /**
+ * acquire plugin name.
+ *
+ * @return plugin name.
+ */
+ @Override
+ public String named() {
+ return PluginEnum.GRPC.getName();
+ }
+
+ /**
+ * plugin is execute.
+ *
+ * @param exchange the current server exchange
+ * @return default false.
+ */
+ @Override
+ public Boolean skip(final ServerWebExchange exchange) {
+ final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
+ assert soulContext != null;
+ return !Objects.equals(soulContext.getRpcType(), RpcTypeEnum.GRPC.getName());
+ }
+
+ @Override
+ public int getOrder() {
+ return PluginEnum.GRPC.getCode();
+ }
+
+ private boolean checkMetaData(final MetaData metaData) {
+ return null != metaData && !StringUtils.isBlank(metaData.getMethodName()) && !StringUtils.isBlank(metaData.getServiceName());
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/ApplicationConfigCache.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/ApplicationConfigCache.java
new file mode 100644
index 000000000000..8fcf99ad1327
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/ApplicationConfigCache.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.cache;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.common.dto.MetaData;
+import org.dromara.soul.common.exception.SoulException;
+import org.dromara.soul.plugin.grpc.resolver.SoulServiceInstance;
+import org.dromara.soul.plugin.grpc.resolver.SoulServiceInstanceLists;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+/**
+ * Grpc config cache.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public final class ApplicationConfigCache {
+
+ private final int maxCount = 50000;
+
+ private final LoadingCache cache = CacheBuilder.newBuilder()
+ .maximumWeight(maxCount)
+ .weigher((Weigher) (string, referenceConfig) -> getSize())
+ .build(new CacheLoader() {
+ @Override
+ public SoulServiceInstanceLists load(final String key) {
+ return new SoulServiceInstanceLists(new CopyOnWriteArrayList<>(), key);
+ }
+ });
+
+ private final Map listener = new ConcurrentHashMap<>();
+
+ private ApplicationConfigCache() {
+ }
+
+ private int getSize() {
+ return (int) cache.size();
+ }
+
+ /**
+ * Get soulServiceInstanceList.
+ *
+ * @param path path
+ * @return SoulServiceInstanceLists instances
+ */
+ public SoulServiceInstanceLists get(final String path) {
+ try {
+ return cache.get(path);
+ } catch (ExecutionException e) {
+ throw new SoulException(e.getCause());
+ }
+ }
+
+ /**
+ * Init prx.
+ *
+ * @param metaData metaData
+ */
+ public void initPrx(final MetaData metaData) {
+ try {
+ SoulServiceInstanceLists soulServiceInstances = cache.get(metaData.getContextPath());
+ List instances = soulServiceInstances.getSoulServiceInstances();
+ String[] ipAndPort = metaData.getAppName().split(":");
+ instances.add(new SoulServiceInstance(ipAndPort[0], Integer.valueOf(ipAndPort[1])));
+ Consumer consumer = listener.get(metaData.getContextPath());
+ if (Objects.nonNull(consumer)) {
+ consumer.accept(System.currentTimeMillis());
+ }
+ } catch (ExecutionException e) {
+ throw new SoulException(e.getCause());
+ }
+ }
+
+ /**
+ * invalidate client.
+ *
+ * @param metaData metaData
+ */
+ public void invalidate(final MetaData metaData) {
+ cache.invalidate(metaData.getContextPath());
+ listener.remove(metaData.getContextPath());
+ GrpcClientCache.removeClient(metaData.getContextPath());
+ }
+
+ /**
+ * Refresh.
+ *
+ * @param key contextPath
+ * @param consumer consumer
+ */
+ public void watch(final String key, final Consumer consumer) {
+ listener.put(key, consumer);
+ }
+
+ /**
+ * Gets instance.
+ *
+ * @return the instance
+ */
+ public static ApplicationConfigCache getInstance() {
+ return ApplicationConfigCacheInstance.INSTANCE;
+ }
+
+ /**
+ * The type Application config cache instance.
+ */
+ static class ApplicationConfigCacheInstance {
+ /**
+ * The Instance.
+ */
+ static final ApplicationConfigCache INSTANCE = new ApplicationConfigCache();
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/GrpcClientCache.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/GrpcClientCache.java
new file mode 100644
index 000000000000..2c542148804c
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/cache/GrpcClientCache.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.cache;
+
+import com.google.common.collect.Maps;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.plugin.grpc.client.GrpcClientBuilder;
+import org.dromara.soul.plugin.grpc.client.SoulGrpcClient;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * The Grpc client cache.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public class GrpcClientCache {
+
+ private static final Map CLIENT_CACHE = Maps.newConcurrentMap();
+
+ static {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ for (Map.Entry entry : CLIENT_CACHE.entrySet()) {
+ SoulGrpcClient grpcClient = entry.getValue();
+ grpcClient.close();
+ }
+ CLIENT_CACHE.clear();
+ }));
+ }
+
+ /**
+ * Init client.
+ *
+ * @param contextPath contextPath
+ */
+ public static void initGrpcClient(final String contextPath) {
+ CLIENT_CACHE.computeIfAbsent(contextPath, s -> {
+ return GrpcClientBuilder.buildClient(contextPath);
+ });
+ }
+
+ /**
+ * Get the client.
+ *
+ * @param contextPath contextPath
+ * @return SoulGrpcClient oulGrpcClient
+ */
+ public static SoulGrpcClient getGrpcClient(final String contextPath) {
+ return CLIENT_CACHE.get(contextPath);
+ }
+
+ /**
+ * Remove client.
+ *
+ * @param contextPath contextPath
+ */
+ public static void removeClient(final String contextPath) {
+ SoulGrpcClient grpcClient = CLIENT_CACHE.remove(contextPath);
+ if (Objects.nonNull(grpcClient)) {
+ grpcClient.close();
+ }
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/GrpcClientBuilder.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/GrpcClientBuilder.java
new file mode 100644
index 000000000000..7224e16931d2
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/GrpcClientBuilder.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.client;
+
+import io.grpc.LoadBalancerRegistry;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.NameResolverRegistry;
+import org.dromara.soul.plugin.grpc.loadbalance.LoadBalancerStrategy;
+import org.dromara.soul.plugin.grpc.loadbalance.RandomLoadBalancerProvider;
+import org.dromara.soul.plugin.grpc.loadbalance.RoundRobinLoadBalancerProvider;
+import org.dromara.soul.plugin.grpc.resolver.SoulNameResolverProvider;
+
+/**
+ * Grpc client Builder.
+ *
+ * @author zhanglei
+ */
+public class GrpcClientBuilder {
+
+ static {
+ LoadBalancerRegistry.getDefaultRegistry().register(new RandomLoadBalancerProvider());
+ LoadBalancerRegistry.getDefaultRegistry().register(new RoundRobinLoadBalancerProvider());
+ NameResolverRegistry.getDefaultRegistry().register(new SoulNameResolverProvider());
+ }
+
+ /**
+ * Build the client.
+ *
+ * @param contextPath contextPath
+ * @return SoulGrpcClient soulGrpcClient
+ */
+ public static SoulGrpcClient buildClient(final String contextPath) {
+ ManagedChannelBuilder> builder = ManagedChannelBuilder.forTarget(contextPath)
+ .defaultLoadBalancingPolicy(LoadBalancerStrategy.Random.getStrategy())
+ .usePlaintext()
+ .maxInboundMessageSize(100 * 1024 * 1024)
+ .disableRetry();
+ ManagedChannel channel = builder.build();
+ channel.getState(true);
+ return new SoulGrpcClient(channel);
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/SoulGrpcClient.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/SoulGrpcClient.java
new file mode 100644
index 000000000000..3b5d19cb41a8
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/client/SoulGrpcClient.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.client;
+
+import java.io.Closeable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.util.JsonFormat;
+import io.grpc.CallOptions;
+import io.grpc.ClientCall;
+import io.grpc.ManagedChannel;
+import io.grpc.MethodDescriptor;
+import io.grpc.stub.StreamObserver;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.common.dto.MetaData;
+import org.dromara.soul.plugin.grpc.proto.DynamicMessageMarshaller;
+import org.dromara.soul.plugin.grpc.proto.MessageWriter;
+import org.dromara.soul.plugin.grpc.proto.SoulGrpcCallRequest;
+import org.dromara.soul.plugin.grpc.proto.SoulGrpcResponse;
+import org.dromara.soul.plugin.grpc.proto.CompleteObserver;
+import org.dromara.soul.plugin.grpc.proto.CompositeStreamObserver;
+import org.dromara.soul.plugin.grpc.reflection.SoulGrpcReflectionClient;
+import org.dromara.soul.plugin.grpc.resolver.ServiceResolver;
+
+import static io.grpc.stub.ClientCalls.asyncUnaryCall;
+import static io.grpc.stub.ClientCalls.asyncServerStreamingCall;
+import static io.grpc.stub.ClientCalls.asyncClientStreamingCall;
+import static io.grpc.stub.ClientCalls.asyncBidiStreamingCall;
+
+
+/**
+ * The Soul grpc client.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public class SoulGrpcClient implements Closeable {
+
+ private final ManagedChannel channel;
+
+ private final SoulGrpcReflectionClient reflectionClient;
+
+ public SoulGrpcClient(final ManagedChannel channel) {
+ this.channel = channel;
+ this.reflectionClient = SoulGrpcReflectionClient.create(channel);
+ }
+
+ /**
+ * Grpc call.
+ *
+ * @param metaData metadata
+ * @param callOptions callOptions
+ * @param requestJsons requestJsons
+ * @return CompletableFuture future
+ */
+ public CompletableFuture call(final MetaData metaData, final CallOptions callOptions, final String requestJsons) {
+ DescriptorProtos.FileDescriptorSet fileDescriptorSet = reflectionClient.resolveService(metaData.getServiceName());
+ if (fileDescriptorSet == null) {
+ return null;
+ }
+ ServiceResolver serviceResolver = ServiceResolver.fromFileDescriptorSet(fileDescriptorSet);
+ Descriptors.MethodDescriptor methodDescriptor = serviceResolver.resolveServiceMethod(metaData);
+ JsonFormat.TypeRegistry registry = JsonFormat.TypeRegistry.newBuilder().add(serviceResolver.listMessageTypes()).build();
+ DynamicMessage requestMessages = reflectionClient.parseToMessages(registry, methodDescriptor.getInputType(), requestJsons);
+ SoulGrpcResponse soulGrpcResponse = new SoulGrpcResponse();
+ StreamObserver streamObserver = MessageWriter.newInstance(registry, soulGrpcResponse);
+ SoulGrpcCallRequest callParams = SoulGrpcCallRequest.builder()
+ .methodDescriptor(methodDescriptor)
+ .channel(channel)
+ .callOptions(callOptions)
+ .requests(requestMessages)
+ .responseObserver(streamObserver)
+ .build();
+ try {
+ this.invoke(callParams).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException("Caught exception while waiting for rpc", e);
+ }
+ return CompletableFuture.completedFuture(soulGrpcResponse);
+ }
+
+ /**
+ * Grpc call.
+ *
+ * @param callParams callParams
+ * @return ListenableFuture future
+ */
+ public ListenableFuture invoke(final SoulGrpcCallRequest callParams) {
+ MethodDescriptor.MethodType methodType = reflectionClient.fetchMethodType(callParams.getMethodDescriptor());
+ DynamicMessage request = callParams.getRequests();
+ StreamObserver responseObserver = callParams.getResponseObserver();
+ CompleteObserver doneObserver = new CompleteObserver<>();
+ StreamObserver compositeObserver = CompositeStreamObserver.of(responseObserver, doneObserver);
+ StreamObserver requestObserver;
+ switch (methodType) {
+ case UNARY:
+ asyncUnaryCall(createCall(callParams), request, compositeObserver);
+ return doneObserver.getCompletionFuture();
+ case SERVER_STREAMING:
+ asyncServerStreamingCall(createCall(callParams), request, compositeObserver);
+ return doneObserver.getCompletionFuture();
+ case CLIENT_STREAMING:
+ requestObserver = asyncClientStreamingCall(createCall(callParams), compositeObserver);
+ requestObserver.onCompleted();
+ return doneObserver.getCompletionFuture();
+ case BIDI_STREAMING:
+ requestObserver = asyncBidiStreamingCall(createCall(callParams), compositeObserver);
+ requestObserver.onCompleted();
+ return doneObserver.getCompletionFuture();
+ default:
+ log.info("Unknown methodType:{}", methodType);
+ return null;
+ }
+ }
+
+ @Override
+ public void close() {
+ this.channel.shutdown();
+ this.reflectionClient.getFileDescriptorCache().clear();
+ }
+
+ private ClientCall createCall(final SoulGrpcCallRequest callParams) {
+ return callParams.getChannel().newCall(createGrpcMethodDescriptor(callParams.getMethodDescriptor()),
+ callParams.getCallOptions());
+ }
+
+ private io.grpc.MethodDescriptor createGrpcMethodDescriptor(final Descriptors.MethodDescriptor descriptor) {
+ return io.grpc.MethodDescriptor.newBuilder()
+ .setType(reflectionClient.fetchMethodType(descriptor))
+ .setFullMethodName(reflectionClient.fetchFullMethodName(descriptor))
+ .setRequestMarshaller(new DynamicMessageMarshaller(descriptor.getInputType()))
+ .setResponseMarshaller(new DynamicMessageMarshaller(descriptor.getOutputType()))
+ .build();
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/AbstractLoadBalancer.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/AbstractLoadBalancer.java
new file mode 100644
index 000000000000..0ffdcb37cd55
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/AbstractLoadBalancer.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance;
+
+import io.grpc.ConnectivityState;
+import io.grpc.EquivalentAddressGroup;
+import io.grpc.LoadBalancer;
+import io.grpc.Status;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.plugin.grpc.loadbalance.picker.AbstractReadyPicker;
+import org.dromara.soul.plugin.grpc.loadbalance.picker.AbstractPicker;
+import org.dromara.soul.plugin.grpc.loadbalance.picker.EmptyPicker;
+import io.grpc.Attributes;
+import io.grpc.ConnectivityStateInfo;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.HashMap;
+import java.util.List;
+import java.util.HashSet;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static io.grpc.ConnectivityState.CONNECTING;
+import static io.grpc.ConnectivityState.IDLE;
+import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
+import static io.grpc.ConnectivityState.SHUTDOWN;
+import static io.grpc.ConnectivityState.READY;
+
+/**
+ * LoadBalancer.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public abstract class AbstractLoadBalancer extends LoadBalancer {
+
+ private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready");
+
+ private final Helper helper;
+
+ private final AtomicReference serviceName = new AtomicReference<>();
+
+ private final Map subchannels = new ConcurrentHashMap<>();
+
+ private ConnectivityState currentState;
+
+ private AbstractPicker currentPicker = new EmptyPicker(EMPTY_OK);
+
+ public AbstractLoadBalancer(final Helper helper) {
+ this.helper = checkNotNull(helper, "helper");
+ }
+
+ private String getServiceName() {
+ return serviceName.get();
+ }
+
+ private void setAttribute(final Attributes attributes) {
+ this.serviceName.compareAndSet(null, attributes.get(GrpcAttributeUtils.appName()).toString());
+ }
+
+ @Override
+ public void handleResolvedAddresses(final ResolvedAddresses resolvedAddresses) {
+ setAttribute(resolvedAddresses.getAttributes());
+ Set currentAddrs = subchannels.keySet();
+ Map latestAddrs = stripAttrs(resolvedAddresses.getAddresses());
+ Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet());
+ for (Map.Entry latestEntry : latestAddrs.entrySet()) {
+ EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey();
+ EquivalentAddressGroup originalAddressGroup = latestEntry.getValue();
+ Subchannel subchannel;
+ Subchannel existingSubchannel = subchannels.get(strippedAddressGroup);
+ if (Objects.nonNull(existingSubchannel)) {
+ subchannel = existingSubchannel;
+ SubChannels.updateAttributes(existingSubchannel, originalAddressGroup.getAttributes());
+ } else {
+ subchannel = SubChannels.createSubChannel(helper, strippedAddressGroup, originalAddressGroup.getAttributes());
+ subchannel.start(state -> processSubchannelState(subchannel, state));
+ subchannels.put(strippedAddressGroup, subchannel);
+ }
+ subchannel.requestConnection();
+ }
+ List removedSubchannels = new ArrayList<>();
+ for (EquivalentAddressGroup addressGroup : removedAddrs) {
+ removedSubchannels.add(subchannels.remove(addressGroup));
+ }
+ updateBalancingState();
+ for (Subchannel removedSubchannel : removedSubchannels) {
+ shutdownSubchannel(removedSubchannel);
+ }
+ }
+
+ private void processSubchannelState(final Subchannel subchannel, final ConnectivityStateInfo stateInfo) {
+ if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) {
+ return;
+ }
+ if (stateInfo.getState() == IDLE) {
+ subchannel.requestConnection();
+ log.info("AbstractLoadBalancer.handleSubchannelState, current state:IDLE, subchannel.requestConnection().");
+ }
+ final ConnectivityStateInfo originStateInfo = SubChannels.getStateInfo(subchannel);
+ if (originStateInfo.getState().equals(TRANSIENT_FAILURE)) {
+ if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) {
+ return;
+ }
+ }
+ SubChannels.setStateInfo(subchannel, stateInfo);
+ updateBalancingState();
+ }
+
+ private Map stripAttrs(final List groupList) {
+ Map addrs = new HashMap<>(groupList.size() * 2);
+ for (EquivalentAddressGroup group : groupList) {
+ addrs.put(stripAttrs(group), group);
+ }
+ return addrs;
+ }
+
+ private static EquivalentAddressGroup stripAttrs(final EquivalentAddressGroup eag) {
+ return new EquivalentAddressGroup(eag.getAddresses());
+ }
+
+ private Set setsDifference(final Set a, final Set b) {
+ Set aCopy = new HashSet<>(a);
+ aCopy.removeAll(b);
+ return aCopy;
+ }
+
+ @Override
+ public void shutdown() {
+ for (Subchannel subchannel : subchannels.values()) {
+ shutdownSubchannel(subchannel);
+ }
+ }
+
+ private void shutdownSubchannel(final Subchannel subchannel) {
+ subchannel.shutdown();
+ SubChannels.setStateInfo(subchannel, ConnectivityStateInfo.forNonError(SHUTDOWN));
+ }
+
+ @Override
+ public void handleNameResolutionError(final Status error) {
+ updateBalancingState(TRANSIENT_FAILURE,
+ currentPicker instanceof AbstractReadyPicker ? currentPicker : new EmptyPicker(error));
+ }
+
+ /**
+ * Updates picker with the list of active subchannels (state == READY).
+ */
+ private void updateBalancingState() {
+ final List activeList = subchannels.values()
+ .stream()
+ .filter(r -> SubChannels.getStateInfo(r).getState() == READY)
+ .collect(Collectors.toList());
+ if (activeList.isEmpty()) {
+ // No READY subchannels
+ boolean isConnecting = false;
+ Status aggStatus = EMPTY_OK;
+ for (Subchannel subchannel : getSubchannels()) {
+ ConnectivityStateInfo stateInfo = SubChannels.getStateInfo(subchannel);
+ if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) {
+ isConnecting = true;
+ }
+ if (aggStatus == EMPTY_OK || !aggStatus.isOk()) {
+ aggStatus = stateInfo.getStatus();
+ }
+ }
+ updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, new EmptyPicker(aggStatus));
+ } else {
+ updateBalancingState(READY, newPicker(new ArrayList<>(subchannels.values())));
+ }
+ }
+
+ private void updateBalancingState(final ConnectivityState state, final AbstractPicker picker) {
+ if (state == currentState && picker.isEquivalentTo(currentPicker)) {
+ return;
+ }
+ helper.updateBalancingState(state, picker);
+ currentState = state;
+ currentPicker = picker;
+ log.info("AbstractPicker update, serviceName:{}, all subchannels:{}, state:{}", serviceName, picker.getSubchannelsInfo(), state);
+ }
+
+ private Collection getSubchannels() {
+ return subchannels.values();
+ }
+
+ /**
+ * Create new picker.
+ *
+ * @param list all subchannels
+ * @return ReadyPicker
+ */
+ protected abstract AbstractReadyPicker newPicker(List list);
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/GrpcAttributeUtils.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/GrpcAttributeUtils.java
new file mode 100644
index 000000000000..069baa86c386
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/GrpcAttributeUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance;
+
+import io.grpc.Attributes;
+
+/**
+ * GrpcAttributeUtils.
+ *
+ * @author zhanglei
+ */
+public class GrpcAttributeUtils {
+
+ /**
+ * The soul instance appname.
+ */
+ private static Attributes.Key appName = Attributes.Key.create("appName");
+
+ /**
+ * AppName.
+ *
+ * @return key
+ */
+ public static Attributes.Key appName() {
+ return appName;
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/LoadBalancerStrategy.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/LoadBalancerStrategy.java
new file mode 100644
index 000000000000..4be818fc8d36
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/LoadBalancerStrategy.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance;
+
+import lombok.Getter;
+
+/**
+ * LoadBalancerStrategy.
+ *
+ * @author zhanglei
+ */
+public enum LoadBalancerStrategy {
+
+ Random("random"),
+ RoundRobin("round-robin");
+
+ @Getter
+ private final String strategy;
+
+ LoadBalancerStrategy(final String strategy) {
+ this.strategy = strategy;
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RandomLoadBalancerProvider.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RandomLoadBalancerProvider.java
new file mode 100644
index 000000000000..f676749856f9
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RandomLoadBalancerProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance;
+
+import io.grpc.LoadBalancer;
+import io.grpc.LoadBalancerProvider;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.plugin.grpc.loadbalance.picker.AbstractReadyPicker;
+import org.dromara.soul.plugin.grpc.loadbalance.picker.RandomPicker;
+
+import java.util.List;
+
+/**
+ * RandomLoadBalancerProvider.
+ *
+ * @author raphael
+ */
+@Slf4j
+public class RandomLoadBalancerProvider extends LoadBalancerProvider {
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public int getPriority() {
+ return 6;
+ }
+
+ @Override
+ public String getPolicyName() {
+ return LoadBalancerStrategy.Random.getStrategy();
+ }
+
+ @Override
+ public LoadBalancer newLoadBalancer(final LoadBalancer.Helper helper) {
+ return new AbstractLoadBalancer(helper) {
+ @Override
+ protected AbstractReadyPicker newPicker(final List list) {
+ return new RandomPicker(list);
+ }
+ };
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RoundRobinLoadBalancerProvider.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RoundRobinLoadBalancerProvider.java
new file mode 100644
index 000000000000..5bf0965ddf3e
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/RoundRobinLoadBalancerProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance;
+
+import io.grpc.LoadBalancer;
+import io.grpc.LoadBalancerProvider;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.plugin.grpc.loadbalance.picker.AbstractReadyPicker;
+import org.dromara.soul.plugin.grpc.loadbalance.picker.RoundRobinPicker;
+
+import java.util.List;
+
+/**
+ * RoundRobinLoadBalancerProvider.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public class RoundRobinLoadBalancerProvider extends LoadBalancerProvider {
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public int getPriority() {
+ return 6;
+ }
+
+ @Override
+ public String getPolicyName() {
+ return LoadBalancerStrategy.RoundRobin.getStrategy();
+ }
+
+ @Override
+ public LoadBalancer newLoadBalancer(final LoadBalancer.Helper helper) {
+ return new AbstractLoadBalancer(helper) {
+ @Override
+ protected AbstractReadyPicker newPicker(final List list) {
+ return new RoundRobinPicker(list);
+ }
+ };
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubChannels.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubChannels.java
new file mode 100644
index 000000000000..d636ffa88ce5
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubChannels.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance;
+
+import io.grpc.Attributes;
+import io.grpc.ConnectivityStateInfo;
+import io.grpc.EquivalentAddressGroup;
+import io.grpc.LoadBalancer;
+import io.grpc.ConnectivityState;
+import lombok.Data;
+
+/**
+ * The grpc SubChannels.
+ *
+ * @author zhanglei
+ */
+public class SubChannels {
+
+ private static final Attributes.Key> STATE_INFO_KEY = Attributes.Key.create("state-info");
+
+ private static final Attributes.Key> WEIGHT_KEY = Attributes.Key.create("weight");
+
+ /**
+ * CreateSubChannel.
+ *
+ * @param helper helper
+ * @param addressGroup addressGroup
+ * @param attributes attributes
+ * @return LoadBalancer.Subchannel subchannel
+ */
+ public static LoadBalancer.Subchannel createSubChannel(final LoadBalancer.Helper helper,
+ final EquivalentAddressGroup addressGroup,
+ final Attributes attributes) {
+ final Attributes newAttributes = attributes.toBuilder()
+ .set(STATE_INFO_KEY, new Ref<>(ConnectivityStateInfo.forNonError(ConnectivityState.IDLE)))
+ .build();
+ return helper.createSubchannel(LoadBalancer.CreateSubchannelArgs
+ .newBuilder()
+ .setAddresses(addressGroup)
+ .setAttributes(newAttributes)
+ .build()
+ );
+ }
+
+ /**
+ * Create Attributes.
+ *
+ * @param weight weight
+ * @return Attributes attributes
+ */
+ public static Attributes createAttributes(final int weight) {
+ return Attributes.newBuilder()
+ .set(WEIGHT_KEY, new Ref<>(weight))
+ .build();
+ }
+
+ /**
+ * Get weight. weight
+ *
+ * @param subchannel subchannel
+ * @return int i
+ */
+ public static int getWeight(final LoadBalancer.Subchannel subchannel) {
+ return getAttributeValue(subchannel, WEIGHT_KEY, 0);
+ }
+
+ /**
+ * Get ConnectivityStateInfo.
+ *
+ * @param subchannel subchannel
+ * @return ConnectivityStateInfo info
+ */
+ public static ConnectivityStateInfo getStateInfo(final LoadBalancer.Subchannel subchannel) {
+ return getAttributeValue(subchannel, STATE_INFO_KEY, null);
+ }
+
+ /**
+ * SetStateInfo.
+ *
+ * @param subchannel subchannel
+ * @param value value
+ */
+ public static void setStateInfo(final LoadBalancer.Subchannel subchannel, final ConnectivityStateInfo value) {
+ setAttributeValue(subchannel, STATE_INFO_KEY, value);
+ }
+
+ private static T getAttributeValue(final LoadBalancer.Subchannel subchannel, final Attributes.Key> key, final T defaultValue) {
+ final Ref ref = subchannel.getAttributes().get(key);
+ return ref == null ? defaultValue : ref.value;
+ }
+
+ private static void setAttributeValue(final LoadBalancer.Subchannel subchannel, final Attributes.Key> key, final T newValue) {
+ final Ref targetRef = subchannel.getAttributes().get(key);
+ if (targetRef != null) {
+ targetRef.value = newValue;
+ }
+ }
+
+ /**
+ * Set AttributeValue.
+ *
+ * @param subchannel subchannel
+ * @param key key
+ * @param newAttributes newAttributes
+ * @param t
+ */
+ private static void setAttributeValue(final LoadBalancer.Subchannel subchannel, final Attributes.Key> key, final Attributes newAttributes) {
+ final Ref newValueRef = newAttributes.get(key);
+ if (newValueRef != null) {
+ setAttributeValue(subchannel, key, newValueRef.value);
+ }
+ }
+
+ /**
+ * UpdateAttributes.
+ *
+ * @param subchannel newAttributes
+ * @param attributes attributes
+ */
+ public static void updateAttributes(final LoadBalancer.Subchannel subchannel, final Attributes attributes) {
+ setAttributeValue(subchannel, WEIGHT_KEY, attributes);
+ }
+
+ @Data
+ static final class Ref {
+
+ private T value;
+
+ Ref(final T value) {
+ this.value = value;
+ }
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubchannelCopy.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubchannelCopy.java
new file mode 100644
index 000000000000..3f178b27c337
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/SubchannelCopy.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance;
+
+import io.grpc.ConnectivityStateInfo;
+import io.grpc.EquivalentAddressGroup;
+import io.grpc.LoadBalancer;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+
+/**
+ * SubchannelCopy.
+ *
+ * @author zhanglei
+ */
+@EqualsAndHashCode
+@Getter
+public class SubchannelCopy {
+
+ private final int weight;
+
+ private final LoadBalancer.Subchannel channel;
+
+ private final EquivalentAddressGroup addressGroup;
+
+ private final ConnectivityStateInfo state;
+
+ public SubchannelCopy(final LoadBalancer.Subchannel channel) {
+ this.channel = channel;
+ this.addressGroup = channel.getAddresses();
+ this.weight = SubChannels.getWeight(channel);
+ this.state = SubChannels.getStateInfo(channel);
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractPicker.java
new file mode 100644
index 000000000000..96384e2e337c
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractPicker.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance.picker;
+
+import io.grpc.LoadBalancer;
+
+/**
+ * Picker abstract.
+ *
+ * @author zhanglei
+ */
+public abstract class AbstractPicker extends LoadBalancer.SubchannelPicker {
+
+ /**
+ * The target picker is equivalent to this.
+ *
+ * @param picker target picker
+ * @return picker is equivalent
+ */
+ public abstract boolean isEquivalentTo(AbstractPicker picker);
+
+ /**
+ * Get the target subChannels.
+ *
+ * @return subChannels infos
+ */
+ public abstract String getSubchannelsInfo();
+}
+
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractReadyPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractReadyPicker.java
new file mode 100644
index 000000000000..c009946b5db4
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/AbstractReadyPicker.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance.picker;
+
+import io.grpc.ConnectivityState;
+import io.grpc.LoadBalancer;
+import io.grpc.Status;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.dromara.soul.plugin.grpc.loadbalance.SubchannelCopy;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+
+/**
+ * The AbstractReadyPicker result.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public abstract class AbstractReadyPicker extends AbstractPicker implements Picker {
+
+ private final boolean hasIdleNode;
+
+ private final List list;
+
+ AbstractReadyPicker(final List list) {
+ this.list = list.stream().map(SubchannelCopy::new).collect(Collectors.toList());
+ this.hasIdleNode = hasIdleNode();
+ }
+
+ private boolean hasIdleNode() {
+ return this.list.stream().anyMatch(r -> {
+ final ConnectivityState state = r.getState().getState();
+ return state == ConnectivityState.IDLE || state == ConnectivityState.CONNECTING;
+ });
+ }
+
+ @Override
+ public LoadBalancer.PickResult pickSubchannel(final LoadBalancer.PickSubchannelArgs args) {
+ final List list = getSubchannels();
+ if (CollectionUtils.isEmpty(list)) {
+ return getErrorPickResult();
+ }
+ SubchannelCopy channel = pick(getSubchannels());
+ return channel == null ? getErrorPickResult() : LoadBalancer.PickResult.withSubchannel(channel.getChannel());
+ }
+
+ /**
+ * Choose subChannel.
+ *
+ * @param list subChannel list
+ * @return result subChannel
+ */
+ protected abstract SubchannelCopy pick(List list);
+
+ @Override
+ public List getSubchannels() {
+ return list.stream().filter(r -> {
+ final ConnectivityState state = r.getState().getState();
+ return state == ConnectivityState.READY;
+ }).collect(Collectors.toList());
+ }
+
+ private LoadBalancer.PickResult getErrorPickResult() {
+ if (hasIdleNode) {
+ return LoadBalancer.PickResult.withNoResult();
+ } else {
+ return LoadBalancer.PickResult.withError(Status.UNAVAILABLE.withCause(new NoSuchElementException()).withDescription("can not find the subChannel")
+ );
+ }
+ }
+
+ @Override
+ public boolean isEquivalentTo(final AbstractPicker picker) {
+ if (!(picker instanceof AbstractReadyPicker)) {
+ return false;
+ }
+ AbstractReadyPicker other = (AbstractReadyPicker) picker;
+ // the lists cannot contain duplicate subchannels
+ return other == this || (list.size() == other.list.size()
+ && new HashSet<>(list).containsAll(other.list));
+ }
+
+ @Override
+ public String getSubchannelsInfo() {
+ final List infos = this.list.stream().map(r -> "Subchannel"
+ + "{ weight=" + r.getWeight()
+ + ", readyState=\"" + r.getState().toString() + "\""
+ + ", address=\"" + r.getChannel().getAddresses() + "\""
+ + "}")
+ .collect(Collectors.toList());
+ return "[ " + String.join(",", infos) + " ]";
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/EmptyPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/EmptyPicker.java
new file mode 100644
index 000000000000..23a803ca475d
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/EmptyPicker.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance.picker;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import io.grpc.LoadBalancer;
+import io.grpc.Status;
+
+/**
+ * The empty picker.
+ *
+ * @author zhanglei
+ */
+public class EmptyPicker extends AbstractPicker {
+
+ private final Status status;
+
+ public EmptyPicker(final Status status) {
+ this.status = Preconditions.checkNotNull(status, "status");
+ }
+
+ @Override
+ public LoadBalancer.PickResult pickSubchannel(final LoadBalancer.PickSubchannelArgs args) {
+ return status.isOk() ? LoadBalancer.PickResult.withNoResult() : LoadBalancer.PickResult.withError(status);
+ }
+
+ @Override
+ public boolean isEquivalentTo(final AbstractPicker picker) {
+ return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) || (status.isOk() && ((EmptyPicker) picker).status.isOk()));
+ }
+
+ @Override
+ public String getSubchannelsInfo() {
+ return "[]";
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/Picker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/Picker.java
new file mode 100644
index 000000000000..de3c417cd06b
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/Picker.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance.picker;
+
+import org.dromara.soul.plugin.grpc.loadbalance.SubchannelCopy;
+
+import java.util.List;
+
+/**
+ * Picker.
+ *
+ * @author zhanglei
+ */
+public interface Picker {
+
+ /**
+ * get channels.
+ *
+ * @return List list
+ */
+ List getSubchannels();
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RandomPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RandomPicker.java
new file mode 100644
index 000000000000..9487fbafec8c
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RandomPicker.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance.picker;
+
+import io.grpc.LoadBalancer;
+import org.apache.commons.collections4.CollectionUtils;
+import org.dromara.soul.plugin.grpc.loadbalance.SubchannelCopy;
+
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * RandomPicker.
+ *
+ * @author zhanglei
+ */
+public class RandomPicker extends AbstractReadyPicker {
+
+ public RandomPicker(final List list) {
+ super(list);
+ }
+
+ @Override
+ protected SubchannelCopy pick(final List list) {
+ if (CollectionUtils.isEmpty(list)) {
+ return null;
+ }
+ if (list.size() == 1) {
+ return list.get(0);
+ }
+ int index = getRandomIndexByWeight(list);
+ return list.get(index);
+ }
+
+ private int getRandomIndexByWeight(final List list) {
+ final int sumWeight = list.stream().mapToInt(SubchannelCopy::getWeight).sum();
+ if (sumWeight <= 0) {
+ return ThreadLocalRandom.current().nextInt(list.size());
+ }
+ int randomInt = ThreadLocalRandom.current().nextInt(sumWeight);
+ int sumI = 0;
+ for (int i = 0; i < list.size(); i++) {
+ sumI += list.get(i).getWeight();
+ if (randomInt < sumI) {
+ return i;
+ }
+ }
+ return list.size() - 1;
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RoundRobinPicker.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RoundRobinPicker.java
new file mode 100644
index 000000000000..afed7591026a
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/loadbalance/picker/RoundRobinPicker.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.loadbalance.picker;
+
+import io.grpc.LoadBalancer;
+import org.apache.commons.collections4.CollectionUtils;
+import org.dromara.soul.plugin.grpc.loadbalance.SubchannelCopy;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+
+/**
+ * RoundRobin picker.
+ *
+ * @author zhanglei
+ */
+public class RoundRobinPicker extends AbstractReadyPicker {
+
+ private static final AtomicIntegerFieldUpdater INDEX_UPDATER = AtomicIntegerFieldUpdater.newUpdater(RoundRobinPicker.class, "index");
+
+ /**
+ * AtomicIntegerFieldUpdater index.
+ */
+ @SuppressWarnings("unused")
+ private volatile Integer index = 0;
+
+ public RoundRobinPicker(final List list) {
+ super(list);
+ }
+
+ @Override
+ protected SubchannelCopy pick(final List list) {
+ if (CollectionUtils.isEmpty(list)) {
+ return null;
+ }
+ final int size = list.size();
+ if (size == 1) {
+ return list.get(0);
+ }
+ int i = INDEX_UPDATER.incrementAndGet(this);
+ if (i >= size) {
+ int oldi = i;
+ i %= size;
+ INDEX_UPDATER.compareAndSet(this, oldi, i);
+ }
+ return list.get(i);
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/param/BodyParamPlugin.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/param/BodyParamPlugin.java
new file mode 100644
index 000000000000..9ad74a44547b
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/param/BodyParamPlugin.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.param;
+
+import org.dromara.soul.common.constant.Constants;
+import org.dromara.soul.common.enums.PluginEnum;
+import org.dromara.soul.common.enums.RpcTypeEnum;
+import org.dromara.soul.common.utils.HttpParamConverter;
+import org.dromara.soul.plugin.api.SoulPlugin;
+import org.dromara.soul.plugin.api.SoulPluginChain;
+import org.dromara.soul.plugin.api.context.SoulContext;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.HttpMessageReader;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.web.reactive.function.server.HandlerStrategies;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The type Body param plugin.
+ *
+ * @author zhanglei
+ */
+public class BodyParamPlugin implements SoulPlugin {
+
+ private final List> messageReaders;
+
+ /**
+ * Instantiates a new Body param plugin.
+ */
+ public BodyParamPlugin() {
+ this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
+ }
+
+ @Override
+ public Mono execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
+ final ServerHttpRequest request = exchange.getRequest();
+ final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
+ if (Objects.nonNull(soulContext) && RpcTypeEnum.GRPC.getName().equals(soulContext.getRpcType())) {
+ MediaType mediaType = request.getHeaders().getContentType();
+ ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
+ if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
+ return body(exchange, serverRequest, chain);
+ }
+ if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) {
+ return formData(exchange, serverRequest, chain);
+ }
+ return query(exchange, serverRequest, chain);
+ }
+ return chain.execute(exchange);
+ }
+
+ @Override
+ public int getOrder() {
+ return PluginEnum.GRPC.getCode() - 1;
+ }
+
+ @Override
+ public String named() {
+ return PluginEnum.GRPC.getName();
+ }
+
+ private Mono body(final ServerWebExchange exchange, final ServerRequest serverRequest, final SoulPluginChain chain) {
+ return serverRequest.bodyToMono(String.class)
+ .switchIfEmpty(Mono.defer(() -> Mono.just("")))
+ .flatMap(body -> {
+ exchange.getAttributes().put(Constants.GRPC_PARAMS, body);
+ return chain.execute(exchange);
+ });
+ }
+
+ private Mono formData(final ServerWebExchange exchange, final ServerRequest serverRequest, final SoulPluginChain chain) {
+ return serverRequest.formData()
+ .switchIfEmpty(Mono.defer(() -> Mono.just(new LinkedMultiValueMap<>())))
+ .flatMap(map -> {
+ exchange.getAttributes().put(Constants.GRPC_PARAMS, HttpParamConverter.toMap(() -> map));
+ return chain.execute(exchange);
+ });
+ }
+
+ private Mono query(final ServerWebExchange exchange, final ServerRequest serverRequest, final SoulPluginChain chain) {
+ exchange.getAttributes().put(Constants.GRPC_PARAMS,
+ HttpParamConverter.ofString(() -> serverRequest.uri().getQuery()));
+ return chain.execute(exchange);
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompleteObserver.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompleteObserver.java
new file mode 100644
index 000000000000..764ce3effcc7
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompleteObserver.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.proto;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import io.grpc.stub.StreamObserver;
+
+/**
+ * A holding a future which completes when the rpc terminates.
+ *
+ * @author zhanglei
+ */
+public class CompleteObserver implements StreamObserver {
+
+ private final SettableFuture doneFuture;
+
+ public CompleteObserver() {
+ this.doneFuture = SettableFuture.create();
+ }
+
+ @Override
+ public synchronized void onCompleted() {
+ doneFuture.set(null);
+ }
+
+ @Override
+ public synchronized void onError(final Throwable t) {
+ doneFuture.setException(t);
+ }
+
+ @Override
+ public void onNext(final T next) {
+ }
+
+ /**
+ * Returns a future which completes when the rpc finishes. The returned future fails if the rpc fails.
+ *
+ * @return ListenableFuture future
+ */
+ public ListenableFuture getCompletionFuture() {
+ return doneFuture;
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompositeStreamObserver.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompositeStreamObserver.java
new file mode 100644
index 000000000000..5b3c900e0bbd
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/CompositeStreamObserver.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.proto;
+
+import com.google.common.collect.ImmutableList;
+import io.grpc.stub.StreamObserver;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * A which groups multiple observers and executes them all.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public final class CompositeStreamObserver implements StreamObserver {
+
+ private final ImmutableList> observers;
+
+ private CompositeStreamObserver(final ImmutableList> observers) {
+ this.observers = observers;
+ }
+
+ /**
+ * CompositeStreamObserver of.
+ *
+ * @param observers observers
+ * @param completeObserver completeObserver
+ * @param t
+ * @return CompositeStreamObserver compositeStreamObserver
+ */
+ public static CompositeStreamObserver of(final StreamObserver observers,
+ final CompleteObserver completeObserver) {
+ return new CompositeStreamObserver<>(ImmutableList.of(observers, completeObserver));
+ }
+
+ @Override
+ public void onCompleted() {
+ for (StreamObserver observer : observers) {
+ try {
+ observer.onCompleted();
+ } catch (Exception t) {
+ log.error("Exception in composite onComplete, moving on", t);
+ }
+ }
+ }
+
+ @Override
+ public void onError(final Throwable t) {
+ for (StreamObserver observer : observers) {
+ try {
+ observer.onError(t);
+ } catch (Exception exception) {
+ log.error("Exception in composite onError, moving on", exception);
+ }
+ }
+ }
+
+ @Override
+ public void onNext(final T value) {
+ for (StreamObserver observer : observers) {
+ try {
+ observer.onNext(value);
+ } catch (Exception exception) {
+ log.error("Exception in composite onNext, moving on", exception);
+ }
+ }
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/DynamicMessageMarshaller.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/DynamicMessageMarshaller.java
new file mode 100644
index 000000000000..a2258760fc61
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/DynamicMessageMarshaller.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.proto;
+
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.ExtensionRegistryLite;
+import io.grpc.MethodDescriptor.Marshaller;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Dynamic messages.
+ *
+ * @author zhanglei
+ */
+public class DynamicMessageMarshaller implements Marshaller {
+
+ private final Descriptor messageDescriptor;
+
+ public DynamicMessageMarshaller(final Descriptor messageDescriptor) {
+ this.messageDescriptor = messageDescriptor;
+ }
+
+ @Override
+ public DynamicMessage parse(final InputStream inputStream) {
+ try {
+ return DynamicMessage.newBuilder(messageDescriptor)
+ .mergeFrom(inputStream, ExtensionRegistryLite.getEmptyRegistry())
+ .build();
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to merge from the supplied input stream", e);
+ }
+ }
+
+ @Override
+ public InputStream stream(final DynamicMessage abstractMessage) {
+ return abstractMessage.toByteString().newInput();
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/MessageWriter.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/MessageWriter.java
new file mode 100644
index 000000000000..5ffa76c7eb43
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/MessageWriter.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.proto;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Message;
+import com.google.protobuf.util.JsonFormat;
+import io.grpc.stub.StreamObserver;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * MessageWriter.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public final class MessageWriter implements StreamObserver {
+
+ private final JsonFormat.Printer printer;
+
+ private final SoulGrpcResponse results;
+
+ private MessageWriter(final JsonFormat.Printer printer, final SoulGrpcResponse results) {
+ this.printer = printer;
+ this.results = results;
+ }
+
+ /**
+ * New instance.
+ *
+ * @param registry registry
+ * @param results results
+ * @param t
+ * @return message message
+ */
+ public static MessageWriter newInstance(final JsonFormat.TypeRegistry registry, final SoulGrpcResponse results) {
+ return new MessageWriter<>(JsonFormat.printer().usingTypeRegistry(registry), results);
+ }
+
+ @Override
+ public void onNext(final T value) {
+ try {
+ results.setResult(printer.print(value));
+ } catch (InvalidProtocolBufferException e) {
+ log.error("Skipping invalid response message", e);
+ }
+ }
+
+ @Override
+ public void onError(final Throwable t) {
+ log.error("Messages write occur errors", t);
+ }
+
+ @Override
+ public void onCompleted() {
+ log.info("Messages write complete");
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcCallRequest.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcCallRequest.java
new file mode 100644
index 000000000000..f8fb63ad3a8e
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcCallRequest.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.proto;
+
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.DynamicMessage;
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.stub.StreamObserver;
+import lombok.Builder;
+import lombok.Getter;
+
+/**
+ * SoulGrpcCallRequest.
+ *
+ * @author zhanglei
+ */
+@Builder
+@Getter
+public class SoulGrpcCallRequest {
+
+ private Channel channel;
+
+ private CallOptions callOptions;
+
+ private DynamicMessage requests;
+
+ private Descriptors.MethodDescriptor methodDescriptor;
+
+ private StreamObserver responseObserver;
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcRequest.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcRequest.java
new file mode 100644
index 000000000000..35e724d4a34b
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcRequest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.proto;
+
+import com.google.protobuf.Descriptors.MethodDescriptor;
+import com.google.protobuf.DynamicMessage;
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.stub.StreamObserver;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.List;
+
+/**
+ * SoulGrpcRequest.
+ *
+ * @author zhanglei
+ */
+@Builder
+@Getter
+public class SoulGrpcRequest {
+
+ private MethodDescriptor methodDescriptor;
+
+ private Channel channel;
+
+ private CallOptions callOptions;
+
+ private List requests;
+
+ private StreamObserver responseObserver;
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcResponse.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcResponse.java
new file mode 100644
index 000000000000..55d10d3c030d
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/proto/SoulGrpcResponse.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.proto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * SoulGrpcResponse.
+ *
+ * @author zhangjikai
+ */
+@Data
+public class SoulGrpcResponse implements Serializable {
+ private String result;
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/LookupServiceHandler.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/LookupServiceHandler.java
new file mode 100644
index 000000000000..2944bce91699
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/LookupServiceHandler.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.reflection;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.grpc.reflection.v1alpha.ServerReflectionRequest;
+import io.grpc.reflection.v1alpha.ServerReflectionResponse;
+import io.grpc.stub.StreamObserver;
+import org.dromara.soul.common.exception.SoulException;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Map;
+
+/**
+ * LookupServiceHandler.
+ *
+ * @author zhanglei
+ */
+public class LookupServiceHandler implements StreamObserver {
+
+ private final String serviceName;
+
+ private final Set requestedDescriptors;
+
+ private final SettableFuture resultFuture;
+
+ private final Map resolvedDescriptors;
+
+ private StreamObserver requestStream;
+
+ private int outstandingRequests;
+
+ public LookupServiceHandler(final String serviceName) {
+ this.serviceName = serviceName;
+ this.resultFuture = SettableFuture.create();
+ this.resolvedDescriptors = new HashMap<>();
+ this.requestedDescriptors = new HashSet<>();
+ this.outstandingRequests = 0;
+ }
+
+ /**
+ * Start the handler.
+ *
+ * @param requestStream stream
+ * @return ListenableFuture future
+ */
+ public ListenableFuture start(final StreamObserver requestStream) {
+ this.requestStream = requestStream;
+ requestStream.onNext(requestForSymbol(serviceName));
+ ++outstandingRequests;
+ return resultFuture;
+ }
+
+ @Override
+ public void onNext(final ServerReflectionResponse response) {
+ ServerReflectionResponse.MessageResponseCase responseCase = response.getMessageResponseCase();
+ switch (responseCase) {
+ case FILE_DESCRIPTOR_RESPONSE:
+ ImmutableSet descriptors =
+ parseDescriptors(response.getFileDescriptorResponse().getFileDescriptorProtoList());
+ descriptors.forEach(d -> resolvedDescriptors.put(d.getName(), d));
+ descriptors.forEach(this::processDependencies);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void onError(final Throwable t) {
+ resultFuture.setException(new RuntimeException("Reflection lookup rpc failed for: " + serviceName, t));
+ }
+
+ @Override
+ public void onCompleted() {
+ if (!resultFuture.isDone()) {
+ resultFuture.setException(new RuntimeException("Unexpected end of rpc"));
+ }
+ }
+
+ private ImmutableSet parseDescriptors(final List descriptorBytes) {
+ ImmutableSet.Builder resultBuilder = ImmutableSet.builder();
+ for (ByteString fileDescriptorBytes : descriptorBytes) {
+ try {
+ resultBuilder.add(DescriptorProtos.FileDescriptorProto.parseFrom(fileDescriptorBytes));
+ } catch (InvalidProtocolBufferException e) {
+ throw new SoulException(e);
+ }
+ }
+ return resultBuilder.build();
+ }
+
+ private void processDependencies(final DescriptorProtos.FileDescriptorProto fileDescriptor) {
+ fileDescriptor.getDependencyList().forEach(dep -> {
+ if (!resolvedDescriptors.containsKey(dep) && !requestedDescriptors.contains(dep)) {
+ requestedDescriptors.add(dep);
+ ++outstandingRequests;
+ requestStream.onNext(requestForDescriptor(dep));
+ }
+ });
+
+ --outstandingRequests;
+ if (outstandingRequests == 0) {
+ resultFuture.set(DescriptorProtos.FileDescriptorSet.newBuilder()
+ .addAllFile(resolvedDescriptors.values())
+ .build());
+ requestStream.onCompleted();
+ }
+ }
+
+ private static ServerReflectionRequest requestForDescriptor(final String name) {
+ return ServerReflectionRequest.newBuilder()
+ .setFileByFilename(name)
+ .build();
+ }
+
+ private static ServerReflectionRequest requestForSymbol(final String symbol) {
+ return ServerReflectionRequest.newBuilder()
+ .setFileContainingSymbol(symbol)
+ .build();
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/SoulGrpcReflectionClient.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/SoulGrpcReflectionClient.java
new file mode 100644
index 000000000000..4e13cf6a8c5b
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/reflection/SoulGrpcReflectionClient.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.reflection;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.protobuf.DescriptorProtos;
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+import io.grpc.Channel;
+import io.grpc.MethodDescriptor;
+import io.grpc.reflection.v1alpha.ServerReflectionGrpc;
+import io.grpc.reflection.v1alpha.ServerReflectionRequest;
+import io.grpc.stub.StreamObserver;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.common.exception.SoulException;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static io.grpc.MethodDescriptor.generateFullMethodName;
+
+/**
+ * The Soul Grpc Reflection Client.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public final class SoulGrpcReflectionClient {
+
+ private final Channel channel;
+
+ /**
+ * key : package + service name.
+ * value : proro file.
+ */
+ private final Map fileDescriptorCache = new ConcurrentHashMap<>();
+
+ private SoulGrpcReflectionClient(final Channel channel) {
+ this.channel = channel;
+ }
+
+ /**
+ * A new reflection client using the supplied channel.
+ *
+ * @param channel channel
+ * @return SoulGrpcReflectionClient client
+ */
+ public static SoulGrpcReflectionClient create(final Channel channel) {
+ return new SoulGrpcReflectionClient(channel);
+ }
+
+ /**
+ * Get Grpc service by the remote server.
+ *
+ * @param serviceName serviceName
+ * @return ListenableFuture future
+ */
+ public ListenableFuture lookupService(final String serviceName) {
+ LookupServiceHandler rpcHandler = new LookupServiceHandler(serviceName);
+ StreamObserver requestStream = ServerReflectionGrpc.newStub(channel)
+ .withDeadlineAfter(10000, TimeUnit.MILLISECONDS)
+ .serverReflectionInfo(rpcHandler);
+ return rpcHandler.start(requestStream);
+ }
+
+ /**
+ * Resolve services.
+ *
+ * @param serviceName serviceName
+ * @return FileDescriptorSet fleDescriptorSet
+ */
+ public DescriptorProtos.FileDescriptorSet resolveService(final String serviceName) {
+ return fileDescriptorCache.computeIfAbsent(serviceName, k -> {
+ try {
+ return this.lookupService(k).get();
+ } catch (InterruptedException e) {
+ log.error("Resolve services get error", e);
+ throw new SoulException(e);
+ } catch (ExecutionException e) {
+ log.error("Resolve services get error", e);
+ throw new SoulException(e);
+ }
+ });
+ }
+
+ /**
+ * Fetch full method name.
+ *
+ * @param methodDescriptor methodDescriptor
+ * @return String str
+ */
+ public String fetchFullMethodName(final Descriptors.MethodDescriptor methodDescriptor) {
+ String serviceName = methodDescriptor.getService().getFullName();
+ String methodName = methodDescriptor.getName();
+ return generateFullMethodName(serviceName, methodName);
+ }
+
+ /**
+ * Fetch method type.
+ *
+ * @param methodDescriptor methodDescriptor
+ * @return MethodType
+ */
+ public MethodDescriptor.MethodType fetchMethodType(final Descriptors.MethodDescriptor methodDescriptor) {
+ boolean clientStreaming = methodDescriptor.toProto().getClientStreaming();
+ boolean serverStreaming = methodDescriptor.toProto().getServerStreaming();
+ if (clientStreaming && serverStreaming) {
+ return MethodDescriptor.MethodType.BIDI_STREAMING;
+ } else if (!clientStreaming && !serverStreaming) {
+ return MethodDescriptor.MethodType.UNARY;
+ } else if (!clientStreaming) {
+ return MethodDescriptor.MethodType.SERVER_STREAMING;
+ } else {
+ return MethodDescriptor.MethodType.SERVER_STREAMING;
+ }
+ }
+
+ /**
+ * Parse message.
+ *
+ * @param registry registry
+ * @param descriptor descriptor
+ * @param jsons jsons
+ * @return DynamicMessage
+ */
+ public DynamicMessage parseToMessages(final JsonFormat.TypeRegistry registry, final Descriptors.Descriptor descriptor, final String jsons) {
+ JsonFormat.Parser parser = JsonFormat.parser().usingTypeRegistry(registry);
+ try {
+ DynamicMessage.Builder messageBuilder = DynamicMessage.newBuilder(descriptor);
+ parser.merge(jsons, messageBuilder);
+ return messageBuilder.build();
+ } catch (InvalidProtocolBufferException e) {
+ throw new IllegalArgumentException("Unable to parse json text", e);
+ }
+ }
+
+ /**
+ * Get FileDescriptor cache.
+ *
+ * @return map
+ */
+ public Map getFileDescriptorCache() {
+ return fileDescriptorCache;
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/ServiceResolver.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/ServiceResolver.java
new file mode 100644
index 000000000000..fe51a1ff0b0c
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/ServiceResolver.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.resolver;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
+import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.Descriptors.Descriptor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.soul.common.dto.MetaData;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Read proto file descriptors and extract method definitions.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public final class ServiceResolver {
+
+ private final ImmutableList fileDescriptors;
+
+ private ServiceResolver(final Iterable fileDescriptors) {
+ this.fileDescriptors = ImmutableList.copyOf(fileDescriptors);
+ }
+
+ /**
+ * Creates a resolver.
+ *
+ * @param descriptorSet descriptorSet
+ * @return ServiceResolver serviceResolver
+ */
+ public static ServiceResolver fromFileDescriptorSet(final FileDescriptorSet descriptorSet) {
+ ImmutableMap descriptorProtoIndex =
+ computeDescriptorProtoIndex(descriptorSet);
+ Map descriptorCache = new HashMap<>();
+
+ ImmutableList.Builder result = ImmutableList.builder();
+ for (FileDescriptorProto descriptorProto : descriptorSet.getFileList()) {
+ try {
+ result.add(descriptorFromProto(descriptorProto, descriptorProtoIndex, descriptorCache));
+ } catch (Descriptors.DescriptorValidationException e) {
+ log.warn("Skipped descriptor " + descriptorProto.getName() + " due to error", e);
+ }
+ }
+ return new ServiceResolver(result.build());
+ }
+
+ /**
+ * Lists all the known message types.
+ *
+ * @return ImmutableSet set
+ */
+ public ImmutableSet listMessageTypes() {
+ ImmutableSet.Builder resultBuilder = ImmutableSet.builder();
+ fileDescriptors.forEach(d -> resultBuilder.addAll(d.getMessageTypes()));
+ return resultBuilder.build();
+ }
+
+ /**
+ * Resolve service method.
+ *
+ * @param metaData metaData
+ * @return MethodDescriptor
+ */
+ public Descriptors.MethodDescriptor resolveServiceMethod(final MetaData metaData) {
+ String fullServiceName = metaData.getServiceName();
+ String serviceName = fullServiceName.substring(fullServiceName.lastIndexOf(".") + 1);
+ String packageName = fullServiceName.substring(0, fullServiceName.lastIndexOf("."));
+ Descriptors.ServiceDescriptor service = findService(packageName, serviceName);
+ Descriptors.MethodDescriptor method = service.findMethodByName(metaData.getMethodName());
+ if (method == null) {
+ throw new IllegalArgumentException(
+ "Unable to find method " + packageName
+ + " in service " + serviceName);
+ }
+ return method;
+ }
+
+ private Descriptors.ServiceDescriptor findService(final String packageName, final String serviceName) {
+ for (Descriptors.FileDescriptor fileDescriptor : fileDescriptors) {
+ if (!fileDescriptor.getPackage().equals(packageName)) {
+ continue;
+ }
+
+ Descriptors.ServiceDescriptor serviceDescriptor = fileDescriptor.findServiceByName(serviceName);
+ if (serviceDescriptor != null) {
+ return serviceDescriptor;
+ }
+ }
+ throw new IllegalArgumentException("Unable to find service with name: " + serviceName);
+ }
+
+ private static ImmutableMap computeDescriptorProtoIndex(final FileDescriptorSet fileDescriptorSet) {
+ ImmutableMap.Builder resultBuilder = ImmutableMap.builder();
+ for (FileDescriptorProto descriptorProto : fileDescriptorSet.getFileList()) {
+ resultBuilder.put(descriptorProto.getName(), descriptorProto);
+ }
+ return resultBuilder.build();
+ }
+
+ private static Descriptors.FileDescriptor descriptorFromProto(
+ final FileDescriptorProto descriptorProto,
+ final ImmutableMap descriptorProtoIndex,
+ final Map descriptorCache) throws Descriptors.DescriptorValidationException {
+ // First check the cache.
+ String descriptorName = descriptorProto.getName();
+ if (descriptorCache.containsKey(descriptorName)) {
+ return descriptorCache.get(descriptorName);
+ }
+ // Then fetch all the required dependencies recursively.
+ ImmutableList.Builder dependencies = ImmutableList.builder();
+ for (String dependencyName : descriptorProto.getDependencyList()) {
+ if (!descriptorProtoIndex.containsKey(dependencyName)) {
+ throw new IllegalArgumentException("Could not find dependency: " + dependencyName);
+ }
+ FileDescriptorProto dependencyProto = descriptorProtoIndex.get(dependencyName);
+ dependencies.add(descriptorFromProto(dependencyProto, descriptorProtoIndex, descriptorCache));
+ }
+ // Finally construct the actual descriptor.
+ Descriptors.FileDescriptor[] empty = new Descriptors.FileDescriptor[0];
+ return Descriptors.FileDescriptor.buildFrom(descriptorProto, dependencies.build().toArray(empty));
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolver.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolver.java
new file mode 100644
index 000000000000..983229966e90
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolver.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.resolver;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import io.grpc.Attributes;
+import io.grpc.NameResolver;
+import io.grpc.Status;
+import io.grpc.EquivalentAddressGroup;
+import io.grpc.SynchronizationContext;
+import io.grpc.internal.SharedResourceHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.dromara.soul.plugin.grpc.cache.ApplicationConfigCache;
+import org.dromara.soul.plugin.grpc.loadbalance.GrpcAttributeUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * SoulNameResolver.
+ *
+ * @author zhanglei
+ */
+@Slf4j
+public class SoulNameResolver extends NameResolver implements Consumer {
+
+ private boolean resolving;
+
+ private Listener2 listener;
+
+ private Executor executor;
+
+ private final String appName;
+
+ private final Attributes attributes;
+
+ private final SynchronizationContext syncContext;
+
+ private final List keep = null;
+
+ private List instanceList = Lists.newArrayList();
+
+ private final SharedResourceHolder.Resource executorResource;
+
+ public SoulNameResolver(final String appName, final Args args, final SharedResourceHolder.Resource executorResource) {
+ this.appName = appName;
+ this.executor = args.getOffloadExecutor();
+ this.executorResource = executorResource;
+ this.attributes = Attributes.newBuilder().set(GrpcAttributeUtils.appName(), appName).build();
+ this.syncContext = Objects.requireNonNull(args.getSynchronizationContext(), "syncContext");
+ }
+
+ @Override
+ public void start(final Listener2 listener) {
+ Preconditions.checkState(this.listener == null, "already started");
+ this.executor = SharedResourceHolder.get(this.executorResource);
+ this.listener = Preconditions.checkNotNull(listener, "listener");
+ ApplicationConfigCache.getInstance().watch(appName, this);
+ resolve();
+ }
+
+ @Override
+ public void accept(final Object o) {
+ syncContext.execute(() -> {
+ if (this.listener != null) {
+ resolve();
+ }
+ });
+ }
+
+ @Override
+ public void refresh() {
+ Preconditions.checkState(this.listener != null, "not started");
+ resolve();
+ }
+
+ private void resolve() {
+ log.info("Scheduled resolve for {}", this.appName);
+ if (this.resolving) {
+ return;
+ }
+ this.resolving = true;
+ this.executor.execute(new Resolve(this.listener, this.instanceList));
+ }
+
+ @Override
+ public String getServiceAuthority() {
+ return appName;
+ }
+
+ @Override
+ public void shutdown() {
+ this.listener = null;
+ if (this.executor != null) {
+ this.executor = SharedResourceHolder.release(this.executorResource, this.executor);
+ }
+ this.instanceList = Lists.newArrayList();
+ }
+
+ private final class Resolve implements Runnable {
+
+ private final Listener2 savedListener;
+
+ private final List savedInstanceList;
+
+ Resolve(final Listener2 listener, final List instanceList) {
+ this.savedListener = Objects.requireNonNull(listener, "listener");
+ this.savedInstanceList = Objects.requireNonNull(instanceList, "instanceList");
+ }
+
+ @Override
+ public void run() {
+ final AtomicReference> resultContainer = new AtomicReference<>();
+ try {
+ resultContainer.set(resolveInternal());
+ } catch (final Exception e) {
+ this.savedListener.onError(Status.UNAVAILABLE.withCause(e)
+ .withDescription("Failed to update server list for " + SoulNameResolver.this.appName));
+ resultContainer.set(Lists.newArrayList());
+ } finally {
+ SoulNameResolver.this.syncContext.execute(() -> {
+ SoulNameResolver.this.resolving = false;
+ final List newInstanceList = resultContainer.get();
+ if (newInstanceList != keep && SoulNameResolver.this.listener != null) {
+ SoulNameResolver.this.instanceList = newInstanceList;
+ }
+ });
+ }
+ }
+
+ private List resolveInternal() {
+ final String name = SoulNameResolver.this.appName;
+ SoulServiceInstanceLists soulServiceInstanceLists = ApplicationConfigCache.getInstance().get(name);
+ List newInstanceList = soulServiceInstanceLists.getSoulServiceInstances();
+ log.info("Got {} candidate servers for {}", newInstanceList.size(), name);
+ if (CollectionUtils.isEmpty(newInstanceList)) {
+ log.info("No servers found for {}", name);
+ this.savedListener.onError(Status.UNAVAILABLE.withDescription("No servers found for " + name));
+ return Lists.newArrayList();
+ }
+ if (!needsToUpdateConnections(newInstanceList)) {
+ log.info("Nothing has changed... skipping update for {}", name);
+ return keep;
+ }
+ log.info("Ready to update server list for {}", name);
+ final List targets = newInstanceList.stream()
+ .map(instance -> {
+ log.info("Found gRPC server {}:{} for {}", instance.getHost(), instance.getPort(), name);
+ return SoulResolverHelper.convertToEquivalentAddressGroup(instance);
+ }).collect(Collectors.toList());
+ this.savedListener.onResult(ResolutionResult.newBuilder()
+ .setAddresses(targets)
+ .setAttributes(attributes)
+ .build());
+ log.info("Done updating server list for {}", name);
+ return newInstanceList;
+ }
+
+ private boolean needsToUpdateConnections(final List newInstanceList) {
+ if (this.savedInstanceList.size() != newInstanceList.size()) {
+ return true;
+ }
+ for (final SoulServiceInstance instance : this.savedInstanceList) {
+ final String host = instance.getHost();
+ final int port = instance.getPort();
+ boolean isSame = newInstanceList.stream().anyMatch(newInstance -> host.equals(newInstance.getHost())
+ && port == newInstance.getPort()
+ && isMetadataEquals(instance.getMetadata(), newInstance.getMetadata()));
+ if (!isSame) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isMetadataEquals(final Map metadata, final Map newMetadata) {
+ final String[] keys = {"weight"};
+ for (String key : keys) {
+ final String value = metadata.get(key);
+ final String newValue = newMetadata.get(key);
+ if (!Objects.equals(value, newValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolverProvider.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolverProvider.java
new file mode 100644
index 000000000000..5f76563e0449
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulNameResolverProvider.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.resolver;
+
+import io.grpc.NameResolver;
+import io.grpc.NameResolverProvider;
+import io.grpc.internal.GrpcUtil;
+import org.dromara.soul.common.enums.PluginEnum;
+
+import java.net.URI;
+
+/**
+ * SoulNameResolverProvider.
+ *
+ * @author zhanglei
+ */
+public class SoulNameResolverProvider extends NameResolverProvider {
+
+ @Override
+ public NameResolver newNameResolver(final URI targetUri, final NameResolver.Args args) {
+ return new SoulNameResolver(targetUri.getPath(), args, GrpcUtil.SHARED_CHANNEL_EXECUTOR);
+ }
+
+ @Override
+ protected boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ protected int priority() {
+ return 6;
+ }
+
+ @Override
+ public String getDefaultScheme() {
+ return PluginEnum.GRPC.getName();
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulResolverHelper.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulResolverHelper.java
new file mode 100644
index 000000000000..a5067fba0f3c
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulResolverHelper.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.resolver;
+
+import io.grpc.Attributes;
+import io.grpc.EquivalentAddressGroup;
+import org.dromara.soul.plugin.grpc.loadbalance.SubChannels;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * SoulResolverHelper.
+ *
+ * @author zhanglei
+ */
+public class SoulResolverHelper {
+
+ /**
+ * ConvertToEquivalentAddressGroup.
+ *
+ * @param instance instance
+ * @return EquivalentAddressGroup
+ */
+ public static EquivalentAddressGroup convertToEquivalentAddressGroup(final SoulServiceInstance instance) {
+ return new EquivalentAddressGroup(
+ new InetSocketAddress(instance.getHost(), instance.getPort()),
+ createAttributes(instance)
+ );
+ }
+
+ /**
+ * CreateAttributes.
+ *
+ * @param instance instance
+ * @return Attributes
+ */
+ private static Attributes createAttributes(final SoulServiceInstance instance) {
+ Map metadata = instance.getMetadata();
+ if (metadata == null) {
+ metadata = Collections.emptyMap();
+ }
+ return SubChannels.createAttributes(instance.getWeight());
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstance.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstance.java
new file mode 100644
index 000000000000..808a2985d2ff
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstance.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.resolver;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Soul Service instance.
+ *
+ * @author zhanglei
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class SoulServiceInstance {
+
+ private String host;
+
+ private int port;
+
+ private Map metadata;
+
+ public SoulServiceInstance(final String host, final int port) {
+ this(host, port, new HashMap<>());
+ }
+
+ /**
+ * Get weight.
+ *
+ * @return int i
+ */
+ public int getWeight() {
+ final String weightValue = metadata.get("weight");
+ if (StringUtils.isEmpty(weightValue)) {
+ return 0;
+ }
+ return Integer.parseInt(weightValue);
+ }
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstanceLists.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstanceLists.java
new file mode 100644
index 000000000000..c6985777aa04
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/resolver/SoulServiceInstanceLists.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.resolver;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Soul service instance list.
+ *
+ * @author zhanglei
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class SoulServiceInstanceLists {
+
+ private CopyOnWriteArrayList soulServiceInstances;
+
+ private String appName;
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/response/GrpcResponsePlugin.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/response/GrpcResponsePlugin.java
new file mode 100644
index 000000000000..5d71040e0208
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/response/GrpcResponsePlugin.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.response;
+
+import org.dromara.soul.common.constant.Constants;
+import org.dromara.soul.common.enums.PluginEnum;
+import org.dromara.soul.common.enums.RpcTypeEnum;
+import org.dromara.soul.common.utils.JsonUtils;
+import org.dromara.soul.plugin.api.SoulPlugin;
+import org.dromara.soul.plugin.api.SoulPluginChain;
+import org.dromara.soul.plugin.api.context.SoulContext;
+import org.dromara.soul.plugin.api.result.SoulResultEnum;
+import org.dromara.soul.plugin.base.utils.SoulResultWrap;
+import org.dromara.soul.plugin.base.utils.WebFluxResultUtils;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.Objects;
+
+/**
+ * this is grpc response plugin.
+ *
+ * @author zhanglei
+ */
+public class GrpcResponsePlugin implements SoulPlugin {
+
+ /**
+ * Process the Web request and (optionally) delegate to the next
+ * {@code WebFilter} through the given {@link SoulPluginChain}.
+ *
+ * @param exchange the current server exchange
+ * @param chain provides a way to delegate to the next filter
+ * @return {@code Mono} to indicate when request processing is complete
+ */
+ @Override
+ public Mono execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
+ return chain.execute(exchange).then(Mono.defer(() -> {
+ final Object result = exchange.getAttribute(Constants.GRPC_RPC_RESULT);
+ if (Objects.isNull(result)) {
+ Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_RESULT_ERROR.getCode(), SoulResultEnum.SERVICE_RESULT_ERROR.getMsg(), null);
+ return WebFluxResultUtils.result(exchange, error);
+ }
+ Object success = SoulResultWrap.success(SoulResultEnum.SUCCESS.getCode(), SoulResultEnum.SUCCESS.getMsg(), JsonUtils.removeClass(result));
+ return WebFluxResultUtils.result(exchange, success);
+ }));
+ }
+
+ @Override
+ public Boolean skip(final ServerWebExchange exchange) {
+ final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
+ assert soulContext != null;
+ return !Objects.equals(soulContext.getRpcType(), RpcTypeEnum.GRPC.getName());
+ }
+
+ @Override
+ public int getOrder() {
+ return PluginEnum.RESPONSE.getCode();
+ }
+
+ /**
+ * acquire plugin name.
+ *
+ * @return plugin name.
+ */
+ @Override
+ public String named() {
+ return PluginEnum.RESPONSE.getName();
+ }
+
+}
diff --git a/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/subscriber/GrpcMetaDataSubscriber.java b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/subscriber/GrpcMetaDataSubscriber.java
new file mode 100644
index 000000000000..921eead5ef6e
--- /dev/null
+++ b/soul-plugin/soul-plugin-grpc/src/main/java/org/dromara/soul/plugin/grpc/subscriber/GrpcMetaDataSubscriber.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.soul.plugin.grpc.subscriber;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.collections4.CollectionUtils;
+import org.dromara.soul.common.dto.MetaData;
+import org.dromara.soul.common.enums.RpcTypeEnum;
+import org.dromara.soul.plugin.grpc.cache.ApplicationConfigCache;
+import org.dromara.soul.plugin.grpc.cache.GrpcClientCache;
+import org.dromara.soul.plugin.grpc.resolver.SoulServiceInstance;
+import org.dromara.soul.sync.data.api.MetaDataSubscriber;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
+
+/**
+ * The grpc metadata subscribe.
+ *
+ * @author zhanglei
+ */
+public class GrpcMetaDataSubscriber implements MetaDataSubscriber {
+
+ private static final ConcurrentMap META_DATA = Maps.newConcurrentMap();
+
+ @Override
+ public void onSubscribe(final MetaData metaData) {
+ if (RpcTypeEnum.GRPC.getName().equals(metaData.getRpcType())) {
+ if (metaData.getContextPath() == null) {
+ return;
+ }
+ MetaData metaExist = META_DATA.get(metaData.getPath());
+ List prxList = ApplicationConfigCache.getInstance()
+ .get(metaData.getContextPath()).getSoulServiceInstances();
+ if (CollectionUtils.isEmpty(prxList)) {
+ GrpcClientCache.initGrpcClient(metaData.getContextPath());
+ }
+ boolean exist = prxList.stream()
+ .filter(instance -> {
+ return isEqual(instance, metaData.getAppName());
+ }).count() > 0;
+ if (!exist) {
+ ApplicationConfigCache.getInstance().initPrx(metaData);
+ }
+ if (Objects.isNull(metaExist)) {
+ META_DATA.put(metaData.getPath(), metaData);
+ }
+ }
+ }
+
+ @Override
+ public void unSubscribe(final MetaData metaData) {
+ if (RpcTypeEnum.GRPC.getName().equals(metaData.getRpcType())) {
+ List prxList = ApplicationConfigCache.getInstance()
+ .get(metaData.getPath()).getSoulServiceInstances();
+ List removePrxList = prxList.stream()
+ .filter(instance -> {
+ return isEqual(instance, metaData.getAppName());
+ })
+ .collect(Collectors.toList());
+ prxList.removeAll(removePrxList);
+ if (CollectionUtils.isEmpty(prxList)) {
+ META_DATA.remove(metaData.getPath());
+ ApplicationConfigCache.getInstance().invalidate(metaData);
+ }
+ }
+ }
+
+ private Boolean isEqual(final SoulServiceInstance instance, final String appName) {
+ String url = String.join(":", instance.getHost(), String.valueOf(instance.getPort()));
+ return url.equals(appName);
+ }
+}
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/pom.xml b/soul-spring-boot-starter/soul-spring-boot-starter-client/pom.xml
index c55ffcb6a8b4..4fecc48249f1 100644
--- a/soul-spring-boot-starter/soul-spring-boot-starter-client/pom.xml
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/pom.xml
@@ -35,7 +35,8 @@
soul-spring-boot-starter-client-apache-dubbosoul-spring-boot-starter-client-sofasoul-spring-boot-starter-client-tars
+ soul-spring-boot-starter-client-grpc
-
\ No newline at end of file
+
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/pom.xml b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/pom.xml
new file mode 100644
index 000000000000..3e2decd3a2fb
--- /dev/null
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/pom.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+ soul-spring-boot-starter-client
+ org.dromara
+ 2.2.1
+
+ 4.0.0
+
+ soul-spring-boot-starter-client-grpc
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.dromara
+ soul-client-grpc
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/java/org/dromara/soul/springboot/starter/client/grpc/SoulGrpcClientConfiguration.java b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/java/org/dromara/soul/springboot/starter/client/grpc/SoulGrpcClientConfiguration.java
new file mode 100644
index 000000000000..aedc8de9fe3e
--- /dev/null
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/java/org/dromara/soul/springboot/starter/client/grpc/SoulGrpcClientConfiguration.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.dromara.soul.springboot.starter.client.grpc;
+
+import org.dromara.soul.client.grpc.GrpcClientBeanPostProcessor;
+import org.dromara.soul.client.grpc.common.config.GrpcConfig;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Grpc type client bean postprocessor.
+ *
+ * @author tydhot
+ */
+@Configuration
+public class SoulGrpcClientConfiguration {
+ /**
+ * Grpc service bean post processor sofa service bean post processor.
+ *
+ * @param grpcConfig the sofa config
+ * @return the grpc service bean post processor
+ */
+ @Bean
+ public GrpcClientBeanPostProcessor grpcServiceBeanPostProcessor(final GrpcConfig grpcConfig) {
+ return new GrpcClientBeanPostProcessor(grpcConfig);
+ }
+
+ /**
+ * Grpc config sofa config.
+ *
+ * @return the grpc config
+ */
+ @Bean
+ @ConfigurationProperties(prefix = "soul.grpc")
+ public GrpcConfig grpcConfig() {
+ return new GrpcConfig();
+ }
+}
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000000..de3972df885e
--- /dev/null
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.dromara.soul.springboot.starter.client.grpc.SoulGrpcClientConfiguration
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides
new file mode 100644
index 000000000000..9cc6a041725a
--- /dev/null
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-client/soul-spring-boot-starter-client-grpc/src/main/resources/META-INF/spring.provides
@@ -0,0 +1,18 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+provides: soul-spring-boot-starter-client-grpc
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/pom.xml b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/pom.xml
index 692a21f2278a..5fd0b4f0ecd7 100644
--- a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/pom.xml
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/pom.xml
@@ -46,7 +46,8 @@
soul-spring-boot-starter-plugin-resilience4jsoul-spring-boot-starter-plugin-tarssoul-spring-boot-starter-plugin-context-path
+ soul-spring-boot-starter-plugin-grpc
-
\ No newline at end of file
+
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/pom.xml b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/pom.xml
new file mode 100644
index 000000000000..4cb893557d14
--- /dev/null
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/pom.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ soul-spring-boot-starter-plugin
+ org.dromara
+ 2.2.1
+
+ 4.0.0
+
+ soul-spring-boot-starter-plugin-grpc
+
+
+
+ org.dromara
+ soul-plugin-grpc
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/java/org/dromara/soul/spring/boot/starter/plugin/grpc/GrpcPluginConfiguration.java b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/java/org/dromara/soul/spring/boot/starter/plugin/grpc/GrpcPluginConfiguration.java
new file mode 100644
index 000000000000..7820303a8a9e
--- /dev/null
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/java/org/dromara/soul/spring/boot/starter/plugin/grpc/GrpcPluginConfiguration.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.dromara.soul.spring.boot.starter.plugin.grpc;
+
+import org.dromara.soul.plugin.api.SoulPlugin;
+import org.dromara.soul.plugin.grpc.GrpcPlugin;
+import org.dromara.soul.plugin.grpc.param.BodyParamPlugin;
+import org.dromara.soul.plugin.grpc.response.GrpcResponsePlugin;
+import org.dromara.soul.plugin.grpc.subscriber.GrpcMetaDataSubscriber;
+import org.dromara.soul.sync.data.api.MetaDataSubscriber;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * The type grpc plugin configuration.
+ *
+ * @author zhanglei
+ */
+@Configuration
+public class GrpcPluginConfiguration {
+
+ /**
+ * grpc plugin soul plugin.
+ *
+ * @return the tars plugin
+ */
+ @Bean
+ public SoulPlugin grpcPlugin() {
+ return new GrpcPlugin();
+ }
+
+ /**
+ * Body param plugin soul plugin.
+ *
+ * @return the soul plugin
+ */
+ @Bean
+ public SoulPlugin bodyParamPlugin() {
+ return new BodyParamPlugin();
+ }
+
+ /**
+ * Grpc response plugin soul plugin.
+ *
+ * @return the soul plugin
+ */
+ @Bean
+ public SoulPlugin grpcResponsePlugin() {
+ return new GrpcResponsePlugin();
+ }
+
+ /**
+ * Grpc meta data subscriber meta data subscriber.
+ *
+ * @return the meta data subscriber
+ */
+ @Bean
+ public MetaDataSubscriber grpcMetaDataSubscriber() {
+ return new GrpcMetaDataSubscriber();
+ }
+}
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.factories b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000000..4f03bb4f02a8
--- /dev/null
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.dromara.soul.spring.boot.starter.plugin.grpc.GrpcPluginConfiguration
diff --git a/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.provides b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.provides
new file mode 100644
index 000000000000..a1099f5407ed
--- /dev/null
+++ b/soul-spring-boot-starter/soul-spring-boot-starter-plugin/soul-spring-boot-starter-plugin-grpc/src/main/resources/META-INF/spring.provides
@@ -0,0 +1,18 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+provides: soul-spring-boot-starter-plugin-grpc