////
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.
////

This section covers all the new features introduced in Grails {grailsMajorVersion}

=== Overview

Grails {grailsMajorVersion} is a major release that includes new features, improvements, and dependency upgrades.
This release focuses on enhancing the developer experience, improving performance, and ensuring compatibility with the latest technologies.

For detailed information on how to upgrade to Grails {grailsMajorVersion}, including major dependency changes, please see the xref:upgrading#upgrading80x[Upgrading from Grails 7 to Grails {grailsMajorVersion}] section.
Notable new features are included below.

==== Platform Baseline

Grails {grailsMajorVersion} raises the standard build and runtime baseline to Java 21 and uses Gradle {gradleVersion}.
The standard Grails BOM remains on Groovy {groovyVersion} and Spock {spockVersion}, while Micronaut-enabled Grails applications use Micronaut-specific BOM variants that align with Micronaut 5 and require JDK 25 or later.

==== Spring Boot 4.1 and Spring Framework 7

Grails {grailsMajorVersion} is built on Spring Boot {springBootVersion} and Spring Framework {springVersion}.
This brings the Spring Boot 4 modular artifact layout, Spring Framework 7 API removals, Jackson 3, Tomcat 11 and Jakarta Servlet 6.1, plus Spring Boot 4.1 dependency management for Spring Security 7.1, Spring Data 2026.0 and Micrometer 1.17.

The Grails 8 upgrade guide calls out the major application-impacting changes and links to the Spring Boot 4.0 migration guide, Spring Boot 4.1 release notes and Spring Framework 7.0 release notes.

==== Hibernate 7 Application Generation

Grails Forge can now generate applications configured for Grails Data with Hibernate 7.
The `create-*` commands also accept a new `-d, --data` option (the previous `-g` and `--gorm` flags remain supported as legacy aliases) with the values `hibernate5`, `hibernate7`, and `mongodb`; the legacy value `hibernate` is still accepted and selects Hibernate 5:

[source,console]
----
grails -t forge create-app --data=hibernate7 com.example.demo
----

Applications generated for Hibernate 7 consume `grails-hibernate7-bom` — or `grails-hibernate7-micronaut-bom` when combined with the `grails-micronaut` feature — consistently across the application dependencies, the buildscript classpath, and `buildSrc`.
This avoids the dependency resolution conflicts that occur when an application generated for Hibernate 5 is manually switched to `grails-data-hibernate7` while the default `grails-bom` remains on the build classpath.
The `database-migration` feature also selects the matching `grails-data-hibernate7-dbmigration` plugin automatically.

==== Plugin Beans Register Before Spring Boot Auto-Configuration

Grails {grailsMajorVersion} unifies and retimes the plugin lifecycle so that the beans a plugin contributes are registered before Spring Boot processes its auto-configurations.
A plugin bean now takes precedence over a Spring Boot default guarded by `@ConditionalOnMissingBean` — Boot backs off in favour of the plugin's bean, with no need to override or remove it afterwards.

Plugins register beans through the new `beanRegistrar()` hook, which returns a Spring Framework {springapi}org/springframework/beans/factory/BeanRegistrar.html[BeanRegistrar]. Because `BeanRegistrar` is a functional interface, a closure coerced with `as BeanRegistrar` is all that is required:

[source,groovy]
----
import org.springframework.beans.factory.BeanRegistrar
import org.springframework.beans.factory.BeanRegistry
import org.springframework.core.env.Environment
import grails.plugins.Plugin

class MyGrailsPlugin extends Plugin {

    @Override
    BeanRegistrar beanRegistrar() {
        { BeanRegistry registry, Environment environment ->
            registry.registerBean('myService', MyServiceImpl)
        } as BeanRegistrar
    }
}
----

`beanRegistrar()` replaces the `doWithSpring` bean builder DSL, which is deprecated in Grails {grailsMajorVersion}.
The DSL continues to work for now, but you are strongly urged to migrate.
See the xref:upgrading#upgrading80x[upgrade guide] for details.

==== CLI Commands Move to Companion `-cli` Artifacts

