likes
comments
collection
share

SpringBoot项目开发规范之参数校验

作者站长头像
站长
· 阅读数 4

引言

在日常中我们运行的应用在接收用户的请求时都需要对请求参数作校验。通常除了在前端一侧限制参数外,为了系统安全还需要在后端做参数校验。因此,在后端的项目开发中需要对参数校验做统一的规范处理。Bean Validation 正是这样一种提供统一的方式来验证数据的规范。


Bean Validation是什么

Bean Validation 是一种 Java 技术规范,旨在提供统一的方式来验证 JavaBean 中的数据。Bean Validation 规范历经几次发展,从JSR 303JSR 394JSR380,到现在的Jakarta Bean Validation 3.0

规范名称JSR 编号发布日期
Jakarta Bean Validation 3.0-2020年10月
Jakarta Bean Validation 2.0-2019年8月
Bean Validation 2.0JSR 3802017年8月
Bean Validation 1.1JSR 3492013年
Bean Validation 1.0JSR 3032009年

Hibernate Validator是什么

前面我们已经知道 Bean Validation 只是一种规范,但是它并没有提供具体的实现。我们要在代码中使用,就需要有规范的具体实现。实际上,也有依据规范提供参考实现的解决方案,Hibernate Validator 就是 Bean Validation 规范的参考实现。 Hibernate Validator 使用基于注解的约束来表达验证规则。在 SpringBoot 项目中通过引入依赖就可以使用。

规范版本Maven构件版本Java版本
Jakarta Bean Validation 3.0hibernate-validator:8.0.1.FinalJava 11, 17 or 21
Jakarta Bean Validation 2.0hibernate-validator:6.2.5.FinalJava 8, 11 or 17
Bean Validation 2.0 (JSR 380)hibernate-validator:6.0.1.FinalJava 8, 11 or 17
Bean Validation 1.1 (JSR 349)hibernate-validator:5.1.1.FinalJava 6 or 7
Bean Validation 1.0 (JSR 303)hibernate-validator:4.3.1.FinalJava 6

SpringBoot项目应用

本次示例基于 Jakarta Bean Validation 2.0 规范,演示在 SpringBoot 2.3.12.RELEASE 版本框架项目中的应用。

引入依赖

在 SpringBoot 2.3.x 版本之前,spring-boot-starter-webspring-boot-starter-validation都已引入hibernate-validator, 而在 SpringBoot 2.3.x 及之后的版本中则需要手动引入依赖。

<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.2.0.Final</version>
</dependency>

常用约束注解

@Null

被注解的元素对象必须为null

@NotNull

被注解的元素对象不能为null

@NotEmpty

被注解的元素对象不能为null或空。用于CharSequenceCollectionMap以及数组类型。

@NotBlank

被注解的元素对象不能为nulltrim后长度大于0。仅用于CharSequence类型。

@AssertTrue

被注解的元素对象必须为true。用于Booleanboolean类型。

@AssertFalse

被注解的元素对象必须为false。用于Booleanboolean类型。

@Size(min=, max=)

被注解的元素对象的长度大小包含在minmax内。用于CharSequenceCollectionMap以及数组类型。

@Min(value=)

被注解的元素数值必须大于等于指定值。用于BigDecimalBigIntegerbyteshortintlong及其包装类;Hibernate Validator 还支持:CharSequence 的任何子类型(字符串表示的数值)、Number的任何子类型以及javax.money.MonetaryAmount

@Max(value=)

被注解的元素数值必须小于等于指定值。用于BigDecimalBigIntegerbyteshortintlong及其包装类;Hibernate Validator 还支持:CharSequence 的任何子类型(字符串表示的数值)、Number的任何子类型以及javax.money.MonetaryAmount

@DecimalMin(value=, inclusive=)

被注解的元素数值必须大于(inclusive=false)或大于等于(inclusive=true)指定值。用于BigDecimalBigIntegerCharSequencebyteshortintlong及其包装类;Hibernate Validator 还支持:Number的任何子类型以及javax.money.MonetaryAmount

