Spring Validation的校验顺序问题解决方案

  • 问题场景
  • 原理剖析
  • 解决方法
  • 创建五个接口
  • 修改Controller控制层代码
  • 修改实体类代码
  • 整改结果
  • 后续问题
  • 问题原因
  • 解决方案


问题场景

测试发现对同一个接口调用多次时,返回的校验异常信息不同,经过问题追踪,入参实体类代码如下:

@Data
public class EditDevNameDto {
    @NotBlank(message = "deviceSn must not null")
    private String deviceSn;
    @NotBlank(message = "deviceName must not null")
    private String deviceName;
}

当这个接口入参的deviceSn和deviceName均为空值时,调用多次的话会出现两个msg错误信息循环返回的问题。

原理剖析

怀疑是调用接口时,校验注解的先后顺序是不确定的,所以,可能deviceSn先被校验,可能deviceName先被校验,那要解决这个问题就要规定校验的顺序才行。

解决方法

使用@GroupSequence注解实现顺序的稳定性。

创建五个接口

public interface GroupA {
}

public interface GroupB {
}

public interface GroupC {
}

public interface GroupD {
}

@GroupSequence({GroupA.class,GroupB.class,GroupC.class,GroupD.class})
public interface Group {
}

修改Controller控制层代码

注意在入参中加入@Validated(Group.class)注解,其中要加入被@GroupSequence修饰的类对象。

@PostMapping("/edit_device_name")
    public ExecuteResult editDevName(
    	@RequestBody @Validated(Group.class) EditDevNameDto dev) {
        // -----逻辑代码
        return null;
    }

修改实体类代码

在实体类中使用校验注解中添加groups属性,顺序按照@GroupSequence类规定的顺序即可。

@Data
public class EditDevNameDto {
    @NotBlank(message = "deviceSn must not null", groups = {GroupA.class})
    private String deviceSn;
    @NotBlank(message = "deviceName must not null", groups = {GroupB.class})
    private String deviceName;
}

整改结果

当这个接口入参的deviceSn和deviceName均为空值时,频繁调用依然是按照先校验deviceSn,后校验deviceName的顺序进行参数校验。

后续问题

问题原因

解决了上面的问题,过了几天,又出现了解决校验顺序的问题,但是这次又不同于上一次,这次的Dto参数接收类有些复杂,代码如下:

@Data
public class UpdateInfoDto {

    @NotBlank(message = "deviceSn can not be empty")
    @GBDeviceSnValid
    private String deviceSn;


    @Valid // 让CommonDto类中的校验属性生效
    @GBChannelDuplicateValid
    @CollectionNotEmptyValid(message = "channels cannot be empty")
    private List<CommonDto> channels;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonDto{

    @GBChannelIdValid
    private String id;

    @NotNull(message = "channelId must not be null")
    @PositiveOrZero(message = "channelId only Integer or zero")
    private Integer channelId;
}

以上代码中有两个类,共同组合成参数接收类,要纠正校验顺序混乱问题,还是需要用到一开始讲到的@GroupSequence注解,但是,有两个问题:

  1. Dto中还有一个对象类型的属性。
  2. 对象类型属性中的子属性(id、channelId)也要成功校验

解决方案

首先,为了让Dto中的对象类型的属性也能正常校验,需要添加@Valid注解;
然后,在对象类型属性中的子属性中也需要像文章前面所说的在校验注解中设置groups属性。
修改后的代码如下:

@Data
public class UpdateInfoDto {

    @NotBlank(message = "deviceSn can not be empty", groups = {GroupA.class})
    @GBDeviceSnValid(groups = {GroupA.class})
    private String deviceSn;


    @Valid // 让CommonDto类中的校验属性生效
    @GBChannelDuplicateValid(groups = {GroupB.class})
    @CollectionNotEmptyValid(message = "channels cannot be empty", groups = {GroupB.class})
    private List<CommonDto> channels;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonDto{

    @GBChannelIdValid(groups = {GroupC.class})
    private String id;

    @NotNull(message = "channelId must not be null", groups = {GroupD.class})
    @PositiveOrZero(message = "channelId only Integer or zero", groups = {GroupD.class})
    private Integer channelId;
}

如果CommonDto中校验属性的注解不设置groups,CommonDto中的校验属性就会失效,这是个大坑