diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java index 74f9b73b0..cc55c7814 100644 --- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java @@ -6,18 +6,14 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import lombok.RequiredArgsConstructor; import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.doc.core.customizer.ClassTagOperationCustomizer; +import org.dromara.common.doc.core.customizer.JavadocOperationCustomizer; import org.dromara.common.doc.config.properties.SpringDocProperties; import org.dromara.common.doc.core.resolver.JavadocResolver; import org.dromara.common.doc.core.resolver.SaTokenAnnotationMetadataJavadocResolver; -import org.dromara.common.doc.handler.OpenApiHandler; import org.springdoc.core.configuration.SpringDocConfiguration; -import org.springdoc.core.customizers.OpenApiBuilderCustomizer; import org.springdoc.core.customizers.OpenApiCustomizer; -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.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -91,16 +87,21 @@ public class SpringDocConfig { } /** - * 自定义 openapi 处理器 + * Controller 类级标签增强 */ @Bean - public OpenAPIService openApiBuilder(Optional openAPI, - SecurityService securityParser, - SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils, - Optional> openApiBuilderCustomisers, - Optional> serverBaseUrlCustomisers, Optional javadocProvider, - List javadocResolvers) { - return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider, javadocResolvers); + public ClassTagOperationCustomizer classTagOperationCustomizer(Optional javadocProvider, + PropertyResolverUtils propertyResolverUtils) { + return new ClassTagOperationCustomizer(javadocProvider, propertyResolverUtils); + } + + /** + * 方法 JavaDoc 与权限描述增强 + */ + @Bean + public JavadocOperationCustomizer javadocOperationCustomizer(Optional javadocProvider, + List javadocResolvers) { + return new JavadocOperationCustomizer(javadocProvider, javadocResolvers); } /** diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/customizer/ClassTagOperationCustomizer.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/customizer/ClassTagOperationCustomizer.java new file mode 100644 index 000000000..b3757c374 --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/customizer/ClassTagOperationCustomizer.java @@ -0,0 +1,148 @@ +package org.dromara.common.doc.core.customizer; + +import cn.hutool.core.io.IoUtil; +import io.swagger.v3.core.util.AnnotationsUtils; +import io.swagger.v3.oas.annotations.tags.Tags; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springdoc.core.providers.JavadocProvider; +import org.springdoc.core.service.OpenAPIService; +import org.springdoc.core.utils.PropertyResolverUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.web.method.HandlerMethod; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Controller 类级标签增强。 + * + * @author Lion Li + */ +@RequiredArgsConstructor +public class ClassTagOperationCustomizer implements OperationCustomizer, OpenApiCustomizer { + + private final Optional javadocProvider; + + private final PropertyResolverUtils propertyResolverUtils; + + private final Map tags = new ConcurrentHashMap<>(); + + private final Set replacedAutoTagNames = ConcurrentHashMap.newKeySet(); + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + Class beanType = handlerMethod.getBeanType(); + List classTags = getClassTags(beanType); + if (!CollectionUtils.isEmpty(classTags)) { + // 优先使用 Controller 类上的 @Tag / @Tags,保持 Swagger 原生注解语义 + addAnnotationTags(operation, classTags); + return operation; + } + + String tagName = getClassJavadocTagName(beanType); + if (StringUtils.isBlank(tagName)) { + return operation; + } + + String autoTagName = OpenAPIService.splitCamelCase(beanType.getSimpleName()); + if (!shouldUseClassJavadocTag(operation, autoTagName)) { + return operation; + } + + // 无显式 @Tag 时,将 springdoc 自动生成的类名 tag 替换为类 JavaDoc 第一行 + removeOperationTag(operation, autoTagName); + addOperationTag(operation, tagName); + replacedAutoTagNames.add(autoTagName); + tags.putIfAbsent(tagName, new Tag().name(tagName).description(javadocProvider.get().getClassJavadoc(beanType))); + return operation; + } + + @Override + public void customise(OpenAPI openApi) { + if (!CollectionUtils.isEmpty(openApi.getTags()) && !CollectionUtils.isEmpty(replacedAutoTagNames)) { + // 移除已被 JavaDoc tag 替换的默认类名 tag,避免 Swagger UI 出现空分组 + openApi.getTags().removeIf(tag -> replacedAutoTagNames.contains(tag.getName())); + } + // 将类级 @Tag 描述或 JavaDoc 描述补充到 OpenAPI 顶层 tags + tags.values().forEach(tag -> { + if (openApi.getTags() == null || openApi.getTags().stream().noneMatch(item -> Objects.equals(item.getName(), tag.getName()))) { + openApi.addTagsItem(tag); + } + }); + } + + private List getClassTags(Class beanType) { + Set tagsSet = AnnotatedElementUtils.findAllMergedAnnotations(beanType, Tags.class); + Set mergedTags = tagsSet.stream() + .flatMap(item -> Stream.of(item.value())) + .collect(Collectors.toSet()); + mergedTags.addAll(AnnotatedElementUtils.findAllMergedAnnotations(beanType, io.swagger.v3.oas.annotations.tags.Tag.class)); + return new ArrayList<>(mergedTags); + } + + private void addAnnotationTags(Operation operation, List classTags) { + classTags.stream() + .map(io.swagger.v3.oas.annotations.tags.Tag::name) + .map(name -> propertyResolverUtils.resolve(name, Locale.getDefault())) + .filter(StringUtils::isNotBlank) + .forEach(name -> addOperationTag(operation, name)); + + AnnotationsUtils.getTags(classTags.toArray(new io.swagger.v3.oas.annotations.tags.Tag[0]), true) + .ifPresent(items -> items.forEach(tag -> { + tag.name(propertyResolverUtils.resolve(tag.getName(), Locale.getDefault())); + tag.description(propertyResolverUtils.resolve(tag.getDescription(), Locale.getDefault())); + if (StringUtils.isNotBlank(tag.getName())) { + tags.putIfAbsent(tag.getName(), tag); + } + })); + } + + private String getClassJavadocTagName(Class beanType) { + if (javadocProvider.isEmpty()) { + return null; + } + String description = javadocProvider.get().getClassJavadoc(beanType); + if (StringUtils.isBlank(description)) { + return null; + } + // 与原 OpenApiHandler 保持一致:类 JavaDoc 第一行作为 tag 名,完整 JavaDoc 作为 tag 描述 + List lines = IoUtil.readLines(new StringReader(description), new ArrayList<>()); + return lines.stream().filter(StringUtils::isNotBlank).findFirst().orElse(null); + } + + private boolean shouldUseClassJavadocTag(Operation operation, String autoTagName) { + return CollectionUtils.isEmpty(operation.getTags()) || operation.getTags().contains(autoTagName); + } + + private void addOperationTag(Operation operation, String tagName) { + if (operation.getTags() == null) { + operation.setTags(new ArrayList<>()); + } + if (!operation.getTags().contains(tagName)) { + operation.addTagsItem(tagName); + } + } + + private void removeOperationTag(Operation operation, String tagName) { + if (!CollectionUtils.isEmpty(operation.getTags())) { + operation.getTags().removeIf(item -> Objects.equals(item, tagName)); + } + } + +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/customizer/JavadocOperationCustomizer.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/customizer/JavadocOperationCustomizer.java new file mode 100644 index 000000000..a8f251bb1 --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/customizer/JavadocOperationCustomizer.java @@ -0,0 +1,58 @@ +package org.dromara.common.doc.core.customizer; + +import io.swagger.v3.oas.models.Operation; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.dromara.common.doc.core.resolver.JavadocResolver; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springdoc.core.providers.JavadocProvider; +import org.springframework.util.CollectionUtils; +import org.springframework.web.method.HandlerMethod; + +import java.util.List; +import java.util.Optional; + +/** + * 方法 JavaDoc 与扩展描述增强。 + * + * @author Lion Li + */ +@RequiredArgsConstructor +public class JavadocOperationCustomizer implements OperationCustomizer { + + private final Optional javadocProvider; + + private final List javadocResolvers; + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + javadocProvider.ifPresent(provider -> { + String description = provider.getMethodJavadocDescription(handlerMethod.getMethod()); + if (StringUtils.isNotBlank(description)) { + // 使用方法 JavaDoc 首句作为接口摘要,完整 JavaDoc 作为接口描述 + operation.setSummary(provider.getFirstSentence(description)); + operation.setDescription(description); + } + }); + + if (CollectionUtils.isEmpty(javadocResolvers)) { + return operation; + } + + StringBuilder description = new StringBuilder(Optional.ofNullable(operation.getDescription()).orElse("")); + List resolvedDescriptions = javadocResolvers.stream() + .sorted() + // 只执行支持当前 HandlerMethod 的扩展解析器,避免无注解接口被追加权限说明 + .filter(resolver -> resolver.supports(handlerMethod)) + .map(resolver -> resolver.resolve(handlerMethod, operation)) + .filter(StringUtils::isNotBlank) + .toList(); + if (!resolvedDescriptions.isEmpty()) { + // 在原方法 JavaDoc 后追加权限等扩展描述,保持现有 resolver 扩展点 + resolvedDescriptions.forEach(description::append); + operation.setDescription(description.toString()); + } + return operation; + } + +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java deleted file mode 100644 index f26ce5b24..000000000 --- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java +++ /dev/null @@ -1,302 +0,0 @@ -package org.dromara.common.doc.handler; - -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 lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.dromara.common.core.utils.StreamUtils; -import org.dromara.common.doc.core.resolver.JavadocResolver; -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.stream.Collectors; -import java.util.stream.Stream; - -/** - * 自定义 openapi 处理器 - * 对源码功能进行修改 增强使用 - */ -@Slf4j -@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; - - /** - * Javadoc解析器接口 - */ - private final List javadocResolvers; - - /** - * 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 - * @param javadocResolvers Javadoc 解析器列表 - */ - public OpenApiHandler(Optional openAPI, SecurityService securityParser, - SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils, - Optional> openApiBuilderCustomizers, - Optional> serverBaseUrlCustomizers, - Optional javadocProvider, - List javadocResolvers) { - 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; - this.javadocResolvers = javadocResolvers == null ? new ArrayList<>() : javadocResolvers; - if (springDocConfigProperties.isUseFqn()) - TypeNameResolver.std.setUseFqn(true); - } - - /** - * 构建接口标签、权限描述与方法摘要。 - * - * @param handlerMethod Handler 方法 - * @param operation OpenAPI 操作对象 - * @param openAPI OpenAPI 文档对象 - * @param locale 当前语言环境 - * @return 处理后的操作对象 - */ - @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)) { - io.swagger.v3.oas.models.tags.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)) { - io.swagger.v3.oas.models.tags.Tag tag = new io.swagger.v3.oas.models.tags.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); - } - - if (javadocProvider.isPresent()) { - String description = javadocProvider.get().getMethodJavadocDescription(handlerMethod.getMethod()); - String summary = javadocProvider.get().getFirstSentence(description); - if (StringUtils.isNotBlank(description)){ - operation.setSummary(summary); - } - // 调用解析器提取JavaDoc中的权限信息 - if (javadocResolvers != null && !javadocResolvers.isEmpty()) { - for (JavadocResolver resolver : javadocResolvers) { - String desc = resolver.resolve(handlerMethod, operation); - description = description + desc; - } - operation.setDescription(description); - } - } - - return operation; - } - - /** - * 从方法注解中提取标签信息。 - * - * @param method 方法对象 - * @param tags 标签集合 - * @param tagsStr 标签名称集合 - * @param locale 当前语言环境 - */ - 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(StreamUtils.toSet(methodTags, tag -> propertyResolverUtils.resolve(tag.name(), locale))); - List allTags = new ArrayList<>(methodTags); - addTags(allTags, tags, locale); - } - } - - /** - * 将注解标签转换并合并到 OpenAPI 标签集合。 - * - * @param sourceTags 注解标签列表 - * @param tags OpenAPI 标签集合 - * @param 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); - }); - }); - } - -}