codeconsole commented on PR #16102:
URL: https://github.com/apache/grails-core/pull/16102#issuecomment-5207010104

   # Spring Boot MessageSource Migration: Comparison
   
   Even without native-image/AOT support, the new implementation is a net 
improvement architecturally and operationally, with a few meaningful 
compatibility and development-workflow costs.
   
   ## Better than the previous implementation
   
   ### Standard Spring Boot behavior
   
   Previously Grails replaced Boot's message source, so standard properties 
such as these were mostly irrelevant:
   
   ```properties
   spring.messages.basename
   spring.messages.encoding
   spring.messages.cache-duration
   spring.messages.fallback-to-system-locale
   spring.messages.always-use-message-format
   spring.messages.use-code-as-default-message
   spring.messages.common-messages
   ```
   
   The new implementation delegates resolution to Boot's stock 
`ResourceBundleMessageSource`, making Grails behave like a normal Boot 
application and reducing custom framework behavior users must learn.
   
   ### Much less Grails-owned code
   
   It deletes roughly 1,000 lines from `PluginAwareResourceBundleMessageSource` 
and Grails' fork of `ReloadableResourceBundleMessageSource`. Those classes 
duplicated caching, merging, discovery, formatting, and plugin-specific loading 
behavior that Spring already handles.
   
   The replacement is primarily integration metadata:
   
   ```text
   bundle files → generated descriptor → Boot basename configuration
   ```
   
   ### No runtime wildcard scanning
   
   Previously Grails searched for `classpath*:*.properties`, while binary 
plugins scanned their resource roots for `*.properties`. Gradle now records 
known bundles during the build, and runtime discovery uses the exact descriptor 
name:
   
   ```text
   META-INF/grails/i18n.properties
   ```
   
   Benefits beyond AOT include:
   
   - lower startup work;
   - unrelated properties files cannot accidentally become message basenames;
   - no `searchClasspath` performance compromise;
   - behavior no longer depends on resource-pattern resolver peculiarities.
   
   ### Deterministic discovery and precedence
   
   The new implementation explicitly defines:
   
   ```text
   user-configured basenames
   → application bundles
   → plugins in reverse topological order
   ```
   
   It preserves the legacy winner for plugin message-code collisions while 
making the rule testable. Classloader enumeration order no longer determines 
behavior.
   
   ### Better plugin isolation
   
   Plugins now use namespaced bundles such as:
   
   ```text
   spring-security-core.properties
   spring-security-core-validation.properties
   ```
   
   rather than all shipping `messages.properties`. This prevents silent 
shadowing and makes bundle ownership obvious. Multiple logical bundles per 
plugin remain supported.
   
   ### Consistent encoding
   
   The old application and plugin paths did not necessarily use the same 
encoding. Binary plugin messages could use the platform encoding while 
application messages used configured UTF-8. Boot now applies one 
`spring.messages.encoding` value consistently to all basenames.
   
   ### Better configuration capabilities
   
   Boot supplies useful behavior Grails did not cleanly expose, particularly:
   
   ```properties
   spring.messages.common-messages
   spring.messages.always-use-message-format
   spring.messages.use-code-as-default-message
   ```
   
   Applications can also add explicit basenames outside `grails-app/i18n` 
without replacing the generated application and plugin list.
   
   For migration convenience, `grails.i18n.cache.seconds` remains temporarily 
supported in reload mode. Grails translates it to 
`spring.messages.cache-duration`, emits a deprecation warning, and lets an 
explicitly configured Boot property win. This preserves an existing 
development-time tuning without retaining the old message-source implementation.
   
   ### Available locales match actual resolution
   
   Previously locale discovery independently scanned resources and could 
disagree with the effective plugin set. Now basename composition and 
`AvailableLocaleResolver` use the same descriptor model. Filtered, evicted, 
failed, or deliberately excluded plugins cannot advertise locales whose 
messages will not resolve.
   
   ### Earlier failure for invalid packaging
   
   The Gradle build now rejects:
   
   - locale-only bundles without a base file;
   - colliding plugin basenames;
   - malformed or ambiguous locale naming;
   - duplicate plugin or application descriptors.
   
   Previously many such problems appeared only as missing or shadowed messages 
at runtime.
   
   ### Stronger tests
   
   The new implementation covers real Gradle application and plugin wiring, 
