《Effective Java》中提到,尽量使用判断代替异常,因为在jvm处理中,异常需要耗费更多性能,
但是在异常带来的便利性上,这些性能损耗是可以接受的…
异常分为两大类,一种是Checked Exception,另一种是Runtime Exception,以下简称 CE 和 RE;
对于CE,编译器要求必须置于_try/catch_中,此时,应该在_catch_中处理此异常,处理的原则是:可继续,则继续,无法继续,则强制终止;
如何判断可继续?产生错误的代码处理的逻辑与数据对后续代码影响不大,对整个业务的影响也不大,是可有可无的代码,或者在_catch_中有完美的替代解决方案(此时,应该尽量按照《Effective Java》所述,使用判断,代替异常);否则,皆是“无法继续”,应该强制退出;
常规处理方式有:日志记录,后备替补方案或者再次包装为RE抛出;
对于RE,非强制_try/catch_捕获,也一般不会手动_try/catch_捕获,如果对这种异常置之不理,则服务器会使用默认的处理方案,产生白页、500页面等;
对待此种异常,常规的处理方式有:在spring环境下,添加全局异常拦截器,判断处理;或者在无spring的web环境中,配置友好的错误页面;
不管是CE还是RE,都不应该直接打印堆栈,或者封装为RE时打破异常链,只使用错误提示封装新RE;这全部为错误做法。
此处的利用异常,主要说的是RE,对于CE,需要直接捕获或者抛出,如果滥用,会使代码中充斥着各种throws/try/catch,造成代码可读性差;
考虑如下代码:
Controller:
public class UserController {
private UserService userService;
@RequestMapping("/saveUser")
public String save(@RquestBody User user) {
int result = userService.saveUser(user);
if(result == 0) {
return "保存成功";
} else if(result == -1) {
return "名字校验错误";
} else if(result == -1) {
return "Email已被使用";
} else {
return "保存失败";
}
}
}
Service:
public class UserService {
private User userDao;
public int saveUser(User user) {
if(!checkName(user.getName())) {
// 名字校验不合格,返回-1
return -1;
}
int count = userDao.countByEmail(user.getEmail());
if(count > 0) {
// 用户Email已被使用,返回-2
return -2;
}
// 其他保存逻辑
...
return 0;
}
}
以上代码,首先controller接收前端的User对象,然后调用Service保存;
保存时,进行判断,如果数据有问题,返回错误码-1或者-2,如果没有问题,返回0,代表成功;
最后controller进行判断,如果为0,提示前端成功,否则,皆以不同的提示语提示前端处理失败。
可以是可以,但是不是感觉不是很好?如果Service还需要返回其他信息怎么办?下边还有一个DAO层,难道DAO层也用返回状态码来处理?
再看下边这种写法:
Controller:
public class UserController {
private UserService userService;
@RequestMapping("/saveUser")
public String save(@RquestBody User user) {
userService.saveUser(user);
return "保存成功";
}
}
Service:
public class UserService {
private User userDao;
public void saveUser(User user) {
if(!checkName(user.getName())) {
throw new ServiceException("名字校验不合格");
}
int count = userDao.countByEmail(user.getEmail());
if(count > 0) {
throw new ServiceException("用户Email已被使用");
}
// 其他保存逻辑
...
}
}
这种写法,既不用返回状态码,也可以再返回其他数据,调用方更不用if/else进行各种判断,因为被调用方,如果可以运行,则执行到底,如果无法运行,就抛出异常,终止了操作;此处ServiceException继承的肯定是RuntimeException;
使用这种方式,还有一个要处理的就是,需要添加异常拦截器,否则服务器默认处理为500错误;
拦截器最好为全局拦截器,所有的异常处理使用统一的处理,那拦截器中的代码逻辑为何?不管遇到什么异常,皆提示用户“服务器异常”吗?
spring的异常拦截器有很多种方式,以下是其中一种:
public class GlobalExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
}
这是比较简单的一种,只需要继承_HandlerExceptionResolver_,实现_resolveException()方法即可,参数中有request、response还有产生错误的exception,
最后返回一个_ModelAndView,实现重定向视图的效果;
不过,有一种简单的处理方式是,不在使用_ModelAndView_的重定向,而是直接使用response输出消息,实现如下:
String rmsg = ...
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.getOutputStream().write(JSON.toJSONString(rmsg).getBytes("UTF-8"));
return new ModelAndView();
rmsg_是根据_exception_处理之后的错误提示语,需要输出给前端;最后返回一个空的_ModelAndView;
以上产生的ServiceException,显然应该将其中的message提示给前端用户,就和第一种使用状态码的返回效果一样;
所以,异常拦截器中,需要进行判断:
if(ex instanceof ServiceException) {
rmsg = ex.getMessage();
}
rmsg代表了最终需要返回给前端的消息;
这样,就实现了和第一种代码中一样的效果;
那其他的异常怎么办?要不就…
if(ex instanceof ServiceException) {
rmsg = ex.getMessage();
} else {
rmsg = "服务器异常!";
}
这种处理方式也是可行的!
但是一个ServiceException就能代表系统中所有的异常了吗?
比如说,封装的工具类中,有一个CE,需要进行try/catch处理,然后catch中需要再次抛出RE,那这个RE是啥?用ServiceException?还是直接用RuntimeException?
再者,需要在Controller中实现RESTful风格的处理(这里不讨论链接的生成),RESTful返回时需要添加额外的HTTP状态码,那怎么处理?RE实现类再加个字段?像这样:
throw new ServiceException("名字不合法", 400);
400是HTTP状态码,意为:bad request;
如果你在Service层中,写了这样的代码,那我估摸着,你的IT生涯也差不多到头了…
所以,现在的需求变成了:
所有的异常扩展自RE这是毫无疑问的,相似的还有spring框架封装的异常类,都是扩展自RE;
在我看来,异常应该分为两种:
1、业务异常,类似于ServiceException,作用是中断程序,并将提示语传递到用户交互层;
2、架构异常,这种异常通常是编码上的错误,或者通过判断之后觉得是无法提示给用户的、无法解决的、必须通过更改代码来解决的异常,这种异常一般以友好提示语返回给用户;
一级:
RuntimeException: 父类异常
二级:
BussinessException: 业务异常,此类及子类异常皆将提示语返回给前端用户,默认HTTP状态码400;
ArchException: 架构异常,此类及子类异常的提示语需记录用于排查错误,返回给前端友好提示语,默认状态码500;
三级:
此级用来细化二级,范围不固定,可灵活扩展;
BussinessException的子类:
ArchException的子类:
有一些异常适合在Controller层使用,有一些只适合在Service层使用,也有一些异常基本不会被使用,知道怎么区分不?
异常分类更详细了,异常拦截器也应该修改;
至少,要添加根据不同的异常类型,返回不同的HTTP状态码的逻辑!
挺简单,不写了;
有异常产生时,绝大多数是要打印日志的,那这个日志如何打印?在什么时机打印?
打印堆栈,肯定不行,System.out也肯定不行,较好的办法是用日志框架,基于SLF4J的任何实现;
考虑如下代码:
if(...) {
log.error("XXX不合法!");
throw new ServiceException("XXX不合法!");
}
能看出问题来吗?
代码出现了冗余,日志中的消息和异常中的消息内容基本一致,至少意思可能一致;即使使用变量引用消息内容,也感觉冗余…
不如将代码改成这样:
if(...) {
throw new ServiceException("XXX不合法!");
}
然后在异常拦截器中:
log.error(exception);
将日志的打印更改到了异常拦截器中,减少了冗余;
还有一个问题是,如果非要在发生问题的时候打印日志,然后异常拦截器又打印了一份,不是重复了吗?
这就需要提供灵活性,即:
在throw出异常时,默认是需要拦截器打印日志的;但是,可以通过传递一个true/false的参数来控制异常拦截器是否打印日志;
这样如何?