大家好,又见面了。
在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注解中的枚举类必须遵循某个固定的格式,但显然这样实施的难度就会提升,并非是我们想要的结果。
先来看下面给定的这个枚举类,其中包含order
、value
、desc
三个属性值,而value
字段是我们的接口字段需要传入的真实取值,desc
是其对应的含义描述,那么该如何让我们自定义Swagger扩展类知晓应该使用value
和desc
字段来生成文档描述内容呢?
@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
我是悟道,聊技术、又不仅仅聊技术~
如果觉得有用,请点赞 + 关注让我感受到您的支持。全网同名,欢迎关注。
期待与你一起探讨,一起成长为更好的自己。