Lombok的@SneakyThrows注解的使用

huangapple go评论69阅读模式
英文:

Use of Lombok @SneakyThrows annotation

问题

我正在尝试在我的Spring Boot应用程序中使用Lombok库。我遇到了@SneakyThrows注解。但我并没有完全理解它的用途。通常,如果可能会出现异常,最好捕获/抛出它,调用者也可以处理和捕获或抛出它,这是一种良好的实践。但是使用@SneakyThrows,我们正在绕过它。那么它的真正优势是什么?

我查看了很多链接,但它们都只说了行为,我没有理解真正的用例。

英文:

I am trying to use Lombok library in my spring boot application. I came across @SneakyThrows annotation. But I didn't really get the full use of it. Usually if there could be an exception, it's always good to catch/throw it and also the caller can handle and catch or throws it, which is a good practice. But using @SneakyThrows we are bypassing it. So what is the real advantage of it ?

I went over lot of links but it all only says about the behaviour and I didn't understand the real use case.

答案1

得分: 6

以下是翻译好的部分:

这个功能的作者在这里 - 这个回答在很大程度上是客观的,但也有一定的主观性 - 在某种程度上,任何语言特性都在轻微地告诉人们如何“应该”编程方面有点独断专行。无法避免这一点。通常情况下,在Stack Overflow上不接受意见,但鉴于这是关于lombok的,而且[A]我编写了这个功能,[B]我仍然是lombok的核心贡献者,我猜我的意见就是所要求的答案,这不能沦为意见之争(除了Roel,我的合作贡献者,他在很大程度上支持这些意见)。

>通常情况下,如果可能会有异常,最好捕获/抛出它

不正确。通常情况下,捕获/抛出它是好的,但并非总是好的。

##快速回顾它的作用

检查异常的概念完全是javac的想象。之所以这不会编译:

public void foo() {
  throw new IOException();
}

是因为javac在其中有一个if构造,说:我不会编译这个。如果你修改javac并删除该构造,生成的类文件完全没有问题。类验证器(检查执行类文件中的代码是否会引起问题的JVM中的代码)不关心。JVM也不关心。它将忠实地运行该代码,然后...抛出异常。“但...没有声明!”,是的,JVM不关心。它不知道throws IOException是什么意思。就java.exe而言,这是一个注释。这解释了为什么像Kotlin这样的针对JVM运行的语言可以做他们所做的事情(它们没有检查异常 - 你可以随意抛出任何你喜欢的异常,而不管throws子句如何。包括用Java编写的检查异常)。

@SneakyThrows就像这样:它允许你抛出异常,而不声明你这样做。异常不会被抑制、包装、忽略或以任何方式修改。@SneakyThrows只有一个任务:告诉编译器停止报错,继续执行。

有两种情况,不正确的捕获检查异常或将其添加到throws行的情况:

##不可能的检查异常

异常被声明了,因此,你__必须__编写处理它的代码(要么catch它,要么将它放入方法的throws子句中),但是,通过规范或可能只是通过经验和代码审查,你__100%确定__异常不可能发生。

这是一个微不足道的例子:

new String(someByteArray, "UTF-8");

这段代码通过将提供的字节数组视为包含UTF-8编码文本来创建字符串。此方法声明要抛出的异常之一(并且这是一个检查异常)是UnsupportedEncodingException

然而,这是不可能的。Java虚拟机规范__保证__UTF-8是可用的。这段代码完全有效:

try {
  new String(someByteArray, "UTF-8");
} catch (UnsupportedEncodingException e) {
  throw new InternalError("Your JDK is corrupt. Reinstall it. Hard-crashing now because continuing in the face of a corrupt JDK install is asking for trouble");
  // 可能记录并运行System.exit(0)!
}

但是,如果我们开始编写这种代码,会有什么结果?你应该编写:

String x = ...;
try {
  x = x.toLowerCase();
} catch (IllegalStateException e) {
  throw new RuntimeError("Your JDK is corrupt");
}

这似乎相当愚蠢。因此,客观地说,尝试实际处理UnsupportedEncodingException的“除非JDK损坏”的情况同样愚蠢,所以我在这里注入的唯一主观的事情是,尝试处理损坏的JDK是愚蠢的;你真的不能,如果你尝试,你最终会写无法测试的非序贯的try/catch everywhere

这正是SneakyThrows的亮点所在。因为你既不想编写try/catch,也不想通过列出一个实际上永远不会发生的异常类型来使调用者负担。

在lombok的SneakyThrows功能发布时new String(someByteArray, StandardCharsets.UTF_8) API还不存在!

它现在存在是一件好事:我会说,如果程序员知道绝对不可能发生的情况下,任何API都会抛出检查异常,那么这个API确实很糟糕。我很高兴现在不再需要处理它。

尽管如此,还存在许多情况下会发生这种情况的库。

