
4.3 获取依赖库
如果读者通过Maven(详见4.5.3节)或者Gradle(详见4.5.4节)插件方式执行静态编译,则不需要提前获取依赖库,可以跳过本节。只有在用脚本编译时才需要手动收集依赖库的信息。
依赖库作为一个整体概念,是运行目标应用程序时不可或缺的一部分,但是在Java的动态特性(如反射和动态类加载)的支持下,有些依赖项在编译时并不需要,只要在运行时能够找到即可。例如下面这种通过反射调用目标函数的情况,编译时并不会依赖a.b.C类,只要运行时的classpath中提供了该类即可。
Class c = Class.forName("a.b.C"); Method m = c.getDeclaredMethod("m"); m.invoke();
与之相反,在执行目标应用程序时也可以不需要编译时的全部依赖库,只要运行时执行不到缺失依赖的那部分程序就不会有问题。例如在代码清单4-3所示的代码中,只要代码执行不到else分支,就不需要在运行时的classpath上提供Foo类。
代码清单4-3 运行时不完全依赖样例
if (someCondition){ System.out.println("Hello"); }else{ //实际场景不会进入 Foo.bar(); }
我们可以认为,Java程序对库的依赖,无论在编译时还是运行时都是不完全的,但是静态编译出于封闭性的要求必须在编译时获得所有依赖,一旦完成编译,依赖就被内化成了二进制可执行程序的一部分,运行时也无法再变化。因此在准备静态编译目标应用程序时,必须先准备好目标应用程序的编译时和运行时两部分的依赖。
当今的开发实践中广泛使用了如Maven之类的自动化项目构建工具,所有应用程序需要的依赖库都添加在了Maven的pom.xml文件中。pom.xml的<dependency>项的<scope>子项用来指定依赖的类型,compile表示编译时依赖,runtime表示运行时依赖。我们只需要使用Maven的插件就可以轻松准备好所有的依赖库,例如Maven有多个插件可以将应用程序和所有依赖库全部打包到一个被称为Fat Jar或者Uber Jar的可执行jar包中,但是GraalVM JDK在静态编译时并不保证能识别Fat Jar中的存储形式,还需要将依赖库解压出来;也有插件如maven-dependency-plugin可以将在Maven项目中的所有依赖都复制到一个指定目录中。本书建议使用后者,以省去再次解压Fat Jar的工作。如使用maven-dependency-plugin,则可以在应用程序的源码根目录里通过下面两行命令得到所有依赖库。
mvn package LIB=`find $LIB_DIR | tr '\n' ':'`
这里假设maven-dependency-plugin插件指定的依赖输出目录是$LIB_DIR,LIB变量即代表所有的依赖库。
总体来说,Maven的相关打包插件已经在实践上帮助解决了依赖库问题,但是在概念上我们仍然需要理解编译时依赖和运行时依赖的差别。