亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

SpringBoot 如何自定義請求參數(shù)校驗

 更新時間:2021年10月19日 14:54:43   作者:自由圣騎士  
這篇文章主要介紹了SpringBoot 如何自定義請求參數(shù)校驗方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教

最近在工作中遇到寫一些API,這些API的請求參數(shù)非常多,嵌套也非常復(fù)雜,如果參數(shù)的校驗代碼全部都手動去實現(xiàn),寫起來真的非常痛苦。

正好Spring輪子里面有一個Validation,這里記錄一下怎么使用,以及怎么自定義它的返回結(jié)果。

一、Bean Validation基本概念

Bean Validation是Java中的一項標(biāo)準(zhǔn),它通過一些注解表達(dá)了對實體的限制規(guī)則。通過提出了一些API和擴(kuò)展性的規(guī)范,這個規(guī)范是沒有提供具體實現(xiàn)的,希望能夠Constrain once, validate everywhere。現(xiàn)在它已經(jīng)發(fā)展到了2.0,兼容Java8。

hibernate validation實現(xiàn)了Bean Validation標(biāo)準(zhǔn),里面還增加了一些注解,在程序中引入它我們就可以直接使用。

Spring MVC也支持Bean Validation,它對hibernate validation進(jìn)行了二次封裝,添加了自動校驗,并將校驗信息封裝進(jìn)了特定的BindingResult類中,在SpringBoot中我們可以添加implementation(‘org.springframework.boot:spring-boot-starter-validation')引入這個庫,實現(xiàn)對bean的校驗功能。

二、基本用法

gradle dependencies如下:

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-validation')
    implementation('org.springframework.boot:spring-boot-starter-web')
}

定義一個示例的Bean,例如下面的User.java。

