项目开发中我们经常需要对接口参数进行检验,有些参数必须有值,有些参数比如手机号,身份证号必须符合一定的格式规范。如果把参数检验都写在业务代码中,会造成业务代码又长又杂,且每个接口都得去校验,影响开发效率以及代码质量。有没有方法可以把参数检验统一在一个地方进行,不对我们的业务代码进行干扰呢?
SpringMVC给我们提供了一些支持数据校验的注解,如@NotNull,@NotBlank,@Length(min=,max=)等,将这些注解添在bean的属性上,结合在Controller里对请求参数添加@Valid注解即可完成参数的校验。
1、创建需要被校验的实体类:
package com.ouyangjia.miaosha.vo;
import lombok.Data;
import org.hibernate.validator.constraints.NotBlank;
@Data
public class LoginVo {
@NotBlank(message="电话号码不能为空")
private String phone;
@NotBlank(message="密码不能为空")
private String password;
}
2、controller 接口设计,在参数接受的地方添加 @Valid
关键字:
@RequestMapping("/do_login_1")
@ResponseBody
public Result doLogin_1(@RequestBody@Valid LoginVo loginVo){
log.info(loginVo.toString());
String str=loginService.login(loginVo);
return Result.success(str);
}
3、访问测试,数据如下:
{
"phone":"",
"password":""
}
4、响应为:
{
"timestamp": 1537152065877,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"errors": [
{
"codes": [
"NotBlank.loginVo.password",
"NotBlank.password",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"loginVo.password",
"password"
],
"arguments": null,
"defaultMessage": "password",
"code": "password"
}
],
"defaultMessage": "密码不能为空",
"objectName": "loginVo",
"field": "password",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
},
{
"codes": [
"NotBlank.loginVo.phone",
"NotBlank.phone",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"loginVo.phone",
"phone"
],
"arguments": null,
"defaultMessage": "phone",
"code": "phone"
}
],
"defaultMessage": "电话号码不能为空",
"objectName": "loginVo",
"field": "phone",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='loginVo'. Error count: 2",
"path": "/login/do_login_1.json"
}
我们可以看到,抛出了MethodArgumentNotValidException异常,由于我们没有对异常进行处理,异常信息将会直接展现在页面上,对用户很不友好。
如果@Valid校验不通过,那么错误信息就会封装到BindingResult对象了,可以通过BindingResult的相关方法获取详细的错误信息并返回给用户。 代码如下:
@RequestMapping("/do_login_2")
@ResponseBody
public Result doLogin_2(@RequestBody@Valid LoginVo loginVo,BindingResult bindingResult){
StringBuffer stringBuffer=new StringBuffer();
if(bindingResult.hasErrors()){
List errorList = bindingResult.getAllErrors();
for(ObjectError error:errorList){
stringBuffer.append(error.getDefaultMessage());
}
return Result.fail(CodeMsg.BIND_ERROR.fillArgs(stringBuffer.toString()));
}
log.info(loginVo.toString());
String str=loginService.login(loginVo);
return Result.success(str);
}
响应如下:
{
"code": "500101",
"msg": "参数校验异常:密码不能为空电话号码不能为空",
"data": null
}
虽然这样能够对异常进行处理,但是在controller中除了正常的业务代码外,还有很大一部分代码用于处理BindingResult对象。造成了对业务代码的侵入,我们希望能够在一个地方统一处理这些异常。
使用@ControllerAdvice+@ExceptionHandler进行全局的Controller层异常处理,其中@ControllerAdvice定义了全局异常处理类,@ExceptionHandler则声明异常处理的方法。这样我们不仅能够处理@Valid检验器的异常,还可以处理controller层的其他异常,少了很多try,catch的代码,是不是棒棒哒!
注:@ControllerAdvice注解,字面意思控制器增强。可用于定义@ExceptionHandler,@InitBinder,@ModelAttribute,并将应用到所有的@RequestMapping方法中。
实例演练:
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public Result exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof MethodArgumentNotValidException){
MethodArgumentNotValidException ex= (MethodArgumentNotValidException) e;
List errors = ex.getBindingResult().getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.fail(CodeMsg.BIND_ERROR.fillArgs(msg));
}else {
return Result.fail(CodeMsg.SERVER_ERROR);
}
}
}
这样,我们的controller中的代码如下:
@RequestMapping("/do_login")
@ResponseBody
public Result doLogin(HttpServletResponse response,@Valid LoginVo loginVo) {
log.info(loginVo.toString());
//登录
String token=userService.login(response,loginVo);
return Result.success(token);
}
少了绑定校验结果BindingResult,代码看起来也清爽了不少。
有的时候已有的注解满足不了我们的校验需求,我们需要自定义注解进行检验。
1、定义注解
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy={IsMobile.MobileValidator.class})
public @interface IsMobile {
String message() default "手机号码格式错误";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
class MobileValidator implements ConstraintValidator{
public void initialize(IsMobile isMobile) {
}
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if(StringUtils.isBlank(s)){
return false;
}else {
Pattern mobile_pattern=Pattern.compile("1\\d{10}");
Matcher m=mobile_pattern.matcher(s);
return m.matches();
}
}
}
}
上述代码,通过@Constraint(validatedBy =IsMobile.MobileValidator.class)限定了注解的方法逻辑---该注解类的名为MobileValidator的内部类。而该内部类实现了ConstraintValidator
2、在要校验的属性上添加@IsMobile注解
@Data
public class LoginVo {
@NotBlank(message="电话号码不能为空")
@IsMobile
private String phone;
@NotBlank(message="密码不能为空")
private String password;
}
3、测试参数:
{
"phone":"1",
"password":"1"
}
4、响应为:
{
"code": "500101",
"msg": "参数校验异常:手机号码格式错误",
"data": null
}
运用注解校验(Spring提供的注解+自定义注解)和全局统一处理器,对参数进行统一的校验和异常处理,避免对业务代码侵入,保持代码简洁性,提升开发效率。你学会了吗?