SpringDoc动态API分组实战:配置文件读取 + 编程式Bean构建,实现灵活分组管理

前言 在SpringBoot项目中,SpringDoc(OpenAPI 3集成工具)的分组功能是API文档管理的关键一环,但它的原生支持却常让人抓狂:原生分组只能基于简单规则如路径前缀划分,功能单一;配置式分组...

前言

在SpringBoot项目中,SpringDoc(OpenAPI 3集成工具)的分组功能是API文档管理的关键一环,但它的原生支持却常让人抓狂:原生分组只能基于简单规则如路径前缀划分,功能单一;配置式分组虽通过yml文件定义,却缺乏动态过滤和扩展能力;而编程式分组又得硬编码GroupedOpenApi Bean,项目一大就得疯狂复制粘贴,维护起来堪比"屎山"堆砌!


作为开发者,我们需要的是一个平衡点:保留配置文件的易读性与集中管理,同时注入编程式的灵活性,实现分组规则的动态加载。于是,我设计了一个轻量级解决方案——通过读取配置文件(如application.yml)中的分组规则,自动生成GroupedOpenApi Bean,无需手动写代码,就能一键生成多维度分组!本文将手把手带您实现这个核心方法:从配置解析到Bean动态注册,最后生成分组文档,代码开源即用。


一、配置式分组

1、最基础分组

首先我们先来看看配置式分组的配置方式,下面的示例中,第一个分组我会把所有的配置项全部列出来,方便大家观察


springdoc:

  group-configs:

    - group: '默认分组'

      # 匹配的接口路径

      paths-to-match: '/**'

      # 排除的接口路径

      # paths-to-exclude: '/**/error/**'

      # 匹配的包路径

      # packages-to-scan: 'com.ren.main.controller'

      # 排除的包路径

      # packages-to-exclude: 'com.ren.main.controller.file'

      # 匹配请求头条件

      # headers-to-match: "X-Version=2.0"

      # 匹配请求内容的媒体类型(MIME类型)

      # consumes-to-match: "application/json"

      # 匹配响应内容的媒体类型(MIME类型)

      # produces-to-match: "application/json"

      # 定义配置的可读名称

      # display-name: '默认分组'

    - group: '商品模块'

      paths-to-match: '/api/product/**'

    - group: '用户模块'

      packages-to-scan: 'com.ren.main.controller.member'

    - group: '文件模块'

      packages-to-scan: 'com.ren.main.controller.file'


attachments-2025-08-ms1ljqHS689ee6f700534,png
大家看,分组成功的实现了,但是呢,这个只是实现了一个最简单的分组,其中,商品、用户、文件模块分别显示自己的内容,但是在默认分组中,他把该系统中所有的接口全都显示出来了

2、实现默认分组仅显示其他分组未显示的内容
好,我们现在提出第一个需求,如果我们现在需要默认分组显示出没有在其他分组中显示的接口该怎么写?如下:

springdoc:
  group-configs:
    - group: '默认分组'
      # 排除的接口路径
      paths-to-exclude: 
      - '/api/product/**'
      packages-to-exclude:
      - 'com.ren.main.controller.member'
      - 'com.ren.main.controller.file'
    - group: '商品模块'
      paths-to-match: '/api/product/**'
    - group: '用户模块'
      packages-to-scan: 'com.ren.main.controller.member'
    - group: '文件模块'
      packages-to-scan: 'com.ren.main.controller.file'

attachments-2025-08-PUHRSfa0689ee71f82a20,png

大家看,功能实现了,但是大家有没有觉得他这个写的太麻烦了呢

3、提出需求
我们这里先把这个问题放下,继续提出第二个问题,后面我们一起解决,我们如果某个Controller中有很多接口,有些想要显示,有些不想要显示,该怎么办呢?

有些朋友可能会说,那就在上面一个个配置不就行了吗?还是那句话,可以,但是不优雅

那我们给出一种思路,SpringDoc中有一个注解@Operation,它本身是用来描述接口信息的,但是我们可以这样,我们要求,只有标注了这个注解,描述了接口信息的接口才允许展示在界面中

