SpringBoot的web完整的异常处理

开发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);
		}
	}
});

采用上述三种方法,基本可以保证所有的页面异常都能处理到了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值