Java异常良好实践

3,975 阅读6分钟

前言

字节码层面学习Java异常底层原理一文中,对Java的异常体系和异常原理进行了学习,但是最后并没有总结出异常实践,本文将结合实际的开发经验,以及阿里巴巴开发手册中对异常的一些约束,给出一个具有一定参考意义的异常良好实践,希望能够帮助大家。

但是有一点需要注意,所谓的良好实践,也只是在特定场景下的一种异常使用方式,大多数时候都难以具有普适性,举个简单例子,可能你们公司的大佬告诉你不要捕获Exception,不要捕获Throwable,但是实际情况我们就是需要有一个对异常的兜底策略,而往往这个策略就是需要捕获一个最顶层的异常基类,你去看一些大名鼎鼎的开源项目,例如MyBatisTomcat等,别人一样在一些地方捕获了Exception,捕获了Throwable,你能说别人写得差吗,不能的吧。

所以有没有一种可能,叫你不要捕获Exception,不要捕获Throwable的大佬,很久没写过代码了。

正文

一. 不要使用异常做流程控制

业务代码应该和异常处理代码解耦,不应该将异常的捕获和处理用在业务代码的流程控制中,比如下面的代码就是十足的坏味道。

public class BadTasteTest {

    @Test
    public void 异常控制业务流程() {
        Demo demo = null;

        // 一系列逻辑
        ......

        try {
            demo.report();
        } catch (NullPointerException e) {
            doWhenDemoNull();
        }
    }

    private void doWhenDemoNull() {}

    private static class Demo {
        public void report() {}
    }

}

要修复上面的坏味道,就是把异常从业务流程中去除,如下所示。

@Test
public void 异常控制业务流程_修复() {
    Demo demo = null;

    // 一系列逻辑
    ......

    if (demo == null) {
        doWhenDemoNull();
    } else {
        demo.report();
    }
}

二. 不要滥用校验异常

校验异常能够强迫应用程序去处理,要么使用try-catch捕获,要么使用throw向上层抛出,这增强了代码的健壮性,但是校验异常也有许多缺点,如下所示。

  1. 无法做到无感。调用一个会抛出校验异常的方法,我们会被强迫去判断抛出的异常是需要处理还是需要再往上层抛出,如果有大量方法滥用校验异常,每个方法我们都需要判断,增加我们的编程负担;
  2. Lambda表达式支持不好。主要体现于如果在Lambda表达式中调用了会抛出校验异常的方法,那么只能在调用方法的地方进行try-catch,而不能向上层抛出。

三. RuntimeException尽量不捕获

这里的RuntimeException指的是类似于NullPointerExceptionClassCastException这种预置的RuntimeException,这些运行时异常尽量不采用捕获的方式去处理,而是对可能会抛出这些运行时异常的代码进行预判断来防止异常抛出。

例如可能会抛出NullPointerException的地方,预先进行判空,如下所示。

if (result != null) {
    return result.data;
}

例如可能会抛出ClassCastException的地方,预先进行类型判断,如下所示。

if (obj instanceof Report) {
    return ((Report) obj).result;
}

四. 建议自定义RuntimeException异常

如果在应用程序中有自定义异常的需求,建议自定义异常继承于RuntimeException,此时我们实现的自定义异常就是运行时异常,属于非校验异常,因此不需要在代码中到处捕获或抛出,我们可以在能够处理这个自定义异常或者在异常处理中心统一进行处理。

当然,这么做的原因就是为了代码的整洁,如果我们自定义的异常真的十分重要,一定要处处都感知到这个自定义异常,那么还是可以继承于Exception

MyBatis中,所有的自定义异常都是RuntimeException

五. 多用try-with-resources来处理资源关闭

对于需要关闭资源的场景,尽量使用try-with-resources来实现,这么做的理由如下。

  1. 代码简洁。不需要自己在finally中写一大堆代码来关闭资源;
  2. 异常抑制try-with-resources帮我们实现了异常抑制的语法糖。

使用案例如下。

try (Connection connection = new Connection()) {
    connection.update();
}

六. 尽量不要在finally中抛出异常

因为直接在finally中抛出异常会发生异常屏蔽的问题,虽然通过改写代码能够解决异常屏蔽,但是写起来十分复杂,所以如果一定要在finally中抛出异常,可以选择使用try-with-resources

七. 一定要正确包装异常信息

当我们捕获一个特定异常并且需要将这个异常包装成另外一个异常时,一定不能丢掉被包装异常的信息。

如下是一个反例。

catch (SQLException e) {
    throw new MySQLException("Additional information " + e.getMessage());
}

抛出去的MySQLException丢失了原始异常SQLException的大部分信息,应该修改如下。

catch (SQLException e) {
    throw new MySQLException("Additional information ", e);
}

八. 不要泛滥的捕获Exception

Exception并非不可以捕获,但是不能在代码中过度的捕获Exception,因为一旦对当前代码块捕获Exception,将会丢失当前代码块会发生的异常信息,最终我们就不知道这个代码块会发生何种类型的异常了,最终只能在正真发生异常时通过日志去定位。

如下就是一个十足的捕获Exception的坏味道的代码。

public void controllerMethod() {
    try {
        serviceMethod();
    } catch (Exception e) {
        LOGGER.error("发生了异常", e);
    }
}

public void serviceMethod() {
    // 业务代码
    // 这里可能会抛出ServiceException
    ...

    try {
        daoMethod();
    } catch (Exception e) {
        LOGGER.error("发生了异常", e);
    }
}

private void daoMethod() {
    // 业务代码
    // 这里可能会抛出DaoException
    ......

    try {
        // 数据库操作
        ......
    } catch (Exception e) {
        LOGGER.error("发生了异常", e);
    } finally {
        // 关闭资源
        // 事务回滚
        ......
    }
}

可以进行如下改进。

public void controllerMethod() {
    try {
        serviceMethod();
    } catch (ServiceException e) {
        LOGGER.error("发生SERVICE了异常", e);
    } catch (Exception e) {
        LOGGER.error("发生了未知异常", e);
    }
}

public void serviceMethod() {
    // 业务代码
    // 这里可能会抛出ServiceException
    ...

    try {
        daoMethod();
    } catch (DaoException e) {
        LOGGER.error("发生了DAO层异常", e);
    }
}

private void daoMethod() {
    // 业务代码
    // 这里可能会抛出DaoException
    ......

    try {
        // 数据库操作
        ......
    } catch (SQLException e) {
        LOGGER.error("发生了SQL异常", e);
    } finally {
        // 关闭资源
        // 事务回滚
        ......
    }
}

九. 不要吞掉异常

意思就是不要捕获异常后什么事情都不做,就像下面这样。

try {
    // 业务代码
    .....
} catch(SQLException e) {
    LOGGER.error("发生了SQL异常", e);
} catch(Exception e) {
    // 什么事情都不做
}

十. 尽量不要捕获Throwable或者Error

主要就是尽量不要捕获Error,因为一旦发生Error,表明程序发生了程序无法修复的问题,这些问题就比如内存溢出问题,或者因为依赖问题导致的找不到某个方法某个字段等,我们需要做的就是让Error一路外抛,直到某一层能明确将这个Error暴露出来为止。

总结

请一定记住,所谓的异常良好实践,最终都会落到具体情况具体分析,如果在业务代码中处处都严格遵循异常的一些使用规则,那么最终写出来的代码,不一定是业务友好的代码。