Animal Sniffer: JVM 上的 API 检查器

For Compatibility and Stability

Posted by Hexi on September 27, 2018

起因

最近给 retrofit 写一个子项目 retrofit-processors时遇到了一个问题:我在父项目根目录下执行 mvn compile 非常正常,然后 mvn test 炸了,报的错是

1
Undefined reference: boolean javax.lang.model.element.ExecutableElement.isDefault()

Undefined reference???,那我怎么编译过的?而且 boolean javax.lang.model.element.ExecutableElement.isDefault() 难道不是 Java8 的 API 咩?

折腾了半天才发现这是因为父项目 pom.xml 里面的插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>animal-sniffer-maven-plugin</artifactId>
  <version>${animal.sniffer.version}</version>
  <executions>
    <execution>
      <phase>test</phase>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <signature>
      <groupId>org.codehaus.mojo.signature</groupId>
      <artifactId>java16</artifactId>
      <version>1.1</version>
    </signature>
  </configuration>
</plugin>

这玩意儿是什么意思呢?

大概意思就是在 maven lifecycletest 阶段检查我所使用的部分 API 是否与 signature 中所签名的 API 是否兼容,org.codehaus.mojo.signature.java16:1.1 显然就是指 JRE-1.6API signature 了。

而我在子项目中使用 boolean javax.lang.model.element.ExecutableElement.isDefault() 这个 Java8 才有的 API 导致 test fail

Animal Sniffer

就这样在机缘巧合之下我看了下 Animal Sniffer 的文档:它是 maven 上的一个插件,虽然整个项目有点凉( GitHub 上只有三十个星星),但看起来挺实用的。

Animal Sniffer 一共提供了两个功能,buildcheckbuild 就是根据一个项目生成它的 API signaturecheck 很显然是根据配置的 signature 对项目进行检查。

Build

Animal Sniffer 既可以 build JREAPI signature 也可以 build 其它的 APIs,详情见文档,本文不作介绍。

Check

pom.xml 中添加配置

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
<project>
  ...
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>animal-sniffer-maven-plugin</artifactId>
        <version>1.16</version>
        ...
        <configuration>
          ...
          <signature>
            <groupId>___group id of signature___</groupId>
            <artifactId>___artifact id of signature___</artifactId>
            <version>___version of signature___</version>
          </signature>
          ...
        </configuration>
        ...
      </plugin>
      ...
    </plugins>
    ...
  </build>
  ...
</project>

然后你可以选择

  • 直接执行命令 mvn animal-sniffer:check
  • 在 maven 生命周期中的某个阶段进行检查,未通过检查会导致构建失败。配置如下
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
29
30
31
32
<project>
  ...
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>animal-sniffer-maven-plugin</artifactId>
        <version>1.16</version>
        <executions>
          ...
          <execution>
            <id>___id of execution___</id>
            ...
            <phase>test</phase>
            ...
            <goals>
              <goal>check</goal>
            </goals>
            ...
          </execution>
          ...
        </executions>
      </plugin>
      ...
    </plugins>
    ...
  </build>
  ...
</project>

