Kotlin中处理Java检查异常和Spring的@Transactional陷阱
我以为能像使用Java一样自如地使用Kotlin,但在意想不到的地方卡住了,所以分享出来。
环境
-
- Spring Boot 3.1.5
-
- Amazon Corretto 17
- Kotlin 1.9.10
Java中的检查异常和非检查异常
在Java中,与其他主要的编程语言不同,存在一种称为”检查异常”的异常。”检查异常”和”非检查异常”之间有以下区别:
-
- 検査例外是一种在try-catch或throws中不处理会导致编译错误的异常。
-
- 非検査例外是一种即使不进行处理也不会导致编译错误的异常(但是也可以进行处理)。
-
- 非検査例外是继承了RuntimeException的异常。
- 特别代表性的非検査例外包括NullPointerException和IllegalArgumentException,也称为”実行時例外”。
如果存在以下这样的方法,只有前者会出现编译错误。
void throwIOException() {
throw new IOException(); // NG: 検査例外なのでtry-catch or throwsが必要
}
void throwNullPointerException() {
throw new NullPointerException(); // OK: 非検査例外なのでコンパイルが通る
}
尝试使用try-catch块或throws语句,就可以使得编译通过。
void throwIOException_trycatch() {
try {
throw new IOException();
} catch (IOException e) {} // OK: catchしているからOK
}
void throwIOException_throws() throws IOException { // OK: throwsで伝播させているからOK
throw new IOException();
}
Kotlin中不存在检查异常。
正如公式所述,Kotlin中不存在受检异常。
主张是指称检查例外在实际中通常被消除,这会降低生产效率和代码质量。
因为以下的链接文章也很有趣,所以如果你感兴趣的话,请阅读一下。
即使是可能发生IOException的方法,即使不明确指示异常发生,也可以通过编译,就像Java的例子一样。
fun throwIOException() {
throw IOException() // OK
}
使用Kotlin + Spring Boot时需要注意的问题
现在,我们已经解释了关于Java和Kotlin检查异常的部分,接下来是我们遇到困难点的核心。
Kotlin与Spring Boot的关系
在Java的著名Web框架中,有Spring和Spring Boot。特别是Spring Boot,可以说是Java的Web框架的事实标准,其普及率非常高。
在中国,Spring Boot这样的框架可以使用Kotlin来开发,这样就可以不用其他的Kotlin框架而直接使用Spring Boot的情况也很多。
在Spring Boot中的事务管理
在Spring Boot中,只需使用@Transactional注解就可以进行事务管理。
@Transactional // これだけでOK
void exsample() {
// なんらかの処理
}
在调用方法时开始事务(BEGIN),如果发生指定的异常则进行回滚(ROLLBACK),否则进行提交(COMMIT)。
如果想要进行回滚的异常可以通过rollbackFor选项等进行指定,如果未指定的话,Java的受检异常将被排除在外。(参考文档)
/**
* ...省略...
* <p>By default, a transaction will be rolled back on {@link RuntimeException}
* and {@link Error} but not on checked exceptions (business exceptions). See
* {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)}
* for a detailed explanation.
* ...省略...
*/
Class<? extends Throwable>[] rollbackFor() default {};
在这种默认情况下,Java的检查异常被排除在外,这将导致陷阱。
Kotlin中的@Transactional为什么是陷阱的原因
由于@Transactional的行为在Java和Kotlin中是相同的,所以乍一看,在Java中没有问题,那么在Kotlin中也不会有问题。然而,在这里,“Kotlin不存在检查异常 = 不需要处理Java的检查异常”这种语言规范起作用。
在Java中,如果发生检查异常,必须通过try-catch或throws来处理,这样即使不需要回滚,也可以适当地使用try-catch进行处理或明确地使用throws进行处理。即使希望进行回滚,也可以通过将其转换为RuntimeException并使用try-catch,或者明确地自定义rollbackFor等方式进行处理。
使用Kotlin进行@Transactional时最大的困扰是无法通过编译错误等方式意识到会发生哪些异常(尤其是Java的检查异常中的异常类型)。除非明确地抛出的异常,否则在通常开发时会使用许多库,因此很难完全了解库会抛出哪些异常。
// Javaでは検査例外の処理が必須なため、コンパイルエラーになってくれる
@Transactional
void throwException() {
// Javaの検査例外が発生する処理やライブラリなどの呼び出し
}
// Kotlinでは検査例外という概念がないためこのままでもコンパイルエラーにならず、実行時に@Transactionalでロールバックされずコミットされてしまう
@Transactional
fun throwException() {
// Javaの検査例外が発生する処理やライブラリなどの呼び出し
}
Kotlin + Spring Boot的资料相对较少,大多数人会根据Java + Spring Boot的资料进行开发。由于大部分资料都是关于“如果添加@Transactional注解,就可以进行事务管理”,所以很容易在使用Kotlin时忽视了这种陷阱。
最后
Kotlin作为Java的替代语言备受关注。它具有类似于Java的使用体验,可以减少代码量并带来诸如空值安全等优点。而且,它还具有较高的兼容性,可以逐步将Java代码替换为Kotlin,或者直接使用Java库。
然而,它仍然是一种完全不同的语言。如果我们仅仅凭感觉使用,可能会在意想不到的地方碰壁,所以请确保正确理解语言规范后再使用。
悠谈
以Java字节码来看,throws似乎是不必要的。
我在研究在 Kotlin 中抛出异常时与 Java 和字节码之间的差异,然而,似乎无论字节码是否有 throws 声明,它都不会影响程序的运行。
我希望以下的Java/Kotlin类生成相同的字节码(即在生成字节码时,Kotlin会自动添加throws),但事实并非如此。
public final class JavaSample {
public final void throwIOException() throws IOException {
throw new IOException();
}
}
class KotlinSample {
fun throwIOException() {
throw IOException()
}
}
在使用javac/kotlinc分别编译后,使用javap命令确认其内容。
$ javap -v JavaSample
...省略...
public final void throwIOException() throws java.io.IOException;
descriptor: ()V
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=1, args_size=1
0: new #7 // class java/io/IOException
3: dup
4: invokespecial #9 // Method java/io/IOException."<init>":()V
7: athrow
LineNumberTable:
line 7: 0
Exceptions:
throws java.io.IOException
}
SourceFile: "JavaSample.java"
...省略...
$ javap -v KotlinSample
...省略...
public final void throwIOException(); # throwsが勝手に付与されるわけではない
descriptor: ()V
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=1, args_size=1
0: new #13 // class java/io/IOException
3: dup
4: invokespecial #14 // Method java/io/IOException."<init>":()V
7: athrow
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/example/KotlinSample;
# JavaのようにExceptionsの記載はない
}
SourceFile: "KotlinSample.kt"
...省略...
从这件事情中可以看出,javac编译器只是把缺少throws视为编译错误,而在实际的字节码执行时,并没有明确要求 throws。
顺便提一下,Kotlin中存在着一个用于明确指定会抛出哪些异常的@Throws注解,使用此注解后,字节码中也会输出throws。
class KotlinSample {
@Throws(IOException::class)
fun throwIOException() {
throw IOException()
}
}
$ javap -v KotlinSample
...省略...
public final void throwIOException() throws java.io.IOException; # throwsの記載がある
descriptor: ()V
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=1, args_size=1
0: new #13 // class java/io/IOException
3: dup
4: invokespecial #14 // Method java/io/IOException."<init>":()V
7: athrow
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/example/KotlinSample;
Exceptions:
throws java.io.IOException # Exceptionsの記載がある
}
SourceFile: "KotlinSample.kt"
...省略...
参考:请提供以下内容的中文本地语义化表达,仅需一种选项 :
关于检查例外
我之前并没有特别对Java中的受检异常感到奇怪,因为Java中的受检异常是最长的。但是,通过这次事件,我重新审视了其他语言,发现许多语言并没有采用受检异常,这让我感到惊讶。
在我进行了各种调查研究之后,我个人认为下面这篇文章很易懂,所以我将其作为参考信息发布出来。