回顾前一小节,我们实现了商品模块api定义,通过openApi3文档的定义,直接让swagger生成器帮我们生成API接口和DTO类,对controller做空实现。测试发现swagger在线文档对后台分组校验并未区分的缺陷。本节我们就通过修改springdoc框架的源码来修复这一缺陷。
分组校验文档定义
在我们开发业务模块Rest API时,对于新增和修改操作通常都会采用一个XxxDTO来保存前端提交的数据。而这两种操作在必填校验上有所区分,而其他格式校验都是相同的,很显然我们就可以采用validation框架的分组校验特性,只对字段是否必输(采用@NotNull
和@NotBlank
注解)做分组的区分。
前面我们也介绍了怎么为基于openApi3文档的swagger生成器扩展分组校验的代码生成功能,下面以商品保存为例,看下扩展后的openApi3文档的定义:
yaml
# schema定义
components:
schemas:
ProductSave:
description: 商品保存DTO类
type: object
x-groups:
- Add
- Update
properties:
id:
description: 商品id
type: integer
format: int64
x-validation: "@NotNull(message = MSG_PRODUCT_ID_REQUIRED, groups = Update.class)"
categoryId:
description: 三级分类id
type: integer
format: int64
x-validation: "@NotNull(message = MSG_CATEGORY_ID_REQUIRED, groups = Add.class)"
name:
description: 商品名称
type: string
x-validation: "@NotBlank(message = MSG_PRODUCT_NAME_REQUIRED, groups = Add.class) @Size(min = 4, max = 20, message = MSG_PRODUCT_NAME_RANGE)"
imgFile:
description: 图片文件
type: multipartFile
detail:
description: 商品详情
type: string
x-validation: "@Size(min = 50, max = 500, message = MSG_PRODUCT_DETAIL_RANGE)"
price:
description: 价格
type: integer
x-validation: "@NotNull(message = MSG_PRODUCT_PRICE_REQUIRED, groups = Add.class) @Min(value = 1, message = MSG_PRODUCT_PRICE_MIN)"
stock:
description: 库存
type: integer
x-validation: "@NotNull(message = MSG_PRODUCT_STOCK_REQUIRED, groups = Add.class) @Max(value = 10000, message = MSG_PRODUCT_STOCK_MAX)"
注意这里扩展的分组列表定义:
yaml
x-groups:
- Add
- Update
列举出所有的分组,字段校验用我们扩展的x-validation
来定义,可以通过groups
属性来指定针对特定分组的校验,如果不指定则都会被应用。
再看API定义:
yaml
/mall-api/admin/products:
post:
tags:
- productAdmin
summary: 商品新增
description: 新增一条商品记录
operationId: addProduct
x-group: Add
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/ProductSave'
responses:
200:
description: 新增成功
put:
tags:
- productAdmin
summary: 商品更新
description: 更新一条商品记录
operationId: updateProduct
x-group: Update
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/ProductSave'
responses:
200:
description: 更新成功
这里我们用扩展的属性x-group
来指定校验要应用的分组。这样通过swagger生成器生成api接口并对controller做空实现后,我们发现swagger在线文档中对输入框的非空校验并没有区分:
这就导致了在线测试会无差别的加非空校验,而无法提交。
修改springdoc源码
从浏览器控制台获取的json文档内容可以看到,这两个api共用了schema:
而该schema会根据后台DTO类字段上加的非空注解将字段整理到一个数组中,并在页面上应用校验规则。
这里我们要做的,就是修改springdoc相关类的源代码,让这两个API不共用schema,而各个使用各自的schema,这样就可以对必输字段进行各自控制了。我们调整下SpringDocAnnotationsUtils.java
:
java
...
public class SpringDocAnnotationsUtils {
...
private static Class<?>[] findValidateGroupClzs(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof Validated) {
return ((Validated) annotation).value();
}
}
return null;
}
private static String genGroupName(Class<?>[] groupClzs) {
StringBuilder sb = new StringBuilder();
for (Class<?> clz : groupClzs) {
sb.append(clz.getSimpleName());
}
return sb.toString();
}
private static boolean containsGroup(Class<?>[] sub, Class<?>[] all) {
for (int i = 0; i < sub.length; i++) {
if (ArrayUtils.contains(all, sub[i])) {
return true;
}
}
return false;
}
public static Schema extractSchema(Components components, Type returnType, JsonView jsonView, Annotation[] annotations) {
Schema schemaN = null;
ResolvedSchema resolvedSchema;
try {
resolvedSchema = ModelConverters.getInstance()
.resolveAsResolvedSchema(
new AnnotatedType(returnType).resolveAsRef(true).jsonViewAnnotation(jsonView).ctxAnnotations(annotations));
// 判断参数是否采用了分组
Class<?>[] validateGroupClzs = findValidateGroupClzs(annotations);
if (!ObjectUtils.isEmpty(validateGroupClzs)) {
String groupName = genGroupName(validateGroupClzs);
String refName = resolvedSchema.schema.get$ref();
String dtoName = refName.substring(21);
resolvedSchema.schema.set$ref(refName + "-" + groupName);
ResolvedSchema rs = new ResolvedSchema();
rs.schema = resolvedSchema.schema;
Map<String, Schema> refSchemas = new LinkedHashMap<>(resolvedSchema.referencedSchemas);
Schema schema;
refSchemas.put(dtoName + '-' + groupName, schema = refSchemas.get(dtoName));
refSchemas.remove(dtoName);
rs.referencedSchemas = refSchemas;
resolvedSchema = rs;
// 按照分组判断哪些字段为必传的
Field[] fields = ((Class) returnType).getDeclaredFields();
for (Field field : fields) {
Method method = ((Class) returnType).getDeclaredMethod("get" + StringUtils.capitalize(field.getName()));
NotNull notNull = AnnotationUtils.findAnnotation(method, NotNull.class);
NotBlank notBlank = AnnotationUtils.findAnnotation(method, NotBlank.class);
if (notNull != null && notNull.groups() != null && !containsGroup(validateGroupClzs, notNull.groups())) {
schema.getRequired().remove(field.getName());
} else if (notBlank != null && notBlank.groups() != null && !containsGroup(validateGroupClzs, notBlank.groups())) {
schema.getRequired().remove(field.getName());
}
}
}
} catch (Exception e) {
LOGGER.warn(Constants.GRACEFUL_EXCEPTION_OCCURRED, e);
return null;
}
...
}
...
}
代码调整说明
这里我们基于swagger核心api所处理得到的
resolvedSchema
,进行了一次搬运,new
出一个副本后,把其中的schema以及引用对象搬运过来,对referencedSchemas
中map结构的key重新绑定,以分组的名称作为其后缀,并通过新的$ref
进行schema的关联。这样就可以对各自的schema中required
列表按照参数对象用@Validated
修饰的分组信息和字段上校验应用的分组信息进行判断,处理要不要从required
列表中移除该字段。
修改后的源码本地重新Install,并在项目中刷新依赖包,这样重新启动服务,访问swagger在线文档,我们将看到schema确实分开了,并且api的测试表单域的非空校验也区分开了:
到此,我们的swagger生成器和springdoc框架源码修改以定制一些扩展功能的尝试基本都结束了,以后我们就可以放心的编写openApi V3的文档,并用生成器生成可较完美的展示在线swagger文档的代码了。这样我们就可以放心的把精力放到业务模块的API设计上了,大家加油!