matrei commented on PR #16237:
URL: https://github.com/apache/grails-core/pull/16237#issuecomment-5696991670

   # Review round 2
   
   **Head:** `e8bb2eb0a5bf4227aee06cab334adb057e50a34f` 
(`codeconsole:feature/spring-media-type-negotiation-8.0.x`)
   **Base:** `8.0.x` — merge-base still `3067d0a855`, clean merge on top of it 
(82 commits, 140 files, +5894/−474).
   **Delta since round 1:** one commit, `e8bb2eb0a5` (29 files, +656/−107).
   
   **What I ran locally (all green):**
   
   - `./gradlew codeStyle`
   - Module tests with `--no-build-cache` and `cleanTest` (result XML confirmed 
fresh): `grails-converters` (114), `grails-rest-transforms` (84), 
`grails-controllers` (93), `grails-xml` (29), `grails-web-common` (97), 
`grails-testing-support-core` (10) — 0 failures.
   - Suites the same way: `grails-test-suite-web` (446), 
`grails-test-suite-uber` (576) — 0 failures.
   - `check -x test` (checkstyle / codenarc / pmd / spotbugs) on every changed 
module plus `grails-gsp` — no violations.
   - CI on this head is green (30805 tests; one known-flaky Geb login test, 
issue #16030).
   - Three scratch specs, since deleted: serializer precedence with an 
initialised `KeyValueMappingContext`; a `ControllerUnitTest` that dumps the 
loaded plugin list, bean count and both render paths; and 
`CompiledTagInvocationSpec` with the new `setup()` removed.
   
   **Verdict: the serialization findings from round 1 are resolved. One new 
should-fix remains, in the test harness.** Every blocking item (GString, 
converter selection, harness parity, named configurations in unit tests, domain 
serializer scope) and every should-fix (default flag, plugin loading, 
`MessageSource` ambiguity, `render json:`, docs) has a real code change with a 
test that exercises the production path. The fix for round-1 item 7, however, 
moved the plugin loading rather than removed it: every web unit test now boots 
nine plugins instead of two, and that is observable in user tests, which the 
PR's own GSP spec change demonstrates. Details below.
   
   ---
   
   ## Should fix before merge
   
   ### 1. Web unit tests now boot the full controllers/urlMappings/i18n plugin 
graph, and it changes existing tests
   
   `GrailsApplicationBuilder.registerPluginDiscoveryBean` 
(`grails-testing-support-core/src/main/groovy/org/grails/testing/GrailsApplicationBuilder.groovy:208`)
 adds `converters`, `restResponder` and `xml` for any `GrailsWebUnitTest`. 
`IncludingPluginFilter` expands `dependsOn`, and `converters` depends on 
`controllers` and `domainClass`, `controllers` on `i18n` and `urlMappings`, 
`xml` on `dataBinding`. Measured in a plain `ControllerUnitTest` on this branch:
   
   ```
   plugins: controllers, converters, core, dataBinding, domainClass, i18n, 
restResponder, urlMappings, xml
   bean definitions: 156
   ```
   
   On `8.0.x` the same test loads `core` and `eventBus` and the interceptor 
defines a curated bean set. The new graph is visible to user tests. 
`UrlMappingsGrailsPlugin.groovy:119` registers `DefaultUrlMappings` when the 
application has none, so `g.link`/`createLink` without a `controller` in the 
request now throws instead of producing a URL. The PR hit this itself and 
patched the one in-repo spec: `CompiledTagInvocationSpec` gained 
`webRequest.controllerName = 'book'` 
(`grails-gsp/plugin/src/test/groovy/org/grails/web/taglib/CompiledTagInvocationSpec.groovy:34`).
 Removing that line reproduces it, four tests fail with:
   
   ```
   grails.web.mapping.exceptions.UrlMappingException: Unable to create URL for 
mapping [/(*)/(*)?/(*)?(.(*))?] and parameters [{}]. Parameter [controller] is 
required, but was not specified!
       at 
org.grails.web.mapping.DefaultLinkGenerator.link(DefaultLinkGenerator.groovy:272)
       at org.grails.plugins.web.taglib.ApplicationTagLib.doCreate
   ```
   
   Any application `TagLibUnitTest` or `ControllerUnitTest` that renders a link 
without a controller in the request will fail the same way after upgrading to 
this `8.0.x`. Nothing in `upgrading80x.adoc` mentions it; section 65 
(`upgrading80x.adoc:3870`) only says the XML plugin is loaded by web traits.
   
   Suggested fix: the Spring path needs only two things at refresh time, a 
`grailsJsonMapperCustomizer` definition present before 
`JacksonAutoConfiguration` builds the mapper, and the `restResponder` beans 
(`SpringMessageConverters`, `rendererRegistry`, 
`validationProblemDetailFactory`). `restResponder` has no `dependsOn`, so 
including it alone drags nothing in. Register the customizer definition 
directly in the builder for web tests, and load `XmlGrailsPlugin` the way 
`ConvertersGrailsPlugin` and `CodecsGrailsPlugin` already are in 
`WebSetupSpecInterceptor`, via `defineBeans(plugin)` when the class is present. 
That keeps the context the harness had on `8.0.x`. If the full graph is kept 
deliberately, it has to be documented as a behaviour change for existing unit 
tests, and the duplicate definitions in point 2 cleaned up.
   
   ### 2. Beans defined twice in the web harness
   
   With the plugins loaded, `WebSetupSpecInterceptor` 
(`grails-testing-support-web/src/main/groovy/org/grails/testing/spock/WebSetupSpecInterceptor.groovy:81-119`)
 still defines `rendererRegistry`, `grailsUrlMappingsHolder`, 
`grailsLinkGenerator`, `localeResolver` and re-applies `ConvertersGrailsPlugin` 
after refresh. The later definition wins in every case, so tests pass, but the 
sequence is confusing: Boot's mapper is customised at refresh by the plugin's 
`grailsJsonMapperCustomizer`, then the interceptor replaces that bean with a 
second instance, and `forGrails` runs on the second one against a mapper 
customised by the first. It works only because `customize` and `forGrails` are 
independent. Whichever way point 1 goes, there should be one definition of each.
   
   ## Minor
   
   - `respond 'ok'` on the legacy path, which is now the default, still throws 
`GroovyCastException: Cannot cast object 'ok' ... to class 
'grails.converters.JSON'` (`DefaultJsonRenderer.groovy:169` and `:172`, `object 
as JSON`; `StringGroovyMethods.asType` shadows the converters extension). This 
is pre-existing on `8.0.x`, but this commit switched `DefaultXmlRenderer` to 
`new XML(object)` for exactly that reason (`DefaultXmlRenderer.groovy:142`), so 
the JSON renderer should get `new JSON(object)` too. Section 65 says "strings 
remain JSON strings" for the Spring path only; with the default off, a plain 
string still 500s.
   - `DefaultJsonRenderer.groovy:200-204` builds a new 
`JacksonJsonHttpMessageConverter` on every response when it substitutes the 
Grails mapper. `forGrails` is cached per mapper; the converter could be cached 
per source converter the same way.
   - The `NamedJsonConfigurationRegistry` supplier 
(`ConvertersGrailsPlugin.groovy:81-92`) does a `getIfUnique` type scan plus a 
bean lookup on every `writer()` call. Memoise the resolved mapper after the 
first successful lookup.
   - `isWebTest()` adds the web plugins even when a spec overrides 
`getIncludePlugins()` to restrict the set. Reasonable, but worth a sentence in 
the testing docs.
   
   ## Round-1 findings, status
   
   | # | Finding | Status |
   |---|---------|--------|
   | 1 | GString serialised as a bean | Fixed. 
`GrailsJsonMapperCustomizer.customize` registers `ToStringSerializer` for 
`GString` on Boot's mapper (`GrailsJsonMapperCustomizer.java:93`); covered 
nested and root in `GrailsJsonMapperCustomizerSpec`, through real converters in 
`DefaultJsonRendererSpec`, and through the harness in 
`ControllerJsonSerializationSpec`. |
   | 2 | `respond 'text'` / `byte[]` picked the String/ByteArray converter | 
Fixed. Both renderers require a converter to advertise a JSON 
(`DefaultJsonRenderer.groovy:190-193`) or XML 
(`DefaultXmlRenderer.groovy:160-163`) media type for the class; tests use the 
real `ByteArrayHttpMessageConverter`, `StringHttpMessageConverter` and Jackson 
converter and assert `"ok"`, `"Saved Grails"`, base64 and the XML `<string>` 
element. Doc corrected (`upgrading80x.adoc:3826-3832`). |
   | 3 | Unit tests never exercised the Spring path | Fixed. 
`JacksonAutoConfiguration` is registered for web tests 
(`GrailsApplicationBuilder.groovy:181`), `WebSetupSpecInterceptor` populates 
`SpringMessageConverters` from the context mapper (`:83-90`), and 
`ControllerJsonSerializationSpec` asserts the Jackson shape, the 
`application/problem+json` 422 body, application `JsonMapperBuilderCustomizer`s 
and named configurations through `respond` and `render`. |
   | 4 | Named configurations unusable in unit tests | Fixed, same mechanism; 
`render json: x, jsonConfiguration:` and `respond x, jsonConfiguration:` 
covered in the harness. |
   | 5 | Domain serializer on the shared Boot mapper | Fixed. `customize` no 
longer adds the domain module; `forGrails` derives a separate mapper 
(`GrailsJsonMapperCustomizer.java:105-116`) used only by the substituted 
converter and the named-configuration registry. See "Verified" for why the 
module reordering is sound. `ControllerJsonSerializationSpec` proves 
`@JsonIgnore` is honoured on Boot's mapper and ignored on the Grails path; the 
guide now states the domain-path limitations (`upgrading80x.adoc:3834-3841`). |
   | 6 | Default flipped `respond` JSON on a minor line | Fixed. 
`grails.web.rendering.json.spring` defaults to `false` 
(`DefaultRendererRegistry.groovy:94`); `DefaultRendererRegistrySpec` covers 
unset/false/true, `RespondMethodSpec` asserts the legacy shape by default. Doc 
rewritten as opt-in. |
   | 7 | `DEFAULT_INCLUDED_PLUGINS` gained `xml` | Reverted for non-web tests 
(`NonWebPluginIsolationSpec`, `DefineBeansPluginHooksSpec` assert it), but 
moved to web tests with a larger graph. See point 1 above. |
   | 8 | `getIfAvailable()` with two `MessageSource` beans | Fixed. 
`it.bean('messageSource', MessageSource)` 
(`RestResponderGrailsPlugin.groovy:75`), test with a second `MessageSource` 
bean. Mapper resolution uses `getIfUnique()` then the `jacksonJsonMapper` name 
(`ConvertersGrailsPlugin.groovy:82-90`), test with two mapper beans. |
   | 9 | `render json:` required a configuration | Fixed. Null name means the 
default writer (`ResponseRenderer.groovy:287-292`, 
`NamedJsonConfigurationRegistry.java:74-85`); covered in 
`NamedJsonRenderArgumentSpec` and the harness. |
   | 10 | Doc claims | Fixed. HATEOAS section replaced with an accurate "HAL 
Rendering" note (`upgrading80x.adoc:3926`, `hal.adoc:128-131`); the "Jackson 
annotations apply consistently" claim is replaced by the scoped description; PR 
description no longer lists the adapter module. |
   | minor | Encoding, unknown `?format=`, `write(int)`, global `Errors` 
serializer | All addressed: UTF-8 intermediate bytes with the servlet writer 
applying the configured encoding (`DefaultJsonRenderer.groovy:205-209`, tested 
with ISO-8859-1); unknown-format fallback documented 
(`upgrading80x.adoc:1538`); `write(int)` no longer allocates; `Errors` 
serializer documented (`upgrading80x.adoc:3843-3846`). |
   
   ## Verified as correct
   
   - **`forGrails` module reordering is sound on Jackson 3.1.6.** 
`MapperBuilder.saveStateApplyModules` saves the builder state *before* modules 
run, and `rebuild()` restores that pristine state and re-adds the modules, so 
`withModules` → `removeAllModules` → domain module → `addModules(original)` 
genuinely applies the domain serializers first and the application's last. 
`SimpleSerializers` are prepended on registration, so the last module wins. My 
scratch spec with an initialised mapping context confirmed: an application 
`ToStringSerializer` for a domain class beats the domain serializer, another 
domain class still gets `{"id":5,"name":"Ada"}`, a snake-case naming strategy 
from the source mapper survives for non-domain beans, and `forGrails` returns 
the same instance per mapper.
   - The PR's own precedence spec (`GrailsJsonMapperCustomizerSpec`, 
"application domain serializers keep precedence") is discriminating even 
without a mapping context: `DefaultGrailsApplication.getMappingContext()` 
returns a throwing proxy, so the domain path yields a 
`DeferredDomainSerializer` that would raise `IllegalStateException` if it won.
   - `ControllerJsonSerializationSpec` runs the real Boot converter through 
`respond`; the problem body asserts status 422, content type 
`application/problem+json;charset=UTF-8`, resolved message, no `rejectedValue`, 
no `properties` wrapper.
   - `SpringErrorsJsonSerializer` and the `GString` serializer are the only 
global additions to Boot's mapper; both are now documented.
   - `NamedJsonConfiguration.writer` still caches one `ObjectWriter` per 
configuration; the derived writer now comes from the Grails mapper, so named 
configurations keep domain compatibility.
   - `NonWebPluginIsolationSpec` and `DefineBeansPluginHooksSpec` prove 
`ServiceUnitTest`/`GrailsUnitTest` contexts contain no `rendererRegistry`, 
`xmlRenderer` or `namedJsonConfigurationRegistry` even with `grails-xml` on the 
classpath.
   - `WriterOutputStreamSpec` now streams a 2000× multi-byte body with emoji 
through one-byte-per-write and stays intact.
   - `DefaultXmlRenderer` `new XML(object)` is equivalent to `object as XML` 
for non-String values and fixes the String case; `SpringXmlRendererSpec` covers 
the `<string>` element with a real `StringHttpMessageConverter`.
   - Everything in the round-1 "Verified as correct" list is unchanged by this 
commit.
   


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