第23章:日志与调试——让构建过程不再黑盒

说实话,我刚用Gradle那会儿,最头疼的就是构建失败时那一堆堆的堆栈信息。你想想看,几百行输出,真正有用的就那么几行。后来我才明白,日志和调试不是事后诸葛亮,而是构建脚本开发的核心技能。

这一章,我就把自己这些年积累的日志配置、调试技巧、异常处理经验,一股脑儿分享给你。

23.1 日志级别:从安静到话痨

Gradle的日志系统,说白了就是六个级别。我习惯把它们分成两组:

级别 命令行参数 用途
ERROR -q / --quiet 只看错误信息
WARN -w / --warn 警告+错误
LIFECYCLE (默认) 构建生命周期信息
INFO -i / --info 详细信息
DEBUG -d / --debug 调试信息(最详细)

我个人最常用的组合是:

  • 日常构建:默认LIFECYCLE级别,干净利落
  • 排查依赖问题--info,看依赖解析过程
  • 调试自定义Task--debug,但只针对特定包
小技巧:别一上来就用--debug。我曾经在CI上开了debug,结果日志文件直接飙到200MB,把磁盘撑爆了。先加--info试试,不够再上debug。

23.2 自定义日志输出:让日志说人话

默认的日志输出格式,说实话不太友好。我习惯在build.gradle里加一些自定义配置。

23.2.1 使用logger对象

在Task里,你可以直接用logger对象输出:

task checkConfig {
    doLast {
        logger.lifecycle("开始检查配置文件...")
        logger.info("当前环境: ${project.property('env')}")
        logger.debug("详细参数: ${project.properties}")
        
        if (someCondition) {
            logger.warn("配置项缺失,使用默认值")
        }
    }
}

嗯,这里要注意:logger.lifecycle是默认可见的,logger.info需要加-i才能看到。我刚开始总搞混,后来养成了习惯——关键步骤用lifecycle,细节用info。

23.2.2 自定义日志格式

如果你觉得默认格式太啰嗦,可以自己定义:

// 在build.gradle顶部
gradle.services.get(LoggingManagerInternal).level = LogLevel.INFO

// 自定义输出格式
gradle.addListener(new TaskExecutionListener() {
    void beforeExecute(Task task) {
        println "[${new Date().format('HH:mm:ss')}] 开始: ${task.name}"
    }
    void afterExecute(Task task, TaskState state) {
        if (state.failure) {
            println "[${new Date().format('HH:mm:ss')}] 失败: ${task.name}"
        } else {
            println "[${new Date().format('HH:mm:ss')}] 完成: ${task.name}"
        }
    }
})

避坑指南:我曾经在自定义日志里用了System.out.println,结果发现输出顺序和实际执行顺序对不上。后来改用logger对象,问题就解决了。记住:在Gradle里,永远用logger,别用System.out。

23.3 调试构建脚本:找到问题根源

构建脚本出问题,最怕的就是「明明没报错,但结果不对」。我总结了三个调试手段。

23.3.1 使用--stacktrace

这个参数能打印完整的堆栈信息:

gradle build --stacktrace

但说实话,堆栈信息太长了。我一般配合--info一起用:

gradle build -i --stacktrace 2>&1 | grep -E "(ERROR|Caused by|at org.gradle)"

23.3.2 打印变量值

最简单的调试方式,就是在关键位置打印变量:

task debugDependencies {
    doLast {
        // 打印所有依赖
        configurations.each { config ->
            println "配置: ${config.name}"
            config.allDependencies.each { dep ->
                println "  -> ${dep.group}:${dep.name}:${dep.version}"
            }
        }
        
        // 打印项目属性
        println "项目属性:"
        project.properties.each { key, value ->
            println "  ${key} = ${value}"
        }
    }
}

23.3.3 使用Gradle内置的调试工具

Gradle 7.0+ 提供了--scan参数,能生成构建扫描报告:

gradle build --scan

这个报告会告诉你:

  • 每个Task的执行时间
  • 依赖解析的详细信息
  • 缓存命中情况
  • 插件加载顺序
