SpringBoot 如何自定義請(qǐng)求參數(shù)校驗(yàn)
最近在工作中遇到寫一些API,這些API的請(qǐng)求參數(shù)非常多,嵌套也非常復(fù)雜,如果參數(shù)的校驗(yàn)代碼全部都手動(dòng)去實(shí)現(xiàn),寫起來真的非常痛苦。
正好Spring輪子里面有一個(gè)Validation,這里記錄一下怎么使用,以及怎么自定義它的返回結(jié)果。
一、Bean Validation基本概念
Bean Validation是Java中的一項(xiàng)標(biāo)準(zhǔn),它通過一些注解表達(dá)了對(duì)實(shí)體的限制規(guī)則。通過提出了一些API和擴(kuò)展性的規(guī)范,這個(gè)規(guī)范是沒有提供具體實(shí)現(xiàn)的,希望能夠Constrain once, validate everywhere?,F(xiàn)在它已經(jīng)發(fā)展到了2.0,兼容Java8。
hibernate validation實(shí)現(xiàn)了Bean Validation標(biāo)準(zhǔn),里面還增加了一些注解,在程序中引入它我們就可以直接使用。
Spring MVC也支持Bean Validation,它對(duì)hibernate validation進(jìn)行了二次封裝,添加了自動(dòng)校驗(yàn),并將校驗(yàn)信息封裝進(jìn)了特定的BindingResult類中,在SpringBoot中我們可以添加implementation(‘org.springframework.boot:spring-boot-starter-validation')引入這個(gè)庫(kù),實(shí)現(xiàn)對(duì)bean的校驗(yàn)功能。
二、基本用法
gradle dependencies如下:
dependencies {
implementation('org.springframework.boot:spring-boot-starter-validation')
implementation('org.springframework.boot:spring-boot-starter-web')
}
定義一個(gè)示例的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對(duì)象的name屬性不能為字符串且長(zhǎng)度不能超過10個(gè)字符。
然后我們暫時(shí)不添加任何多余的代碼,直接寫一個(gè)UserController對(duì)外提供一個(gè)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,簡(jiǎn)單包含了code、message字段返回結(jié)果。
public class ServiceResponse {
private int code;
private String message;
... 省略getter、setter ...
}
啟動(dòng)SpringBoot程序,發(fā)一個(gè)測(cè)試請(qǐng)求看一下:
http://127.0.0.1:8080/validation/get?name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&password=1

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

早期Spring版本中,都是在Controller的方法中添加Errors/BindingResult參數(shù),由Spring注入Errors/BindingResult對(duì)象,再在Controller中手寫校驗(yàn)邏輯實(shí)現(xiàn)校驗(yàn)。
新版本提供注解的方式(Controller上面bean加一個(gè)@Validated注解),將校驗(yàn)邏輯和Controller分離。
三、自定義校驗(yàn)
3.1 自定義注解
顯然除了自帶的NotNull、NotBlank、Size等注解,實(shí)際業(yè)務(wù)上還會(huì)需要特定的校驗(yàn)規(guī)則。
假設(shè)我們有一個(gè)參數(shù)address,必須以Beijing開頭,那我們可以定義一個(gè)注解和一個(gè)自定義的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中增加一個(gè)address屬性,并給它加上上面這個(gè)自定義的注解,這里我們定義了一個(gè)可以傳入start參數(shù)的注解,表示應(yīng)該以什么開頭。
@StartWithValidation(message = "Param 'address' must be start with 'Beijing'.", start = "Beijing") private String address;
除了定義可以作用于屬性的注解外,其實(shí)還可以定義作用于class的注解(@Target({TYPE})),用于校驗(yàn)class的實(shí)例。
3.2 自定義Validator
第一步,實(shí)現(xiàn)一個(gè)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實(shí)例,并給Controller的方法參數(shù)加上@Validated注解,即可完成和前面自定義注解一樣的校驗(yàn)功能。
@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;
}
}
這個(gè)方法和自定義注解的區(qū)別在于不需要在Bean里面添加注解,并且可以更加靈活的把一個(gè)Bean里面所有的Field的校驗(yàn)代碼都搬到一起,而不是每一個(gè)屬性都去加注解,如果校驗(yàn)的屬性非常多,且默認(rèn)注解的能力又不夠的話,這種方式也是不錯(cuò)的,可以避免大量的自定義注解。
3.3 以編程的方式校驗(yàn)(手動(dòng))
這種方式可以算是原始的Hibernate-Validation的方式。直接看代碼,這里有一個(gè)比較不同的是,可以使用Hibernate-Validation的Fail fast mode。因?yàn)榍懊娴姆绞剑紝⑺械膮?shù)都驗(yàn)證完了,再把錯(cuò)誤返回。有時(shí)我們希望遇到一個(gè)參數(shù)錯(cuò)誤,就立即返回。
設(shè)置fast-fail為true可以達(dá)到這個(gè)目的。不過貌似不能再用@Validated注解方法參數(shù)了,而是要用ValidatorFactory創(chuàng)建Validator。
在實(shí)際開發(fā)中,不必每次都編寫代碼創(chuàng)建Validator,可以采用@Configuration的方式創(chuàng)建,然后再@Autowired注入到每個(gè)需要使用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í),拋異常。由于設(shè)置了failFast,這里set最多就一個(gè)元素
ServiceResponse serviceResponse = new ServiceResponse();
serviceResponse.setCode(0);
serviceResponse.setMessage("test");
return serviceResponse;
}
}
3.4 定義分組校驗(yàn)
有的時(shí)候,我們會(huì)有兩個(gè)不同的接口,但是會(huì)使用到同一個(gè)Bean來作為VO(意思是兩個(gè)接口的URI不同,但參數(shù)中都用到了同一個(gè)Bean)。
而在不同的接口上,對(duì)Bean的校驗(yàn)需求可能不一樣,比如接口2需要校驗(yàn)studentId,而接口1不需要。那么此時(shí)就可以用到校驗(yàn)注解的分組groups。
// User.java
public class User {
... 省略其他屬性
// 指明在groups={Student.class}時(shí)才需要校驗(yàn)studentId
@NotNull(groups = {Student.class}, message = "Param 'studentId' must not be null.")
private Long studentId;
// 增加Student interface
public interface Student {
}
}
// UserController.java,增加了一個(gè)/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ū)別,其代碼注釋寫著后者是對(duì)前者的一個(gè)擴(kuò)展,支持了group分組的功能。
3.5 定制返回碼和消息
第二節(jié)中定義了一個(gè)ServiceResponse,其實(shí)作為一個(gè)開放的API,不論用戶傳入任何參數(shù),返回的結(jié)果都應(yīng)該是預(yù)先定義好的格式,并且可以寫明在接口文檔中,即使發(fā)生了校驗(yàn)失敗,應(yīng)該返回一個(gè)包含錯(cuò)誤碼code(發(fā)生錯(cuò)誤時(shí)一般大于0)和message字段。
{
"code": 51000,
"message": "Param 'name' must be less than 10 characters."
}
的結(jié)果,而HTTP STATUS CODE一直都是200。
為了實(shí)現(xiàn)這個(gè)目的,我們加一個(gè)全局異常處理方法。
// 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(非請(qǐng)求body參數(shù),例如@RequestParam接收的)和MethodArgumentNotValidException(請(qǐng)求body里面的參數(shù),例如@RequestBody接收的),這兩類Exception里面都有一個(gè)BindingResult對(duì)象,它里面有一個(gè)包裝成FieldError的List,保存著Bean對(duì)象出現(xiàn)錯(cuò)誤的Field等信息。
取出它里面defaultMessage,放到統(tǒng)一的ServiceResponse返回即可實(shí)現(xiàn)返回碼和消息的定制。由于消息內(nèi)容是有注解默認(rèn)的DefaultMessage決定的,為了按照自定義的描述返回,在Bean對(duì)象的注解上需要手動(dòng)賦值為希望返回的消息內(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ù)長(zhǎng)度超過10時(shí),就會(huì)返回
{
"code": 51000,
"message": "Param 'name' must be less than 10 characters."
}
這里的FieldError fieldError = ex.getFieldError();只會(huì)隨機(jī)返回一個(gè)出錯(cuò)的屬性,如果Bean對(duì)象的多個(gè)屬性都出錯(cuò)了,可以調(diào)用ex.getFieldErrors()來獲得,這里也可以看到Spring Validation在參數(shù)校驗(yàn)時(shí)不會(huì)在第一次碰到參數(shù)錯(cuò)誤時(shí)就返回,而是會(huì)校驗(yàn)完成所有的參數(shù)。
如果不想手動(dòng)編程去校驗(yàn),那么這里可以只讀取一個(gè)隨機(jī)的FieldError,返回它的錯(cuò)誤消息即可。
3.6 更加細(xì)致的返回碼和消息
其實(shí)還有一種比較典型的自定義返回,就是錯(cuò)誤碼(code)和消息(message)是一一對(duì)應(yīng)的,比如:
- 51001:字符串長(zhǎng)度過長(zhǎng)
- 51002:參數(shù)取值過大
- …
這種情況比較特殊,一般當(dāng)參數(shù)錯(cuò)誤的時(shí)候,會(huì)返回一個(gè)整體的參數(shù)錯(cuò)誤的錯(cuò)誤碼,然后攜帶參數(shù)的錯(cuò)誤信息。但有時(shí),業(yè)務(wù)
上就要不同的參數(shù)錯(cuò)誤,既要錯(cuò)誤碼不同,錯(cuò)誤信息也要不同。我想了下,有兩種思路。
- 第一種:通過message同時(shí)包含錯(cuò)誤碼和錯(cuò)誤信息,在全局異常捕獲方法中,再把它們拆開。
- 第二種:手動(dòng)校驗(yàn),拋出自定義的Exception(里面帶有code、message)。手動(dòng)校驗(yàn)這里,如果每一個(gè)Controller都去寫一遍,確實(shí)比較費(fèi)勁,可以結(jié)合AOP來實(shí)現(xiàn),或者抽出一個(gè)基類BaseController的方式。
四、小結(jié)
其實(shí)在實(shí)際的工作中,肯定還有更復(fù)雜的校驗(yàn)邏輯,但是不一定非要都用框架去實(shí)現(xiàn),框架里面的實(shí)現(xiàn)(比如注解)應(yīng)該是一個(gè)比較簡(jiǎn)單通用的校驗(yàn),能夠達(dá)到復(fù)用,減少重復(fù)的勞動(dòng)。
而更加復(fù)雜的邏輯校驗(yàn),一定是存在具體業(yè)務(wù)當(dāng)中的,最好是在業(yè)務(wù)代碼里面實(shí)現(xiàn)。
還有一點(diǎn)需要注意,Spring Validation的isValid方法,如果返回false,那么Controller不再會(huì)被調(diào)用,而是直接返回。如果你在Controller上面加了AOP進(jìn)行接口調(diào)用統(tǒng)計(jì)的話,可能會(huì)漏掉。
這個(gè)時(shí)候,我們不應(yīng)該讓Controller不調(diào)用,建議這種情況在AOP里面對(duì)Controller的參數(shù)切面進(jìn)行校驗(yàn)后,拋出統(tǒng)一的業(yè)務(wù)異常。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Springboot中如何使用過濾器校驗(yàn)PSOT類型請(qǐng)求參數(shù)內(nèi)容
- SpringBoot常見get/post請(qǐng)求參數(shù)處理、參數(shù)注解校驗(yàn)及參數(shù)自定義注解校驗(yàn)詳解
- springboot整合JSR303參數(shù)校驗(yàn)與全局異常處理的方法
- springboot接口參數(shù)校驗(yàn)JSR303的實(shí)現(xiàn)
- SpringBoot使用jsr303校驗(yàn)的實(shí)現(xiàn)
- Springboot集成JSR303參數(shù)校驗(yàn)的方法實(shí)現(xiàn)
- Spring Boot中使用JSR-303實(shí)現(xiàn)請(qǐng)求參數(shù)校驗(yàn)
相關(guān)文章
Spring Boot對(duì)Future模式的支持詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot對(duì)Future模式的支持的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用spring boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧2019-01-01
JAVA NIO按行讀寫大文件出現(xiàn)中文亂碼問題的解決
這篇文章主要為大家詳細(xì)介紹了JAVA在使用NIO進(jìn)行按行讀寫大文件時(shí)出現(xiàn)中文亂碼問題是如何解決的,文中的示例代碼簡(jiǎn)潔易懂,有需要的小伙伴可以參考一下2025-02-02
Java中的遞增i++與++i的實(shí)現(xiàn)原理詳解
這篇文章主要介紹了Java中的i++與++i的實(shí)現(xiàn)原理詳解,在Java中,i++是一種常見的遞增操作符,用于將變量i的值增加1,它是一種簡(jiǎn)潔且方便的方式來實(shí)現(xiàn)循環(huán)和計(jì)數(shù)功能,i++可以用于各種情況,本文來看一下其實(shí)現(xiàn)原理,需要的朋友可以參考下2023-10-10
java.lang.OutOfMemoryError: Java heap space錯(cuò)誤
本文主要介紹了java.lang.OutOfMemoryError: Java heap space錯(cuò)誤的問題解決,包括內(nèi)存泄漏、數(shù)據(jù)過大和JVM堆大小配置不足,提供了解決方法,具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
Java中的UrlDecoder 和 UrlEncoder_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
HTML 格式編碼的實(shí)用工具類。該類包含了將 String 轉(zhuǎn)換為 application/x-www-form-urlencoded MIME 格式的靜態(tài)方法。下文通過實(shí)例代碼給大家介紹Java中的UrlDecoder 和 UrlEncoder知識(shí),感興趣的的朋友一起看看吧2017-07-07

