@SneakyThrows注解

被 @SneakyThrows 坑过之后,我终于搞懂了它的底层原理

之前在项目里写代码的时候,@SneakyThrows 用得挺随意的,Service 层标一个,Controller 里也标一个,觉得挺方便。后来有一次线上出了问题,排查了半天发现跟这个注解有关系,才决定认真研究一下。

这一研究,发现这个东西确实有说道。


先说说它解决了什么问题

写 Java 的应该都碰到过这种情况:

java 复制代码
List<String> files = getFilePaths();

files.forEach(file -> {
    // 这里编译报错,FileReader 要抛 IOException
    // 但 forEach 里的 lambda 不允许抛受检异常
    BufferedReader reader = new BufferedReader(new FileReader(file));
});

没办法,只能这么写:

java 复制代码
files.forEach(file -> {
    try {
        BufferedReader reader = new BufferedReader(new FileReader(file));
        // 业务逻辑
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

代码量直接翻倍,而且那个 try-catch 里其实没什么有意义的异常处理逻辑,纯粹是为了过编译器那一关。

然后 Lombok 告诉你:加个 @SneakyThrows 就行了。

java 复制代码
@SneakyThrows
public String readLine(String path) {
    return new BufferedReader(new FileReader(path)).readLine();
}

干净利落,编译器也不报错了。看起来挺美好。


它到底干了什么

我当时第一反应是,这注解是把异常给吞了吧?仔细一看源码,还真不是。

Lombok 处理完之后,生成的代码大概长这样:

java 复制代码
public String readLine(String path) {
    try {
        return new BufferedReader(new FileReader(path)).readLine();
    } catch (Throwable t) {
        throw Lombok.sneakyThrow(t);
    }
}

关键在 sneakyThrow 这个方法。点进去看:

java 复制代码
public static RuntimeException sneakyThrow(Throwable t) {
    return Lombok.<RuntimeException>uncheckedThrow(t);
}

private static <T extends Throwable> RuntimeException uncheckedThrow(Throwable t) throws T {
    throw (T) t;
}

看到这行 throw (T) t 的时候,我愣了几秒。

这是个泛型的花活儿。编译的时候,编译器看到 (T) t,T 被推断成 RuntimeException,所以编译器觉得"哦,抛的是非受检异常,没问题,放行"。但到了运行时,泛型擦除,T 变成了 Throwable,实际上抛出来的还是原来的那个 IOException

说白了就是钻了泛型擦除的空子,编译期和运行时的类型检查对不上,让受检异常偷偷溜过去了。

这个技巧其实早就有了,不是什么新东西。Scala 早就这么干了,Lombok 把它包装成了一个注解。


我为什么反对在业务代码里乱用

搞清楚了原理之后,再回头看自己之前的写法,问题就很明显了。

我当时在 Service 层这么写:

java 复制代码
@SneakyThrows
public Order processOrder(OrderRequest request) {
    // 里面可能抛 IOException、SQLException 什么的
    saveOrderToFile(request);
    return orderRepository.save(request);
}

调用方一看方法签名,没有任何 throws 声明,以为这方法不会抛出什么受检异常,于是就没写 try-catch

结果线上出了 IO 异常,请求直接 500,调用方完全没有兜底逻辑。

这就是 @SneakyThrows 最大的问题:它让异常变得不可见了

Java 设计受检异常,就是想让调用方知道"这个方法可能会出错,你得处理"。你用 @SneakyThrows 把这个信号给抹掉了,等于是在给后面维护代码的人挖坑。

而且你还没法正常 catch 它:

java 复制代码
try {
    service.processOrder(request);
} catch (IOException e) {
    // 编译报错:这里不会抛出 IOException
    // 编译器被骗了,但运行时确实会抛
}

编译器认为 processOrder 不会抛 IOException,所以你写 catch (IOException e) 直接编译不通过。你说气不气人。


那什么时候能用

也不是说完全不能用。有几种情况我觉得是合理的:

Lambda 和函数式接口里,这基本是它的主场:

java 复制代码
executor.submit(() -> {
    // 这里没法声明 throws,写 try-catch 又很啰嗦
    doSomethingThatThrows();
});

Stream 操作里面,也是类似的情况:

java 复制代码
list.stream()
    .map(this::parseConfig)
    .collect(Collectors.toList());

@SneakyThrows
private Config parseConfig(String path) {
    return mapper.readValue(new File(path), Config.class);
}

这些地方你确实没什么好的选择,用 @SneakyThrows 比写一堆 try-catch 包装强。

但有一点要注意:即使用在这些地方,方法上最好加个注释说明可能抛什么异常,别让后面看代码的人猜。

java 复制代码
/**
 * 注意:可能抛出 FileNotFoundException
 */
@SneakyThrows
private Config parseConfig(String path) { ... }

跟事务配合还有个坑

java 复制代码
@Transactional(rollbackFor = IOException.class)
@SneakyThrows
public void process() {
    throw new IOException("失败了");
}

这个事务能正常回滚吗?能。因为 @SneakyThrows 只是骗了编译器,实际抛出来的还是 IOException,Spring 事务拦截器按实际异常类型判断,没问题。

但问题是,读代码的人看到 process 方法,没有任何迹象表明它会抛 IOException。万一哪天有人改了事务配置,把 rollbackFor 去掉了,出了 bug 都找不到原因。


我后来的使用原则

踩过坑之后,给自己定了几个规矩:

  • Lambda、Stream、Runnable 这些没法声明 throws 的地方,可以用
  • Controller、Service、接口定义,不许用
  • 用了的话,必须加注释说明可能抛什么异常

其实说到底,@SneakyThrows 不是什么洪水猛兽,但你得知道它在干什么。它不是消灭了异常,只是把异常藏起来了。藏起来的东西,迟早会在你没想到的地方蹦出来。

写代码也是这样,编译器的报错虽然烦,但它是在保护你。想办法绕过它,往往意味着你将来要付出更大的代价去处理那些被绕过的东西。


基于 Lombok 1.18.x,JDK 8+

相关推荐
我的xiaodoujiao1 小时前
快速学习Python基础知识详细图文教程14--模块
开发语言·python·学习·测试工具
NoteStream2 小时前
【c语言基础】C语言常见概念
c语言·开发语言·编辑器
吃好睡好便好2 小时前
MATLAB中图像的线性变换
开发语言·图像处理·学习·计算机视觉·matlab
charlie1145141912 小时前
Cinux —— 给物理内存建账本:bitmap 物理内存管理器
开发语言·c++·操作系统·开源项目
ShiXZ2132 小时前
Java 8 Stream API 实用技巧详解:从入门到精通
java·开发语言
冻柠檬飞冰走茶2 小时前
PTA基础编程题目集 7-27 冒泡法排序(C语言实现)
c语言·开发语言·数据结构·算法
听雨入夜2 小时前
“同声传译”还是“全文翻译”?为何HotSpot虚拟机仍要保留解释器?
开发语言·python
随风M记忆s2 小时前
GEE&Python-demo:利用Sentinel-监测北京奥林匹克森林公园年NDVI变化(附Python版)
开发语言·python·sentinel
C++、Java和Python的菜鸟3 小时前
第9章 后端Web进阶(AOP)
java·开发语言·前端