前言

异常处理,在每个项目中都不可避免,本文就来探讨一下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来更优雅的实现异常处理,进一步解放开发人员。

你可能感兴趣的内容
spring mvc整合velocity 收藏,13854 浏览
Spring mvc 文件上传 收藏,24902 浏览
0条评论

selfly

交流QQ群:32261424
Owner