The problem background

Have students feedback, there is a project from the kotlin upgrade to 1.2 after 1.3 kotlin Spring projects cannot be started, the Java. Lang. An IllegalStateException: Ambiguous Mapping. Cannot map ‘XXX’ method Error

No other variables are introduced, only kotlin’s version is changed, presumably because the compiled bytecode is not the same, the problem function is as follows.

@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;
}
Copy the code

Kotlin handles the default value in a function by generating a static function, such as the one below.

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(); }}Copy the code

Part of the generated bytecode is as follows, depending on the function signature

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_SYNTHETICCopy the code

By reading bytecode, human flesh translates to 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(a) {
        foo$default(this.101."bar".0.0 D.4.null); // 4 = b0100
        foo$default(this.101.null.0.0 D.6.null); // 6 = b0110
        foo$default(this.0.null.0.0 D.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); }}Copy the code

As you can see, Kotlin’s approach to default parameters is to use a mask that tells the logic behind whether the default values are needed for parameters in a particular location.

Back to the original getActivityGameModulePage method, this method has two annotations, kotlin after compilation will add a static method

// The default method
@OptionalAuthAPI
@GetMapping("/page")
public staticAPIResult<Page<ActivityGameModuleRespDTO>> getActivityGameByPage(...) {}// Add a method
@OptionalAuthAPI
@GetMapping("/page")
public static 
APIResult<Page<ActivityGameModuleRespDTO>> 
getActivityGameByPage$default(...) {
}
Copy the code

So Spring won’t have any problems when scanning? Both methods are marked @getmapping (“/page”) to be processed, and in theory both Koltin1.2 and 1.3 should have problems with processing.

When in doubt, upload bytecode

Bytecode compiled by Kotlin 1.2

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

Bytecode compiled by Kotlin 1.3

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

After careful comparison, the only difference is in the method flags, with 1.3’s bytecode missing the ACC_BRIDGE.

It is well known that ACC_BRIDGE is a method automatically generated by the compiler to implement certain language features. In addition to Kotlin, Java itself also uses ACC_BRIDGE to implement type erasure and other scenarios. I won’t expand the details here, you can try it out.

Is that what’s causing the problem?

Let’s take a look at how our current version of Spring handles method scanning, debugging us into this method

You can see in Spring 4.3.10 that the logic to determine if a user-written method is a method that is not a bridge and is not in the Object class, so it is now clear.

In kotlin1.2, because the compiled getActivityGameByPage$default() contains a bridge, it’s ignored during Spring scans, and in kotlin1.3, because the method signature doesn’t contain a bridge, As a result, it is used as the user’s own writing method to participate in the scan, so that the controller conflicts, and therefore an error is reported in Ambiguous mapping.

How to solve

Isn’t Kotlin going to solve such a serious problem? Yes, kotlin does not solve the problem, so it can only be compatible with the upper layer framework. Spring has made a fix in the subsequent version, adding the judgment of ACC_SYNTHETIC.

In this way, in the new version of Spring, this problem does not exist, and the upgrade is found to fix the problem.

Kotlin compiler source code exploration

With the results of the experiment, it was easy to find the cause in reverse, go to the source code for Kotlin 1.2, then look through the source code and immediately find the corresponding logic. In a commit 4 years ago, one of the guys killed the ACC_BRIDGE flag.

The corresponding source code is modified as follows

Kotlin new version of logic

When I look at the bytecode, I see that the new version of getActivityGameByPage$default() doesn’t have annotations anymore. This fixes the problem at the source.

Now we know the truth. Get ready to go.

summary

Learning a little bit of bytecode is very helpful in solving some of the problems of the JVM and middleware. This is also the motivation for me to explore bytecode. This solves another problem.

If you are interested in bytecode, decompile, software cracking, APM, etc., you can go to my booklet JVM Bytecode from Beginner to Master on the Nuggets or get a machine Press book Understanding JVM Bytecode in Depth.