From bdb1e00664937b8b3e284258120113ab12b4cc79 Mon Sep 17 00:00:00 2001 From: James Daugherty Date: Thu, 28 May 2026 14:27:12 -0400 Subject: [PATCH 1/3] Add grails-hibernate5-micronaut BOM (Micronaut BOM split) Introduces a Hibernate-version-specific Micronaut BOM so Micronaut projects can target a pinned Hibernate version: - Add grails-hibernate5-micronaut-bom (and its sample app) alongside the generic grails-micronaut-bom - Publish grails-micronaut-bom / grails-micronaut only when the Micronaut island is not skipped (skipMicronautProjects) - validateMicronautBom now accepts grails-micronaut-bom and grails-hibernate5-micronaut-bom as valid enforcedPlatform BOMs - Document the Hibernate-specific Micronaut BOM usage in the Micronaut config guide and the 8.0.x upgrade notes Carved out of the Hibernate 7 Step 1 PR (#15654) so the Micronaut BOM split can be reviewed as a single topic. The Hibernate 7 variant (grails-hibernate7-micronaut-bom) is intentionally excluded here because 8.0.x does not yet contain Hibernate 7; it remains in the Step 2 branch. Assisted-by: claude-code:claude-4.7-opus --- .../tasks/bom/PropertyNameCalculator.groovy | 15 ++ .../grails/buildsrc/CompilePlugin.groovy | 3 +- dependencies.gradle | 11 +- gradle/publish-root-config.gradle | 11 +- gradle/test-config.gradle | 7 + grails-bom/hibernate5-micronaut/build.gradle | 254 ++++++++++++++++++ grails-bom/micronaut/build.gradle | 7 +- grails-doc/src/en/guide/conf/micronaut.adoc | 17 ++ .../src/en/guide/upgrading/upgrading80x.adoc | 19 ++ .../plugin/core/GrailsGradlePlugin.groovy | 24 +- .../micronaut-hibernate5/build.gradle | 61 +++++ .../grails-app/conf/application.yml | 74 +++++ .../grails-app/conf/logback.xml | 37 +++ .../micronaut/hibernate5/UrlMappings.groovy | 29 ++ .../micronaut/hibernate5/Application.groovy | 30 +++ .../grails-app/views/index.gsp | 25 ++ .../hibernate5/ApplicationStartupSpec.groovy | 33 +++ settings.gradle | 4 + 18 files changed, 644 insertions(+), 17 deletions(-) create mode 100644 grails-bom/hibernate5-micronaut/build.gradle create mode 100644 grails-test-examples/micronaut-hibernate5/build.gradle create mode 100644 grails-test-examples/micronaut-hibernate5/grails-app/conf/application.yml create mode 100644 grails-test-examples/micronaut-hibernate5/grails-app/conf/logback.xml create mode 100644 grails-test-examples/micronaut-hibernate5/grails-app/controllers/micronaut/hibernate5/UrlMappings.groovy create mode 100644 grails-test-examples/micronaut-hibernate5/grails-app/init/micronaut/hibernate5/Application.groovy create mode 100644 grails-test-examples/micronaut-hibernate5/grails-app/views/index.gsp create mode 100644 grails-test-examples/micronaut-hibernate5/src/integration-test/groovy/micronaut/hibernate5/ApplicationStartupSpec.groovy diff --git a/build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/PropertyNameCalculator.groovy b/build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/PropertyNameCalculator.groovy index cdc3c7024a5..af4761e3157 100644 --- a/build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/PropertyNameCalculator.groovy +++ b/build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/PropertyNameCalculator.groovy @@ -117,6 +117,21 @@ class PropertyNameCalculator { possibleKey = lastIndex > 0 ? possibleKey.substring(0, lastIndex) : null } + // Fallback: check if any existing version property is a prefix of the artifact key. + // This handles cases like 'derbyclient' matching 'derby.version' to align with + // Spring Boot's BOM property naming conventions. + String artifactKey = keyMappings[found.coordinates] + if (artifactKey) { + String match = versions.keySet() + .findAll { it.endsWith('.version') } + .collect { it - '.version' } + .findAll { artifactKey.startsWith(it) } + .max { it.length() } + if (match) { + return "${match}.version" as String + } + } + null } } diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy index a8116836a0c..8718cd3e510 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy @@ -58,8 +58,9 @@ class CompilePlugin implements Plugin { } private static void configureJavaVersion(Project project) { + Integer javaVersion = lookupPropertyByType(project, 'javaVersion', Integer) project.tasks.withType(JavaCompile).configureEach { - it.options.release.set(lookupPropertyByType(project, 'javaVersion', Integer)) + it.options.release.set(javaVersion) } } diff --git a/dependencies.gradle b/dependencies.gradle index e02883f1f21..fd072ae7b40 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -217,14 +217,23 @@ ext { // grails-micronaut-bom project. Declared with `strictly` in the BOM project so // they win over Micronaut platform's higher versions when consumers use // enforcedPlatform(:grails-micronaut-bom). - else if (project.name == 'grails-micronaut-bom') { + else if (project.name in ['grails-micronaut-bom', 'grails-hibernate5-micronaut-bom']) { customBomVersions = [ + 'liquibase-hibernate.version': '4.27.0', + 'liquibase.version' : '4.27.0', + 'hibernate.version' : '5.6.15.Final', 'groovy.version' : '5.0.5', 'spock.version' : '2.4-groovy-5.0', 'protobuf.version': '4.30.2', ] combinedVersions += customBomVersions customBomDependencies = [ + 'liquibase' : "org.liquibase:liquibase:${combinedVersions['liquibase.version']}", + 'liquibase-cdi' : "org.liquibase:liquibase-cdi:${combinedVersions['liquibase.version']}", + 'liquibase-core' : "org.liquibase:liquibase-core:${combinedVersions['liquibase.version']}", + 'liquibase-hibernate' : "org.liquibase.ext:liquibase-hibernate5:${combinedVersions['liquibase-hibernate.version']}", + 'hibernate-core-jakarta': "org.hibernate:hibernate-core-jakarta:${combinedVersions['hibernate.version']}", + 'hibernate-ehcache' : "org.hibernate:hibernate-ehcache:${combinedVersions['hibernate.version']}", 'groovy' : "org.apache.groovy:groovy:${combinedVersions['groovy.version']}", 'groovy-ant' : "org.apache.groovy:groovy-ant:${combinedVersions['groovy.version']}", 'groovy-astbuilder' : "org.apache.groovy:groovy-astbuilder:${combinedVersions['groovy.version']}", diff --git a/gradle/publish-root-config.gradle b/gradle/publish-root-config.gradle index b0ba7c29dfa..6757868df34 100644 --- a/gradle/publish-root-config.gradle +++ b/gradle/publish-root-config.gradle @@ -33,7 +33,6 @@ def publishedProjects = [ 'grails-base-bom', 'grails-bom', 'grails-hibernate5-bom', - 'grails-micronaut-bom', 'grails-bootstrap', 'grails-cache', 'grails-codecs', @@ -68,7 +67,6 @@ def publishedProjects = [ 'grails-interceptors', 'grails-layout', 'grails-logging', - 'grails-micronaut', 'grails-mimetypes', 'grails-rest-transforms', 'grails-scaffolding', @@ -143,6 +141,15 @@ def publishedProjects = [ 'grails-profiles-web-plugin', ] +def skipMicronautProjects = providers.gradleProperty('skipMicronautProjects').isPresent() +if (!skipMicronautProjects) { + publishedProjects.addAll([ + 'grails-hibernate5-micronaut-bom', + 'grails-micronaut', + 'grails-micronaut-bom', + ]) +} + subprojects { if (name in publishedProjects) { // This has to be applied here in the root project due to the nexus plugin requirements diff --git a/gradle/test-config.gradle b/gradle/test-config.gradle index ae1a2b407be..d8d51dbabec 100644 --- a/gradle/test-config.gradle +++ b/gradle/test-config.gradle @@ -63,6 +63,13 @@ tasks.withType(Test).configureEach { useJUnitPlatform() jvmArgs += java17moduleReflectionCompatibilityArguments +// develocity { +// testRetry { +// maxRetries = configuredTestParallel == 1 ? 1 : 2 +// maxFailures = 20 +// failOnPassedAfterRetry = true +// } +// } testLogging { events('passed', 'skipped', 'failed') showExceptions = true diff --git a/grails-bom/hibernate5-micronaut/build.gradle b/grails-bom/hibernate5-micronaut/build.gradle new file mode 100644 index 00000000000..8ee4214370c --- /dev/null +++ b/grails-bom/hibernate5-micronaut/build.gradle @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import org.apache.grails.gradle.tasks.bom.ExtractDependenciesTask +import org.apache.grails.gradle.tasks.bom.ExtractedDependencyConstraint +import org.apache.grails.gradle.tasks.bom.PropertyNameCalculator + +buildscript { + apply from: rootProject.layout.projectDirectory.file('dependencies.gradle') +} + +plugins { + id 'java-platform' + id 'org.apache.grails.buildsrc.publish' + id 'org.apache.grails.buildsrc.sbom' +} + +version = projectVersion +group = 'org.apache.grails' + +javaPlatform { + allowDependencies() +} + +ext { + isReleaseBuild = System.getenv('GRAILS_PUBLISH_RELEASE') == 'true' + isPublishedExternal = System.getenv().containsKey('NEXUS_PUBLISH_STAGING_PROFILE_ID') + // TODO: It should be possible to pull these build names using includedBuild, but I haven't found a way to do so + gradleBuildProjects = [ + 'grails-gradle-plugins':'org.apache.grails', + 'grails-gradle-model':'org.apache.grails.gradle', + 'grails-gradle-common':'org.apache.grails.gradle', + 'grails-gradle-tasks':'org.apache.grails', + ] +} + +// Register the Micronaut platform in combinedPlatforms/combinedVersions so +// PropertyNameCalculator (used by extractConstraints and pomCustomization) can +// resolve a property name for the micronaut-platform constraint. +project.ext.combinedPlatforms = combinedPlatforms + ['micronaut-platform': "io.micronaut.platform:micronaut-platform:$micronautPlatformVersion".toString()] +project.ext.combinedVersions = combinedVersions + ['micronaut-platform.version': micronautPlatformVersion as String] + +// Coordinates we override via customBomDependencies — these must be excluded from the +// inherited platform chain so they don't conflict with our strictly-versioned overrides +// when consumers apply this BOM via enforcedPlatform. +def overriddenModules = customBomDependencies.values().collect { String coord -> + def parts = coord.split(':') + [group: parts[0], module: parts[1]] +} +Set overriddenCoords = overriddenModules.collect { "${it.group}:${it.module}".toString() } as Set + +dependencies { + api(platform(project(':grails-base-bom'))) { + overriddenModules.each { ovr -> + exclude group: ovr.group, module: ovr.module + } + } + + // Re-export the Micronaut platform so consumers inherit Micronaut's managed versions + // transitively. Exclude Groovy since we declare the required version explicitly via + // customBomDependencies. Exclude Spock since we manage that version ourselves. + api(platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")) { + exclude group: 'org.apache.groovy' + exclude group: 'org.spockframework' + } + + constraints { + // Re-declare base BOM constraints directly so enforcedPlatform() consumers + // get forced versions. Constraints inherited via platform() are not enforced + // by enforcedPlatform — only direct constraints are. + // Skip entries we override below in customBomDependencies to avoid conflicting + // strictly constraints under enforcedPlatform. + gradleBomDependencies.values().each { String coord -> + def parts = coord.split(':') + String key = parts[0] + ':' + parts[1] + if (key in overriddenCoords) { + return + } + api coord + } + bomDependencies.values().each { String coord -> + def parts = coord.split(':') + String key = parts[0] + ':' + parts[1] + if (key in overriddenCoords) return + api coord + } + for (def entry : bomPlatformDependencies.entrySet()) { + api entry.value + } + // Re-declare the Micronaut platform as a constraint for enforcedPlatform support + api "io.micronaut.platform:micronaut-platform:$micronautPlatformVersion" + for (def entry : customBomDependencies.entrySet()) { + def parts = entry.value.split(':') + if (parts.length == 3) { + api("${parts[0]}:${parts[1]}") { + version { + strictly parts[2] + } + } + } else { + api entry.value + } + } + } +} + +configurations.register('bomDependencies').configure { + it.canBeResolved = true + it.transitive = true + it.extendsFrom(configurations.named('api').get()) +} + +tasks.register('extractConstraints', ExtractDependenciesTask).configure { ExtractDependenciesTask it -> + it.captureProjectServices(project.dependencies, project.configurations) + it.configuration = configurations.named('bomDependencies') + it.configurationName = 'bomDependencies' + it.destination = project.layout.buildDirectory.file('grails-hibernate5-micronaut-bom-constraints.adoc') + it.platformDefinitions = combinedPlatforms + it.definitions = combinedDependencies + it.projectName = project.name + it.versions = combinedVersions + // Micronaut's platform imports many sub-BOMs (micronaut-*-bom, netty-bom, etc.) that are + // not explicitly registered in dependencies.gradle. Auto-register them so extractConstraints + // can document their versions without requiring manual entries for every transitive platform. + // this is required because the micronaut bom format uses gradle modules instead of a pom like spring boot + it.autoRegisterTransitivePlatforms = true + rootProject.subprojects.each { p -> + evaluationDependsOn(p.path) + } + it.projectArtifactIds.set(project.provider { + Map artifactIdMappings = [:] + + rootProject.subprojects.each { p -> + artifactIdMappings[p.name] = p.findProperty('pomArtifactId') ?: p.name + } + + for (Map.Entry dependency : project.ext.gradleBuildProjects.entrySet()) { + artifactIdMappings[dependency.key] = dependency.key + } + + artifactIdMappings + }) + it.forcedGroupPrefixes.set(['org.apache.grails.profiles': 'grails-profile']) + it.projectCoordinateProperties.set(project.provider { + Map projectCoordinates = [:] + + rootProject.subprojects.each { p -> + String artifactId = p.findProperty('pomArtifactId') as String ?: p.name + String baseVersionName = artifactId.replaceAll('[.]', '-') + projectCoordinates["${p.group}:${artifactId}:${p.version}" as String] = baseVersionName + } + + for (Map.Entry dependency : project.ext.gradleBuildProjects.entrySet()) { + projectCoordinates["${dependency.value}:${dependency.key}:${project.version}" as String] = dependency.key + } + + projectCoordinates + }) + + it.dependsOn(project.tasks.named('generateMetadataFileForMavenPublication'), project.tasks.named('generatePomFileForMavenPublication')) +} + +def validateNoSnapshotDependencies = tasks.register('validateNoSnapshotDependencies') +validateNoSnapshotDependencies.configure { Task it -> + it.group = 'publishing' + it.description = 'Validates that no snapshot dependencies are present in the project when performing a release.' + + it.doLast { + configurations.each { config -> + config.allDependencies.each { dep -> + if (dep.version && dep.version.contains('-SNAPSHOT')) { + throw new GradleException("Releases cannot have a snapshot dependency: ${dep.group}:${dep.name} (${dep.version})") + } + } + } + } +} + +if (ext.isReleaseBuild && ext.isPublishedExternal) { + project.afterEvaluate { + tasks.named('generateMetadataFileForMavenPublication').configure { + dependsOn(validateNoSnapshotDependencies) + } + tasks.named('generatePomFileForMavenPublication').configure { + dependsOn(validateNoSnapshotDependencies) + } + } +} + +ext { + pomDescription = 'Grails Hibernate 5 Micronaut BOM (Bill of Materials) for Grails projects integrating with Micronaut and Hibernate 5. Layers Hibernate 5 dependency management on top of grails-micronaut-bom; consume as enforcedPlatform.' + pomCustomization = { xml -> + def root = xml.asNode() + + def propertiesNode = root.properties ? root.properties[0] : root.appendNode('properties') + + def depMgmt = root.dependencyManagement?.getAt(0) + def deps = depMgmt?.dependencies?.getAt(0) + if (deps) { + PropertyNameCalculator propertyNameCalculator = new PropertyNameCalculator(combinedPlatforms, combinedDependencies, combinedVersions) + propertyNameCalculator.addForcedGroupPrefix('org.apache.grails.profiles', 'grails-profile') + propertyNameCalculator.addProjects(rootProject.subprojects) + for (String gradleArtifactId : project.ext.gradleBuildProjects) { + propertyNameCalculator.addProject('org.apache.grails.gradle', gradleArtifactId, project.version as String, gradleArtifactId) + } + + Map pomProperties = [:] + deps.dependency.each { dep -> + String groupId = dep.groupId.text().trim() + String artifactId = dep.artifactId.text().trim() + boolean isBom = dep.scope.text().trim() == 'import' + + String inlineVersion = dep.version.text().trim() + if (inlineVersion == 'null') { + inlineVersion = null + } + + if (inlineVersion) { + ExtractedDependencyConstraint extractedConstraint = propertyNameCalculator.calculate(groupId, artifactId, inlineVersion, isBom) + if (extractedConstraint?.versionPropertyReference) { + // use the property reference instead of the hard coded version so that it can be + // overridden by the spring boot dependency management plugin + dep.version[0].value = extractedConstraint.versionPropertyReference + + // Add an entry in the node with the actual version number + pomProperties.put(extractedConstraint.versionPropertyName, inlineVersion) + } + } else if (!inlineVersion) { + throw new GradleException("Dependency $groupId:$artifactId does not have a version.") + } + } + + for (Map.Entry property : pomProperties.entrySet()) { + propertiesNode.appendNode(property.key, property.value) + } + } + } +} diff --git a/grails-bom/micronaut/build.gradle b/grails-bom/micronaut/build.gradle index 47664408767..ccece8310fd 100644 --- a/grails-bom/micronaut/build.gradle +++ b/grails-bom/micronaut/build.gradle @@ -72,10 +72,9 @@ dependencies { } } - // Re-export the Micronaut platform so consumers of grails-micronaut-bom inherit Micronaut's - // managed versions transitively and don't need to declare the platform themselves. - // Exclude Groovy since we declare the required version explicitly below via customBomDependencies. - // Exclude Spock since the base BOM manages that version. + // Re-export the Micronaut platform so consumers inherit Micronaut's managed versions + // transitively. Exclude Groovy since we declare the required version explicitly via + // customBomDependencies. Exclude Spock since we manage that version ourselves. api(platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")) { exclude group: 'org.apache.groovy' exclude group: 'org.spockframework' diff --git a/grails-doc/src/en/guide/conf/micronaut.adoc b/grails-doc/src/en/guide/conf/micronaut.adoc index be08a4b7628..7d08c8337b3 100644 --- a/grails-doc/src/en/guide/conf/micronaut.adoc +++ b/grails-doc/src/en/guide/conf/micronaut.adoc @@ -43,6 +43,23 @@ dependencies { The `grails-micronaut-bom` layers Micronaut-specific dependency overrides on top of `grails-bom` and pins the `io.micronaut.platform:micronaut-platform` version it was built against. Applying it as `enforcedPlatform` makes all of its constraints strictly versioned so that no transitive dependency (including Micronaut's own platform) can override them — there is no need to set a `micronautPlatformVersion` Gradle property. See link:{versionsRef}Grails%20BOM%20Micronaut.html[Grails Micronaut BOM Dependencies] for the full list of managed versions. +==== Hibernate-Specific Micronaut BOMs + +If your project targets a specific Hibernate version, use the corresponding Hibernate-specific Micronaut BOM instead of the generic `grails-micronaut-bom`: + +* `org.apache.grails:grails-hibernate5-micronaut-bom` -- Micronaut + Hibernate 5 (the default). See link:{versionsRef}Grails%20BOM%20Hibernate5%20Micronaut.html[Grails Hibernate 5 Micronaut BOM Dependencies]. + +[source,groovy] +.build.gradle - Hibernate 5 with Micronaut +---- +dependencies { + implementation enforcedPlatform("org.apache.grails:grails-hibernate5-micronaut-bom:$grailsVersion") + implementation 'org.apache.grails:grails-micronaut' +} +---- + +The generic `grails-micronaut-bom` remains available and currently tracks the Hibernate 5 default. If the framework changes its default Hibernate version in the future, `grails-micronaut-bom` will follow the new default while the Hibernate-specific BOMs remain pinned. + When the `grails-micronaut` plugin is present, the Grails Gradle plugin will automatically apply the required annotation processors to your project and validate that `grails-micronaut-bom` is applied as `enforcedPlatform`. The validation fails the build at configuration time with an actionable error if the BOM is missing or applied as plain `platform(...)`. ==== Disabling the Micronaut Auto-Setup diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index ef5dd4e1ede..ad5424f1373 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -267,6 +267,25 @@ This affects any code path that goes through Micronaut's HTTP client, filters, o NOTE: The Grails Forge generator enforces this requirement: selecting any Micronaut feature with a JDK version below 25 will fail with `IllegalArgumentException` at generation time. +===== 7.4 Hibernate-Specific Micronaut BOMs + +Grails 8 introduces a new BOM for projects that combine Micronaut integration with a specific Hibernate version: + +* `org.apache.grails:grails-hibernate5-micronaut-bom` -- For Micronaut projects using Hibernate 5. This is the default Micronaut BOM for Hibernate 5 users (analogous to how `grails-micronaut-bom` is the default). Use this when your Micronaut-enabled Grails application targets Hibernate 5. + +This BOM extends `grails-base-bom` and the Micronaut platform directly, and includes the appropriate Hibernate dependency versions alongside the Micronaut-specific overrides (Groovy 5, Spock 2.4, etc.). This ensures no version conflicts when consumed via `enforcedPlatform`. + +[source,groovy] +.build.gradle - Hibernate 5 with Micronaut (default) +---- +dependencies { + implementation enforcedPlatform("org.apache.grails:grails-hibernate5-micronaut-bom:$grailsVersion") + implementation 'org.apache.grails:grails-micronaut' +} +---- + +The existing `grails-micronaut-bom` remains available and is equivalent to `grails-hibernate5-micronaut-bom` for backward compatibility. If the framework changes its default Hibernate version in the future, `grails-micronaut-bom` will track the new default while the Hibernate-specific BOM remains pinned to its version. + ==== 8. Enum Serialization Default Changed As announced in the Grails 7.0.2 deprecation notice, the `SimpleEnumMarshaller` is now the default for JSON and XML enum serialization in Grails 8. diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy index 4399d3fb184..277982d1f72 100644 --- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy +++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy @@ -464,11 +464,11 @@ ${importStatements} } /** - * Validates that grails-micronaut-bom is applied as an enforcedPlatform when micronaut is used. - * The grails-micronaut-bom layers Micronaut-specific overrides (e.g. javaparser-core) on top - * of grails-bom; without enforcedPlatform, Micronaut's platform would override these versions - * via Gradle's conflict resolution. Regular Grails projects (without Micronaut) should continue - * to use the spring-managed versions via plain platform(:grails-bom). + * Validates that a Micronaut-compatible BOM is applied as an enforcedPlatform when micronaut is used. + * The grails-micronaut-bom (and its hibernate-specific variants) layers Micronaut-specific overrides + * (e.g. javaparser-core) on top of grails-bom; without enforcedPlatform, Micronaut's platform would + * override these versions via Gradle's conflict resolution. Regular Grails projects (without Micronaut) + * should continue to use the spring-managed versions via plain platform(:grails-bom). */ @CompileStatic protected static void validateMicronautBom(Project project) { @@ -477,8 +477,13 @@ ${importStatements} return } + Set validMicronautBoms = [ + 'grails-micronaut-bom', + 'grails-hibernate5-micronaut-bom', + ] as Set + for (Dependency dep : implConfig.dependencies) { - if (dep.name == 'grails-micronaut-bom' && dep instanceof ModuleDependency) { + if (dep.name in validMicronautBoms && dep instanceof ModuleDependency) { Object categoryAttr = ((ModuleDependency) dep).attributes.getAttribute( org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE ) @@ -489,10 +494,11 @@ ${importStatements} } throw new GradleException( - "Project '${project.name}' uses Micronaut but does not apply grails-micronaut-bom as an enforcedPlatform. " + + "Project '${project.name}' uses Micronaut but does not apply a Micronaut BOM as an enforcedPlatform. " + "Micronaut's platform declares higher versions of javaparser-core and other libraries that would " + - 'override the grails-bom versions via conflict resolution. Change to:\n\n' + - ' implementation enforcedPlatform(project(\':grails-micronaut-bom\'))\n' + 'override the grails-bom versions via conflict resolution. Change to one of:\n\n' + + ' implementation enforcedPlatform("org.apache.grails:grails-micronaut-bom:$grailsVersion")\n' + + ' implementation enforcedPlatform("org.apache.grails:grails-hibernate5-micronaut-bom:$grailsVersion")\n' ) } diff --git a/grails-test-examples/micronaut-hibernate5/build.gradle b/grails-test-examples/micronaut-hibernate5/build.gradle new file mode 100644 index 00000000000..2ef04fb941d --- /dev/null +++ b/grails-test-examples/micronaut-hibernate5/build.gradle @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +plugins { + id 'org.apache.grails.buildsrc.properties' + id 'org.apache.grails.buildsrc.compile' + id 'org.apache.grails.buildsrc.dependency-validator' +} + +version = '0.1' +group = 'micronaut.hibernate5' + +apply plugin: 'org.apache.grails.gradle.grails-web' +apply plugin: 'cloud.wondrify.asset-pipeline' +apply plugin: 'org.apache.grails.gradle.grails-gsp' + +dependencies { + implementation enforcedPlatform(project(':grails-hibernate5-micronaut-bom')) + + implementation 'io.micronaut:micronaut-http-client' + implementation 'io.micronaut.serde:micronaut-serde-jackson' + implementation 'org.apache.grails:grails-dependencies-starter-web' + implementation 'org.apache.grails:grails-data-hibernate5' + implementation 'org.apache.grails:grails-micronaut' + + annotationProcessor enforcedPlatform(project(':grails-hibernate5-micronaut-bom')) + annotationProcessor 'io.micronaut:micronaut-inject-java' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api' + + testAndDevelopmentOnly enforcedPlatform(project(':grails-hibernate5-micronaut-bom')) + testAndDevelopmentOnly 'org.apache.grails:grails-dependencies-assets' + + runtimeOnly 'cloud.wondrify:asset-pipeline-grails' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'org.apache.tomcat:tomcat-jdbc' + + testImplementation 'org.apache.grails:grails-dependencies-test' + + integrationTestImplementation testFixtures('org.apache.grails:grails-geb') +} + +apply { + from rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/test-webjar-asset-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle') +} diff --git a/grails-test-examples/micronaut-hibernate5/grails-app/conf/application.yml b/grails-test-examples/micronaut-hibernate5/grails-app/conf/application.yml new file mode 100644 index 00000000000..1100bfd590b --- /dev/null +++ b/grails-test-examples/micronaut-hibernate5/grails-app/conf/application.yml @@ -0,0 +1,74 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +grails: + profile: web + codegen: + defaultPackage: micronaut.hibernate5 +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +app: + name: test-micronaut-hibernate5-app + +--- +grails: + mime: + types: + all: '*/*' + html: + - text/html + - application/xhtml+xml + json: + - application/json + - text/json + xml: + - text/xml + - application/xml + views: + gsp: + encoding: UTF-8 + htmlcodec: xml + codecs: + scriptlet: html +--- +hibernate: + cache: + queries: false + use_second_level_cache: false + use_query_cache: false +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: '' + +environments: + development: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + test: + dataSource: + dbCreate: update + url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + production: + dataSource: + dbCreate: none + url: jdbc:h2:./prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE diff --git a/grails-test-examples/micronaut-hibernate5/grails-app/conf/logback.xml b/grails-test-examples/micronaut-hibernate5/grails-app/conf/logback.xml new file mode 100644 index 00000000000..11f34868ac6 --- /dev/null +++ b/grails-test-examples/micronaut-hibernate5/grails-app/conf/logback.xml @@ -0,0 +1,37 @@ + + + + + + + + + + UTF-8 + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex + + + + + + + \ No newline at end of file diff --git a/grails-test-examples/micronaut-hibernate5/grails-app/controllers/micronaut/hibernate5/UrlMappings.groovy b/grails-test-examples/micronaut-hibernate5/grails-app/controllers/micronaut/hibernate5/UrlMappings.groovy new file mode 100644 index 00000000000..2758d738e57 --- /dev/null +++ b/grails-test-examples/micronaut-hibernate5/grails-app/controllers/micronaut/hibernate5/UrlMappings.groovy @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package micronaut.hibernate5 + +class UrlMappings { + + static mappings = { + "/$controller/$action?/$id?(.$format)?"{ + constraints { + } + } + + "/"(view: "/index") + } +} diff --git a/grails-test-examples/micronaut-hibernate5/grails-app/init/micronaut/hibernate5/Application.groovy b/grails-test-examples/micronaut-hibernate5/grails-app/init/micronaut/hibernate5/Application.groovy new file mode 100644 index 00000000000..99100b79039 --- /dev/null +++ b/grails-test-examples/micronaut-hibernate5/grails-app/init/micronaut/hibernate5/Application.groovy @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package micronaut.hibernate5 + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration +import groovy.transform.CompileStatic +import io.micronaut.spring.boot.starter.EnableMicronaut + +@CompileStatic +@EnableMicronaut +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } +} diff --git a/grails-test-examples/micronaut-hibernate5/grails-app/views/index.gsp b/grails-test-examples/micronaut-hibernate5/grails-app/views/index.gsp new file mode 100644 index 00000000000..1c032d67936 --- /dev/null +++ b/grails-test-examples/micronaut-hibernate5/grails-app/views/index.gsp @@ -0,0 +1,25 @@ +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%> + + + + Micronaut Hibernate 5 Test App + + +

Micronaut Hibernate 5 Test Application

+ + diff --git a/grails-test-examples/micronaut-hibernate5/src/integration-test/groovy/micronaut/hibernate5/ApplicationStartupSpec.groovy b/grails-test-examples/micronaut-hibernate5/src/integration-test/groovy/micronaut/hibernate5/ApplicationStartupSpec.groovy new file mode 100644 index 00000000000..b6a889f8685 --- /dev/null +++ b/grails-test-examples/micronaut-hibernate5/src/integration-test/groovy/micronaut/hibernate5/ApplicationStartupSpec.groovy @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package micronaut.hibernate5 + +import grails.plugin.geb.ContainerGebSpec +import grails.testing.mixin.integration.Integration + +@Integration +class ApplicationStartupSpec extends ContainerGebSpec { + + void "test the application starts and the home page renders"() { + when: 'The home page is visited' + go '/' + + then: 'The page loads successfully' + title || true // Grails default index page has a title, but we just need the server to respond + driver.currentUrl.contains('/') + } +} diff --git a/settings.gradle b/settings.gradle index 969f1c7f65c..cf9363143c2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -511,14 +511,18 @@ includeBuild('./build-logic') { if (!skipMicronautProjects) { include( 'grails-micronaut-bom', + 'grails-hibernate5-micronaut-bom', 'grails-micronaut', 'grails-test-examples-issue-11767', 'grails-test-examples-micronaut', 'grails-test-examples-micronaut-groovy-only', + 'grails-test-examples-micronaut-hibernate5', 'grails-test-examples-plugins-issue-11767', 'grails-test-examples-plugins-micronaut-singleton', ) project(':grails-micronaut-bom').projectDir = file('grails-bom/micronaut') + project(':grails-hibernate5-micronaut-bom').projectDir = file('grails-bom/hibernate5-micronaut') + project(':grails-test-examples-micronaut-hibernate5').projectDir = file('grails-test-examples/micronaut-hibernate5') project(':grails-test-examples-issue-11767').projectDir = file('grails-test-examples/issue-11767') project(':grails-test-examples-micronaut').projectDir = file('grails-test-examples/micronaut') project(':grails-test-examples-micronaut-groovy-only').projectDir = file('grails-test-examples/micronaut-groovy-only') From 35527d80c590183aaa14adfeda800c3beae30e1f Mon Sep 17 00:00:00 2001 From: James Daugherty Date: Thu, 28 May 2026 14:42:52 -0400 Subject: [PATCH 2/3] Address Copilot review feedback on the Micronaut BOM split - Remove the broken doc link to the not-yet-generated "Grails BOM Hibernate5 Micronaut" reference page - Clarify that validateMicronautBom accepts grails-micronaut-bom or grails-hibernate5-micronaut-bom as the enforcedPlatform BOM - Register grails-hibernate5-micronaut-bom in GrailsDependencyValidatorPlugin.BOM_PROJECT_NAMES so the new sample's dependency versions are actually validated - Make the micronaut-hibernate5 startup spec assert the rendered page title instead of the always-true title || true expression - Drop the commented-out Develocity testRetry block from test-config Assisted-by: claude-code:claude-4.7-opus --- .../grails/buildsrc/GrailsDependencyValidatorPlugin.groovy | 2 +- gradle/test-config.gradle | 7 ------- grails-doc/src/en/guide/conf/micronaut.adoc | 4 ++-- .../micronaut/hibernate5/ApplicationStartupSpec.groovy | 2 +- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsDependencyValidatorPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsDependencyValidatorPlugin.groovy index 74b1a7c0751..7dc312d79fb 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsDependencyValidatorPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsDependencyValidatorPlugin.groovy @@ -55,7 +55,7 @@ class GrailsDependencyValidatorPlugin implements Plugin { */ static final String ALLOWED_OVERRIDES_EXT = 'allowedBomOverrides' - private static final Set BOM_PROJECT_NAMES = ['grails-bom', 'grails-gradle-bom', 'grails-base-bom', 'grails-hibernate5-bom', 'grails-micronaut-bom'].toSet() + private static final Set BOM_PROJECT_NAMES = ['grails-bom', 'grails-gradle-bom', 'grails-base-bom', 'grails-hibernate5-bom', 'grails-micronaut-bom', 'grails-hibernate5-micronaut-bom'].toSet() @Override void apply(Project project) { diff --git a/gradle/test-config.gradle b/gradle/test-config.gradle index d8d51dbabec..ae1a2b407be 100644 --- a/gradle/test-config.gradle +++ b/gradle/test-config.gradle @@ -63,13 +63,6 @@ tasks.withType(Test).configureEach { useJUnitPlatform() jvmArgs += java17moduleReflectionCompatibilityArguments -// develocity { -// testRetry { -// maxRetries = configuredTestParallel == 1 ? 1 : 2 -// maxFailures = 20 -// failOnPassedAfterRetry = true -// } -// } testLogging { events('passed', 'skipped', 'failed') showExceptions = true diff --git a/grails-doc/src/en/guide/conf/micronaut.adoc b/grails-doc/src/en/guide/conf/micronaut.adoc index 7d08c8337b3..cf2429985ce 100644 --- a/grails-doc/src/en/guide/conf/micronaut.adoc +++ b/grails-doc/src/en/guide/conf/micronaut.adoc @@ -47,7 +47,7 @@ The `grails-micronaut-bom` layers Micronaut-specific dependency overrides on top If your project targets a specific Hibernate version, use the corresponding Hibernate-specific Micronaut BOM instead of the generic `grails-micronaut-bom`: -* `org.apache.grails:grails-hibernate5-micronaut-bom` -- Micronaut + Hibernate 5 (the default). See link:{versionsRef}Grails%20BOM%20Hibernate5%20Micronaut.html[Grails Hibernate 5 Micronaut BOM Dependencies]. +* `org.apache.grails:grails-hibernate5-micronaut-bom` -- Micronaut + Hibernate 5 (the default). [source,groovy] .build.gradle - Hibernate 5 with Micronaut @@ -60,7 +60,7 @@ dependencies { The generic `grails-micronaut-bom` remains available and currently tracks the Hibernate 5 default. If the framework changes its default Hibernate version in the future, `grails-micronaut-bom` will follow the new default while the Hibernate-specific BOMs remain pinned. -When the `grails-micronaut` plugin is present, the Grails Gradle plugin will automatically apply the required annotation processors to your project and validate that `grails-micronaut-bom` is applied as `enforcedPlatform`. The validation fails the build at configuration time with an actionable error if the BOM is missing or applied as plain `platform(...)`. +When the `grails-micronaut` plugin is present, the Grails Gradle plugin will automatically apply the required annotation processors to your project and validate that a Micronaut-compatible BOM (`grails-micronaut-bom` or `grails-hibernate5-micronaut-bom`) is applied as `enforcedPlatform`. The validation fails the build at configuration time with an actionable error if no such BOM is applied or it is applied as plain `platform(...)`. ==== Disabling the Micronaut Auto-Setup diff --git a/grails-test-examples/micronaut-hibernate5/src/integration-test/groovy/micronaut/hibernate5/ApplicationStartupSpec.groovy b/grails-test-examples/micronaut-hibernate5/src/integration-test/groovy/micronaut/hibernate5/ApplicationStartupSpec.groovy index b6a889f8685..4fd91a3bc84 100644 --- a/grails-test-examples/micronaut-hibernate5/src/integration-test/groovy/micronaut/hibernate5/ApplicationStartupSpec.groovy +++ b/grails-test-examples/micronaut-hibernate5/src/integration-test/groovy/micronaut/hibernate5/ApplicationStartupSpec.groovy @@ -27,7 +27,7 @@ class ApplicationStartupSpec extends ContainerGebSpec { go '/' then: 'The page loads successfully' - title || true // Grails default index page has a title, but we just need the server to respond + title == 'Micronaut Hibernate 5 Test App' driver.currentUrl.contains('/') } } From 31ec1060bc14d7a56b4424c8fe92d21c8a6efa0e Mon Sep 17 00:00:00 2001 From: James Fredley Date: Fri, 29 May 2026 02:18:09 -0400 Subject: [PATCH 3/3] fix(bom): exclude tools.jackson from grails-hibernate5-micronaut-bom platform The new grails-hibernate5-micronaut-bom re-exported the Micronaut platform without excluding Jackson 3 (tools.jackson), unlike grails-micronaut-bom. As a result micronaut-platform's jackson-bom 3.1.0 leaked into the BOM's managed versions while spring-boot-dependencies (SB 4.0.6) ships 3.1.2, causing validateDependencyVersions to fail for grails-test-examples-micronaut-hibernate5 (resolved 3.1.2, expected 3.1.0). Mirror the grails-micronaut-bom exclusion so Spring Boot manages the Jackson 3 version consistently. Verified locally: :grails-test-examples-micronaut-hibernate5:validateDependencyVersions passes. Assisted-by: claude-code:claude-4.8-opus --- grails-bom/hibernate5-micronaut/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/grails-bom/hibernate5-micronaut/build.gradle b/grails-bom/hibernate5-micronaut/build.gradle index 8ee4214370c..9d91c8b5b8c 100644 --- a/grails-bom/hibernate5-micronaut/build.gradle +++ b/grails-bom/hibernate5-micronaut/build.gradle @@ -75,9 +75,13 @@ dependencies { // Re-export the Micronaut platform so consumers inherit Micronaut's managed versions // transitively. Exclude Groovy since we declare the required version explicitly via // customBomDependencies. Exclude Spock since we manage that version ourselves. + // Exclude Jackson 3 (tools.jackson) since spring-boot-dependencies manages that version, + // and Micronaut can lag behind Spring Boot's patch bumps (e.g. SB 4.0.6 ships + // jackson-bom 3.1.2 while micronaut-platform 5.0.0-M2 still pins 3.1.0). api(platform("io.micronaut.platform:micronaut-platform:$micronautPlatformVersion")) { exclude group: 'org.apache.groovy' exclude group: 'org.spockframework' + exclude group: 'tools.jackson' } constraints {