在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'
大家可以看到功能已经实现了,和上面配置式分组是一样的效果
接下来我们使用编程式分组的方式实现默认分组仅仅显示其他分组没有显示的接口,不显示全部接口
@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();
}
}
成功了
我们先看Controller代码
接下来,我们编写配置类代码
@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();
}
}
@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
}
}
@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);
};
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!