Run GSP in a Spring Boot application without the Grails plugin lifecycle - #16184
Run GSP in a Spring Boot application without the Grails plugin lifecycle#16184codeconsole wants to merge 28 commits into
Conversation
…gistered
CoreGrailsPlugin registers a Groovy aware auto-proxy creator under Spring's
internalAutoProxyCreator bean name, and Spring rejects a creator class it does
not know as soon as anything asks it to register or escalate one - Boot's
AopAutoConfiguration, @EnableAspectJAutoProxy, the aop namespace:
Class name [org.grails.spring.aop.autoproxy.
GroovyAwareInfrastructureAdvisorAutoProxyCreator]
is not a known auto-proxy creator class
The reflection patch that makes them known has sat in a GrailsAutoConfiguration
static initializer since 2015, which held for as long as the plugin lifecycle
ran through that class. Since the lifecycle was retimed it runs from
GrailsPluginLifecycleInitializer for every Spring Boot application with
grails-core on the class path, so an application that is not a Grails
application registers the creator without ever loading GrailsAutoConfiguration,
and fails to start.
Move the patch to GroovyAwareAutoProxyCreators and apply it where the creator is
registered. It is idempotent, so both call sites can apply it, and it warns
rather than leaving the failure to surface later as Spring's error.
Four things stood between a plain Spring Boot application and a decorated GSP, leaving the gsp-spring-boot example unable to start and its runtime test disabled: - the tag library beans were autowired by name, which injected the view resolver eagerly - viewResolver is an alias of gspViewResolver - and defeated the @lazy that RenderSitemeshTagLib declares to break exactly that cycle - <g:applyLayout> resolves its layout through a bean qualified jspViewResolver, the name Grails gives an application's own view resolver, which a standalone application has no bean for; gspViewResolver now answers to that name too, as GrailsSiteMeshViewResolverBeanPostProcessor already documented that it would - that post processor compared the target against the bean name only, so an aliased resolver was never wrapped and no page was decorated - Sitemesh3LayoutTagLib, which carries the grailsLayout namespace the GSP compiler emits for the head, title and body of a decorated page, was not registered, so those capture tags reached the browser as literal markup The SiteMesh 3 layout finder also ignored sitemesh.decorator.default, the key a Spring Boot application configures its default layout with; it is now consulted after the two Grails keys, which keep precedence. Re-enable the example's runtime test, covering decoration as well as rendering.
The example excluded *.gsp from its resources on the assumption that the precompiled GSP classes would serve the views. They cannot: compileGroovyPages keys its view registry by the Grails view convention (/WEB-INF/grails-app/views/...), which a standalone Spring Boot application never searches - it looks under its configured template roots - and only the Grails GSP plugin loads that registry into the page locator at all. The result was a bootJar that started and then answered 404 for every view, having fallen through to a servlet dispatch for a path nothing serves. Ship the sources so the packaged application renders, as bootRun already did from the project directory.
GroovyPagePlugin registered its compiled pages with output.dir('gsp-classes'),
which resolves against the project directory rather than the build directory and
so named a directory that is never written. The pages and the view registry
beside them reached the archives only through the copies the jar and war tasks
make of the compile task's real destination, which left them off the class path
of anything else: an application started with bootRun or exercised by a test
could not load a single page it would ship with.
Add the compile tasks' destinations to the runtime and test runtime class paths
instead. They cannot be registered as source set output, which is what the
`classes` task builds, because compileGroovyPages runs after `classes` and the
two would form a cycle.
Only the Grails GSP plugin read the view registry that the compileGroovyPages build task writes, so a standalone Spring Boot application could not render a view it had compiled: it searched its template roots, found no template where the templates had been left out of the artifact, and fell through to a servlet dispatch for a path nothing serves. Read the registry into the page locator, and search the path a view is registered under - the one below the template root, naming no resource root - alongside the roots themselves. The registry is left unread when a template root on the file system is in play. The locator prefers a compiled view over a template it can find, so reading it during development would serve every page as it stood when the application was built, however often its template was edited.
Now that a standalone application can render the views compiled into it, the example ships those rather than the templates they were built from, which is what it always intended: the *.gsp exclusion is restored, undoing the stopgap in "Ship the GSP sources in the packaged gsp-spring-boot example". serverpath registers each view under its path below the template root, where the default registers it below the /WEB-INF/grails-app/views/ a Grails application looks under, and nothing here ever searches. The new test renders with the template root pointed at the class path, which holds no templates, so only the compiled views can answer it - the same arrangement the packaged application runs under, where every earlier failure in this series was invisible to a test run from the project directory.
GroovyPageCompiler merged the registry it writes into the one an earlier run had left behind, on the stated grounds that only changed pages are added to the mapping. They are not: compileGSP records a page whether or not it had to recompile it, so the mapping already names every page of the run. The merge only preserved entries no run would write again - a page since renamed or removed, or one registered under a different view prefix - each naming a class that is no longer there. They survived a clean, since a merged registry is what the build cache had stored, and were left for the locator to trip over at runtime, where a mapping that resolves to no class costs a failed Class.forName and a warning per render before it falls back.
The template engine bean took a TagLibraryResolver as a required dependency, and the only resolver is contributed by a configuration conditional on grails-web-jsp being present. An application that renders GSP without JSP support therefore did not start at all, failing on a missing bean of a type it had no use for. Take the resolver as an ObjectProvider. A page that uses a JSP tag library still needs the resolver, and could not have used one without JSP support anyway.
The executable jar packages no JSP - src/main/webapp goes into a war, not a jar - and Jasper compiles a JSP from the servlet context, so the /jsp link the layout rendered led to a 500 there while working in a war and under bootRun. Offer the link, and switch the form over, only where the JSP is in the servlet context. A request typed in by hand now leaves the form on GSP rather than failing to render. What settles it is the page being there rather than a document root existing, which an executable jar has - a temporary and empty one - and rather than the JSP libraries being on the class path, which they are in every artifact: the GSP form uses Spring's form tag library, a JSP tag library, and renders nothing without them. That is also why the jar keeps those libraries. Leaving them out would take rewriting the GSP form to drop <form:form>, giving up what it demonstrates.
Two auto-configurations define a bean named codecLookup: the codecs module's, unconditionally and as the primary one, and GSP's stand-in for it, guarded by @ConditionalOnMissingBean. With no order declared between them GSP's was processed first, so its guard found nothing to back off from and the unconditional definition overrode it - leaving an application to enable bean-definition overriding before it would start, for a bean it never wanted twice. Declaring GSP's after the codecs module lets the guard do its work: the module's lookup registers, GSP's backs off, and the stand-in is left to the application that has no codecs module on its class path. Named rather than referenced, since that is the application GSP has to keep working for. The gsp-spring-boot example no longer opts into bean-definition overriding, and starts with no override logged at all, which is what its tests now hold to.
Every tag library is autowired with the lookup that finds it, by the
TagLibraryInvoker trait they all carry. The standalone auto-configuration in
turn built the lookup out of its tag libraries, holding them as inner beans of
its tagLibInstances property - so the lookup depended on beans that depended on
the lookup, and an application had to allow circular references before it would
start:
gspTagLibraryLookup <-> (inner bean) RenderTagLib
Register the tag libraries as beans of the context, leaving the lookup to find
them through the @taglib annotation once they all exist. StandaloneTagLibraryLookup
already looked for them on a context refreshed event; it now does so as a
SmartInitializingSingleton as well, which is before the web server accepts a
request rather than after, so the first page to render cannot outrun the tag
libraries it uses.
An application can still register a tag library under one of these names itself,
and the registrar leaves that one alone.
The gsp-spring-boot example no longer allows circular references. With the
preceding commit it now asks for neither that nor bean-definition overriding -
it renders GSP with nothing but its own configuration - and its tests hold it
there, since either would stop the application from starting.
…is one CoreAutoConfiguration is contributed to every Spring Boot application with grails-core on its class path, and two of its beans are the GrailsApplication read through another type: the class loader it was built with, and its config as ConfigProperties. Both took the application as a bean, so a context without one could not be created at all - it failed asking for a bean of a type it had no use for. Condition the two on a GrailsApplication being there. An application that has one is unchanged; one that has none keeps the beans that never needed it, the placeholder configurer among them, and starts.
GrailsPluginLifecycleInitializer is registered for every Spring Boot application that has grails-core on its class path, so the lifecycle ran for applications that are not Grails applications: one depending on a Grails library - GSP for its views, say - was given a GrailsApplication, a plugin manager and the beans of every plugin found, over the top of what the libraries it did ask for auto-configure for themselves. Stand the phase down unless one of the context's sources is a GrailsApplicationClass. A Grails application is unaffected. A Spring Boot application using a Grails library gets that library's auto-configuration and nothing else - which is what it asked for, and what such an application got before the lifecycle was retimed onto this initializer. The GSP auto-configuration contributes the GrailsApplicationAware post processor that the core plugin contributes to a Grails application, so the beans that read the application through it - the page locator, the tag library lookup, the JSP tag library resolver - are handed it either way.
The configuration that applies the SiteMesh defaults to a context built without SpringApplication - a test context, where the environment post processor that serves every other application does not run - was a BeanDefinitionRegistryPostProcessor with both of its methods empty. That is a way of being instantiated early enough for EnvironmentAware to matter, and it cost every application two warnings a boot about a @configuration class created too early to enhance. It declares no beans, so there is nothing to enhance: say so with proxyBeanMethods = false, and implement the plainer BeanFactoryPostProcessor, which is instantiated just as early and has one method to leave empty rather than two.
The plugin lifecycle stood down unless a source of the context was a Grails application class, which left out an application that GrailsApp launched from sources of another kind - a plain @configuration class, as DevelopmentModeWatchSpec starts one. Being launched by GrailsApp is as much a statement that this is a Grails application as the application class is, and GrailsApp already records it by stashing the sources it was given. Take either as the answer. What stays out is what was meant to: an application that Spring Boot launched, of sources that say nothing about Grails.
Two of its three properties said nothing about GSP. The trace level for web logging is a debugging aid that made every request print a page of Spring internals, and the tag library descriptor scan pattern restated a narrower form of what the GSP JSP integration already defaults to - the Spring form tag library the example's form uses is scanned for either way. What is left is the default layout, which is the one thing an application rendering GSP through SiteMesh has to say.
…t-standalone-8.0.x # Conflicts: # grails-core/src/main/groovy/grails/boot/config/GrailsAutoConfiguration.groovy # grails-doc/src/en/guide/upgrading/upgrading80x.adoc
Jasper compiles a JSP at run time with the Eclipse compiler, 3.3MB of the example's 41.5MB jar. An executable jar packages no JSP to compile - a JSP is served from the servlet context, which a war carries and a jar does not - so none of it is reachable there. Excluding ecj from Jasper itself would take the compiler from the war and from bootRun as well, where a JSP does render: both answer /jsp with a 500 and "No Java compiler available" without it. It is left out of the jar rather than out of the dependency, which is the artifact that cannot use it. Jasper stays in every artifact regardless: the GSP form uses Spring's form tag library, which is a JSP tag library and needs the JSP API that Jasper carries.
GSP arrives with the web tier behind it, and this application uses the part of it that renders a view. It routes and binds with Spring MVC, it has no domain classes, and it answers in HTML, so the rest is packaged and never called: Grails URL mappings and the constraint validation behind them - which carries commons-validator and commons-collections 3.2.2 - Grails data binding, Grails MVC, and Jackson. Each is excluded from the dependency that brings it and named with what it is for, rather than filtered out of the class path as a set, so what the application does not use is a statement about the application. The jar goes from 41.5MB to 34.2MB. The GSP page, both layouts, the form and its validation, and the JSP rendering in the war and under bootRun are unchanged.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16184 +/- ##
==================================================
+ Coverage 53.7560% 53.8005% +0.0445%
- Complexity 19829 19865 +36
==================================================
Files 2086 2088 +2
Lines 99667 99737 +70
Branches 17603 17616 +13
==================================================
+ Hits 53577 53659 +82
+ Misses 38428 38415 -13
- Partials 7662 7663 +1
🚀 New features to boost your workflow:
|
A boot archive packages every directory of its class path into its classes directory, and the compiled pages are copied there by the archive already, so every page arrived twice and the copy failed - which is what building any Grails example with views did. They stay on the test runtime class path, which no archive reads, so a test still loads the pages the application ships, the view registry among them. An application run from the build renders its templates as they are edited, which a page compiled ahead of the edit would have stood in the way of.
A Spring Boot application has no plugins to scan for artefacts, so a tag library reaches a page by being a bean of the context. The lookup found the beans marked @taglib, which is how a tag library written for GSP is marked, and passed over the ones marked @ArteFact("TagLib"), which is how a Grails plugin marks the tag libraries it carries - the asset pipeline's <asset:...> among them. Both are now detected, so a tag library out of a plugin can be declared as a bean and used in a page exactly as in a Grails application. Artefacts of other kinds are left alone.
The example now uses a tag library written for Grails - the asset pipeline's <asset:stylesheet> - from a Spring Boot application, which is what a plugin's tag libraries are for an application that installs no plugins: beans of the context, declared in AssetPipelineConfiguration alongside the filter that serves what the pipeline compiled. Bootstrap is an input to the build rather than a library the application ships. It is declared on the assets configuration, the manifest names the one file the page needs, and 246KB of compiled CSS is packaged instead of the 1.8MB webjar. The URL mappings exclusion comes back out: the asset pipeline contributes a link generator, and a link generator reads a mappings holder.
The link generator declared the URL mappings holder as a required dependency, though it reads it in one place: the link to a controller and action. A link to a resource or to a path never touches it, so an application routing with Spring MVC could not have a link generator at all - it had to declare an empty mappings holder for a bean it would never ask a mapping of. The holder is optional now, and a link that does need mappings says so instead of failing on a null. The gsp-spring-boot example drops the empty holder it declared for the asset pipeline's link generator.
The form is Bootstrap's - labels, controls, help text, and the invalid state Spring's form tag library marks a rejected field with - inside a card, under a navbar that carries the light / dark / auto theme menu the Grails welcome page has. The menu is Bootstrap's own colour modes, applied before the page paints by a script in the head, and remembered per browser. Bootstrap's script and its icon font join the stylesheet on the assets configuration. Only what the two manifests compile ships, plus the icon font's own files - the ones its stylesheet asks the browser for by name - and no source maps. Two things the styling turned up: The session id was carried in the URL for a visitor arriving without a cookie, which is how the container rewrites a form action, and Spring MVC answers a path carrying one with a 404 - so submitting the form on a first visit failed. Sessions are tracked by cookie now, where the id belongs. The results view was told nothing of what rendered it, because only the form handler said so, and its heading came out unfinished. Every view is told, by the same interceptor that offers the JSP rendering.
…x/gsp-spring-boot-standalone-8.0.x # Conflicts: # grails-doc/src/en/guide/upgrading/upgrading80x.adoc
| * only the tag library and the filter that serves the compiled assets are declared here. | ||
| */ | ||
| @Configuration(proxyBeanMethods = false) | ||
| public class AssetPipelineConfiguration { |
There was a problem hiding this comment.
This configuration shouldn't be in this PR. We should be contributing this to the asset pipeline as a separate library that's included here.
There was a problem hiding this comment.
@jdaugherty It will be removed after the asset pipeline is released
|
The configuration mentioned is part of the Grails application lifecycle management within |
The beans this class declares are being contributed to the asset pipeline as an auto-configuration, and the class goes when a release carries it.
…t-standalone-8.0.x
✅ All tests passed ✅🏷️ Commit: 4e2d14c Learn more about TestLens at testlens.app/docs. |
A Spring Boot application that renders its views with GSP could not start, and behind the first failure were four more, each hidden by the one before it. The
gsp-spring-bootexample now starts, renders, decorates and packages, and its runtime test is enabled again.Using GSP from a Spring Boot application
Views render from a plain
SpringApplication, with no Grails application class and no Grails plugins:The whole of the example's
application.propertiesis now the one property that says something about GSP:sitemesh.decorator.default=mainspring.main.allow-circular-referencesandspring.main.allow-bean-definition-overridingare gone. Both were opted into because of how the GSP beans were wired, not because the application wanted either.Views compiled by the
compileGroovyPagesbuild task are rendered from their compiled classes, so an application can ship without its templates:jar { processResources.exclude('**/*.gsp') } compileGroovyPages { source = project.file("${project.projectDir}/src/main/resources/templates") serverpath = '/' // the path a standalone application looks a view up by }Templates on disk still win where they are there to edit, so
bootRunre-renders a template as it changes.Using a tag library written for Grails
A Grails plugin's tag libraries are marked
@Artefact("TagLib")and found by scanning the plugin's artefacts. An application with no plugins declares one as a bean instead, and the tags are then usable in a page exactly as the ones GSP contributes itself. The example styles itself with Bootstrap through the asset pipeline's<asset:...>:Bootstrap is an input to the build rather than a library the application ships - it goes on the
assetsconfiguration, and the artifact carries the 246KB compiled from it instead of the 1.8MB webjar:What changed
The Grails plugin lifecycle runs for a Grails application only — one that
GrailsApplaunched, or one with a Grails application class among the sources.GrailsPluginLifecycleInitializeris registered for every Spring Boot application with grails-core on its class path, so an application using a Grails library was given aGrailsApplication, a plugin manager and the beans of every plugin found, over the top of what the libraries it did ask for auto-configure for themselves. It now gets that library's auto-configuration and nothing else. Documented in the 8.0 upgrade notes.Bean wiring. Tag libraries are beans of the context, found by the lookup once they exist, rather than held inside it — they are autowired with the lookup, so a lookup that held them was a cycle. The GSP codec lookup is configured after the codecs module, so its
@ConditionalOnMissingBeanguard can do its work instead of being overridden. The core beans that read theGrailsApplicationare contributed only where there is one, so a context without one starts rather than failing on a bean of a type it never had.Standalone GSP. The view registry
compileGroovyPageswrites is read into the page locator;<g:applyLayout>resolves its layout; thegrailsLayoutnamespace is registered, so the capture tags the GSP compiler emits no longer reach the browser as markup; the JSP tag library resolver is optional, so GSP renders without JSP support on the class path; and the tag libraries a Grails plugin carries are found alongside the ones marked@TagLib.Auto-proxy creators. The reflection patch that makes Spring recognise the Groovy aware auto-proxy creator had sat in a
GrailsAutoConfigurationstatic initializer since 2015, whileCoreGrailsPluginis what registers the creator. It moved toGroovyAwareAutoProxyCreators, applied where the creator is registered, so it holds for an applicationGrailsApplaunched from sources that do not load that class.Links are generated in an application that maps no URLs.
DefaultLinkGeneratordeclared the URL mappings holder as a required dependency, though it reads it in one place - the link to a controller and action. A link to a resource or to a path never touches it, so an application routing with Spring MVC could not have a link generator at all without declaring an empty mappings holder for a bean it would never ask a mapping of. The holder is optional now, and a link that does need mappings says so rather than failing on a null.The view registry is written whole rather than merged into what an earlier build left behind, which kept naming views since renamed or removed, against classes no longer there.
The compiled pages are on the test class path.
GroovyPagePluginregistered them with a path resolved against the project directory rather than the build directory, so an application tested from the build could load none of them. They are kept off the main runtime class path, which a boot archive packages into its classes directory - where the archive copies them already.The JSP compiler is left out of the executable jar — 3.3MB of Eclipse compiler that a jar cannot use, since it packages no JSP to compile. The war and
bootRun, which do serve JSPs, keep it.Limitations
serverpathset to match the template root. The default registers each view below/WEB-INF/grails-app/views/, which is where a Grails application looks and a standalone application never does.<asset:stylesheet>builds its URL throughassetPath, which lives in thegnamespace. An application declaring the tag libraries as beans has to declare both, as a plugin ships both.grails-coreon the class path for theGrailsApplicationthat the page locator, tag library lookup and JSP tag library resolver read, and with itgrails-datastore-core, javassist, caffeine andjakarta.persistence-api— around 2.6MB of persistence machinery thatGrailsApplication.getMappingContext()makes structural rather than incidental.