Grails commands (`ApplicationCommand` implementations such as the `dbm-*` database-migration
commands, the scaffolding `generate-*` commands, `url-mappings-report`, and `schema-export`) no
longer ship inside the runtime plugin artifacts.
Each command-bearing module now publishes a companion artifact under its own Maven coordinate with
a `-cli` suffix — for example `org.apache.grails:grails-core-cli` and
`org.apache.grails:grails-data-hibernate7-dbmigration-cli`.

The command contract itself moved from `grails.dev.commands.*` to `org.apache.grails.core.cli.*`
in the new `grails-core-cli` artifact.
Applications consume cli artifacts through the new `grailsCli` Gradle configuration, which is
compile-visible for `grails-app/commands` sources and on the command-runner classpath, but never on
`runtimeClasspath` — commands and their CLI-only dependencies (such as `grails-shell-cli`) are
excluded from `bootRun`, `bootJar`, and `bootWar` entirely.
The Grails Gradle plugin provisions `grailsCli` automatically: it adds `grails-core-cli` and
`grails-console`, and discovers every companion cli artifact advertised by the application's
dependency graph — a command-bearing plugin advertises its companion through the
`Grails-Cli-Artifact` manifest attribute of its runtime jar, and its commands are registered in a
`META-INF/grails-cli.factories` file inside the companion jar. Declaring a plugin is therefore all
that is needed; its commands (including the per-command Gradle tasks such as `dbmUpdate`) follow
automatically, even when the plugin is only a transitive dependency:

[source,groovy]
.build.gradle
----
dependencies {
    // the dbm-* commands are discovered automatically: the plugin jar advertises
    // org.apache.grails:grails-data-hibernate7-dbmigration-cli, which is added to grailsCli
    implementation 'org.apache.grails:grails-data-hibernate7-dbmigration'
}
----

The auto-provisioning can be disabled with `grails { cliAutoProvision = false }`, in which case
`grailsCli` dependencies are declared explicitly.

The plugin can also provision `grails-core-cli-legacy` into a separate execution-only
`grailsCliLegacy` bucket when both `cliAutoProvision` (default `true`) and
`legacyCommandSupport` (default `false`) are enabled. That bucket contributes to
`grailsCliClasspath`, never to application or plugin compile classpaths. Opt in with
`grails { legacyCommandSupport = true }` so unchanged Grails 7 command plugins keep working
without a re-release, without leaking `grails.dev.commands.*` into the Grails 8
command-authoring ABI.

When that bridge is disabled, runtime command discovery still detects plugins that publish legacy
`grails.dev.commands.ApplicationCommand` factories without loading their command classes. Grails
logs the originating plugin artifact and appends the exact opt-in or upgrade remediation when an
unknown command is requested. The detection remains on the resolved command runtime classpath and
does not add a Gradle configuration-time dependency scan.

Plugin authors get the whole publishing side from the new
`org.apache.grails.gradle.grails-plugin-cli` Gradle plugin: it creates the `cli` source set as a
feature variant, wires the command contract onto its compile classpath, advertises the companion
from the runtime jar, and publishes it (via the Grails publish plugin) under the plugin's
coordinate with the `-cli` suffix — the framework's own command-bearing modules are built with the
same plugin.

This removes CLI-only dependencies — and their side effects, such as the WAR-deployment failure
caused by a CLI Spring Boot initializer on the runtime classpath — from packaged applications.

The split does not hold up application upgrades on plugin re-releases: a plugin that ships no
commands is unaffected, and a command-bearing plugin that has not yet been rebuilt for Grails 8
keeps its runtime functionality. Its legacy `grails.dev.commands.ApplicationCommand` registrations
can continue to work through a deprecated compatibility layer when `legacyCommandSupport` is
enabled, so those commands remain available without a re-release; publishing a companion `-cli`
artifact is recommended when migrating to the Grails 8 command API. New commands should use
`org.apache.grails.core.cli.*`. A plugin that still authors legacy commands must explicitly compile
against `grails-core-cli-legacy`; consumers of an already-published legacy plugin enable the bridge
with `grails { legacyCommandSupport = true }` (or declare `grailsCliLegacy` themselves). See the
xref:upgrading#upgrading80x[upgrade guide] for the migration steps and the full upgrade-impact
breakdown.

