备注:Knife4j  通过 com.github.xiaoymin.knife4j.spring.extension.Knife4jOpenApiCustomizer 类的 addOrderExtension 方法给 Tag 类应用 @ApiSupport 注解的 order 属性。
问题描述 在我们的一个项目中使用了 Knife4j 来显示 Swagger 的文档,具体的依赖如下
1 2 3 4 5 <dependency >     <groupId > com.github.xiaoymin</groupId >      <artifactId > knife4j-openapi3-jakarta-spring-boot-starter</artifactId >      <version > 4.3.0</version >  </dependency > 
 
同时我们有两个 Controller,它们分别是
1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController @RequestMapping("/user") @Tag(name = "用户管理") public  class  UserController  {     } @RestController @RequestMapping("/role") @Tag(name = "角色管理") public  class  RoleController  {     } 
 
默认情况下,在 Knife4j 的页面中“角色管理”显示在了“用户管理”的前面。我们期望“用户管理”显示在“角色管理”的前面。为了达到这个目的我们使用了 Knife4j 提供的注解 @ApiSupport,修改后的代码如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/user") @Tag(name = "用户管理") @ApiSupport(order = 1) public  class  UserController  {     } @RestController @RequestMapping("/role") @Tag(name = "角色管理") @ApiSupport(order = 2) public  class  RoleController  {     } 
 
但是并没有什么效果,“用户管理”并没有显示在“角色管理”的前面,即 @ApiSupport 注解没有生效。
 
原因分析 因为在前述依赖的 Knife4j 版本下,它依赖了 springdoc-openapi 。springdoc-openapi 是在 OpenAPIService 的 addTags 方法解析 io.swagger.v3.oas.annotations.tags.Tag 注解构建 io.swagger.v3.oas.models.tags.Tag 对象
1 2 3 4 5 6 7 8 9 10 11 12 private  void  addTags (List<Tag> sourceTags, Set<io.swagger.v3.oas.models.tags.Tag> tags, Locale locale)  {    Optional<Set<io.swagger.v3.oas.models.tags.Tag>> optionalTagSet = AnnotationsUtils             .getTags(sourceTags.toArray(new  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);         });     }); } 
 
注意到这个方法将解析 @Tag 注解并构建 Tag 类型的功能的功能委托给了 AnnotationsUtils 类的 getTags 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public  static  Optional<Set<Tag>> getTags (io.swagger.v3.oas.annotations.tags.Tag[] tags, boolean  skipOnlyName)  {    if  (tags == null ) {         return  Optional.empty();     }     Set<Tag> tagsList = new  LinkedHashSet <>();     for  (io.swagger.v3.oas.annotations.tags.Tag tag : tags) {         if  (StringUtils.isBlank(tag.name())) {             continue ;         }         if  (skipOnlyName &&                 StringUtils.isBlank(tag.description()) &&                 StringUtils.isBlank(tag.externalDocs().description()) &&                 StringUtils.isBlank(tag.externalDocs().url())) {             continue ;         }         Tag  tagObject  =  new  Tag ();         if  (StringUtils.isNotBlank(tag.description())) {             tagObject.setDescription(tag.description());         }         tagObject.setName(tag.name());         getExternalDocumentation(tag.externalDocs()).ifPresent(tagObject::setExternalDocs);         if  (tag.extensions().length > 0 ) {             Map<String, Object> extensions = AnnotationsUtils.getExtensions(tag.extensions());             if  (extensions != null ) {                 extensions.forEach(tagObject::addExtension);             }         }         tagsList.add(tagObject);     }     if  (tagsList.isEmpty()) {         return  Optional.empty();     }     return  Optional.of(tagsList); } 
 
注意到第 7 行到第 15 行的两个条件判断,它们的意思是如果 @Tag 注解的 name 属性为空则不构建 Tag 对象,如果调用方式传入的 skipOnlyName 为 true,那么当只有 name 属性有值时也不构建 Tag 对象。前面的例子完全符合这两个条件,因此最终没有构建 Tag 对象。这一结果会导致最终创建的 OpenAPI 对象的 tags 属性为空(页面能正常显示两个标签那是另一个问题)。
那我们为 @ApiSupport 注解的 description 属性也赋上值看看,修改后的代码如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/user") @Tag(name = "用户管理", description = "用户管理") @ApiSupport(order = 1) public  class  UserController  {     } @RestController @RequestMapping("/role") @Tag(name = "角色管理", description = "角色管理") @ApiSupport(order = 2) public  class  RoleController  {     } 
 
非常遗憾的是这并没有达到我们想要的效果,肯定还有其他遗漏的地方。我们继续分析。
经过上面的修改我们已经能够正常构建 Tag 对象,即 OpenAPI 对象的 tags 属性有值了,通过 /v3/api-docs 接口获取到了 Swagger 文档也正常的返回了 tags 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 {          "tags" :  [          {              "name" :  "角色管理" ,              "description" :  "角色管理"          } ,          {              "name" :  "用户管理" ,              "description" :  "用户管理"          }      ] ,       } 
 
我们注意到它没有 Knife4j 用来排序用的 x-order 属性。我们得想个办法把 x-order 属性加上,并且它的值就是 @ApiSupport 注解的 order 属性的值。
为了达到这个目的我们需要实现我们自己的 OpenAPIService,在构建好 Tag 对象后把 x-order 属性加在它的扩展属性 extensions 里,即我们要继承 OpenAPIService 类,并重写它的 buildTags 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import  com.github.xiaoymin.knife4j.annotations.ApiSupport;import  com.github.xiaoymin.knife4j.core.conf.ExtensionsConstants;import  io.swagger.v3.core.util.AnnotationsUtils;import  io.swagger.v3.oas.annotations.tags.Tag;import  io.swagger.v3.oas.annotations.tags.Tags;import  io.swagger.v3.oas.models.OpenAPI;import  io.swagger.v3.oas.models.Operation;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.core.annotation.AnnotatedElementUtils;import  org.springframework.core.annotation.AnnotationUtils;import  org.springframework.util.CollectionUtils;import  org.springframework.web.method.HandlerMethod;import  java.util.ArrayList;import  java.util.HashSet;import  java.util.List;import  java.util.Locale;import  java.util.Optional;import  java.util.Set;import  java.util.stream.Collectors;import  java.util.stream.Stream;public  class  CustomOpenAPIService  extends  OpenAPIService  {    private  final  PropertyResolverUtils propertyResolverUtils;     public  CustomOpenAPIService (Optional<OpenAPI> openAPI, SecurityService securityParser, SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils, Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers, Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers, Optional<JavadocProvider> javadocProvider)  {         super (openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);         this .propertyResolverUtils = propertyResolverUtils;     }     @Override      public  Operation buildTags (HandlerMethod handlerMethod, Operation operation, OpenAPI openAPI, Locale locale)  {         super .buildTags(handlerMethod, operation, openAPI, locale);                  Set<io.swagger.v3.oas.models.tags.Tag> tags = new  HashSet <>();         buildTags(handlerMethod.getBeanType(), tags, locale);         ApiSupport  apiSupport  =  AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ApiSupport.class);         if  (!CollectionUtils.isEmpty(openAPI.getTags()) && !CollectionUtils.isEmpty(tags) && apiSupport != null ) {             for  (io.swagger.v3.oas.models.tags.Tag tag : tags) {                 List<io.swagger.v3.oas.models.tags.Tag> tagetTags = openAPI.getTags().stream().filter(e -> e.equals(tag)).toList();                 for  (io.swagger.v3.oas.models.tags.Tag tagetTag : tagetTags) {                     tagetTag.addExtension(ExtensionsConstants.EXTENSION_ORDER, apiSupport.order());                 }             }         }         return  operation;     }          private  void  buildTags (Class<?> beanType, Set<io.swagger.v3.oas.models.tags.Tag> tags, Locale locale)  {         Set<Tags> tagsSet = AnnotatedElementUtils.findAllMergedAnnotations(beanType, Tags.class);         Set<Tag> classTags = tagsSet.stream().flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet());         classTags.addAll(AnnotatedElementUtils.findAllMergedAnnotations(beanType, Tag.class));         if  (!CollectionUtils.isEmpty(classTags)) {             List<Tag> allTags = new  ArrayList <>(classTags);             addTags(allTags, tags, locale);         }     }          private  void  addTags (List<Tag> sourceTags, Set<io.swagger.v3.oas.models.tags.Tag> tags, Locale locale)  {         Optional<Set<io.swagger.v3.oas.models.tags.Tag>> optionalTagSet = AnnotationsUtils.getTags(sourceTags.toArray(new  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);                 }             });         });     } } 
 
然后我们实现一个配置类,把它加入 Spring 的容器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import  io.swagger.v3.oas.models.OpenAPI;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.SecurityService;import  org.springdoc.core.utils.PropertyResolverUtils;import  org.springframework.context.annotation.Bean;import  org.springframework.context.annotation.Configuration;import  java.util.List;import  java.util.Optional;@Configuration public  class  CustomSwaggerConfiguration  {    @Bean      public  CustomOpenAPIService customOpenAPIService (Optional<OpenAPI> openAPI,                                                       SecurityService securityParser,                                                      SpringDocConfigProperties springDocConfigProperties,                                                      PropertyResolverUtils propertyResolverUtils,                                                      Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,                                                      Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,                                                      Optional<JavadocProvider> javadocProvider)  {        return  new  CustomOpenAPIService (openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);     } } 
 
现在调用 /v3/api-docs 接口,返回的 Swagger 文档数据中已经有 x-order 属性的值了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {          "tags" :  [          {              "name" :  "角色管理" ,              "description" :  "角色管理" ,              "x-order" :  2          } ,          {              "name" :  "用户管理" ,              "description" :  "用户管理" ,              "x-order" :  1          }      ] ,       } 
 
Knife4j 显示的顺序也符合我们的期望了。
解决问题 经过上面的分析后,解决问题主要在两个方面
@Tag 注解除了要给 name 属性赋值外,至少还应该给 description、externalDocs.description、externalDocs.url 之一进行赋值 
重写 OpenAPIService 类的 buildTags 方法,把 x-order 属性添加到 Tag 类的扩展属性 extensions 中