我的经验:构建扫描报告特别适合排查「构建越来越慢」的问题。有一次我发现某个Task执行了3分钟,点进去一看,原来是在下载一个巨大的依赖包。后来改成本地缓存,构建时间直接降到30秒。

23.4 异常处理:优雅地处理构建失败

构建脚本里的异常处理,和普通Java代码不太一样。你不能随便try-catch,因为Gradle有自己的异常处理机制。

23.4.1 使用GradleException

当检测到配置错误时,我习惯抛出GradleException

task validateConfig {
    doFirst {
        def requiredProps = ['jdkVersion', 'outputDir']
        requiredProps.each { prop ->
            if (!project.hasProperty(prop)) {
                throw new GradleException(
                    "缺少必要属性: ${prop}。请在gradle.properties中配置"
                )
            }
        }
    }
}

23.4.2 使用StopExecutionException

如果你想停止当前Task,但不影响其他Task:

task checkDiskSpace {
    doLast {
        def freeSpace = new File('/').freeSpace
        if (freeSpace < 1024 * 1024 * 1024) { // 小于1GB
            throw new StopExecutionException(
                "磁盘空间不足,当前剩余: ${freeSpace / 1024 / 1024 / 1024}GB"
            )
        }
        println "磁盘空间充足"
    }
}

23.4.3 全局异常处理

你可以注册一个全局的异常处理器:

gradle.addBuildListener(new BuildAdapter() {
    void buildFinished(BuildResult result) {
        if (result.failure) {
            println "构建失败,原因: ${result.failure.message}"
            // 可以在这里发送通知、记录日志等
        }
    }
})
注意:全局异常处理器里不要做耗时操作。我曾经在里面写了个发送邮件的逻辑,结果每次构建失败都要等30秒才能看到错误信息。后来改成异步发送,问题就解决了。

23.5 知识体系总览

下面这张图,是我对日志与调试的整体理解:

Gradle日志与调试知识体系 日志级别 ERROR → DEBUG 自定义输出 logger / 格式 / 监听器 调试手段 stacktrace / 打印 / scan -q / -w / 默认 / -i / -d logger对象 / 格式定制 / 监听器 --stacktrace / 打印变量 / --scan 异常处理 GradleException StopExecutionException 全局异常处理器 核心原则:日志要清晰,调试要精准,异常要优雅

这张图把日志与调试分成了四个维度:日志级别控制输出量,自定义输出让信息更可读,调试手段帮你定位问题,异常处理则让构建更健壮。这四个维度缺一不可。

23.6 实战:一个完整的日志配置示例

最后,我给你看一个我项目里实际在用的日志配置:

// build.gradle
import org.gradle.internal.logging.LoggingManagerInternal

// 1. 设置日志级别
gradle.startParameter.logLevel = LogLevel.INFO

// 2. 自定义日志输出
gradle.addListener(new TaskExecutionListener() {
    private long startTime
    
    void beforeExecute(Task task) {
        startTime = System.currentTimeMillis()
        logger.lifecycle "▶ 执行: ${task.path}"
    }
    
    void afterExecute(Task task, TaskState state) {
        def duration = System.currentTimeMillis() - startTime
        if (state.failure) {
            logger.error "✗ 失败: ${task.path} (${duration}ms)"
        } else if (state.skipped) {
            logger.warn "○ 跳过: ${task.path}"
        } else {
            logger.lifecycle "✓ 完成: ${task.path} (${duration}ms)"
        }
    }
})

// 3. 异常处理
gradle.addBuildListener(new BuildAdapter() {
    void buildFinished(BuildResult result) {
        if (result.failure) {
            logger.error "构建失败,请检查日志"
            // 可以在这里做清理工作
        }
    }
})

这个配置我用了两年多,效果很好。每次构建都能看到清晰的执行流程,出问题也能快速定位。

最后说一句:日志和调试不是锦上添花,而是构建脚本开发的必备技能。你花10分钟配置好日志,未来可能省下10小时的排查时间。这笔账,怎么算都划算。


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