feat: Replace Spring Dependency Management plugin with Gradle platform + lightweight BOM property overrides#15467
feat: Replace Spring Dependency Management plugin with Gradle platform + lightweight BOM property overrides#15467jamesfredley wants to merge 45 commits into
Conversation
…m and lightweight BOM property overrides Replace the Spring Dependency Management Gradle plugin with Gradle's native platform() support plus a lightweight BomManagedVersions utility that preserves the ability to override BOM-managed dependency versions via project properties (ext[] or gradle.properties). This allows Grails to standardize on Gradle platforms - the modern dependency management solution - while retaining the one feature Gradle platforms lack: property-based version overrides from BOMs. Changes: - Add BomManagedVersions: parses BOM POM XML to extract property-to- artifact mappings, applies version overrides via eachDependency() - Update GrailsGradlePlugin to use platform() + BomManagedVersions instead of Spring DM plugin - Deprecate GrailsExtension.springDependencyManagement flag - Remove Spring DM plugin from plugins/build.gradle dependency - Remove Spring DM plugin from example projects - Update documentation to reflect Gradle platform approach - Add unit tests (BomManagedVersionsSpec) and functional test (BomPlatformFunctionalSpec) Note: build-logic/docs-core/ExtractDependenciesTask still uses Spring DM's shaded Maven model classes and should be addressed in a follow-up. Assisted-by: Claude Code <Claude@Claude.ai>
There was a problem hiding this comment.
Pull request overview
This PR replaces the Spring Dependency Management plugin with Gradle's native platform() mechanism and introduces a lightweight utility (BomManagedVersions) to enable property-based version overrides from BOMs—the one feature Gradle platforms don't natively support.
Changes:
- Removed Spring Dependency Management plugin dependency and usage across the codebase
- Added
BomManagedVersionsutility (~350 lines) to parse BOM POMs and apply property-based version overrides - Updated plugin to use
platform()for BOM import instead of Spring DM'smavenBom()
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy |
New utility class that parses BOM POMs and enables property-based version overrides via Gradle's resolution strategy |
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy |
Replaced Spring DM plugin application with native Gradle platform support plus BomManagedVersions |
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy |
Deprecated springDependencyManagement flag with guidance on new approach |
grails-gradle/plugins/build.gradle |
Removed Spring DM plugin dependency |
grails-test-examples/gsp-spring-boot/app/build.gradle |
Removed Spring DM plugin from example project |
grails-data-graphql/examples/spring-boot-app/build.gradle |
Removed Spring DM plugin from example project |
grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc |
Updated documentation to reflect platform-based approach and property override mechanism |
grails-profiles/plugin/templates/grailsCentralPublishing.gradle |
Updated comment to reflect new version management approach |
grails-bom/build.gradle |
Updated comment about version property references |
grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy |
Unit tests for BOM parsing and property extraction |
grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy |
Functional test verifying platform integration |
grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/* |
Test project files for functional testing |
grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom |
Test BOM POM for unit tests |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…h constant Add factory.setXIncludeAware(false) for explicit XML security hardening and extract magic number 10 to MAX_PROPERTY_INTERPOLATION_DEPTH constant. Assisted-by: Claude Code <Claude@Claude.ai>
eb85805 to
5643538
Compare
The Spring Dependency Management plugin applied version constraints globally to every configuration via configurations.all() and resolutionStrategy.eachDependency(). With the switch to Gradle's native platform(), version constraints must be added explicitly. Apply the grails-bom platform to all declarable configurations using configureEach, matching the previous global behavior. Non-declarable configurations (apiElements, runtimeElements, etc.) inherit constraints through their parent configurations. Code quality tool configurations (checkstyle, codenarc, etc.) are excluded because platform() constraints participate in version conflict resolution and can upgrade transitive dependencies, breaking the tools. Also ensure the developmentOnly configuration always exists via maybeCreate. Assisted-by: Claude Code <Claude@Claude.ai>
5643538 to
5e89656
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
# Conflicts: # grails-data-graphql/examples/spring-boot-app/build.gradle
…atform applyGrailsBom() adds a regular platform(grails-bom) to every declarable configuration, including 'implementation'. Micronaut projects still need to declare an enforcedPlatform(grails-bom) explicitly. With both present on the same configuration, validateEnforcedBom() was failing on the first grails-bom it encountered (the plugin-injected regular platform) before it could check the user's enforcedPlatform declaration. Scan all grails-bom declarations on 'implementation' and accept the project as correctly configured if any one of them is an enforcedPlatform. Only error when grails-bom is present but no enforcedPlatform exists. Assisted-by: claude-code:claude-opus-4
…form applyGrailsBom() adds a regular platform(grails-bom) to every declarable configuration. For annotation-processor classpaths this conflicts with the platform the user imports themselves (typically io.micronaut.platform:micronaut-platform for Micronaut projects). Since platform() constraints participate in Gradle version conflict resolution, grails-bom's higher javaparser-core version wins over what Micronaut's inject-java processor was compiled against, producing NoSuchMethodError on StaticJavaParser.parseJavadoc(String) during compileJava. Exclude annotationProcessor and *AnnotationProcessor configurations using the same mechanism that already excludes code-quality tool classpaths (checkstyle, codenarc, pmd, spotbugs). Rename the helper to reflect the broadened scope. Assisted-by: claude-code:claude-opus-4
Final pass (commits
|
| Pattern | Pre-existing uses | Where we use it |
|---|---|---|
project.afterEvaluate { ... } for late-binding |
13 in GrailsGradlePlugin |
applyGrailsBom(), BomPropertyOverridesPlugin.apply() |
project.objects.property(...).convention(...) for extension fields |
GrailsExtension.indy, GrailsExtension.preserveParameterNames, BomPropertyOverridesExtension.autoDetect/boms |
GrailsExtension.autoApplyBom |
project.extensions.create(...) for plugin extensions |
2 existing | BomPropertyOverridesPlugin.apply() |
configurations.detachedConfiguration(...) for ad-hoc POM resolution |
GrailsDependencyValidatorPlugin, ExtractDependenciesTask |
BomManagedVersions.resolvePomFile() |
JDK DocumentBuilderFactory POM parsing with XXE hardening |
BomManagedVersions.parseXml() (this PR) |
ExtractDependenciesTask.parsePom() (same JDK pattern, same XXE flags) |
captureProjectServices-style service capture |
ExtractDependenciesTask, TestPhasesGradlePlugin |
BomManagedVersions.resolve(ConfigurationContainer, DependencyHandler, ...) |
Verification
./gradlew :grails-gradle-plugins:build
-> BUILD SUCCESSFUL, 31 tests pass
(was 30; +1 AutoApplyBomSpec)
codenarc clean, validatePlugins clean
./gradlew :grails-base-bom:extractConstraints
-> BUILD SUCCESSFUL
grails-bom-constraints.adoc produced (184.9 KB)
identical structure to pre-refactor output
./gradlew --configuration-cache :grails-gradle-plugins:test
-> 0 CC warnings from BOM code
(7 pre-existing warnings in Jar tasks + test-config.gradle, none from this PR)
git grep "io.spring.gradle.dependencymanagement" -> 0 matches
git grep "spring-boot-gradle-plugin" -> 0 matches outside this commit's removal
PR description rewritten to describe the final state and justify the custom code.
All 28 review threads are now resolved (15 in the original review pass + 13 in the three follow-up passes).
Resolve conflicts in GrailsGradlePlugin.groovy and upgrading80x.adoc: - validateMicronautBom(): keep the validMicronautBoms set (grails-micronaut-bom, grails-hibernate5-micronaut-bom) introduced on 8.0.x together with the PR's explanatory comment. - Upgrade guide: renumber sections so the PR's new "Spring Dependency Management Plugin Replaced by Gradle Platforms" section and 8.0.x's new TagLib sections coexist as a sequential 1-25, fixing the duplicate section numbers and updating the cross-references to the renumbered TagLib parameter-name subsection. Assisted-by: claude-code:claude-4.8-opus
Three correctness fixes to the new Gradle platform + BOM property-override feature, identified during review of PR #15467: 1. Honor the deprecated `springDependencyManagement` opt-out. `applyGrailsBom()` only checked `autoApplyBom`, so `grails { springDependencyManagement = false }` (a documented Grails 7 opt-out still used by grails-extension-gradle-config.gradle across the build, and by external projects) had become a silent no-op that unexpectedly applied `platform(grails-bom)`. The deprecated setter now maps `false` onto `autoApplyBom = false`. 2. Apply property overrides as strict dependency constraints instead of `ResolutionStrategy.eachDependency().useVersion()`. A soft override loses to a platform's `require` constraint during conflict resolution, so a downgrade override was silently ignored. A strict constraint wins in both directions. 3. Honor imported-BOM property overrides. `BomManagedVersions` now computes every managed artifact's version twice (with BOM defaults and with project overrides, including imported-BOM selector versions) and records the difference as an override. Every managed entry is considered, not only `${property}` references, so overriding a property that selects an imported BOM (e.g. `spring-boot.version`) re-imports that BOM and picks up its updated managed set, including hardcoded versions. A BOM's own direct entries take precedence over imported ones. Adds GrailsExtensionSpec and a BomOverrideResolutionFunctionalSpec backed by a local-Maven-repo fixture that resolves real dependencies to prove the downgrade override and the imported-BOM (hardcoded) cascade. Assisted-by: claude-code:claude-4.8-opus
The override behaviour changed so that property overrides are applied as strict dependency constraints (winning in both directions, including downgrades) rather than soft resolution rules. Update the user-facing docs that still described the old behaviour: - gradleDependencies.adoc: overrides are strict constraints that win over the platform's require() constraints (including downgrades); only the BOM's default managed versions still participate in highest-version-wins conflict resolution; document imported-BOM (e.g. spring-boot.version) re-import. - upgrading80x.adoc: correct the migration table (strict constraints, not useVersion()) and the conflict-resolution note to distinguish explicit overrides (strict, always win) from default BOM versions (conflict resolution; use enforcedPlatform to force). Assisted-by: claude-code:claude-4.8-opus
Update: merged
|
Address @matrei's review comments (pullrequestreview-4407310454) on the BOM property-overrides code. Style-only, no behavioral change: - Use `def`/inferred types for local variables in new Groovy code - Drop unnecessary terminal `return` statements in simple methods - Use implicit `it` closure parameters where the type is statically inferred - Simplify DocumentBuilderFactory and detached-configuration setup with `tap {}` - Break an over-long lifecycle log statement - Remove now-unused imports (DependencyConstraint, MutableVersionConstraint, Configuration) Verified: grails-gradle-plugins and grails-docs-core compile clean. Assisted-by: claude-code:claude-opus-4-8
jdaugherty
left a comment
There was a problem hiding this comment.
I strongly disagree with this change. We need to use the maven libraries to parse POMs and not roll our own solution. Otherwise, how do we know we have deviated from the maven implementation (especially with maven 4 around the corner)
| api 'org.yaml:snakeyaml:2.4' | ||
|
|
||
| api "org.asciidoctor:asciidoctorj:${gradleBomDependencyVersions['asciidoctorj.version']}" | ||
| implementation "org.springframework.boot:spring-boot-gradle-plugin:${gradleBomDependencyVersions['spring-boot.version']}" |
There was a problem hiding this comment.
While I understand we're proposing to switch, don't we still need to offer this support to existing Grails apps? That means we should have a test app that uses it so we know we don't break anything.
There was a problem hiding this comment.
Don't we still have to apply the gradle plugin? Did you add a test app that uses the gradle plugin so we know historically if it will continue to work if manually applied? @jamesfredley
There was a problem hiding this comment.
Added in 59dcb7e - grails-test-examples/spring-dependency-management, a Grails app that applies io.spring.dependency-management by hand and imports grails-bom. Its integration test boots the app and serves a request.
|
|
||
| MavenXpp3Reader reader = new MavenXpp3Reader() | ||
| Model model = reader.read(new FileReader(bomPomFile)) | ||
| def doc = parsePom(bomPomFile) |
There was a problem hiding this comment.
I strongly disagree with this approach. We're now rolling our own xml parsing instead of using files that adhere to the maven standard. Libraries like plexus and others exist to parse this. We shouldn't be rolling our own. Especially because this is the dependencies task which is meant to resolve properties like maven would. There is no guarantee this resolution will mirror what upstream Maven does by not adopting the maven specific libraries.
There was a problem hiding this comment.
I stopped reviewing after I saw this - we can't use custom xml parsing. We should be using upstream libraries to do this (it doesn't have to be Spring's). Maven actually publishes their maven-model library for this exact purpose.
There was a problem hiding this comment.
Maven itself publishes maven-model for this.
There was a problem hiding this comment.
Done in 20f3304.
ExtractDependenciesTask now parses BOM POMs with Maven's own org.apache.maven:maven-model (MavenXpp3Reader -> Model) instead of the hand-rolled DocumentBuilderFactory parsing, so parent/properties/dependencyManagement and <scope>import</scope> resolution mirror upstream Maven rather than a bespoke approximation.
On versioning: Spring Boot's BOM does not manage org.apache.maven:maven-model (the spring-boot-dependencies POM only manages org.apache.maven.plugins:*), so there is no Spring-Boot-managed version to inherit. The version is pinned in dependencies.gradle's gradleBomDependencyVersions map alongside the other build-tooling versions, on the Maven 3.9.x line (3.9.16), which still ships org.apache.maven.model.io.xpp3.MavenXpp3Reader. It's deliberately kept out of gradleBomDependencies so it is not published as a managed constraint in the consumer-facing grails-bom.
Verified: :grails-docs-core:compileGroovy compiles, and :grails-bom:extractConstraints / :grails-base-bom:extractConstraints regenerate the constraint tables with property references (e.g. ${logback.version}) correctly resolved from the imported spring-boot-dependencies BOM.
The Micronaut platform ships org.ow2.asm 9.10.1, above the 9.9.1 inherited via grails-base-bom, so five micronaut test-example modules used ext.allowedBomOverrides to exempt the mismatch from the dependency-validator. That escape hatch defeats the validator's purpose. Pin org.ow2.asm:asm and asm-util at 9.10.1 in the micronaut BOM (customBomVersions/customBomDependencies) so the resolved version equals the BOM-managed version, and remove the per-app allowedBomOverrides from all five modules. Also drop the redundant micronaut-specific groovy.version override; it now tracks the main bom (still 5.0.7-SNAPSHOT via combinedVersions). validateDependencyVersions passes for all five micronaut modules. See #15677 / #15467 for the upstream Micronaut platform / Gradle platform work that resolves the asm conflict longer term. Assisted-by: claude-code:claude-4.8-opus
The docs-core ExtractDependenciesTask parsed BOM POM files with a hand-rolled JDK DocumentBuilderFactory parser. Switch to Maven's published org.apache.maven:maven-model (MavenXpp3Reader -> Model) so property and imported-BOM resolution mirror upstream Maven, as requested in PR review. Spring Boot does not manage maven-model, so its version is pinned in dependencies.gradle's gradleBomDependencyVersions map (Maven 3.9.x, which still provides MavenXpp3Reader). It is intentionally not added to gradleBomDependencies so it is not exposed as a managed constraint in the consumer-facing grails-bom. Assisted-by: claude-code:claude-4.8-opus
jdaugherty
left a comment
There was a problem hiding this comment.
This PR is out of date - we changed our assumption that only 1 grails-bom would be applied (the micronaut variant if using micronaut, etc).
| * Scans the project's configurations to find which BOM project is in use. | ||
| * | ||
| * <p>When multiple known BOMs are declared on the same project (for example, | ||
| * the {@code grails-app} plugin auto-injects {@code platform(grails-bom)} on |
There was a problem hiding this comment.
I don't believe this is possible now with the current bom design - we should only be using one bom.
There was a problem hiding this comment.
Done in 6ef4ec2. detectBomPath no longer prefers an enforcedPlatform among several - it expects exactly one Grails BOM and fails if it finds more than one distinct BOM.
| * @param propertyToArtifacts output map to receive property name to artifact coordinate mappings | ||
| */ | ||
| static void parseBomFile(File pomFile, Map<String, String> bomProperties, Map<String, List<String>> propertyToArtifacts) { | ||
| def doc = parseXml(pomFile) |
There was a problem hiding this comment.
We should use the maven model in this file.
There was a problem hiding this comment.
Done in bc933fa. BomManagedVersions now parses POMs with org.apache.maven:maven-model (MavenXpp3Reader into a Model), matching ExtractDependenciesTask.
| * | ||
| * <pre> | ||
| * grails { | ||
| * autoApplyBom = false |
There was a problem hiding this comment.
Because the boms are split betwen hibernate 5 & 7, I don't think we should do this.
There was a problem hiding this comment.
Addressed in 6ef4ec2. The hardcoded grails-bom assumption is gone - grails { bom = ... } selects the variant (e.g. grails-hibernate5-bom, grails-micronaut-bom), defaulting to grails-bom.
|
|
||
| applyBomImport(dme, project) | ||
| def grailsVersion = (project.findProperty('grailsVersion') ?: BuildSettings.grailsVersion) as String | ||
| def bomCoordinates = "org.apache.grails:grails-bom:${grailsVersion}" as String |
There was a problem hiding this comment.
We can't assume this. If we're really going to set the bom, we should change the boolean to the bom name and then default it. If it's null, we simply don't apply.
There was a problem hiding this comment.
Done in 6ef4ec2 - replaced the autoApplyBom boolean with a Property<String> bom (the BOM artifact name, default grails-bom, null to opt out), exactly as suggested. The plugin resolves it to org.apache.grails:$bom:$grailsVersion.
| // The Grails Gradle Plugin injects a regular platform(grails-bom) into each | ||
| // declarable configuration via applyGrailsBom(), excluding code-quality and | ||
| // annotation-processor classpaths (see isExcludedFromBomPlatform). For Micronaut | ||
| // projects the user must additionally declare an enforcedPlatform on a Micronaut BOM |
There was a problem hiding this comment.
The original design was to only have one bom applied, I believe this PR is out of date.
There was a problem hiding this comment.
|
I'm a -1 on this until the following is addressed:
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15467 +/- ##
==================================================
- Coverage 49.0042% 48.9431% -0.0611%
- Complexity 16759 16785 +26
==================================================
Files 2014 2017 +3
Lines 94747 94994 +247
Branches 16547 16617 +70
==================================================
+ Hits 46430 46493 +63
- Misses 41019 41180 +161
- Partials 7298 7321 +23
🚀 New features to boost your workflow:
|
Replace the hand-rolled JDK DocumentBuilderFactory parsing in BomManagedVersions with Maven's own maven-model library (MavenXpp3Reader into a Model), matching the docs ExtractDependenciesTask. Parent-POM inheritance and imported-BOM resolution now follow Maven semantics, with per-BOM property scoping, while preserving the two-pass default/effective diff that detects property-based version overrides. Add org.apache.maven:maven-model to the grails-gradle-plugins runtime classpath (its only transitive is plexus-utils); the version is pinned in dependencies.gradle and declared inline so it is not published as a managed constraint in the consumer-facing grails-bom. Assisted-by: claude-code:claude-4.8-opus
Replace the GrailsExtension.autoApplyBom boolean with a Property<String> bom that names the Grails BOM artifact to apply (default grails-bom, null or blank to opt out). The Grails Gradle plugin applies a single BOM as a platform (or an enforcedPlatform for the Micronaut variants), honors a BOM the build declares by hand instead of layering a second one, fills the remaining declarable configurations with that same BOM, and fails fast when more than one distinct Grails BOM is declared. The deprecated springDependencyManagement = false flag maps onto bom = null. GrailsDependencyValidatorPlugin.detectBomPath now expects exactly one Grails BOM. Update the upgrade guide and the affected tests accordingly. Assisted-by: claude-code:claude-4.8-opus
Grails 8 no longer applies io.spring.dependency-management automatically.
Add regression coverage that it still works when applied by hand:
- grails-test-examples/spring-dependency-management: a Grails application
that opts out of the automatic BOM (grails { bom = null }) and manages
versions with io.spring.dependency-management importing grails-bom.
- grails-test-examples/gsp-spring-boot: re-enable the non-Grails Spring
Boot + GSP application and have it manage versions with the Spring
Dependency Management plugin importing grails-bom.
Assisted-by: claude-code:claude-4.8-opus
Only treat a Grails BOM as hand-declared when it carries platform semantics (a platform() / enforcedPlatform() declaration with the Category attribute), not a plain dependency that imports no constraints. Also assert that a sibling configuration which does not declare a BOM still receives the single effective BOM via per-configuration injection. Assisted-by: claude-code:claude-4.8-opus
|
@jdaugherty I've pushed changes addressing all four blocking items from your review. Commits:
1. Only one Grails BOM is ever applied.
2. Legacy Spring Dependency Management plugin coverage (Grails app). New 3. Spring Boot apps keep using the Spring plugin. Re-enabled 4. Verified locally: One small remaining nit from your review I did not touch: |
The other entries in the gradleBomDependencyVersions map are uncommented; drop the comment for consistency (review feedback). Assisted-by: claude-code:claude-4.8-opus
✅ All tests passed ✅🏷️ Commit: f63f028 Learn more about TestLens at testlens.app. |
Summary
Grails 8 manages dependency versions with Gradle's native
platform()support plus a small bundled Gradle plugin (org.apache.grails.gradle.bom-property-overrides) that adds the one capability Gradle platforms lack: reading<properties>from BOM POMs and letting projects override those versions. Theio.spring.gradle:dependency-management-plugin(Spring DM) is not applied anywhere - in the framework, in generated/example apps, or on the build classpath.What the Grails Gradle plugin does
GrailsGradlePlugin.applyGrailsBom()(runs inafterEvaluate):grails.bomproperty (defaultgrails-bom), as a Gradleplatform()- or anenforcedPlatform()for the Micronaut variants - on every declarable project configuration, giving the BOM the same global reach Spring DM had.platform()/enforcedPlatform()(e.g. a Micronaut app declaringenforcedPlatform(grails-micronaut-bom), or an app generated by Grails Forge / a profile), the plugin does not layer a second BOM on top of it; it fills the remaining declarable configurations with that same single BOM. Declaring more than one distinct Grails BOM fails the build.checkstyle,codenarc,pmd,spotbugs,spotbugsPlugins,annotationProcessor, and any*AnnotationProcessor. A non-enforcedplatform()participates in version conflict resolution, so adding a BOM to those tool classpaths would let Gradle upgrade transitives (e.g.javaparser-core; Micronaut processors importio.micronaut.platform:micronaut-platformthere) and break the tools.developmentOnlyconfiguration always exists (configurations.maybeCreate('developmentOnly')), even without Spring Boot.org.apache.grails.gradle.bom-property-overridesplugin.Selecting / opting out of the BOM
grails { bom = 'grails-micronaut-bom' // pick a curated variant (applied as enforcedPlatform) // bom = null // opt out: manage Grails versions yourself }grails.bomis the BOM artifact name withinorg.apache.grails(defaultgrails-bom); the plugin resolves it toorg.apache.grails:$bom:$grailsVersion. Setting it tonull(or blank) suppresses both the automatic BOM injection and thebom-property-overridesplugin.The deprecated
grails { springDependencyManagement = false }flag is still honored for backward compatibility: setting it tofalseis equivalent tobom = null. New builds should usebom.Micronaut
validateMicronautBom()(viaconfigureMicronaut()) requires a Micronaut BOM applied asenforcedPlatformwhengrails-micronautis on the classpath. It acceptsgrails-micronaut-bomorgrails-hibernate5-micronaut-bom. Because exactly one Grails BOM is applied, the Micronaut variant is the only BOM in play - the plugin no longer layersgrails-bomalongside it. The build fails at configuration time with an actionable message otherwise.The
org.apache.grails.gradle.bom-property-overridespluginA standalone, BOM-agnostic Gradle plugin (bundled in
grails-gradle-plugins, applicable to any project by id). It restores Maven-style<properties>version overrides on top of nativeplatform():Auto-detects every
platform()/enforcedPlatform()declared on the project's configurations and registers each for override processing.Optional DSL for explicit control:
bomPropertyOverrides { autoDetect = false // default: true bom 'com.example:my-bom:1.0.0' bom 'com.example:other-bom:2.0.0' }A Grails web project gets this automatically through the Grails Gradle plugin; no extra setup.
Override workflow
Either form overrides the BOM-managed version.
How overrides are resolved (
BomManagedVersions)The engine has a services-based API (no
Projectreference, so it is configuration-cache safe) and parses BOM POMs with Maven's ownorg.apache.maven:maven-modellibrary (MavenXpp3Readerinto aModel), the same way the docsExtractDependenciesTaskdoes. Parent-POM inheritance and<scope>import</scope>resolution follow Maven semantics (e.g.grails-bomimportsspring-boot-dependencies), with<properties>scoped per BOM (and its parents) rather than leaking across unrelated imported BOMs, and property interpolation depth capped at 10.Override detection uses a two-pass version diff: it computes the effective version of every managed artifact once with the BOM's default
<properties>and once with the project's overrides applied. Any artifact whose version differs between the two passes becomes an override. This model:${property}references and hardcoded versions), not just property-backed ones.spring-boot.version, which picks thespring-boot-dependenciesBOM) re-import that BOM at the overridden version and pull in its entire updated managed set.<dependencyManagement>entries precedence over the entries it imports (matching Maven's importing-POM-wins precedence).Each detected override is applied as a strict dependency constraint on the project's declarable configurations.
Override semantics
ext['…']/gradle.properties) is a strict constraint, so it wins over therequireconstraintsplatform(grails-bom)contributes - in both directions, including downgrades (e.g. pinningslf4j.versionbelow the BOM default actually downgrades it). This matches Spring DM, where overrides always took effect. (A plain resolution rule would lose to Gradle's highest-version-wins conflict resolution on a downgrade.)platform()and still participate in Gradle's highest-version-wins conflict resolution, so a transitive can pull a non-overridden version higher than the BOM default. DeclareenforcedPlatform(grails-bom)to force the default versions to win unconditionally (Micronaut projects already do this).Build-side (
grails-bomgeneration and docs)grails-bom(grails-bom/base/build.gradle+ itspomCustomization) rewrites managed versions to${property}references in the published POM and emits a matching<properties>block, so consumers can override them.BomManagedVersions) andbuild-logic/docs-core/ExtractDependenciesTasknow parse BOM POMs with Maven'smaven-modellibrary, which removes the last consumer of Spring DM's shaded Maven classes and letsspring-boot-gradle-pluginbe dropped frombuild-logic/docs-core/build.gradle.Migration notes (7.x -> 8.x)
End-user code consuming Grails via the standard
grails-webplugin needs no changes: the BOM is still applied automatically andgradle.properties/ext['…']overrides work as before (now via strict constraints). Section 14 of the upgrade guide covers the full migration.Two Spring DM features have no automatic migration (not used inside
grails-core, but external consumers might rely on them):dependencyManagement { dependencies { dependency 'g:a:v' } }for arbitrary non-BOM pinsdependencies { implementation 'g:a:v' }, orconfigurations.all { resolutionStrategy.force 'g:a:v' }for a transitive1.+,[1.0,2.0)) inside BOM<properties>Using the legacy Spring Dependency Management plugin
Spring DM is no longer applied automatically, but an application (Grails or plain Spring Boot) may still apply
io.spring.dependency-managementby hand and importgrails-bom. Two example apps exercise this (see Testing below).Testing
Functional tests use Gradle TestKit;
BomOverrideResolutionFunctionalSpecruns a real resolution against a local Maven repo.BomManagedVersionsSpec- POM parsing (maven-model), property extraction, property-to-artifact mapping, hardcoded-version handling.BomPropertyOverridesPluginSpec/BomPropertyOverridesPluginFunctionalSpec- extension registration, DSL,platform()/enforcedPlatform()detection, auto-detect.BomPlatformFunctionalSpec- (1) a Grails project appliesgrails-bomas aplatform(), applies the overrides plugin, does not apply Spring DM; (2) a project that declares a Grails BOM by hand does not get a second BOM, and sibling configurations still receive the single effective BOM.BomOptOutFunctionalSpec-grails { bom = null }suppresses both the platform injection and the overrides plugin.GrailsExtensionSpec-bomdefault/selection/clear;springDependencyManagement = falsemaps tobom = null.BomOverrideResolutionFunctionalSpec- real resolution proving a downgrade override wins over the platform constraint, and that overriding an imported-BOM selector property bumps a hardcoded transitively-managed version.grails-test-examples/spring-dependency-management- a Grails application that opts out of the auto BOM (grails { bom = null }) and manages versions with the legacyio.spring.dependency-managementplugin importinggrails-bom; its integration test boots and serves a request.grails-test-examples/gsp-spring-boot- a non-Grails Spring Boot application that renders Grails GSP and manages versions withio.spring.dependency-managementimportinggrails-bom.Review feedback (jdaugherty) addressed
The four blocking items from the latest review have been addressed:
GrailsExtension.autoApplyBom(boolean) is replaced by aProperty<String> bom(the BOM artifact name; defaultgrails-bom,nullto opt out). The plugin applies a single BOM (enforcedPlatformfor Micronaut variants), honors a hand-declared BOM instead of layering a second one, fills remaining configurations with that one BOM, and fails on more than one distinct Grails BOM.GrailsDependencyValidatorPlugin.detectBomPathnow expects exactly one.grails-test-examples/spring-dependency-managementGrails app.grails-test-examples/gsp-spring-bootapp is re-enabled and manages versions with Spring DM.maven-modelfor XML parsing -BomManagedVersionsnow parses POMs withorg.apache.maven:maven-model(parent-POM recursion + per-BOM property scoping), matchingExtractDependenciesTask.Related
<properties>override feature ("very unlikely" to be implemented natively).