前言
异常处理,在每个项目中都不可避免,本文就来探讨一下web项目中的异常处理。
异常处理说到底就两个目标:
一、出异常后能保持用户体验友好
二、开发人员处理方便,最好不用去关心具体的异常处理细节。
本文探讨的异常处理基于web框架Spring MVC,因为这是当前项目的实际应用情况,所以就用它来抛砖引玉。
网上关于Spring MVC的异常处理文章很多,但都是基于框架技术本身,并没有结合具体的业务场景,本次要探讨的就是在具体的业务场景下如何用,又如何进行扩展。
异常处理回顾
在正式探讨之前先来回顾一下我所经历的几个异常处理的阶段。
不管如何处理,全局的异常肯定都是要拦截的,当Controller出现异常或有未捕获的异常时都会统一跳转到error页面(后面会讲到当是restful服务不能返回页面时又该如何处理),避免用户直接看到异常堆栈类信息的不友好页面。
全局拦截都是一锅端到error页了,体验肯定是不友好的,所以就必需要局部处理异常,这里我们以最典型的用户登录场景来做示例。
分析:
- 登录成功,跳转到成功页面,如首页
- 登录失败,仍跳回到登录页,显示错误信息,主要是这一步的处理,即表单提交不成功仍需要回到表单提交页
- Controller层等出现错误(代码不规范等),跳转到错误页 这部分统一拦截了这里不再做说明
假设条件:
1.已经自定义了异常类LoginException
:
public class LoginException extends RuntimeException {
private String errorCode;
private String errorMsg;
public LoginException(String code, String msg) {
super(msg);
this.errorCode = code;
this.errorMsg = msg;
}
}
2.登录业务层方法代码:
public User login(String username, String password) {
User user = userDao.getUser(username, password);
if (user == null) {
throw new LoginException("10001", "用户不存在或密码错误");
}
return user;
}
原始方式
最直接的就是在Controller
中try catch了,简单明了,代码如下:
@PostMapping("login")
public String login(String username, String password, Model model) {
try {
User user = userService.login(username, password);
model.addAttribute("user", user);
//.....
} catch (LoginException e) {
// log ...
// .....
model.addAttribute("message", e.getErrorMsg());
return "login";
} catch (Exception e) {
// log ...
// .....
model.addAttribute("message", "登录出现错误");
return "login";
}
return "index";
}
看起来很直观,也达到了需求,但缺点也很明显:
1.代码冗余繁琐,异常处理的代码比业务处理的代码还多
2.每个Controller方法都需要这样捕获处理,对开发人员不友好
业务模板模式
定义一套业务模板,用匿名实现类的方式在模板外围捕获异常,大致逻辑如下。
定义业务模板BizTemplate
,用来捕获异常,注意参数BizExecutor
以及catch中的将事务状态设为回滚:
public class BizTemplate {
private static final Logger LOGGER = LoggerFactory.getLogger(BizTemplate.class);
public void execute(BizResult result, BizExecutor executor) {
try {
executor.execute();
} catch (BizException e) {
LOGGER.error("出现异常", e);
result.setSuccess(false);
result.setCode(e.getErrorCode());
result.setMessage(e.getErrorMsg());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
} catch (Exception e) {
LOGGER.error("出现异常", e);
result.setSuccess(false);
result.setCode(e.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
执行器接口BizExecutor
,就一个执行方法:
public interface BizExecutor {
void execute();
}
最后在业务层使用:
public BizResult login(String username, String password) {
BizResult<User> result = new BizResult<User>(true);
bizTemplate.execute(result, new BizExecutor() {
@Override
public void execute() {
//执行业务代码
//User user = ....
//result.setData(user);
}
});
return result;
}
在Controller
中处理异常信息:
@PostMapping("login")
public String login(String username, String password, Model model) {
BizResult result = userService.login(username, password);
if (result.isSuccess()) {
//...
return "index";
} else {
//....
return "login";
}
}
优点:
1.异常完全被捕获,并可以根据需要转换成提示信息
2.开发者不需要关心异常的捕获及处理,只需要在匿名实现类中实现业务逻辑即可。
缺点:
1.业务方法内必须要使用bizTemplate
模板并用匿名实现类的方法来写业务逻辑,且返回结果必须是BizResult
或它的子类
2.对于每个业务方法的调用都需要判断result的isSuccess
,如果嵌套调用会显繁琐
3.嵌套调用时因为每个调用异常都已被捕获,事务已被设置为回滚,需要人为判断isSuccess
后再决定业务逻辑是否往下走,否则会出错
4.嵌套方式下失去了抛异常的方便与优雅,杜绝嵌套调用实际业务中可能很难满足
拦截器模式
鉴于上面的模板方式限定了返回结果及需要匿名实现类比较麻烦,改用拦截器来统一拦截异常。
拦截器主要代码,简单处理了嵌套问题,当在嵌套内时直接往外抛异常,没有嵌套或最外层将异常信息封装在RunBinder
内:
@Around("serviceAnnotation() || serviceName() || aspectRunBinder()")
public Object around(ProceedingJoinPoint pjp) {
AtomicInteger ai = methodHierarchy.get();
if (ai == null) {
ai = new AtomicInteger(1);
methodHierarchy.set(ai);
} else {
ai.incrementAndGet();
}
//返回结果
Object result = null;
try {
//此处调用业务方法
result = pjp.proceed();
} catch (BizException bizException) {
if (ai.get() > 1) {
throw bizException;
}
RunBinderTransactionAspectSupport.setRollbackOnly();
RunBinder.addError(bizException);
//log....
} catch (Throwable throwable) {
if (ai.get() > 1) {
throw new RuntimeException(throwable);
}
RunBinderTransactionAspectSupport.setRollbackOnly();
RunBinder.addError("UN_KNOWN_EXCEPTION", "未知异常");
//log....
} finally {
if (ai.decrementAndGet() == 0) {
methodHierarchy.remove();
}
}
return result;
}
Controller
使用:
@PostMapping("login")
public String login(String username, String password, Model model) {
User user = userService.login(username, password);
if (RunBinder.hasErrors()) {
//...
return "login";
} else {
//....
return "index";
}
}
优点:
1.解决了对返回值的限定和需要匿名实现类的问题
2.处理了嵌套调用问题
缺点:
1.Controller层还是需要if (RunBinder.hasErrors())
进行判断对结果进行处理
2.当项目内有Restful接口(返回json不需要页面)时,不判断异常会被吃掉,判断显得多余(但因为异常已被拦截器捕获无法统一处理了)
3.不能做到强制性的出错或提示,当项目组不在一个团队时,很多开发人员会不知道需要结合RunBinder
来判断,导致异常被吃掉
以上是对几种异常处理方式的总结,可以看到开发人员多少都要对返回的异常信息做判断处理,这一点上还是没有达到理想状态的,但它们也都有个好处就是跟具体的MVC框架没有强的耦合性。
但是在实际的项目开发中,更换MVC框架几乎是不可能发生的事情,所以处理的方便性其实要高于这个耦合性,特别是目前微服务的流行大量接口都没有页面只返回json数据,上面的处理方式就更显麻烦。
后面我们会讨论在使用Spring MVC的场景下,怎样结合Spring MVC来更优雅的实现异常处理,进一步解放开发人员。