在这种情况下,异常神秘地逃离 try-catch 块的最可能原因是什么?

What is the most likely cause of exceptions mysteriously escaping a try-catch block in this case?

我在 Kotlin 项目中使用 Spring WebClient,如下所示:

data class DTO(val name: String)

@Component
class Runner: ApplicationRunner
{
    override fun run(args: ApplicationArguments?)
    {
        try
        {
            val dto = get<DTO>()
        }
        catch (e: Exception)
        {
            println("ERROR, all exceptions should have been caught in 'get' ")
        }
    }
}

inline private fun<reified TResult: Any> get(): TResult?
{
    var result: TResult? = null

    try
    {
    result = WebClient.create("https://maps.googleapis.com/maps/api/nonexisting")
        .get()
        .retrieve()
        .bodyToMono<TResult>()
        .block()
    }
    catch (e: Exception)
    {
        println("WORKS AS EXPECTED!!")
    }

    return result
}

客户端会抛出一个异常,因为 API 会 return 一个 404。但是异常没有在它应该出现的地方被捕获,即在 get 的正文中函数,但它被传播到外部异常处理程序。

有趣的是,只有在 WebClient 抛出异常时才会发生这种情况。如果我用一个简单的 throw Exception("error") 替换 try 子句中的代码,异常就会在它应该出现的地方被捕获。

同样,当我将 get 的签名更改为 non-generic inline private fun get(): DTO? 时,问题也消失了。

对于逃避 try-catch 块的异常似乎是 Kotlin 工具中的一个基本错误。另一方面,这仅发生在 WebClient class 的事实表明这是一个 Spring 问题。或者,可能只是我以错误的方式使用这些工具。

我真的很困惑,不知道如何进行。欢迎任何关于为什么会发生这种情况的想法。为了完整起见,这就是它在调试器中的样子:

编辑

升级后问题消失Spring 启动到 2.0.0.M6,它仍然存在于 M5 中。

所以这似乎是一个 Spring 问题而不是 Kotlin 问题。另一方面,了解您包含的库如何看似导致程序违反其编写的编程语言的法律仍然是件好事。

我用 Spring 引导版本 2.0.0.M52.0.0.M6 尝试了代码,看起来以下块的行为在这两个版本之间是不同的:

result = WebClient.create("https://maps.googleapis.com/maps/api/nonexisting")
    .get()
    .retrieve()
    .bodyToMono<TResult>()
    .block()

沿着链的某处,在 Spring 引导 2.0.0.M5 上,WebClientResponseExceptionreturned,在 Spring boot 2.0.0.M6抛出.

如果您将 e.printStackTrace() 添加到外部捕获,您会注意到堆栈跟踪是:

java.lang.ClassCastException: org.springframework.web.reactive.function.client.WebClientResponseException cannot be cast to com.example.demo.DTO at com.example.demo.Runner.run(Test.kt:18) at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:780) at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:770) at org.springframework.boot.SpringApplication.afterRefresh(SpringApplication.java:760) at org.springframework.boot.SpringApplication.run(SpringApplication.java:328) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1245) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1233) at com.example.demo.DemoApplicationKt.main(DemoApplication.kt:10)

所以,实际上,问题是,returned WebClientResponseException 试图在 [ 的时刻转换为 DTO classreturn 的调用 val dto = get<DTO>()。这意味着,当您分配 result = ... 时,还没有完成类型检查。因此,如果您将代码更改为,例如,调用 get<Object>() 而不是 get<DTO>(),它不会命中任何 catch 块。

如果你在IntelliJ Idea里面转成字节码,再反编译成Java,你可以看到这个块:

public class Runner implements ApplicationRunner {
   public void run(@Nullable ApplicationArguments args) {
      try {
         Object result$iv = null;

         try {
            ResponseSpec $receiver$iv$iv = WebClient.create("https://maps.googleapis.com/maps/api/nonexisting").get().retrieve();
            Mono var10000 = $receiver$iv$iv.bodyToMono((ParameterizedTypeReference)(new Runner$run$$inlined$get()));
            Intrinsics.checkExpressionValueIsNotNull(var10000, "bodyToMono(object : Para…zedTypeReference<T>() {})");
            result$iv = var10000.block();
         } catch (Exception var7) {
            String var5 = "WORKS AS EXPECTED!!";
            System.out.println(var5);
         }

         DTO var2 = (DTO)result$iv;
      } catch (Exception var8) {
         String var3 = "ERROR, all exceptions should have been caught in 'get' ";
         System.out.println(var3);
      }

   }
}

在这里你可以注意到转换到 DTO 是在方法 return 的点上完成的(它不再是 return 因为它是 内联 ),在内部 catch 块之后:DTO var2 = (DTO)result$iv;。这似乎是具有具体化类型参数的内联方法的行为。

这是由于 SPR-16025 (see related commit),因为 Kotlin 扩展在内部使用 ParameterizedTypeReference 变体,该变体已在 Spring Framework 5.0.1 中修复,并在 Spring 启动 2.0.0.M6.

请注意,如果您将 bodyToMono(TResult::class.java) 与 Spring Boot 2.0.0.M5 一起使用,它将按预期工作。