大家好,又见面了。

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. 什么是静态代码块?java中如何使用空参构造方法自动生成不同名字的对象,使用非静态的属性和静态属性有什么区别,原因是什么?如何理解static关键字

    静态代码块?类加载就执行,最先执行 class demo{ static int num; static{ num=10; num*=3; System.out.println("haha& ...

  2. Eclipse中设置在创建新类时自动生成注释

    方法一:Eclipse中设置在创建新类时自动生成注释 windows-->preference Java-->Code Style-->Code Templates code--&g ...

  3. springboot成神之——swagger文档自动生成工具

    本文讲解如何在spring-boot中使用swagger文档自动生成工具 目录结构 说明 依赖 SwaggerConfig 开启api界面 JSR 303注释信息 Swagger核心注释 User T ...

  4. java中遍历实体类,获取属性名和属性值

    方式一(实体类): //java中遍历实体类,获取属性名和属性值 public static void testReflect(Object model) throws Exception{ for ...

  5. 【Java思考】Java 中的实参与形参之间的传递到底是值传递还是引用传递呢?

    科普: 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数. 引用传递(pass by reference)是指在 ...

  6. atitit.提升开发效率---使用服务器控件生命周期 asp.net 11个阶段 java jsf 的6个阶段比较

    atitit.提升开发效率---使用服务器控件生命周期  asp.net 11个阶段  java jsf 的6个阶段比较 如下列举了服务器控件生命周期所要经历的11个阶段. (1)初始化-- --在此 ...

  7. Java进阶教程:使用Lombok提升开发效率

    Java进阶教程:使用Lombok提升开发效率 Lombok Lombok是一种Java™实用工具,可用来帮助开发人员消除Java的冗长代码,尤其是对于简单的Java对象(POJO).它通过注释实现这 ...

  8. atitit.提升开发效率---MDA 软件开发方式的革命(3)----自动化建表

    atitit.提升开发效率---MDA 软件开发方式的革命(3)----自动化建表 1. 建模在后自动建表 1 1. 传统上,需要首先建表,在业务编码.. 1 2. 模型驱动建表---更多简化法是在建 ...

  9. atitit.提升开发效率---使用server控件生命周期 asp.net 11个阶段 java jsf 的6个阶段比較

    atitit.提升开发效率---使用server控件生命周期  asp.net 11个阶段  java jsf 的6个阶段比較 例如以下列举了server控件生命周期所要经历的11个阶段. (1)初始 ...

随机推荐

  1. 论文解读(SR-GNN)《Shift-Robust GNNs: Overcoming the Limitations of Localized Graph Training Data》

    论文信息 论文标题:Shift-Robust GNNs: Overcoming the Limitations of Localized Graph Training Data论文作者:Qi Zhu, ...

  2. Node.js精进(5)——HTTP

    HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种获取网络资源(例如图像.HTML文档)的应用层协议,它是互联网数据通信的基础,由请求和响应构成. 在 Node ...

  3. RabbitMQ:消息丢失 | 消息重复 | 消息积压的原因+解决方案+网上学不到的使用心得

    前言 首先说一点,企业中最常用的实际上既不是RocketMQ,也不是Kafka,而是RabbitMQ. RocketMQ很强大,但主要是阿里推广自己的云产品而开源出来的一款消息队列,其实中小企业用Ro ...

  4. Iterator接口介绍和迭代器的代码实现

    定义:Iterator接口是Java集合框架中的一员. 作用:Collection接口与Map接口主要用于存储元素. 常用方法:  boolen hasNext();    //判断游标右边是否还有元 ...

  5. C#(.net) 面试题

    1.ASP.NET的页面生存周期 .aspx/.ashx->IIS->Asp.net_isapi.dll->HttpRuntime.ProcessRequest() ->Htt ...

  6. CodeSys中编程实现串口通讯

    第一步:Linux中启用串口设备.[以树莓派4B为例] 树莓派4B有6个串口,参考上一篇<>,在 /boot/config.txt 中添加一行,开启 uart2 功能:   dtoverl ...

  7. Linux下IPC之共享内存的使用方法

    基本参考 <Unix环境高级编程>第14.9节共享内存来学习. 参考blog:https://blog.csdn.net/weixin_45794138/article/details/1 ...

  8. 第八天pyhton3 函数的返回值、作用域

    返回值 pthon函数使用return语句返回"返回值": 所有函数都有返回值,如果没有return语句,隐式调用return None: return 语句并不一定是函数的语句块 ...

  9. BufferedWriter字符缓冲输出流和BufferedReader字符缓冲输入流

    package com.yang.Test.BufferedStudy; import java.io.BufferedWriter; import java.io.FileWriter; impor ...

  10. Iconfont(矢量图标)+iconmoon(图标svg互转)配合javascript来打造属于自己的个性化社交分享系统

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_143 每一个应用程序,其实都会有分享的需求,比如一键分享一篇文章或者一些活动到微博或者微信亦或者是twitter等社交平台,因为人 ...