diff --git a/pom.xml b/pom.xml
index be8c6d80e734..48d7ead0d802 100644
--- a/pom.xml
+++ b/pom.xml
@@ -728,7 +728,7 @@
/script/shenyu_checkstyle.xml
/script/checkstyle-header.txt
true
- **/transfer/**/*
+ **/transfer/**/*,**/generated*/**/*
diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ApiServiceImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ApiServiceImpl.java
index 979e6d7e1ab9..ad05dbc78855 100644
--- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ApiServiceImpl.java
+++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ApiServiceImpl.java
@@ -21,6 +21,7 @@
import java.util.ArrayList;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
+import java.util.Objects;
import org.apache.shenyu.admin.disruptor.RegisterClientServerDisruptorPublisher;
import org.apache.shenyu.admin.mapper.ApiMapper;
import org.apache.shenyu.admin.mapper.TagMapper;
@@ -40,7 +41,6 @@
import org.apache.shenyu.admin.model.vo.RuleVO;
import org.apache.shenyu.admin.model.vo.TagVO;
import org.apache.shenyu.admin.service.ApiService;
-import org.apache.shenyu.common.enums.ApiSourceEnum;
import org.apache.shenyu.common.enums.PluginEnum;
import org.apache.shenyu.common.utils.JsonUtils;
import org.apache.shenyu.common.utils.ListUtil;
@@ -243,12 +243,14 @@ public ApiVO findById(final String id) {
tagVOs = tagDOS.stream().map(TagVO::buildTagVO).collect(Collectors.toList());
}
ApiVO apiVO = ApiVO.buildApiVO(item, tagVOs);
- if (apiVO.getApiSource().equals(ApiSourceEnum.SWAGGER.getValue())) {
+ if (StringUtils.isNotBlank(apiVO.getDocument())) {
DocItem docItem = JsonUtils.jsonToObject(apiVO.getDocument(), DocItem.class);
- apiVO.setRequestHeaders(docItem.getRequestHeaders());
- apiVO.setRequestParameters(docItem.getRequestParameters());
- apiVO.setResponseParameters(docItem.getResponseParameters());
- apiVO.setBizCustomCodeList(docItem.getBizCodeList());
+ if (Objects.nonNull(docItem)) {
+ apiVO.setRequestHeaders(docItem.getRequestHeaders());
+ apiVO.setRequestParameters(docItem.getRequestParameters());
+ apiVO.setResponseParameters(docItem.getResponseParameters());
+ apiVO.setBizCustomCodeList(docItem.getBizCodeList());
+ }
}
return apiVO;
diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ApiServiceTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ApiServiceTest.java
index dd7b719e769b..7ffc2c87b057 100644
--- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ApiServiceTest.java
+++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ApiServiceTest.java
@@ -108,6 +108,60 @@ public void testFindById() {
assertNotNull(byId);
}
+ @Test
+ public void testFindByIdWithDocumentNotBlank() {
+ String id = "456";
+ ApiDTO apiDTO = new ApiDTO();
+ apiDTO.setId(id);
+ apiDTO.setContextPath("string");
+ apiDTO.setApiPath("string");
+ apiDTO.setHttpMethod(0);
+ apiDTO.setConsume("string");
+ apiDTO.setProduce("string");
+ apiDTO.setVersion("string");
+ apiDTO.setRpcType("string");
+ apiDTO.setState(0);
+ apiDTO.setApiOwner("string");
+ apiDTO.setApiDesc("string");
+ apiDTO.setApiSource(0);
+ apiDTO.setDocument("{\"module\":\"test-module\",\"requestParameters\":[],\"responseParameters\":[]}");
+ ApiDO apiDO = ApiDO.buildApiDO(apiDTO);
+ Timestamp now = Timestamp.valueOf(LocalDateTime.now());
+ apiDO.setDateCreated(now);
+ apiDO.setDateUpdated(now);
+ given(this.apiMapper.selectByPrimaryKey(eq(id))).willReturn(apiDO);
+ ApiVO byId = this.apiService.findById(id);
+ assertNotNull(byId);
+ assertNotNull(byId.getRequestParameters());
+ assertNotNull(byId.getResponseParameters());
+ }
+
+ @Test
+ public void testFindByIdWithBlankDocument() {
+ String id = "789";
+ ApiDTO apiDTO = new ApiDTO();
+ apiDTO.setId(id);
+ apiDTO.setContextPath("string");
+ apiDTO.setApiPath("string");
+ apiDTO.setHttpMethod(0);
+ apiDTO.setConsume("string");
+ apiDTO.setProduce("string");
+ apiDTO.setVersion("string");
+ apiDTO.setRpcType("string");
+ apiDTO.setState(0);
+ apiDTO.setApiOwner("string");
+ apiDTO.setApiDesc("string");
+ apiDTO.setApiSource(0);
+ apiDTO.setDocument("");
+ ApiDO apiDO = ApiDO.buildApiDO(apiDTO);
+ Timestamp now = Timestamp.valueOf(LocalDateTime.now());
+ apiDO.setDateCreated(now);
+ apiDO.setDateUpdated(now);
+ given(this.apiMapper.selectByPrimaryKey(eq(id))).willReturn(apiDO);
+ ApiVO byId = this.apiService.findById(id);
+ assertNotNull(byId);
+ }
+
@Test
public void testListByPage() {
PageParameter pageParameter = new PageParameter();
diff --git a/shenyu-client/shenyu-client-core/pom.xml b/shenyu-client/shenyu-client-core/pom.xml
index 683afdcc7ce4..e792a70df701 100644
--- a/shenyu-client/shenyu-client-core/pom.xml
+++ b/shenyu-client/shenyu-client-core/pom.xml
@@ -81,5 +81,55 @@
spring-boot-starter-tomcat
test
+
+ com.google.protobuf
+ protobuf-java
+ test
+
+
+ io.grpc
+ grpc-stub
+ test
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.6.2
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+ true
+
+ com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}
+ ${project.basedir}/src/test/proto
+ ${project.build.directory}/generated-test-sources/protobuf/java
+ false
+
+
+
+
+ test-compile
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ ${project.basedir}/src/test/java
+
+
+
+
+
diff --git a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java
index 32ef07fcda57..1aef7389b781 100644
--- a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java
+++ b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java
@@ -17,7 +17,6 @@
package org.apache.shenyu.client.core.client;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
@@ -213,7 +212,7 @@ private List buildApiDocDTO(final Object bean, final Method m
String apiPath = pathJoin(contextPath, superPath, value);
ApiHttpMethodEnum[] value3 = sextet.getValue3();
for (ApiHttpMethodEnum apiHttpMethodEnum : value3) {
- String documentJson = buildDocumentJson(pairs.getRight(), apiPath, method);
+ String documentJson = buildDocumentJson(pairs.getRight(), apiPath, method, sextet.getValue4());
String extJson = buildExtJson(method);
ApiDocRegisterDTO build = ApiDocRegisterDTO.builder()
.consume(sextet.getValue1())
@@ -258,15 +257,8 @@ protected ApiDocRegisterDTO.ApiExt customApiDocExt(final ApiDocRegisterDTO.ApiEx
return ext;
}
- private String buildDocumentJson(final List tags, final String path, final Method method) {
- Map documentMap = ImmutableMap.builder()
- .put("tags", tags)
- .put("operationId", path)
- .put("parameters", OpenApiUtils.generateDocumentParameters(path, method))
- .put("responses", OpenApiUtils.generateDocumentResponse(path))
- .put("responseType", Collections.singletonList(OpenApiUtils.parseReturnType(method)))
- .build();
- return GsonUtils.getInstance().toJson(documentMap);
+ private String buildDocumentJson(final List tags, final String path, final Method method, final RpcTypeEnum rpcTypeEnum) {
+ return OpenApiUtils.buildDocumentJson(tags, path, method, rpcTypeEnum);
}
protected abstract Sextet buildApiDocSextet(Method method, Annotation annotation, Map beans);
diff --git a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/AbstractApiDocRegistrar.java b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/AbstractApiDocRegistrar.java
index 6d2e5358b360..30ddb8828638 100644
--- a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/AbstractApiDocRegistrar.java
+++ b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/AbstractApiDocRegistrar.java
@@ -17,7 +17,6 @@
package org.apache.shenyu.client.core.register.registrar;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.client.apidocs.annotations.ApiDoc;
@@ -42,9 +41,7 @@
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
-import java.util.Map;
public abstract class AbstractApiDocRegistrar extends AbstractApiRegistrar {
@@ -128,14 +125,7 @@ protected List parse(final ApiBean.ApiDefinition apiDefinitio
}
private String buildDocumentJson(final List tags, final String path, final Method method) {
- Map documentMap = ImmutableMap.builder()
- .put("tags", tags)
- .put("operationId", path)
- .put("parameters", OpenApiUtils.generateDocumentParameters(path, method))
- .put("responses", OpenApiUtils.generateDocumentResponse(path))
- .put("responseType", Collections.singletonList(OpenApiUtils.parseReturnType(method)))
- .build();
- return GsonUtils.getInstance().toJson(documentMap);
+ return OpenApiUtils.buildDocumentJson(tags, path, method, rpcTypeEnum);
}
private String buildExtJson(final ApiBean.ApiDefinition apiDefinition) {
diff --git a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImpl.java b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImpl.java
index 91b842f97f5d..10d07877a4b2 100644
--- a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImpl.java
+++ b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/register/registrar/ApiDocRegistrarImpl.java
@@ -17,7 +17,6 @@
package org.apache.shenyu.client.core.register.registrar;
-import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.client.core.constant.ShenyuClientConstants;
import org.apache.shenyu.client.core.disruptor.ShenyuClientRegisterEventPublisher;
@@ -38,7 +37,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -132,14 +130,9 @@ private String getDocument(final ApiBean.ApiDefinition api) {
return document;
}
final String path = getPath(api);
- final Map documentMap = ImmutableMap.builder()
- .put("tags", buildTags(api))
- .put("operationId", path)
- .put("parameters", OpenApiUtils.generateDocumentParameters(path, api.getApiMethod()))
- .put("responses", OpenApiUtils.generateDocumentResponse(path))
- .put("responseType", Collections.singletonList(OpenApiUtils.parseReturnType(api.getApiMethod())))
- .build();
- return GsonUtils.getInstance().toJson(documentMap);
+ final String rpcType = getRpcType(api);
+ RpcTypeEnum rpcTypeEnum = RpcTypeEnum.acquireByName(rpcType);
+ return OpenApiUtils.buildDocumentJson(buildTags(api), path, api.getApiMethod(), rpcTypeEnum);
}
private String getRpcType(final ApiBean.ApiDefinition api) {
diff --git a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/OpenApiUtils.java b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/OpenApiUtils.java
index 5affbce77a91..d74ba59b6915 100644
--- a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/OpenApiUtils.java
+++ b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/utils/OpenApiUtils.java
@@ -21,6 +21,8 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.shenyu.client.core.constant.ShenyuClientConstants;
+import org.apache.shenyu.common.enums.RpcTypeEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
@@ -37,6 +39,7 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -58,16 +61,73 @@ public class OpenApiUtils {
private static final String[] QUERY_CLASSES = new String[]{"org.springframework.web.bind.annotation.RequestParam", "org.springframework.web.bind.annotation.RequestPart"};
+ /**
+ * Check if the given RPC type uses Spring MVC parameter parsing.
+ * HTTP, WebSocket and Spring Cloud types use Spring MVC annotations for parameter resolution,
+ * while other RPC types (Dubbo, gRPC, etc.) parse parameters from Java method signatures directly.
+ *
+ * @param rpcTypeEnum the RPC type enum
+ * @return true if Spring MVC parameter parsing should be used
+ */
+ public static boolean useSpringMvcParamParsing(final RpcTypeEnum rpcTypeEnum) {
+ return rpcTypeEnum == RpcTypeEnum.HTTP
+ || rpcTypeEnum == RpcTypeEnum.WEB_SOCKET
+ || rpcTypeEnum == RpcTypeEnum.SPRING_CLOUD;
+ }
/**
- * generateDocumentParameters.
+ * Build document JSON string for the given API method.
+ * Dispatches to the appropriate parameter/response generation based on RPC type.
+ *
+ * @param tags the API tags
+ * @param path the API path
+ * @param method the Java method
+ * @param rpcTypeEnum the RPC type
+ * @return document JSON string
+ */
+ public static String buildDocumentJson(final List tags, final String path,
+ final Method method, final RpcTypeEnum rpcTypeEnum) {
+ boolean useSpringMvcParamParsing = useSpringMvcParamParsing(rpcTypeEnum);
+ Map documentMap;
+ if (useSpringMvcParamParsing) {
+ documentMap = ImmutableMap.builder()
+ .put("tags", tags)
+ .put("operationId", path)
+ .put("requestParameters", generateRequestDocParameters(path, method))
+ .put("responseParameters", Collections.singletonList(parseReturnType(method)))
+ .put("responses", generateDocumentResponse(path))
+ .build();
+ } else if (rpcTypeEnum == RpcTypeEnum.GRPC) {
+ documentMap = ImmutableMap.builder()
+ .put("tags", tags)
+ .put("operationId", path)
+ .put("requestParameters", generateGrpcRequestDocParameters(method))
+ .put("responseParameters", Collections.singletonList(parseGrpcReturnType(method)))
+ .put("responses", generateGrpcDocumentResponse(path, method))
+ .build();
+ } else {
+ documentMap = ImmutableMap.builder()
+ .put("tags", tags)
+ .put("operationId", path)
+ .put("requestParameters", generateRpcRequestDocParameters(method))
+ .put("responseParameters", Collections.singletonList(parseReturnType(method)))
+ .put("responses", generateRpcDocumentResponse(path, method))
+ .build();
+ }
+ return GsonUtils.getInstance().toJson(documentMap);
+ }
+
+
+ /**
+ * Generate request parameters for HTTP methods.
+ * This produces Parameter objects with name, in, type, and refs fields.
*
* @param path the api path
* @param method the method
- * @return documentParameters
+ * @return request parameters
*/
- public static List generateDocumentParameters(final String path, final Method method) {
- ArrayList list = new ArrayList<>();
+ public static List generateRequestDocParameters(final String path, final Method method) {
+ List list = new ArrayList<>();
Pair query = isQuery(method);
if (query.getLeft()) {
for (Annotation[] annotations : query.getRight()) {
@@ -89,34 +149,182 @@ public static List generateDocumentParameters(final String path, fina
parameter.setIn("query");
parameter.setRequired(required);
parameter.setName(name);
- parameter.setSchema(new Schema("string", null));
+ parameter.setType("string");
list.add(parameter);
}
}
}
- } else {
- List segments = UrlPathUtils.getSegments(path);
- for (String segment : segments) {
- if (EVERY_PATH.equals(segment)) {
- Parameter parameter = new Parameter();
- parameter.setIn("path");
- parameter.setName(segment);
- parameter.setRequired(true);
- parameter.setSchema(new Schema("string", null));
- list.add(parameter);
+ }
+ List segments = UrlPathUtils.getSegments(path);
+ for (String segment : segments) {
+ if (EVERY_PATH.equals(segment)) {
+ Parameter parameter = new Parameter();
+ parameter.setIn("path");
+ parameter.setName(segment);
+ parameter.setRequired(true);
+ parameter.setType("string");
+ list.add(parameter);
+ }
+ if (segment.startsWith(LEFT_ANGLE_BRACKETS) && segment.endsWith(RIGHT_ANGLE_BRACKETS)) {
+ String name = segment.substring(1, segment.length() - 1);
+ Parameter parameter = new Parameter();
+ parameter.setIn("path");
+ parameter.setName(name);
+ parameter.setRequired(true);
+ parameter.setType("string");
+ list.add(parameter);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Generate request parameters for RPC methods.
+ * Unlike HTTP methods that use Spring annotations, RPC method parameters
+ * are parsed from Java method parameter types directly.
+
+ * @param method the method
+ * @return request parameters
+ */
+ public static List generateRpcRequestDocParameters(final Method method) {
+ List list = new ArrayList<>();
+ java.lang.reflect.Parameter[] methodParams = method.getParameters();
+ for (java.lang.reflect.Parameter methodParam : methodParams) {
+ Type paramType = methodParam.getParameterizedType();
+ Schema schema = parseSchema(paramType, 0, new HashMap<>(16));
+ Parameter parameter = convertSchemaToParameter(methodParam.getName(), schema);
+ parameter.setRequired(true);
+ list.add(parameter);
+ }
+ return list;
+ }
+
+ /**
+ * Generate request parameters for gRPC methods.
+ * gRPC method signatures differ from other RPC types:
+ * - Unary/ServerStreaming: void method(Request req, StreamObserver{Response} observer)
+ * - ClientStreaming/BidiStreaming: StreamObserver{Request} method(StreamObserver{Response} observer)
+ * StreamObserver parameters are excluded from request parameters.
+ *
+ * @param method the method
+ * @return request parameters
+ */
+ public static List generateGrpcRequestDocParameters(final Method method) {
+ List list = new ArrayList<>();
+ java.lang.reflect.Parameter[] methodParams = method.getParameters();
+ for (java.lang.reflect.Parameter methodParam : methodParams) {
+ if (isStreamObserver(methodParam.getType())) {
+ continue;
+ }
+ Type paramType = methodParam.getParameterizedType();
+ Schema schema = parseSchema(paramType, 0, new HashMap<>(16));
+ Parameter parameter = convertSchemaToParameter(methodParam.getName(), schema);
+ parameter.setRequired(true);
+ list.add(parameter);
+ }
+ if (list.isEmpty()) {
+ Type returnType = method.getGenericReturnType();
+ Type actualType = extractStreamObserverTypeParam(returnType);
+ if (Objects.nonNull(actualType)) {
+ Schema schema = parseSchema(actualType, 0, new HashMap<>(16));
+ Parameter parameter = convertSchemaToParameter("request", schema);
+ parameter.setRequired(true);
+ list.add(parameter);
+ }
+ }
+ return list;
+ }
+
+ private static Parameter convertSchemaToParameter(final String name, final Schema schema) {
+ Parameter parameter = new Parameter();
+ parameter.setName(name);
+ parameter.setType(schema.getType());
+ if (Objects.nonNull(schema.getRefs()) && !schema.getRefs().isEmpty()) {
+ List refs = new ArrayList<>();
+ for (Schema ref : schema.getRefs()) {
+ refs.add(convertSchemaToParameter(ref.getName(), ref));
+ }
+ parameter.setRefs(refs);
+ }
+ return parameter;
+ }
+
+ /**
+ * Parse return type for gRPC methods.
+ * - Unary/ServerStreaming: response type is extracted from StreamObserver{Response} parameter
+ * - ClientStreaming/BidiStreaming: response type is extracted from StreamObserver{Response} parameter
+ *
+ * @param method the method
+ * @return response type
+ */
+ public static ResponseType parseGrpcReturnType(final Method method) {
+ java.lang.reflect.Parameter[] methodParams = method.getParameters();
+ for (java.lang.reflect.Parameter methodParam : methodParams) {
+ if (isStreamObserver(methodParam.getType())) {
+ Type paramType = methodParam.getParameterizedType();
+ Type actualType = extractStreamObserverTypeParam(paramType);
+ if (Objects.nonNull(actualType)) {
+ return parseType("ROOT", actualType, 0, new HashMap<>(16));
}
- if (segment.startsWith(LEFT_ANGLE_BRACKETS) && segment.endsWith(RIGHT_ANGLE_BRACKETS)) {
- String name = segment.substring(1, segment.length() - 1);
- Parameter parameter = new Parameter();
- parameter.setIn("path");
- parameter.setName(name);
- parameter.setRequired(true);
- parameter.setSchema(new Schema("string", null));
- list.add(parameter);
+ }
+ }
+ ResponseType voidType = new ResponseType();
+ voidType.setName("ROOT");
+ voidType.setType("void");
+ return voidType;
+ }
+
+ /**
+ * Generate document response for gRPC methods.
+ *
+ * @param path the api path
+ * @param method the method
+ * @return documentResponseMap
+ */
+ public static Map generateGrpcDocumentResponse(final String path, final Method method) {
+ String returnTypeStr = "void";
+ java.lang.reflect.Parameter[] methodParams = method.getParameters();
+ for (java.lang.reflect.Parameter methodParam : methodParams) {
+ if (isStreamObserver(methodParam.getType())) {
+ Type paramType = methodParam.getParameterizedType();
+ Type actualType = extractStreamObserverTypeParam(paramType);
+ if (Objects.nonNull(actualType)) {
+ returnTypeStr = resolveTypeName(actualType);
}
+ break;
}
}
- return list;
+ ImmutableMap