那么,我们想要的这个需求,使用配置式分组就无法实现了,我们就要用到下一个内容,编程式分组

二、编程式分组
我们先使用编程式分组写出最基础的分组,一步步推进

@Configuration
@OpenAPIDefinition(info = @Info(
    title = "项目API文档",
    version = "1.0",
    description = "SpringBoot项目接口文档"
))
public class SpringDocConfig {
//默认分组
@Bean
public GroupedOpenApi defaultGroup() {
return GroupedOpenApi.builder().group("默认分组").pathsToMatch("/**")
.build();
}

//商品分组的配置(使用请求路径扫描的方式进行配置)
@Bean
public GroupedOpenApi userGroup() {
// 使用路径匹配方式:仅包含 /api/product/** 下的接口
return GroupedOpenApi.builder().group("商品模块").pathsToMatch("/api/product/**")
.build();
}

//会员分组的配置(使用包扫描的方式进行配置)
@Bean
public GroupedOpenApi productGroup() {
// 使用包扫描方式:扫描 com.ren.main.controller.member 包下的所有接口
return GroupedOpenApi.builder().group("用户模块").packagesToScan("com.ren.main.controller.member")
.build();
}

//文件分组的配置(使用包扫描的方式进行配置)
@Bean
public GroupedOpenApi fileGroup() {
// 使用包扫描方式:扫描 com.ren.main.controller.file 包下的所有接口
return GroupedOpenApi.builder().group("文件模块").packagesToScan("com.ren.main.controller.file")
.build();
}
}


attachments-2025-08-e9cf6f6a689ee76137e6d,png

大家可以看到功能已经实现了,和上面配置式分组是一样的效果

2、实现默认分组仅显示其他分组未显示的内容

接下来我们使用编程式分组的方式实现默认分组仅仅显示其他分组没有显示的接口,不显示全部接口

@Configuration

@OpenAPIDefinition(info = @Info(

    title = "项目API文档",

    version = "1.0",

    description = "SpringBoot项目接口文档"

))

public class SpringDocConfig {

//默认分组

@Bean

public GroupedOpenApi defaultGroup() {

return GroupedOpenApi.builder()

.group("默认分组")

.pathsToExclude("/api/product/**")

.packagesToExclude("com.ren.main.controller.file","com.ren.main.controller.member")

.build();

}


//商品分组的配置(使用请求路径扫描的方式进行配置)

@Bean

public GroupedOpenApi userGroup() {

// 使用路径匹配方式:仅包含 /api/product/** 下的接口

return GroupedOpenApi.builder()

.group("商品模块")

.pathsToMatch("/api/product/**")

.build();

}


//会员分组的配置(使用包扫描的方式进行配置)

@Bean

public GroupedOpenApi productGroup() {

// 使用包扫描方式:扫描 com.ren.main.controller.member 包下的所有接口

return GroupedOpenApi.builder()

.group("用户模块")

.packagesToScan("com.ren.main.controller.member")

.build();

}


//文件分组的配置(使用包扫描的方式进行配置)

@Bean

public GroupedOpenApi fileGroup() {

// 使用包扫描方式:扫描 com.ren.main.controller.file 包下的所有接口

return GroupedOpenApi.builder()

.group("文件模块")

.packagesToScan("com.ren.main.controller.file")

.build();

}

}

attachments-2025-08-1dIycfxl689ee9bd8d6a0,png

成功了

3、实现使用@Operation注解来限制哪些接口需要显示

我们先看Controller代码

attachments-2025-08-77BAHQwd689ee9cac8bf1,png

接下来,我们编写配置类代码

@Configuration

@OpenAPIDefinition(info = @Info(

    title = "项目API文档",

    version = "1.0",

    description = "SpringBoot项目接口文档"

))

public class SpringDocConfig {

//默认分组

@Bean

public GroupedOpenApi defaultGroup() {

return GroupedOpenApi.builder()

.group("默认分组")

.pathsToExclude("/api/product/**")

.packagesToExclude("com.ren.main.controller.file","com.ren.main.controller.member")

.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class)) // 过滤条件:只包含标注@Operation的接口

.build();

}


