1、前言
编写控制层时,我们可能会自己去校验请求参数,就会出现这样的代码:
if (StringUtils.isEmpty(memberSid)) {return new JsonResult(false, "参数memberSid为空");
}
if (null == test) {return new JsonResult(false, "参数test为空");
}
相同的参数在不同的地方运用就是出现类型的冗余的代码。如果我们要快速开发要么把参数校验复制一份出来,要么抽成公用的方法 。重复的代码本来就是代码重构和优化的一个表象,所以这种方式本身不可取。抽成公用方法确实是一个不错的方法,但是未能从本质像解决。
由此,看能不能使用框架的技术去解决这样问题。试着使用【Spring Validation】能否解决问题。
2、引入依赖
非springboot 项目需要引入一下依赖
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>1.1.0.Final</version>
</dependency><dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>5.4.1.Final</version>
</dependency>
springboot 项目需要区分版本:
springboot2.3之前,只需要引入一下依赖即可:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3之前的版本</version>
</dependency>
springboot2.3之后(含2.3),取消了【hibernate-validator】相关的依赖,需要手动引入一下依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
以下为官方的的说明:
3、了解【Spring Validation】的相关注解
Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。
针对参数的注解:
@Validated和@Valid的区别:
@Validated
- 提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,可参考高效使用hibernate-validator校验框架。
- 可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上。
@Valid
- 作为标准JSR-303规范,不支持分组的功能。
- 可以用在方法、构造函数、方法参数和成员属性(字段)上。
* 因为@Valid可以用来成员属性上,该注解也可以用来校验嵌套的校验。
4、最佳实践
4.1 请求参数的实体
实体中针对需要校验的字段使用相应的注解,指定输出的message,以及分组。分组是为了区分不同业务场景实体的公用。
@Data
public class BookDTO implements Serializable {private static final long serialVersionUID = 6833185176640090531L;private Integer id;@Max(value = 30L, message = "最高价格不超过30!", groups = {SaveAction.class})private Double price;@NotNull(message = "作者信息不能为空!", groups = {UpdateAction.class})private String author;@Length(min = 5, max = 10, message = "书名的长度必须在5~10之间")private String name;private Date buyDate;// 开启嵌套校验@Validprivate BookContent bookContent;
}
4.2 嵌套类
@Data
public class BookContent implements Serializable {private static final long serialVersionUID = -3018013509162052516L;@NotBlank(message = "书的内容不能为空!")private String content;private Integer pageCount;
}
4.3 分组接口
分组接口只是为了标识使用的场景,接口中没有任何的实现。
public interface SaveAction {}public interface UpdateAction {}
4.4 控制层的实现
@RequestBody是为了测试方便,使用的json参数提交,也可以使用表单提交。
参数中没有加分组的都属于默认分组,使用Default.class即可。
@RestController
@RequestMapping("/foo")
public class FooController {@PostMapping ("/test01")public String test01(@Validated({SaveAction.class, Default.class}) @RequestBody BookDTO bookDTO){System.out.println(JSON.toJSONString(bookDTO));Assert.notNull(bookDTO.getId(), "ID信息不能为空!");return "success";}@GetMapping("/test02")public String test02(@RequestParam String a, Date date){if (date != null) {System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(date));}return "success:" + a;}
}
4.5 统一参数处理
参数的校验不通过,如果控制层不处理的话,都会抛出异常,只需要针对异常处理,获取异常的报错信息响应即可。
@ControllerAdvice是针对所有的控制Controller。因为前端传递的参数有可能是日期。那我们需要将对应的字符串转成日期,可以使用@InitBinder完成对日期的解析。
@InitBinder对日期的解析只是针对form-data结构有效,如果是JSON数据的话则无效。
@ControllerAdvice
@Slf4j
public class GlobalHandler {@ExceptionHandler(Exception.class)public ResponseEntity<String> globalExceptionHandler(Exception e){log.warn("异常信息:{}", e);String msg = e.getMessage();if (e instanceof BindException) {msg = ((BindException)e).getBindingResult().getFieldError().getDefaultMessage();}else if (e instanceof MissingServletRequestParameterException) {MissingServletRequestParameterException me = (MissingServletRequestParameterException)e;msg = "参数缺失,缺失的参数:" + me.getParameterName();}return ResponseEntity.ok(msg);}@InitBinderpublic void globalWebDataBinder(WebDataBinder binder){DateFormatter dateFormatter = new DateFormatter("yyyy-MM-dd");String[] patterns = {"yyyy/MM/dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss"};dateFormatter.setFallbackPatterns(patterns);binder.addCustomFormatter(dateFormatter, Date.class);}
}
5、异常的处理
对于绑定参数的处理,归根到底是对异常的处理。对于异常的处理,我们最终需要获取到异常的信息,那我们只需要处理有差异的异常即可。
曾看到很多文档这对异常逐个处理,但是最终的结果都是 e.getMessage() 。个人觉得没有必要。
异常的处理包括框架的异常,以及自定义异常。最终包装成用户可以识别的错误信息返回。这也是异常处理的目的。