【swagger动态配置输入参数忽略某些字段】

文章目录

    • 一,背景
        • 1.介绍
        • [2. 现有做法1](#2. 现有做法1)
        • [3. springfox支持的方式2](#3. springfox支持的方式2)
    • 二,问题
    • 三,思路
    • 四,实现
        • [1. 定义注解SwaggerInputFieldIgnore](#1. 定义注解SwaggerInputFieldIgnore)
        • [2. 实现WebMvcOpenApiTransformationFilter](#2. 实现WebMvcOpenApiTransformationFilter)
        • [3. 实现WApiListingBuilderPlugin](#3. 实现WApiListingBuilderPlugin)
    • 五,结果

一,背景

1.介绍

使用springfox生成swagger.json文件,然后导入yapi,进行展示;当使用的request和response有共用的模型时,一般是需要输入的模型字段会少一些,而输出的模型信息字段会更多些。

比如:

以用户管理为例,定义了如下结构

/**
 * swagger 用户测试方法
 * 
 * @author ruoyi
 */
@Anonymous
@Api("用户信息管理")
@RestController
@RequestMapping("/test/user")
public class TestController extends BaseController
{
    private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();
    {
        users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));
        users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
    }

    @ApiOperation("获取用户列表")
    @GetMapping("/list")
    public R<List<UserEntity>> userList()
    {
        List<UserEntity> userList = new ArrayList<UserEntity>(users.values());
        return R.ok(userList);
    }

    @ApiOperation("新增用户")
    @ApiImplicitParams({
        @ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class),
        @ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class),
        @ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class)
//        @ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class)
    })
    @PostMapping("/save")
    public R<String> save(@ApiIgnore UserEntity user)
    {
        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
        {
            return R.fail("用户ID不能为空");
        }
        users.put(user.getUserId(), user);
        return R.ok();
    }

    @ApiOperation("更新用户")
    @PutMapping("/update")
    public R<String> update(@RequestBody UserEntity user)
    {
        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
        {
            return R.fail("用户ID不能为空");
        }
        if (users.isEmpty() || !users.containsKey(user.getUserId()))
        {
            return R.fail("用户不存在");
        }
        users.remove(user.getUserId());
        users.put(user.getUserId(), user);
        return R.ok();
    }

}

@Data
@ApiModel(value = "UserEntity", description = "用户实体")
class UserEntity extends BaseEntity
{
    @ApiModelProperty("用户ID")
    private Integer userId;

    @ApiModelProperty("用户名称")
    private String username;

    @ApiModelProperty("用户密码")
    private String password;

    @ApiModelProperty("用户手机")
    private String mobile;

}

如此,则在用户更新和用户列表查询时,共用了UserEntity 模型。对于大部分实体共用的字段(BaseEntity),比如:创建时间,更新时间,是否删除等一并在yapi侧的请求api进行了展示,而对于前端开发同学来讲,这些都是非必需的。

我们的目标是:在前端传参数时,只使用必要的参数,其他参数不展示。

2. 现有做法1

定义一个新的DTO,比如:UpdateUserDTO,只包含必要的部分字段

弊端:

增加了一个新的类,每次需要copy一些字段,有些冗余。

3. springfox支持的方式2

使用@ApiIgnore注解,如上action插入用户时所做。

弊端:

需要使用@ApiImplicitParams和 @ApiImplicitParam注解,在参数比较多时,编写麻烦。

二,问题

以上都会比较繁琐,新增模型DTO后会有转换的编码操作;使用@ApiIgnore和@ApiImplicitParam时需要写好多跟业务逻辑无关的注解配置;二者更新时都有一些工作量存在;那么有没有一种更好的方式,既可以共用模型实体(UserEntity),又可以使用少量的编码,去除掉在用户输入时的各种非必需字段

三,思路

step1. 添加注解@SwaggerInputFieldIgnore,标识作为输入参数时,忽略的字段

step2. 在swagger加载各种@API配置时,解析出使用了哪些模型实体,尤其是有@SwaggerInputFieldIgnore标识的模型

step3. 在生成swagger.json内容时,通过反射处理@SwaggerInputFieldIgnore标识的字段,在结果中忽略该字段

四,实现

思路算是比较清晰,在实际编码实现的过程中,对于swagger的自动化配置原理以及spring-plugin的机制需要有必要的知识储备,我把最终编码结果整理如下:

1. 定义注解SwaggerInputFieldIgnore
/**
 * swagger schema模型中,
 * 1. 在作为body入参的场景下,忽略某些字段
 * 2. 在作为出参的场景下,不必忽略某些字段
 *
 * 比如:
 *     @ApiOperation("获取用户列表, UserEntity作为出参")
 *     @GetMapping("/list")
 *     public R<List<UserEntity>> userList();
 *
 *     @ApiOperation("更新用户,UserEntity作为入参")
 *     @PutMapping("/update")
 *     public R<String> update(@RequestBody UserEntity user)
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SwaggerInputFieldIgnore {
}
2. 实现WebMvcOpenApiTransformationFilter
@Component
public class InputFieldIgnoreOpenApiTransformationFilter implements WebMvcOpenApiTransformationFilter {
    private static final Logger log = getLogger(InputFieldIgnoreOpenApiTransformationFilter.class);
    public static final String SUFFIX_INPUT = "-input";

    private Map<String, InjectedInputSchema> injectedSchemaMap = new HashedMap();
    @Autowired
    private ModelRetriveApiListingPugin modelRetriveApiListingPugin;

    @Value("${swagger.fieldIgnore.Enable:true}")
    private Boolean fieldIgnoreEnable;

    public static final class InjectedInputSchema {
        String originKey;
        String injectedKey;
        Class schemaClazz;
        Schema injectedSchema;

        public InjectedInputSchema(String originKey, Class schemaClazz) {
            this.originKey = originKey;
            this.injectedKey = originKey + "-input";
            this.schemaClazz = schemaClazz;
        }
    }

    /**
     * 1. 根据注解@SwaggerInputFieldIgnore,解析余下的字段
     * 2. 增加对应的schema
     *
     * @param oas
     */
    private void processInputInject(OpenAPI oas) {
        if (!fieldIgnoreEnable) {
            return;
        }

        // 1. 解析paths,替换$ref; 同时,收集schema元数据用于补充schema-input。
        for (PathItem each : oas.getPaths().values()) {
            appendRefWithInput(each.getPut());
            appendRefWithInput(each.getPost());
        }

        if(MapUtils.isEmpty(injectedSchemaMap)){
            return;
        }

        // 2. 补充schema-input
        for (InjectedInputSchema each : injectedSchemaMap.values()) {
            // 2.1. 构造schema
            Schema old = oas.getComponents().getSchemas().get(each.originKey);
            Schema schema = constructSchema(each, old);
            // 2.2. 加入oas.components.schemas
            oas.getComponents().getSchemas().put(each.injectedKey, schema);
        }
    }

    private void appendRefWithInput(Operation operation) {
        if (operation == null || operation.getRequestBody() == null) {
            return;
        }
        try {
            Content content = operation.getRequestBody().getContent();
            MediaType mediaType = content.get(org.springframework.http.MediaType.APPLICATION_JSON_VALUE);
            Schema schema = mediaType.getSchema();
            String $ref = schema.get$ref();
            String originName = $ref.substring($ref.lastIndexOf("/") + 1);
            QualifiedModelName modelName = modelRetriveApiListingPugin.getApiModels().get(originName);
            if (modelName != null) {
                schema.set$ref($ref + SUFFIX_INPUT);
                injectedSchemaMap.put(originName, new InjectedInputSchema(originName, constructClazz(modelName)));
            }
        } catch (Exception e) {
            log.error("error occured", e);
        }
    }

    private static Class<?> constructClazz(QualifiedModelName modelName) throws ClassNotFoundException {
        return Class.forName(modelName.getNamespace() + "." + modelName.getName());
    }

    private Schema constructSchema(InjectedInputSchema each, Schema old) {

        Schema result = new ObjectSchema();
        result.title(each.injectedKey);
        result.type(old.getType());
        result.description(old.getDescription());

        HashMap<String, Schema> props = new HashMap<>(old.getProperties());
        Set<String> removingKey = new HashSet();
        props.keySet().forEach(filedName -> {
            Field field = ReflectionUtils.findField(each.schemaClazz, filedName);
            SwaggerInputFieldIgnore anno = AnnotationUtils.findAnnotation(field, SwaggerInputFieldIgnore.class);
            if (anno != null) {
                removingKey.add(filedName);
            }
        });

        removingKey.forEach(field -> props.remove(field));
        result.setProperties(props);

        return result;
    }

    @Override
    public OpenAPI transform(OpenApiTransformationContext<HttpServletRequest> context) {
        OpenAPI openApi = context.getSpecification();
        processInputInject(openApi);
        return openApi;
    }

    @Override
    public boolean supports(DocumentationType delimiter) {
        return delimiter == DocumentationType.OAS_30;
    }
}
3. 实现WApiListingBuilderPlugin
@Order(value = Ordered.LOWEST_PRECEDENCE)
@Component
public class ModelRetriveApiListingPugin implements ApiListingBuilderPlugin {
    private static final Logger log = LoggerFactory.getLogger(ModelRetriveApiListingPugin.class);

    private Map<String, QualifiedModelName> apiModels = new HashedMap();

    @Override
    public void apply(ApiListingContext apiListingContext) {
        Field filed = ReflectionUtils.findField(ApiListingBuilder.class, "modelSpecifications");
        filed.setAccessible(true);
        Map<String, ModelSpecification> specsMap = (Map<String, ModelSpecification>) ReflectionUtils.getField(filed, apiListingContext.apiListingBuilder());

        retriveApiModels(specsMap.values());
    }

    private void retriveApiModels(Collection<ModelSpecification> specs) {
//        Collection<ModelSpecification> specs = each.getModelSpecifications().values();
        specs.forEach(spec -> {
            ModelKey modelKey = spec.getCompound().get().getModelKey();
            QualifiedModelName modelName = modelKey.getQualifiedModelName();
            apiModels.put(modelName.getName(), modelName);
            log.info(modelName.toString());
        });
    }

    Map<String, QualifiedModelName> getApiModels() {
        return apiModels;
    }

    @Override
    public boolean supports(DocumentationType delimiter) {
        return true;
    }
}

五,结果

  1. 加入以上java文件,并设置模型字段注解后,可以看到产出的swagger.json内容已经在入参数中忽略掉了注解字段
  2. 导入yapi后,配置忽略的字段已经没有了,^_^ !
相关推荐
WaaTong2 分钟前
Java反射
java·开发语言·反射
九圣残炎35 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge37 分钟前
Netty篇(入门编程)
java·linux·服务器
成富1 小时前
文本转SQL(Text-to-SQL),场景介绍与 Spring AI 实现
数据库·人工智能·sql·spring·oracle
Re.不晚1 小时前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐1 小时前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野1 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航1 小时前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself2 小时前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言