//商品分组的配置(使用请求路径扫描的方式进行配置)

@Bean

public GroupedOpenApi userGroup() {

// 使用路径匹配方式:仅包含 /api/product/** 下的接口

return GroupedOpenApi.builder()

.group("商品模块")

.pathsToMatch("/api/product/**")

.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class)) // 过滤条件:只包含标注@Operation的接口

.build();

}


//会员分组的配置(使用包扫描的方式进行配置)

@Bean

public GroupedOpenApi productGroup() {

// 使用包扫描方式:扫描 com.ren.main.controller.member 包下的所有接口

return GroupedOpenApi.builder()

.group("用户模块")

.packagesToScan("com.ren.main.controller.member")

.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class)) // 过滤条件:只包含标注@Operation的接口

.build();

}


//文件分组的配置(使用包扫描的方式进行配置)

@Bean

public GroupedOpenApi fileGroup() {

// 使用包扫描方式:扫描 com.ren.main.controller.file 包下的所有接口

return GroupedOpenApi.builder()

.group("文件模块")

.packagesToScan("com.ren.main.controller.file")

.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class)) // 过滤条件:只包含标注@Operation的接口

.build();

}

}


我们在上面配置类中主要给每个分组都添加了一行

.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class)) // 过滤条件:只包含标注@Operation的接口

addOpenApiMethodFilter是添加一个针对方法的过滤器,它的参数允许传入一个函数式接口,这个函数式接口内容就是用来判断这个方法上方是否包含@Operation注解,包含则返回true显示,不包含则返回false不显示
我们来看看效果:
attachments-2025-08-VuXv1ASi689ee9f92a7ac,png
到这里为止我们上面提出的两个问题都解决了,但是不知道大家有没有感觉,这个写的也太臃肿了,这才三四个分组,就写了这么一大串,我要有十几个分组还了得,而且后面如果一旦要加一个需求,这十几个分组还要都再改一遍

那么我们有没有一个办法,既可以像配置式分组一样简单写一下配置文件就能完成配置,又能包含编程式分组这些特殊好用的功能呢

SpringDoc原生并没有支持,就需要我们手动来实现了

三、自实现灵活分组
在我们实现功能之前,我们需要先讲几个前置知识点,当然,大家如果原本就了解,可以跳过

1、Bean
1.1、是什么
在 Spring(包括 Spring Boot)中,​Bean 是由 Spring IoC(Inversion of Control)容器自动进行实例化、组装和管理的 Java 对象。​
也就是说Bean本身其实就是一个Java对象,只不过不是由我们创建的,而是由SpringIoc容器进行创建的,我们用的时候直接用就好,省的去创建了
而SpringDoc的分组,其实就是SpringDoc在启动的时候,去查找系统中存在几个GroupedOpenApi类型的Bean,然后将这些Bean注册为分组

1.2、常规创建Bean的几种方式
@Service:标识服务层 Bean(业务逻辑)。
@Repository:标识数据访问层 Bean(持久层),通常还包含异常转换。
@Controller / @RestController:标识 Web 控制器层 Bean,处理 HTTP 请求。
@Configuration:标识配置类,用于定义其他 Bean(配合 @Bean 方法)。
这里只写出了常规创建Bean的几种方式,并不是所有,比如下面我们创建Bean的方式就不是这几种中的

2、BeanDefinition与BeanDefinitionRegistryPostProcessor
2.1、BeanDefinition
2.1.1、是什么
大家可以简单的把它理解为Bean的图纸,什么意思呢?
要理解这个问题,我们要先知道Spring是如何创建Bean的,Spring创建Bean要经过两个流程:
5. 创建图纸:先在容器初始化的一开始,将所有需要创建的Bean生成为一张张图纸,这个图纸定义了Bean的各种信息,比如这个 Bean 的类是什么(class属性)、作用域(单例还是多例?)、是否懒加载、依赖哪些其他零件(Bean)、初始化方法、销毁方法等等,这个图纸就是BeanDefinition
6. 正式生成:在容器初始化的最后,Spring会收集所有的Bean图纸,按照图纸中所定义的一系列信息,正式生成Bean实例