@DecimalMax(value=, inclusive=)

被注解的元素数值必须小于(inclusive=false)或小于等于(inclusive=true)指定值。用于BigDecimalBigIntegerCharSequencebyteshortintlong及其包装类;Hibernate Validator 还支持:Number的任何子类型以及javax.money.MonetaryAmount

@Digits(integer=, fraction=)

被注解的元素数值必须在最大的整数位数和最大的小数位位数之内。用于BigDecimalBigIntegerCharSequencebyteshortintlong及其包装类;Hibernate Validator 还支持:Number的任何子类型以及javax.money.MonetaryAmount

@Email

被注解的元素字符串必须是邮箱地址格式。仅用于CharSequence类型。

@Valid

被注解的元素对象会进行级联校验 (Cascaded validation)。如果是对象数组、Iterable类型则会对内容元素遍历校验,对于Map则只对其中的Map.Entry#getValue值进行递归校验。

应用示例(JavaBean校验)

我们以一个用户登录接口为例,在这个接口中请求参数会传入usernamepassword;对于业务上来讲,要求这两个参数都不能为null且必须有字符内容,那么就需要用@NotEmpty来约束。如下:

@Data
public class AdminLoginDTO {

    @NotEmpty(message = "用户名不能为空")
    private String username;
    
    @NotEmpty(message = "密码不能为空")
    private String password;
}

之后在controller层的接口方法参数加上注解@Valid即可开启校验。如下:

@PostMapping("/account/login")
public ApiResponse<String> login(@RequestBody @Valid AdminLoginDTO adminLogin) {
    // ......
}

我们在 Postman 中发起请求,尝试传递空字符串的username,响应结果如下:

SpringBoot项目开发规范之参数校验

上面可以看到请求失败了,我们查看控制台输出,发现抛出了MethodArgumentNotValidException,同时也给了一个default message [用户名不能为空]的提示,也证明了参数校验成功应用了。

o.s.w.s.m.support.DefaultHandlerExceptionResolver [199] - Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.lrkj.base.api.ApiResponse<java.lang.String> com.lrkj.admin.controller.admin.AdminAccountController.login(com.lrkj.mall.dto.admin.AdminLoginDTO,javax.servlet.http.HttpServletResponse): [Field error in object 'adminLoginDTO' on field 'username': rejected value [null]; codes [NotEmpty.adminLoginDTO.username,NotEmpty.username,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminLoginDTO.username,username]; arguments []; default message [username]]; default message [用户名不能为空]] ]

统一异常处理

前面我们成功地演示了参数校验的应用示例。不过那样的响应显然对开发和用户都不够友好,我们希望接口响应能给出明确的错误提示,这时就需要统一异常处理,对抛出MethodArgumentNotValidException的异常信息重新封装。 要统一异常处理,可以在统一在一个@RestControllerAdvice类中处理,当要处理某个异常时则在方法上使用@ExceptionHandler注解,之后在方法中编码异常处理逻辑,如下:

@RestControllerAdvice
public class GlobalExceptionAdvice {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public <T> ApiResponse<T> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {

        String message = "参数错误";
        BindingResult result = e.getBindingResult();
        // 判断异常结果中有无错误信息
        if (result.hasErrors()) {
            // 获取错误信息
            List<ObjectError> errors = result.getAllErrors();
            if (!errors.isEmpty()) {
                // 获取第一条错误信息就足够了
                message = errors.get(0).getDefaultMessage();
            }
        }
        return ApiResponse.result(ApiResult.PARAM_ERROR, message);
    }
}

方法参数校验

在前面的应用示例中,展示的是 JavaBean 中的参数校验,对于方法参数同样也可以实现参数校验。 例如需要对获取用户详情接口的id入参作校验,那么就使用NotNull注解作约束,之后还需要在 Controller 类上注解@Validated,这是一个 Spring 提供的可以用于方法参数校验的注解,基于@Valid封装。