public class User {
    @NotBlank
    @Size(max=10)
    private String name;
    private String password;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

在name屬性上,添加@NotBlank和@Size(max=10)的注解,表示User對象的name屬性不能為字符串且長度不能超過10個字符。

然后我們暫時不添加任何多余的代碼,直接寫一個UserController對外提供一個RESTful的GET接口,注意接口的參數(shù)用到了@Validated注解。

// UserController.java,省略其他代碼
@RestController
public class UserController {
    @RequestMapping(value = "/validation/get", method = RequestMethod.GET)
    public ServiceResponse validateGet(@Validated User user) {
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(0);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
}
// ServiceResponse.java,簡單包含了code、message字段返回結(jié)果。
public class ServiceResponse {
    private int code;
    private String message;
    ... 省略getter、setter ...
}

啟動SpringBoot程序,發(fā)一個測試請求看一下:

http://127.0.0.1:8080/validation/get?name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&password=1

在這里插入圖片描述

此時已經(jīng)可以實現(xiàn)參數(shù)的校驗了,但是返回的結(jié)果不太友好,下面看一下怎么定制返回的消息。在定制返回結(jié)果前,先看下一下內(nèi)置的校驗注解有哪些,在這里我不一個個去貼了,寫代碼的時候根據(jù)需要進(jìn)入到源碼里面去看即可。

在這里插入圖片描述

早期Spring版本中,都是在Controller的方法中添加Errors/BindingResult參數(shù),由Spring注入Errors/BindingResult對象,再在Controller中手寫校驗邏輯實現(xiàn)校驗。

新版本提供注解的方式(Controller上面bean加一個@Validated注解),將校驗邏輯和Controller分離。

三、自定義校驗

3.1 自定義注解

顯然除了自帶的NotNull、NotBlank、Size等注解,實際業(yè)務(wù)上還會需要特定的校驗規(guī)則。

假設(shè)我們有一個參數(shù)address,必須以Beijing開頭,那我們可以定義一個注解和一個自定義的Validator。

// StartWithValidator.java
public class StartWithValidator implements ConstraintValidator<StartWithValidation, String> {
    private String start;
    @Override
    public void initialize(StartWithValidation constraintAnnotation) {
        start = constraintAnnotation.start();
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (!StringUtils.isEmpty(value)) {
            return value.startsWith(start);
        }
        return true;
    }
}
// StartWithValidation.java
@Documented
@Constraint(validatedBy = StartWithValidator.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface StartWithValidation {
    String message() default "不是正確的性別取值范圍";
    String start() default "_";
    Class[] groups() default {};
    Class[] payload() default {};
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        GenderValidation[] value();
    }
}

然后在User.java中增加一個address屬性,并給它加上上面這個自定義的注解,這里我們定義了一個可以傳入start參數(shù)的注解,表示應(yīng)該以什么開頭。

@StartWithValidation(message = "Param 'address' must be start with 'Beijing'.", start = "Beijing")
private String address;

除了定義可以作用于屬性的注解外,其實還可以定義作用于class的注解(@Target({TYPE})),用于校驗class的實例。

3.2 自定義Validator

第一步,實現(xiàn)一個Validator。(這種方法不需要我們的bean里面有任何注解之類的東西)

package com.example.validation.demo;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
@Component
public class UserValidator implements Validator {
    @Override
    public boolean supports(Class clazz) {
        return User2.class.equals(clazz);
    }
    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
        User2 p = (User2) target;
        if (p.getId() == 0) {
            errors.rejectValue("id", "can not be zero");
        }
    }
}

第二步,修改Controller代碼,注入上面的UserValidator實例,并給Controller的方法參數(shù)加上@Validated注解,即可完成和前面自定義注解一樣的校驗功能。

@RestController
public class UserController {
    @Autowired
    UserValidator validator;
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setValidator(validator);
    }
    @RequestMapping(value = "/user/post", method = RequestMethod.POST)
    public ServiceResponse handValidatePost(@Validated @RequestBody User user) {
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(0);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
}

這個方法和自定義注解的區(qū)別在于不需要在Bean里面添加注解,并且可以更加靈活的把一個Bean里面所有的Field的校驗代碼都搬到一起,而不是每一個屬性都去加注解,如果校驗的屬性非常多,且默認(rèn)注解的能力又不夠的話,這種方式也是不錯的,可以避免大量的自定義注解。

3.3 以編程的方式校驗(手動)

這種方式可以算是原始的Hibernate-Validation的方式。直接看代碼,這里有一個比較不同的是,可以使用Hibernate-Validation的Fail fast mode。因為前面的方式,都將所有的參數(shù)都驗證完了,再把錯誤返回。有時我們希望遇到一個參數(shù)錯誤,就立即返回。

設(shè)置fast-fail為true可以達(dá)到這個目的。不過貌似不能再用@Validated注解方法參數(shù)了,而是要用ValidatorFactory創(chuàng)建Validator。

在實際開發(fā)中,不必每次都編寫代碼創(chuàng)建Validator,可以采用@Configuration的方式創(chuàng)建,然后再@Autowired注入到每個需要使用Validator的Controller當(dāng)中。

@RestController
public class UserController {
    ...
    @RequestMapping(value = "/validation/postStudent", method = RequestMethod.POST)
    public ServiceResponse validatePostStudent(@RequestBody User user) {
        // User參數(shù)前面沒有@Validated注解了,User類里面那些注解還是保留著即可。
        HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class).configure();
        ValidatorFactory factory = configuration.failFast(true).buildValidatorFactory(); // fastFail
        Validator validator = factory.getValidator();
        Set<constraintviolation> set = validator.validate(user);
        // 根據(jù)set的size,大于0時,拋異常。由于設(shè)置了failFast,這里set最多就一個元素
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(0);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
}

3.4 定義分組校驗

有的時候,我們會有兩個不同的接口,但是會使用到同一個Bean來作為VO(意思是兩個接口的URI不同,但參數(shù)中都用到了同一個Bean)。

而在不同的接口上,對Bean的校驗需求可能不一樣,比如接口2需要校驗studentId,而接口1不需要。那么此時就可以用到校驗注解的分組groups。

// User.java
public class User {
    ... 省略其他屬性
    // 指明在groups={Student.class}時才需要校驗studentId
    @NotNull(groups = {Student.class}, message = "Param 'studentId' must not be null.")
    private Long studentId;
    // 增加Student interface
    public interface Student {
    }
}
// UserController.java,增加了一個/getStudent接口
@RestController
public class UserController {
    @RequestMapping(value = "/validation/get", method = RequestMethod.GET)
    public ServiceResponse validateGet(@Validated User user) {
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(200);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
    @RequestMapping(value = "/validation/getStudent", method = RequestMethod.GET)
    public ServiceResponse validateGetStudent(@Validated({User.Student.class}) User user) {
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(0);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
}

到這里,也可以帶一嘴Valid和Validated注解的區(qū)別,其代碼注釋寫著后者是對前者的一個擴(kuò)展,支持了group分組的功能。

3.5 定制返回碼和消息

第二節(jié)中定義了一個ServiceResponse,其實作為一個開放的API,不論用戶傳入任何參數(shù),返回的結(jié)果都應(yīng)該是預(yù)先定義好的格式,并且可以寫明在接口文檔中,即使發(fā)生了校驗失敗,應(yīng)該返回一個包含錯誤碼code(發(fā)生錯誤時一般大于0)和message字段。

{
    "code": 51000,
    "message": "Param 'name' must be less than 10 characters."
}

的結(jié)果,而HTTP STATUS CODE一直都是200。

為了實現(xiàn)這個目的,我們加一個全局異常處理方法。

// ServiceExceptionHandler.java
package com.example.validation.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
@RestControllerAdvice
public class ServiceExceptionHandler {
    static final Logger LOG = LoggerFactory.getLogger(ServiceExceptionHandler.class);
    @ExceptionHandler(value = {Exception.class})
    public ServiceResponse handleBindException(Exception ex) {
        LOG.error("{}", ex);
        StringBuilder message = new StringBuilder();
        if (ex instanceof BindException) {
            List fieldErrorList = ((BindException) ex).getFieldErrors();
            if (!CollectionUtils.isEmpty(fieldErrorList)) {
                for (FieldError fieldError : fieldErrorList) {
                    if (fieldError != null && fieldError.getDefaultMessage() != null) {
                        message.append(fieldError.getDefaultMessage()).append(" ");
                    }
                }
            }
        } else if (ex instanceof MethodArgumentNotValidException) {
            List fieldErrorList = ((MethodArgumentNotValidException) ex).getBindingResult().getFieldErrors();
            if (!CollectionUtils.isEmpty(fieldErrorList)) {
                for (FieldError fieldError : fieldErrorList) {
                    if (fieldError != null && fieldError.getDefaultMessage() != null) {
                        message.append(fieldError.getDefaultMessage()).append(" ");
                    }
                }
            }
        }
        // 生成返回結(jié)果
        ServiceResponse errorResult = new ServiceResponse();
        errorResult.setCode(51000); // ErrorCode.PARAM_ERROR = 51000
        errorResult.setMessage(message.toString());
        return errorResult;
    }
}
// User.java,注解傳入指定Message
public class User {
    @NotBlank(message = "Param 'name' can't be blank.")
    @Size(max=10, message = "Param 'name' must be less than 10 characters.")
    private String name;
    ...
}

在上面的方法中,我們處理了BindException(非請求body參數(shù),例如@RequestParam接收的)和MethodArgumentNotValidException(請求body里面的參數(shù),例如@RequestBody接收的),這兩類Exception里面都有一個BindingResult對象,它里面有一個包裝成FieldError的List,保存著Bean對象出現(xiàn)錯誤的Field等信息。

取出它里面defaultMessage,放到統(tǒng)一的ServiceResponse返回即可實現(xiàn)返回碼和消息的定制。由于消息內(nèi)容是有注解默認(rèn)的DefaultMessage決定的,為了按照自定義的描述返回,在Bean對象的注解上需要手動賦值為希望返回的消息內(nèi)容。

@NotBlank(message = "Param 'name' can't be blank.")
@Size(max=10,message = "Param 'name' must be less than 10 characters.")
private String name;

這樣當(dāng)name參數(shù)長度超過10時,就會返回

{
    "code": 51000,
    "message": "Param 'name' must be less than 10 characters."
}

這里的FieldError fieldError = ex.getFieldError();只會隨機(jī)返回一個出錯的屬性,如果Bean對象的多個屬性都出錯了,可以調(diào)用ex.getFieldErrors()來獲得,這里也可以看到Spring Validation在參數(shù)校驗時不會在第一次碰到參數(shù)錯誤時就返回,而是會校驗完成所有的參數(shù)。

如果不想手動編程去校驗,那么這里可以只讀取一個隨機(jī)的FieldError,返回它的錯誤消息即可。

3.6 更加細(xì)致的返回碼和消息

其實還有一種比較典型的自定義返回,就是錯誤碼(code)和消息(message)是一一對應(yīng)的,比如:

  • 51001:字符串長度過長
  • 51002:參數(shù)取值過大

這種情況比較特殊,一般當(dāng)參數(shù)錯誤的時候,會返回一個整體的參數(shù)錯誤的錯誤碼,然后攜帶參數(shù)的錯誤信息。但有時,業(yè)務(wù)

上就要不同的參數(shù)錯誤,既要錯誤碼不同,錯誤信息也要不同。我想了下,有兩種思路。

  • 第一種:通過message同時包含錯誤碼和錯誤信息,在全局異常捕獲方法中,再把它們拆開。
  • 第二種:手動校驗,拋出自定義的Exception(里面帶有code、message)。手動校驗這里,如果每一個Controller都去寫一遍,確實比較費勁,可以結(jié)合AOP來實現(xiàn),或者抽出一個基類BaseController的方式。

四、小結(jié)

其實在實際的工作中,肯定還有更復(fù)雜的校驗邏輯,但是不一定非要都用框架去實現(xiàn),框架里面的實現(xiàn)(比如注解)應(yīng)該是一個比較簡單通用的校驗,能夠達(dá)到復(fù)用,減少重復(fù)的勞動。

而更加復(fù)雜的邏輯校驗,一定是存在具體業(yè)務(wù)當(dāng)中的,最好是在業(yè)務(wù)代碼里面實現(xiàn)。

還有一點需要注意,Spring Validation的isValid方法,如果返回false,那么Controller不再會被調(diào)用,而是直接返回。如果你在Controller上面加了AOP進(jìn)行接口調(diào)用統(tǒng)計的話,可能會漏掉。

這個時候,我們不應(yīng)該讓Controller不調(diào)用,建議這種情況在AOP里面對Controller的參數(shù)切面進(jìn)行校驗后,拋出統(tǒng)一的業(yè)務(wù)異常。

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java數(shù)據(jù)庫連接池c3p0過程解析

    Java數(shù)據(jù)庫連接池c3p0過程解析

    這篇文章主要介紹了Java數(shù)據(jù)庫連接池c3p0過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-07-07
  • Java線程池的應(yīng)用實例分析

    Java線程池的應(yīng)用實例分析

    這篇文章主要介紹了Java線程池的應(yīng)用,結(jié)合具體實例形式分析了java線程池的斐波那契數(shù)列計算與模擬工人做工等應(yīng)用的操作技巧,需要的朋友可以參考下
    2019-10-10
  • 實例分析java對象中淺克隆和深克隆

    實例分析java對象中淺克隆和深克隆

    在本篇文章中我們給大家分享了關(guān)于java對象中淺克隆和深克隆的相關(guān)知識點和相關(guān)代碼內(nèi)容,有興趣的朋友們學(xué)習(xí)下。
    2018-10-10
  • 淺談基于SpringBoot實現(xiàn)一個簡單的權(quán)限控制注解

    淺談基于SpringBoot實現(xiàn)一個簡單的權(quán)限控制注解

    這篇文章主要介紹了基于SpringBoot實現(xiàn)一個簡單的權(quán)限控制注解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-01-01
  • Java?嵌入數(shù)據(jù)引擎從?SQLite?到?SPL詳解

    Java?嵌入數(shù)據(jù)引擎從?SQLite?到?SPL詳解

    這篇文章主要介紹了Java?嵌入數(shù)據(jù)引擎:從?SQLite?到?SPL,SQLite架構(gòu)簡單,其核心雖然是C語言開發(fā)的,但封裝得比較好,對外呈現(xiàn)為一個小巧的Jar包,能方便地集成在Java應(yīng)用中,本文給大家介紹的非常詳細(xì),需要的朋友參考下
    2022-07-07
  • Java中實現(xiàn)WebSocket方法詳解

    Java中實現(xiàn)WebSocket方法詳解

    這篇文章主要介紹了Java中實現(xiàn)WebSocket方法詳解,WebSocket?是一種新型的網(wǎng)絡(luò)協(xié)議,它允許客戶端和服務(wù)器之間進(jìn)行雙向通信,可以實現(xiàn)實時數(shù)據(jù)交互,需要的朋友可以參考下
    2023-07-07
  • Java Netty實現(xiàn)心跳機(jī)制過程解析

    Java Netty實現(xiàn)心跳機(jī)制過程解析

    這篇文章主要介紹了Java Netty實現(xiàn)心跳機(jī)制過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-03-03
  • 數(shù)據(jù)庫阿里連接池 druid配置詳解

    數(shù)據(jù)庫阿里連接池 druid配置詳解

    本篇文章主要介紹了數(shù)據(jù)庫阿里連接池 druid配置詳解,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-05-05
  • Java的線程池ThreadPoolExecutor及多種線程池實現(xiàn)詳解

    Java的線程池ThreadPoolExecutor及多種線程池實現(xiàn)詳解

    這篇文章主要介紹了Java的線程池ThreadPoolExecutor及多種線程池實現(xiàn)詳解,ThreadPoolExecutor 使用 int 的高 3 位來表示線程池狀態(tài),低 29 位表示線程數(shù)量,之所以將信息存儲在一個變量中,是為了保證原子性,需要的朋友可以參考下
    2024-01-01
  • 詳解SpringBoot如何創(chuàng)建自定義Starter

    詳解SpringBoot如何創(chuàng)建自定義Starter

    Spring Boot的自動配置機(jī)制為開發(fā)人員提供了一種輕松集成和配置各種功能的便捷方式,本文將深入探討在Spring Boot中如何創(chuàng)建自定義Starter,為構(gòu)建模塊化且易維護(hù)的應(yīng)用提供有力的支持,需要的朋友可以參考下
    2024-02-02

最新評論