开发SpringBoot应用,往往需要定义全局的异常处理机制。在系统处理用户请求出现错误时,能够返回自定义的错误页面。
错误信息页面设计
错误页面可以采用freemarker的模板的形式(尽量在Java代码中处理业务逻辑,而将页面渲染尽量写在html或者html模板中)。Freemarker可在pom文件中引用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
这样就可以在src/main/resources/templates中创建showerr.ftl文件,显示错误信息
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8"></meta>
<title>访问错误</title>
</head>
<body>
<div class="showerr">
<img src="${contextPath}/img/error.jpg" alt="出错了"/>
<h1>oops,出错了,错误代码:<span>${code}</spa></h1>
<#escape msg as msg?html?replace('\n', '<br>')>
<div>${msg}</div>
</#escape>
</div>
</body>
</html>
这里用到了三个参数:
- contextPath:用于传入应用的contextPath,因为页面错误有可能是在各种路径下出现的,因此定义contextPath让浏览器能够通过绝对路径显示相关的图片
- code:显示http的错误代码。
- msg:显示具体的错误信息。
Controller异常处理
SpringBoot常见的一种异常,就是由后端的Controller通过throw Exception抛出的。查看Spring DispatcherServlet的代码。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
可见,如果出现异常会进入到processDispatchResult函数进行处理。最终会调用到ExceptionHandlerExceptionResolver的doResolveHandlerMethod方法,即
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
}
Throwable cause = exception.getCause();
if (cause != null) {
// Expose cause as provided argument as well
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
}
else {
// Otherwise, just the given exception as-is
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
}
}
catch (Throwable invocationEx) {
// Any other than the original exception is unintended here,
// probably an accident (e.g. failed assertion or the like).
if (invocationEx != exception && logger.isWarnEnabled()) {
logger.warn("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
}
// Continue with default processing of the original exception...
return null;
}
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
}
else {
ModelMap model = mavContainer.getModel();
HttpStatus status = mavContainer.getStatus();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
mav.setViewName(mavContainer.getViewName());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
}
}
注意第一行会查找对应的exceptionHandlerMethod,继续追踪其代码可以发现
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}
找到就是@ControllerAdvice的Bean。因此为了处理Controller抛出的异常,注解一个ControllerAdvice进行处理即可,即
@ControllerAdvice
@Slf4j
public class MyControllerExceptionHandler {
// handle exception thrown by controller
@ExceptionHandler(Exception.class)
public ModelAndView defaultExceptionHandler(Exception ex, WebRequest request) throws Exception{
log.error("error happens:", ex);
ModelAndView mav = new ModelAndView();
mav.setViewName("showerr");
String str = "未知错误";
CharArrayWriter cw = new CharArrayWriter();
try (PrintWriter w = new PrintWriter(cw)) {
ex.printStackTrace(w);
str = cw.toString();
}
mav.addObject("msg", str);
mav.addObject("code", 500);
mav.addObject("contextPath", request.getContextPath()); // pass the context path
return mav;
}
}
这里返回的modelAndView即为之前创建的错误页面的模板文件,系统会将相应的参数填在模板里,返回给前端显示。
404异常处理
从刚才的代码可以看到,@ControllerAdvice只适合处理已有的Controller抛出的异常。但是页面其它的异常,@ControllerAdvice就无法捕获了。常见的一种异常就是前端访问了一个不存在的页面,这时SpringBoot就会默认FallBack到/error上进行处理。因此,为了捕获这种异常,可以定义ErrorController。
// handle exceptions that mycontrollerExceptionHandler cannot handle, e.g. 404 errors
@Controller
@Slf4j
public class MyFallBackExceptionHandler implements ErrorController {
private static final String ERROR_PATH = "/error";
@RequestMapping(ERROR_PATH)
public ModelAndView handleError(HttpServletRequest request) {
Integer code = (Integer) request.getAttribute( RequestDispatcher.ERROR_STATUS_CODE);
Exception ex = (Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
String url = request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI).toString();
ModelAndView mav = new ModelAndView();
mav.setViewName("showerr");
String str = "访问"+url+"出错";
if(ex != null) {
CharArrayWriter cw = new CharArrayWriter();
try (PrintWriter w = new PrintWriter(cw)) {
ex.printStackTrace(w);
str = cw.toString();
}
log.error("error happens:", ex);
}
mav.addObject("msg", str);
mav.addObject("code", code);
mav.addObject("contextPath", request.getContextPath()); // pass the context path
return mav;
}
@Override
public String getErrorPath() {
return ERROR_PATH;
}
}
ErrorController可以处理ControllerAdvice无法捕获的异常,也可以处理ControllerAdvice自己抛出的异常。
前端自身的异常处理
前面两项只处理了后端出现错误时的页面异常显示。然而,如果浏览器前端无法访问到后端,那么自然后台再怎么渲染异常,前台也无法展现了。这个时候就要求前端自身也需要有异常处理机制。如果前端采用了Jquery,就可以用Jquery的ajaxError处理异常。
$(document).on({
ajaxError: function(event,xhr,options,exc) {
var status = xhr.status;
var msg = xhr.responseText;
var $body = $("body").empty();
// this js should be included by the html in the root directory
$("<img/>").attr("src", "img/error.jpg").attr("alt","出错了").appendTo($body);
$("<h1/>").text("oops,出错了").appendTo($body);
if(options.dataTypes.indexOf("html") >= 0) {
$("<div/>").html(msg).appendTo($body);
}
else {
$("<div/>").text(msg).appendTo($body);
}
}
});
采用上述三种方法,基本可以保证所有的页面异常都能处理到了。