springboot項目後端表單驗證(javax.validation.api與hibernate-validator)

1 引言

1.1 場景

最近的這個項目是互聯網項目,網絡用戶需要填寫表單信息保存提交。頁面輸入信息需要進行數據格式校驗,從而避免無效數據被保存或者提交。這些檢查工作包括必填項檢查、數值檢查、長度檢查、身份證號碼、手機號碼檢查等工作。如果將這些字段校驗和業務邏輯混合一起寫,則會干擾原有邏輯,而且不容易維護。下面即將要介紹的是後端api對錶單數據的驗證處理技術。

1.2 名詞或技術介紹

(1) JSR

JSR:Java Specification Requests的縮寫,意思是Java 規範提案。是指向JCP(Java Community Process)提出新增一個標準化技術規範的正式請求。任何人都可以提交JSR,以向Java平臺增添新的API和服務。JSR已成爲Java界的一個重要標準。
Bean Validation 是一個運行時的數據驗證框架,在驗證之後驗證的錯誤信息會被馬上返回。BeanValidation就是這個規範。
提到JSR,相信有小夥伴想去看下到底是個啥。可以看到規範從JSR 303 到 JSR 380,目前最新規範是Bean Validation 2.0。這是地址:
https://jcp.org/en/jsr/summary?id=bean+validation
在這裏插入圖片描述

(2) javax.validation.api

Java 在2009年的 JAVAEE 6 中發佈了 JSR303以及javax下的validation包內容。
這項工作的主要目標是爲java應用程序開發人員提供 基於java對象的 約束(constraints)聲明 和 對約束的驗證工具(validator),以及約束元數據存儲庫和查詢API,以及默認實現。
Java8開始,Java EE改名爲Jakarta EE,注意javax.validation相關的api在jakarta.validation的包下。所以大家看不同的版本的時候,會發現以前的版本包在javax.validation包下。javase的支持還在jcp,Java EE改名JakartaEE,JakartaEE的官網及其支持的項目:
https://jakarta.ee/
在這裏插入圖片描述
Bean Validation 2.0規範及默認實現的地址:
https://beanvalidation.org/2.0/spec/#whatsnew

(3) hibernate-validator

Hibernate-Validator框架 提供了 JSR 303 規範中所有內置 constraint 的實現,除此之外還有一些附加的 constraint。注意:此處的Hibernate 不是 Hibernate ORM沒有任何關係,hibernate-validator是Hibernate 基金會下的項目之一。
hibernate-validator的介紹地址:
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/
源碼地址:
https://github.com/hibernate/hibernate-validator

2 常用註解

下面列出常用約束,每個約束都有參數 message,groups 和 payload。這是 Bean Validation 規範的要求。
請注意,這裏沒有區分哪些是默認註解,哪些是hibernate-validator的註解,因爲我們實際開發中,肯定會同時用兩個包,大家在可以直接看得到使用註解屬於哪個包。

(1)2個標識註解

@Valid(規範、常用)
標記用於驗證級聯的屬性、方法參數或方法返回類型。
在驗證屬性、方法參數或方法返回類型時,將驗證在對象及其屬性上定義的約束。
此行爲是遞歸應用的。

@Validated(spring)
spring 提供的擴展註解,可以方便的用於分組校驗
其中,message 是提示消息,groups 可以根據情況來分組。

(2)22個約束註解

以下每一個註解都可以在相同元素上定義多個。

@AssertFalse
檢查元素是否爲 false,支持數據類型:boolean、Boolean

@AssertTrue
檢查元素是否爲 true,支持數據類型:boolean、Boolean

@DecimalMax(value=, inclusive=)
inclusive:boolean,默認 true,表示是否包含,是否等於
value:當 inclusive=false 時,檢查帶註解的值是否小於指定的最大值。當 inclusive=true 檢查該值是否小於或等於指定的最大值。參數值是根據 bigdecimal 字符串表示的最大值。
支持數據類型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封裝類)

@DecimalMin(value=, inclusive=)
支持數據類型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封裝類)
inclusive:boolean,默認 true,表示是否包含,是否等於
value:
當 inclusive=false 時,檢查帶註解的值是否大於指定的最大值。當 inclusive=true 檢查該值是否大於或等於指定的最大值。參數值是根據 bigdecimal 字符串表示的最小值。

@Digits(integer=, fraction=)
檢查值是否爲最多包含 integer 位整數和 fraction 位小數的數字
支持的數據類型:
BigDecimal, BigInteger, CharSequence, byte, short, int, long 、原生類型的封裝類、任何 Number 子類。

@Email
檢查指定的字符序列是否爲有效的電子郵件地址。可選參數 regexp 和 flags 允許指定電子郵件必須匹配的附加正則表達式(包括正則表達式標誌)。
支持的數據類型:CharSequence

