SpringMVC全局异常处理 SpringMVC除了可以做URL映射和请求拦截外,还可以做全局异常的处理。全局异常处理可能我们平时比较少机会接触,但是每个项目都肯定会做这个处理。比如在上一间公司,是前后端分离的架构,所以后端只要有运行时异常就会报“系统异常,请稍后再试”。如果想要走上架构师的话,这个肯定是要学会的。
SpringMVC全局异常处理机制 首先,要知道全局异常处理,SpringMVC提供了两种方式:
实现HandlerExceptionResolver接口,自定义异常处理器。
使用HandlerExceptionResolver接口的子类,也就是SpringMVC提供的异常处理器。
所以,总得来说就两种方式,一种是自定义异常处理器,第二种是SpringMVC提供的。接下来先说SpringMVC提供的几种异常处理器的使用方式,然后再讲自定义异常处理器。
SpringMVC提供的异常处理器有哪些呢?我们可以直接看源码的类图。
可以看出有四种:
DefaultHandlerExceptionResolver,默认的异常处理器。根据各个不同类型的异常,返回不同的异常视图。
SimpleMappingExceptionResolver,简单映射异常处理器。通过配置异常类和view的关系来解析异常。
ResponseStatusExceptionResolver,状态码异常处理器。解析带有@ResponseStatus注释类型的异常。
ExceptionHandlerExceptionResolver,注解形式的异常处理器。对@ExceptionHandler注解的方法进行异常解析。
DefaultHandlerExceptionResolver 这个异常处理器是SprngMVC默认的一个处理器,处理一些常见的异常,比如:没有找到请求参数,参数类型转换异常,请求方式不支持等等。
接着我们看DefaultHandlerExceptionResolver类的doResolveException()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override @Nullable protected ModelAndView doResolveException (HttpServletRequest request, HttpServletResponse response,@Nullable Object handler, Exception ex) { try { if (ex instanceof HttpRequestMethodNotSupportedException) { return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, request, response, handler); } else if (ex instanceof HttpMediaTypeNotSupportedException) { return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, request, response, handler); } else if (ex instanceof HttpMediaTypeNotAcceptableException) { return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response, handler); } }catch (Exception handlerException) { if (logger.isWarnEnabled()) { logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception" , handlerException); } } return null ; }
通过if-else判断,判断继承什么异常就显示对应的错误码和错误提示信息。由此可以知道,处理一般有两步,一是设置响应码,二是在响应头设置异常信息。下面是MissingServletRequestPartException的处理的源码:
1 2 3 4 5 6 7 8 9 protected ModelAndView handleMissingServletRequestPartException (MissingServletRequestPartException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); return new ModelAndView(); } public static final int SC_BAD_REQUEST = 400 ;
为什么要存在这个异常处理器呢?
从框架的设计理念来看,这种公共的、常见的异常应该交给框架本身来完成,是一些必需处理的异常。比如参数类型转换异常,如果程序员不处理,还有框架提供默认的处理方式,不至于出现这种错误而无法排查 。
SimpleMappingExceptionResolver 这种异常处理器需要提前配置异常类和对应的view视图。一般用于使用JSP的项目中,出现异常则通过这个异常处理器跳转到指定的页面。
怎么配置?首先搭建JSP项目我就不浪费篇幅介绍了。首先要加载一个XML文件。
1 2 3 4 5 6 7 8 @SpringBootApplication @ImportResource("classpath:spring-config.xml") public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
然后在resources目录下,创建一个spring-config.xml文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean class ="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver" > <property name ="defaultErrorView" value ="err" /> <property name ="exceptionAttribute" value ="ex" /> <property name ="exceptionMappings" > <props > <prop key ="java.lang.ArrayIndexOutOfBoundsException" > err/arrayIndexOutOfBounds</prop > <prop key ="java.lang.NullPointerException" > err/nullPointer</prop > </props > </property > </bean > </beans >
然后在webapp也就是存放JSP页面的目录下,创建两个JSP页面。
arrayIndexOutOfBounds.jsp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>数组越界异常</title> </head> <body> <h1>数组越界异常</h1> <br> <%-- 打印异常到页面上 --%> <% Exception ex = (Exception)request.getAttribute("ex" ); %> <br> <div><%= ex.getMessage() %></div> <% ex.printStackTrace(new java.io.PrintWriter(out)); %> </body> </html>
nullPointer.jsp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>空指针异常</title> </head> <body> <h1>空指针异常</h1> <br> <%-- 打印异常到页面上 --%> <% Exception ex = (Exception)request.getAttribute("ex" ); %> <br> <div><%=ex.getMessage()%></div> <% ex.printStackTrace(new java.io.PrintWriter(out)); %> </body> </html>
接着创建两个Controller,分别抛出空指针异常和数组越界异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Controller @RequestMapping("/error") public class ErrController { @RequestMapping("/null") public String err () throws Exception { String str = null ; int length = str.length(); System.out.println(length); return "index" ; } @RequestMapping("/indexOut") public String indexOut () throws Exception { int [] nums = new int [2 ]; for (int i = 0 ; i < 3 ; i++) { nums[i] = i; System.out.println(nums[i]); } return "index" ; } }
启动项目后,我们发送两个请求,就可以看到:
通过上述例子可以看出,其实对于现在前后端分离的项目 来说,这种异常处理器已经不是很常用了 。
ResponseStatusExceptionResolver 这种异常处理器主要用于处理带有@ResponseStatus注释的异常。下面演示一下使用方式。
首先自定义异常类继承Exception,并且使用@ResponseStatus注解修饰。如下:
1 2 3 4 @ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "My defined Exception") public class DefinedException extends Exception {}
然后再在Controller层抛出此异常。如下:
1 2 3 4 5 6 7 8 9 10 11 12 @Controller @RequestMapping("/error") public class ErrController { @RequestMapping("/myException") public String ex (@RequestParam(name = "num") Integer num) throws Exception { if (num == 1 ) { throw new DefinedException(); } return "index" ; } }
然后启动项目,请求接口,可以看到如下信息:
使用这种异常处理器,需要自定义一个异常,一定要一直往上层抛出异常,如果不往上层抛出,在service或者dao层就try-catch处理掉的话,是不会触发的。
ExceptionHandlerExceptionResolver 这个异常处理器才是最重要的,也是最常用,最灵活的,因为是使用注解。首先我们还是简单地演示一下怎么使用:
首先需要定义一个全局的异常处理器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BaseException.class) public ErrorInfo errorHandler (HttpServletRequest req, BaseException e) throws Exception { ErrorInfo r = new ErrorInfo(); r.setMessage(e.getMessage()); r.setCode(ErrorInfo.ERROR); r.setUrl(req.getRequestURL().toString()); return r; } }
然后我们自定义一个自定义异常类BaseException:
1 2 3 4 5 public class BaseException extends Exception { public BaseException (String message) { super (message); } }
然后在Controller层定义一个方法测试:
1 2 3 4 5 6 7 8 @Controller @RequestMapping("/error") public class ErrController { @RequestMapping("/base") public String base () throws BaseException { throw new BaseException("系统异常,请稍后重试。" ); } }
老规矩,启动项目,请求接口可以看到结果:
你也可以不自定义异常BaseException,而直接拦截常见的各种异常都可以。所以这是一个非常灵活的异常处理器。你也可以做跳转页面,返回ModelAndView即可(以免篇幅过长就不演示了,哈哈)。
小结 经过以上的演示后我们学习了SpringMVC四种异常处理器的工作机制,最后这种作为程序员我觉得是必须掌握的,前面的简单映射异常处理器和状态映射处理器可以选择性掌握,默认的异常处理器了解即可。
那这么多异常处理器,究竟是如何工作的呢?为什么是设计一个接口,下面有一个抽象类加上四个实现子类呢?接下来我们通过源码分析来揭开谜底!
源码分析 源码分析从哪里入手呢?在SpringMVC中,其实你想都不用想,肯定在DispatcherServlet类里。经过我顺藤摸瓜,我定位在了processHandlerException()方法。怎么定位的呢?其实很简单,看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void processDispatchResult (HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false ; if (exception != null ) { if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered" , exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null ); mv = processHandlerException(request, response, handler, exception); } } }
processHandlerException() 就是这个直接的一个if-else判断,那个processHandlerException()方法又是怎么处理的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Nullable protected ModelAndView processHandlerException (HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { ModelAndView exMv = null ; if (this .handlerExceptionResolvers != null ) { for (HandlerExceptionResolver handlerExceptionResolver : this .handlerExceptionResolvers) { exMv = handlerExceptionResolver.resolveException(request, response, handler, ex); if (exMv != null ) { break ; } } } if (exMv != null ) { if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null ; } if (!exMv.hasView()) { String defaultViewName = getDefaultViewName(request); if (defaultViewName != null ) { exMv.setViewName(defaultViewName); } } return exMv; } throw ex; }
这不就是责任链模式吗!提前加载异常处理器到handlerExceptionResolvers集合中,然后遍历去执行,能处理就处理,不能处理就跳到下一个异常处理器处理。
那接下来我们就有一个问题了,handlerExceptionResolvers集合是怎么加载异常处理器的?这个问题很简单,就是使用DispatcherServlet.properties配置文件。这个文件真的很重要!!!
1 2 3 org.springframework.web.servlet.HandlerExceptionResolver =org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\ org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
默认是加载以上三种异常处理器到集合中,所以只要带有@ControllerAdvice、@ExceptionHandler、@ResponseStatus注解的都会被扫描。SimpleMappingExceptionResolver则是通过xml文件(当然也可以使用@Configuration)去配置。
resolveException() 其实在resolveException()处理异常的方法中,还使用了模板模式。
1 2 3 4 5 6 7 8 9 10 11 @Override @Nullable public ModelAndView resolveException (HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { prepareResponse(ex, response); ModelAndView result = doResolveException(request, response, handler, ex); }
抽象方法doResolveException(),由子类实现。
1 2 3 @Nullable protected abstract ModelAndView doResolveException (HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) ;
怎么识别模板方法,其实很简单,只要看到抽象类,有个具体方法里面调用了抽象方法,那很大可能就是模板模式。抽象方法就是模板方法,由子类实现。
子类我们都知道就是那四个异常处理器实现类了。
总结 用流程图概括一下:
经过以上的学习后,我们知道只需要把异常处理器加到集合中,就可以执行。所以我们可以使用直接实现HandlerExceptionResolver接口的方式来实现异常处理器。
实现HandlerExceptionResolver接口实现全局异常处理 首先自定一个异常类MyException。
1 2 3 4 5 public class MyException extends Exception { public MyException (String message) { super (message); } }
然后实现HandlerExceptionResolver接口定义一个异常处理器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component public class MyExceptionHandler implements HandlerExceptionResolver { @Override public ModelAndView resolveException (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (ex instanceof MyException) { response.setContentType("text/html;charset=utf-8" ); response.getWriter().println(ex.getMessage()); return null ; } } catch (IOException e) { e.printStackTrace(); } return null ; } }
然后在Controller层定义一个方法测试:
1 2 3 4 5 6 7 8 9 @Controller @RequestMapping("/error") public class ErrController { @RequestMapping("/myEx") public String myEx () throws MyException { System.out.println("执行myEx()" ); throw new MyException("自定义异常提示信息" ); } }
启动项目,请求接口,我们可以看到:
最后说几句 以上就是我对于SpringMVC全局异常处理机制的理解。更多的java技术分享,可以关注我的公众号“java技术爱好者 ”,后续会不断更新。