refactor(api-doc): 重构 api-doc 模块,简化代码配置

This commit is contained in:
liquor
2025-12-24 08:52:42 +00:00
committed by Charles7c
parent 840e77d1ca
commit 4a82325b51
6 changed files with 4 additions and 321 deletions

View File

@@ -16,34 +16,24 @@
package top.continew.starter.apidoc.autoconfigure;
import cn.hutool.core.map.MapUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.apidoc.handler.BaseEnumParameterHandler;
import top.continew.starter.core.autoconfigure.application.ApplicationProperties;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
import java.util.Map;
/**
* API 文档自动配置
*
@@ -52,7 +42,6 @@ import java.util.Map;
*/
@EnableWebMvc
@AutoConfiguration(before = SpringDocConfiguration.class)
@EnableConfigurationProperties(SpringDocExtensionProperties.class)
@PropertySource(value = "classpath:default-api-doc.yml", factory = GeneralPropertySourceFactory.class)
public class SpringDocAutoConfiguration implements WebMvcConfigurer {
@@ -68,7 +57,7 @@ public class SpringDocAutoConfiguration implements WebMvcConfigurer {
*/
@Bean
@ConditionalOnMissingBean
public OpenAPI openApi(ApplicationProperties applicationProperties, SpringDocExtensionProperties properties) {
public OpenAPI openApi(ApplicationProperties applicationProperties) {
Info info = new Info().title("%s %s".formatted(applicationProperties.getName(), "API 文档"))
.version(applicationProperties.getVersion())
.description(applicationProperties.getDescription());
@@ -82,54 +71,9 @@ public class SpringDocAutoConfiguration implements WebMvcConfigurer {
}
OpenAPI openApi = new OpenAPI();
openApi.info(info);
Components components = properties.getComponents();
if (components != null) {
openApi.components(components);
Map<String, SecurityScheme> securitySchemeMap = components.getSecuritySchemes();
if (MapUtil.isNotEmpty(securitySchemeMap)) {
SecurityRequirement securityRequirement = new SecurityRequirement();
securitySchemeMap.keySet().forEach(securityRequirement::addList);
openApi.addSecurityItem(securityRequirement);
}
}
return openApi;
}
/**
* 全局自定义配置(全局添加鉴权参数)
*/
@Bean
@ConditionalOnMissingBean
public GlobalOpenApiCustomizer globalOpenApiCustomizer(SpringDocExtensionProperties properties) {
return openApi -> {
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((path, pathItem) -> {
// 为所有接口添加鉴权
Components components = properties.getComponents();
if (components != null && MapUtil.isNotEmpty(components.getSecuritySchemes())) {
Map<String, SecurityScheme> securitySchemeMap = components.getSecuritySchemes();
pathItem.readOperations().forEach(operation -> {
SecurityRequirement securityRequirement = new SecurityRequirement();
securitySchemeMap.keySet().forEach(securityRequirement::addList);
operation.addSecurityItem(securityRequirement);
});
}
});
}
};
}
/**
* 自定义 BaseEnum 枚举参数配置(针对实现了 BaseEnum 的枚举,优化其枚举值和描述展示)
*
* @return {@link BaseEnumParameterHandler }
* @since 2.4.0
*/
@Bean
public BaseEnumParameterHandler customParameterCustomizer(ObjectMapper mapper) {
return new BaseEnumParameterHandler(mapper);
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'ApiDoc' completed initialization.");

View File

@@ -1,45 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.apidoc.autoconfigure;
import io.swagger.v3.oas.models.Components;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* API 文档扩展配置属性
*
* @author Charles7c
* @since 1.0.1
*/
@ConfigurationProperties("springdoc")
public class SpringDocExtensionProperties {
/**
* 组件配置(包括鉴权配置等)
*/
@NestedConfigurationProperty
private Components components;
public Components getComponents() {
return components;
}
public void setComponents(Components components) {
this.components = components;
}
}

View File

@@ -1,128 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.apidoc.handler;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ClassUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.SimpleType;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.springdoc.core.customizers.ParameterCustomizer;
import org.springframework.core.MethodParameter;
import top.continew.starter.apidoc.util.ApiDocUtils;
import top.continew.starter.core.enums.BaseEnum;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* 自定义 BaseEnum 枚举参数处理器
* <p>
* 针对实现了 BaseEnum 的枚举,优化其枚举值和描述展示
* </p>
*
* @author echo
* @since 2.5.2
*/
public class BaseEnumParameterHandler extends ModelResolver implements ParameterCustomizer {
public BaseEnumParameterHandler(ObjectMapper mapper) {
super(mapper);
}
@Override
public Parameter customize(Parameter parameterModel, MethodParameter methodParameter) {
Class<?> parameterType = methodParameter.getParameterType();
// 判断是否为 BaseEnum 的子类型
if (!ClassUtil.isAssignable(BaseEnum.class, parameterType)) {
return parameterModel;
}
String description = parameterModel.getDescription();
if (CharSequenceUtil.contains(description, "color:red")) {
return parameterModel;
}
// 自定义枚举描述并封装参数配置
configureSchema(parameterModel.getSchema(), parameterType);
parameterModel.setDescription(appendEnumDescription(description, parameterType));
return parameterModel;
}
@Override
public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
Schema resolve = super.resolve(type, context, chain);
Class<?> rawClass = resolveRawClass(type.getType());
// 判断是否为 BaseEnum 的子类型
if (!ClassUtil.isAssignable(BaseEnum.class, rawClass)) {
return resolve;
}
// 自定义参数描述并封装参数配置
configureSchema(resolve, rawClass);
resolve.setDescription(appendEnumDescription(resolve.getDescription(), rawClass));
return resolve;
}
/**
* 封装 Schema 配置
*
* @param schema Schema
* @param enumClass 枚举类型
*/
private void configureSchema(Schema schema, Class<?> enumClass) {
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
List<String> valueList = Arrays.stream(enums).map(e -> e.getValue().toString()).toList();
schema.setEnum(valueList);
String enumValueType = ApiDocUtils.getEnumValueTypeAsString(enumClass);
schema.setType(enumValueType);
schema.setFormat(ApiDocUtils.resolveFormat(enumValueType));
}
/**
* 追加枚举描述
*
* @param originalDescription 原始描述
* @param enumClass 枚举类型
* @return 追加后的描述字符串
*/
private String appendEnumDescription(String originalDescription, Class<?> enumClass) {
return originalDescription + "<span style='color:red'>" + ApiDocUtils.getDescMap(enumClass) + "</span>";
}
/**
* 解析原始类
*
* @param type 类型
* @return 原始类的 Class 对象
*/
private Class<?> resolveRawClass(Type type) {
if (type instanceof SimpleType simpleType) {
return simpleType.getRawClass();
} else if (type instanceof CollectionType collectionType) {
return collectionType.getContentType().getRawClass();
} else {
return Object.class;
}
}
}

View File

@@ -17,15 +17,6 @@
package top.continew.starter.apidoc.util;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.web.bind.annotation.RestController;
import top.continew.starter.core.enums.BaseEnum;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 接口文档工具类
@@ -63,84 +54,4 @@ public class ApiDocUtils {
public static GroupedOpenApi buildGroupedOpenApi(String group, String displayName, String... pathsToMatch) {
return GroupedOpenApi.builder().group(group).displayName(displayName).pathsToMatch(pathsToMatch).build();
}
/**
* 获取枚举值类型
*
* @param enumClass 枚举类型
* @return 枚举值类型
*/
public static String getEnumValueTypeAsString(Class<?> enumClass) {
// 获取枚举类实现的所有接口
Type[] interfaces = enumClass.getGenericInterfaces();
// 定义枚举值类型的映射
Map<Class<?>, String> typeMap = Map
.of(Integer.class, "integer", Long.class, "long", Double.class, "number", String.class, "string");
// 遍历所有接口
for (Type type : interfaces) {
// 检查接口是否为参数化类型并且原始类型为 BaseEnum
if (type instanceof ParameterizedType parameterizedType && parameterizedType
.getRawType() == BaseEnum.class) {
Type actualType = parameterizedType.getActualTypeArguments()[0];
// 检查实际类型参数是否为类类型,并返回对应的字符串类型
if (actualType instanceof Class<?> actualClass) {
return typeMap.getOrDefault(actualClass, "string");
}
}
}
// 默认返回 "string" 类型
return "string";
}
/**
* 解析枚举值的格式
*
* @param enumValueType 枚举值类型
* @return String 格式化类型
*/
public static String resolveFormat(String enumValueType) {
return switch (enumValueType) {
case "integer" -> "int32";
case "long" -> "int64";
case "number" -> "double";
default -> enumValueType;
};
}
/**
* 具有 RestController 注释既检查是否继承了BaseController
*
* @param clazz clazz
* @return boolean
*/
public static boolean hasRestControllerAnnotation(Class<?> clazz) {
// 如果注释包含 RestController 注解,则返回 true
if (clazz.isAnnotationPresent(RestController.class)) {
return true;
}
// 递归检查父类
Class<?> superClass = clazz.getSuperclass();
// 循环检查父类
while (superClass != null && !superClass.equals(Object.class)) {
// 如果父类包含 RestController 注解,则返回 true
if (hasRestControllerAnnotation(superClass)) {
return true;
}
// 递归检查接口
superClass = superClass.getSuperclass();
}
return false;
}
/**
* 获取枚举描述 Map
*
* @param enumClass 枚举类型
* @return 枚举描述 Map
*/
public static Map<Object, String> getDescMap(Class<?> enumClass) {
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
return Arrays.stream(enums)
.collect(Collectors.toMap(BaseEnum::getValue, BaseEnum::getDescription, (a, b) -> a, LinkedHashMap::new));
}
}

View File

@@ -30,7 +30,8 @@ import java.io.IOException;
* 通用配置文件读取工厂
* <p>
* DefaultPropertySourceFactory 仅支持 properties
* 配置文件读取,详见:<a href="https://docs.spring.io/spring-boot/docs/2.0.6.RELEASE/reference/html/boot-features-external-config.html#boot-features-external-config-yaml-shortcomings">YAMLShortcomings</a>
* 配置文件读取,详见:<a
* href="https://docs.spring.io/spring-boot/docs/2.0.6.RELEASE/reference/html/boot-features-external-config.html#boot-features-external-config-yaml-shortcomings">YAMLShortcomings</a>
* </p>
*
* @author Charles7c

View File

@@ -62,7 +62,7 @@
<crane4j.version>2.9.0</crane4j.version>
<!-- API Documentation Versions -->
<nextdoc4j.version>1.1.0</nextdoc4j.version>
<nextdoc4j.version>1.1.5</nextdoc4j.version>
<swagger-annotations.version>2.2.36</swagger-annotations.version>
<!-- Tracing and Logging Versions -->