2.1.2、有什么用
当我们在一开始并不知道要创建多少个Bean,需要根据配置文件中的配置来动态的生成Bean时,常规的生成Bean的方式就无效了(大家可以想一想,常规的创建Bean的方式无非就是使用上面讲过的注解生成,但是我们现在连要创建多少个Bean都不知道,那这个注解要加在哪里),这时候我们就需要用到BeanDefinition来动态的生成Bean图纸,让Spring在最后帮我们根据这个图纸进行实例化

2.2、BeanDefinitionRegistryPostProcessor
2.2.1、是什么
它是 Spring 提供的一个非常重要的 扩展点接口。它允许你在 Spring 完成所有常规方式(注解扫描、XML解析、配置类解析)的 BeanDefinition 收集之后,但在根据这些 BeanDefinition 创建任何 Bean 实例之前,介入容器启动过程

2.2.2、有什么用
获取所有现有的“图纸”: 通过 registry 参数,你可以获取到当前 Spring 容器已经收集到的所有 BeanDefinition(包括 Spring 自己内部的、通过注解扫描来的、通过 XML 来的、通过 @Bean 方法来的)。
动态注册新“图纸”: 你可以通过 registry.registerBeanDefinition(…) 方法,动态地、编程式地向容器添加全新的 BeanDefinition(图纸)。
修改已有“图纸”: 你可以通过 registry.getBeanDefinition(…) 获取到某个现有的 BeanDefinition,然后修改它的属性(比如修改它的 class 类型,修改它的作用域 scope,添加/修改属性值,甚至可以完全替换它)。
删除“图纸”: 你可以通过 registry.removeBeanDefinition(…) 方法移除一个已有的 BeanDefinition(告诉工厂这份图纸作废,不生产这个 Bean 了)。
我们这里要用到的就是他的第二个作用,用来动态注册新的图纸

3、EnvironmentAware
3.1、是什么
EnvironmentAware 是 Spring Framework 中提供的一个​特定 Aware 接口,它允许一个 Bean 在 Spring IoC 容器初始化过程中感知并获取其运行时环境的抽象表示——即 Environment 对象。

3.2、有什么用:​​
​访问环境信息:提供一种在 Bean 生命周期的早期阶段​(在依赖注入完成之后,在初始化方法如@PostConstruct或 InitializingBean.afterPropertiesSet()之前)获取 Spring Environment 对象的机制。
统一访问配置源:​​ Environment 对象是一个强大的抽象接口,它集成了应用程序运行的所有环境信息源,包括但不限于:
应用程序属性文件(application.properties / application.yml)
外部化配置(系统属性、环境变量、命令行参数)
Spring Profile (如 dev, test, prod)
可选的自定义属性源 (通过实现PropertySource接口)
动态解析配置:方便在 Bean 初始化时动态地读取、解析配置文件中的配置项(特别是当这些值无法或不便通过@Value或@ConfigurationProperties 注入时)。
通俗的说,他最大的作用就是用来在我们不方便使用@Value或@ConfigurationProperties注解来读取配置文件中的属性时,用来手动读取配置文件中的内容

为啥要用他呢?由于我们准备使用BeanDefinitionRegistryPostProcessor来创建Bean,而由于Spring初始化顺序的原因,在BeanDefinitionRegistryPostProcessor创建Bean的时候,常规的读取配置文件的方式(@Value、@ConfigurationProperties)还没有执行,还不能读取到配置文件中的内容,于是我们这时候就需要使用EnvironmentAware来读取了

