异常处理一直以来都是java中的一块重点难点内容,在项目中合理的处理异常一方面可以给用户带来良好的体验,另一方面也可以帮我们开发人员准确快速的定位问题,那么本篇blog就结合这两点总结一下我在Spring MVC框架的项目中的异常处理方式。
通常MVC框架都为我们提供了全局异常处理的相关API,Spring MVC自然也不例外,全局异常处理的好处是规范了异常处理的方式,在一个方法中统一的处理异常,避免了在每个方法中进行单独的异常捕获,省略了大量重复性代码,比如:try…catch、logger.error(ex)等这些代码(此处暂且不考虑用户体验)。Spring MVC提供了多种全局异常处理方式,我这里就不一一介绍了,具体可参考这篇博客:
“使用Spring MVC统一异常处理实战” http://cgs1999.iteye.com/blog/1547197
正如标题,我这里仅仅介绍一下我认为的best practice以及异常处理需要注意的细节,我选择的全局异常处理方式是通过自定义一个异常处理类并且继承HandlerExceptionResolver并声明为受spring管理的bean即可。回顾blog开头,一开始我提到了两点我认为的异常处理的根本目标:
这里我们暂时先不关注第一点,首先结合第二点看看我们自定义的HandlerExceptionResolver的代码:
package com.wl.exception;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import com.wl.util.JsonUtils;
/**
* 类概要: 全局异常处理类
* 创建时间: 2016-3-3 下午5:50:09
*
* @Project raito-framework(com.wl.exception)
* @author Wang Liang
* @version 1.0.0
*/
public class GlobalExceptionHandler implements HandlerExceptionResolver {
private static final Logger logger = Logger.getLogger(GlobalExceptionHandler.class);
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
// 日志记录异常message和stacktrace
logger.error(ex.getMessage(),ex);
Map map = new HashMap();
map.put("exception", ex.getMessage());
String header = request.getHeader("X-Requested-With");
// 处理异步请求
if (StringUtils.isNotBlank(header) && (header.equals("X-Requested-With") || header.equals("XMLHttpRequest"))) {
response.setContentType("application/json;charset=UTF-8");
PrintWriter pw = null;
try {
pw = response.getWriter();
pw.write(JsonUtils.toJson(map));
pw.flush();
pw.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (pw != null)
pw.close();
}
return null;
}
return new ModelAndView("error", map);
}
}
首先注意34行,我们在这里通过log4j记录了exception的message和stacktrace,注意尤其是stacktrace很重要,因为只有通过它我们才能准确的定位问题,否则我们无法知道最底层是哪个类的哪一行报错了(由于log4j的logger绑定了GlobalExceptionHandler这个类,所以logger仅能记录当前类的异常位置),所以这里注意要使用logger.error(Object message,Throwable t)而不是logger.error(Object message),只有前者才能记录到stacktrace,这一点很重要:
同时注意一下36行的小细节,尽管我给log里面写入了stack trace,但是给ajax请求的响应中我们就没必要加入stack trace了,因为它是给用户看的,所以这里我们通过ex.getMesage()
得到异常的message即可,这样也方便自定义异常的显示(在后面的2.3中会见到)。
下面在项目中加一行错误代码int a = 1/0;
我们来看一下log文件中记录的内容:
2016-03-04 12:26:57 ERROR [com.wl.exception.GlobalExceptionHandler] - / by zero
java.lang.ArithmeticException: / by zero
at com.wl.controller.UserController.login(UserController.java:110)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:777)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:706)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:943)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:857)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:224)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:169)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:927)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:987)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:579)
at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:1805)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
at java.lang.Thread.run(Thread.java:722)
log的第三行已经一目了然的找到了问题根源,显然这对于一个正在生产环境运行的线上项目来说至关重要。关于上述的第二点的目标暂且就说这么多,下面继续看我们自定义的GlobalExceptionHandler这个类。
注意看一下第37行,我们这里单独判断了异步请求的异常情况,因为这是必然的,异步请求往往我们需要给客户端返回json格式的数据,异常数据也是如此。首先看一下我们的判断方式是通过request.getHeader("X-Requested-With")
来进行判断的,之所以这样做,是因为异步请求的请求报文(request message)会比一般的同步请求多一项 X-Requested-With,通过firebug可以看到:
它的值是XMLHttpRequest,所以我们根据这一项即可判断是否为一个ajax请求了。40行~53行记录了ajax请求的响应方式,即以json格式给客户端写回数据即可。最后别忘记将这个类声明为受Spring管理的bean,我们在Spring MVC的配置文件中声明即可:
id="exceptionHandler" class="com.wl.exception.GlobalExceptionHandler"/>
到这里关于全局异常处理的使用方法和注意点差不多都已介绍完毕,那么接下来讨论一下上述的第一个目标,即:当出现异常时,给用户友好的界面展示与提示?
上面的问题正如这个小标题,但怎么样设计异常结构(何时捕获?何时抛出?)以及如何设计自定义异常才是合理的呢?其实有很多方式,关于这个话题的讨论网上也有许多,感兴趣的朋友可以继续去了解学习,这里就不一一赘述,结合主题,这里仅仅记录一下我在项目中使用的处理方式。
首先,在Spring MVC的三层架构中,Dao层和Service层始终向上层抛出异常,由Controller层统一处理。首先我们定义一个简单的自定义异常类,用于存放异常代码和异常信息:
package com.wl.exception;
/**
* 类概要: 自定义异常类
* 创建时间: 2016-3-4 下午5:44:43
*
* @Project raito-framework(com.wl.exception)
* @author Wang Liang
* @version 1.0.0
*/
public class CustomException extends RuntimeException {
private static final long serialVersionUID = 1L;
public CustomException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
// TODO Auto-generated constructor stub
}
public CustomException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public CustomException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public CustomException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}
比较传统的写法,我们的自定义异常仅仅重写了父类的4个构造方法,如上所述,在Controller层我们捕获异常,并通过这个自定义异常包装后再次抛出,最终交给Spring MVC的全局异常处理机制来处理即可,这样做的目的就是一开始提到的第一点:提示友好。下面贴出Controller中的一个测试方法的代码片段:
@RequestMapping(value = "/{username}/delete", method = RequestMethod.POST)
@ResponseBody
public Map<String,Object> delete(@PathVariable String username) {
Map<String, Object> map = new HashMap<String, Object>();
try {
int a = 1/0;
users.remove(username);
} catch (Exception e) {
throw new CustomException("删除失败,服务器内部错误!");
}
return map;
}
很简单,在第6行制造了一个异常(java.lang.ArithmeticException),然后我们再次捕获包装成了自定义异常,并给出了相应的提示信息,紧接着这个异常会被Spring MVC的全局异常处理机制捕获并处理,通过firebug可以看到给客户端返回的json数据:
同时,log4j也正确的记录了异常的stack trace:
2016-03-04 18:13:21 ERROR [com.wl.exception.GlobalExceptionHandler] - 删除失败,服务器内部错误!
com.wl.exception.CustomException: 删除失败,服务器内部错误!
at com.wl.controller.UserController.delete(UserController.java:106)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:777)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:706)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:943)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:641)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:224)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:169)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:927)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:987)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:579)
at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:1805)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
at java.lang.Thread.run(Thread.java:722)
这样就完美的实现了我们最初的两个目的:
当然,关于Spring MVC的异常处理还有许多值得研究的地方,我这里的使用方式也有许多不足,比如可以适当优化一下自定义异常类,通过定义一些枚举类型来保存异常的code和message而不是通过硬编码直接写入程序等等。
本篇blog到此就记录完毕,关于异常处理一直都是一个非常值得探究的话题,毕竟一个项目中的异常结构设计可以直接影响到项目质量的好坏和后期的维护成本,关于blog中提到的点如果有更好的实现方式或者我写错的地方欢迎批评指正,谢谢~ The End。