自定义springdoc-openapi适配ORM框架遇两类问题,求解决方案
自定义springdoc-openapi适配ORM框架的解决方案
问题1:识别无is/get前缀的接口方法为模型属性
目标ORM的实体接口采用类似Java record的无前缀方法定义,但springdoc默认仅识别is/get开头的方法作为模型属性,导致Swagger UI中模型无属性展示。可通过自定义ModelConverter实现这一需求:
1. 实现自定义ModelConverter
import io.swagger.v3.core.converter.AbstractModelConverter; import io.swagger.v3.core.converter.ModelConverterContext; import io.swagger.v3.core.converter.ResolvedSchema; import io.swagger.v3.oas.models.media.Schema; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Iterator; public class NoPrefixModelConverter extends AbstractModelConverter { @Override public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) { Schema schema = super.resolve(type, context, chain); if (schema == null || !type.getType().isInterface()) { return schema; } Class<?> clazz = (Class<?>) type.getType(); // 筛选无参数、非Object类声明的方法,作为模型属性 Arrays.stream(clazz.getDeclaredMethods()) .filter(method -> method.getParameterCount() == 0) .filter(method -> !Object.class.equals(method.getDeclaringClass())) .forEach(method -> { String propName = method.getName(); // 解析方法返回值对应的Schema ResolvedSchema resolvedSchema = context.resolve( new AnnotatedType(method.getGenericReturnType()) ); schema.addProperties(propName, resolvedSchema.schema); // 集合/数组类型标记为可空(根据业务需求调整) if ("array".equals(resolvedSchema.schema.getType()) || "object".equals(resolvedSchema.schema.getType())) { schema.getProperties().get(propName).setNullable(true); } }); return schema; } }
2. 注册自定义Converter到springdoc
在Spring配置类中注册该Converter,使其被springdoc加载生效:
import org.springdoc.core.customizers.OpenApiCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.swagger.v3.core.converter.ModelConverters; @Configuration public class SpringDocConfig { @Bean public OpenApiCustomizer modelConverterCustomizer() { return openApi -> ModelConverters.getInstance().addConverter(new NoPrefixModelConverter()); } }
问题2:基于@FetchBy注解控制动态响应对象的形状
目标ORM支持GraphQL风格的动态查询,同一实体类在不同接口返回不同结构的对象。通过自定义@FetchBy注解标记响应对象的实际形状,可通过OperationCustomizer结合Schema解析逻辑替换默认Schema:
1. 定义@FetchBy注解
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE_USE) @Retention(RetentionPolicy.RUNTIME) public @interface FetchBy { String value(); // 指定静态常量形式的Fetcher名称 }
2. 实现自定义OperationCustomizer
import org.springdoc.core.customizers.OperationCustomizer; import org.springdoc.core.utils.SpringDocUtils; import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.responses.ApiResponse; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Map; @Component public class FetchByOperationCustomizer implements OperationCustomizer { private final ModelConverters modelConverters = ModelConverters.getInstance(); @Override public Operation customize(Operation operation, MethodParameter methodParameter) { // 解析返回值的泛型结构,提取带@FetchBy注解的实际类型 Type returnType = SpringDocUtils.resolveReturnType(methodParameter); if (returnType instanceof ParameterizedType paramType) { Type actualType = paramType.getActualTypeArguments()[0]; if (actualType instanceof AnnotatedType annotatedType) { FetchBy fetchBy = annotatedType.getAnnotation(FetchBy.class); if (fetchBy != null) { try { // 获取控制器类中定义的静态Fetcher常量 Field fetcherField = methodParameter.getContainingClass().getDeclaredField(fetchBy.value()); fetcherField.setAccessible(true); Object fetcher = fetcherField.get(null); // 根据Fetcher生成对应结构的Schema Schema<?> dynamicSchema = buildSchemaFromFetcher(fetcher); // 替换接口响应中的Schema operation.getResponses().values().forEach(response -> { if (response.getContent() != null && response.getContent().containsKey("application/json")) { response.getContent().get("application/json").setSchema(dynamicSchema); } }); } catch (NoSuchFieldException | IllegalAccessException e) { // 根据业务需求处理异常,如日志记录 e.printStackTrace(); } } } } return operation; } // 核心逻辑:根据Fetcher的结构生成对应的Schema,需适配目标ORM的Fetcher API private Schema<?> buildSchemaFromFetcher(Object fetcher) { Schema<?> schema = new Schema<>(); // 示例逻辑(需根据实际Fetcher API调整): // 1. 解析Fetcher中的标量字段,添加到Schema的properties // 2. 解析关联对象的Fetcher,递归生成嵌套Schema // if (fetcher instanceof BookFetcher bookFetcher) { // // 添加标量字段 // bookFetcher.getScalarFields().forEach(fieldName -> { // Type fieldType = getFieldType(Book.class, fieldName); // 自定义方法获取字段类型 // Schema fieldSchema = modelConverters.resolve(new AnnotatedType(fieldType)).schema; // schema.addProperties(fieldName, fieldSchema); // }); // // 添加关联对象store // if (bookFetcher.hasStoreFetcher()) { // Schema storeSchema = buildSchemaFromFetcher(bookFetcher.getStoreFetcher()); // schema.addProperties("store", storeSchema); // } // // 添加关联数组authors // if (bookFetcher.hasAuthorsFetcher()) { // Schema authorSchema = buildSchemaFromFetcher(bookFetcher.getAuthorsFetcher()); // Schema arraySchema = new Schema<>(); // arraySchema.setType("array"); // arraySchema.setItems(authorSchema); // schema.addProperties("authors", arraySchema); // } // } return schema; } // 辅助方法:根据实体类和字段名获取字段类型(示例实现,需适配ORM) private Type getFieldType(Class<?> entityClass, String fieldName) { // 实际实现需从ORM元数据中获取字段类型,或通过反射获取方法返回值 try { return entityClass.getDeclaredMethod(fieldName).getGenericReturnType(); } catch (NoSuchMethodException e) { throw new RuntimeException("Field not found: " + fieldName, e); } } }
关键说明
buildSchemaFromFetcher方法需要完全适配目标ORM的Fetcher API,解析其中包含的标量字段、关联对象等信息,递归生成嵌套的Schema结构。- 对于
ResponseEntity<>等包装类型,SpringDocUtils.resolveReturnType方法可自动解析其内部泛型类型,无需额外处理。
内容的提问来源于stack exchange,提问作者Tao Chen
相关产品推荐
相关产品推荐

