让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设计上了,大家加油!

相关推荐
用户8307196840824 小时前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解5 小时前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解5 小时前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记9 小时前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者1 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840821 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解1 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者2 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺2 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端
Derek_Smart2 天前
从一次 OOM 事故说起:打造生产级的 JVM 健康检查组件
java·jvm·spring boot