草庐IT

JAVA中自定义扩展Swagger的能力,自动生成参数取值含义说明,提升开发效率

架构悟道 2023-04-16 原文

大家好,又见面了。

JAVA做前后端分离的项目开发的时候,服务端需要提供接口文档供周边人员做接口的对接指导。越来越多的项目都在尝试使用一些基于代码自动生成接口文档的工具来替代由开发人员手动编写接口文档,而Swagger作为一款优秀的在线接口文档生成工具,以其功能强大、集成方便而得到了广泛的使用。

在项目中有一种非常常见的场景,就是接口的请求或者响应参数中会有一些字段的取值会限定为固定的几个可选值之一,而在代码中这些可选值往往会通过定义枚举类的方式来承载,比如:

根据操作类型,过滤对应类型的用户操作日志列表
如: http://127.0.0.1:8088/test/queryOperateLogs?operateType=2

这里的请求参数operateType传入的值需要在后端约定的取值范围内,这个取值范围的定义如下:

@Getter
@AllArgsConstructor
public enum OperateType {
    ADD(1, "新增或者创建操作"),
    MODIFY(2, "更新已有数据操作"),
    DELETE(3, "删除数据操作"),
    QUERY(4, "查询数据操作");

    private int value;
    private String desc;
}

这里就需要我们在接口文档里面将此接口中operateType的可选值以及每个可选值对应的含义信息都说明清楚,这样调用方在使用的时候才知道应该传入什么值。

我们基于Swagger提供的基础注解能力来实现时,比较常见的会看到如下两种写法:

  • 写法1接口定义的时候,指定入参的取值说明

接口URL中携带的请求入参信息,通过@ApiImplicitParam注解来告诉调用方此接口允许接收的合法operateType的取值范围以及各个取值的含义。

比如下面这种场景:

@GetMapping("/queryOperateLogs")
@ApiOperation("查询指定操作类型的操作日志列表")
@ApiImplicitParam(name = "operateType", value = "操作类型,取值说明: 1,新增;2,更新;3,除;4,查询", dataType = "int", paramType = "query")
public List<OperateLog> queryOperateLogs(int operateType) {
    return testService.queryOperateLogs(operateType);
}

这样,在swagger界面上就可以显示出字段的取值说明信息。

其实还有一种写法,即在代码的入参前面添加@ApiParam注解的方式来实现。比如:

    @GetMapping("/queryOperateLogs")
    @ApiOperation("查询指定操作类型的操作日志列表")
    public List<OperateLog> queryOperateLogs(@ApiParam(value = "操作类型,取值说明: 1,新增;2,更新;3,删除;4,查询") @RequestParam("type") int operateType) {
        return testService.queryOperateLogs(operateType);
    }

这样也能达到相同的效果。

  • 写法2请求或者响应的Body体中解释字段的取值说明

对于需要使用json体进行传输的请求或者响应消息体Model中,可以使用@ApiModelProperty添加含义说明。

@Data
@ApiModel("操作记录信息")
public class OperateLog {
    @ApiModelProperty("操作类型,取值说明: 1,新增;2,更新;3,删除;4,查询")
    private int operateType;
    @ApiModelProperty("操作用户")
    private String user;
    @ApiModelProperty("操作详情")
    private String detail;
}

同样,在Swagger界面就可以清楚的知道每个字段的具体含义与取值说明。

但是上面的两个写法,都存在着同一个问题,就是如果枚举类中的值内容含义有变更,比如OperateType枚举类中新增了一个BATCH_DELETE(5, "批量删除"), 则必须手动去修改所有涉及的接口上的Swagger描述信息。如果有大量场景都涉及此字段,则要改动的地方就非常多,且极易漏掉(因为不好通过代码关联关系直接搜索到)。这样对于开发人员维护起来的成本就会增加,久而久之会导致接口文档的内容与实际代码处理情况不相匹配。

那么,有没有什么简单的方式,可以让接口文档自动根据对应枚举类的内容变更而动态变更呢?

Swagger没有提供原生的此方面能力支持,但是我们可以通过一些简单的方式对Swagger的能力进行扩展,让Swagger支持我们的这种诉求。一起来看下如何实现吧。

扩展可行性分析

既然想要改变生成的Swagger文档中指定字段的描述内容,那么首先就应该是要搞清楚Swagger中现在的内容生成逻辑是如何处理的。我们以@ApiParam为例进行分析。因为@ApiParam中指定的内容会被显示到Swagger界面上,那么在Swagger的框架中,一定有个地方会尝试去获取此注解中指定的相关字段值,然后将注解的内容转为界面上的文档内容。所以想要定制,首先必须要了解当前是如何处理的。