==== GSP Tag Library Improvements

Grails {grailsMajorVersion} continues the move toward method-based TagLib handlers while preserving compatibility with existing closure-based tags.
Method-defined tags now bind named attributes more predictably, exclude inherited framework and `Object` methods from tag dispatch, and preserve real namespace property getters.

The Grails Gradle extension now defaults `preserveParameterNames` to `true`, so application Groovy compilation preserves method parameter names for features such as typed method TagLib arguments.

Tag library unit tests also clean up and rebuild TagLib metadata automatically between features.
Tests that use `TagLibUnitTest` no longer need to manage `purgeTagLibMetaClass`, and specs that mock additional tag libraries continue to work across feature methods.

==== Namespace-Aware Link Generation

Links, form actions, pagination links, sortable column links, redirects, chains, and includes now resolve the target controller namespace automatically when `namespace` is omitted.
In the normal case, where only one controller has the target name, `controller` and `action` are enough to generate the correct namespaced or non-namespaced URL.
If multiple controllers share the same name, specify `namespace` to choose one explicitly.
Use `namespace=""` in GSP markup, or `namespace: null` in Groovy code, to target a non-namespaced controller.

==== @GrailsCompileStatic on Controllers That Use Tag Libraries

Controllers annotated with `@GrailsCompileStatic` can now invoke tag library methods without compile-time errors.

Both calling patterns are supported out of the box:

[source,groovy]
----
import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class BookController {

    def index() {
        // Direct call in the default namespace
        response.writer << link(controller: 'book', action: 'list')

        // Namespaced call via a dispatcher property
        response.writer << my.customTag(attr: 'value')
    }
}
----

`@GrailsCompileStatic` recognises controllers and tag libraries by convention and marks tag dispatch points as permissible dynamic calls, while leaving the rest of the class fully type-checked.

==== Opt All Controllers, Services and Tag Libraries into GrailsCompileStatic

Static compilation can now be enabled for every controller, service and tag library in an application from the nested `compileStatic` block of the `grails` build configuration, without annotating each class:

[source,groovy]
.build.gradle
----
grails {
    compileStatic {
        controllers = true
        services = true
        tagLibs = true
    }
}
----

All three options are disabled by default and independent, or `compileStatic { all = true }` can be used as a shortcut to enable all of them. Any class that declares its own `@CompileDynamic` (or `@GrailsCompileStatic`/`@CompileStatic`/`@GrailsTypeChecked`/`@TypeChecked`) annotation keeps that setting, so the opt-in never overrides an explicit per-class choice. Tag libraries that still declare tags as closure fields (the deprecated form) are skipped automatically by the `tagLibs` opt-in and left dynamically compiled with a build warning, so define tags as methods to opt them in. See the xref:staticTypeCheckingAndCompilation/grailsCompileStatic[GrailsCompileStatic] section for details.

==== Test HTTP Client Form Requests

Integration tests that implement `HttpClientSupport` can now use `httpPostForm(...)` to send `application/x-www-form-urlencoded`
request bodies. The helper URL-encodes form fields using UTF-8, and repeated values in a `Collection` or array are encoded as
repeated keys in insertion order.

==== Latency Testing Support

The new `grails-testing-support-latency` module injects artificial latency into the application under test, so that
requests randomly take longer. Slower responses widen the window in which timing bugs occur, turning intermittently
flaky functional tests — assertions that race a navigation, missing waits, ordering assumptions — into deterministic
failures. The module is inert until `grails.testing.latency.enabled` is set to `true`, and the delay range, the
fraction of requests affected, and the url patterns are all configurable. See the
xref:testing/functionalTesting[Functional Testing] section for details.

==== GORM for MongoDB: TTL, Text, and Reconciled Indexes