@Max(value=)
檢查值是否小於或等於指定的最大值
支持的數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字符序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@Min(value=)
檢查值是否大於或等於指定的最大值
支持的數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字符序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@NotBlank
檢查字符序列是否爲空,以及去空格後的長度是否大於 0。與 @NotEmpty 的不同之處在於,此約束只能應用於字符序列,並且忽略尾隨空格。
支持數據類型:CharSequence

@NotNull
檢查值是否不爲 null
支持數據類型:任何類型

@NotEmpty
檢查元素是否爲 null 或 空
支持數據類型:CharSequence, Collection, Map, arrays

@Size(min=, max=)
檢查元素個數是否在 min(含)和 max(含)之間
支持數據類型:CharSequence,Collection,Map, arrays

@Negative
檢查元素是否嚴格爲負數。零值被認爲無效。
支持數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字符序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@NegativeOrZero
檢查元素是否爲負或零。
支持數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字符序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@Positive
檢查元素是否嚴格爲正。零值被視爲無效。
支持數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字符序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@PositiveOrZero
檢查元素是否爲正或零。
支持數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字符序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@Null
檢查值是否爲 null
支持數據類型:任何類型

@Future
檢查日期是否在未來
支持的數據類型:
java.util.Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate
如果 Joda Time API 在類路徑中,ReadablePartial 和ReadableInstant 的任何實現類

@FutureOrPresent
檢查日期是現在或將來
支持數據類型:同@Future

@Past
檢查日期是否在過去
支持數據類型:同@Future

@PastOrPresent
檢查日期是否在過去或現在
支持數據類型:同@Future

@Pattern(regex=, flags=)
根據給定的 flag 匹配,檢查字符串是否與正則表達式 regex 匹配
支持數據類型:CharSequence

3 代碼實戰

接下來在springboot項目中,結合springAOP和AspectJ,實現通過註解的方式進行數據格式驗證。

3.1 Jar包引入

需要引入jakata.validation-api,以及hibernate-validator的jar包,但是springboot項目中,spring-boot-starter-web 包中已經引入了 hibernate-validator 6.0.17.RELEASE了。
在這裏插入圖片描述

3.2 對需要驗證的字段進行約束

級聯驗證用valid,複雜驗證用正則表達式
給一個正則表達式的地址:https://www.w3cschool.cn/zhengzebiaodashi/regexp-metachar.html

package com.galen.demo.validator.domain;

import org.hibernate.validator.constraints.Length;
//javax.validation.constraints包下所支持的一些約束類型
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import java.util.Date;

/**
 * 員工信息
 * @author galen
 * @date 2020/2/26
 * */
public class Employee {
    @NotNull(message = "姓名必填!")
    @Length(max = 20, message = "姓名過長!")
    private String name;

    @NotNull(message = "工牌必填!")
    @Pattern(regexp = "^\\d{10}",message = "請輸入10位數字工牌!")//長度10,0-9
    private String badgeCode;

    @Pattern(regexp = "^[1-2]",message = "性別參數錯誤!")
    @NotNull(message = "性別必填!")
    private String gender;

    @Past(message = "無效的出生日期!")
    private Date birthDate;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Email
    private String email;

    @NotNull(message = "身份證號碼必填!")
    @Pattern(regexp = "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"
            ,message = "身份證號碼格式錯誤")
    private String idCardNumber;

    @Valid
    private  Salary salary;

    public Salary getSalary() {
        return salary;
    }

