Springboot3基于SpringDoc实现接口分组功能
问题
最近在接触SpringBoot3,整合Swagger文档组件的过程中发现一个问题,通过springdoc.group-configs[0].display-name=示例接口
配置分组名称在界面上面还是显示OpenAPI definition
,如下图所示:
查询了下官方文档,可以通过注入GroupedOpenApi
对象实现接口分组描述信息展示,同时找到了一个issues提供了一种硬编码的实现方式,但是硬编码的实现方式不利于后期接口描述信息的问题,理想方式还是在配置文件中进行维护,随后查阅了SpringDoc底层的AutoConfiguration
实现逻辑,并没有找到相关配置项,所以开始动手改造,增加相关的配置项,然后动态注入GroupedOpenApi
。
解决思路
自定义GroupConfig
配置类:
public class CustomerGroupConfig extends SpringDocConfigProperties.GroupConfig {
private Info apiInfo;
public CustomerGroupConfig() {
}
public CustomerGroupConfig(String group, List<String> pathsToMatch, List<String> packagesToScan, List<String> packagesToExclude, List<String> pathsToExclude, List<String> producesToMatch, List<String> consumesToMatch, List<String> headersToMatch, String displayName, Info apiInfo) {
super(group, pathsToMatch, packagesToScan, packagesToExclude, pathsToExclude, producesToMatch, consumesToMatch, headersToMatch, displayName);
this.apiInfo = apiInfo;
}
public Info getApiInfo() {
return apiInfo;
}
public CustomerGroupConfig setApiInfo(Info apiInfo) {
this.apiInfo = apiInfo;
return this;
}
}
public class CustomSpringDocConfigProperties {
private Set<CustomerGroupConfig> groupConfigs = new HashSet();
public CustomSpringDocConfigProperties() {
}
public CustomSpringDocConfigProperties(Set<CustomerGroupConfig> groupConfigs) {
this.groupConfigs = groupConfigs;
}
public Set<CustomerGroupConfig> getGroupConfigs() {
return groupConfigs;
}
public Set<SpringDocConfigProperties.GroupConfig> getOriginGroupConfigs() {
return groupConfigs.stream().map(this::convert).collect(Collectors.toSet());
}
public SpringDocConfigProperties.GroupConfig convert(CustomerGroupConfig config) {
SpringDocConfigProperties.GroupConfig groupConfig = new SpringDocConfigProperties.GroupConfig();
groupConfig.setGroup(config.getGroup());
groupConfig.setDisplayName(config.getDisplayName());
groupConfig.setPackagesToScan(config.getPackagesToScan());
return groupConfig;
}
}
通过BeanDefinitionRegistryPostProcessor
向Spring容器中动态注入GroupedOpenApi
接口分组对象,部分代码参考了源码类SpringdocBeanFactoryConfigurer
:
@Configuration
@ConditionalOnProperty(
name = {"springdoc.api-docs.enabled"},
matchIfMissing = true
)
@ConditionalOnWebApplication
public class DynamicSpringDocGroupConfiguration implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private static final String CONFIG_PRO_PREFIX = "springdoc.custom";
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
final DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
final BindResult<CustomSpringDocConfigProperties> result = Binder.get(environment)
.bind(CONFIG_PRO_PREFIX, CustomSpringDocConfigProperties.class);
if (result.isBound()) {
List<GroupedOpenApi> groupedOpenApis = result.get().getGroupConfigs().stream()
.map(elt -> {
GroupedOpenApi.Builder builder = GroupedOpenApi.builder();
if (!CollectionUtils.isEmpty(elt.getPackagesToScan()))
builder.packagesToScan(elt.getPackagesToScan().toArray(new String[0]));
if (!CollectionUtils.isEmpty(elt.getPathsToMatch()))
builder.pathsToMatch(elt.getPathsToMatch().toArray(new String[0]));
if (!CollectionUtils.isEmpty(elt.getProducesToMatch()))
builder.producesToMatch(elt.getProducesToMatch().toArray(new String[0]));
if (!CollectionUtils.isEmpty(elt.getConsumesToMatch()))
builder.consumesToMatch(elt.getConsumesToMatch().toArray(new String[0]));
if (!CollectionUtils.isEmpty(elt.getHeadersToMatch()))
builder.headersToMatch(elt.getHeadersToMatch().toArray(new String[0]));
if (!CollectionUtils.isEmpty(elt.getPathsToExclude()))
builder.pathsToExclude(elt.getPathsToExclude().toArray(new String[0]));
if (!CollectionUtils.isEmpty(elt.getPackagesToExclude()))
builder.packagesToExclude(elt.getPackagesToExclude().toArray(new String[0]));
if (StringUtils.isNotEmpty(elt.getDisplayName()))
builder.displayName(elt.getDisplayName());
if (Optional.ofNullable(elt.getApiInfo()).isPresent()) {
builder.addOpenApiCustomizer(openApi -> openApi.info(elt.getApiInfo()));
}
return builder
.group(elt.getGroup())
.build();
})
.toList();
groupedOpenApis.forEach(elt -> {
BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(GroupedOpenApi.class, () -> elt).getBeanDefinition();
defaultListableBeanFactory.registerBeanDefinition(String.format("%sGroupedOpenApi", elt.getGroup()), beanDefinition);
});
}
}
}
增加配置项,用于@Conditional(CacheOrGroupedOpenApiCondition.class)
条件成立,注入MultipleOpenApiSupportConfiguration
等配置类:
springdoc.show-actuator=true
接口分组配置如下:
springdoc.custom.group-configs[0].group=demo
springdoc.custom.group-configs[0].display-name=示例接口
springdoc.custom.group-configs[0].packages-to-scan[0]=cn.codest.server.common
springdoc.custom.group-configs[0].api-info.title=示例接口
springdoc.custom.group-configs[0].api-info.description=示例接口清单
springdoc.custom.group-configs[0].api-info.version=v1.0
springdoc.custom.group-configs[0].api-info.termsOfService=http://localhost:8080/
springdoc.custom.group-configs[0].api-info.license.name=Apache 2.0
springdoc.custom.group-configs[1].group=user
springdoc.custom.group-configs[1].display-name=用户管理接口
springdoc.custom.group-configs[1].packages-to-scan[0]=cn.codest.server.user
springdoc.custom.group-configs[1].api-info.title=用户管理
springdoc.custom.group-configs[1].api-info.description=用户管理接口清单
springdoc.custom.group-configs[1].api-info.version=v1.0
springdoc.custom.group-configs[1].api-info.termsOfService=http://localhost:8080/
springdoc.custom.group-configs[1].api-info.license.name=Apache 2.0
效果如下:
后面有空再GitHub提交一个PR给SpringDoc。