groovy静态编译容易引发的问题

Posted by hcy on October 16, 2021

groovy静态编译容易引发的问题

groovy为了提高性能,有一个静态编译的注解@CompileStatic,在类上添加该注解后,编译出来的class文件会更加静态化,更像java文件编译出来的class字节码。 但是因此会导致一些问题,今天我就遇到一个,下面将其记录下来。

众所周知,groovy是动态类型的,如下面重载的函数,使用java调用和groovy调用结果是不同的。

1. 测试groovy非静态编译

在groovy中,定义变量时使用的是Object,是在调用时决定调用哪一个方法的,下面两次调用test方法,均能正确识别出对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test {
    static void main(String[] args) {
        Object param = "a"
        test(param)   //---> String

        Object param2 = 1
        test(param2)   //---> Integer
    }

    static void test(Object b) {
        println "Object"
    }

    static void test(String a) {
        println "String"
    }

    static void test(Integer b) {
        println "Integer"
    }
}

2.测试java

上面同样的代码,使用java写就会变得不一样,因为java是在编译期间就决定应该调用哪个方法的,下面三个方法中,只会调用参数为Object的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test2 {

    public static void main(String[] args) {
        Object param = "a";
        test(param);   //---> Object

        Object param2 = 1;
        test(param2);   //---> Object

    }

    static void test(Object a) {
        System.out.println("Object");
    }

    static void test(String b) {
        System.out.println("String");
    }

    static void test(Integer b) {
        System.out.println("Integer");
    }
}

3.测试groovy静态编译

现在已经明白groovy和java调用的区别了,那如果将groovy进行静态编译后,结果是什么样子呢? 使用下面代码测试后,发现,groovy启用静态编译后仍然能够识别到对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@CompileStatic
class Test {

    static void main(String[] args) {
        Object param = "a"
        test(param)   //---> String

        Object param2 = 1
        test(param2)   //---> Integer
    }

    static void test(Object b) {
        println "Object"
    }

    static void test(String a) {
        println "String"
    }

    static void test(Integer b) {
        println "Integer"
    }
}

4.加大难度测试groovy静态编译

既然添加静态编译注解后,为啥编译出来的代码的运行行为仍然和非静态编译的groovy效果一样呢?我怀疑是代码太简单了,groovy能识别出类型,我将情况弄得再复杂一些。 为了给groovy编译器增加难度,我将参数先存储到list里,再取出,看groovy能否识别。 经过下面的测试结果,完全出乎我的预期,groovy将Integer强转成String了,他做了转换。即使参数是Date对象,他仍然将其转成了字符串,这种转换在大部分情况下都是不符合正常人预期的。 大部分情况下会导致问题。而移除@CompileStatic注解后,因为是在运行时判断类型的,所以能够识别出正确的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@CompileStatic
class Test {

    static void main(String[] args) {
        List list = Arrays.asList("a", 1,new Date())
        test(list.get(0))   //---> String

        test(list.get(1))   //---> String
        
        test(list.get(2))   //---> String
    }

    static void test(Object b) {
        println "Object"
    }

    static void test(String a) {
        println "String"
    }

    static void test(Integer b) {
        println "Integer"
    }
}

上面这种不符合预期的方法查找就是产生bug的根源,并且idea 点击ctrl左键跳转的方法,与实际选择的方法不一致,这也增加了查找bug的难度。所以我建议减少使用静态编译注解的使用,虽然其能够提高点性能,但可能结果是无法预期的。

5.测试java中正确,groovy中不正确的例子。内层闭包使用外层闭包变量,无法正确识别变量类型

下面的代码中,我们使用了静态编译,其中map操作符中的pop参数是String类型的,然而真实调用时,触发testFunction的却是参数为Object的。 这是因为我们在使用pop参数时,没有在当前闭包中使用,而是再创建了一个闭包,我们在内层闭包中使用pop时,没有将pop识别成String类型,而在java中因为是强类型,是可以识别的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@CompileStatic
class Test {

    static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c")

        Object response = list.stream()
                .map({ pop ->
                    return {
                        testFunction(pop)
                        return "sss"
                    }.call()
                })
                .collect(Collectors.toList());

        System.out.println(response)
    }

    static void testFunction(Object a) {   //此函数被调用
        System.out.println("object");
    }

    static void testFunction(String a) {
        System.out.println("String");
    }
}

将上面的代码中添加一行,在第一个闭包中使用一下pop变量,那么就能正确识别到变量pop是String类型,从而调用参数为String的testFunction方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@CompileStatic
class Test {

    static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c")

        Object response = list.stream()
                .map({ pop ->
                    def a =  pop    //在外层闭包中使用一次pop变量
                    return {
                        testFunction(pop)
                        return "sss"
                    }.call()
                })
                .collect(Collectors.toList());

        System.out.println(response)
    }

    static void testFunction(Object a) {
        System.out.println("object");
    }

    static void testFunction(String a) {  //此函数被调用
        System.out.println("String");
    }
}

第五条描述的现象(静态编译下,内层闭包直接使用外层闭包参数,导致参数类型无法识别)也是我今天第一次发现。我还没有查找资料,我猜应该是bug吧,毕竟这样与java差距太大,不符合常人预期,后面有发现我再继续更新。


转载请注明出处:https://www.huangchaoyu.com/2021/10/16/groovy静态编译容易引发的问题/