4、正式开始
4.1、编写自定义的配置项
#application.yml
modules:
  groups:
    - name: 商品模块 #分组名称
      matchMode: PATH # 匹配模式
      paths:
        - '/api/product/**' # 使用接口路径匹配模式时,对应的接口路径
      title: 商品管理接口 # 该分组所对应的基础信息模块的标题
      description: 商品相关接口 # 该分组所对应的基础信息模块的描述
      version: 1.0.0 # 该分组所对应基础信息模块的版本号
      contactName: 张三 # 该分组所对应基础信息模块的联系人名称
      contactEmail: support@zhangsan.com # 该分组所对应基础信息模块的联系邮箱
      licenseName: Apache 2.0 # 该分组所对应基础信息模块的授权名称
      licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.html # 该分组所对应基础信息模块的授权地址
    - name: 用户模块 #分组名称
      matchMode: PACKAGE # 匹配模式
      packages:
        - 'com.ren.main.controller.member' # 使用包名匹配模式时,对应的包名
      title: 用户管理接口 # 该分组所对应的基础信息模块的标题
      description: 用户相关接口 # 该分组所对应的基础信息模块的描述
      version: 1.0.0 # 该分组所对应基础信息模块的版本号
      contactName: 李四 # 该分组所对应基础信息模块的联系人名称
      contactEmail: support@lisi.com # 该分组所对应基础信息模块的联系邮箱
      licenseName: Apache 2.0 # 该分组所对应基础信息模块的授权名称
      licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.html # 该分组所对应基础信息模块的授权地址
    - name: 文件模块 #分组名称
      matchMode: PACKAGE # 匹配模式
      packages:
        - 'com.ren.main.controller.file' # 使用包名匹配模式时,对应的包名
      title: 文件管理接口 # 该分组所对应的基础信息模块的标题
      description: 文件相关接口 # 该分组所对应的基础信息模块的描述
      version: 1.0.0 # 该分组所对应基础信息模块的版本号
      contactName: 王五 # 该分组所对应基础信息模块的联系人名称
      contactEmail: support@wangwu.com # 该分组所对应基础信息模块的联系邮箱
      licenseName: Apache 2.0 # 该分组所对应基础信息模块的授权名称
      licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.html # 该分组所对应基础信息模块的授权地址

4.2、创建两个实体类读取配置文件

@Data

public class ModuleGroups {

// 分组列表 ModuleGroupProperties--每个分组

private List<ModuleGroupProperties> groups;

// 当前分组的基础请求路径

private String basePath = "";

}


@Data

public class ModuleGroupProperties {

private String name; // 分组名称

private MatchMode matchMode; // 匹配模式

private List<String> paths; // 路径匹配规则列表

private List<String> packages; // 包匹配规则列表


private String title;         // API 标题

private String description;   // API 描述

private String version;       // API 版本

private String contactName;   // 联系人姓名

private String contactEmail;  // 联系人邮箱

private String contactUrl;    // 联系人URL

private String licenseName;   // 许可证名称

private String licenseUrl;    // 许可证URL


public enum MatchMode {

PATH, PACKAGE

}

}

4.3、编写配置类

@Configuration

public class SpringDocConfig implements EnvironmentAware {


//环境对象,用于后续读取配置文件中的内容

private Environment environment;


/**

* 设置环境对象,用于后续配置绑定

*

* @param environment Spring环境对象,提供访问配置属性的能力

*/

@Override

public void setEnvironment(Environment environment) {

this.environment = environment;

}


/**

* 默认分组(关键:确保分组功能激活)

*

* 作用:  1. 作为分组功能的激活Bean(解决show-actuator=false时分组不显示的问题) 

*        2. 收集未被其他分组包含的接口 

*   3. 提供默认的API信息

*

* @return GroupedOpenApi 默认分组对象

*/

@Bean

public GroupedOpenApi defaultGroup() {

// 获取需要排除的路径和包

HashMap<String, Set<String>> allExcludedPathAndPackage = getExcludedPathAndPackage();

// 需要排除的路径列表,使用Set,防止重复

Set<String> allExcludedPaths = allExcludedPathAndPackage.get("allExcludedPaths");

// 需要排除的包列表,使用Set,防止重复

Set<String> allExcludedPackages = allExcludedPathAndPackage.get("allExcludedPackages");

return GroupedOpenApi.builder().group("未分组接口") // 分组标识

.displayName("未分组接口") // 在UI中显示的名称

.pathsToExclude(allExcludedPaths.toArray(new String[0])) // 排除其他分组已经包含的路径

.packagesToExclude(allExcludedPackages.toArray(new String[0])) // 排除其他分组已经包含的包

.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))

// 过滤条件:只包含标注@Operation的接口

