生活打了我们一巴掌,我们,一定要想办法再打回来
上一章简单介绍了SpringBoot上传文件到远程服务器(二十九),如果没有看过,请观看上一章
一. 为什么要实现异常信息自定义展示
在 Springboot 项目开发中,包括以前的 SSM 框架开发中,我们常常会碰到 404 500 相应的错误信息.
如访问一个除以 0 的功能 (500) 错误
@RequestMapping("/div")
@ResponseBody
public String div(){
int result=10/0;
return "相除后的结果是:"+result;
}
或者访问一个不存在的页面 404 错误
这两个页面都是 SpringBoot 默认提供的.
用户在使用的过程中,如果没有相应的开发或者网络经验,对这些提示信息是很讨厌的。
所以常常进行一些有趣的设计,来减轻项目运行错误导致的不良感受。
如: (以下图片来源于网络)
我们可以自定义错误页面.
二. 自定义错误页面
二.一 Tomcat 服务器配置错误页面
在以前的 Tomcat 和 JSP 时代 ,我们可以这样配置错误页面.
二.一.一 自定义 404.jsp 和 505.jsp 页面
404.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isErrorPage="true"%>
<% response.setStatus(HttpServletResponse.SC_OK);%>
404,地址错误
500.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isErrorPage="true"%>
<% response.setStatus(HttpServletResponse.SC_OK);%>
500, 服务器错误,联系管理员
二.一.二 web.xml 配置信息
在 web.xml 配置中,进行配置
<!--错误状态码指定-->
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/jsp/404.jsp</location>
</error-page>
<!--异常类型指定,也可以细化一下-->
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/WEB-INF/jsp/500.jsp</location>
</error-page>
<!--错误状态码指定-->
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/jsp/500.jsp</location>
</error-page>
二. 二SpringBoot 配置错误页面
根据上面的提示,我们知道,需要在 /error 目录下进行配置相关的页面信息.
配置错误页面,包括两种,一种是静态的异常页面,一种是动态的异常页面.
二.二.一 静态异常页面
在 static 目录下 创建 error 目录, 里面放置 404.html 和 500.html 页面
404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404异常</title>
</head>
<body>
404异常
</body>
</html>
500.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>500 异常信息</title>
</head>
<body>
500异常
</body>
</html>
重新运行服务器,并且访问
在访问 div (有异常的方法)
在访问 404 错误页面时
就变成了我们自定义的异常页面了.
除了指定 404 ,500 这样确切的错误状态码外,也可以使用 4xx, 5xx 这样的来统一进行接收.
二.二.二 动态错误页面
有时候常常需要将错误的信息展示出来,便于开发人员进行处理.
可以使用动态错误页面,通常放置在 templates 目录下
在 templates 目录下,创建动态错误页面
404.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>404状态码展示错误</title>
</head>
<body>
<h1>404</h1>
<table border="1">
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
</table>
</body>
</html>
500.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>5xx状态码展示错误</title>
</head>
<body>
<h1>5xx</h1>
<table border="1">
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
</table>
</body>
</html>
这五个属性信息, path, error,message,timestamp,status
是 由: org.springframework.boot.web.reactive.error.DefaultErrorAttributes 进行定义的
这个时候,再进行访问
会展示出具体的信息.
发现,动态的错误页面是生效的。
如果动态页面和静态页面同时定义了异常处理页面,
例如 classpath:/static/error/404.html
和 classpath:/templates/error/404.html
同时存在时,
默认使用动态页面。
完整的错误页面查找方式应该是这样:
发生了 404错误–>查找动态 404.html 页面–>查找静态 404.html –> 查找动态 4xx.html–>查找静态 4xx.html。
500 也一样.
发生了 500 错误–>查找动态 500.html 页面–>查找静态 500.html –> 查找动态 5xx.html–>查找静态 5xx.html。
三. 后端服务器自定义异常
三.一 提示信息和内容自定义
SpringBoot 提供的页面和属性信息,有时候不符合业务场景,需要后端开发人员进行自定义(自带的 message 是英文的,不符合国人习惯)
我们可以自定义修改
package top.yueshushu.learn.error;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Map;
/**
* @ClassName:MyErrorAttributes
* @Description 后端自定义的提示信息
* @Author zk_yjl
* @Date 2021/11/24 17:34
* @Version 1.0
* @Since 1.0
**/
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
//也可以放置其他的属性信息,或者替换属性,如 message,或者 timestamp
if ((Integer)map.get("status") == 500) {
map.put("message", "服务器内部错误!");
}
if ((Integer)map.get("status") == 404) {
map.put("message", "页面找不到!");
}
if ((Integer)map.get("status") == 403) {
map.put("message", "未授权!");
}
return map;
}
}
提示信息发生了改变,变成了开发人员自定义的提示信息.
三.二 自定义视图解析和返回内容
将 MyErrorAttributes 去掉 @Component 组件注解
继承DefaultErrorViewResolver 解析视图类
@Component
public class MyErrorViewResolver extends DefaultErrorViewResolver {
/**
构造方法
*/
public MyErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
super(applicationContext, resourceProperties);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 可以根据状态 status, 自定义视图页面和 model
//不能直接修改 model
Map<String, Object> newMap = new HashMap<>();
for(Map.Entry<String,Object> entry:model.entrySet()){
newMap.put(entry.getKey(),entry.getValue());
}
newMap.put("findUser","两个蝴蝶飞");
// NOT_FOUND(404, "Not Found"),
if(HttpStatus.NOT_FOUND.equals(status)){
return new ModelAndView("/self/404.html",newMap);
}
// 500错误
// INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
if(HttpStatus.INTERNAL_SERVER_ERROR.equals(status)){
return new ModelAndView("/self/500.html",newMap);
}
//走一个默认的页面
return new ModelAndView("/static/error/500.html",newMap);
}
}
在 template 目录下, 创建 self , 后续创建 500.html, 404.html
500.html , 404.html 添加一个自定义的属性, 就是 findUser
四. 前后端处理自定义异常信息
现在的项目,都是前后端分离的项目,希望在出现异常时,返回的是一个 json格式的数据,并不是跳转到页面。
前端开发人员,拿到错误的信息之后,个性化进行处理.
去掉 MyErrorViewResolver 上面的 @Component 注解
同时,去掉 自定义的 静态异常和动态异常信息, 将 error 文件夹重命名为 error2
四.一 定义异常信息
四.一.一 统一返回结果 OutputResult
@Data
public class OutputResult implements Serializable {
/**
* @param code 响应代码
* @param message 响应信息
* @param data 响应的数据
*/
private Integer code;
private String message;
private Map<String,Object> data=new HashMap<String,Object>();
/**
* 构造方法 私有。 避免外部构造
*/
private OutputResult(){
}
/**
* 成功
* @return
*/
public static OutputResult fail(){
OutputResult outputResult=new OutputResult();
outputResult.code=500;
outputResult.message="失败";
return outputResult;
}
/**
* 成功
* @return
*/
public static OutputResult fail(String message){
OutputResult outputResult=new OutputResult();
outputResult.code=500;
outputResult.message=message;
return outputResult;
}
/**
* 成功
* @return
*/
public static OutputResult success(){
OutputResult outputResult=new OutputResult();
outputResult.code=200;
outputResult.message="成功";
return outputResult;
}
/**
* 成功
* @param data 要响应的数据
* @return
*/
public static OutputResult success(Object data){
OutputResult outputResult=new OutputResult();
outputResult.code=200;
outputResult.message="成功";
outputResult.data.put("result",data);
return outputResult;
}
}
四.一.二 自定义的异常类 BusinessException
public class BusinessException extends Exception {
String message="";
public BusinessException(String message){
super(message);
this.message=message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
}
四.一.三 Controller 层异常方法 ExceptionController
@Controller
public class ExceptionController {
@RequestMapping("/")
public String index(){
return "index";
}
/**
* 会出现除 0异常
* @date 2021/11/9 20:54
* @author zk_yjl
* @param
* @return java.lang.String
*/
@RequestMapping("/div")
@ResponseBody
public OutputResult div(){
int result=10/0;
return OutputResult.success(result);
}
/**
* 会出现空指针异常
* @date 2021/11/9 20:54
* @author zk_yjl
* @param
* @return java.lang.String
*/
@RequestMapping("/npe")
@ResponseBody
public OutputResult npe(){
String str=null;
return OutputResult.success(str.length());
}
/**
* 会出现下标越界异常
* @date 2021/11/9 20:54
* @author zk_yjl
* @param
* @return java.lang.String
*/
@RequestMapping("/array")
@ResponseBody
public OutputResult array(){
String[] arr=new String[]{"岳泽霖","两个蝴蝶飞"};
return OutputResult.success(arr[arr.length]);
}
/**
* 会出现下业务型异常
* @date 2021/11/9 20:54
* @author zk_yjl
* @param
* @return java.lang.String
*/
@RequestMapping("/bus")
@ResponseBody
public OutputResult bus() throws BusinessException {
try{
int aa=10/0;
}catch (Exception e){
//去查询数据库
throw new BusinessException("查询数据库失败了");
}
return OutputResult.success("查询数据库成功");
}
/**
* 会出现下业务型异常
* @date 2021/11/9 20:54
* @author zk_yjl
* @param
* @return java.lang.String
*/
@RequestMapping("/other")
@ResponseBody
public OutputResult other() throws Exception {
//去查询数据库
try{
int aa=10/0;
}catch (Exception e){
//去查询数据库
throw new Exception("其他的异常信息");
}
return OutputResult.success("查询数据库成功");
}
}
进行调用时,
这样很不好看,也不方便管理.
四.二 自定义全局异常处理器 MyExceptionHandler
通过 @ExceptionHandler 注解,接收相应的异常信息
@RestControllerAdvice
public class MyExceptionHandler {
/**
* 处理空指向的异常信息
* @date 2021/11/10 11:52
* @author zk_yjl
* @param
* @return top.yueshushu.learn.response.OutputResult
*/
@ExceptionHandler(NullPointerException.class)
public OutputResult npeException(HttpServletRequest req, NullPointerException e){
return OutputResult.fail(e.getMessage());
}
/**
* 处理算术的异常信息
* @date 2021/11/10 11:52
* @author zk_yjl
* @param
* @return top.yueshushu.learn.response.OutputResult
*/
@ExceptionHandler(ArithmeticException.class)
public OutputResult ariException(HttpServletRequest req, ArithmeticException e){
return OutputResult.fail(e.getMessage());
}
/**
* 处理数组下标越界异常的异常信息
* @date 2021/11/10 11:52
* @author zk_yjl
* @param
* @return top.yueshushu.learn.response.OutputResult
*/
@ExceptionHandler(ArrayIndexOutOfBoundsException.class)
public OutputResult arrException(HttpServletRequest req, ArrayIndexOutOfBoundsException e){
return OutputResult.fail(e.getMessage());
}
/**
* 处理自定义的业务异常的异常信息
* @date 2021/11/10 11:52
* @author zk_yjl
* @param
* @return top.yueshushu.learn.response.OutputResult
*/
@ExceptionHandler(BusinessException.class)
public OutputResult busException(HttpServletRequest req, BusinessException e){
return OutputResult.fail(e.getMessage());
}
/**
* 处理其他的另外异常的异常信息
* @date 2021/11/10 11:52
* @author zk_yjl
* @param
* @return top.yueshushu.learn.response.OutputResult
*/
@ExceptionHandler(Exception.class)
public OutputResult otherException(HttpServletRequest req, Exception e){
return OutputResult.fail(e.getMessage());
}
}
这时候,再进行访问:
目前老蝴蝶的 状态码都是 500, 实际用的时候,会定义成不同的状态码, npe表示 300, 除 o表示 301,其它异常都有各自的状态码。
前端可以根据状态码,进行不同的展示信息
从而实现前后端分离下的全局异常处理机制.
五. 非抛出异常处理
在实际的业务中,会有 dao数据库层,servie层, controller 层,每一层都会有相应的异常抛出,也可以各自在自己的层捕获异常。
当异常被补获时,没有被抛出时,是什么情况呢? 一般不会在 dao层捕获异常.
五.一 Service 层
public interface ExceptionService {
/**
普通的两个异常信息,不补获
*/
public void div();
/**
普通的两个异常信息,内部进行补获
*/
public void tryDiv();
/**
往外抛出异常
*/
public void throwDivExcepiton() throws Exception;
public void throwBusExcepiton() throws Exception;
}
五.二 SerivceImpl 实现
@Service
public class ExceptionServiceImpl implements ExceptionService {
@Override
public void div() {
int a=10/0;
}
@Override
public void tryDiv() {
try{
int a=10/0;
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void throwDivExcepiton() throws Exception {
try{
int a=10/0;
}catch (Exception e){
throw e;
}
}
@Override
public void throwBusExcepiton() throws Exception {
throw new BusinessException("数据库查询出现异常");
}
}
五.三 Controller 层 ServiceExceptionController
@RestController
@RequestMapping("/exce")
public class ServiceExceptionController {
@Autowired
private ExceptionService exceptionService;
@GetMapping("/div")
public OutputResult div(){
exceptionService.div();
return OutputResult.success();
}
@GetMapping("/tryDiv")
public OutputResult tryDiv(){
exceptionService.tryDiv();
return OutputResult.success();
}
@GetMapping("/throwDivExcepiton")
public OutputResult throwDivExcepiton() throws Exception {
exceptionService.throwDivExcepiton();
return OutputResult.success();
}
@GetMapping("/throwBusExcepiton")
public OutputResult throwBusExcepiton() throws Exception {
exceptionService.throwBusExcepiton();
return OutputResult.success();
}
@GetMapping("/throwDivExcepiton2")
public OutputResult throwDivExcepiton2() {
try{
exceptionService.throwDivExcepiton();
return OutputResult.success();
}catch (Exception e){
e.printStackTrace();
return OutputResult.success("内部补获异常");
}
}
@GetMapping("/throwBusExcepiton2")
public OutputResult throwBusExcepiton2() {
try{
exceptionService.throwBusExcepiton();
return OutputResult.success();
}catch (Exception e){
e.printStackTrace();
return OutputResult.success("内部补获异常");
}
}
/*
* 异常处理,
* 出现的异常,没有显式的捕获,都会接收到。如果被try catch 到,即不往外抛出, 抛出到 controller 层的话, 是不会接收到这个异常的。
* 从而不触发异常机制信息.
* */
}
运行处理展示:
五.三.一 div 方法
service 层和 controller 层均没有 捕获异常
五.三.二 tryDiv
service 层捕获异常, controller 层没有捕获
没有接收到异常信息
五.三.三 throwDivExcepiton
service 层捕获异常,但往外抛出了, controller 层继续抛出
五.三.四 throwBusExcepiton
service 层抛出异常, controller 层继续抛出
五.三.五 throwDivExcepiton2
service 层捕获异常,但继续抛出异常, controller 捕获异常
五.三.六 throwBusExcepiton2
service 层抛出异常, controller 捕获异常
总结: 异常捕获,是在 Controller 层再往上一层进行处理的, 如果 Controller 层抛出异常,才能获取到,转换成 json的形式,如果 controller 层不抛出异常,是无法获取到的,也无法转换成 json的形式.
本章节的代码放置在 github 上:
https://github.com/yuejianli/springboot/tree/develop/SpringBoot_Exception
谢谢您的观看,如果喜欢,请关注我,再次感谢 !!!