Stop @EnableWebMvc from being automatically added to Grails Applications#13863
Conversation
I have converted this PR to Draft for now. |
|
This is now been here for almost a year and half with no progress. I'm going to close the PR for now - please reopen when you feel it's ready for review. |
809b7f6 to
b05894e
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #13863 +/- ##
================================================
+ Coverage 0 49.3636% +49.3636%
- Complexity 0 16789 +16789
================================================
Files 0 1990 +1990
Lines 0 93413 +93413
Branches 0 16351 +16351
================================================
+ Hits 0 46112 +46112
- Misses 0 40174 +40174
- Partials 0 7127 +7127
🚀 New features to boost your workflow:
|
Addresses the base (apache#13863) review gaps: - GrailsViewResolverAutoConfiguration now honours the legacy spring.gsp.removeDefaultViewResolverBean property (which exists in apache/8.0.x) as a deprecated fallback for grails.web.removeDefaultViewResolverBean, logging a deprecation warning when the old name is used — instead of silently ignoring it. - Adds upgrading80x.adoc section 27 documenting that Boot's WebMvcAutoConfiguration is now active (no auto-injected @EnableWebMvc), what Grails still owns, the defaultViewResolver removal + opt-out flag, and that no action is required for typical apps. The JSON-views path the removal is justified by is already covered by the views-functional-tests example app (JSON/HAL render assertions + CircularSpec for the circular-view-path scenario), which passes on this branch — verified by running :grails-test-examples-views-functional-tests:integrationTest. Note: these harden content from base PR apache#13863; if apache#13863 merges standalone, cherry-pick them there.
sbglasius
left a comment
There was a problem hiding this comment.
I am not sure, I understand the full impact of this PR; but the code looks sound and if tests passes, I'm good.
I would however, like to see it documented in the upgrade notes, so that users can figure out, why their application might fail, if that would ever happen?
|
@sbglasius thanks for the review — I've documented this so users can see why an app might be affected and how to control it:
|
|
Thanks @jdaugherty (and Fabled) — both are spot on, and I've updated this PR to address them. Quick context on the diff: the original 1. Undocumented
2. And you're right that the old override test only checked bean identity — I reworked it to assemble the real servlet chain via |
- Remove the empty configuration closure and comment on the spring-test compileOnly dependency - Condense the grailsWebRequest bean comment to the two essential points (RequestContextFilter back-off and the registration-bean-name gate)
|
Acknowledged on sequencing. Worth noting the approach here registers Grails' MVC beans ahead of |
|
I missed the dependency when the other PR was merged. I don't think we want these values registered and instead of docuemnting we need to disable them. |
|
@codeconsole I pushed the test showing the material differences. In summary: 4 failures — each one a Grails 8 behavior change due to removing the annotation:
I think |
PUT/PATCH form fields were already exposed through Grails' params (GrailsParameterMap); the filter's observable addition for controller code is DELETE, and it also makes all three visible via the standard request.getParameter(...) API. Disabling the filter reverts DELETE to unparsed while PUT/PATCH continue to work.
With @EnableWebMvc no longer auto-injected, Spring Boot's WebMvcAutoConfiguration is active and registers a catch-all (/**) static-resource handler over classpath:/public (etc.) plus welcome-page mappings that serve a static index.html at '/'. Both overlap with Grails' own /static/** handler and UrlMappings-based root handling, and change the 404 semantics of unmapped URLs. Default spring.web.resources.add-mappings to false (overridable) via a new EnvironmentPostProcessor so Boot's catch-all handler is not registered, and remove the welcomePageHandlerMapping / welcomePageNotAcceptableHandlerMapping beans via GrailsWelcomePageAutoConfiguration (opt-out grails.web.removeWelcomePageMapping) so Grails' UrlMappings own '/'.
Spring's FormContentFilter (registered by Boot's WebMvcAutoConfiguration) parses form-encoded PUT/PATCH/DELETE bodies at the servlet layer for every Grails servlet web application, so GrailsParameterMap no longer needs to parse the body itself — it reads the request parameter map, as it already does for POST. This removes a duplicate parser that in practice only ran when no filter was present. - GrailsParameterMap: drop the manual PUT/PATCH form-body parse and its now-unused imports/logger; retain the REQUEST_BODY_PARSED constant for binary compatibility. - GrailsMockHttpServletRequest: parse form-encoded PUT/PATCH/DELETE bodies into the parameter map so unit-test mocks reflect a post-filter request (preserves the raw-body -> params ergonomic and adds DELETE for parity with runtime). - GrailsParameterMapTests: exercise the real FormContentFilter (PUT/PATCH/DELETE) rather than the removed fallback; GrailsMockHttpServletRequestSpec covers the mock's form-body parsing. With no in-map fallback, spring.mvc.formcontent.filter.enabled=false now fully disables non-POST form-parameter parsing (single-switch semantics).
The previous commit made Spring's FormContentFilter the single non-POST body parser, but the filter is only registered by Boot's WebMvcAutoConfiguration — which backs off when an application declares @EnableWebMvc, leaving PUT/PATCH/ DELETE form bodies unparsed. GrailsFormContentFilterAutoConfiguration contributes an OrderedFormContentFilter so a filter is always present. Ordered after WebMvcAutoConfiguration and gated on @ConditionalOnMissingBean(FormContentFilter) so Boot's takes precedence in a default app; it only fills the gap when Boot's is absent (the @EnableWebMvc case). It honours spring.mvc.formcontent.filter.enabled=false so the explicit opt-out stays a real off-switch. Result: PUT/PATCH/DELETE form parsing is uniform in every Grails servlet app unless explicitly disabled. Also: - enable-mvc-check: the Application class imported @EnableWebMvc but never applied it, so the app silently ran as a normal Grails app. Apply the annotation, and update WebMvcDefaultsFunctionalSpec to expect DELETE parsed (Grails supplies the filter even under @EnableWebMvc) alongside PUT/PATCH. - upgrading80x.adoc: note that Grails supplies the filter for @EnableWebMvc apps and that disabling it turns off PUT/PATCH/DELETE form parsing entirely.
Add grails.i18n.localeResolver (session [default], cookie, acceptHeader, fixed) so applications can choose how the locale is resolved. The session and cookie resolvers are mutable and keep the ?lang= LocaleChangeInterceptor; acceptHeader (the locale follows the Accept-Language header) and fixed are read-only, so the interceptor is not registered for them. The fixed resolver uses grails.i18n.default.locale, falling back to the JVM default. The default remains SessionLocaleResolver, so existing ?lang= switching and the language selector are unchanged.
…nly resolvers Spring 7's WebMvcConfigurationSupport contributes its own localeResolver (an AcceptHeaderLocaleResolver). Under @EnableWebMvc that bean pre-exists, so I18nAutoConfiguration's @ConditionalOnMissingBean backed off and Grails' session resolver never registered, silently disabling ?lang= switching — a regression from Grails 7, where the i18n resolver always took precedence. GrailsLocaleResolverAutoConfiguration now removes the WebMvcConfigurationSupport- contributed localeResolver (identified by its factory type) before I18nAutoConfiguration is evaluated, so the Grails-configured resolver registers instead. An application-defined localeResolver is left untouched. ParamsAwareLocaleChangeInterceptor now handles a read-only LocaleResolver (AcceptHeaderLocaleResolver/FixedLocaleResolver) gracefully: it logs the unsupported setLocale at debug rather than an error on every ?lang= request. The interceptor stays registered so urlMappingsHandlerMapping still has a handler interceptor; it simply has no effect with a read-only resolver. The enable-mvc-check app opts into grails.i18n.localeResolver=acceptHeader to demonstrate header-based resolution cleanly.
Form-content parsing: consolidating on
|
Follow-on changes: neutralizing the Boot MVC defaults exposed by removing
|
…ing is disabled Now that Grails guarantees a servlet-level FormContentFilter for every servlet web application (Boot's OrderedFormContentFilter by default, the Grails fallback under @EnableWebMvc), it runs at order -9500 — ahead of the whole Spring Security chain (-100) — and wraps the request before the chain starts. So the plugin's own in-chain FormContentFilter (inserted at EXCEPTION_TRANSLATION_FILTER.order - 10) is redundant: the security filters already see the parsed parameters. It was only needed historically, when Grails 7 auto-injected @EnableWebMvc and no servlet-level form filter existed. - SpringSecurityCoreGrailsPlugin: drop the formContentFilter bean registration and its import (also removes a bean that Boot could otherwise auto-register on /*). - SpringSecurityUtils: drop the chain insertion. - SpringSecurityUtilsIntegrationSpec: update the chain assertions accordingly. The one config where nothing would then parse non-POST form bodies is an explicit spring.mvc.formcontent.filter.enabled=false. Rather than have the plugin silently re-add a filter there (ignoring the opt-out), GrailsFormContentFilterAutoConfiguration now logs a startup warning when the filter is disabled and none is present, so the consequence (PUT/PATCH/DELETE form bodies not parsed) is visible.
…rm parsing Responds to review feedback on the @EnableWebMvc removal: - Docs (upgrading80x.adoc): the upgrade notes were out of date. Boot's static- resource handler and welcome page are now removed by default (new section 31.3), not "active"; renumber the remaining Boot-MVC notes to 31.4; add 31.5 for locale resolution. Clarify that the form-content filter only affects application/x-www-form-urlencoded bodies (also in the auto-config javadoc). - Docs (changingLocales.adoc): spell out precedence — with session/cookie the Accept-Language header sets the initial locale and ?lang= overrides and persists it; with acceptHeader/fixed the resolver is read-only and ?lang= is ignored. - Config metadata: add grails.web.removeDefaultViewResolverBean, grails.web.removeWelcomePageMapping and grails.i18n.localeResolver (with value hints) to additional-spring-configuration-metadata.json. - Tests: since GrailsParameterMap no longer parses PUT/PATCH bodies itself, add a functional test in app1 (a default, non-@EnableWebMvc app) asserting form-encoded PUT/PATCH/DELETE/POST bodies are parsed into params via Boot's OrderedFormContentFilter. - Remove the enable-mvc-check README (it only existed to explain the test).
The disabled-filter warning message split its concatenation with a leading +, which trips Checkstyle's OperatorWrap rule (:grails-controllers:checkstyleMain).
✅ All tests passed ✅🏷️ Commit: 4e458f6 Learn more about TestLens at testlens.app. |
jdaugherty
left a comment
There was a problem hiding this comment.
My only remaining concern is the spring security impacts since it's not clear if the form filter is now applying before or after the spring security filters.
Spring Boot's ContentNegotiatingViewResolver — active for Grails servlet apps since @EnableWebMvc stopped being auto-added (#13863) — collects every ViewResolver bean while it initializes, force-creating the lazy jspViewResolver. When that happens before the SiteMesh BeanPostProcessor wrap is in effect, the raw resolver is captured and, sitting behind the highest-precedence viewResolver, renders every HTML request undecorated: layouts silently stop applying. Whether the race is hit depends on bean-creation order, so it varies with the plugin set from app to app. Sitemesh3ViewResolverDefinitionPostProcessor now rewrites the jspViewResolver bean definition itself into a GrailsSiteMeshViewResolver with the original definition embedded as the inner bean — mirroring the SiteMesh 2 module's GrailsLayoutViewResolverPostProcessor — so the decorating resolver is what gets instantiated no matter how early a consumer forces the bean into existence. The bean post-processor is kept as a safety net for instance-registered resolvers and to keep upstream's wrap-all post-processor from registering; upstream never re-wraps a SiteMeshViewResolver, so the mechanisms cannot double-decorate. The plugin's doWithSpring() is removed outright: the SiteMesh property defaults now come from a Sitemesh3EnvironmentPostProcessor registered in spring.factories (making the post-hoc PropertySourcesConfig reassignment unnecessary), and grailsRenderViewMutator is registered through beanRegistrar(), the modern replacement for the bean-builder DSL.
Fixes #13843
This produces unknown/unexpected behavior and should not be done. @EnableWebMvc is a Spring Framework configuration annotation not to be used with Spring Boot. It produces undesired behavior by automatically creating beans that are not wanted. WebMvcAutoConfiguration performs all the behavior of this configuration because it extends it and adds conditional logic.
Fixes
vs
With
@EnableWebMvcgone, Boot'sWebMvcAutoConfigurationis now active for Grails servlet web apps. This PR adapts Grails so the change is transparent:@EnableWebMvc(ApplicationClassInjector).defaultViewResolverthat breaks REST/JSON-views apps withCircular view patherrors. NewGrailsViewResolverAutoConfigurationremoves it centrally for every Grails servlet web app, controlled bygrails.web.removeDefaultViewResolverBean(defaulttrue). This replaces the GSP-onlyspring.gsp.removeDefaultViewResolverBean;spring.gsp.replaceViewResolverBeanis unchanged.GrailsWebRequestFilternow extendsRequestContextFilterand is exposed as aRequestContextFilterbean so Boot'sWebMvcAutoConfigurationbacks off its ownOrderedRequestContextFilter, which would otherwise rebind a plainServletRequestAttributesbetween Grails' filter and the Spring Security chain and cause aGrailsWebRequestClassCastException.Docs — upgrade-notes section "27. @EnableWebMvc No Longer Added to Grails Applications" in
upgrading80x.adoc.Tests —
ControllersAutoConfigurationSpecverifies the filter is exposed as aRequestContextFilterat the Grails request-filter order, and that Boot'srequestContextFilteractually backs off (with a contrast case proving it's present without the Grails auto-config).Backward Compatibility
For legacy apps dependent on previous configuration, restoration is possible by simply adding
@EnableWebMvcmanually toApplication.groovy