翻看Swagger的源码,发现在ApiParamParameterBuilder类中进行此部分逻辑的处理,处理逻辑如下:

看了下此类是ParameterBuilderPlugin接口的一个实现类,Swagger框架在遍历并逐个生成parameter说明信息的时候会被调用此实现类的逻辑来执行。

到这里其实问题就已经很明显了,我们可以自定义一个处理类并实现ParameterBuilderPlugin接口,然后将我们的诉求在自定义的处理类中进行实现,这样不就可以实现我们的诉求了吗?

相同的策略,我们可以找到处理@ApiImplicitParam@ApiModelProperty对应的接口类。

根据上面的分析,我们只需要提供个自定义实现类,然后分别实现这几个接口就可以搞定我们的诉求了。那应该如何进行封装,将其作为一个通用能力供所有场景使用呢,下面详细讨论下。

自定义注解实现基于枚举类生成描述

前面已经找到了一种思路将我们的定制逻辑注入到Swagger的文档生成框架中进行调用,那么下一步我们就得确认一种相对简单的策略,告诉框架哪个字段需要使用枚举来自动生成取值说明,以及使用哪个枚举类来生成。

这里我们使用自定义注解的方式来实现。Swagger为不同的场景分别提供了@APIParam@ApiImplicitParam@ApiModelProperty等不同的注解,我们可以简化下,提供一个统一的自定义注解即可。

比如:

@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiPropertyReference {
    // 接口文档上的显示的字段名称,不设置则使用field本来名称
    String name() default "";
    // 字段简要描述,可选
    String value() default "";
    // 标识字段是否必填
    boolean required() default  false;
    // 指定取值对应的枚举类
    Class<? extends Enum> referenceClazz();
}

这样呢,对于需要添加取值说明的字段或者接口上,我们就可以添加@ApiPropertyReference并指定对应的枚举类即可。

比如下面这样:

@Data
@ApiModel("操作记录信息")
public class OperateLog {
    @ApiPropertyReference(value = "操作类型", referenceClazz = OperateType.class)
    private int operateType;
    // ... 
}

上面示例代码中,OperateType是一个已经定义好的枚举类。现在又遇到一个问题,枚举类的实现形式其实也不一样,要如何才能让我们的自动内容生成服务知道获取枚举类中的哪些内容进行处理呢?当然我们可以约定用于Swagger注解中的枚举类必须遵循某个固定的格式,但显然这样实施的难度就会提升,并非是我们想要的结果。

先来看下面给定的这个枚举类,其中包含ordervaluedesc三个属性值,而value字段是我们的接口字段需要传入的真实取值,desc是其对应的含义描述,那么该如何让我们自定义Swagger扩展类知晓应该使用valuedesc字段来生成文档描述内容呢?

@Getter
@AllArgsConstructor
public enum OperateType {
    ADD(1, 11, "新增"),
    MODIFY(2, 22, "更新"),
    DELETE(3, 33, "删除");
    private int order;
    private int value;
    private String desc;
}

答案其实不陌生,依旧是自定义注解!只要提供个自定义注解,然后添加到枚举类上,指定到底使用枚举类中的哪个字段作为value值,以及哪个字段用作含义描述desc字段值就行了。

@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SwaggerDisplayEnum {
    String value() default "value";
    String desc() default "desc";
}

这样,在枚举类上添加下@SwaggerDisplayEnum并指定下字段的映射,即可用于Swagger注解中:

到这里呢,我们需要的数据来源以及取值转换规则就已经全部确定,剩下的就是如何将一个枚举类中需要的值与描述字段给拼接成想要的内容了。因为是通用能力,所以此处需要通过反射的方式来实现:

private String generateValueDesc(ApiPropertyReference propertyReference) {
    Class<? extends Enum> rawPrimaryType = propertyReference.referenceClazz();
    SwaggerDisplayEnum swaggerDisplayEnum = AnnotationUtils.findAnnotation(rawPrimaryType,
            SwaggerDisplayEnum.class);
    String enumFullDesc = Arrays.stream(rawPrimaryType.getEnumConstants())
            .filter(Objects::nonNull)
            .map(enumConsts -> {
                Object fieldValue = ReflectUtil.getFieldValue(enumConsts, swaggerDisplayEnum.value());
                Object fieldDesc = ReflectUtil.getFieldValue(enumConsts, swaggerDisplayEnum.desc());
                return fieldValue + ":" + fieldDesc;
            }).collect(Collectors.joining(";"));
    return propertyReference.value() + "(" + enumFullDesc + ")";
}

测试下输出如下面的格式,自动将枚举类中所有的枚举值及其描述信息都展示出来了。