descriptor regeneration, plugin-name derivation, custom declarations, namespace 
enforcement, precedence, formatted and unformatted lookups, plugin filtering, 
reload behavior, and configuration precedence.
   
   ## Worse or more restrictive
   
   ### Existing plugin artifacts must be rebuilt and renamed
   
   This is the biggest cost. A plugin that currently ships 
`messages.properties` must move to a namespaced basename. Old plugin artifacts 
without generated descriptors no longer contribute messages automatically.
   
   Backward binary compatibility is explicitly out of scope, but this is still 
an ecosystem migration.
   
   The source migration is less restrictive than the artifact break suggests: 
namespaced bundles also work with Grails 7's classpath-scanning implementation. 
A plugin can therefore rename its bundles once and build the same source for 
both Grails 7 and Grails 8; only the Grails 8 artifact gains and requires the 
generated descriptor.
   
   ### Bundle naming is more constrained
   
   The build must infer whether an underscore introduces a locale. For example, 
`api_fr.properties` could mean basename `api` in French or unsuffixed basename 
`api_fr`. The new implementation reserves valid locale suffixes and requires 
explicit configuration for ambiguous cases:
   
   ```groovy
   grails {
       i18n {
           basenames = ['api_fr']
       }
   }
   ```
   
   This is deterministic, but requires more ceremony than the previous 
permissive scanning model.
   
   ### More build-time coupling
   
   Message availability now depends on the Grails Gradle plugin generating and 
packaging the descriptor. Nonstandard build systems must reproduce the 
descriptor format, and malformed descriptors fail startup. This trades runtime 
flexibility for build-time correctness.
   
   ### Structural reload is weaker
   
   Editing an existing bundle still reloads automatically. However:
   
   - a new locale requires descriptor regeneration before appearing in 
`<g:localeSelect>`;
   - a new basename requires restart;
   - removing a basename may stop resolution after cache invalidation while its 
metadata remains until restart.
   
   A newly added basename already effectively required reinitialization 
previously, so the available-locale descriptor dependency is the clearest 
development-time regression.
   
   ### More startup bootstrap integration
   
   The custom message source disappears, but the new system introduces an 
`EnvironmentPostProcessor`, descriptor parsing, effective plugin filtering, a 
Gradle descriptor task, and an AOT processor. This is less semantic duplication 
overall, but behavior now spans build time, bootstrap, and application context.
   
   ### Strict descriptor failures may expose unusual classpaths
   
   The new code rejects multiple application descriptors, duplicate plugin 
descriptors, and unsupported descriptor versions. This improves correctness but 
may expose ambiguous layered applications or unusual test fixtures that 
previously started.
   
   ### `ResourceBundleMessageSource` is less reload-oriented
   
   Boot uses `ResourceBundleMessageSource`, not 
`ReloadableResourceBundleMessageSource`. Grails compensates by setting a short 
cache duration in reload mode and clearing JDK bundle caches. This is tested, 
but less direct than calling `clearCache()` on a reloadable source.
   
   ## Overall comparison
   
   | Area | Previous | New implementation |
   |---|---|---|
   | Message implementation | Grails-specific | Standard Boot/Spring |
   | Discovery | Runtime wildcard scans | Build-generated descriptors |
   | Plugin collision handling | Runtime merge | Unique basenames |
   | Configuration | Grails properties/custom behavior | `spring.messages.*` |
   | Encoding | Potential app/plugin mismatch | Unified |
   | Startup cost | Classpath/resource scanning | Exact descriptor lookup |
   | Error detection | Often runtime or silent | Build/startup failure |
   | Existing plugin compatibility | Broad artifact compatibility | Rebuild 
required; migrated source can support Grails 7 and 8 |
   | Content hot reload | Supported | Supported |
   | Structural hot reload | Limited but more dynamic locale scan | 
Descriptor/restart dependent |
   | Nonstandard builds | More tolerant | Must generate metadata |
   | Maintenance | Large custom message-source stack | Smaller integration 
layer |
   
   ## Verdict
   
   Excluding AOT, the new implementation is still substantially better for 
framework maintainability, startup determinism, configuration consistency, and 
failure diagnostics.
   
   Its main disadvantages are the deliberate old-artifact compatibility break 
and the shift from permissive runtime discovery to stricter build-time 
metadata. The temporary cache-property bridge and cross-version-compatible 
source layout reduce upgrade friction without carrying the old message-source 
implementation forward. For a major-version modernization where backward plugin 
artifact compatibility is explicitly not required, that is a favorable trade.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to