SpringMvc 异常全局解读
异常处理思路
首先来看一下在springmvc中,异常处理的思路
如上图所示,系统的dao、service、controller出现异常都通过throws Exception向上抛出,最后由springmvc前端控制器交由异常处理器进行异常处理。springmvc提供全局异常处理器(一个系统只有一个异常处理器)进行统一异常处理。明白了springmvc中的异常处理机制,下面就开始分析springmvc中的异常处理。
SpringMVC 提供了 3 种异常处理方法:
使用 @ExceptionHandler 注解实现异常处理
简单异常处理器 SimpleMappingExceptionResolver
实现异常处理接口 HandlerExceptionResolver,自定义异常处理器
通过比较,实现异常处理接口 HandlerExceptionResolver 是最合适的,可以给定更多的异常信息,可一自定义异常显示页面等,下面介绍使用这种方式处理异常。
- 定义自己的异常
实现自定义异常是为了能更灵活的添加一些默认异常没有的功能,例如制定异常显示的页面,如果需要更多功能,自行扩展。
package com.xtuer.exception;
/**
* 定义应用程序级别统一的异常,能够指定异常显示的页面,即 error view name。
*/
public class ApplicationException extends RuntimeException {
private String errorViewName = null;
public ApplicationException(String message) {
this(message, null);
}
public ApplicationException(String message, String errorViewName) {
super(message);
this.errorViewName = errorViewName;
}
public String getErrorViewName() {
return errorViewName;
}
}
- 异常处理类
异常没有被我们捕获,被抛给 Spring 后,Spring 会调用此异常处理类把异常默认显示在 error.htm,errorViewName 可以在上面的 ApplicationException 里定义,如果是 AJAX 访问,异常信息则以 AJAX 的方式返回给客户端,而不是显示在错误页面。
package com.xtuer.exception;
import com.alibaba.fastjson.JSON;
import com.xtuer.bean.Result;
import com.xtuer.controller.UriView;
import com.xtuer.util.NetUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ui.ModelMap;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* SpringMvc 使用的异常处理类,统一处理未捕捉的异常:
* 1. 当 AJAX 请求时发生异常,返回 JSON 格式的错误信息
* 2. 非 AJAX 请求时发生异常,错误信息显示到 HTML 网页
*/
public final class XHandlerExceptionResolver implements HandlerExceptionResolver {
private static Logger logger = LoggerFactory.getLogger(XHandlerExceptionResolver.class);
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
String error = ex.getMessage();
String stack = ExceptionUtils.getStackTrace(ex);
// 异常记录到日志里,对于运维非常重要
logger.warn(error);
logger.warn(stack);
return NetUtils.useAjax(request) ? handleAjaxException(response, error, stack)
: handleNonAjaxException(ex, error, stack);
}
/**
* 处理 AJAX 请求时的异常: 把异常信息使用 Result 格式化为 JSON 格式,以 AJAX 的方式写入到响应数据中。
*
* @param response HttpServletResponse 对象
* @param error 异常的描述信息
* @param stack 异常的堆栈信息
* @return 返回 null,这时 SpringMvc 不会去查找 view,会根据 response 中的信息进行响应。
*/
private ModelAndView handleAjaxException(HttpServletResponse response, String error, String stack) {
Result<String> result = Result.fail(error, stack);
NetUtils.ajaxResponse(response, JSON.toJSONString(result));
return null;
}
/**
* 处理非 AJAX 请求时的异常:
* 1. 如果异常是 ApplicationException 类型的
* A. 如果指定了 errorViewName,则在 errorViewName 对应的网页上显示异常
* B. 如果没有指定 errorViewName,则在显示异常的默认页面显示异常
* 2. 非 ApplicationException 的异常,即其它所有类型的异常,则在显示异常的默认页面显示异常
*
* @param ex 异常对象
* @param error 异常的描述信息
* @param stack 异常的堆栈信息
* @return ModelAndView 对象,给定了 view 和异常信息
*/
private ModelAndView handleNonAjaxException(Exception ex, String error, String stack) {
String errorViewName = "error.htm"; // 显示错误的默认页面
// 如果是我们定义的异常 ApplicationException,则取得它的异常显示页面的 view name
if (ex instanceof ApplicationException) {
ApplicationException appEx = (ApplicationException) ex;
errorViewName = (appEx.getErrorViewName() == null) ? errorViewName : appEx.getErrorViewName();
}
ModelMap model = new ModelMap();
model.addAttribute("error", error); // 异常信息
model.addAttribute("detail", stack); // 异常堆栈
return new ModelAndView(errorViewName, model);
}
}
package com.xtuer.util;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class NetUtils {
private static Logger logger = LoggerFactory.getLogger(NetUtils.class);
/**
* 判断请求是否 AJAX 请求
*
* @param request HttpServletRequest 对象
* @return 如果是 AJAX 请求则返回 true,否则返回 false
*/
public static boolean useAjax(HttpServletRequest request) {
return "XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"));
}
/**
* 使用 AJAX 的方式把响应写入 response 中
*
* @param response HttpServletResponse 对象,用于写入请求的响应
* @param data 响应的数据
*/
public static void ajaxResponse(HttpServletResponse response, String data) {
response.setContentType("application/json"); // 使用 ajax 的方式
response.setCharacterEncoding("UTF-8");
try {
// 写入数据到流里,刷新并关闭流
PrintWriter writer = response.getWriter();
writer.write(data);
writer.flush();
writer.close();
} catch (IOException ex) {
logger.warn(ExceptionUtils.getStackTrace(ex));
}
}
}
- 在 spring-mvc.xml 里注册异常处理器
<!--<bean class="com.xtuer.exception.XHandlerExceptionResolver"/>-->
<bean id="compositeExceptionResolver" class="org.springframework.web.servlet.handler.HandlerExceptionResolverComposite">
<property name="exceptionResolvers">
<list>
<bean class="com.xtuer.exception.XHandlerExceptionResolver"/>
</list>
</property>
<property name="order" value="0"/>
</bean>
<bean class="com.xtuer.exception.XHandlerExceptionResolver"/> 注册的异常处理器只能处理进入 Controller 中的方法体里抛出的异常,例如缺少参数,参数类型转换错误时发生异常则不会进入 Controller 的方法,这时抛出的异常则不能被这种方式注册的异常处理器处理。
如果需要所有的异常都能被我们的异常处理器处理,则需要按上面的方法使用 HandlerExceptionResolverComposite 来注册我们的异常处理器,并且 order 的值要足够小,order 越小优先级越高,因为 SpringMVC 有一个异常处理器链。
- 测试用的 Controller
@Controller
public class DemoController {
// http://localhost:8080/exception
@GetMapping("/exception")
public String exception() {
throw new RuntimeException("普通访问发生异常");
}
// http://localhost:8080/exception-ajax
@GetMapping("/exception-ajax")
@ResponseBody
public Result exceptionWhenAjax(Demo demo) {
System.out.println(JSON.toJSONString(demo));
throw new RuntimeException("AJAX 访问发生异常");
}
}
- 显示错误的页面 error.htm
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>错误</title>
</head>
<body>
错误: ${error!}<br>
<#if detail??>
<pre>错误细节:<br>${detail}</pre>
</#if>
</body>
</html>
- 测试
@ControllerAdvice,是Spring3.2提供的新注解,从名字上可以看出大体意思是控制器增强
package com.kaishengit.controller.exception;
@ControllerAdvice
public class ControllerExceptionHanlder {
@ExceptionHandler(IOException.class)
public String ioExceptionHandler() {
return "error/500";
}
}
package com.kaishengit.controller;
@Controller
public class HelloController {
@RequestMapping(value = "/hello",method = RequestMethod.GET)
public String hello() throws NullPointerException {
System.out.println("-----> hello,springMVC");
if (true){
throw new NotFoundException();
}
return "hello";
}
/**
* 处理这个Controller内部发生的运行时异常
*
* @return
*/
@ExceptionHandler(NotFoundException.class)
public String notFoundException() {
return "error/500";
}
}
import org.springframework.stereotype.Controller;
@Controller
public class HelloController {
@RequestMapping(value = "/hello",method = RequestMethod.GET)
public String hello() throws NullPointerException {
System.out.println("-----> hello,springMVC");
if (true){
throw new NotFoundException();
}
return "hello";
}
@ExceptionHandler(NotFoundException.class)
public String notFoundException() {
return "error/500";
}
}
错误页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
</head>
<body>
<h3>Sorry,Server Error!</h3>
</body>
</html>
测试:
Sorry,Server Error!
源码
- 看@ControllerAdvice的实现:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.web.bind.annotation;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
没什么特别之处,该注解使用@Component注解,这样的话当我们使用<context:component-scan>扫描时也能扫描到。
- 再一起看看官方提供的comment。
大致意思是:
- @ControllerAdvice是一个@Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法。
- Spring4之前,@ControllerAdvice在同一调度的Servlet中协助所有控制器。Spring4已经改变:@ControllerAdvice支持配置控制器的子集,而默认的行为仍然可以利用。
- 在Spring4中, @ControllerAdvice通过annotations(), basePackageClasses(), basePackages()方法定制用于选择控制器子集。
不过据经验之谈,只有配合@ExceptionHandler最有用,其它两个不常用。
♥ ==@ExceptionHandler和@ResponseStatus我们提到,如果单使用@ExceptionHandler,只能在当前Controller中处理异常。但当配合@ControllerAdvice一起使用的时候,就可以摆脱那个限制了。==
package com.kaishengit.controller.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.io.IOException;
@ControllerAdvice
public class ControllerExceptionHanlder {
@ExceptionHandler(IOException.class)
public String ioExceptionHandler() {
return "error/500";
}
}
Ajax
package com.kaishengit.controller.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class ControllerExceptionHanlder {
@ExceptionHandler(IOException.class)
@ResponseBody
public Map ioExceptionHandler() {
Map map = new HashMap();
map.put("status",500);
return map;
}
}
当我们访问http://localhost:8080/的时候会抛出ArrayIndexOutOfBoundsException异常,这时候定义在@ControllerAdvice中的@ExceptionHandler就开始发挥作用了。
如果我们想定义一个处理全局的异常
package com.somnus.advice;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler({ Exception.class })
@ResponseBody
public String handException(HttpServletRequest request ,Exception e) throws Exception {
e.printStackTrace();
return e.getMessage();
}
}
乍一眼看上去毫无问题,但这里有一个纰漏,由于Exception是异常的父类,如果你的项目中出现过在自定义异常中使用@ResponseStatus的情况,你的初衷是碰到那个自定义异常响应对应的状态码,而这个控制器增强处理类,会首先进入,并直接返回,不会再有@ResponseStatus的事情了,这里为了解决这种纰漏,我提供了一种解决方式。
package com.somnus.advice;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler({ Exception.class })
@ResponseBody
public String handException(HttpServletRequest request ,Exception e) throws Exception {
e.printStackTrace();
//If the exception is annotated with @ResponseStatus rethrow it and let
// the framework handle it - like the OrderNotFoundException example
// at the start of this post.
// AnnotationUtils is a Spring Framework utility class.
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null){
throw e;
}
// Otherwise setup and send the user to a default error-view.
/*ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", request.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;*/
return e.getMessage();
}
}
如果碰到了某个自定义异常加上了@ResponseStatus,就继续抛出,这样就不会让自定义异常失去加上@ResponseStatus的初衷。