让swagger文档支持后台分组校验

回顾前一小节,我们实现了商品模块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设计上了,大家加油!

相关推荐
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭9 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
AskHarries11 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion12 小时前
Springboot的创建方式
java·spring boot·后端
Yvemil712 小时前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
星河梦瑾14 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
计算机学长felix15 小时前
基于SpringBoot的“交流互动系统”的设计与实现(源码+数据库+文档+PPT)
spring boot·毕业设计
.生产的驴15 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲15 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
撒呼呼15 小时前
# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)
数据库·spring boot·spring·mvc·springboot
因我你好久不见15 小时前
springboot java ffmpeg 视频压缩、提取视频帧图片、获取视频分辨率
java·spring boot·ffmpeg