Gradle 构建工具


#Java 笔记


使用更快的仓库

gradle 配置阿里云的代理仓库

如果使用 gradle 管理java项目,可以在 build.gradle 中加入:

allprojects {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/public/' }
        mavenLocal()
        mavenCentral()
    }
}

gradle 配置腾讯云的代理仓库

类似的,配置如下:

allprojects {
    repositories {
        maven { url 'http://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
        mavenLocal()
        mavenCentral()
    }
}

Java 插件

在当前目录中创建 build.gradle ,内容如下:

apply plugin: 'java'

或者写成:

plugins {
    id 'java'
}

执行下面的命令查看有哪些任务:

$ gradle tasks

> Task :tasks

------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.

Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'gradle-demo-0004'.
components - Displays the components produced by root project 'gradle-demo-0004'. [incubating]
dependencies - Displays all dependencies declared in root project 'gradle-demo-0004'.
dependencyInsight - Displays the insight into a specific dependency in root project 'gradle-demo-0004'.
dependentComponents - Displays the dependent components of components in root project 'gradle-demo-0004'. [incubating]
help - Displays a help message.
model - Displays the configuration model of root project 'gradle-demo-0004'. [incubating]
projects - Displays the sub-projects of root project 'gradle-demo-0004'.
properties - Displays the properties of root project 'gradle-demo-0004'.
tasks - Displays the tasks runnable from root project 'gradle-demo-0004'.

Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.

Rules
-----
Pattern: clean<TaskName>: Cleans the output files of a task.
Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration.
Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration.

To see all tasks and more detail, run gradle tasks --all

To see more detail about a task, run gradle help --task <task>

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

使用 init 初始化 Java 项目

创建 Java Application 项目

执行 gradle init --type java-application,根据提示输入内容即可。

▶ gradle init --type java-application

Select build script DSL:
  1: groovy
  2: kotlin
Enter selection (default: groovy) [1..2] 1

Select test framework:
  1: junit
  2: testng
  3: spock
Enter selection (default: junit) [1..3] 1

Project name (default: gradle-demo-0004): gradle-demo
Source package (default: gradle.demo): demo

BUILD SUCCESSFUL in 33s
2 actionable tasks: 2 executed

创建完成后,目录结构如下:

▶ tree .
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── demo
    │   │       └── App.java
    │   └── resources
    └── test
        ├── java
        │   └── demo
        │       └── AppTest.java
        └── resources

setting.gradle 内容如下:

rootProject.name = 'gradle-demo-0004'

build.gradle 内容如下:

plugins {
    // Apply the java plugin to add support for Java
    id 'java'

    // Apply the application plugin to add support for building an application
    id 'application'
}

repositories {
    // Use jcenter for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

dependencies {
    // This dependency is found on compile classpath of this component and consumers.
    implementation 'com.google.guava:guava:27.0.1-jre'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
}

// Define the main class for the application
mainClassName = 'demo.App'

创建 Java Library 项目

▶ gradle init --type java-library

Select build script DSL:
  1: groovy
  2: kotlin
Enter selection (default: groovy) [1..2] 1

Select test framework:
  1: junit
  2: testng
  3: spock
Enter selection (default: junit) [1..3] 1

Project name (default: gradle-demo-0004): gradle-demo
Source package (default: gradle.demo): demo

BUILD SUCCESSFUL in 19s
2 actionable tasks: 2 executed
▶ tree .
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── demo
    │   │       └── Library.java
    │   └── resources
    └── test
        ├── java
        │   └── demo
        │       └── LibraryTest.java
        └── resources

setting.gradle 内容如下:

rootProject.name = 'gradle-demo'

build.gradle 内容如下:

plugins {
    // Apply the java-library plugin to add support for Java Library
    id 'java-library'
}

repositories {
    // Use jcenter for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

dependencies {
    // This dependency is exported to consumers, that is to say found on their compile classpath.
    api 'org.apache.commons:commons-math3:3.6.1'

    // This dependency is used internally, and not exposed to consumers on their own compile classpath.
    implementation 'com.google.guava:guava:27.0.1-jre'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
}

创建含有多模块(即多个子项目)的项目

示例1:各子项目单独配置

项目结构:

.
├── child1
│   ├── build.gradle
│   └── src
│       └── main
│           └── java
│               └── gradle
│                   └── hello
│                       └── child1
│                           └── Calculate.java
├── child2
│   ├── build.gradle
│   └── src
│       └── main
│           └── java
│               └── gradle
│                   └── hello
│                       └── child2
│                           └── CalculateService.java
├── build.gradle
└── settings.gradle

该示例中没有根项目,只有两个子项目child1child2

根目录文件

build.gradle 内容为空。

setting.gradle 内容如下:

rootProject.name = 'gradle-demo-0001'
include 'child1'
include 'child2'

child1/build.gradle 内容为:

group 'gradle.hello'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

child1 项目

child1 中的 build.gradle 内容如下:

group 'gradle.hello'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

child1 中的 Calculate 类内容为:

package gradle.hello.child1;

public class Calculate {

    public static int add(int a, int b) {
        return a+b;
    }

}

child2 项目

child2 中的 build.gradle 类内容为:

group 'gradle.hello'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile project(':child1')  // 声明对 child1 项目的依赖

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

child2 中的 CalculateService 类内容为:

package gradle.hello.child2;

import gradle.hello.child1.Calculate; // 注意,这个是 child1 中的类

public class CalculateService {

    public static void main(String[] args) {
        int result = Calculate.add(1, 2);
        System.out.println(result);
    }

}

运行 CalculateService ,会输出 3。

查看所有任务(包括子项目的任务)

$ gradle tasks --all

示例2:集中配置子项目

项目结构:

.
├── child1
│   └── src
│       ├── main
│           ├── java
│           │   └── gradle
│           │       └── hello
│           │           └── child1
│           │               └── Calculate.java
│           └── resources
├── child2
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── gradle
│       │   │       └── hello
│       │   │           └── child2
│       │   │               └── CalculateService.java
│       │   └── resources
├── build.gradle
└── settings.gradle

结构和示例1类似,但是 child1 和 child2 中没有 build.gradle 文件。

根目录下的 settings.gradle 内容:

rootProject.name = 'gradle-demo-0002'
include 'child1'
include 'child2'

根目录下的 build.gradle 内容:

group 'gradle.hello'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

subprojects { // 给所有的子项目进行配置
    apply plugin: 'java'

    repositories {
        mavenCentral()
    }

}

// 配置 child1 子项目
project(":child1") {

    dependencies {

        testCompile group: 'junit', name: 'junit', version: '4.12'

    }
}

// 配置 child2 子项目
project(":child2") {

    dependencies {

        compile project(":child1")  // 声明对 child1 的依赖

        testCompile group: 'junit', name: 'junit', version: '4.12'
    }

}

执行 child2 下的 CalculateService ,会输出 3。

示例3:同时配置子项目和根项目

项目结构:

.
├── settings.gradle
├── build.gradle
├── child1
│   └── src
│       ├── main
│           ├── java
│           │   └── gradle
│           │       └── hello
│           │           └── child1
│           │               └── Calculate.java
│           └── resources
├── child2
│   └── src
│       ├── main
│           ├── java
│           │   └── gradle
│           │       └── hello
│           │           └── child2
│           │               └── CalculateService.java
│           └── resources
└── src
    ├── main
        ├── java
        │   └── gradle
        │       └── hello
        │           └── Main.java
        └── resources

结构和示例2类似,但是在根目录下增加了 src 目录,作为根项目的内容。

根项目下 src 目录中的 Main.java 内容如下:

package gradle.hello;

import gradle.hello.child1.Calculate; // 这是 child1 项目中的类

public class Main {

    public static void main(String[] args) {
        int result = Calculate.add(1, 2);
        System.out.println(result);
    }
}

根目录下 settings.gradle 内容如下:

rootProject.name = 'gradle-demo-0003'
include 'child1'
include 'child2'

根目录下 build.gradle 内容如下:

group 'gradle.hello'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8



allprojects { // 给根项目和所有的子项目进行配置
    apply plugin: 'java'

    repositories {
        mavenCentral()
    }

}

project(":child1") {  // 配置子项目 child1

    dependencies {

        testCompile group: 'junit', name: 'junit', version: '4.12'

    }
}

project(":child2") {  // 配置子项目 child2

    dependencies {

        compile project(":child1") // 依赖子项目 child1

        testCompile group: 'junit', name: 'junit', version: '4.12'
    }

}

// 配置根项目(rootProject)
dependencies {

    compile project(":child1") // 依赖子项目 child1

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

运行 Main 类,会输出3。

plugins 和 apply plugin 的异同

我们有时会看到:

plugins {
    id 'java'
}

有时会看到:

apply plugin: 'java'

这两个有什么异同吗 ?

plugins 和 apply plugin 的效果基本是一样的。

  1. plugins 是后来添加的特性。
  2. plugins 中指定的插件必须是 https://plugins.gradle.org/ 存在的。
  3. apply plugin 可以用在 allprojects 和 subprojects 中。

compile 和 testCompile 的区别

compile 和 testCompile 都是用于声明依赖,但适用范围不同。

例如一个项目的结构如下:

▶ tree .
.
├── build.gradle
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── demo
    │   │       └── Calculate.java
    │   └── resources
    └── test
        ├── java
        │   └── demo
        │       └── CalculateTest.java
        └── resources

其中,build.gradle 声明了如下依赖:

dependencies {
    compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

compile 用于声明整个项目的依赖,也就是src/main/javasrc/test/java中都可以适用 compile 声明的依赖。

testCompile 声明的依赖只能用于测试代码,即 src/test/java 中的代码。

运行 Java 测试用例

我们用一个小项目来举例子。

项目结构如下:

▶ tree .
.
├── build.gradle
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── demo
    │   │       └── Calculate.java
    │   └── resources
    └── test
        ├── java
        │   └── demo
        │       └── CalculateTest.java
        └── resources

build.gradle 内容如下:

plugins {
    id 'java'
}

group 'com.example'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

test {
    // 这个配置,是为了让gradle在运行测试时将标准输出等展示出来
    testLogging {
        outputs.upToDateWhen {false}
        showStandardStreams = true
    }
}

src/main目录下的 Calculate 类:

package demo;

public class Calculate {

    public static int add(int a, int b) {
        return a+b;
    }

}

src/test 中的 CalculateTest 测试类:

package demo;

import org.junit.Assert;
import org.junit.Test;

public class CalculateTest {

    @Test
    public void testAdd01() {
        System.out.println("测试 1+2");
        Assert.assertEquals(3, Calculate.add(1, 2));
    }

    @Test
    public void testAdd02() {
        System.out.println("测试 2+2");
        Assert.assertEquals(4, Calculate.add(2, 2));
    }

}

运行整个项目的单测

使用 gradle 执行测试:

▶ gradle test

> Task :test

demo.CalculateTest > testAdd01 STANDARD_OUT
    测试 1+2

demo.CalculateTest > testAdd02 STANDARD_OUT
    测试 2+2

BUILD SUCCESSFUL in 2s
3 actionable tasks: 3 executed

注意,我们在 build.gradle 中配置了:

testLogging {
    outputs.upToDateWhen {false}
    showStandardStreams = true
}

如果不配置这段内容,gradle test的结果是:

▶ gradle test

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 up-to-date

是的,没有什么详细内容。

运行一个测试类

▶ gradle test --tests demo.CalculateTest             

> Task :test

demo.CalculateTest > testAdd01 STANDARD_OUT
    测试 1+2

demo.CalculateTest > testAdd02 STANDARD_OUT
    测试 2+2

BUILD SUCCESSFUL in 1s
3 actionable tasks: 1 executed, 2 up-to-date

运行单个测试

比如运行 CalculateTest 中的 testAdd01:

▶ gradle test --tests demo.CalculateTest.testAdd01 

> Task :test

demo.CalculateTest > testAdd01 STANDARD_OUT
    测试 1+2

BUILD SUCCESSFUL in 1s
3 actionable tasks: 1 executed, 2 up-to-date

Gradle 下载项目依赖

示例 1

在 build.gradle 中添加任务:

task getDeps(type: Copy) {
  from sourceSets.main.runtimeClasspath
  into 'runtime/'
}

运行 gradle getDeps,会将依赖的jar下载到 runtime 目录。

示例 2

在 build.gradle 中添加任务:

task copyDeps(type: Copy) {
    from (configurations.compile + configurations.testCompile) {
        include "*.jar"
        include "*.so", "*.dll"
    }
    into rootProject.rootDir.getAbsolutePath() + "/lib"
}

运行 gradle copyDeps,会将依赖的jar下载到 lib 目录。

参考

将 maven 项目转换为 gradle 项目

进入项目根目录,也就是有 pom.xml 文件的目录,执行:

$ gradle init

会提示要不要转换成 gradle 项目,根据提示操作即可。

操作完成后,会生成 build.gradle、settings.gradle 文件。

清理 Gradle 生成的 build、out 目录

build、out 目录会存放缓存、class文件等。使用 Intellij IDEA 编写代码时,有时会遇到缓存、class文件内容和实际代码不一样的情况,会发生一些很诡异的事情,比如运行结果不符合预期、运行报错等。

这个不一样可能是:

  • 代码中挪动类的位置了,缓存中没挪。
  • 代码中某个资源文件更新了,但是缓存中没更新。
  • 等等

解决办法是删除这些目录。手动删,或者写个 gradle task 删除:

task cleanBuildDir(type: Delete) {
    delete "${projectDir}/build"
    delete "${projectDir}/out"
}

如果一个项目中有多个子项目,那么会有很多 build、out 目录, 可以这样写 task :

allprojects {
    task cleanBuildDir(type: Delete) {
        delete "${projectDir}/build"
        delete "${projectDir}/out"
    }
}

执行顶层项目的 cleanBuildDir 任务即可。

使用 Gradle 将源码打包为 jar

在 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 内容: 窗前明月光


( 本文完 )