build(doc):升级springdoc (#631)

* 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: 老周 <zh.sqy@qq.com>
This commit is contained in:
PengyuDeng 2025-05-12 16:23:54 +08:00 committed by GitHub
parent 3ae7e668c2
commit c57dbda68a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 305 additions and 64 deletions

View File

@ -74,7 +74,13 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-core</artifactId>
<artifactId>springdoc-openapi-starter-webflux-api</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<scope>compile</scope>
</dependency>

View File

@ -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<ModelConverter> 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<String, ApiResponse> entry : responses.entrySet()) {
if (entry.getValue().getContent() == null) {
continue;
}
for (Map.Entry<String, MediaType> 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<ResponseMessage> 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;
}
}

View File

@ -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);
}
}

View File

@ -298,6 +298,12 @@
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-access-logging-aop</artifactId>
<version>${hsweb.framework.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
@ -307,13 +313,13 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-ui</artifactId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-springdoc-ui</artifactId>
<version>2.0.8</version>
<artifactId>knife4j-openapi3-ui</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>

View File

@ -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

23
pom.xml
View File

@ -52,11 +52,11 @@
<log4j.version>2.24.3</log4j.version>
<slf4j.version>2.0.16</slf4j.version>
<logback.version>1.5.12</logback.version>
<springdoc.version>1.8.0</springdoc.version>
<springdoc.version>2.8.6</springdoc.version>
<jackson.version>2.17.1</jackson.version>
<gson.version>2.11.0</gson.version>
<opentelemetry.version>1.39.0</opentelemetry.version>
<swagger.version>2.2.22</swagger.version>
<swagger.version>2.2.29</swagger.version>
<jna.version>5.12.1</jna.version>
<aliyun.sdk.core>4.5.2</aliyun.sdk.core>
<jsonata.version>2.4.1</jsonata.version>
@ -275,6 +275,19 @@
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<source>${project.build.jdk}</source>
<target>${project.build.jdk}</target>
</configuration>
</plugin>
</plugins>
</build>
@ -362,19 +375,19 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId>
<artifactId>springdoc-openapi-starter-common</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-core</artifactId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-ui</artifactId>
<artifactId>springdoc-openapi-starter-webflux-api</artifactId>
<version>${springdoc.version}</version>
</dependency>