之间整理过一篇 springboot 项目里使用 hibernate-validator 校验参数, 然后在 freemarker 模板里取异常信息展示 的博客
现在都流行前后端分离了, 服务端大都直接返 JSON, 又稍微折腾了一下, 结合统一异常处理, 优雅的实现请求参数的校验
创建项目
这个不多说, 引入一个依赖即可
- <dependency>
- <groupId>org.hibernate.validator</groupId>
- <artifactId>hibernate-validator</artifactId>
- <version>6.0.17.Final</version>
- </dependency>
简单使用
使用 hibernate-validator 分两步
@Validated @NotNull
简单例子
- import com.example.hibernatevalidator.util.Result;
- import org.springframework.validation.annotation.Validated;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.Web.bind.annotation.RequestMapping;
- import org.springframework.Web.bind.annotation.RestController;
- import javax.validation.constraints.NotNull;
- /**
- * Created by tomoya at 2019/9/11
- */
- @RestController
- @RequestMapping("/")
- @Validated
- public class IndexController {
- @GetMapping("/index")
- public Object index(@NotNull(message = "姓名不能为空") String name) {
- return Result.success("你好," + name);
- }
- }
可以看到 @NotNull 注解是 javax.validation.constraints 这个包下的, 后面介绍的一些校验注解都是这个包下的, 可见 hibernate-validator 是实现的 java 的 API
然后启动项目, 浏览器访问 http://localhost:8080/index 可以看到出现了下面这个错误页面
这个页面是 springboot 内置的错误页面
项目都前后端分离了, 错误信息肯定最好也用 JSON 返回了, 其实这时候如果用 postman 这样的工具来访问这个接口返回的就是一段 JSON, 长下面这个样
- {
- "timestamp": "2019-09-11T07:42:56.703+0000",
- "status": 500,
- "error": "Internal Server Error",
- "message": "index.name: 姓名不能为空",
- "path": "/index"
- }
就算是这个样也不是我想要的, 项目里封装了一个类, 专门返回 JSON 的, 长这个样
- public class Result {
- private int code;
- private String description;
- private Object detail;
- public static Result success(Object detail) {
- Result result = new Result();
- result.setCode(200);
- result.setDetail(detail);
- return result;
- }
- public static Result error(String description) {
- Result result = new Result();
- result.setCode(201);
- result.setDescription(description);
- return result;
- }
- // getter setter
- }
怎么才能让 hibernate-validator 校验的异常信息以自己定义的类的格式返回呢? 这就要用到统一异常处理了
链接文原:
统一异常处理
springmvc 是相当的强大的, 只需要两个注解就能使用统一异常处理 @ControllerAdvice 外加上 @ExceptionHandler
- import com.example.hibernatevalidator.util.Result;
- import org.springframework.Web.bind.annotation.ControllerAdvice;
- import org.springframework.Web.bind.annotation.ExceptionHandler;
- import org.springframework.Web.bind.annotation.ResponseBody;
- @ControllerAdvice
- public class GlobalExceptionHandler {
- @ExceptionHandler(value = Exception.class)
- @ResponseBody
- public Result defaultErrorHandler(Exception e) {
- System.out.println(e.getMessage());
- return Result.error(e.getMessage());
- }
- }
这时候再用浏览器访问就可以看到返回的是一段 JSON 字符串了
常用校验注解
@NotNull 只能判断是 null 的参数
@NotBlank 可以判断 null 和空字符串
@Email 校验参数是否是标准的 email 格式
@URL 判断参数是否是一个正常的 url 地址
@Max 判断一个数字最大值是多少, 超过抛出异常
@Min 与 @Max 相反
@Size 指定一个字符串的长度范围, 包含最小和最大
@Pattern 自定义正则校验参数
一个参数可以有多个校验注解来修饰
- import com.example.hibernatevalidator.util.Result;
- import org.springframework.validation.annotation.Validated;
- import org.springframework.Web.bind.annotation.GetMapping;
- import org.springframework.Web.bind.annotation.RequestMapping;
- import org.springframework.Web.bind.annotation.RestController;
- import javax.validation.constraints.*;
- /**
- * Created by tomoya at 2019/9/11
- */
- @RestController
- @RequestMapping("/")
- @Validated
- public class IndexController {
- @GetMapping("/index")
- public Object index(@NotBlank(message = "姓名不能为空") String name,
- @NotNull(message = "age1 不能为空") @Max(value = 12, message = "最大为 12") Integer age1,
- @NotNull(message = "age2 不能为空") @Min(value = 6, message = "最小为 6") Integer age2,
- @NotNull(message = "address 不能为空") @Size(min = 6, max = 20, message = "范围要在 [6,20] 之间") String address,
- @NotNull(message = "password 不能为空") @Pattern(regexp = "[a-zA-Z0-9]{4,16}$", message = "密码为字母 + 数字组合 4-16 位") String password) {
- return Result.success("你好," + name);
- }
- }
访问浏览器 http://localhost:8080/index 输出的 JSON 长下面这个样
- {
- "code": 201,
- "description": "index.address: address 不能为空, index.age1: age1 不能为空, index.password: password 不能为空, index.age2: age2 不能为空, index.name: 姓名不能为空"
- }
异常分别处理
它是把错误信息都拼在一块输出的, 这样在前端不好展示的, 可以通过 : 和 , 将字符串分隔成前端想要的格式再返回, 这个在统一异常处理那做就可以了
但这又会出一个问题, 一个项目里用上了统一异常处理后, 可不止校验这一个地方会出现异常, 如果只是简单的使用 e.getMessage() 将异常信息拿出来进行分隔, 肯定会出问题的, 因为如果是空指针异常的话 e.getMessage() 取出来的是个 null, 这时候再进行 split() 肯定又会报错的
解决办法就是根据异常类来进行区分, 通过断点可以发现 hibernate-validator 校验的异常类是 javax.validation.ConstraintViolationException
这样就可以做文章了, 代码如下
- @ControllerAdvice
- public class GlobalExceptionHandler {
- @ExceptionHandler(value = Exception.class)
- @ResponseBody
- public Result defaultErrorHandler(Exception e) {
- System.out.println(e.getMessage());
- if (e instanceof ConstraintViolationException) {
- ConstraintViolationException constraintViolationException = (ConstraintViolationException) e;
- String message = StringUtils.collectionToCommaDelimitedString(
- constraintViolationException.getConstraintViolations()
- .stream()
- .map(ConstraintViolation::getMessage)
- .collect(Collectors.toList()));
- return Result.error(message);
- }
- return Result.error("服务异常");
- }
- }
再次访问 异常信息就长下面这个样了
- {
- "code": 201,
- "description": "age2 不能为空, address 不能为空, age1 不能为空, password 不能为空, 姓名不能为空"
- }
这只是一种分隔方法, 可以自己定义自己的封装方式
对象校验
如果一个方法的参数相当的多, 也可以将其封装在一个对象中, 然后对这个对象进行校验, 对象代码如下
文链接原:
- import javax.validation.constraints.*;
- /**
- * Created by tomoya at 2019/9/11
- */
- public class User {
- @NotBlank(message = "姓名不能为空")
- private String name;
- @NotNull(message = "age1 不能为空")
- @Max(value = 12, message = "最大为 12")
- private Integer age1;
- @NotNull(message = "age2 不能为空")
- @Min(value = 6, message = "最小为 6")
- private Integer age2;
- @NotNull(message = "address 不能为空")
- @Size(min = 6, max = 20, message = "范围要在 [6,20] 之间")
- private String address;
- @NotNull(message = "password 不能为空")
- @Pattern(regexp = "[a-zA-Z0-9]{4,16}$", message = "密码为字母 + 数字组合 4-16 位")
- private String password;
- // getter setter
- }
controller 方法里参数也要换成对象, 注解也变了
- @GetMapping("/index1")
- public Object index1(@Valid User user) {
- return Result.success("你好," + user.getName());
- }
访问浏览器, 输出 JSON 如下
- {
- "code": 201,
- "description": "服务异常"
- }
这一看就不对, 不是上面封装的异常处理信息, 断点查看原来对对象进行校验异常类变成了 org.springframework.validation.BindException
知道是哪个异常类就好办了, 另外对这个异常类也做一下处理即可
- @ControllerAdvice
- public class GlobalExceptionHandler {
- @ExceptionHandler(value = Exception.class)
- @ResponseBody
- public Result defaultErrorHandler(Exception e) {
- System.out.println(e.getMessage());
- if (e instanceof ConstraintViolationException) {
- ConstraintViolationException constraintViolationException = (ConstraintViolationException) e;
- String message = StringUtils.collectionToCommaDelimitedString(
- constraintViolationException.getConstraintViolations()
- .stream()
- .map(ConstraintViolation::getMessage)
- .collect(Collectors.toList()));
- return Result.error(message);
- } else if (e instanceof BindException) {
- BindException bindException = (BindException) e;
- String message = StringUtils.collectionToCommaDelimitedString(
- bindException.getAllErrors()
- .stream()
- .map(DefaultMessageSourceResolvable::getDefaultMessage)
- .collect(Collectors.toList())
- );
- return Result.error(message);
- }
- return Result.error("服务异常");
- }
- }
再次访问输出 JSON
- {
- "code": 201,
- "description": "姓名不能为空, age2 不能为空, address 不能为空, age1 不能为空, password 不能为空"
- }
异常信息再次正常了
总结
hibernate-validator 还是比较好用的, 比在 controller 里一个参数一个参数的去判断要好看的多, 但它也有个问题, 如果一个请求方法里参数相当的多, 就会想着去封装对象, 这样看着也好看, 但如果参数很多的方法非常多的话, 就要多封装很多这样的对象, 也就大大增加了项目中实体类的数量, 这个我个人不太喜欢, 所以不是太多的方法我一般都是直接写在 controller 方法的参数里的
来源: http://www.tuicool.com/articles/3EBzUrq