.addOpenApiCustomizer(openApi -> openApi.info(

newInfo()

.title("未分组接口")

.description("其他未归类的接口")

.version("1.0")

)) // 自定义API信息(分组基础信息模块展示的内容)

.build();

}


/**

* 获取需要排除的路径和包

*

* @return java.util.HashMap<java.lang.String,java.util.Set<java.lang.String>>

* @author ren

* @date 2025/07/03 11:06

*/

private HashMap<String, Set<String>> getExcludedPathAndPackage() {

// 使用environment,获取配置文件中配置的所有组

// 将配置文件中modules配置下的所有内容读取到ModuleGroups实体类中

ModuleGroups moduleGroups =

Binder.get(environment).bind("modules", Bindable.of(ModuleGroups.class)).orElse(new ModuleGroups());

List<ModuleGroupProperties> groups = moduleGroups.getGroups();


// 收集所有需要排除的路径和包

Set<String> allExcludedPaths = new HashSet<>();

Set<String> allExcludedPackages = new HashSet<>();

if (groups != null) {

for (ModuleGroupProperties group : groups) {

if (group.getMatchMode() == ModuleGroupProperties.MatchMode.PATH) {

if (group.getPaths() != null) {

allExcludedPaths.addAll(group.getPaths());

}

} else if (group.getMatchMode() == ModuleGroupProperties.MatchMode.PACKAGE) {

if (group.getPackages() != null) {

allExcludedPackages.addAll(group.getPackages());

}

}

}

}


// 将需要排除的路径列表和包列表放入Map进行返回

return new HashMap<>() {

{

put("allExcludedPaths", allExcludedPaths);

put("allExcludedPackages", allExcludedPackages);

}

};

}


/**

* 动态注册Swagger分组Bean

*

* 主要流程: 1. 从环境变量中绑定分组配置 2. 为每个分组创建Bean定义 3. 将分组Bean注册到Spring容器

*

* @return BeanDefinitionRegistryPostProcessor 用于动态注册Bean的处理器

*/

@Bean

public BeanDefinitionRegistryPostProcessor groupedOpenApiRegistrar() {

return new BeanDefinitionRegistryPostProcessor() {

/**

* 注册Bean定义到Spring容器

*

* @param registry Spring Bean定义注册器

*/

@Override

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {

// 使用environment,获取配置文件中配置的所有组

// 将配置文件中modules配置下的所有内容读取到ModuleGroups实体类中

ModuleGroups moduleGroups =

Binder.get(environment).bind("modules", Bindable.of(ModuleGroups.class)).orElse(new ModuleGroups());


// 处理配置中定义的分组

if (moduleGroups.getGroups() != null) {

for (ModuleGroupProperties group : moduleGroups.getGroups()) {

// 创建Bean名称(替换空格为下划线)(防止重复)

String beanName = "groupedOpenApi_" + group.getName().replaceAll("\\s+", "_");


// 构建Bean定义

BeanDefinition definition = BeanDefinitionBuilder

.genericBeanDefinition(GroupedOpenApi.class, () -> buildGroup(group, moduleGroups.getBasePath())).getBeanDefinition();


// 注册Bean定义到Spring容器

registry.registerBeanDefinition(beanName, definition);

}

}

}


@Override

public void postProcessBeanFactory(

org.springframework.beans.factory.config.ConfigurableListableBeanFactory beanFactory) {

// 不需要实现

}

};

}


/**

* 构建分组对象

*

* 根据配置创建GroupedOpenApi实例,包含: 1. 分组基本信息(名称、显示名) 2. API信息自定义器(标题、描述、版本等) 3. 接口匹配规则(路径匹配或包匹配)

*

* @param group 分组配置属性

* @return GroupedOpenApi 分组对象

*/

private GroupedOpenApi buildGroup(ModuleGroupProperties group, String basePath) {

// 创建分组构建器

GroupedOpenApi.Builder builder = GroupedOpenApi.builder()

.group(group.getName()) // 分组标识

.displayName(group.getName()) // 在UI中显示的名称

.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class));// 过滤条件:只包含标注了@Operation的接口


// 如果配置了API信息,添加自定义器

