是服務(wù)就需要對(duì)外提供接口枉证,否則該服務(wù)就沒有任何意義。接口需要指定具體的入?yún)⑶闆r概漱,以保證服務(wù)能夠正常地運(yùn)行啥纸。spring mvc通過controller中的method對(duì)外提供接口服務(wù),本文就如何在spring mvc中對(duì)RequestParam
誓斥、PathVariable
和RequestBody
三種類型的參數(shù)做參數(shù)校驗(yàn)做簡(jiǎn)單介紹综看。
為了更好地介紹上訴三種類型參數(shù)校驗(yàn)的方式,本文將通過一個(gè)簡(jiǎn)單的接口需求來完成相應(yīng)的接口校驗(yàn)岖食,相關(guān)的代碼在spring-demo 項(xiàng)目上红碑。
1 需求
現(xiàn)在有一個(gè)學(xué)生管理系統(tǒng)(假設(shè)所有學(xué)生的姓名都是唯一的),我們需要對(duì)學(xué)生信息進(jìn)行管理,即實(shí)現(xiàn)最常見的CURD
操作析珊。學(xué)生類(Student
)如下所示:
@JsonIgnoreProperties(ignoreUnknown = true)
public class Student {
private String studentName;
private int age;
private int gender;
private LocalDate birthDay;
// ... getter setter toString
}
現(xiàn)在需要提供CURD
四個(gè)接口羡鸥,接口的具體要求如下:
- 添加:采用
POST
的方式請(qǐng)求,姓名不能為空忠寻,年齡在1~200之間惧浴,性別用0和1表示,出生日期不能為空且只能是過去的時(shí)間奕剃。 - 刪除:根據(jù)姓名刪除學(xué)生衷旅,姓名不能為空
- 修改:修改指定學(xué)生的出生日期
- 查詢:查詢所有出生日期在指定范圍內(nèi)的學(xué)生
默認(rèn)情況下請(qǐng)求參數(shù)無法直接映射成
LocalDate
類型的,需要在spring中配置jackson
的objectmapper
添加JavaTimeModule
纵朋。
2 依賴項(xiàng)
2.1 JSR 380
JSR
制定了許多Java開發(fā)的規(guī)范柿顶,其中JSR 380就制定Bean Validation的相關(guān)規(guī)范,可以在pom.xml
中加入依賴引入相關(guān)API操软。
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
JSR 380
只是規(guī)范嘁锯,并沒有具體實(shí)現(xiàn)檢驗(yàn)的方法,如果直接使用validation-api
進(jìn)行校驗(yàn)聂薪,會(huì)拋出javax.validation.NoProviderFoundException
家乘,提示需要提供實(shí)現(xiàn)JSR 380的校驗(yàn)器。
2.2 Hibernate Validator
Hibernate Validator是JSR 380
規(guī)范的具體實(shí)現(xiàn)藏澳,并且除了JSR 380
中的校驗(yàn)器仁锯,它還提供了更多的自定義的校驗(yàn)器。
在pom.xml
中加入如下依賴引入Hibernate Validator
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.13.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b09</version>
</dependency>
實(shí)際上只需要添加2.2的依賴即可翔悠,
2.1
的依賴可以不用添加扑馁,因?yàn)?code>2.2中已經(jīng)包含了validation-api
中的內(nèi)容。
3 應(yīng)用Hibernate Validator
Hibernate Validator實(shí)現(xiàn)了
JSR 380
的規(guī)范凉驻,提供了諸如@NotNull
等的校驗(yàn)器,本文這里不具體介紹Hibernate Validator都提供了哪些校驗(yàn)器复罐,感興趣的話可以去Hibernate Validator官網(wǎng)查看相關(guān)的文檔涝登。
3.1 添加學(xué)生信息
-
根據(jù)
1
中所述的需求,現(xiàn)在對(duì)StudentModel
的字段添加校驗(yàn)規(guī)則效诅,如下:public class StudentModel { @NotBlank(message = "studentName不能為空") private String studentName; @Min(value = 1, message = "參數(shù)age不能小于1") @Max(value = 200, message = "參數(shù)age不能大于200") @Range(min = 1, max = 200, message = "age只能在1到200之間") private int age; @Range(min = 0, max = 1, message = "gender只能取0或者1") private int gender; @Past(message = "birthDay只能是過去的時(shí)間") private LocalDate birthDay; }
-
在方法的參數(shù)中對(duì)StudentModel通過
@Validation
進(jìn)行校驗(yàn)@PostMapping(value = "/add", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Map<String, Object> add(@Valid @RequestBody StudentModel studentModel) { return OutPut.success(HttpStatusWrapper.OK,"成功", studentModel); }
@Valid
是Hibernate Validator中用來校驗(yàn)對(duì)象合法性的注解胀滚。
請(qǐng)求運(yùn)行并且請(qǐng)求add
接口的時(shí)候,當(dāng)post body中的數(shù)據(jù)不符合設(shè)置的校 驗(yàn)規(guī)則是乱投,系統(tǒng)并沒有返回對(duì)應(yīng)的錯(cuò)誤信息咽笼,而是輸出下面的信息:Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.util.Map<java.lang.String, java.lang.Object> com.lianglei.spring.demo.controller.StudentController.add(com.lianglei.spring.demo.model.StudentModel): [Field error in object 'studentModel' on field 'gender': rejected value [2]; codes [Range.studentModel.gender,Range.gender,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [studentModel.gender,gender]; arguments []; default message [gender],1,0]; default message [gender只能取0或者1]] ]
從上面的異常信息中可以看出,
gender
要求只能是0
或者1
戚炫,但是輸入的2
剑刑,被Hibernate Validator給rejected
了。從中我們可以發(fā)現(xiàn),校驗(yàn)不通過的時(shí)候施掏,會(huì)拋出org.springframework.web.bind.MethodArgumentNotValidException
異常钮惠。因此我們可以通過統(tǒng)一異常捕獲的方式處理校驗(yàn)不通過的情況,給出友好的接口返回七芭。 -
捕獲
MethodArgumentNotValidException
@RestControllerAdvice public class GlobalExceptionHandler { private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 接口參數(shù)校驗(yàn)異常 * @param e * @return */ @ExceptionHandler(value = {MethodArgumentNotValidException.class}) public Map<String, Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { LOGGER.error(e.getMessage(), e); final String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining()); return OutPut.failure(HttpStatusWrapper.ILLEGAL_REQUEST_PARAMETERS, message); } }
添加異常捕獲后的輸出如下:
{ "status": "Illegal request parameters", "code": 460, "msg": "gender只能取0或者1" }
如果想在
GlobalExceptionHandler
中處理MethodArgumentNotValidException.class
異常的話素挽,需要注意GlobalExceptionHandler
不能繼承ResponseEntityExceptionHandler
否則會(huì)發(fā)生沖突。org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'handlerExceptionResolver' defined in org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerExceptionResolver]: Factory method 'handlerExceptionResolver' threw exception; nested exception is java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [class org.springframework.web.bind.MethodArgumentNotValidException]: {public java.util.Map com.lianglei.spring.demo.exception.GlobalExceptionHandler.handleMethodArgumentNotValidException(org.springframework.web.bind.MethodArgumentNotValidException), public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}?
-
request body中參數(shù)名與
StudentModel
不一致的情況
在實(shí)際開發(fā)過程中狸驳,經(jīng)常會(huì)出現(xiàn)入?yún)⒌拿峙c請(qǐng)求類中的名字不一致的情況预明,比如說請(qǐng)求是student_name
,而類中字段名為studentName
耙箍。因?yàn)槭?code>POST請(qǐng)求撰糠,采用的是
JSON
的方式,所以只需要在studentName
上通過@JsonProperty
注解一下即可究西,如下:@JsonProperty("student_name") @NotBlank(message = "student_name不能為空") private String studentName;
3.2 刪除學(xué)生信息
采用DELETE
刪除指定姓名的學(xué)生窗慎,需要判斷姓名不能為空,這里采用@RequestParam
獲取student_name
參數(shù)卤材。
-
直接通過
@NotBlank
校驗(yàn)@DeleteMapping(value = "/delete", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Map<String, Object> delete(@NotBlank(message = "student_name不可以為空") @RequestParam(name = "student_name") String name) { return OutPut.success(HttpStatusWrapper.OK,"成功", name); }
這里通過
@NotBlank
要求student_name
不可以為空(null或者trim之后的"")遮斥,運(yùn)行后程序正常運(yùn)行,但是@NotBlank
并沒有生效--student_name
即使填了空白也沒有報(bào)錯(cuò)這是因?yàn)椴幌?code>@Valid可以直接作用在
@RequestBody
參數(shù)上扇丛,@NotBlank
并不會(huì)直接在@RequestParam
參數(shù)上生效术吗。 -
Controller
上添加@Validated
配合@NotBlank
校驗(yàn)參考Validating RequestParams and PathVariables in Spring MVC這篇文章,了解到
@RequestParam
上的validation需要在類上標(biāo)注@Validated
注解(即在StudentController
上注解)然而添加改注解運(yùn)行后帆精,
@NotBlank
仍然沒有生效较屿。原因是沒有為@RequestParam
配置注解器。 -
spring mvc配置校驗(yàn)器
@Configuration @EnableWebMvc @EnableAspectJAutoProxy @EnableScheduling @ComponentScan(basePackages = "com.lianglei.spring.demo") public class ApplicationConfig implements WebMvcConfigurer { @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory(); return validatorFactory.getValidator(); } @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor(); methodValidationPostProcessor.setValidator(validator()); return methodValidationPostProcessor; } }
failFast
的意思只要出現(xiàn)校驗(yàn)失敗的情況卓练,就立即結(jié)束校驗(yàn)隘蝎,不再進(jìn)行后續(xù)的校驗(yàn)。如何在spring中配置bean襟企,若有疑問請(qǐng)參看Spring中的Bean配置方式一文
配置成功后嘱么,再次運(yùn)行服務(wù)進(jìn)行
delete
請(qǐng)求,系統(tǒng)拋出如下異常:[ERROR] 2018-12-23 09:51:10,243 method:com.lianglei.spring.demo.exception.GlobalExceptionHandler.handleException(GlobalExceptionHandler.java:34) delete.arg0: name不可以為空 javax.validation.ConstraintViolationException: delete.arg0: name不可以為空 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) at com.lianglei.spring.demo.controller.StudentController$$EnhancerBySpringCGLIB$$2cfc55af.delete(<generated>) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:215) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:142) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:998) at org.springframework.web.servlet.FrameworkServlet.doDelete(FrameworkServlet.java:923) ...
從這里可以看出顽悼,
@RequestParam
上validate失敗后拋出的異常是javax.validation.ConstraintViolationException
而不是@RequestBody
中的MethodArgumentNotValidException
異常曼振。 -
捕獲
ConstraintViolationException
異常@ExceptionHandler(value = {ConstraintViolationException.class}) public Map<String, Object> handleConstraintViolationException(ConstraintViolationException e) { LOGGER.error(e.getMessage(), e); final String message = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining()); return OutPut.failure(HttpStatusWrapper.ILLEGAL_REQUEST_PARAMETERS, message); }
自此,
@RequestParam
就能夠自動(dòng)生效了蔚龙。{ "status": "Illegal request parameters", "code": 460, "msg": "student_name不可以為空" }
3.3 修改學(xué)生信息
修改學(xué)生信息一般通過@RequestBody
將參數(shù)傳遞給StudentModel
冰评,這時(shí)候校驗(yàn)方式同添加學(xué)生信息
中所述。但是木羹,如果需要通過StudentModel
mapping 原先的@RequestParam
參數(shù)甲雅,又該如何呢?
-
請(qǐng)求中直接通過
@Valid
校驗(yàn)StudentModel
@PutMapping(value = "/update", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Map<String, Object> update(@Valid StudentModel studentModel) { return OutPut.success(HttpStatusWrapper.OK,"成功", studentModel); }
執(zhí)行請(qǐng)求:
localhost:8080/student/update?student_name=wangwu&age=1&gender=0&birth_day=1994-06-15
異常如下:
Field error in object 'studentModel' on field 'studentName': rejected value [null]; codes [NotBlank.studentModel.studentName,NotBlank.studentName,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [studentModel.studentName,studentName]; arguments []; default message [studentName]]; default message [studentName不能為空] at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:164) at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124) at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:165) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:998) at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:912) at javax.servlet.http.HttpServlet.service(HttpServlet.java:710) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:875) at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:848)
可見
student_name
并沒有映射到studentName
上,導(dǎo)致studentName
為null
务荆。這也是必然的妆距,因?yàn)?code>@JsonProperty是用來處理json字符串轉(zhuǎn)對(duì)象的,而請(qǐng)求中并沒有json格式的學(xué)生信息函匕。當(dāng)把請(qǐng)求中的student_name
改回studentName
后娱据,studentName
就會(huì)被正確賦值。
那么盅惜,如何才能解決非POST json
形式下中剩,請(qǐng)求參數(shù)和對(duì)象的屬性名不一致的情況呢?- 請(qǐng)求及接口修改成
PSOT json
的形式 - 參考 How to customize parameter names when binding spring mvc command objects中的討論或者綁定SpringMvc GET請(qǐng)求對(duì)象時(shí)自定義參數(shù)名總結(jié)的方法。
- 拆分對(duì)象字段到接口參數(shù)中抒寂,通過
@RequestParam
結(jié)合Hibernate Validator
完成驗(yàn)證
- 請(qǐng)求及接口修改成
3.4 查詢學(xué)生信息
通過GET
方式查詢學(xué)生信息结啼,student_name
以@PathVariable
的方式進(jìn)行賦值。
@GetMapping(value = "/get/{student_name}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Map<String, Object> get(@NotBlank(message = "student_name不可以為空") @Size(min = 3, message = "student_name長(zhǎng)度不能小于3") @PathVariable(name = "student_name") String name) {
return OutPut.success(HttpStatusWrapper.OK,"成功", name);
}
-
正常請(qǐng)求
localhost:8080/student/get/wangwu
返回
{ "status": "OK", "code": 200, "msg": "成功", "data": "wangwu" }
-
異常請(qǐng)求
localhost:8080/student/get/yy
返回
{ "status": "Illegal request parameters", "code": 460, "msg": "student_name長(zhǎng)度不能小于3" }
拋出的與
@RequestParam
方式一樣的ConstraintViolationException
異常屈芜。
4 總結(jié)
通過上面的示例演示郊愧,對(duì)于spring mvc中的參數(shù)校驗(yàn),可以得出如下結(jié)論:
- 如果接口參數(shù)對(duì)應(yīng)的是請(qǐng)求中請(qǐng)求體部分(
@RequestBody
)井佑,且請(qǐng)求體的格式為json
属铁,可以將請(qǐng)求參數(shù)封裝到一個(gè)類中,在類中通過@NotNull
等標(biāo)注設(shè)置校驗(yàn)規(guī)則躬翁,在接口中通過@Valid
表明需要進(jìn)行校驗(yàn)焦蘑,校驗(yàn)失敗后會(huì)拋出MethodArgumentNotValidException
異常。如果請(qǐng)求中參數(shù)的名稱和接口參數(shù)中字段的名稱不一致盒发,可以通過@JsonProperty
標(biāo)注進(jìn)行重命名例嘱。 - 如果接口參數(shù)中對(duì)應(yīng)的是請(qǐng)求參數(shù)(
@RequestParam
)或者請(qǐng)求路徑中的變量(@PathVariable
),則可以通過對(duì)應(yīng)的@RequestParam
或者@PathVariable
結(jié)合@NotNull
進(jìn)行參數(shù)檢驗(yàn)宁舰,注意這里需要在Controller
上添加@Validated
注解拼卵,并且需要給spring配置MethodValidationPostProcessor
才能工作。如果校驗(yàn)失敗蛮艰,會(huì)拋出ConstraintViolationException
異常腋腮。如果需要將這些參數(shù)封裝到一個(gè)類中,那么請(qǐng)求中的參數(shù)名必須和類中的字段一致印荔,否則會(huì)匹配不上。當(dāng)然详羡,可以通過額外的配置滿足這個(gè)需求仍律,但是比較麻煩而且不支持繼承的類。