项目开发过程中,经常会涉及到Exception的处理,我们项目使用JSF,所以在Unchecked Exception方面着实碰到了麻烦。
现在说一下我们项目的Exception处理框架,无论是checked exception or unchecked exception,我们都需要将其记录到LOG文件中,用log4j是个不错的选择。
Checked Excpetion Handling:
checked exception比较好处理,我的方案是,定义一两个用户自定义的exception, 比如ApplicationException or CustomException, 一般checked exception会分两种,一个是程序员感兴趣或者需要可以处理的exception, 比如有两个文件,一个加载不到就加载另一个,前者加载不到时,程序会抛出FileNotFoundException,那么程序员可以catch住,在catch块中加载另一个文件(假定这次不出问题)。另一种情况就是比如程序抛出SQLAccessException,那么可以不用理它,catch住后抛出自定义的exception。
我们的工程是这样:任何地方可能抛出exception的地方,只需要用Exception这一个catch clause抓住,然后在catch块中,记录它,并抛出我们自定义的exception,只在JSF的Backing Bean中做处理。
public String associateAppsAndModule(PageGroup pageGroup)
throws MyFleetTPOException {
String saveState = null;
try {
StringBuffer urlSb = new StringBuffer("http://localhost:8080/MyFleetTPO");
urlSb.append(TpoConstants.WS_ROOT).append(TpoConstants.WS_BIND_APP_MODULE);
saveState = ServiceUtil.getInstance().doPost(new URL(urlSb.toString()), pageGroup);
} catch (Exception e) { // actually here throws MalformedURLException, but we only catch Exception
// because in below line, log4j can show error stack with exact error type
logger.error(e.getLocalizedMessage(), e);
throw new MyFleetTPOException(e);
}
return saveState;
}
然后在Backing Bean中抓住并处理:
public String bindAppsAndModule(){
String bindPageApp = TpoConstants.SAVE_FAILED;
try {
bindPageApp = pageGroupingService.associateAppsAndModule(pageGroup);
} catch (MyFleetTPOException e) {
logger.error(e.getLocalizedMessage(), e);
}
return bindPageApp;
}
页面根据bindPageApp,显示消息,或干别的等等。
Unchecked Excpetion Handling:
对于unchecked exception,我们还需要把用户导航到一个公共的错误页面。在讲述unchecked exception handling内容之前,讲讲我的摸索经历吧。
- 一开始我的确想到了使用AOP的方式去处理,由于我想覆盖大部分java类方法抛出的异常,所以Spring AOP(只能对Service层做切面)就不足以完成这个功能,那么AspectJ就是一个必须要选择的工具了,它可以做到Domain Object的切面。
- 首先想到的是@AfterThrowing,但是这种方法只能将异常的信息输出到Log中,但是始终没办法实现页面的跳转。于是打算通过配置web.xml实现跳转:
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error.jsp</location>
</error-page>
但是也失败了,我猜想啊:可能是JSF的生命周期导致的,具体原因没有考证过。。。。。。
- 当时始终为拿不到当前请求request变量,为了跳过获得request,尝试了FacesContext.getCurrentInstance().getExternalContext.dispatch()方法,未果后,又希望通过AspectJ对FacesServlet.service() && args(request, response)做切面,希许可以拿到request,始终失败,后来才知道FacesServlet是一个final类,没办法对其proxy.
- 经过一番google,发现可以利用JSF的NavigationHandler去处理,但是前提是通过继承现有的JSF的ActionListnerImpl并重写processAction()方法。
public class ThrowableActionListener extends ActionListenerImpl {
private static final Logger logger = Logger.getLogger(ThrowableActionListener.class);
@Override
public void processAction(ActionEvent event) {
try {
super.processAction(event);
} catch (Throwable e) {
logger.error(e.getLocalizedMessage(), e);
FacesContext fc = FacesContext.getCurrentInstance(); fc.getApplication().getNavigationHandler().handleNavigation(fc, null, "exceptionNavigation"); fc.renderResponse();
}
}
}
同时需要在faces-config.xml中配置下:
<application>
<action-listener>
com.ge.energy.pgs.myfleet.tpo.common.ThrowableActionListener
</action-listener>
</application>
<navigation-rule>
<from-view-id>/*</from-view-id>
<navigation-case>
<from-outcome>exceptionNavigation</from-outcome>
<to-view-id>/pages/common/common_error.jsp</to-view-id>
</navigation-case>
</navigation-rule>
总结一下:由于FacesServlet是个final类,所以没办法使用子类来复写service()方法,但是对于struts的DispactchServlet,我们完全可以使用类似于这次的ThrowableActionListener的方法,轻而易举地在catch块中实现页面转向,log
更新于2016-11-20:
项目中使用SSM框架,DAL使用的是MyBatis自动生成的代码,**Mapper.xml里面写SQL,Service层组装数据,并做业务逻辑处理。任何地方都可以抓异常,无论是Checked 还是 Unchecked异常,都可以抓住后抛出RuntimeException, 这样Service 的方法签名就不需要throws Exception这样的声明了。
然后,所以的异常都会bubble up到Controller层,我们可以定义一个GlobalExceptionHandler统一处理,这样前端拿到的就是一个合规的JSON响应。(getTrace() 方法则是打印整个错误栈)
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler{
private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ResponseBody
@ExceptionHandler(value = { Exception.class, RuntimeException.class })
public BaseResponse<String> defaultErrorHandler(HttpServletResponse response, Exception ex) throws Exception {
BaseResponse<String> resp = new BaseResponse<>(RESPONSE_STATUS.FAIL);
logger.warn("抓到了一个异常, 以下是完整的出错路径", ex.getMessage());
logger.error(getTrace(ex));
resp.setMsg(ex.getMessage());
return resp;
}
private String getTrace(Exception e) {
StringWriter stringWriter= new StringWriter();
PrintWriter writer= new PrintWriter(stringWriter);
e.printStackTrace(writer);
StringBuffer buffer= stringWriter.getBuffer();
return buffer.toString();
}
}
对于多线程的处理,则分情况了,如果对于执行的是Runnable的方法,那么为了能抓住run()方法里面抛出的异常,就有几个注意点了:
1. 不能使用submit 方法提交线程到线程池,而只能使用execute()方法
2. 不能直接使用Executors.newFixedThreadPool(threadNum)创建线程池,因为这样就没办法抓到 run()方法抛出的异常,而是需要覆盖afterExecute方法
ThreadPoolExecutor exe = new ThreadPoolExecutor(2, 2,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()){
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println(t);
throw new RuntimeException(t);
}
};
如果执行的是Callable的方法,那么就没必要这么麻烦了,所有的异常都会在 future.get() 里面通过ExecutionException抛出来。
for (Future<List<String>> result : futureResults){
try {
while(result.isDone()){
List<String> pIdList = result.get();
reducedPersonIdList.addAll(pIdList);
}
} catch (Exception e) {
throw new RuntimeException("执行多线程匹配的时候,在获取Future.get()的时候发出异常:", e);
}
}