From c57dbda68af1d0568ae28f369607cf96c0aea8d5 Mon Sep 17 00:00:00 2001 From: PengyuDeng <89559616+PengyuDeng@users.noreply.github.com> Date: Mon, 12 May 2025 16:23:54 +0800 Subject: [PATCH] =?UTF-8?q?build(doc):=E5=8D=87=E7=BA=A7springdoc=20(#631)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build(doc):升级springdoc * Update pom.xml * Update SpringDocCustomizerConfiguration.java * 增加单独的类处理 * Update ResponseWrapperConverter.java * 增加普通类型的处理 * Update ResponseWrapperConverter.java * Update ResponseWrapperConverter.java * Update SpringDocCustomizerConfiguration.java * 增加泛型转换 * Update ResponseWrapperConverter.java * Update ResponseWrapperConverter.java * Update pom.xml * openai参数配置化 --------- Co-authored-by: 老周 --- .../configure-component/pom.xml | 8 +- .../doc/ResponseWrapperConverter.java | 251 ++++++++++++++++++ .../doc/SpringDocCustomizerConfiguration.java | 62 +---- jetlinks-standalone/pom.xml | 12 +- .../src/main/resources/application.yml | 13 + pom.xml | 23 +- 6 files changed, 305 insertions(+), 64 deletions(-) create mode 100644 jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/doc/ResponseWrapperConverter.java diff --git a/jetlinks-components/configure-component/pom.xml b/jetlinks-components/configure-component/pom.xml index 7dd58760..9f2a5875 100644 --- a/jetlinks-components/configure-component/pom.xml +++ b/jetlinks-components/configure-component/pom.xml @@ -74,7 +74,13 @@ org.springdoc - springdoc-openapi-webflux-core + springdoc-openapi-starter-webflux-api + compile + + + + org.springdoc + springdoc-openapi-starter-webflux-ui compile diff --git a/jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/doc/ResponseWrapperConverter.java b/jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/doc/ResponseWrapperConverter.java new file mode 100644 index 00000000..294faaaa --- /dev/null +++ b/jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/doc/ResponseWrapperConverter.java @@ -0,0 +1,251 @@ +package org.jetlinks.community.configure.doc; + +import com.fasterxml.jackson.databind.JavaType; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverter; +import io.swagger.v3.core.converter.ModelConverterContext; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import org.apache.commons.lang3.ClassUtils; +import org.hswebframework.web.api.crud.entity.EntityFactory; +import org.hswebframework.web.crud.web.ResponseMessage; +import org.reactivestreams.Publisher; +import org.springdoc.core.converters.ResponseSupportConverter; +import org.springdoc.core.customizers.GlobalOperationComponentsCustomizer; +import org.springdoc.core.providers.ObjectMapperProvider; +import org.springdoc.core.utils.SpringDocAnnotationsUtils; +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.method.HandlerMethod; +import reactor.core.publisher.Flux; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +public class ResponseWrapperConverter extends ResponseSupportConverter implements GlobalOperationComponentsCustomizer, Ordered { + + private final EntityFactory entityFactory; + + private final ObjectMapperProvider provider; + + public ResponseWrapperConverter(EntityFactory entityFactory, ObjectMapperProvider springDocObjectMapper) { + super(springDocObjectMapper); + this.entityFactory = entityFactory; + this.provider = springDocObjectMapper; + } + + @Override + public Schema resolve(AnnotatedType _type, ModelConverterContext context, Iterator chain) { + JavaType javaType = provider.jsonMapper().constructType(_type.getType()); + if (javaType != null) { + _type.type(provider + .jsonMapper() + .constructType(getRealType(ResolvableType.forType(_type.getType())).getType())); + } + return super.resolve(_type, context, chain); + } + + @Override + public Operation customize(Operation operation, Components components, HandlerMethod handlerMethod) { + MethodParameter parameter = handlerMethod.getReturnType(); + ApiResponses responses = operation.getResponses(); + + if (responses != null) { + // 只展示2xx的响应 + responses + .keySet() + .stream() + .filter(code -> !code.startsWith("2")) + .collect(Collectors.toSet()) + .forEach(operation.getResponses()::remove); + + // 原始返回类型 + ResolvableType originType = ResolvableType.forMethodParameter( + handlerMethod.getReturnType(), + ResolvableType.forType(handlerMethod.getBeanType())); + + for (Map.Entry entry : responses.entrySet()) { + if (entry.getValue().getContent() == null) { + continue; + } + + for (Map.Entry typeEntry : + entry.getValue().getContent().entrySet()) { + + Schema schema = typeEntry.getValue().getSchema(); + if (schema != null) { + ResolvableType type = resolveWrappedType(originType); + org.springframework.http.MediaType mediaType = org.springframework.http.MediaType.parseMediaType(typeEntry.getKey()); + // 流式响应,不包装. + if (org.springframework.http.MediaType.TEXT_EVENT_STREAM.includes(mediaType) + || org.springframework.http.MediaType.APPLICATION_NDJSON.includes(mediaType)) { + type = type.getGeneric(0); + } + if (originType.equalsType(type)) { + continue; + } + Type _type = provider + .jsonMapper() + .getTypeFactory() + .constructType(type.getType()); + + Schema schema_ = SpringDocAnnotationsUtils + .extractSchema(components, + _type, + null, + parameter.getParameterAnnotations(), + SpecVersion.V31 + ); + + typeEntry.getValue().schema(schema_); + } + } + } + } + + return operation; + } + + + public Type resolveWrappedType(Type type) { + return resolveWrappedType(ResolvableType.forType(type)).getType(); + } + + public ResolvableType resolveWrappedType(ResolvableType type) { + Class typeClass = type.toClass(); + + @SuppressWarnings("all") + Class msgType = entityFactory.getInstanceType(ResponseMessage.class); + + // 处理响应式类型 + if (Publisher.class.isAssignableFrom(typeClass)) { + ResolvableType actualType = type.getGeneric(0); + // 如果已经是ResponseEntity或者ResponseMessage + if (ResponseEntity.class.isAssignableFrom(actualType.toClass()) || + ResponseMessage.class.isAssignableFrom(actualType.toClass())) { + ResolvableType real = getRealType(actualType.getGeneric(0)); + return ResolvableType + .forClassWithGenerics(actualType.toClass(), real); + } + ResolvableType realType = getRealType(actualType); + // flux 返回List + if (Flux.class.isAssignableFrom(type.toClass())) { + return ResolvableType + .forClassWithGenerics( + msgType, + ResolvableType.forClassWithGenerics(List.class, realType) + ); + } + + return ResolvableType + .forClassWithGenerics( + msgType, + realType + ); + } + + // 处理ResponseEntity 或者 ResponseMessage + if (ResponseEntity.class.isAssignableFrom(typeClass) + || ResponseMessage.class.isAssignableFrom(typeClass)) { + ResolvableType realType = getRealType(type.getGeneric(0)); + return ResolvableType + .forClassWithGenerics(type.toClass(), realType); + } + + // 其他类型,直接获取真实类型并包装到ResponseMessage中 + ResolvableType realType = getRealType(type); + + return ResolvableType.forClassWithGenerics(msgType, realType); + } + + private ResolvableType getRealType(ResolvableType type) { + if (isIgnoreWrapped(type.getType())) { + return type; + } + + Class typeClazz = type.toClass(); + // Iterable + if (Iterable.class.isAssignableFrom(typeClazz)) { + ResolvableType _type = ResolvableType + .forType(typeClazz) + .as(Iterable.class); + return ResolvableType.forClassWithGenerics( + Iterable.class, getRealType(_type.getGeneric(0)) + ); + } + // Map + if (Map.class.isAssignableFrom(typeClazz)) { + ResolvableType _type = ResolvableType + .forType(typeClazz) + .as(Map.class); + return ResolvableType.forClassWithGenerics( + Map.class, + _type.getGeneric(0), + getRealType(_type.getGeneric(1)) + ); + } + if (typeClazz != Object.class) { + Class t = entityFactory.getInstanceType(typeClazz); + if (t == null) { + return type; + } + ResolvableType resolved = ResolvableType.forClass(t); + ResolvableType[] generics = resolved.getGenerics(); + ResolvableType[] typeGenerics = type.getGenerics(); + // 泛型 + // todo 不同长度的匹配? + if (generics.length > 0 && generics.length == typeGenerics.length) { + return ResolvableType.forClassWithGenerics( + t, + Arrays.stream(typeGenerics) + .map(this::getRealType) + .toArray(ResolvableType[]::new) + ); + } + + return ResolvableType.forClass(t); + } + + // 如果不是Class类型,则直接返回原类型 + return type; + } + + private boolean isIgnoreWrapped(Type type) { + if (type == null) { + return true; + } + if (type instanceof Class clazz) { + return clazz == Object.class + || ClassUtils.isPrimitiveOrWrapper(clazz) + || CharSequence.class.isAssignableFrom(clazz) + || Enum.class.isAssignableFrom(clazz); + } + return false; + } + + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + + return operation; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} + diff --git a/jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/doc/SpringDocCustomizerConfiguration.java b/jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/doc/SpringDocCustomizerConfiguration.java index 90534069..0a0d6384 100644 --- a/jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/doc/SpringDocCustomizerConfiguration.java +++ b/jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/doc/SpringDocCustomizerConfiguration.java @@ -1,69 +1,21 @@ package org.jetlinks.community.configure.doc; import org.hswebframework.web.api.crud.entity.EntityFactory; -import org.hswebframework.web.crud.web.ResponseMessage; -import org.reactivestreams.Publisher; -import org.springdoc.core.ReturnTypeParser; -import org.springdoc.webflux.core.SpringDocWebFluxConfiguration; +import org.springdoc.core.converters.ResponseSupportConverter; +import org.springdoc.core.providers.ObjectMapperProvider; +import org.springdoc.webflux.core.configuration.SpringDocWebFluxConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; -import org.springframework.http.ResponseEntity; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.List; @Configuration @AutoConfigureBefore(SpringDocWebFluxConfiguration.class) public class SpringDocCustomizerConfiguration { @Bean - public ReturnTypeParser operationCustomizer(EntityFactory factory) { - - return new ReturnTypeParser() { - @Override - public Type getReturnType(MethodParameter methodParameter) { - Type type = ReturnTypeParser.super.getReturnType(methodParameter); - - if (type instanceof ParameterizedType) { - ParameterizedType parameterizedType = ((ParameterizedType) type); - Type rawType = parameterizedType.getRawType(); - if (rawType instanceof Class && Publisher.class.isAssignableFrom(((Class) rawType))) { - Type actualType = parameterizedType.getActualTypeArguments()[0]; - - if (actualType instanceof ParameterizedType) { - actualType = ((ParameterizedType) actualType).getRawType(); - } - if (actualType == ResponseEntity.class || actualType == ResponseMessage.class) { - return type; - } - boolean returnList = Flux.class.isAssignableFrom(((Class) rawType)); - - //统一返回ResponseMessage - return ResolvableType - .forClassWithGenerics( - Mono.class, - ResolvableType.forClassWithGenerics( - factory.getInstanceType(ResponseMessage.class), - returnList ? - ResolvableType.forClassWithGenerics( - List.class, - ResolvableType.forType(parameterizedType.getActualTypeArguments()[0]) - ) : - ResolvableType.forType(parameterizedType.getActualTypeArguments()[0]) - )) - .getType(); - - } - } - - return type; - } - }; + public ResponseSupportConverter responseSupportConverter(EntityFactory entityFactory, + ObjectMapperProvider springDocObjectMapper) { + return new ResponseWrapperConverter(entityFactory, springDocObjectMapper); } + } diff --git a/jetlinks-standalone/pom.xml b/jetlinks-standalone/pom.xml index 70383160..f2206375 100644 --- a/jetlinks-standalone/pom.xml +++ b/jetlinks-standalone/pom.xml @@ -298,6 +298,12 @@ org.hswebframework.web hsweb-access-logging-aop ${hsweb.framework.version} + + + io.swagger + swagger-annotations + + @@ -307,13 +313,13 @@ org.springdoc - springdoc-openapi-webflux-ui + springdoc-openapi-starter-webflux-ui com.github.xiaoymin - knife4j-springdoc-ui - 2.0.8 + knife4j-openapi3-ui + 4.4.0 diff --git a/jetlinks-standalone/src/main/resources/application.yml b/jetlinks-standalone/src/main/resources/application.yml index a7dfb9c3..4f1f683a 100644 --- a/jetlinks-standalone/src/main/resources/application.yml +++ b/jetlinks-standalone/src/main/resources/application.yml @@ -226,7 +226,20 @@ management: metrics: export: enabled: false +# knife4j的增强配置,不需要增强可以不配 +knife4j: + enable: true + setting: + language: zh_cn springdoc: + openapi: + info: + title: "jetlinks" + description: "jetlinks平台API" + version: "2.10" + api-docs: + enabled: true + path: /v3/api-docs swagger-ui: path: /swagger-ui.html # packages-to-scan: org.jetlinks diff --git a/pom.xml b/pom.xml index 72bee662..7ce8bd40 100644 --- a/pom.xml +++ b/pom.xml @@ -52,11 +52,11 @@ 2.24.3 2.0.16 1.5.12 - 1.8.0 + 2.8.6 2.17.1 2.11.0 1.39.0 - 2.2.22 + 2.2.29 5.12.1 4.5.2 2.4.1 @@ -275,6 +275,19 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + -parameters + + ${project.build.jdk} + ${project.build.jdk} + + + @@ -362,19 +375,19 @@ org.springdoc - springdoc-openapi-common + springdoc-openapi-starter-common ${springdoc.version} org.springdoc - springdoc-openapi-webflux-core + springdoc-openapi-starter-webflux-ui ${springdoc.version} org.springdoc - springdoc-openapi-webflux-ui + springdoc-openapi-starter-webflux-api ${springdoc.version}