GORM for MongoDB can now declare a https://www.mongodb.com/docs/manual/core/index-ttl/[TTL index] directly in the mapping DSL.
Adding `expireAfterSeconds` to the `indexAttributes` of a single date-valued property creates a TTL index, and MongoDB
automatically removes each document once it reaches the configured age:

[source,groovy]
----
class LoginEvent {
    Date dateCreated
    static mapping = {
        dateCreated index: true, indexAttributes: [expireAfterSeconds: 3600]
    }
}
----

A single property can likewise declare a `$text` index — or another special type such as `2dsphere` — through
`indexAttributes: [type: 'text']`.

When the options of an already-created index change between restarts, GORM now reconciles the difference instead of only
logging a conflict. A changed TTL is applied in place with MongoDB's `collMod` command — no drop, no rebuild — while any
other option change is applied by declaring `indexAttributes: [recreateOnConflict: true]`.

==== GORM for MongoDB and Spring Data MongoDB Interoperability

A new optional module, `grails-data-mongodb-spring-data`, lets an application use GORM for MongoDB and
https://spring.io/projects/spring-data-mongodb[Spring Data MongoDB] — its `MongoTemplate` and repositories — side by
side over the *same* `MongoClient`, database and codecs, and, within a single `@Transactional` method, the *same*
MongoDB transaction. When the module and `spring-data-mongodb` are on the classpath of a Spring Boot application that
already has a GORM `MongoDatastore`, it auto-configures a `MongoDatabaseFactory`, a `MongoTemplate` and a primary
`transactionManager` over GORM's existing connection — GORM keeps ownership of the client:

[source,groovy]
----
@Transactional
void transfer(MongoTemplate mongoTemplate) {
    new Account(name: "from").save()                  // GORM
    mongoTemplate.insert(new LedgerEntry(amount: 10)) // Spring Data
    // both commit together, or neither is applied if an exception is thrown
}
----

The unified transaction shares GORM's server-side `ClientSession`, so it requires GORM multi-document transactions to
be enabled (`grails.mongodb.transactional = true`). Spring Data repositories are enabled the usual way with
`@EnableMongoRepositories`, on a package separate from the GORM `@Entity` classes; only the connection, codecs and
session are shared, and the two object-mapping models stay separate. See the
link:{mongodb5Guide}index.html#springDataInterop[Spring Data MongoDB Interoperability] section of the GORM for MongoDB
guide for details.

==== Ahead-of-Time Processing

A Grails application can now be processed by Spring's ahead-of-time engine. Applying Spring Boot's AOT plugin adds a
`processAot` task, which generates the bean definitions as Java source at build time instead of deriving them from
classpath scanning and annotations on every start:

[source,groovy]
.build.gradle
----
apply plugin: 'org.springframework.boot.aot'

tasks.named('processAot') {
    systemProperty 'grails.env', 'production'
}
----

[source,bash]
----
java -Dspring.aot.enabled=true -jar build/libs/myapp.jar
----

AOT processing is also what a GraalVM native image requires: Spring Boot refuses to start an image without a
build-time generated initializer. Reachability metadata for the application's own artefacts and compiled pages is
written from the build output, and `traceNativeMetadata` records what the framework reflects on along a request path.

See the xref:deployment/deploymentAot[Ahead-of-Time Processing] section for what it changes at runtime and what it
does not yet cover.

==== Ahead-of-Time Caching

On JDK 25 or later, `grails.aotCache` trains a JDK AOT cache by running the packaged application once and recording
the classes it loaded and linked and the methods it ran, so the next start reads that work rather than repeating it:

[source,groovy]
.build.gradle
----
grails {
    aotCache {
        enabled = true
        paths = ['/', '/login', '/book/index']
    }
}
----

See the xref:deployment/deploymentAotCache[Ahead-of-Time Caching] section, which includes measured figures and the
conditions under which a JVM will decline a cache.

==== Banner Colour, Container Version and Start Mark

The startup banner is coloured, reports the servlet container it is running on, and says how the application was
started where that is worth saying — `NATIVE` for an image, `AOT CACHE` for a JVM given a cache to read, `AOT` for one
running generated bean definitions. An ordinary start says nothing.

