Gradle 如何从同一源文件夹生成多个 JAR 包

40

目前我们有一个项目结构,只有一个名为src的源代码文件夹,其中包含三个模块的源代码。我想要做的是:

1)编译源代码。这可以通过sourceSets定义轻松完成:

sourceSets {
    main {
        java {
            srcDir 'src'
        }
    }
}

2) 将编译结果放入三个jar文件中。我通过三个“jar”类型的任务来完成这个操作:

现在我是通过三个单独的任务来进行此操作的:

  • util.jar

  • task utilJar(type: Jar) {
        from(sourceSets.main.output) {
            include "my/util/package/**"
        }
    }
    
  • 客户端.jar

  • task clientJar(type: Jar) {
        from(sourceSets.main.output) {
            include "my/client/package/**"
        }
    }
    
  • 服务器.jar

  • task serverJar(type: Jar) {
        from(sourceSets.main.output) {
            include "**"
        }
        excludes.addAll(utilJar.includes)
        excludes.addAll(clientJar.includes)
    }
    

问题在于 server.jar 应该包含所有未包含在 client.jarutil.jar 中的类。在 ant 构建脚本中,我们通过使用 difference ant 任务来解决此问题。如何在 gradle 中完成这个任务(我的当前方法不起作用)?

也许我的方法完全错误,请给出建议。

附言:目前我们无法更改项目源代码文件夹结构。

4个回答

36

我将在此回答中发布我的工作解决方案(我在gradle论坛上找到了一些提示)。

Gradle中的范围非常奇怪 :) 我认为每个任务定义都创建了某个“Task”类的对象,这在特定情况下类似于“JarTask”。然后我可以从build.gradle脚本的任何地方访问类的任何属性。但是,我发现唯一能够看到包含在jar文件中的模式的地方是任务的from块内。所以,我目前的工作解决方案是:

1)定义一个项目级集合来包含要从server.jar中排除的模式

2)在serverJar任务的from块中排除所有模式。

请参见下面的最终版本。

sourceSets {  
    main {  
        java {  
            srcDir 'src'  
        }  
    }  
} 

// holds classes included into client.jar and util.jar, so they are to be excluded from server.jar
ext.serverExcludes = []

// util.jar
task utilJar(type: Jar) {  
    from(sourceSets.main.output) {  
        include "my/util/package/**" 
        project.ext.serverExcludes.addAll(includes)
    }  
}

// client.jar
task clientJar(type: Jar) {  
    from(sourceSets.main.output) {  
        include "my/client/package/**"
        project.ext.serverExcludes.addAll(includes)
    }  
}

// server.jar
task serverJar(type: Jar) {  
    from(sourceSets.main.output) {  
        exclude project.ext.serverExcludes
    }  
}

2
иҝҷеҫҲжЈ’пјҢдҪҶжҳҜдҪ еҰӮдҪ•д»Һе…¶д»–йЎ№зӣ®еј•з”ЁеҚ•зӢ¬зҡ„.jarж–Ү件 - дҫӢеҰӮпјҢдҪ еҰӮдҪ•е°Ҷclient.jarеј•е…ҘеҸҰдёҖдёӘеӯҗйЎ№зӣ®дёӯпјҹ - z0r
@z0r,我们只是将构件发布到存储库,然后在子项目中将其用作依赖项。P.S. 抱歉回复稍有耽搁。 - vitalidze

32

我认为这种方法是错误的。我建议创建一个包含3个子项目的项目。

project
- util
- server (depends on util)
- client (depends on util)

如果由于某些原因您无法更改类结构,请使用此种类型的构建文件:

settings.gradle

include 'util', 'client', 'server'

build.gradle

subprojects {
    apply plugin: 'java'
}

project(':util') {
    sourceSets {
        main {
            java {
                srcDir '../src'
                include 'util/**'
            }
        }
    }
}

project(':server') {
    sourceSets {
        main {
            java {
                srcDir '../src'
                include 'server/**'
            }
        }
    }
    dependencies {
        compile project(':util')
    }
}

project(':client') {
    sourceSets {
        main {
            java {
                srcDir '../src'
                include 'client/**'
            }
        }
    }
    dependencies {
        compile project(':util')
    }
}

您仍然需要子项目的目录,但源代码都在一个地方,就像您想要的那样。

当您运行gradle assemble时,您将拥有3个带有不同类集的jar文件。这种解决方案的优点在于,我们创建了一个正确依赖关系的Gradle多模块项目,而不仅仅是用于构建jar文件的任务。

请阅读Multi-Project Builds


