SpringBoot系列五:SpringBoot错误处理(数据验证、处理错误页、全局异常)
声明:本文来源于MLDN培训视频的课堂笔记,写在这里只是为了方便查阅。
1、概念:
SpringBoot 错误处理
2、具体内容
在之前的程序里面如果一旦出现了错误之后就会出现一堆的大白板,这个白板会有一些错误信息(虽然这些错误信息你可能 看不懂,但是这些错误信息依然要告诉给用户)。在 SpringBoot 里面针对于错误的处理一共提供有三种方式:数据验证错误、错误 页指派以及全局异常的处理。
2.1、数据验证
现在假设说要进行表单信息提交,肯定需要有一个表单,而后这个表单要将数据提交到 VO 类中,所以现在的基本实现如下:
1、 建立一个 Member.java 的 VO 类:
package cn.study.microboot.vo; import java.io.Serializable; import java.util.Date; @SuppressWarnings("serial") public class Member implements Serializable { private String mid ; private Integer age ; private Double salary ; private Date birthday ; public String getMid() { return mid; } public void setMid(String mid) { this.mid = mid; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Double getSalary() { return salary; } public void setSalary(Double salary) { this.salary = salary; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override public String toString() { return "Member [mid=" + mid + ", age=" + age + ", salary=" + salary + ", birthday=" + birthday + "]"; } }
2、 由于此时的程序之中需要进行日期的转换处理操作,那么就需要为其做一个转换处理的格式配置,修改 AbstractBaseController 类,追加如下的转换操作方法绑定:
@InitBinder public void initBinder(WebDataBinder binder) { // 在本程序里面需要针对于日期格式进行处理 // 首先建立一个可以将字符串转换为日期的工具程序类 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd") ; // 明确的描述此时需要注册一个日期格式的转化处理程序类 binder.registerCustomEditor(java.util.Date.class, new CustomDateEditor(sdf, true)); }
3、 建立一个 MemberController 程序类,负责实现 Member 的控制层处理操作。
package cn.study.microboot.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import cn.study.microboot.util.controller.AbstractBaseController; import cn.study.microboot.vo.Member; @Controller public class MemberController extends AbstractBaseController { @RequestMapping(value = "/addPre", method = RequestMethod.GET) public String addPre() { // 增加前的准备操作路径 return "member_add" ; } @RequestMapping(value = "/add", method = RequestMethod.POST) @ResponseBody public Object add(Member vo) { // 增加前的准备操作路径 return vo ; } }
4、 编写一个页面进行用户的表单填写(在 src/main/view/templates 下建立):member_add.html;
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>SpringBoot模版渲染</title> <link rel="icon" type="image/x-icon" href="/images/study.ico"/> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> </head> <body> <form action="add" method="post"> 用户邮箱:<input type="text" name="mid" value="studyjava@163.com"/><br/> 用户年龄:<input type="text" name="age" value="18"/><br/> 用户工资:<input type="text" name="salary" value="1000"/><br/> 用户生日:<input type="text" name="birthday" value="2010-10-10"/><br/> <input type="submit" value="提交"/> <input type="reset" value="重置"/> </form> </body> </html>
5、 此时的代码只是一个最为普通的处理操作,但是这个时候对于该程序也是存在有如下问题的:
· 如果某些数据没有输入,则内容是 null,如果要进行严格控制,这些 null 不应该存在;
· 某些数据需要进行格式验证,例如:用户名应该是邮箱,这个的信息应该进行邮箱验证;
所以现在如果要想进行这些的验证,SpringBoot 里面有默认的支持,只不过这种支持未必是最好的,在 SpringBoot 里面为了 方便用户编写验证专门提供有一个 hibernate-validation.jar 工具包,这个工具包是由 hibernate 开发框架提供的。
6、 如果要想进行验证,那么首先要解决的问题就必须是错误的提示信息问题,而在 SpringBoot 里面对于错误信息的保存,都要 求其保存在 ValidationMessages.properties 文件,在“src/main/resources”目录中建立此文件;
member.mid.notnull.error=用户名不允许为空!
member.mid.email.error=用户名的注册必须输入正确的邮箱!
member.mid.length.error=用户名的格式错误!
member.age.notnull.error=年龄不允许为空
member.age.digits.error=年龄必须是合法数字!
member.salary.notnull.error=工资不允许为空!
member.salary.digits.error=工资必须是合法数字!
member.birthday.notnull.error=生日不允许为空!
提示:你一个表单就需要编写这么多的配置项,那么如果要有几百个表单呢?这样的配置项太可怕了,所以最好的数据检测还是利 用拦截器处理最合适。
7、 修改 Member.java 程序类追加验证的处理方式:
package cn.study.microboot.vo; import java.io.Serializable; import java.util.Date; import javax.validation.constraints.Digits; import javax.validation.constraints.NotNull; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; @SuppressWarnings("serial") public class Member implements Serializable { @NotNull(message="{member.mid.notnull.error}") @Email(message="{member.mid.email.error}") @Length(min=6,message="{member.mid.length.error}") private String mid ; @NotNull(message="{member.age.notnull.error}") @Digits(integer=3,fraction=0,message="{member.age.digits.error}") private Integer age ; @NotNull(message="{member.salary.notnull.error}") @Digits(integer=20,fraction=2,message="{member.salary.digits.error}") private Double salary ; @NotNull(message="{member.birthday.notnull.error}") private Date birthday ; public String getMid() { return mid; } public void setMid(String mid) { this.mid = mid; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Double getSalary() { return salary; } public void setSalary(Double salary) { this.salary = salary; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override public String toString() { return "Member [mid=" + mid + ", age=" + age + ", salary=" + salary + ", birthday=" + birthday + "]"; } }
8、 修改 MemberController 类中的 add()方法来观察错误信息的显示:
package cn.study.microboot.controller; import java.util.Iterator; import javax.validation.Valid; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import cn.study.microboot.util.controller.AbstractBaseController; import cn.study.microboot.vo.Member; @Controller public class MemberController extends AbstractBaseController { @RequestMapping(value = "/add", method = RequestMethod.POST) @ResponseBody public Object add(@Valid Member vo, BindingResult result) { // 增加前的准备操作路径 if (result.hasErrors()) { // 现在表示执行的验证出现错误 Iterator<ObjectError> iterator = result.getAllErrors().iterator(); // 获取全部错误信息 while (iterator.hasNext()) { ObjectError error = iterator.next() ; // 取出每一个错误 System.out.println("【错误信息】code = " + error.getCode() + ",message = " + error.getDefaultMessage()); } return result.getAllErrors() ; } else { return vo; } } @RequestMapping(value = "/addPre", method = RequestMethod.GET) public String addPre() { // 增加前的准备操作路径 return "member_add"; } }
对于此类的验证大家理解即可,不需要将其作为重点,但是需要清楚,默认情况下 SpringBoot 提供的数据验证需要通过注解 以及一系列的资源文件进行定义后才可以使用,而且所有的错误都必须用户自己来处理,这一点的设计不如直接编写具体的反射拦 截器方便。
2.2、处理错误页
错误页绝对是所有的 WEB 项目之中必须具有的一项信息显示处理,但是在传统的 WEB 项目开发过程之中,错误页都是在 web.xml 文件之中进行配置的,不过遗憾的是 SpringBoot 之中并不存在有 web.xml 配置文件这一项,那么如果要想进行错误页的处 理,最好的做法是需要根据每一个错误代码创建一个属于自己的错误显示页。
1、 所有的错误页都是普通的静态文件,那么建议在“src/main/view/static”目录下创建几个常见的错误页(常见的错误的 HTTP 返回编码:404、500、400)
2、 添加一个错误页的配置类,在启动类中编写一个错误页的配置项;
package cn.study.microboot.config; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.web.servlet.ErrorPage; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; @Configuration public class ErrorPageConfig { @Bean public EmbeddedServletContainerCustomizer containerCustomizer() { return new EmbeddedServletContainerCustomizer() { @Override public void customize( ConfigurableEmbeddedServletContainer container) { ErrorPage errorPage400 = new ErrorPage(HttpStatus.BAD_REQUEST, "/error-400.html"); ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-404.html"); ErrorPage errorPage500 = new ErrorPage( HttpStatus.INTERNAL_SERVER_ERROR, "/error-500.html"); container.addErrorPages(errorPage400, errorPage404, errorPage500); } }; } }
那么此时只要出现了错误,就会找到相应的 http 状态码,而后跳转到指定的错误路径上进行显示。
2.3、全局异常
下面首先来观察一个程序代码,例如:现在建立一个控制器,而后这个控制器自己抛出一个异常。
@RequestMapping(value="/get") @ResponseBody public String get() { System.out.println("除法计算:" + (10 / 0)); return "hello world" ; }
如果此时配置有错误页,那么这个时候错误会统一跳转到 500 所在的路径上进行错误的显示,但是如果说现在希望能够显示 出错误更加详细的内容呢?
所以这个时候可以单独定义一个页面进行错误的信息显示处理,而这个页面,可以定义在“src/main/view/templates/error.html”, 这个页面里面要求可以输出一些信息;
1、 定义一个全局的异常处理类:
import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; @ControllerAdvice // 作为一个控制层的切面处理 public class GlobalExceptionHandler { public static final String DEFAULT_ERROR_VIEW = "error"; // 定义错误显示页,error.html @ExceptionHandler(Exception.class) // 所有的异常都是Exception子类 public ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) { // 出现异常之后会跳转到此方法 ModelAndView mav = new ModelAndView(DEFAULT_ERROR_VIEW); // 设置跳转路径 mav.addObject("exception", e); // 将异常对象传递过去 mav.addObject("url", request.getRequestURL()); // 获得请求的路径 return mav; } }
2、 定义 error.html 页面:
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>SpringBoot模版渲染</title> <link rel="icon" type="image/x-icon" href="/images/study.ico"/> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> </head> <body> <p th:text="${url}"/> <p th:text="${exception.message}"/> </body> </html>
对于全局异常信息显示除了采用以上的跳转处理方式之外,也可以做的简单一些,使用 Rest 进行显示。
范例:修改全局异常处理类
package cn.study.microboot.advice; import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; //@ControllerAdvice// 作为一个控制层的切面处理 @RestControllerAdvice public class GlobalExceptionHandler { public static final String DEFAULT_ERROR_VIEW = "error"; // 定义错误显示页,error.html @ExceptionHandler(Exception.class) // 所有的异常都是Exception子类 public Object defaultErrorHandler(HttpServletRequest request,Exception e) { class ErrorInfo { private Integer code ; private String message ; private String url ; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } } ErrorInfo info = new ErrorInfo() ; info.setCode(100); // 标记一个错误信息类型 info.setMessage(e.getMessage()); info.setUrl(request.getRequestURL().toString()); return info ; } // public ModelAndView defaultErrorHandler(HttpServletRequest request, // Exception e) { // 出现异常之后会跳转到此方法 // ModelAndView mav = new ModelAndView(DEFAULT_ERROR_VIEW); // 设置跳转路径 // mav.addObject("exception", e); // 将异常对象传递过去 // mav.addObject("url", request.getRequestURL()); // 获得请求的路径 // return mav; // } }
如果现在要想把异常的信息显示的更加华丽一些(不是面对所有用户),那么最好的做法就是使用全局异常处理的方式完成。