@RestController
@RequestMapping("/admin-service")
@Validated
public class AdminUserController {
    
    @GetMapping("/adminUser/get")
    public ApiResponse<AdminUser> get(@RequestParam("id") @NotNull(message = "id不能为空") Long id) {

        return ApiResponse.success(adminUserService.findById(id));
    }
}

当然,这时候异常则是抛出ConstraintViolationException异常,同样地我们也要统一处理,如下:

@ExceptionHandler(ConstraintViolationException.class)
public <T> ApiResponse<T> ConstraintViolationExceptionHandler(ConstraintViolationException e) {

    String message = "参数错误";
    Set<ConstraintViolation<?>> result = e.getConstraintViolations();
    if (!result.isEmpty()) {
        ConstraintViolation<?> error = result.stream().findFirst().orElse(null);
        message = error.getMessage();
    }
    return ApiResponse.result(ApiResult.PARAM_ERROR, message);
}

级联校验

如果要进行参数的校验的 JavaBean 中还有嵌套类,且需要对嵌套类作参数校验,那么也需要使用@Valid注解。例如在保存用户接口中除了username还有一个UserInfo类型字段,且要求不能为null,其写法如下:

@Data
public class AdminLoginDTO implements Serializable {

    @NotEmpty(message = "用户名不能为空")
    private String username;

    @Valid
    @NotNull(message = "用户信息不能为空")
    private UserInfo userInfo;

    @Data
    public class UserInfo {
        @NotNull(message = "年龄为必填项")
        private Integer age;
    }
}

业务编码校验

前面的参数校验示例中,都是基于注解来实现校验的。但是在实际业务编码中,也需要单独对参数作校验。 首先我们先编写一个用于校验的工具类,其中可以自定义各类校验方法。在定义的参数校验方法中,第一个参数value代表要被校验的元素对象,第二个参数errorMsg则代表未通过校验的错误信息;当约束不符合预期时则抛出ValidationException异常。如下:

public class Validator {

    private Validator() {}

    public static <T> T validateNotEmpty(T value, String errorMsg) throws ValidationException {
        if (isEmpty(value)) {
            throw new ValidationException(errorMsg);
        } else {
            return value;
        }
    }
}

ValidationException是一个自定义的异常,如下:

public class ValidationException extends RuntimeException {

    public ValidationException() {
    }

    public ValidationException(String message) {
        super(message);
    }
}

同样地,对于异常我们作统一处理,如下:

@ExceptionHandler(ValidationException.class)
public <T> ApiResponse<T> ValidationException(ValidationException e) {

    return ApiResponse.result(ApiResult.FIELD_VALIDATE_ERROR, e.getMessage());
}

最后,我们以一个保存用户的接口方法讲解如何应用。 在这个接口方法中需要一个AdminUser作为入参,但是在它的实体类中没有使用约束注解,也就是需要在编码中进行参数校验,那么我们就可以使用定义好的Validator来完成,调用方式如下:

@PostMapping("/adminUser/save")
public <T> ApiResponse<T> save(@RequestBody AdminUser adminUser) {

    Validator.validateNotEmpty(adminUser.getUsername(), "用户名不能为空");
    Validator.validateNotEmpty(adminUser.getPassword(), "密码不能为空");
    // ......
    return ApiResponse.success();
}

我们使用 Postman 调试保存用户的接口,并将username参数的值以空字符传递。当看到响应结果提示用户名不为空就代表成功应用参数校验了。

SpringBoot项目开发规范之参数校验

结语

至此,SpringBoot 项目开发的参数校验规范的实施也就完成了。在项目中我们既应用了 Bean Validation 规范,使用 Hibernate Validator 完成注解式参数校验,也对业务编码应用了自定义校验。如此,我们就足以应对日常开发中需要完成参数校验的情况了,也对这一环节的系统安全性达到保证。

参考

[1] Jakarta Bean Validation