【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后,配置忽略的字段已经没有了,_ !
相关推荐
优雅的38度3 小时前
maven的多仓库配置理解
java·架构
周末吃鱼3 小时前
研发快速使用JMeter
java·jmeter
EntyIU3 小时前
自己实现mybatisplus的批量插入
java·后端
小途软件4 小时前
基于深度学习的人脸检测算法研究
java·人工智能·pytorch·python·深度学习·语言模型
小CC吃豆子4 小时前
Java数据结构与算法
java·开发语言
晨旭缘4 小时前
后端日常启动及常用命令(Java)
java·开发语言
CodeAmaz4 小时前
ArrayList 底层原理
java·arraylist
用户0203388613144 小时前
手写Spring(2):实现AOP与JdbcTemplate
spring
山峰哥4 小时前
3000字深度解析:SQL调优如何让数据库查询效率提升10倍
java·服务器·数据库·sql·性能优化·编辑器
tkevinjd4 小时前
JUC2(多线程中常用的成员方法)
java