    public void setSalary(Salary salary) {
        this.salary = salary;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @NotNull(message = "聯繫電話必填!")
    @Pattern(regexp = "^[1]+\\d{10}$"
            ,message = "電話號碼格式錯誤")
    private String phoneNumber;



    public String getIdCardNumber() {
        return idCardNumber;
    }

    public void setIdCardNumber(String idCardNumber) {
        this.idCardNumber = idCardNumber;
    }



    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBadgeCode() {
        return badgeCode;
    }

    public void setBadgeCode(String badgeCode) {
        this.badgeCode = badgeCode;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Date getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(Date birthDate) {
        this.birthDate = birthDate;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", badgeCode='" + badgeCode + '\'' +
                ", gender=" + gender +
                ", birthDate=" + birthDate +
                '}';
    }
}

3.3 添加自定義註解

package com.galen.demo.validator.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author galen
 * @date 2020/2/26
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanValidation {
    String handler() default "default";
}

3.4 定義自定義註解的處理類

/**
     * 定義環繞通知,在指定註解處(切點)攔截,獲取切點對象參數,以及獲取BeanValidation的自定義參數
     * */
    @Around("@annotation(beanValidation)")
    public final Object validateParamByAnnotation(ProceedingJoinPoint ponit, BeanValidation beanValidation) throws Throwable {
        log.info("==========================  start bean validation in :" + ponit.getSignature().getDeclaringTypeName() + "==========================");
        String errorMsg = packErrorMsg(ponit.getArgs());
        if (errorMsg != null && errorMsg.length() > 0) {
            String handlerName = beanValidation.handler();
            if ("default".equals(handlerName)) {
                return R.error(errorMsg);
            }
            return customHandlerInvoke(handlerName, errorMsg);
        }
        return ponit.proceed();
    }

3.5 在需要驗證的方法上加註解

package com.galen.demo.validator.controller;

import com.galen.demo.validator.annotation.BeanValidation;
import com.galen.demo.validator.domain.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.*;
import java.util.List;
import java.util.Set;

/**
 * 用戶信息接口
 * @author galen
 * @date 2020/2/26
 * */
@RequestMapping(path = "/employee")
@RestController
public class EmployController {

    private static String lineSeparator = System.lineSeparator();

    /**
     * 註解實現
     * 使用@Valid 註解 實體, 並傳入參數bindResult以獲取校驗結果信息
     * @param employee
     * @param bindingResult
     * @return
     */
    @PostMapping("/bindingResult")
    public Object addEmployee(@RequestBody @Valid Employee employee, BindingResult bindingResult){
        if (bindingResult.hasErrors()){
            //校驗結果以集合的形式返回,當然也可以獲取單個。具體可以查看bindResult的API文檔
            List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
            //StringBuilder組裝異常信息
            StringBuilder builder = new StringBuilder();
            //遍歷拼裝
            fieldErrorList.forEach(error -> {
                builder.append(error.getDefaultMessage() + lineSeparator);
            });
            builder.insert(0,"use @Valid n BingdingResult :" +lineSeparator);
            return builder.toString();
        }

        //TODO there can invoke service layer method to do someting
        return "添加職員信息成功:" + employee.toString();
    }

    //Spring boot 已幫我們把 validation 的關鍵對象的實例裝載如 IOC 容器中
    @Autowired
    private ValidatorFactory autowiredValidatorFactory;

    @Autowired
    private Validator autowiredValidator;
    /**
     * 調用validator實現
     * @param employee
     * @return
     */
    @PostMapping("/validator")
    public Object addEmployee(@RequestBody Employee employee){
        System.out.println("這裏將導入 由 Springboot 的 IOC 容器中獲取的 校驗器工廠和 校驗器類");
        System.out.println("validator工廠類:"+ autowiredValidatorFactory.toString());
        System.out.println("validator類:"+ autowiredValidator.toString());

        /**
         * 下述的工廠類和校驗器類也可以使用上述由IOC容器中獲取的對象實例代替
         */

        //實例化一個 validator工廠
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        //獲取validator實例
        Validator validator = validatorFactory.getValidator();
        //調用調用,得到校驗結果信息 Set
        Set<ConstraintViolation<Employee>> constraintViolationSet = validator.validate(employee);
        //StringBuilder組裝異常信息
        StringBuilder builder = new StringBuilder();
        //遍歷拼裝
        constraintViolationSet.forEach(violationInfo -> {
            builder.append(violationInfo.getMessage() + lineSeparator);
        });
        if (builder.toString().length() > 0){
            builder.insert(0,"use validator :" +lineSeparator);
            return builder.toString();
        }
        return "添加職員信息成功:" + employee.toString();
    }

    /**
     * 調用BeanValidation 切面實現
     * @param employee
     * @return
     */
    @PostMapping("/validateByAspect")
    @BeanValidation
    public Object addEmployeeByAspect(@RequestBody Employee employee1){
        return "添加職員信息成功" ;
    }

}

3.6 效果演示

在這裏插入圖片描述

最後附上源碼地址和參考地址

本文實戰源碼地址:https://gitee.com/muziye/validator

相關參考地址:
正則表達式:https://www.w3cschool.cn/zhengzebiaodashi/regexp-metachar.html
身份證正則:https://blog.csdn.net/loa_loa/article/details/81737086
表單驗證通用切面:https://blog.csdn.net/weixin_43740223/article/details/100889250
驗證拓展:自定義驗證+分組驗證:
https://www.cnblogs.com/beiyan/p/5946345.html
https://blog.csdn.net/Mr_rain/article/details/78247857
對hibernate註解進一步說明
https://blog.csdn.net/zalan01408980/article/details/79059434
https://www.cnblogs.com/lw5946/p/11574987.html
https://beanvalidation.org/
https://github.com/hibernate/hibernate-validator

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章