Gradle: Jackson NoClassDefFoundError on Upgrade
Jackson NoClassDefFoundError in WAR after Spring Boot upgrade? Gradle picks highest version but third-party overrides.
- Gradle build script is a declarative configuration file (build.gradle or build.gradle.kts) that defines project build, test, and deployment steps
- Uses Directed Acyclic Graph (DAG) to determine optimal task execution order
- Three-phase lifecycle: Initialization, Configuration, Execution — code outside doLast runs in Configuration phase
- Performance insight: Incremental builds skip unchanged tasks, reducing build times by 70-90% in large projects
- Production insight: Configuration phase slowdowns (network calls, file I/O) affect every Gradle command, even
gradle tasks - Biggest mistake: Treating build script as imperative code instead of declarative DSL, leading to slow builds and maintainability issues
Think of a Gradle Build Script as a high-tech recipe for your software. Instead of just listing ingredients (dependencies), it provides a set of smart instructions on how to mix, cook, and package them. If the kitchen (your environment) changes, the script adapts to ensure the final dish (your JAR file) always tastes exactly the same.
Gradle Build Script Basics is a fundamental concept in Java and Spring Boot development. It moves beyond the rigid XML structure of Maven to provide a flexible, code-centric approach to automation. Understanding the build script is essential for managing complex project lifecycles effectively.
In this guide, we'll break down exactly what a Gradle build script is, why its Directed Acyclic Graph (DAG) architecture was designed this way, and how to use it correctly in real-world Java projects.
By the end, you'll have both the conceptual understanding and practical code examples to use Gradle with confidence in any 'TheCodeForge' production environment.
What Is a Gradle Build Script and Why Does It Exist?
A Gradle build script (typically build.gradle or build.gradle.kts) is the declarative configuration that defines how a project is compiled, tested, and deployed. It exists to solve the problem of build rigidity found in older tools. By utilizing a powerful Domain Specific Language (DSL), Gradle allows developers to define custom logic while maintaining high performance through incremental builds and build caching. It treats your build as a set of interdependent tasks that can be executed in parallel where possible.
The core strength lies in how Gradle models the build as a Directed Acyclic Graph (DAG). This means every task knows exactly what it needs to run before it can start. If you run a 'build' task, Gradle calculates the most efficient path to get there, skipping tasks whose inputs and outputs haven't changed since the last run.
gradle tasks command to a println that triggered a remote service call.Common Mistakes and How to Avoid Them
When learning Gradle, most developers fall into the trap of treating the build script like a standard imperative program. This leads to common mistakes such as performing heavy I/O operations or network calls during the configuration phase, which slows down every build command. Another frequent error is ignoring the 'Gradle Wrapper' (gradlew), leading to 'it works on my machine' syndrome where different team members use conflicting Gradle versions.
At TheCodeForge, we enforce the 'Configuration Avoidance API'. This means registering tasks instead of creating them eagerly. When you use tasks.register, the task is only configured if it's actually going to be executed, saving precious seconds during every developer's inner-loop build cycle.
The Three-Phase Lifecycle: Initialization, Configuration, Execution
Gradle's build lifecycle is divided into three distinct phases: Initialization, Configuration, and Execution. During Initialization, Gradle locates the settings file (settings.gradle) and creates the project hierarchy. In the Configuration phase, it evaluates all build scripts and task configurations, building the DAG of tasks. The Execution phase runs the tasks that are actually needed for the requested command.
A critical nuance: any code you write outside of a task's doLast block executes in the Configuration phase. This includes println statements, file reads, and network calls. If you write such code at the top level of build.gradle, it runs every time you run any Gradle command, even gradle tasks. This is the single biggest source of build slowdowns.
Gradle's configuration avoidance API (tasks.register) defers task configuration until the task is actually executed, speeding up the Configuration phase significantly.
- Initialization: Assemble the cast (projects).
- Configuration: Read the script (build scripts) and plan scenes (tasks).
- Execution: Film only the scenes needed for the final cut (requested tasks).
- Any line spoken during script reading slows down the whole production.
gradle tasks taking 45 seconds. Profiling revealed a top-level apply from: 'http://...' that fetched a remote script each time. Moved to a local file and cached the result — tasks dropped to 3 seconds.--profile to find configuration bottlenecks.Dependency Management: implementation vs api vs compile
Gradle offers several dependency configurations to control how dependencies are exposed to consumers. The most important distinction is between implementation and api (provided by the java-library plugin).
implementation: The dependency is used by the module but not exposed to consumers. If your module depends on library A viaimplementation, projects that depend on your module cannot access library A's classes. This improves compilation speed because changes to library A only trigger recompilation of your module, not its consumers.api: The dependency is part of the module's public API. Consumers of your module will see library A on their compile classpath. Use this when the types from the dependency appear in the module's public method signatures.compile(deprecated): The older, broader configuration that always exposed dependencies. It has been replaced byimplementationandapito give finer control.
compile in older scripts, replace it with implementation unless you explicitly need to expose the dependency. The java-library plugin adds api; without it, api is not available.api causes a recompilation cascade. At TheCodeForge, a change in a low-level logging library triggered recompilation of 15 microservices because it was declared as api in a shared utility module. Changing it to implementation reduced build time by 60%.implementation; only use api when the dependency's types are part of your public API.Build Performance: Incremental Builds, Build Cache and Configuration Avoidance
Gradle's incremental build capability is its strongest performance feature. A task is considered UP-TO-DATE if its inputs and outputs haven't changed since the last execution. Gradle tracks inputs (source files, properties, other tasks' outputs) and outputs (generated files, archives). To leverage this, define your custom tasks with @InputFiles, @Input, @OutputDirectory, and similar annotations.
The Build Cache extends this concept across machines. When enabled, Gradle stores task outputs in a shared cache (remote or local). If another developer or CI runs the same task with the same inputs, Gradle downloads the output instead of re-executing. This can dramatically speed up CI pipelines.
Configuration avoidance (using tasks.register instead of tasks.create) ensures that the Configuration phase only processes tasks that are actually needed. This reduces the 'time-to-first-task' and is especially beneficial in multi-project builds.
Gradle Dependency Resolution Breaks Spring Boot Upgrade
constraints or force to lock Jackson version across the project. Add an explicit dependency with implementation('com.fasterxml.jackson.core:jackson-databind:2.15.2') and use resolutionStrategy.force in the build script. Also, run dependencies task to inspect the tree.- Always run
gradle dependenciesbefore and after major upgrades. - Use Gradle's 'consistent resolution' feature or a version catalog (libs.versions.toml) to centralize dependency versions.
- Do not trust BOM alone — third-party libraries can override effectively.
gradle dependencies to see the full dependency tree. Look for version conflicts or missing repositories. Add missing repository or exclude transitive dependency.--profile flag to generate a build scan. Move expensive logic into doLast blocks. Use tasks.register instead of tasks.create.testImplementation dependencies for scope misconfiguration. Ensure you're using testImplementation not implementation for test-only libs. Run tests with --info logging to see classpath.gradle.properties: org.gradle.jvmargs=-Xmx4g. Also check for memory leaks in build scripts, e.g., accumulating large data structures in configuration.gradle-wrapper.properties. Also verify that CI has access to required repositories and that the daemon is not causing inconsistency (disable daemon on CI with --no-daemon).resolutionStrategy.force for the required version in the build script.Key takeaways
Common mistakes to avoid
4 patternsRunning logic in the Configuration Phase
Hardcoding local file paths
Using 'compile' or 'api' when 'implementation' is sufficient
Not using the Gradle Wrapper
Interview Questions on This Topic
Explain the three phases of the Gradle Build Lifecycle and what specifically happens in the 'Configuration' phase versus the 'Execution' phase.
doLast block runs here. The Execution phase runs the tasks that are actually needed for the requested command. A key insight: the Configuration phase runs for every command, even gradle tasks, so it should be kept lightweight. Use tasks.register to defer configuration until execution is needed.Frequently Asked Questions
That's Build Tools. Mark it forged?
3 min read · try the examples if you haven't