在 build.gradle 中应用 java
插件后,默认会有一个 jar 任务用于打包。在 Gradle 项目根目录执行下面命令即可:
$ gradle jar
但是,该任务默认不会将依赖打包进去(也就是不会打包成 fat jar)。如果要将依赖打包进去,可以修改 jar 任务的配置:
jar {
manifest {
attributes 'Main-Class': 'com.example.App' // 启动类全路径,需要根据项目自定义,或者不配置
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
}
或者自定义一个 fatJar 任务:
task fatJar(type: Jar) {
manifest {
attributes 'Main-Class': 'com.example.App'
}
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
测试示例1
Java 版本:1.8 Gradle 版本: 5.2
项目结构:
.
├── build.gradle
├── settings.gradle
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ ├── App.java
│ │ └── Utils.java
│ └── resources
└── test
├── java
└── resources
settings.gradle :
rootProject.name = 'test-gradle-jar-01'
build.gradle :
plugins {
id 'java'
}
group 'com.example'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
}
Utils.java :
package com.example;
public class Utils {
public static int add(int a, int b) {
return a + b;
}
}
App.java :
package com.example;
public class App {
public static void main(String[] args) {
System.out.println("1 + 1 = " + Utils.add(1, 1));
}
}
在项目根目录执行 gradle jar
命令,然后可以在 build/libs
目录中找到打包好的 jar :
$ ls build/libs
test-gradle-jar-01-1.0-SNAPSHOT.jar
# 查看 jar 中文件
$ jar tf build/libs/test-gradle-jar-01-1.0-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/App.class
com/example/Utils.class
# 执行
$ java -jar build/libs/test-gradle-jar-01-1.0-SNAPSHOT.jar
build/libs/test-gradle-jar-01-1.0-SNAPSHOT.jar中没有主清单属性
# 上面的执行报错了,原因是 MANIFEST.MF 没指定主类,
$ unzip -q -c build/libs/test-gradle-jar-01-1.0-SNAPSHOT.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
# 执行
$ java -cp build/libs/test-gradle-jar-01-1.0-SNAPSHOT.jar com.example.App
1 + 1 = 2
测试示例2
项目结构:
.
├── build.gradle
├── settings.gradle
└── src
└── main
├── java
│ └── com
│ └── example
│ └── App.java
└── resources
└── test.txt
settings.gradle :
rootProject.name = 'test-gradle-jar-02'
build.gradle :
plugins {
id 'java'
}
group 'com.example'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
maven { url 'http://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
mavenLocal()
mavenCentral()
}
dependencies {
compile group: 'com.google.guava', name: 'guava', version: '28.2-jre'
}
test.txt :
Hello World
App.java :
package com.example;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import java.io.IOException;
import java.net.URL;
public class App {
public static void main(String[] args) throws IOException {
URL url = Resources.getResource("test.txt");
String content = Resources.toString(url, Charsets.UTF_8);
System.out.println(content);
}
}
使用 gradle jar 打包后,执行 App 类会失败。
$ java -cp build/libs/test-gradle-jar-02-1.0-SNAPSHOT.jar com.example.App
Exception in thread "main" java.lang.NoClassDefFoundError: com/google/common/io/Resources
at com.example.App.main(App.java:12)
Caused by: java.lang.ClassNotFoundException: com.google.common.io.Resources
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
没找到 guava 中的 Resources 类,所以报错了。
解决方法1:在 classpath 中指定依赖路径
在 build.gradle 中增加下面的任务:
task getDeps(type: Copy) {
from sourceSets.main.runtimeClasspath
into 'runtime/'
}
执行 gradle getDeps
下载依赖到 runtime
目录。
运行 App 类:
# 方式1
$ java -cp build/libs/test-gradle-jar-02-1.0-SNAPSHOT.jar:runtime/guava-28.2-jre.jar com.example.App
Hello World
# 方式2
$ java -cp build/libs/test-gradle-jar-02-1.0-SNAPSHOT.jar:"runtime/*" com.example.App
Hello World
解决方法2:构造 fat jar
将 build.gradle 修改如下:
plugins {
id 'java'
}
group 'com.example'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
maven { url 'http://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
mavenLocal()
mavenCentral()
}
task getDeps(type: Copy) {
from sourceSets.main.runtimeClasspath
into 'runtime/'
}
// 构建 fat jar
task fatJar(type: Jar) {
manifest {
attributes 'Main-Class': 'com.example.App'
}
baseName = project.name + '-fatJar'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
dependencies {
compile group: 'com.google.guava', name: 'guava', version: '28.2-jre'
}
执行 gradle fatJar
,在 build/libs 目录会生成test-gradle-jar-02-fatJar-1.0-SNAPSHOT.jar
。
执行 App 类:
$ java -cp build/libs/test-gradle-jar-02-fatJar-1.0-SNAPSHOT.jar com.example.App
Hello World
因为在清单文件中指定了主类,也可以用下面的方式执行:
$ java -jar build/libs/test-gradle-jar-02-fatJar-1.0-SNAPSHOT.jar
Hello World
fatJar 长什么样子 ?
$ jar tf build/libs/test-gradle-jar-02-fatJar-1.0-SNAPSHOT.jar
META-INF/MANIFEST.MF
com/google/common/util/concurrent/Striped.class
... 省略部分内容
com/google/j2objc/annotations/Weak.class
com/example/
com/example/App.class
test.txt
看一下 META-INF/MANIFEST.MF 的内容:
$ unzip -q -c build/libs/test-gradle-jar-02-fatJar-1.0-SNAPSHOT.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: com.example.App
测试示例3
这个一个多模块的项目。
项目结构:
.
├── build.gradle
├── settings.gradle
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── App.java
│ └── resources
│ └── test.txt
└── sub-project
└── src
└── main
├── java
│ └── com
│ └── example
│ └── sub
│ └── Utils.java
└── resources
├── test.txt
└── test2.txt
settings.gradle :
rootProject.name = 'test-gradle-jar-03'
include 'sub-project'
build.gradle :
plugins {
id 'java'
}
group 'com.example'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
allprojects {
apply plugin: 'java'
repositories {
maven { url 'http://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
mavenLocal()
mavenCentral()
}
}
jar {
manifest {
attributes 'Main-Class': 'com.example.App'
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
}
project(":sub-project") {
dependencies {
compile group: 'com.google.guava', name: 'guava', version: '28.2-jre'
}
}
dependencies {
compile project(":sub-project")
}
sub-project/src
目录下的 test.txt :
子项目: 你好
sub-project/src
目录下的 test2.txt :
子项目: 你好2
sub-project/src
目录下的 Utils.java :
package com.example.sub;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import java.io.IOException;
import java.net.URL;
public class Utils {
public static int add(int a, int b) {
return a + b;
}
public static String readResourceFileContent(String path) {
try {
URL url = Resources.getResource(path);
return Resources.toString(url, Charsets.UTF_8);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
src
目录下的 test.txt :
Hello World
src
目录下的 App.java :
package com.example;
import com.example.sub.Utils;
public class App {
public static void main(String[] args) {
System.out.println("test.txt 内容: " + Utils.readResourceFileContent("test.txt"));
System.out.println("test2.txt 内容: " + Utils.readResourceFileContent("test2.txt"));
System.out.println("1+1 = " + Utils.add(1, 1));
}
}
执行 gradle jar
打包。
执行 jar:
$ java -jar build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar
test.txt 内容: 子项目: 你好
test2.txt 内容: 子项目: 你好2
1+1 = 2
题外话:jar 中出现同路径、同名文件怎么办
上面的代码中,有两个 test.txt 文件。
在执行jar时,发现 test.txt 的内容是 sub-project 下的文件内容:
$ java -jar build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar
test.txt 内容: 子项目: 你好
test2.txt 内容: 子项目: 你好2
1+1 = 2
这并不意味着只有 sub-project 下的 test.txt 被放到了 jar 中,实际上两个 test.txt 都被打包进去了。
$ jar tf build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar | grep "test"
test.txt
test.txt
test2.txt
为什么 ? jar 其实使用 zip 打包,而zip内部支持同路径、同名文件出现多次。不过我们的操作系统只允许一次,所以讲jar解压后,只会保留一个 test.txt 。java 在运行时也只会保留一个。
guava 的 Resources.getResource 底层用的 ClassLoader 下的 getResource 方法,该方法只会返回一个 URL。而 ClassLoader 下的 getResources 可以返回多个 URL 。我们用 getResources 测试下。
增加 App2.java 类:
package com.example;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
public class App2 {
public static void main(String[] args) throws IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 符合要求的文件可能不止一个
Enumeration<URL> urlList = classLoader.getResources("test.txt");
while (urlList.hasMoreElements()) {
URL url = urlList.nextElement();
System.out.printf("%s 内容: %s\n", url.getPath(), Resources.toString(url, Charsets.UTF_8));
}
}
}
使用 gradle jar
打包后执行 App2 :
$ java -cp build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar com.example.App2
file:/path/to/build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar!/test.txt 内容: 子项目: 你好
但是,在 Intellij IDEA 中执行 App2.java ,会输出两个:
/path/to/out/production/resources/test.txt 内容: Hello World
/path/to/sub-project/out/production/resources/test.txt 内容: 子项目: 你好
这是IDE的运行机制不同导致的,IDEA 在执行 App2.java 的 java -classpath
参数中, 有下面一段内容:
/path/to/out/production/classes:/path/to/out/production/resources:/path/to/sub-project/out/production/classes:/path/to/sub-project/out/production/resources:
这意味着同一个jar、同一个目录下只能有一个同名文件生效。
我们再测试下。
在 build/libs
目录中创建 test.txt ,内容是:
窗前明月光
再次执行 jar 中的 App2,但 classpath 中增加该目录:
$ java -cp build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar:build/libs/ com.example.App2
file:/path/to/build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar!/test.txt 内容: 子项目: 你好
/path/to/build/libs/test.txt 内容: 窗前明月光
如果再把 test.txt 打包到test.jar
中,再执行 App2 :
$ java -cp build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar:build/libs/:build/libs/test.jar com.example.App2
file:/path/to/build/libs/test-gradle-jar-03-1.0-SNAPSHOT.jar!/test.txt 内容: 子项目: 你好
/path/to/build/libs/test.txt 内容: 窗前明月光
file:/path/to/build/libs/test.jar!/test.txt 内容: 窗前明月光