Java项目构建:Java插件详解、SourceSet配置、编译与测试、Jar与Manifest配置

说实话,Gradle 的 Java 插件是我用得最顺手的部分。它不像 Maven 那样死板,也不像 Ant 那样需要你手写一堆任务。Java 插件提供了一套完整的生命周期,你只需要告诉它「我要做什么」,剩下的它自己搞定。

我个人习惯,接手一个新 Java 项目时,第一件事就是打开 build.gradle,看看有没有 apply plugin: 'java'。没有?那这项目八成是个坑。

Java 插件到底干了什么?

Java 插件本质上是一组预定义的任务和约定。它帮你做了这几件事:

  • 定义了 compileJavaprocessResourcesclassesjartest 等核心任务
  • 引入了 implementationapitestImplementation 等依赖配置
  • 自动创建了 src/main/javasrc/test/java 等标准目录结构
  • 提供了 sourceSets 扩展,让你能自定义源码布局

嗯,这里要注意:Java 插件是基础插件,很多其他插件(比如 Spring Boot 插件)都依赖它。所以如果你用 java 插件,其实已经站在了巨人的肩膀上。

核心要点:Java 插件不只是一个任务集合,它定义了一套完整的构建约定。你遵循约定,它给你效率;你想打破约定,它也给你自由。

SourceSet 配置:别被默认目录绑死

默认情况下,Gradle 认为你的源码在 src/main/java,测试在 src/test/java。但我在项目中遇到过几次特殊情况:

  • 一个老项目,源码放在 src/javasrc/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'
}

这里有个坑:sourceCompatibilitytargetCompatibility 一定要一致。我见过有人设了 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 与 Manifest Manifest 属性配置 Fat Jar / Shadow 可执行 jar 打包 图:Java 项目构建核心知识体系

这张图把 Java 项目构建分成了四个模块。你从上往下看,先理解 Java 插件做了什么,再学会用 SourceSet 调整目录结构,然后优化编译和测试流程,最后用 Jar 配置把成果打包出来。每一步都踩过坑,每一步都有技巧。

我的建议:别一次性把所有配置都写完。先跑通默认配置,再逐步调整。比如先 gradle build 看看能不能编译,再慢慢加 SourceSet、改 Manifest。一步到位往往意味着一步出错。

好了,这一章的内容就到这里。Java 项目构建说复杂也复杂,说简单也简单。你只要掌握了 Java 插件、SourceSet、编译测试、Jar 打包这四个核心点,大部分项目都能应付自如。

公众号:蓝海资料掘金营,微信deep3321