You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 14:55:15