Spring Security and the servlet container are also shown by default now. What allows that is a smaller change
underneath: a version shown by default that cannot be determined is left out rather than shown as `unknown`, so
an application without Spring Security says nothing about it instead of saying it does not know. One asked for
by name still reads `unknown`, because saying nothing about what was asked for reads as the option having been
ignored. See
xref:conf/applicationClass/customizing[Customizing the Application Class] for the full set of banner options, and the
xref:upgrading#upgrading80x[upgrade guide] for how to restore the previous output.

==== Message Bundles Resolved by Spring Boot

Internationalization now runs on Spring Boot's own `MessageSourceAutoConfiguration`. Grails' custom
message source is gone, and with it the `classpath*:*.properties` scan it performed at start-up.

The base names Spring Boot needs are recorded at build time, per application and per plugin, so the
whole `spring.messages.*` surface works as it does in any Boot application:

[source,yaml]
----
spring:
    messages:
        cache-duration: 5s
        use-code-as-default-message: true
----

Those properties were previously ignored, because Grails' own message source claimed the bean.

Because discovery no longer scans, the message bundles an application and its plugins ship are known
ahead of time. Grails registers the GraalVM resource hints for them — including plugin bundles and any
base name an application configures itself, neither of which Spring Boot's own registrar covers.

Plugin message bundles must now be namespaced on the plugin name — `spring-security-core.properties`
rather than `messages.properties` — so that two plugins cannot shadow one another. See
<<upgrading80x>> for the details.

==== Quartz Scheduling Survives a Bad Schedule

The Quartz plugin no longer lets one broken schedule take an application with it. A trigger that can
never fire — a cron expression whose last occurrence has passed, an end time
already gone by — is reported in the log and left unscheduled instead of stopping startup, and
`quartz.failOnNeverFiringTriggers` restores the old behaviour for anyone who wants a bad schedule to fail
a deployment.

Jobs are also stamped with the application that registered them, so an application starting up removes
only its own jobs from the job store. Two applications, or a clustered application and code scheduling
through the Quartz API, can now share Quartz tables without one deleting the other's jobs and triggers.

The scheduling methods a job class gets say what is wrong when they are misused: a null repeat interval,
cron expression, date or trigger names the argument that is missing, and calling them on a job the
scheduler does not know about explains why it is not registered — neither ends in a `NullPointerException`
any more. See <<quartzLongRunningJobs,Long-Running Jobs>> for what a job that outlives its trigger's
interval needs, and <<upgrading80x>> for the details.

==== Compiled Tag Resolution

Tag libraries are now described when they are compiled, and that description resolves tag calls in
pages, tag libraries and controllers compiled afterwards. In a tag library or a controller, a call
whose namespace and tag are known is compiled into a direct invocation rather than being dispatched
through the metaclass, and no tag methods are installed onto tag library, page or dispatcher
metaclasses to make dispatch work:

[source,groovy]
----
class BookController {
    def index() {
        String markup = g.link(controller: 'book')   // compiled into a direct invocation
    }
}
----

The same applies to a call written without a namespace, to calls written inside a closure such as a
tag body, and to a tag expression in a GSP declaring `compileStatic`. The tag itself is still selected
by name when the call runs, so a tag library that overrides another, one registered while the
application is running, and the order tag libraries are registered in all behave exactly as before. A
namespace no compiled tag library declares, and a name something else in scope answers to, are left to
dispatch as they did.

A tag that no compiled tag library declares is left to resolve at runtime with nothing reported, since
a namespace can legitimately hold tag libraries carrying no description. An application whose tag
libraries are all described can ask for an error instead:

[source,groovy]
.build.gradle
----
grails {
    compileStatic {
        strictTags = true
        dynamicTagNamespaces = ['legacy']   // registered while the application runs
    }
}
----

Defining a tag as a `Closure` field remains supported but is deprecated and now warns at compile time:
a closure carries no signature, so nothing about a call to such a tag can be checked. Define tags as
methods taking `Map attrs` and, where a body is needed, `Closure body`. See
link:theWebLayer.html#compiledTags[Compiled Tag Resolution].
