diff --git a/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/autoconfigure/SpringDocAutoConfiguration.java b/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/autoconfigure/SpringDocAutoConfiguration.java index 8214e218..5e92dce9 100644 --- a/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/autoconfigure/SpringDocAutoConfiguration.java +++ b/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/autoconfigure/SpringDocAutoConfiguration.java @@ -17,17 +17,28 @@ package top.continew.starter.apidoc.autoconfigure; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.databind.type.SimpleType; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springdoc.core.configuration.SpringDocConfiguration; +import org.springdoc.core.customizers.*; +import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springdoc.core.providers.JavadocProvider; +import org.springdoc.core.service.OpenAPIService; +import org.springdoc.core.service.SecurityService; +import org.springdoc.core.utils.PropertyResolverUtils; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -37,11 +48,13 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import top.continew.starter.apidoc.handler.OpenApiHandler; +import top.continew.starter.apidoc.util.EnumTypeUtils; import top.continew.starter.core.autoconfigure.project.ProjectProperties; +import top.continew.starter.core.enums.BaseEnum; import top.continew.starter.core.util.GeneralPropertySourceFactory; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.TimeUnit; /** @@ -51,7 +64,7 @@ * @since 1.0.0 */ @EnableWebMvc -@AutoConfiguration +@AutoConfiguration(before = SpringDocConfiguration.class) @EnableConfigurationProperties(SpringDocExtensionProperties.class) @PropertySource(value = "classpath:default-api-doc.yml", factory = GeneralPropertySourceFactory.class) public class SpringDocAutoConfiguration implements WebMvcConfigurer { @@ -127,6 +140,112 @@ public GlobalOpenApiCustomizer globalOpenApiCustomizer(SpringDocExtensionPropert }; } + /** + * 自定义 openapi 处理器 + */ + @Bean + public OpenAPIService openApiBuilder(Optional openAPI, + SecurityService securityParser, + SpringDocConfigProperties springDocConfigProperties, + PropertyResolverUtils propertyResolverUtils, + Optional> openApiBuilderCustomisers, + Optional> serverBaseUrlCustomisers, + Optional javadocProvider) { + return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider); + } + + /** + * 展示 枚举类型和值 + * + * @return + */ + @Bean + public ParameterCustomizer customParameterCustomizer() { + return (parameterModel, methodParameter) -> { + // 判断方法参数类型是否为 IBaseEnum 的子类型 + if (ClassUtil.isAssignable(BaseEnum.class, methodParameter.getParameterType())) { + String description = parameterModel.getDescription(); + // TODO 会重复调用,有什么优雅的判读方式吗? + if (StrUtil.contains(description, "color:red")) { + return parameterModel; + } + Schema schema = parameterModel.getSchema(); + + // 获取方法参数类型的所有枚举常量 + BaseEnum[] enumConstants = (BaseEnum[])methodParameter.getParameterType().getEnumConstants(); + List list = new ArrayList<>(); + Map descMap = new HashMap<>(); + + // 遍历枚举常量,获取其值和描述 + for (BaseEnum constant : enumConstants) { + list.add(constant.getValue().toString()); + descMap.put(constant.getValue(), constant.getDescription()); + } + + // 枚举值类型 + String enumValueType = EnumTypeUtils.getEnumValueTypeAsString(methodParameter.getParameterType()); + schema.setType(enumValueType); + switch (enumValueType) { + case "integer" -> schema.setFormat("int32"); + case "long" -> schema.setFormat("int64"); + case "number" -> schema.setFormat("double"); + } + + // 设置枚举值列表和描述 + schema.setEnum(list); + parameterModel.setDescription(description + "" + descMap + ""); + } + return parameterModel; + }; + } + + /** + * 展示 枚举类型和值 + * + * @return + */ + @Bean + public PropertyCustomizer customPropertyCustomizer() { + return (schema, type) -> { + Class rawClass; + // 获取原始类的类型 + if (type.getType() instanceof SimpleType) { + rawClass = ((SimpleType)type.getType()).getRawClass(); + } else if (type.getType() instanceof CollectionType) { + rawClass = ((CollectionType)type.getType()).getContentType().getRawClass(); + } else { + rawClass = Object.class; + } + + // 检查原始类是否实现了 IBaseEnum 接口 + if (ClassUtil.isAssignable(BaseEnum.class, rawClass)) { + BaseEnum[] enumConstants = (BaseEnum[])rawClass.getEnumConstants(); + List list = new ArrayList<>(); + Map descMap = new HashMap<>(); + // 遍历枚举常量,获取其值和描述 + for (BaseEnum constant : enumConstants) { + list.add(constant.getValue().toString()); + descMap.put(constant.getValue(), constant.getDescription()); + } + // 获取泛型类型 + String enumValueType = EnumTypeUtils.getEnumValueTypeAsString(rawClass); + schema.setType(enumValueType); + // 根据枚举值类型设置 schema 的格式 + switch (enumValueType) { + case "integer" -> schema.setFormat("int32"); + case "long" -> schema.setFormat("int64"); + case "number" -> schema.setFormat("double"); + } + + // 设置枚举值列表和描述 + schema.setEnum(list); + schema.setDescription(schema.getDescription() + "" + descMap + ""); + return schema; + } + return schema; + }; + } + @PostConstruct public void postConstruct() { log.debug("[ContiNew Starter] - Auto Configuration 'ApiDoc' completed initialization."); diff --git a/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/handler/OpenApiHandler.java b/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/handler/OpenApiHandler.java new file mode 100644 index 00000000..36052c8e --- /dev/null +++ b/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/handler/OpenApiHandler.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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 top.continew.starter.apidoc.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import io.swagger.v3.core.jackson.TypeNameResolver; +import io.swagger.v3.core.util.AnnotationsUtils; +import io.swagger.v3.oas.annotations.tags.Tags; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.tags.Tag; +import org.apache.commons.lang3.StringUtils; +import org.springdoc.core.customizers.OpenApiBuilderCustomizer; +import org.springdoc.core.customizers.ServerBaseUrlCustomizer; +import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springdoc.core.providers.JavadocProvider; +import org.springdoc.core.service.OpenAPIService; +import org.springdoc.core.service.SecurityService; +import org.springdoc.core.utils.PropertyResolverUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.web.method.HandlerMethod; + +import java.io.StringReader; +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 自定义 openapi 处理器 对源码功能进行修改 增强使用 + */ +@SuppressWarnings("all") +public class OpenApiHandler extends OpenAPIService { + + /** + * The Basic error controller. + */ + private static Class basicErrorController; + + /** + * The Security parser. + */ + private final SecurityService securityParser; + + /** + * The Mappings map. + */ + private final Map mappingsMap = new HashMap<>(); + + /** + * The Springdoc tags. + */ + private final Map springdocTags = new HashMap<>(); + + /** + * The Open api builder customisers. + */ + private final Optional> openApiBuilderCustomisers; + + /** + * The server base URL customisers. + */ + private final Optional> serverBaseUrlCustomizers; + + /** + * The Spring doc config properties. + */ + private final SpringDocConfigProperties springDocConfigProperties; + + /** + * The Cached open api map. + */ + private final Map cachedOpenAPI = new HashMap<>(); + + /** + * The Property resolver utils. + */ + private final PropertyResolverUtils propertyResolverUtils; + + /** + * The javadoc provider. + */ + private final Optional javadocProvider; + + /** + * The Context. + */ + private ApplicationContext context; + + /** + * The Open api. + */ + private OpenAPI openAPI; + + /** + * The Is servers present. + */ + private boolean isServersPresent; + + /** + * The Server base url. + */ + private String serverBaseUrl; + + /** + * Instantiates a new Open api builder. + * + * @param openAPI the open api + * @param securityParser the security parser + * @param springDocConfigProperties the spring doc config properties + * @param propertyResolverUtils the property resolver utils + * @param openApiBuilderCustomizers the open api builder customisers + * @param serverBaseUrlCustomizers the server base url customizers + * @param javadocProvider the javadoc provider + */ + public OpenApiHandler(Optional openAPI, + SecurityService securityParser, + SpringDocConfigProperties springDocConfigProperties, + PropertyResolverUtils propertyResolverUtils, + Optional> openApiBuilderCustomizers, + Optional> serverBaseUrlCustomizers, + Optional javadocProvider) { + super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider); + if (openAPI.isPresent()) { + this.openAPI = openAPI.get(); + if (this.openAPI.getComponents() == null) + this.openAPI.setComponents(new Components()); + if (this.openAPI.getPaths() == null) + this.openAPI.setPaths(new Paths()); + if (!CollectionUtils.isEmpty(this.openAPI.getServers())) + this.isServersPresent = true; + } + this.propertyResolverUtils = propertyResolverUtils; + this.securityParser = securityParser; + this.springDocConfigProperties = springDocConfigProperties; + this.openApiBuilderCustomisers = openApiBuilderCustomizers; + this.serverBaseUrlCustomizers = serverBaseUrlCustomizers; + this.javadocProvider = javadocProvider; + if (springDocConfigProperties.isUseFqn()) + TypeNameResolver.std.setUseFqn(true); + } + + @Override + public Operation buildTags(HandlerMethod handlerMethod, Operation operation, OpenAPI openAPI, Locale locale) { + + Set tags = new HashSet<>(); + Set tagsStr = new HashSet<>(); + + buildTagsFromMethod(handlerMethod.getMethod(), tags, tagsStr, locale); + buildTagsFromClass(handlerMethod.getBeanType(), tags, tagsStr, locale); + + if (!CollectionUtils.isEmpty(tagsStr)) + tagsStr = tagsStr.stream() + .map(str -> propertyResolverUtils.resolve(str, locale)) + .collect(Collectors.toSet()); + + if (springdocTags.containsKey(handlerMethod)) { + Tag tag = springdocTags.get(handlerMethod); + tagsStr.add(tag.getName()); + if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) { + openAPI.addTagsItem(tag); + } + } + + if (!CollectionUtils.isEmpty(tagsStr)) { + if (CollectionUtils.isEmpty(operation.getTags())) + operation.setTags(new ArrayList<>(tagsStr)); + else { + Set operationTagsSet = new HashSet<>(operation.getTags()); + operationTagsSet.addAll(tagsStr); + operation.getTags().clear(); + operation.getTags().addAll(operationTagsSet); + } + } + + if (isAutoTagClasses(operation)) { + + if (javadocProvider.isPresent()) { + String description = javadocProvider.get().getClassJavadoc(handlerMethod.getBeanType()); + if (StringUtils.isNotBlank(description)) { + Tag tag = new Tag(); + + // 自定义部分 修改使用java注释当tag名 + List list = IoUtil.readLines(new StringReader(description), new ArrayList<>()); + // tag.setName(tagAutoName); + tag.setName(list.get(0)); + operation.addTagsItem(list.get(0)); + + tag.setDescription(description); + if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) { + openAPI.addTagsItem(tag); + } + } + } else { + String tagAutoName = splitCamelCase(handlerMethod.getBeanType().getSimpleName()); + operation.addTagsItem(tagAutoName); + } + } + + if (!CollectionUtils.isEmpty(tags)) { + // Existing tags + List openApiTags = openAPI.getTags(); + if (!CollectionUtils.isEmpty(openApiTags)) + tags.addAll(openApiTags); + openAPI.setTags(new ArrayList<>(tags)); + } + + // Handle SecurityRequirement at operation level + io.swagger.v3.oas.annotations.security.SecurityRequirement[] securityRequirements = securityParser + .getSecurityRequirements(handlerMethod); + if (securityRequirements != null) { + if (securityRequirements.length == 0) + operation.setSecurity(Collections.emptyList()); + else + securityParser.buildSecurityRequirement(securityRequirements, operation); + } + + return operation; + } + + private void buildTagsFromMethod(Method method, Set tags, Set tagsStr, Locale locale) { + // method tags + Set tagsSet = AnnotatedElementUtils.findAllMergedAnnotations(method, Tags.class); + Set methodTags = tagsSet.stream() + .flatMap(x -> Stream.of(x.value())) + .collect(Collectors.toSet()); + methodTags.addAll(AnnotatedElementUtils + .findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.tags.Tag.class)); + if (!CollectionUtils.isEmpty(methodTags)) { + tagsStr.addAll(toSet(methodTags, tag -> propertyResolverUtils.resolve(tag.name(), locale))); + List allTags = new ArrayList<>(methodTags); + addTags(allTags, tags, locale); + } + } + + private void addTags(List sourceTags, Set tags, Locale locale) { + Optional> optionalTagSet = AnnotationsUtils.getTags(sourceTags + .toArray(new io.swagger.v3.oas.annotations.tags.Tag[0]), true); + optionalTagSet.ifPresent(tagsSet -> { + tagsSet.forEach(tag -> { + tag.name(propertyResolverUtils.resolve(tag.getName(), locale)); + tag.description(propertyResolverUtils.resolve(tag.getDescription(), locale)); + if (tags.stream().noneMatch(t -> t.getName().equals(tag.getName()))) + tags.add(tag); + }); + }); + } + + /** + * 将collection转化为Set集合,但是两者的泛型不同
+ * {@code Collection ------> Set } + * + * @param collection 需要转化的集合 + * @param function collection中的泛型转化为set泛型的lambda表达式 + * @param collection中的泛型 + * @param Set中的泛型 + * @return 转化后的Set + */ + public static Set toSet(Collection collection, Function function) { + if (CollUtil.isEmpty(collection) || function == null) { + return CollUtil.newHashSet(); + } + return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.toSet()); + } + +} diff --git a/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/util/EnumTypeUtils.java b/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/util/EnumTypeUtils.java new file mode 100644 index 00000000..a84410a3 --- /dev/null +++ b/continew-starter-api-doc/src/main/java/top/continew/starter/apidoc/util/EnumTypeUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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 top.continew.starter.apidoc.util; + +import top.continew.starter.core.enums.BaseEnum; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * 枚举类型工具 + * + * @Author echo + * @date 2024/07/31 + */ +public class EnumTypeUtils { + + /** + * 获取enum值类型 + * + * @param enumClass enum class + * @return {@link String } + */ + public static String getEnumValueTypeAsString(Class enumClass) { + try { + // 获取枚举类实现的所有接口 + Type[] interfaces = enumClass.getGenericInterfaces(); + // 遍历所有接口 + for (Type type : interfaces) { + // 检查接口是否为参数化类型 + if (type instanceof ParameterizedType parameterizedType) { + // 检查接口的原始类型是否为 BaseEnum + if (parameterizedType.getRawType() == BaseEnum.class) { + Type actualType = parameterizedType.getActualTypeArguments()[0]; + // 检查实际类型参数是否为类类型 + if (actualType instanceof Class actualClass) { + if (actualClass == Integer.class) { + return "integer"; + } else if (actualClass == Long.class) { + return "long"; + } else if (actualClass == Double.class) { + return "number"; + } else if (actualClass == String.class) { + return "string"; + } + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return "string"; + } +}