##未规定的入口点

这是有效的Java:

public static void main(String[] args) throws Exception {}

main方法允许这样做。我强烈建议所有的Java程序员养成这个习惯。检查异常的概念(即,调用者需要“担心”它们)对于库函数来说很棒(任何具有作为API而不是作为实现被理解的代码,要被应用于编写它的团队之外

英文:

Author of the feature here - this answer is mostly objective, but there is a certain subjective nature to it all - at some point, any language feature is lightly dictatorial about telling people how one 'should' program. It cannot be avoided. Ordinarily, opinion isn't acceptable on SO, but given that this is about lombok, and [A] I wrote this feature, and [B] I remain a core committer on lombok, I guess my opinion is the answer asked for, and this cannot devolve into a war of opinions (except Roel, my co-committer, who by and large stands by these opinions).

> Usually if there could be an exception, its always good to catch/throw it

Incorrect. It's usually good to catch/throw it, but not always.

A quick review of what it does

the concept of the checked exception is entirely a figment of javac's imagination. The reason this doesn't compile:

public void foo() {
  throw new IOException();
}

is because javac has an if construct in there that says: I won't compile this. If you hack javac and remove that construct, the resulting class file is completely fine. The class verifier (the code in the JVM that checks if executing the code in a class file would cause issues) doesn't care. The JVM doesn't care either. It will dutifully run that code and just... throw the exception. "But... it's not declared!", yeah, the JVM doesn't care. It doesn't know what throws IOException means. It's a comment, as far as java.exe is concerned. This explains why languages like kotlin that are targeted to run on the JVM can do what they do (they don't have checked exceptions - you can throw whatever you like regardless of throws clause. Including java-written checked exceptions).

@SneakyThrows is like that: It lets you throw exceptions without declaring that you do so. The exception is not suppressed, or wrapped, or ignored, or modified in any way. @SneakyThrows does just one job: Tell the compiler to stop erroring and just get on with it.

The specific 2 cases where it isn't correct to either catch a checked exception, or add it to your throws line:

Impossible checked exceptions

The exception is declared, and therefore, you must write code that handles it (either catch it, or stick it in the throws clause of your method), however, by way of spec or possibly simply experience and code review you know for 100% sure that the exception cannot possibly happen.

Here is a trivial example:

new String(someByteArray, "UTF-8");

This code makes a string by treating the provided byte array as containing UTF-8 encoded text. One of the exceptions that this method is declared to throw (and this is a checked exception) is UnsupportedEncodingException.

However, this is impossible. The java virtual machine specification guarantees that UTF-8 is available. This code is entirely valid:

try {
  new String(someByteArray, "UTF-8");
} catch (UnsupportedEncodingException e) {
  throw new InternalError("Your JDK is corrupt. Reinstall it. Hard-crashing now because continuing in the face of a corrupt JDK install is asking for trouble");
  // possibly log that and run System.exit(0) instead!
}

However, if we start writing that kind of code, where does it end? Should you write:

String x = ...;
try {
  x = x.toLowerCase();
} catch (IllegalStateException e) {
  throw new RuntimeError("Your JDK is corrupt");
}

That seems rather silly. Hence, objectively, attempting to actually deal with the 'impossible unless JDK corrupt' situation of that UnsupportedEncodingException is equally silly, so the only subjective thing I am injecting here is that it is silly to try to deal with a corrupt JDK; you really can't, and if you try, you end up writing untestable non-sequitur try/catches everywhere.

This kind of thing is exactly where SneakyThrows shines. Because you neither want to write the try/catch, nor do you want to burden callers by listing an exception type that cannot actually ever happen.

At the time lombok's SneakyThrows feature was released, the new String(someByteArray, StandardCharsets.UTF_8) API didn't exist yet!__

The fact that it exists now is a good thing: I'd say that any API that throws a checked exception in cases where the programmer knows it cannot possibly happen, is really cruddy API. I'm glad you now no longer have to deal with it.

Regardless, plenty of libraries exist where this situation does occur.

Underspecced entrypoints

This is valid java:

public static void main(String[] args) throws Exception {}

main methods are allowed to do that. I strongly advise all java programmers to get into the habit of doing that. The notion of checked exceptions (as in, callers need to 'worry' about them) is great for library functions (any code written with the intent to be understood as an API and not as an implementation, and is to be used by code outside of the direct confines of the team that writes it. That doesn't necessarily imply 'by other people', but may imply: By another modular layer of the application we are writing).

It is silly for application code, though!

A lot of exceptions are effectively unhandleable. A library has no idea what that even means (e.g. the code of new FileInputStream has no idea if one could perhaps handle 'file not found' - maybe this is the 'file open' dialog of a GUI app and the GUI can simply tell the user they typed a non-existent file name and need to pick something else - that's handling it). Application code, however, always knows. They can, or they can't. Often, they can't.

So, what do you do when you get an exception that you cannot handle?

You have really only one option: Throw it onward and hope that some layer above you (a caller somewhere in the call chain) can handle it. Any other act is silly and results in bad code, bad behaviour. For example, this is ubiquitous and yet evil:

try {
 stuff;
catch (Thingie e) {
 e.printStackTrace();
}

This is the java version of basic's ON ERROR RESUME NEXT: When an error occurs, toss half of the info about it in the garbage, make it impossible for any other code to try to deal with it, log it to someplace few folks check, and then just keep going as if nothing is wrong, which likely means if something does go wrong, 85 stack traces appear because all code is written like this, and generally 'just keep going' when an exception has occurs just means more exceptions will occur soon.

When we speak of entrypoints, which isn't just main, but is also, e.g. for web frameworks, the many many methods that serve as the point where the web framework begins calling your code (i.e. this is the entrypoint handler for the /users/{username} URL) are all 'entrypoints' and 'application code', and here's the key insight:

It is non-trivial to deal with a generalized, unhandleable-by-the-business-logic error condition.

For example, let's say you have a web framework entrypoint and the first thing it does is open the database to check the user's credentials. If the database is just flat out down, what should the SQLException that occurs result in, for the webbrowsing user? Surely 'an error message', but it's more complicated than that. What you optimally want to happen is:

  • The system gathers pertinent data which isn't 100% encoded in that exception. It also involves the request parameters for example, that should be quite useful. Perhaps not here (not useful to debug 'db is down'), but for many errors, useful.
  • The system encrypts all this with a server key into a blob of data.
  • The system serves an error page to the user (with code 500) and includes a form that results in an email (our server is down or in trouble, we can't do the normal thing and let them submit to our server and save it, after all, our server is probably not capable of responding if it's in dire straits, and it is, if the DB is down!), it includes the encrypted blob. This prevents issues where a hacker can use the stack trace and error relevant info to glean hidden info whilst still letting our dev team decrypt it and figure out what went wrong.
  • All this data should also go into a log someplace.

That's decidedly non-trivial! You do not want to have to write all that logic in 5000 separate try/catch blocks all over your code base.

The best place to handle such a generalized error response is in whatever code calls entrypoints.

For main, that's already how it works: You can throw whatever you want, and the way java.exe deals with it, (this'd be the thread's default exception handler) is to print the type, message, stack trace, and causal chain, and then kill that thread.

Many, many frameworks, however, messed up. For example, the servlet framework requires that you throw ServletException. That was silly of them. Lombok's @SneakyThrows can help you by letting you throw these exceptions. Usually (and this includes servlets), it works fine - the servlet entrypoint runners actually catch everything.

There is a third useful but less advisable use of SneakyThrows:

Milestones

It's less pretty as a codebase, but there are cases where exceptions really can occur, but rarely will (from experience / the situation) and whilst you really ought to catch them and wrap them in a properly typed exception, you're working towards a milestone release (a proof of concept for example), and whilst you don't want to write throw-away code, you're going to leave non-crucial stuff (not crucial for a working first version) documented as 'will fix later'. In such cases, sneakythrows is better than all alternatives, because sneakythrows doesn't mess with the exception at all.

答案2

得分: 1

以下是要翻译的内容:

Lombok SneakyThrows文档 中提到了一些常见的用例:

不必要严格的接口,例如Runnable - 无论你的run()方法传播出什么异常,无论是已检查的还是未检查的,它都将传递给线程的未处理异常处理程序。捕获已检查的异常并将其包装在某种RuntimeException中只会掩盖问题的真正原因。

一个“不可能”的异常。例如,new String(someByteArray, "UTF-8"); 声明它可以抛出UnsupportedEncodingException,但根据JVM规范,UTF-8必须始终可用。在这里抛出UnsupportedEncodingException就像在使用String对象时抛出ClassNotFoundError一样不太可能,而您也不会捕获这些异常!

当然,如果您不同意,完全可以不使用该注解。

英文:

The Lombok documentation for SneakyThrows mentions some common use cases:

> A needlessly strict interface, such as Runnable - whatever exception propagates out of your run() method, checked or not, it will be passed to the Thread's unhandled exception handler. Catching a checked exception and wrapping it in some sort of RuntimeException is only obscuring the real cause of the issue.

and

> An 'impossible' exception. For example, new String(someByteArray, "UTF-8"); declares that it can throw an UnsupportedEncodingException but according to the JVM specification, UTF-8 must always be available. An UnsupportedEncodingException here is about as likely as a ClassNotFoundError when you use a String object, and you don't catch those either!

Of course, if you don't agree, it's perfectly OK to not use the annotation at all.

huangapple
  • 本文由 发表于 2023年7月17日 22:30:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/76705502.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定