if (hasApiInfo(group)) {

builder.addOpenApiCustomizer(createInfoCustomizer(group,basePath));

}


// 根据匹配模式配置接口扫描规则

switch (group.getMatchMode()) {

case PATH:

// 路径匹配模式:配置路径列表

if (group.getPaths() != null && !group.getPaths().isEmpty()) {

builder.pathsToMatch(group.getPaths().toArray(new String[0]));

}

break;


case PACKAGE:

// 包匹配模式:配置包列表

if (group.getPackages() != null && !group.getPackages().isEmpty()) {

builder.packagesToScan(group.getPackages().toArray(new String[0]));

}

break;

}


return builder.build();

}


/**

* 检查分组是否配置了API信息

*

* 判断条件:标题、描述、版本、联系人等至少有一项不为空

*

* @param group 分组配置属性

* @return boolean 是否包含API信息

*/

private boolean hasApiInfo(ModuleGroupProperties group) {

return group.getTitle() != null || group.getDescription() != null || group.getVersion() != null

|| group.getContactName() != null;

}


/**

* 创建API信息自定义器

*

* 根据分组配置创建OpenAPI的Info对象,包含: 1. 标题、描述、版本 2. 联系人信息(姓名、邮箱、URL) 3. 许可证信息

*

* @param group 分组配置属性

* @return OpenApiCustomizer API信息自定义器

*/

private OpenApiCustomizer createInfoCustomizer(ModuleGroupProperties group, String basePath) {

return openApi -> {

// 创建Info对象

Info info = new Info();


// 设置基本信息

if (group.getTitle() != null)

info.setTitle(group.getTitle());

if (group.getDescription() != null)

info.setDescription(group.getDescription());

if (group.getVersion() != null)

info.setVersion(group.getVersion());


// 设置联系人信息

if (group.getContactName() != null || group.getContactEmail() != null || group.getContactUrl() != null) {

Contact contact = new Contact();

if (group.getContactName() != null)

contact.setName(group.getContactName());

if (group.getContactEmail() != null)

contact.setEmail(group.getContactEmail());

if (group.getContactUrl() != null)

contact.setUrl(group.getContactUrl());

info.setContact(contact);

}


// 设置许可证信息

if (group.getLicenseName() != null || group.getLicenseUrl() != null) {

License license = new License();

if (group.getLicenseName() != null)

license.setName(group.getLicenseName());

if (group.getLicenseUrl() != null)

license.setUrl(group.getLicenseUrl());

info.setLicense(license);

}


// 获取原有的Paths对象

Paths originalPaths = openApi.getPaths();

// 创建一个新的Paths对象来存储带前缀的路径

Paths prefixedPaths = new Paths();

// 遍历所有路径,添加前缀并放入新的Paths对象

if (originalPaths != null) {

originalPaths.forEach((path, item) ->

prefixedPaths.addPathItem(basePath + path, item)

);

}

// 设置新的Paths对象到OpenAPI文档

openApi.setPaths(prefixedPaths);


// 将Info对象设置到OpenAPI

openApi.setInfo(info);

};

}

}

到这里所有的代码就已经完事了,给大家看看效果
代码中每一个步骤是做什么,已经包含了详细的注解,相信大家看一看就能够明白
attachments-2025-08-Nby1XeZN689eed2cd7ef2,png
attachments-2025-08-wahAGqN0689eed3454241,png

总结
本文探索了一种 SpringDoc 分组管理的新思路。通过结合配置文件的易读性与编程式分组的灵活性,利用 BeanDefinitionRegistryPostProcessor 动态注册分组 Bean,实现了基于 YAML 配置的规则驱动分组。方法支持路径匹配、包扫描及注解过滤,简化了多维度分组管理,解决了硬编码带来的维护负担。源码已开源,希望能为需要灵活 API 文档管理的开发者提供一种参考。

  • 发表于 2025-08-15 15:49
  • 阅读 ( 15 )

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
石天
石天

437 篇文章

作家榜 »

  1. shitian 662 文章
  2. 石天 437 文章
  3. 每天惠23 33 文章
  4. 小A 29 文章