Skip to content

Run GSP in a Spring Boot application without the Grails plugin lifecycle - #16184

Open
codeconsole wants to merge 28 commits into
apache:8.0.xfrom
codeconsole:fix/gsp-spring-boot-standalone-8.0.x
Open

Run GSP in a Spring Boot application without the Grails plugin lifecycle#16184
codeconsole wants to merge 28 commits into
apache:8.0.xfrom
codeconsole:fix/gsp-spring-boot-standalone-8.0.x

Conversation

@codeconsole

@codeconsole codeconsole commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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-boot example 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:

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

The whole of the example's application.properties is now the one property that says something about GSP:

sitemesh.decorator.default=main

spring.main.allow-circular-references and spring.main.allow-bean-definition-overriding are gone. Both were opted into because of how the GSP beans were wired, not because the application wanted either.

Views compiled by the compileGroovyPages build 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 bootRun re-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:...>:

@Bean
public AssetsTagLib assetsTagLib(AssetProcessorService assetProcessorService, GrailsApplication grailsApplication) {
    AssetsTagLib assetsTagLib = new AssetsTagLib();
    assetsTagLib.setAssetProcessorService(assetProcessorService);
    assetsTagLib.setGrailsApplication(grailsApplication);
    return assetsTagLib;
}
<asset:stylesheet src="application.css"/>

Bootstrap is an input to the build rather than a library the application ships - it goes on the assets configuration, and the artifact carries the 246KB compiled from it instead of the 1.8MB webjar:

assets platform(project(':grails-bom'))
assets 'org.webjars.npm:bootstrap'

What changed

The Grails plugin lifecycle runs for a Grails application only — one that GrailsApp launched, or one with a Grails application class among the sources. GrailsPluginLifecycleInitializer is registered for every Spring Boot application with grails-core on its class path, so an application using a Grails library 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. 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 @ConditionalOnMissingBean guard can do its work instead of being overridden. The core beans that read the GrailsApplication are 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 compileGroovyPages writes is read into the page locator; <g:applyLayout> resolves its layout; the grailsLayout namespace 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 GrailsAutoConfiguration static initializer since 2015, while CoreGrailsPlugin is what registers the creator. It moved to GroovyAwareAutoProxyCreators, applied where the creator is registered, so it holds for an application GrailsApp launched from sources that do not load that class.

Links are generated in an application that maps no URLs. DefaultLinkGenerator 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 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. GroovyPagePlugin registered 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

  • JSP cannot be served from an executable jar. Jasper compiles a JSP from the servlet context, and a jar packages none, so the example offers its JSP rendering only where it can serve one. Jasper itself stays in every artifact: the GSP form uses Spring's form tag library, which is a JSP tag library, and renders nothing without the JSP API Jasper carries.
  • Precompiled views need serverpath set 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.
  • The asset pipeline carries two tag libraries, and the first calls the second: <asset:stylesheet> builds its URL through assetPath, which lives in the g namespace. An application declaring the tag libraries as beans has to declare both, as a plugin ships both.
  • Rendering GSP still puts grails-core on the class path for the GrailsApplication that the page locator, tag library lookup and JSP tag library resolver read, and with it grails-datastore-core, javassist, caffeine and jakarta.persistence-api — around 2.6MB of persistence machinery that GrailsApplication.getMappingContext() makes structural rather than incidental.

…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

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.65306% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.8005%. Comparing base (c4f60ce) to head (4e2d14c).

Files with missing lines Patch % Lines
...ain/java/grails/gsp/boot/GspAutoConfiguration.java 71.0526% 11 Missing ⚠️
...ng/aop/autoproxy/GroovyAwareAutoProxyCreators.java 76.9231% 3 Missing ⚠️
...ig/GrailsEarlyPluginRegistrationPostProcessor.java 90.9091% 0 Missing and 1 partial ⚠️
...ls/gradle/plugin/views/gsp/GroovyPagePlugin.groovy 75.0000% 0 Missing and 1 partial ⚠️
...g/grails/web/pages/StandaloneTagLibraryLookup.java 91.6667% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                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     
Files with missing lines Coverage Δ
.../grails/boot/config/GrailsAutoConfiguration.groovy 60.7143% <100.0000%> (-3.9916%) ⬇️
.../groovy/org/grails/plugins/CoreGrailsPlugin.groovy 71.6049% <100.0000%> (+0.3549%) ⬆️
.../org/grails/gsp/compiler/GroovyPageCompiler.groovy 73.5043% <ø> (+7.6709%) ⬆️
...GrailsSiteMeshViewResolverBeanPostProcessor.groovy 68.4210% <100.0000%> (+8.4210%) ⬆️
...ils/plugins/sitemesh3/Sitemesh3GrailsPlugin.groovy 76.4706% <100.0000%> (+1.4706%) ⬆️
...daloneGrailsApplicationAwareBeanPostProcessor.java 100.0000% <100.0000%> (ø)
...org/grails/web/mapping/DefaultLinkGenerator.groovy 80.0000% <100.0000%> (+0.5645%) ⬆️
...ig/GrailsEarlyPluginRegistrationPostProcessor.java 83.9623% <90.9091%> (-2.6357%) ⬇️
...ls/gradle/plugin/views/gsp/GroovyPagePlugin.groovy 57.9710% <75.0000%> (+1.9104%) ⬆️
...g/grails/web/pages/StandaloneTagLibraryLookup.java 71.7949% <91.6667%> (+71.7949%) ⬆️
... and 2 more

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.
@codeconsole
codeconsole requested review from jamesfredley and matrei and removed request for jamesfredley August 21, 2026 09:46
…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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdaugherty It will be removed after the asset pipeline is released

@bito-code-review

Copy link
Copy Markdown

The configuration mentioned is part of the Grails application lifecycle management within grails-core. This PR modifies how Grails auto-configuration and plugin registration are handled to ensure they only activate when a true Grails application is detected, preventing unnecessary plugin beans from being injected into standard Spring Boot applications that might only be using specific Grails libraries.

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.
@testlens-app

testlens-app Bot commented Aug 21, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 4e2d14c
▶️ Tests: 76366 executed
⚪️ Checks: 91/91 completed


Learn more about TestLens at testlens.app/docs.

@codeconsole
codeconsole requested a review from jdaugherty August 22, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants