6

排查Spring 无法启动的问题

 2 years ago
source link: https://blog.51cto.com/u_15714222/5524744
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

排查Spring 无法启动的问题

原创

这几天有小伙伴跟我反馈,有一个项目从 kotlin 1.2 升级到 kotlin 1.3 以后 Spring 项目无法启动,报 java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'xxx' method 错误,下面咱们一个个分析排查

排查Spring 无法启动的问题_spring

没有引入任何其它变量,只是更改了 kotlin 的版本,猜测可能是编译出来的字节码不一样,出问题的函数如下。

@OptionalAuthAPI@GetMapping("/page")fun getActivityGameModulePage( @OptionalAuthRes authRes: OptionalAuthResDTO, @RequestParam(name = "type", defaultValue = "0") type: Int = 0, @RequestParam(name = "page", defaultValue = "0") page: Int = 0, @RequestParam(name = "pageSize", defaultValue = "30") pageSize: Int = 0): APIResult<Page<ActivityGameModuleRespDTO>> { return;}

kotlin 处理函数中 default 值的方法是生成一个静态的函数,比如下面的函数。

class MyTest1 { private var m = 101 fun foo(x: Int = 100, y: String = "foo", z: Double = 1.0){ println("" + x + y + m + z) } fun bar(){ foo(101, "bar") foo(101); foo(); }}

生成的部分字节码如下,主要看函数签名

public final void foo(int, java.lang.String, double); descriptor: (ILjava/lang/String;D)V flags: ACC_PUBLIC, ACC_FINAL public static void foo$default(MyTest1, int, java.lang.String, double, int, java.lang.Object); descriptor: (LMyTest1;ILjava/lang/String;DILjava/lang/Object;)V flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNTHETIC

通过阅读字节码,人肉翻译为 java 就是:

public class MyTest3 { private int m; public void foo(int x, String y, double z){ String str = "" + x + y + this.m + z; System.out.println(str); } public void bar(){ foo$default(this, 101, "bar", 0.0D, 4, null); // 4 = b0100 foo$default(this, 101, null, 0.0D, 6, null); // 6 = b0110 foo$default(this, 0, null, 0.0D, 7, null); // 7 = b0111 } public static void foo$default(MyTest3 thisObj, int x, String y, double z, int mask, Object obj) { if ((mask & 0x01) != 0) { x = 100; } if ((mask & 0x02) != 0) { y = "foo"; } if ((mask & 0x04) != 0) { z = 1.0; } thisObj.foo(x, y, z); }}

由此可以看到 kotlin 对于默认参数的处理办法就是用一个 mask,告诉后面的逻辑,特定位置的参数是否需要使用默认值。

回到原 getActivityGameModulePage 方法,这个方法上有两个注解,kotlin 在编译以后会新增一个 static 的方法

// 默认方法@OptionalAuthAPI@GetMapping("/page")public static APIResult<Page<ActivityGameModuleRespDTO>> getActivityGameByPage(...) {}// 新增方法@OptionalAuthAPI@GetMapping("/page")public static APIResult<Page<ActivityGameModuleRespDTO>> getActivityGameByPage$default(...) {}

咦,这样 Spring 在扫描的时候,不会出问题吗?两个方法都标注了 @GetMapping("/page") 要处理,理论上不论是 Koltin1.2 还是 1.3 在处理的时候都会出问题才对。

遇事不决,上字节码

kotlin 1.2 编译出来的字节码

public static APIResult getActivityGameByPage$default(); flags: ACC_PUBLIC, ACC_STATIC, ACC_BRIDGE, ACC_SYNTHETIC

kotlin 1.3 编译出来的字节码

public static APIResult getActivityGameByPage$default(); flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNTHETIC

经过仔细对比,发现只有该方法的 flags 上面有一些区别,1.3 的字节码少了 ACC_BRIDGE 。

众所周知知, ACC_BRIDGE 是一种为了实现某些语言特性而由编译器自动生成的方法。除了 Kotlin,Java 自己本身在实现类型擦除等场景下也会用到 ACC_BRIDGE ,具体我这里就不展开了,大家可以去试一下。

是不是就是这个导致的问题呢?

我们来看我们当前用的什么 Spring 版本是如何处理方法的,通过调试我们进入到了这个方法

排查Spring 无法启动的问题_java_02

可以看到 Spring 4.3.10 版本判断是否是用户自己写的方法时的逻辑是方法不是 bridge 且方法不同于 Object 类中,因此现在情况就很明朗了。

在 kotlin1.2 中文,因为编译出的 getActivityGameByPage$default() 包含了 bridge,在 Spring 扫描的过程中就会被忽略掉,而 kotlin1.3 中,因为方法签名不包含 bridge,所以被当做了用户自己书写的方法,参与到扫描中,这样 controller 就冲突了,所以报了 Ambiguous mapping 错误。

那这么严重的问题,难道 kotlin 不解决吗?是的,kotlin 不解决,那就只能上层框架兼容了,Spring 在后续的版本中做了修复,增加了对 ACC_SYNTHETIC 的判断,修改的地方如下:

排查Spring 无法启动的问题_java_03

这样,在新版本的 Spring 中,就不存在这个问题了,升级以后果然解决了问题。

Kotlin 编译器源码探秘

有了实验的结果,反过来寻找原因就很简单了,找到原因 kotlin 1.2 的源码,然后翻一翻源码,马上找到了对应的逻辑。在 4 年前的一个 commit 中,有一个伙计干掉了 ACC_BRIDGE 标记。

排查Spring 无法启动的问题_java_04

对应的源码修改如下

排查Spring 无法启动的问题_字节码_05

Kotlin 新版逻辑

有小伙伴又试了 kotlin 1.4+,发现问题也消失了,这又引起了我的兴趣,看了一下字节码,发现新版本的 getActivityGameByPage$default() 中,已经没有了注解,这下从源头解决了问题。这下就弄明白了


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK