Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
120: The Gradle Build Lifecycle
I remember the first time I tried to automate a version-stamping process in Gradle. I wanted to calculate a build timestamp and inject it into a properties file, so I just put a println and some logic right at the top of my build.gradle file to make sure it was working. I figured, "I'm running the jar task, so this code will run right before the JAR is built."
The "Why is this printing again?" moment
I wrote something simple. I didn't even create a custom task yet; I just put the logic in the open:
println "DEBUG: Calculating build version timestamp..."
version = "1.0-" + new Date().format("yyyyMMdd")
tasks.register("hello") {
doLast {
println "Hello from the task!"
}
}
I ran ./gradlew hello. I expected to see the timestamp debug message, then the "Hello from the task!" message. That's what happened. Great. But then I ran ./gradlew tasks just to see my available tasks. To my surprise, I saw this in the console:
DEBUG: Calculating build version timestamp...
Tasks:
...
Wait. I didn't run the hello task. I didn't run a build. I just asked Gradle for a list of tasks, and it still executed my "Calculating build version" code. I thought I was losing my mind. I tried ./gradlew clean. Again: DEBUG: Calculating build version timestamp... appeared.
Wait, everything is a configuration?
This is the "aha!" moment where I realized I was fundamentally misunderstanding how Gradle works. I was treating the build.gradle file like a script that executes linearly from top to bottom only when a task is called. It's not. Gradle has a lifecycle, and my print statement was living in the Configuration Phase.
Gradle doesn't just jump to the task you requested. It has to build a "Project Object Model" first. It reads every single build.gradle file in the project to figure out what tasks exist, what their dependencies are, and how they are configured. Anything written outside of a task's action block (like doLast or doFirst) is executed during this phase.
If you put a heavy database call or a complex file-system scan at the top level of your build script, your build will feel sluggish every single time you run any command—even a simple --version check—because you're forcing Gradle to do that work during configuration.
Moving logic into the Execution Phase
I realized that calculating the version timestamp was something that should only happen if I'm actually building the project, not every time I check the task list. So, I moved the logic into a task action.
tasks.register("stampVersion") {
doLast {
println "DEBUG: Calculating build version timestamp..."
// Logic to write to a file would go here
}
}
tasks.named("jar") {
dependsOn("stampVersion")
}
Now, when I run ./gradlew tasks, the debug message is gone. Silence. But when I run ./gradlew jar, I see:
:stampVersion
DEBUG: Calculating build version timestamp...
:jar
By wrapping the code in doLast, I moved it into the Execution Phase. This is the stage where Gradle actually runs the tasks that were identified as necessary based on the dependency graph it built during the Configuration phase.
The hidden first step: Initialization
Now, you might wonder, "How does Gradle even know which build.gradle files to look at?" That's the Initialization Phase. This happens before configuration. Gradle looks for a settings.gradle (or settings.gradle.kts) file.
If you have a multi-project build, this is where you define include 'app', 'library', 'core'. Gradle evaluates the settings file to determine which projects are part of the build. If you put a println in settings.gradle, you'll notice it prints even before the configuration prints. It's the very first domino to fall.
So, the mental model is: Initialization (What projects are we building?) → Configuration (What tasks exist and how are they linked?) → Execution (Run the specific tasks requested).
📋 Practical Task
Exercise: Fixing the "Heavy Configuration" Leak
You have been handed a legacy build script where a previous developer accidentally put a simulated "heavy" operation in the configuration phase, causing the build to lag every time any command is run. Your goal is to move this operation into the execution phase so it only runs when the buildArchive task is called.
Current build.gradle:
// Simulated heavy operation (Configuration Phase)
println "Starting heavy resource validation... (this is slowing down the build!)"
Thread.sleep(2000)
println "Resource validation complete."
tasks.register("buildArchive") {
doLast {
println "Archiving project files..."
}
}
Requirements:
- Modify the script so that the "heavy resource validation" logic (including the
Thread.sleep) only executes when./gradlew buildArchiveis run. - Ensure that running
./gradlew tasksno longer triggers the 2-second delay or the validation print statements. - The validation must happen before the archiving logic inside the
buildArchivetask executes.
There are no comments for now.