1
谢谢您的评论。我考虑过这种方法,但最好不要改变目录结构。主要原因是我们有很多SVN分支(超过50个),在目录结构更改后合并它们将是真正的痛苦(您知道,树冲突)。另一个原因是utilclient彼此依赖,所以我想它们应该在单独的模块中,这带来了同样的问题。我知道我们的源代码结构设计混乱,但我相信gradle可以做到ant能做的一切 :) - vitalidze
2
@vitalidze 我提出的解决方案不涉及更改目录结构。额外的空目录是否会成为问题?你是否存在依赖循环? - Grzegorz Żur
1
问题在于类并没有按文件夹(例如客户端、服务器、工具)分开。例如,我将'a/b'和'z/x/y'包含在客户端jar中,工具包括'q/w/e',而服务器应该包括所有其余的类,这些类可能位于'a'、'z'、'z/x'、'q'、'q/w'下面。 - vitalidze
@vitalidze,你仍然可以使用包含和排除模式来完成很多工作。请参阅http://www.gradle.org/docs/current/javadoc/org/gradle/api/tasks/util/PatternFilterable.html。 - Grzegorz Żur
2
我给这个点踩了下去,因为有些情况下你需要一个单一的项目来生成多个构件,而这正是 OP 所寻找的。 - Farrukh Najmi
1
如果您使用的是Gradle >= 3,则compile配置已被弃用,应改为使用implementation。请参阅Migrate to Android Plugin for Gradle 3.0.0 - bejado

4
我们公司也遇到了同样的问题,即难以将遗留代码迁移到“良好”的项目结构中,并且需要从同一代码库构建多个jar包。我们决定定义不同的sourceSets并使用标准Gradle构建每个sourceSet。然后,我们使用迭代器为每个sourceSet添加jar和javadoc任务:
sourceSets.all { SourceSet sourceSet ->
    Task jarTask = tasks.create("jar" + sourceSet.name, Jar.class)
    jarTask.from(sourceSet.output)
    // Configure other jar task properties: group, description, manifest etc

    Task javadocTask = tasks.create("javadoc" + sourceSet.name, Javadoc.class)
    javadocTask.setClasspath(sourceSet.output + sourceSet.compileClasspath)
    javadocTask.setSource(sourceSet.allJava)
    // Extra config for the javadoc task: group, description etc

    Task javadocJarTask = tasks.create("javadocJar" + sourceSet.name, Jar.class)
    javadocJarTask.setClassifier("javadoc") // adds "-javadoc" to the name of the jar
    javadocJarTask.from(javadocTask.outputs)
    // Add extra config: group, description, manifest etc
}

3

我同意接受的答案原则上也是这样。我发现一个项目,客户需要两个JAR文件,本质上是相同的文件,只有Class-Path键在Manifest中不同。

jar {
    manifest {
        attributes(
                "Main-Class": platformMainClass,
                "Implementation-Title": platformDisplayName,
                "Implementation-Description": platformDescription,
                "Platform-Version": platformVersion,
                "Implementation-Version": version,
                "Build-Assembly-User": System.getProperty("user.name"),
                "Build-Assembly-Date": new java.util.Date().toString(),
                "Class-Path": configurations.compile.collect { "lib/"+it.getName() }.join(' ')
        )
    }

    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    exclude( [ 'log4j*.properties', 'uk/gov/acme/secret/product/server/**' ])
}

同样的清单和源代码如下:
task applicationClientJar(type: Jar, description: "Creates the Application  Client JAR file.") {
    dependsOn compileJava
    manifest {
        attributes(
                "Main-Class": platformMainClass,
                "Implementation-Title": platformDisplayName,
                "Implementation-Description": platformDescription,
                "Platform-Version": platformVersion,
                "Implementation-Version": version,
                "Assembly-Date": new java.util.Date().toString()
        )
    }
    archiveName = "acme-client-${platformVersion}.jar"
    destinationDir = file("${buildDir}/libs")
    from sourceSets.main.output

    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    exclude( [ 'log4j*.properties', 'uk/gov/acme/secret/product/server/**'     }

所以Grzegorz的注释是正确的,因为Gradle应该知道有两个不同的具有GAVs的JAR。多模块是首选选项。
compile "uk.gov.acme.secret:acme:1.0"  // CORE
compile "uk.gov.acme.secret:acme-client:1.0"

唯一配置此选项的方法是使用Multi-Module Gradle项目,然后将编译依赖项和/或部署依赖项添加到核心/主项目中。

project(':common:acme-micro-service-webapp') {
    dependencies {
        compile project(':common:acme-core')
    }
}

在“acme-micro-service-webapp”项目中,这确保依赖项“common:acme-core”首先被编译。
PS:我仍在努力找到更好的解决方案。
PS PS:如果您也使用Maven,可能可以挂接“install”任务。

如果项目目录结构必须保持不变(例如某些非标准项目结构),那么这个答案非常巧妙。Grzegorz Żur的答案是一种干净的方法,但需要修改目录结构,这对我来说不是一个选项。我有多个带有main函数的类,我需要每个类都有一个jar文件。我会在build.gradle文件中添加tasks.build.dependsOn(['JarOne', 'JarTwo', 'applicationClientJar']),以确保简单的gradle build将jar任务添加到其过程中。 - Jonathan Komar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接