Java项目构建:Java插件详解、SourceSet配置、编译与测试、Jar与Manifest配置
说实话,Gradle 的 Java 插件是我用得最顺手的部分。它不像 Maven 那样死板,也不像 Ant 那样需要你手写一堆任务。Java 插件提供了一套完整的生命周期,你只需要告诉它「我要做什么」,剩下的它自己搞定。
我个人习惯,接手一个新 Java 项目时,第一件事就是打开 build.gradle,看看有没有 apply plugin: 'java'。没有?那这项目八成是个坑。
Java 插件到底干了什么?
Java 插件本质上是一组预定义的任务和约定。它帮你做了这几件事:
- 定义了
compileJava、processResources、classes、jar、test等核心任务 - 引入了
implementation、api、testImplementation等依赖配置 - 自动创建了
src/main/java、src/test/java等标准目录结构 - 提供了
sourceSets扩展,让你能自定义源码布局
嗯,这里要注意:Java 插件是基础插件,很多其他插件(比如 Spring Boot 插件)都依赖它。所以如果你用 java 插件,其实已经站在了巨人的肩膀上。
核心要点:Java 插件不只是一个任务集合,它定义了一套完整的构建约定。你遵循约定,它给你效率;你想打破约定,它也给你自由。
SourceSet 配置:别被默认目录绑死
默认情况下,Gradle 认为你的源码在 src/main/java,测试在 src/test/java。但我在项目中遇到过几次特殊情况:
- 一个老项目,源码放在
src/java和src/test里 - 一个多模块项目,需要把集成测试和单元测试分开
- 一个 Android 项目,需要混合 Java 和 Kotlin 源码
这时候,SourceSet 就派上用场了。说白了,SourceSet 就是告诉 Gradle:「我的源码在哪,资源在哪,输出到哪」。
sourceSets {
main {
java {
srcDirs = ['src/java']
}
resources {
srcDirs = ['src/resources']
}
}
test {
java {
srcDirs = ['src/test']
}
}
// 自定义一个集成测试 SourceSet
integrationTest {
java {
srcDirs = ['src/integration-test/java']
}
resources {
srcDirs = ['src/integration-test/resources']
}
compileClasspath += sourceSets.main.output + configurations.testRuntimeClasspath
runtimeClasspath += output + compileClasspath
}
}
你想想看,有了 SourceSet,你甚至可以定义多个「main」源码集,比如一个给生产环境,一个给开发环境。不过我个人建议,别滥用,保持简单才是王道。
小技巧:如果你自定义了 SourceSet,记得也要配置对应的任务。比如上面定义了 integrationTest,你还需要手动创建 integrationTestTask 来执行它。Gradle 不会自动帮你生成测试任务。
编译与测试:别让构建变慢
编译和测试是 Java 项目中最耗时的两个环节。我曾经在一个大型微服务项目里,每次构建都要跑 5 分钟,后来发现是测试配置出了问题。
先看编译配置:
compileJava {
options.encoding = 'UTF-8'
options.compilerArgs << '-Xlint:unchecked' << '-Xlint:deprecation'
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
compileTestJava {
options.encoding = 'UTF-8'
options.compilerArgs << '-Xlint:all'
}
这里有个坑:sourceCompatibility 和 targetCompatibility 一定要一致。我见过有人设了 source 是 11,target 是 8,结果编译出来的字节码在 Java 8 上跑,但用了 Java 11 的 API,运行时直接崩溃。
再看测试配置:
test {
useJUnitPlatform()
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
forkEvery = 100
testLogging {
events "passed", "skipped", "failed"
exceptionFormat "full"
}
reports {
html.required = true
junitXml.required = true
}
}
为什么我要设置 maxParallelForks?因为单线程跑测试太慢了。但也不是越多越好,我曾经设成和 CPU 核心数一样,结果测试之间互相干扰,出现了一些诡异的失败。后来我改成核心数的一半,稳定多了。
避坑指南:我曾经在测试配置里忘了加 useJUnitPlatform(),结果 JUnit 5 的测试一个都没跑。Gradle 默认用的是 JUnit 4 的引擎,如果你用 JUnit 5,必须显式声明。
Jar 与 Manifest 配置:打包的艺术
打包是 Java 项目的最后一步,也是最容易被忽视的一步。默认的 jar 任务只会把编译后的 class 文件和资源文件打包进去,但实际项目中,我们往往需要更多。
先看一个基础的 Jar 配置:
jar {
archiveBaseName = 'my-app'
archiveVersion = project.version
archiveClassifier = 'full'
manifest {
attributes(
'Main-Class': 'com.example.Main',
'Implementation-Title': 'My Application',
'Implementation-Version': archiveVersion,
'Built-By': System.getProperty('user.name'),
'Build-Timestamp': new Date().format('yyyy-MM-dd HH:mm:ss')
)
}
}
这里 Main-Class 是最关键的属性。没有它,你打出来的 jar 包没法用 java -jar 直接运行。我见过有人把 Main-Class 写成了 Main,结果类路径不对,启动报错。
但光有 Manifest 还不够。如果你的项目依赖了第三方库,你还需要把依赖也打包进去。这时候有两种选择:
- Fat Jar(胖包):把所有依赖的 class 都解压后合并到一个 jar 里
- Shaded Jar(阴影包):把依赖的 class 重命名后打包,避免冲突
我个人更推荐用 Shadow 插件来做这件事:
plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
shadowJar {
archiveBaseName = 'my-app'
archiveClassifier = 'all'
mergeServiceFiles()
manifest {
attributes 'Main-Class': 'com.example.Main'
}
}
你想想看,有了 Shadow 插件,你只需要一个命令 gradle shadowJar,就能生成一个包含所有依赖的可执行 jar。部署的时候直接扔到服务器上,java -jar my-app-all.jar 就完事了。
核心要点:Jar 配置的核心是 Manifest。它就像 jar 包的身份证,告诉 JVM 这个包是谁、从哪里来、要到哪里去。别偷懒,把版本号、构建时间、作者信息都写进去,出了问题好排查。
知识体系总览
下面这张图,是我对本章知识点的总结。你可以把它当作一个快速索引:
这张图把 Java 项目构建分成了四个模块。你从上往下看,先理解 Java 插件做了什么,再学会用 SourceSet 调整目录结构,然后优化编译和测试流程,最后用 Jar 配置把成果打包出来。每一步都踩过坑,每一步都有技巧。
我的建议:别一次性把所有配置都写完。先跑通默认配置,再逐步调整。比如先 gradle build 看看能不能编译,再慢慢加 SourceSet、改 Manifest。一步到位往往意味着一步出错。
好了,这一章的内容就到这里。Java 项目构建说复杂也复杂,说简单也简单。你只要掌握了 Java 插件、SourceSet、编译测试、Jar 打包这四个核心点,大部分项目都能应付自如。