天天看點

Java Bean Validation完成後端資料校驗前言代碼實作

前言

資料的校驗是互動式網站一個不可或缺的功能,前端的js校驗可以涵蓋大部分的校驗職責,如使用者名唯一性,生日格式,郵箱格式校驗等等常用的

校驗。但是為了避免使用者繞過浏覽器,使用http工具直接向後端請求一些違法資料,服務端的資料校驗也是必要的,可以防止髒資料落到資料庫中,如

果資料庫中出現一個非法的郵箱格式,也會讓運維人員頭疼不已。我在之前保險産品研發過程中,系統對資料校驗要求比較嚴格且追求可變性及效率,

曾使用drools作為規則引擎,兼任了校驗的功能。而在一般的應用,可以使用本文将要介紹的validation來對資料進行校驗。

JSR303/JSR-349

簡述JSR303/JSR-349,Hibernate Validation,Spring Validation之間的關系。JSR303是一項标準,JSR-349是其的更新版本,添加了一些

新特性,他們規定一些校驗規範即校驗注解,如@Null,@NotNull,@Pattern,他們位于javax.validation.constraints包下,隻提供規範不提供

實作。而hibernate validation是對這個規範的實踐(不要将hibernate和資料庫orm架構聯系在一起),他提供了相應的實作,并增加了一些其他校

驗注解,如@Email,@Length,@Range等等,他們位于org.hibernate.validator.constraints包下。而萬能的Spring為了給開發者提供便捷,對

Hibernate Validation進行了二次封裝,顯示校驗validated bean時,你可以使用Spring Validation或者Hibernate Validation,而Spring 

validation另一個特性,便是其在SpringMVC子產品中添加了自動校驗,并将校驗資訊封裝進了特定的類中。這無疑便捷了我們的web開發。本文主要介

紹在SpringMVC中自動校驗的機制。注解如下:

JSR提供的校驗注解:

| 注解        | 注釋   |

| --------   | ------  |

|@Null | 被注釋的元素必須為 null|

|@NotNull | 被注釋的元素必須不為 null|

|@AssertTrue | 被注釋的元素必須為 true|

|@AssertFalse | 被注釋的元素必須為 false|

|@Min(value) | 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值|

|@Max(value) | 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值|

|@DecimalMin(value) | 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值|

|@DecimalMax(value) | 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值|

|@Size(max=, min=) | 被注釋的元素的大小必須在指定的範圍内|

|@Digits (integer, fraction)  | 被注釋的元素必須是一個數字,其值必須在可接受的範圍内|

|@Past | 被注釋的元素必須是一個過去的日期 |

|@Future | 被注釋的元素必須是一個将來的日期|

|@Pattern(regex=,flag=) | 被注釋的元素必須符合指定的正規表達式|

Hibernate Validator提供的校驗注解:

| 注解        | 注釋   |

| --------   | ------  | 

|@NotBlank(message =) | 驗證字元串非null,且長度必須大于0|

|@Email | 被注釋的元素必須是電子郵箱位址|

|@Length(min=,max=) | 被注釋的字元串的大小必須在指定的範圍内|

|@NotEmpty | 被注釋的字元串的必須非空|

|@Range(min=,max=,message=) | 被注釋的元素必須在合适的範圍内|

代碼實作

添加JAR包依賴

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.7.Final</version>
    <!--<classifier>sources</classifier>-->
</dependency>
           

簡單校驗

1.在pojo中指定校驗規則

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.*;
import java.util.Date;

@ApiModel
@Getter
@Setter
public class UserInfo {
    @ApiModelProperty(value = "姓名")
    @NotEmpty(message = "姓名不能為空!")
    @Max(value = 5, message = "姓名長度不能超過5!")
    private String name;
    @Length(max = 10, message = "昵稱長度不能超過10!")
    @ApiModelProperty(value = "昵稱")
    private String nickname;
    @Pattern(regexp = "[男|女]", message = "性别隻能在男或女中選擇!")
    @ApiModelProperty(value = "性别")
    private String sex;
    @Digits(integer = 18, fraction = 28, message = "年齡必須在18-28之間!")
    @ApiModelProperty(value = "年齡")
    private Integer age;
    @Past(message = "生日必須在過去的時間裡!")
    @ApiModelProperty(value = "生日")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;
    @ApiModelProperty(value = "籍貫")
    private String nativePlace;
    @Pattern(regexp = "^0\\d{2,3}-\\d{7,8}$", message = "固定電話格式不正确!")
    @ApiModelProperty(value = "固定電話")
    private String telephone;
    @Pattern(regexp = "^1\\d{10}$", message = "行動電話格式不正确!")
    @ApiModelProperty(value = "行動電話")
    private String phone;
    @Email(message = "郵箱格式不正确!")
    @ApiModelProperty(value = "郵箱")
    private String email;

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                ", nativePlace='" + nativePlace + '\'' +
                ", telephone='" + telephone + '\'' +
                ", phone='" + phone + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}
           

