Spring Boot工程中如何优雅地处理异常

转自: https://taosha.club/topic/61a5f5131260746bd823d770

对于异常处理, 有几个原则

使用异常而非返回码

在很久以前,许多语言都不支持异常。这些语言处理和汇报错误的手段都有限。你要么设置一个错误标识,要么返回给调用者检查的错误码。以下代码展示了这些手段

public class DeviceController {
    public void sendShutDown() {
        DeviceHandle handle = getHandle(DEV1);
        // Check the state of the device
        if (handle != DeviceHandle.INVALID) {
            // Save the device status to the record field
            retrieveDeviceRecord(handle);
            // If not suspended, shut down
            if (record.getStatus() != DEVICE_SUSPENDED) {
                pauseDevice(handle);
                clearDeviceWorkQueue(handle);
                closeDevice(handle);
            } else {
                logger.log("Device suspended. Unable to shut down");
            }
        } else {
            logger.log("Invalid handle for: " + DEV1.toString());
        }
    }
}

这类手段的问题在于,它们搞乱了调用者代码。调用者必须在调用之后即刻检查错误。不幸的是,这个步骤很容易被遗忘。所以,遇到错误时,最好抛出一个异常。调用代码很整洁,其逻辑不会被错误处理搞乱。

以下代码展示了在方法中遇到错误时抛出异常的情形

public class DeviceController {
    public void sendShutDown() {
        try {
            tryToShutDown();
        } catch (DeviceShutDownError e) {
            logger.log(e);
        }
    }

    private void tryToShutDown() throws DeviceShutDownError {
        DeviceHandle handle = getHandle(DEV1);
        DeviceRecord record = retrieveDeviceRecord(handle);
        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
    }
}

注意这段代码整洁了很多。这不仅关乎美观。这段代码更好,因为之前纠结的元素设备关闭算法和错误处理现在被隔离了。你可以查看其中任一元素,分别理解它。

给出异常发生的环境说明

你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所。在Java中,你可以从任何异常里得到堆栈踪迹(stack trace);然而,堆栈踪迹却无法告诉你该失败操作的初衷。

应创建信息充分的错误消息,并和异常一起传递出去。在消息中,包括失败的操作和失败类型。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。

别返回null值

要讨论错误处理,就一定要提及那些容易引发错误的做法。第一项就是返回null值。我不想去计算曾经见过多少几乎每行代码都在检查null值的应用程序。下面就是个例子:

public void registerItem (Item item){
    if (item != null) {
        ItemRegistry registry = peristentStore.getItemRegistry();
        if (registry != null) {
            Item existing = registry.getItem(item.getID());
            if (existing.getBillingPeriod().hasRetailOwner()) {
                existing.register(item);
            }
        }
    }
}

这种代码看似不坏,其实糟透了!返回null值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没检查null值,应用程序就会失控。

你有没有注意到,嵌套if语句的第二行没有检查null值?如果在运行时persistentStore为null会发生什么事?我们会在运行时得到一个NullPointerException异常,也许有人在代码顶端捕获这个异常,也可能没有捕获。两种情况都很糟糕。对于从应用程序深处抛出的NullPointerException异常,你到底该作何反应呢?

可以敷衍说上列代码的问题是少做了一次null值检查,其实问题多多。如果你打算在方法中返回null值,不如抛出异常,或是返回特例对象。如果你在调用某个第三方API中可能返回null值的方法,可以考虑用新方法打包这个方法,在新方法中抛出异常或返回特例对象。

在许多情况下,特例对象都是爽口良药。设想有这么一段代码:

List<Employee> employees = getEmployees();
if (employees != null) {  
    for(Employee e : employees) {  
        totalPay += e.getPay();  
    }
}

所幸Java有Collections.emptyList()方法,该方法返回一个预定义不可变列表,可用于这种目的:

public List<Employee> getEmployees () {
    if ( ..there are no employees .. )
    		return Collections.emptyList();
}

这样编码,就能尽量避免NullPointerException的出现,代码也就更整洁了。

别传递null值

在方法中返回null值是糟糕的做法,但将null值传递给其他方法就更糟糕了。除非API要求你向它传递null值,否则就要尽可能避免传递null值。

举例说明原因。用下面这个简单的方法计算两点的投射:

public class MetricsCalculator{  
    public double xProjection(Point p1, Point p2) {  
        return (p2.x – p1.x) * 1.5;  
    }  
    …
}

如果有人传入null值会怎样?

calculator.xProjection(null, new Point(12, 13));

当然,我们会得到一个NullPointerException异常。

如何修正?可以创建一个新异常类型并抛出:

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2) {
        if (p1 == null || p2 == null) {
            throw InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection");
        }
        return (p2.x –p1.x) *1.5;
    }
}

这样做好些吗?可能比null指针异常好一些,但要记住,我们还得为InvalidArgumentException异常定义处理器。这个处理器该做什么?还有更好的做法吗?

还有替代方案。可以使用一组断言:

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2) {
        assert(p1 != null,  "p1 should not be null");
        assert(p2 != null,  "p2 should not be null");
        return (p2.x –p1.x) *1.5;
    }
}

看上去很美,但仍未解决问题。如果有人传入null值,还是会得到运行时错误。思考下, 还可以怎么继续优化?

整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码。做到这一步,我们就能单独处理它,也极大地提升了代码的可维护性。

项目中的最佳实践

  • 千万别吞噬

  • 使用全局异常捕获,防止前端用户看到不该看到的

  • 异常堆栈完整打印

  • 自定义异常可打印3到5行异常栈就够用

  • 定义错误码枚举值,分门别类清楚

  • 不要在异常catch中做业务操作

  • 对于无需关注的可控异常(checked exception),使用@SneakyThrows, 不要在业务代码里写try/catch来处理

    可控异常的代价就是违反开放/闭合原则。如果你在方法中抛出可控异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常处之间的每个方法签名中声明该异常。这意味着对软件中较低层级的修改,都将波及较高层级的签名。修改好的模块必须重新构建、发布,即便它们自身所关注的任何东西都没改动过。

  • 如无必要, 不要在业务代码里写try/catch, 统一交由spring boot全局异常拦截器处理(全局异常拦截器会做"异常堆栈完整打印", “定义错误码枚举值,分门别类清楚” 这几件事, 不用业务开发人员时刻操心), 以下情况例外:

    • lamda表达式中的必须捕获异常;
    • 新起的线程, 异常会被吞噬;
    • 有的异常不能影响业务,要自己处理掉, 这种情况的规律是: 如果发生异常, 希望逻辑继续执行下去, 就自己try/catch, 否则就不要try/catch, 交由统一异常拦截器处理

根据以上原则和最佳实践实操优化

具体的问题和优化过程review时讲解

image-20211202112136131

总结

我们写的代码基本可以分为三种:

  • 一种是实现业务正常逻辑的代码;
  • 一种是为了实现业务逻辑, 不得不写的额外操作的代码;
  • 一种是小心翼翼地处理各种错误或意外逻辑的代码;

作为程序猿, 应时刻思考下, 自己写的代码是否是第一种代码, 如果不是, 看看可以如何优化减少第二or第三种代码, 毕竟真正有价值, 且能带给我们愉悦感的都是第一种代码;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值