(1:新增;2:更新;3:删除)

实现自定义扩展处理器

至此呢,我们已经做好了全部的准备工作,下面就可以按照前面分析的策略,来自定义一个实现类去实现相关接口,将我们的处理转换逻辑注入到Swagger框架中去。

@Component
@Primary
public class SwaggerEnumBuilderPlugin implements ModelPropertyBuilderPlugin, ParameterBuilderPlugin {
    @Override
    public void apply(ModelPropertyContext context) {
        // Model中field字段描述的自定义处理策略
    }
    @Override
    public void apply(ParameterContext parameterContext) {
        // API中入参的自定义处理策略
    }
    @Override
    public boolean supports(DocumentationType delimiter) {
        return true;
    }
}

下面只需要在apply方法中补充上我们的自定义处理逻辑即可。

自动生成API入参的取值说明

前面已经讲了如何将指定的枚举类中的枚举值生成为描述字符串,在这里我们直接调用,然后将结果设置到context上下文中即可。

@Override
public void apply(ParameterContext context) {
    ApiPropertyReference reference =
            context.getOperationContext().findAnnotation(ApiPropertyReference.class).orNull();
    String desc = generateValueDesc(reference);
    if (StringUtils.isNotEmpty(reference.name())) {
        context.parameterBuilder().name(reference.name());
    }
    context.parameterBuilder().description(desc);
    AllowableListValues allowableListValues = getAllowValues(reference);
    context.parameterBuilder().allowableValues(allowableListValues);
}

自动生成Model中字段取值说明

同样的策略,我们处理下数据实体类中的field对应的含义说明。

@Override
public void apply(ModelPropertyContext modelPropertyContext) {
    if (!modelPropertyContext.getBeanPropertyDefinition().isPresent()) {
        return;
    }
    BeanPropertyDefinition beanPropertyDefinition = modelPropertyContext.getBeanPropertyDefinition().get();
    // 生成需要拼接的取值含义描述内容
    String valueDesc = generateValueDesc(beanPropertyDefinition);
    modelPropertyContext.getBuilder().description(valueDesc)
            .type(modelPropertyContext.getResolver()
                    .resolve(beanPropertyDefinition.getField().getRawType()));
}
}

效果演示

到这里呢,代码层面的处理就全部完成了。接下来运行下程序,看下效果。先来看下API接口中入参的含义描述效果:

从界面效果上可以看出,不仅自动将取值说明描述给显示出来,同时界面调测的时候,输入框也变为了下拉框 (因为我们自动给设置了allowableValues属性),只能输入允许的值。同样的,再来看下Model中的字段的含义说明描述效果:

可以看到,接口文档中的参数描述信息中,已经自动带上了枚举类中定义的候选取值内容与说明。我们仅修改下枚举类中的内容,其余地方不做修改,再次看下界面,发现Swagger接口中的描述内容已经同步更新为最新的内容。

完美,大功告成???。

总结

好啦,关于如何通过自定义注解的方式扩展Swagger的能力让Swagger支持自动从指定的枚举类生成接口文档中的字段描述的实现思路,这里就给大家分享到这里啦。关于本篇内容你有什么自己的想法或独到见解么?欢迎在评论区一起交流探讨下吧。

??啰嗦两句

  • 写到这里忽然察觉到,其实 Swagger 会用很容易,但想用好却还是需要一定功夫的,所以趁势决定针对如何在项目中真正的用好Swagger再单独的写一篇文档,近期会分享出来。感兴趣的小伙伴可以关注下,避免迷路。???

  • 关于本文中涉及的演示代码的完整示例,我已经整理并提交到github中,如果您有需要,可以自取:https://github.com/veezean/JavaBasicSkills

我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点赞 + 关注让我感受到您的支持。也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。

有关JAVA中自定义扩展Swagger的能力,自动生成参数取值含义说明,提升开发效率的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  3. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  4. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  5. ruby - 主要 :Object when running build from sublime 的未定义方法 `require_relative' - 2

    我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby​​1.9+ 关于ruby-主要:Objectwhenrun

  6. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  7. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  8. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  9. ruby - 定义方法参数的条件 - 2

    我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano

  10. ruby - 如何在 Grape 中定义哈希数组? - 2

    我使用Ember作为我的前端和GrapeAPI来为我的API提供服务。前端发送类似:{"service"=>{"name"=>"Name","duration"=>"30","user"=>nil,"organization"=>"org","category"=>nil,"description"=>"description","disabled"=>true,"color"=>nil,"availabilities"=>[{"day"=>"Saturday","enabled"=>false,"timeSlots"=>[{"startAt"=>"09:00AM","endAt"=>

随机推荐