2.controller中對其校驗綁定進行使用

import io.swagger.annotations.Api;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import qgs.csmp.zzz.UserInfo;
import qgs.framework.core.annotation.AuthPassport;
import qgs.framework.core.common.BaseController;

@Api(tags = "validation校驗demo")
@Controller
@RequestMapping("/validation")
public class DemoController extends BaseController {

    @AuthPassport(value = false)
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET)
    public String save(@Validated UserInfo userInfo, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            StringBuilder sb = new StringBuilder();
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                sb.append(fieldError.getDefaultMessage()).append(",\n");
            }
            return sb.toString();
        }
        return "success";
    }
}
           
  • 注:

1、@Validated作用就是将pojo内的注解資料校驗規則(@NotNull等)生效,如果沒有該注解的聲明,pojo内有注解資料校驗規則也不會生效   

2、BindingResult對象用來擷取校驗失敗的資訊(@NotNull中的message),與@Validated注解必須配對使用,一前一後

對BindingResult統一異常攔截

統一異常攔截後,不必每次都對controller接口增加參數BindingResult。代碼實作如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import qgs.framework.util.utilty.StringUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;

@ControllerAdvice
public class ExceptionLogInterceptor {

    @SuppressWarnings("rawtypes")
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public void resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
        Logger logger = LoggerFactory.getLogger(getClass());

        StringBuilder errorMsg = new StringBuilder();
        StringBuilder errorLog = new StringBuilder();
        if (e instanceof BindingResult || e instanceof MethodArgumentNotValidException ||
                e instanceof ConstraintViolationException) {
            BindingResult bindingResult = null;
            if (e instanceof BindingResult) {
                bindingResult = (BindingResult) e;
            }
            //實體類中包含其他實體
            if (e instanceof MethodArgumentNotValidException) {
                MethodArgumentNotValidException validException = (MethodArgumentNotValidException) e;
                bindingResult = validException.getBindingResult();
            }
            if (bindingResult != null && bindingResult.getAllErrors() != null && !bindingResult.getAllErrors().isEmpty()) {
                errorMsg = new StringBuilder(bindingResult.getAllErrors().get(0).getDefaultMessage());
            }
            if (e instanceof ConstraintViolationException) {
                Set<ConstraintViolation<?>> violations = ((ConstraintViolationException) e).getConstraintViolations();
                for (ConstraintViolation<?> violation : violations) {
                    errorMsg.append(violation.getMessage()).append(", ");
                }
            }
        } else {
            errorMsg.append(StringUtil.isNullOrEmpty(e.getMessage()) ? e.toString() : e.getMessage());
            errorLog.append(StringUtil.isNullOrEmpty(e.getMessage()) ? e.toString() : e.getMessage());
            errorLog.append("\r\n");
            for (StackTraceElement traceElement : e.getStackTrace()) {
                errorLog.append(traceElement.toString());
                errorLog.append("\r\n");
            }
        }
        logger.error(errorLog.toString());
        try {
            response.setStatus(500);
            response.setContentType("application/json; charset=utf-8");
            PrintWriter out = response.getWriter();
            out.append("{\"msg\":\"").append(errorMsg.toString()).append("\"}");
            out.close();
        } catch (IOException ignored) {
        }
    }

}
           
  • 注:
這裡隻對一處不符合規則的錯誤資訊輸出

此時,controller代碼可更改為:

public class DemoController extends BaseController {
    @AuthPassport(value = false)
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET)
    public String save(@Validated UserInfo userInfo) {
        return "success";
    }
}
           

或者

public class DemoController extends BaseController {
    @AuthPassport(value = false)
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET)
    public String save(@Valid UserInfo userInfo) {
        return "success";
    }
}
           

@Validated或者@Valid均可

分組校驗

1.什麼是分組校驗?

校驗規則是在pojo制定的,而同一個pojo可以被多個Controller使用,此時會有問題,即:不同的Controller方法對同一個pojo進行校驗,此時