当然,有些情况下我们应当忽略对一些类或者方法的检查,多见于项目代码中的一些逻辑针对不同版本 API 有不同的实现。如 retrofit 项目本身实现了一个 PlatForm 工厂类

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class Platform {
  private static final Platform PLATFORM = findPlatform();

  static Platform get() {
    return PLATFORM;
  }

  private static Platform findPlatform() {
    try {
      Class.forName("android.os.Build");
      if (Build.VERSION.SDK_INT != 0) {
        return new Android();
      }
    } catch (ClassNotFoundException ignored) {
    }
    try {
      Class.forName("java.util.Optional");
      return new Java8();
    } catch (ClassNotFoundException ignored) {
    }
    return new Platform();
  }

  @Nullable Executor defaultCallbackExecutor() {
    return null;
  }

  CallAdapter.Factory defaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
    if (callbackExecutor != null) {
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }
    return DefaultCallAdapterFactory.INSTANCE;
  }

  boolean isDefaultMethod(Method method) {
    return false;
  }

  @Nullable Object invokeDefaultMethod(Method method, Class<?> declaringClass, Object object,
      @Nullable Object... args) throws Throwable {
    throw new UnsupportedOperationException();
  }

  @IgnoreJRERequirement // Only classloaded and used on Java 8.
  static class Java8 extends Platform {
    @Override boolean isDefaultMethod(Method method) {
      return method.isDefault();
    }

    @Override Object invokeDefaultMethod(Method method, Class<?> declaringClass, Object object,
        @Nullable Object... args) throws Throwable {
      // Because the service interface might not be public, we need to use a MethodHandle lookup
      // that ignores the visibility of the declaringClass.
      Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class, int.class);
      constructor.setAccessible(true);
      return constructor.newInstance(declaringClass, -1 /* trusted */)
          .unreflectSpecial(method, declaringClass)
          .bindTo(object)
          .invokeWithArguments(args);
    }
  }

  static class Android extends Platform {
    @IgnoreJRERequirement // Guarded by API check.
    @Override boolean isDefaultMethod(Method method) {
      if (Build.VERSION.SDK_INT < 24) {
        return false;
      }
      return method.isDefault();
    }

    @Override public Executor defaultCallbackExecutor() {
      return new MainThreadExecutor();
    }

    @Override CallAdapter.Factory defaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
      if (callbackExecutor == null) throw new AssertionError();
      return new ExecutorCallAdapterFactory(callbackExecutor);
    }

    static class MainThreadExecutor implements Executor {
      private final Handler handler = new Handler(Looper.getMainLooper());

      @Override public void execute(Runnable r) {
        handler.post(r);
      }
    }
  }
}

里面就针对不同的 JRE 版本分别实现了 boolean isDefaultMethod(Method method),其中当 JRE 版本为 1.8 时不可避免地要用到只有 JRE-1.8 才有的 boolean java.lang.reflect.Method.isDefault()

这时可以看到上面有一些方法用了 @IgnoreJRERequirement,这个 annotation 来自 org.codehaus.mojo.animal-sniffer-annotations,可以让类或者方法逃过 API 签名检查。

当然也可以在 pom.xml 里配置忽略:

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
<project>
  ...
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>animal-sniffer-maven-plugin</artifactId>
        <version>1.16</version>
        ...
        <configuration>
          ...
          <ignores>
            ...
            <ignore>java.lang.reflect.Method</ignore>
            ...
          </ignores>
          ...
        </configuration>
        ...
      </plugin>
      ...
    </plugins>
    ...
  </build>
  ...
</project>

另外,Animal Sniffer 默认是不检查依赖项目的,如果要确保你的 API 版本跟 JRE 版本兼容,可能需要检查依赖项,即在 pom.xml 加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<project>
  ...
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>animal-sniffer-maven-plugin</artifactId>
        <version>1.16</version>
        ...
        <configuration>
          ...
          <ignoreDependencies>false</ignoreDependencies>
          ...
        </configuration>
        ...
      </plugin>
      ...
    </plugins>
    ...
  </build>
  ...
</project>

后记:我的解决方案

说完了 Animal Sniffer,我们回到最开始的问题,由于我要写的是 annotation processors,所以并不需要兼容 JRE-1.8 之前版本的 API,所以最后在子项目的 pom.xml 里面加上了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>animal-sniffer-maven-plugin</artifactId>
  <version>${animal.sniffer.version}</version>
  <executions>
    <execution>
      <phase>test</phase>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <signature>
      <groupId>org.codehaus.mojo.signature</groupId>
      <artifactId>java18</artifactId>
      <version>1.0</version>
    </signature>
  </configuration>
</plugin>

使用 JRE-1.8API signature 覆盖了父项目的的 JRE-1.6,测试通过

TEST SUCCESS