当前位置: 首页 > 科技观察

Java自定义扩展Swagger的能力,以及通过枚举类自动生成参数值含义描述的实现策略

时间:2023-03-20 11:24:30 科技观察

大家好,我们又见面了。JAVA在开发前后端分离的项目时,服务端需要提供接口文档,供外围人员进行接口引导。越来越多的项目正在尝试使用一些工具,根据代码自动生成接口文档,而不是开发人员手动编写接口文档。Swagger作为一款优秀的在线界面文档生成工具,以其强大的功能和方便的集成而著称。已被广泛使用。项目中有一个很常见的场景,就是接口的请求或响应参数中某些字段的取值会被限定为几个固定可选值中的一个,而代码中这些可选值经常定义为by例如根据操作类型,过滤对应类型的用户操作日志列表,如:http://127.0.0.1:8088/test/queryOperateLogs?operateType=2传入的值这里的请求参数operateType需要在后台约定的取值范围内。取值范围定义如下:@Getter@AllArgsConstructorpublicenumOperateType{ADD(1,"添加或创建操作"),MODIFY(2,"更新已有数据操作"),DELETE(3,"删除数据操作"),QUERY(4,"查询数据操作");私有整数值;privateStringdesc;}这里需要在接口文档中添加这个接口中operateType的可选值,并且清楚的解释每个可选值对应的含义信息,让调用者知道在使用的时候应该传入什么值。当我们基于Swagger提供的基本注解能力实现时,我们会比较常见的看到以下两种写法:写法一:定义接口时,指定入参的值,表示接口携带的请求输入信息URL,通过@ApiImplicitParam注解告诉调用者这个接口允许接收的合法operateType值的范围以及每个值的含义。例如下面的场景:@GetMapping("/queryOperateLogs")@ApiOperation("查询指定操作类型的操作日志列表")@ApiImplicitParam(name="operateType",value="操作类型,值说明:1,newIncrement;2,Update;3,Divide;4,Query",dataType="int",paramType="query")publicListqueryOperateLogs(intoperateType){returntestService.queryOperateLogs(operateType);}中这样,在swagger界面上就可以显示该字段的值描述信息了。其实还有另外一种写法,就是在代码的入参前加上@ApiParam注解来实现。例如:@GetMapping("/queryOperateLogs")@ApiOperation("查询指定操作类型的操作日志列表")publicListqueryOperateLogs(@ApiParam(value="操作类型,取值说明:1,new;2、更新;3、删除;4、查询")@RequestParam("type")intoperateType){returntestService.queryOperateLogs(operateType);}这样也可以达到同样的效果。写法二:请求或响应body中解释字段的值描述对于需要使用jsonbody传输的请求或响应消息体Model,可以使用@ApiModelProperty添加含义描述。@Data@ApiModel("操作记录信息")publicclassOperateLog{@ApiModelProperty("操作类型,取值说明:1、增加;2、更新;3、删除;4、查询")privateintoperateType;@ApiModelProperty("操作用户")privateString用户;@ApiModelProperty("OperationDetails")privateStringdetail;}同样可以清楚的知道Swagger接口上各个字段的具体含义和取值说明。但是,以上两种写法都存在相同的问题,即如果枚举类中值内容的含义发生变化,例如在OperateType中增加了一个newBATCH_DELETE(5,"batchdelete")枚举类,则需要手动修改所有涉及接口的Swagger描述信息。如果这个字段涉及到的场景比较多,需要改的地方很多,很容易遗漏(因为直接通过代码关联关系查找不好)。这样一来,开发人员的维护成本就会增加,久而久之,接口文档的内容就会与实际的代码处理不匹配。那么,有什么简单的方法可以让接口文档根据对应枚举类的内容自动动态变化呢?Swagger并没有提供这方面的原生能力支持,但是我们可以通过一些简单的方式扩展Swagger能力,让Swagger支持我们的需求。让我们来看看如何实现它。扩展可行性分析既然要在生成的Swagger文档中更改指定字段的描述内容,那么首先应该弄清楚Swagger中当前的内容生成逻辑是如何处理的。我们以@ApiParam为例进行分析。因为@ApiParam中指定的内容会显示在Swagger界面上,那么在Swagger的框架中,肯定会有一个地方会尝试获取这个注解中指定的相关字段值,然后传递内容对文档界面内容的注解。所以想要定制,首先要了解它目前是如何处理的。查看Swagger的源码,发现这部分逻辑是在ApiParamParameterBuilder类中处理的。处理逻辑如下:读取这个类后,就是ParameterBuilderPlugin接口的一个实现类。当Swagger框架逐个遍历生成参数描述信息时,就会调用这个实现类的逻辑来执行。至此,问题就已经很明显了。我们可以自定义一个处理类,实现ParameterBuilderPlugin接口,然后在自定义处理类中实现我们的需求。难道不能实现我们的诉求吗?同样的策略,我们可以找到@ApiImplicitParam和@ApiModelProperty对应的接口类。根据上面的分析,我们只需要提供一个自定义的实现类,然后分别实现这些接口就可以满足我们的需求了。那么作为全场景的通用能力应该如何封装和使用呢?让我们在下面详细讨论它。基于枚举类生成描述的自定义注解实现我们已经找到了将我们自定义的逻辑注入到Swagger的文档生成框架中进行调用的方法,所以下一步我们要确认一个比较简单的策略,告诉框架哪些Field需要使用枚举自动生成值描述,以及使用哪个枚举类来生成它们。这里我们使用自定义注解来实现。Swagger针对不同的场景提供了@APIParam、@ApiImplicitParam、@ApiModelProperty等不同的注解。我们可以对其进行简化,提供统一的自定义注解。例如:@Target({ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)public@interfaceApiPropertyReference{//接口文档上显示的字段名,如果没有设置,则使用原来的字段名称Stringname()default"";//字段简要说明,optionalStringvalue()default"";//标识字段是否必填booleanrequired()defaultfalse;//指定值对应的枚举类ClassreferenceClazz();}这样,对于需要添加值描述的字段或接口,我们可以添加@ApiPropertyReference,并指定对应的枚举类。例如如下:@Data@ApiModel("操作记录信息")publicclassOperateLog{@ApiPropertyReference(value="操作类型",referenceClazz=OperateType.class)privateintoperateType;//...}在上面的示例代码中,OperateType是定义的枚举类。现在我们遇到了另一个问题。枚举类的实现形式其实是不一样的。怎样才能让我们的内容自动生成服务知道获取枚举类中的哪些内容进行处理呢?当然,我们可以同意Swagger注解中使用的枚举类必须遵循固定的格式,但是显然实现难度会增加,这不是我们想要的结果。先看下面给出的枚举类,包含三个属性值order、value、desc,其中value字段就是我们接口字段需要传入的真实值,desc是它对应的description的意思,那么如何让我们自定义Swagger扩展类知道应该使用value和desc字段来生成文档描述内容呢?@Getter@AllArgsConstructorpublicenumOperateType{ADD(1,11,"添加"),MODIFY(2,22,"更新"),DELETE(3,33,"删除");私人订单;私有整数值;privateStringdesc;}答案并不陌生,还是自定义注解!只需要提供一个自定义的注解,然后添加到枚举类中,并指定枚举类中的哪个字段作为值,用哪个字段来描述desc字段的值。@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public@interfaceSwaggerDisplayEnum{Stringvalue()默认“值”;Stringdesc()default"desc";}这样,在枚举类SwaggerDisplayEnum上加上@,指定下一个字段的映射,可以在Swagger注解中使用:这里,我们的数据来源和值转换规则需要的都已经确定了,剩下的就是如何把一个枚举类中需要的值和描述字段拼接成想要的内容了。因为是通用能力,所以这里需要通过反射来实现:privateStringgenerateValueDesc(ApiPropertyReferencepropertyReference){ClassrawPrimaryType=propertyReference.referenceClazz();SwaggerDisplayEnumswaggerDisplayEnum=AnnotationUtils.findAnnotation(rawPrimaryType,SwaggerDisplayEnum);StringenumFullDesc=Arrays.stream(rawPrimaryType.getEnumConstants()).filter(Objects::nonNull).map(enumConsts->{ObjectfieldValue=ReflectUtil.getFieldValue(enumConsts,swaggerDisplayEnum.value());ObjectfieldDesc=ReflectUtil.getFieldValue(enumConsts,swaggerDisplayEnum.desc());returnfieldValue+":"+fieldDesc;}).collect(Collectors.joining(";"));returnpropertyReference.value()+"("+enumFullDesc+")";}测试下输出如下格式,自动显示枚举类中的所有枚举值及其描述信息。(1:添加;2:更新;3:删除)自定义扩展处理器的实现至此,我们已经做好了所有的准备工作。接下来,我们可以定义一个实现类来实现相关的Interface,将我们的处理转换逻辑注入到Swagger框架中。@Component@PrimarypublicclassSwaggerEnumBuilderPluginimplementsModelPropertyBuilderPlugin,ParameterBuilderPlugin{@Overridepublicvoidapply(ModelPropertyContextcontext){//Model中field字段描述的自定义处理策略}@Overridepublicvoidapply(ParameterContextparameterContext){//API自定义中的参数处理策略}@Overridepublicbooleansupports(DocumentationTypedelimiter){returntrue;接下来,我们只需要在apply方法中加入我们自定义的处理逻辑即可。API输入参数值自动生成说明我们已经讲过如何将指定枚举类中的枚举值生成为描述字符串。这里我们直接调用它,然后将结果设置到上下文中。@Overridepublicvoidapply(ParameterContextcontext){ApiPropertyReferencereference=context.getOperationContext().findAnnotation(ApiPropertyReference.class).orNull();Stringdesc=generateValueDesc(参考);如果(StringUtils.isNotEmpty(reference.name())){context.parameterBuilder().name(reference.name());}context.parameterBuilder().description(desc);AllowableListValuesallowableListValues=getAllowValues(reference);context.parameterBuilder().allowableValues(allowableListValues);}自动生成Model中字段的值,解释同理。我们来处理数据实体类中字段对应的含义的描述。@Overridepublicvoidapply(ModelPropertyContextmodelPropertyContext){if(!modelPropertyContext.getBeanPropertyDefinition().isPresent()){返回;}BeanPropertyDefinitionbeanPropertyDefinition=modelPropertyContext.getBeanPropertyDefinition().get();//生成需要拼接的值描述内容StringvalueDesc=generateValueDesc(beanPropertyDefinition);modelPropertyContext.getBuilder().description(valueDesc).type(modelPropertyContext.getResolver().resolve(beanPropertyDefinition.getField().getRawType()));}}效果演示到这里,代码层级处理全部完成。接下来运行程序,看看效果。先看API接口中输入参数含义的描述效果:从接口效果可以看出,不仅自动显示了值的描述,而且在接口调试的时候,输入box也变成了下拉框(因为我们自动设置了allowableValues属性),只能输入允许的值。同样我们看一下Model中字段的描述效果:可以看到接口文档中的参数描述信息已经自动携带了枚举类中定义的候选值内容和描述。我们只修改枚举类中的内容,其他不变。再看界面,发现Swagger界面中的描述内容已经同步更新为最新内容。完美,你完成了