這些校驗資訊是共享在這不同的Controller方法中,但是實際上每個Controller方法可能需要不同的校驗,在這種情況下,就需要使用分組校驗來

解決這種問題,通俗的講,一個pojo中有很多屬性,controller中的方法1可能隻需要校驗pojo中的屬性1,controller中的方法2隻需要校驗pojo中

的屬性2,但是pojo中的校驗注解有很多,怎樣才能使方法1隻校驗屬性1,方法二隻校驗屬性2呢?就需要用分組校驗來解決了。

2.定義分組

1.定義空的接口

public interface ValidationGroup1 {
}
           
public interface ValidationGroup2 {
}
           

2.修改pojo,注解增加參數groups

public class UserInfo {
    @ApiModelProperty(value = "姓名")
    @NotEmpty(message = "姓名不能為空!", groups = ValidationGroup1.class)
    @Max(value = 5, message = "姓名長度不能超過5!")
    private String name;
    @Length(max = 10, message = "昵稱長度不能超過10!", groups = ValidationGroup2.class)
    @ApiModelProperty(value = "昵稱")
    private String nickname;
    @Pattern(regexp = "[男|女]", message = "性别隻能在男或女中選擇!")
    @ApiModelProperty(value = "性别")
    private String sex;
    @Digits(integer = 18, fraction = 28, message = "年齡必須在18-28之間!")
    @ApiModelProperty(value = "年齡")
    private Integer age;
    @Past(message = "生日必須在過去的時間裡!")
    @ApiModelProperty(value = "生日")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;
    @ApiModelProperty(value = "籍貫")
    private String nativePlace;
    @Pattern(regexp = "^0\\d{2,3}-\\d{7,8}$", message = "固定電話格式不正确!")
    @ApiModelProperty(value = "固定電話")
    private String telephone;
    @Pattern(regexp = "^1\\d{10}$", message = "行動電話格式不正确!")
    @ApiModelProperty(value = "行動電話")
    private String phone;
    @Email(message = "郵箱格式不正确!")
    @ApiModelProperty(value = "郵箱")
    private String email;

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                ", nativePlace='" + nativePlace + '\'' +
                ", telephone='" + telephone + '\'' +
                ", phone='" + phone + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}
           

2.修改controller

public class DemoController extends BaseController {
    @AuthPassport(value = false)
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET)
    public String save(@Validated(value = {ValidationGroup1.class}) UserInfo userInfo) {
        return "success";
    }
}
           
  • 注:

此時隻能使用@Validated注解    

如上,隻校驗pojo中groups為ValidationGroup1的屬性,如name有兩處校驗,隻會校驗是否為空而不會校驗長度是否大于5

自定義注解校驗

業務需求總是比架構提供的這些簡單校驗要複雜的多,我們可以自定義校驗來滿足我們的需求。自定義Spring Validation非常簡單,主要分為兩步。

  1.自定義校驗注解

我們嘗試添加一個“字元串不能包含空格”的限制。

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CannotHaveBlankValidator.class})

public @interface CannotHaveBlank {
    
    String message() default "不能包含空格";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};

    //指定多個時使用
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        CannotHaveBlank[] value();
    }
}
           

2 編寫校驗類

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CannotHaveBlankValidator implements ConstraintValidator<CannotHaveBlank, String> {

    @Override
    public void initialize(CannotHaveBlank constraintAnnotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        //null時不進行校驗
        if (value != null && value.contains(" ")) {
            //擷取預設提示資訊
            String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
            System.out.println("default message :" + defaultConstraintMessageTemplate);
            //禁用預設提示資訊
            context.disableDefaultConstraintViolation();
            //設定提示語
            context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
            return false;
        }
        return true;
    }
}
           
  • 注:

所有的驗證者都需要實作ConstraintValidator接口,它的接口包含一個初始化事件方法,和一個判斷是否合法的方法。    

ConstraintValidatorContext 這個上下文包含了認證中所有的資訊,我們可以利用這個上下文實作擷取預設錯誤提示資訊,禁用錯誤提示資訊,

改寫錯誤提示資訊等操作。

## 基于方法校驗(controller層方法中單個參數校驗)

@Api(tags = "validation校驗demo")
@Controller
@RequestMapping("/validation")
@Validated
public class DemoController extends BaseController {

    @AuthPassport(value = false)
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET)
    public String save(@NotNull(message = "不能為空") Integer id) {
        return "success";
    }
}
           

1.為類添加@Validated注解

2.校驗方法的傳回值和入參