Kotlin中处理Java检查异常和Spring的@Transactional陷阱

我以为能像使用Java一样自如地使用Kotlin,但在意想不到的地方卡住了,所以分享出来。

环境

    • Spring Boot 3.1.5

 

    • Amazon Corretto 17

 

    Kotlin 1.9.10

Java中的检查异常和非检查异常

在Java中,与其他主要的编程语言不同,存在一种称为”检查异常”的异常。”检查异常”和”非检查异常”之间有以下区别:

    1. 検査例外是一种在try-catch或throws中不处理会导致编译错误的异常。

 

    1. 非検査例外是一种即使不进行处理也不会导致编译错误的异常(但是也可以进行处理)。

 

    1. 非検査例外是继承了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中的受检异常是最长的。但是,通过这次事件,我重新审视了其他语言,发现许多语言并没有采用受检异常,这让我感到惊讶。

在我进行了各种调查研究之后,我个人认为下面这篇文章很易懂,所以我将其作为参考信息发布出来。

 

不仅Spring Boot,Kotlin还可以直接使用许多Java库和框架。因此,在使用时最好设定如@Transactional(rollbackFor = [Exception::class])之类的内容。由于Spring拥有众多功能,所以将“只需添加 @Transactional”作为初学者接触信息并不正确。让我们确保那些考虑架构的成员在理解规范后再予以采纳。@Throws主要用于从存在受检异常的语言如Java、Swift和Objective-C调用Kotlin程序时使用。
bannerAds