参考资料
- SpringBoot异常处理机制-BasicErrorController与@ControllerAdvice
- Java开发从工作到原理–BasicErrorController统一异常处理
- 【spring boot】spring boot 处理异常
- SpringBoot一个请求的处理全过程
- @ControllerAdvice和ErrorPageRegistrar接口配置错误页面的问题
- SpringBoot全局异常处理
- Springboot异常处理只会@ControllerAdvice+@ExceptionHandler?还远远不够!
目录
- 前期准备
- 1.1 后台
- 1.2 错误页面跳转Controller
- 一. 静态资源文件的方式
- 1.1 在`/static/error/`下配置错误页面
- 1.2 在`/templates/error/`下配置错误页面
- 二. 实现ErrorController接口
- 三. 实现ErrorPageRegistrar接口
- 四. @ControllerAdvice + @ExceptionHandler注解
- 4.1 前台
- 4.2 全局异常捕获
- 4.3 Ajax请求异常效果
- 4.4 普通请求异常效果
- 五. @ControllerAdvice + @ExceptionHandler注解与其他方式共同使用
当系统出现异常时候,如404或500异常的时候,默认返回的错误页面通常非常简陋。用户也看不懂,这时候我们想通过一些手段,提示用户访问的资源不存在,或者告知系统异常。
前期准备
1.1 后台
- 通过运行时异常来触发错误页面跳转
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("/test22")
public class Test22Controller {
@GetMapping("/init")
public ModelAndView init() {
// 🤪运行时异常
int a = 1 / 0;
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("test22");
return modelAndView;
}
@PostMapping("/test")
public ResponseEntity<Void> test() {
// 🤪运行时异常
int a = 1 / 0;
return ResponseEntity.noContent().build();
}
}
1.2 错误页面跳转Controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("/error")
public class ExceptionPageController {
// 404异常
@GetMapping("/404error")
public ModelAndView init404Page() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error/500");
return modelAndView;
}
// 500异常
@GetMapping("/500error")
public ModelAndView init500Page() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error/500");
return modelAndView;
}
}
一. 静态资源文件的方式
当SpringBoot工程启动的时候,默认会去如下所示路径初始化存放静态异常页面
/META-INF/resources/error/
👉优先顺最高/resources/error/
/static/error/
/public/error/
👉优先顺最低
❗注意,默认配置下,静态页面的名称要和状态码相同。
即404异常的静态页面需命名为404.html
1.1 在/static/error/
下配置错误页面
可以看到只需创建error文件夹和对应的状态码.html页面,错误页面就配置成功
⏹ 效果
1.2 在/templates/error/
下配置错误页面
❗注意,此时/static/error/
下面还存在error页面
⏹ 效果
👉可以看到/templates/error/
下配置的错误页面覆盖了/static/error/
下的错误页面
二. 实现ErrorController接口
- ErrorController接口的优先级 > 静态资源文件的方式
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Controller
public class CustomErrorController implements ErrorController {
private final static String ERROR_PATH = "/error";
// 状态码和对应页面的map映射
private final static Map<Integer, String> codePageMap = new HashMap<>(){
{
put(HttpStatus.NOT_FOUND.value(), "/common/404_page");
put(HttpStatus.INTERNAL_SERVER_ERROR.value(), "/common/500_page");
}
};
@RequestMapping(value = ERROR_PATH, produces = MediaType.TEXT_HTML_VALUE)
public String errorView(HttpServletRequest request) {
// 从request对象中获取状态码
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
request.setAttribute("statusCode", "错误状态码:" + status);
if (ObjectUtils.isEmpty(status)) {
return this.getDefaultView();
}
// 若状态码包含在map映射中,就返回到指定的错误页面
Integer statusCode = Integer.valueOf(status.toString());
if (codePageMap.containsKey(statusCode)) {
return codePageMap.get(statusCode);
}
return this.getDefaultView();
}
// 默认的页面
private String getDefaultView() {
return codePageMap.get(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
⏹ 效果
❗可以看到,断点经过我们自定义的CustomErrorController类之后,跳转到了
templates/common/
中,我们自定义的404_page.html
页面中。
由此可以证明: ErrorController接口的实现类的优先顺 > /templates/error/
三. 实现ErrorPageRegistrar接口
ErrorPageRegistrar
接口的优先级 >ErrorController
接口
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
@Component
public class ErrorPageConfig implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
ErrorPage[] errorPages = {
// 若指定了静态的html页面,则该页面需要存储在 static/ 路径下
new ErrorPage(HttpStatus.NOT_FOUND, "/error/404.html"),
// 如果需要指定 templates/ 路径下的页面,则需要配合get请求的方式,通过ModelAndView进行跳转
new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/common/500error"),
};
registry.addErrorPages(errorPages);
}
}
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("/common")
public class ExceptionPageController1 {
@GetMapping("/500error")
public ModelAndView init500Page() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("common/500_page");
return modelAndView;
}
}
⏹ 效果
四. @ControllerAdvice + @ExceptionHandler注解
- 工作中大多使用这种方式
- Exception异常可能由普通的get请求触发,大多数情况下会由Ajax请求的get和post方法触发
4.1 前台
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<a th:href="@{/test22/init}">点击跳转页面</a>
<hr>
<button id="sendRequest">发送jQuery-Ajax请求</button>
</div>
<script th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script th:inline="javascript">
$("#sendRequest").click(function () {
$.ajax({
url: `/test22/test`,
type: 'POST',
data: JSON.stringify(null),
// 向服务器发送的数据类型
contentType: 'application/json;charset=utf-8',
success: function (data, status, xhr) {
console.log(data);
},
error(xhr, textStatus, errorMessage) {
// 若状态码为500,则跳转到系统异常页面
if (xhr.status == 500) {
location.href = `${location.origin}/error/500Page`;
}
}
});
});
</script>
</body>
</html>
4.2 全局异常捕获
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ControllerAdvice
public final class GlobalExceptionHandler {
@Resource
private HttpServletRequest request;
@Resource
private HttpServletResponse response;
@ExceptionHandler(Exception.class)
public ModelAndView handleException(Exception ex) {
/*
* 如果是Ajax引发的Exception异常的话,就将错误的信息返回给前台
* 然后在前台的error回调方法中访问后台的error接口,从而跳转到systemError页面
* */
String requestedWithHeader = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWithHeader)) {
// HttpServletResponse对象是java中的对象
// response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
// HttpStatus对象是Spring中的对象,指定错误的状态码,保证jquery的ajax走error方法
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
jsonView.setExtractValueFromSingleKeyModel(true);
jsonView.setModelKey("jsonKey");
ModelAndView mv = new ModelAndView(jsonView);
mv.addObject("jsonKey", Map.of("exception", ex.getMessage()));
return mv;
}
/*
* 重定向到指定的Controller,然后在Controller中,跳转到systemError页面
* */
String redirectPath = request.getContextPath() + "/error/500error";
RedirectView redirectView = new RedirectView(redirectPath);
return new ModelAndView(redirectView, Map.of("exception", ex.getMessage()));
}
}
4.3 Ajax请求异常效果
4.4 普通请求异常效果
五. @ControllerAdvice + @ExceptionHandler注解与其他方式共同使用
@ControllerAdvice
public final class GlobalExceptionHandler {
// 省略注入......
@ExceptionHandler(Exception.class)
public ModelAndView handleException(Exception ex) {
String requestedWithHeader = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWithHeader)) {
// 省略Ajax处理的情况......
}
// 仅返回ModelAndView对象,不在其中指定View视图的名称
ModelAndView modelView = new ModelAndView();
modelView.addObject("message", ex.getMessage());
return modelView;
}
}