使用Java 9的模块化来构建零依赖的原生应用
2018-01-10 15:56
501 查看
摘要:本文通过实例介绍了如果通过Java 9的模块化特性来构建一个独立的、零依赖的可执行程序。以下是译文。
在Java刚刚出现的时候,主流的编程语言要么可以编译为独立的可执行文件(例如C/C++、COBOL),要么运行在解释器中(例如Perl、Tcl)。对于大部分的程序员来说,Java对字节码编译器和运行时解释器的需求让他们开始转变自己的思维。编译模型使得Java比“脚本”语言更适合于业务编程。然而,运行时模型则需要在每台目标机器上部署和使用合适的JVM。
人们对此有些抵触。在早期的网络论坛,以及后来的StackOverflow问题中,为了避免在目标机器上安装Java运行时,开发者们都在寻找某种能将Java应用程序发布为“原生”可执行文件的方法。
解决办法从一开始就有。 Excelsior JET是一个超前的Java编译器,提供了部分C++风格的体验。然而,它的许可费用高达数千美元。当然,也有免费工具,例如Launch4j和JDK
8的javapackager工具。这些工具能帮你把Java运行时环境与启动程序捆绑在一起,以使用该JRE来启动应用程序。但是,嵌入JRE会使应用程序增加大约200MB。由于技术上的原因以及许可问题,应用程序的大小很难降下来。
Java 9中最广为人知的新功能是新的模块化系统,称为Jigsaw项目。简而言之,这个新模块用于隔离代码块及其依赖关系。
这个模块不仅适用于外部库,也适用于Java标准库本身。这意味着应用程序可以声明自己需要标准库的哪些部分,并排除所有其他的部分。
这个功能是通过随JDK一同提供的
应用程序代码及其依赖关系;
一个嵌入式Java运行时环境;
本地启动器(例如,bash脚本或Windows批处理文件),用于通过嵌入式JRE启动应用程序。
https://github.com/steve-perkins/jlink-demo
这个代码库包含一个多项目的Gradle构建。
9兼容:
2
下面将创建
/cli/src/main/java/module-info.java:
2
3
/gui/src/main/java/module-info.java:
2
3
4
5
6
7
这里的CLI应用程序只是对
但并不是所有的应用程序都使用JavaFX,所以这里的GUI应用程序必须声明它对
Java开发人员(包括我自己)需要一些时间来感悟新的标准库模块以及它们包含的内容。 JDK包含的
要使用
2
3
4
5
6
据我所知,Gradle还没有一个插件能够提供干净并且可以无缝集成的
2
生成的包将包含
最后,这些标志
在项目的根目录下,这个Gradle命令建立并链接了两个子项目:
2
由此产生的可部署包可以在以下位置找到:
2
3
我们来看看链接出来的CLI和GUI应用程序的大小,以及精简的嵌入式JRE:
这是在Windows机器上,用64位的JRE打包打出来的结果(Linux包的大小有点大,但比例仍大致相同)。下面是一些注意事项:
作为比较,这个平台上完整的JRE大小是203MB。
用Go编写的“Hello World”命令行程序编译出来是2MB左右。 Hugo是用于发布此博客的网站生成器,它是一个27.2MB大小的Go可执行文件。
对于跨平台的GUI开发,一个典型的Qt或GTK应用程序仅附带大约15MB的Windows DLL。Electron快速开始例程会生成一个131
MB的程序。
说句公道话,一个包含启动脚本的应用程序包并不像“单单一个.EXE程序”那样简单。另外,由于JIT编译器的原因,JRE在启动时相对较为缓慢。
即便如此,Java仍然是一种能够生成大小与其他编译语言相媲美、独立的、零依赖的应用程序的语言(并且比Electron等Web混合程序更为优秀)。此外,Java 9 包含了一个实验性的AOT编译器,这也许可以加速程序的启动。尽管这个
虽然Go在早期的云基础设施CLI工具中备受瞩目(例如Docker,Kubernetes、Consul、Vault等),但Java正在成为一个强有力的替代者,特别是对于具有构建Java经验的项目组。对于跨平台桌面GUI应用程序,我认为JavaFX与Java
9模块化的结合是现在最好的选择。
“为什么没办法创建一个.EXE程序?”
在Java刚刚出现的时候,主流的编程语言要么可以编译为独立的可执行文件(例如C/C++、COBOL),要么运行在解释器中(例如Perl、Tcl)。对于大部分的程序员来说,Java对字节码编译器和运行时解释器的需求让他们开始转变自己的思维。编译模型使得Java比“脚本”语言更适合于业务编程。然而,运行时模型则需要在每台目标机器上部署和使用合适的JVM。人们对此有些抵触。在早期的网络论坛,以及后来的StackOverflow问题中,为了避免在目标机器上安装Java运行时,开发者们都在寻找某种能将Java应用程序发布为“原生”可执行文件的方法。
解决办法从一开始就有。 Excelsior JET是一个超前的Java编译器,提供了部分C++风格的体验。然而,它的许可费用高达数千美元。当然,也有免费工具,例如Launch4j和JDK
8的javapackager工具。这些工具能帮你把Java运行时环境与启动程序捆绑在一起,以使用该JRE来启动应用程序。但是,嵌入JRE会使应用程序增加大约200MB。由于技术上的原因以及许可问题,应用程序的大小很难降下来。
Java 9来了
Java 9中最广为人知的新功能是新的模块化系统,称为Jigsaw项目。简而言之,这个新模块用于隔离代码块及其依赖关系。这个模块不仅适用于外部库,也适用于Java标准库本身。这意味着应用程序可以声明自己需要标准库的哪些部分,并排除所有其他的部分。
这个功能是通过随JDK一同提供的
jlink工具来实现的。乍一看,
jlink类似于
javapackager。它会生成一个包,包括:
应用程序代码及其依赖关系;
一个嵌入式Java运行时环境;
本地启动器(例如,bash脚本或Windows批处理文件),用于通过嵌入式JRE启动应用程序。
jlink建立了“链接时”这个新的可选阶段,它处于编译时和运行时之间,用于执行优化,例如删除不可达的代码。与捆绑整个标准库的
javapackager不同,
jlink把精简过的JRE和仅包含应用程序所需的那些模块捆绑在一起。
示例
jlink和老版本的
javapackager之间的区别非常的大。为了说明这一点,我们来看一个示例项目:
https://github.com/steve-perkins/jlink-demo
(1)创建一个模块化的项目
这个代码库包含一个多项目的Gradle构建。 cli子目录中是一个“Hello World”命令行程序,而
gui是一个JavaFX桌面应用程序。请注意,对于这两者,通过在
build.gradle文件中配置下面这一句来让每个项目与Java
9兼容:
sourceCompatibility = 1.91
2
下面将创建
module-info.java文件,为每个项目设置模块化。
/cli/src/main/java/module-info.java:
module cli { }1
2
3
/gui/src/main/java/module-info.java:
module gui { requires javafx.graphics; requires javafx.controls; exports gui; }1
2
3
4
5
6
7
这里的CLI应用程序只是对
System.out.println()的调用,所以它只依赖于
java.base模块(这是隐式的,不需要声明)。
但并不是所有的应用程序都使用JavaFX,所以这里的GUI应用程序必须声明它对
javafx.graphics和
javafx.controls模块的依赖。而且,由于JavaFX的工作方式不太一样,所以底层库需要访问我们的代码。
export gui这一行赋予了这个库的可见性。
Java开发人员(包括我自己)需要一些时间来感悟新的标准库模块以及它们包含的内容。 JDK包含的
jdeps工具可以帮忙用来解决这个问题。而且只要某个项目被设置为模块化,IntelliJ就能识别出丢失的声明并协助开发人员自动完成它们。即使Eclipse和NetBeans没有类似的支持,它们很快也会添加进去的。
(2)构建一个可执行的JAR
要使用jlink构建一个可部署的包,首先要将应用程序打包成一个可执行的JAR文件。如果项目依赖第三方库,那么需要使用“shaded”或“fat-JAR”插件来生成包含所有依赖关系的单个JAR。我们这个例子只使用了标准库,所以构建一个可执行的JAR是一件非常简单的事情,只需让Gradle的
jar插件包含一个声明可执行类的
META-INF/MANIFEST.MF文件:
jar { manifest { attributes'Main-Class': 'cli.Main' } }1
2
3
4
5
6
(3)运行jlink
据我所知,Gradle还没有一个插件能够提供干净并且可以无缝集成的jlink。所以,我的构建脚本使用
Exec任务来运行该工具。命令行调用是这样的:
[JAVA_HOME]/bin/jlink --module-path libs:[JAVA_HOME]/jmods --add-modules cli --launcher cli=cli/cli.Main --output dist --strip-debug --compress 2 --no-header-files --no-man-pages1
2
--module-path标志类似于CLASSPATH。它告诉工具应该在哪里查找已编译的模块二进制文件(即JAR文件或新的JMOD格式文件)。在这里,我们告诉它寻找项目的
libs子目录(因为这是Gradle放置可执行JAR的地方)以及标准库模块的JDK目录。
--add-modules标志用于声明哪些模块要添加到结果包中。我们只需要声明自己项目的模块即可(
cli或
gui),因为它所依赖的模块将作为传递依赖被引入。
生成的包将包含
/bin子目录,用于执行应用程序的bash脚本或Windows批处理文件。
--launcher标志允许你为这个脚本指定一个名字,以及它应该调用哪个Java类(这看起来有点多余,因为这已经在可执行JAR中指定了)。在上面的命令中,我们将创建一个名为
bin/cli的脚本,它将调用模块
cli中的
cli.Main类。
--output标志直观地指定了放置结果包的子目录。在这里,我们使用一个名为
dist的目标目录。
最后,这些标志
--strip-debug,
--compress 2,
--no-header-files和
--no-man-pages是我所做的一些优化,以减少产生的包的大小。
在项目的根目录下,这个Gradle命令建立并链接了两个子项目:
./gradlew linkAll1
2
由此产生的可部署包可以在以下位置找到:
[PROJECT_ROOT]/cli/build/dist [PROJECT_ROOT]/gui/build/dist1
2
3
结果
我们来看看链接出来的CLI和GUI应用程序的大小,以及精简的嵌入式JRE:应用 | 原始大小 | 用7-zip压缩后的大小 |
---|---|---|
cli | 21.7 MB | 10.8 MB |
gui | 45.8 MB | 29.1 MB |
作为比较,这个平台上完整的JRE大小是203MB。
用Go编写的“Hello World”命令行程序编译出来是2MB左右。 Hugo是用于发布此博客的网站生成器,它是一个27.2MB大小的Go可执行文件。
对于跨平台的GUI开发,一个典型的Qt或GTK应用程序仅附带大约15MB的Windows DLL。Electron快速开始例程会生成一个131
MB的程序。
结论
说句公道话,一个包含启动脚本的应用程序包并不像“单单一个.EXE程序”那样简单。另外,由于JIT编译器的原因,JRE在启动时相对较为缓慢。即便如此,Java仍然是一种能够生成大小与其他编译语言相媲美、独立的、零依赖的应用程序的语言(并且比Electron等Web混合程序更为优秀)。此外,Java 9 包含了一个实验性的AOT编译器,这也许可以加速程序的启动。尽管这个
jaotc工具最初只适用于64位Linux平台,但它很快就会扩展到其他平台。
虽然Go在早期的云基础设施CLI工具中备受瞩目(例如Docker,Kubernetes、Consul、Vault等),但Java正在成为一个强有力的替代者,特别是对于具有构建Java经验的项目组。对于跨平台桌面GUI应用程序,我认为JavaFX与Java
9模块化的结合是现在最好的选择。
相关文章推荐
- 使用Java 9的模块化来构建零依赖的原生应用
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- .NET程序员也用JAVA:使用BlazeDS,SpringFramework,MySql,Flex构建RIA应用 part 3 :Flex及As 3代码编写
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 基于ES6,使用React、Webpack、Babel构建模块化JavaScript应用
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- Java8新特性--使用CompletableFuture构建异步应用
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- .NET程序员也用JAVA:使用BlazeDS,SpringFramework,MySql,Flex构建RIA应用 part 1 :环境搭建.
- Java8新特性8--使用CompletableFuture构建异步应用
- gradle 构建java应用 使用笔记
- React Native:使用 JavaScript 构建原生应用 详细剖析
- Gradle构建Java Web应用:Servlet依赖与Tomcat插件(转)
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 使用 JSR 356 API 构建 Java WebSocket 应用
- Gradle构建Java Web应用:Servlet依赖与Tomcat插件
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 非web的JAVA应用使用Spring的依赖注入