From bf6d4dd3d7ec785a587b09d6fd3bad8ebd0fb665 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Fri, 14 Nov 2025 12:17:32 +0100 Subject: [PATCH 1/9] feat: add versions to banner --- .../src/main/resources/grails-banner.txt | 3 +- .../main/groovy/grails/boot/GrailsApp.groovy | 4 +- .../groovy/grails/boot/GrailsBanner.groovy | 99 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy diff --git a/grails-bootstrap/src/main/resources/grails-banner.txt b/grails-bootstrap/src/main/resources/grails-banner.txt index fdc35394390..c6792b01bcf 100644 --- a/grails-bootstrap/src/main/resources/grails-banner.txt +++ b/grails-bootstrap/src/main/resources/grails-banner.txt @@ -1,4 +1,3 @@ - > < > ____ _ _ < > / ___|_ __ __ _(_) |___ < @@ -6,4 +5,4 @@ > | |_| | | | (_| | | \__ \ < > \____|_| \__,_|_|_|___/ < > https://grails.apache.org < -> < +> < \ No newline at end of file diff --git a/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy b/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy index 16bc28b2a88..2fd34aee6d0 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy @@ -27,12 +27,10 @@ import org.codehaus.groovy.control.CompilationFailedException import org.codehaus.groovy.control.CompilationUnit import org.codehaus.groovy.control.CompilerConfiguration -import org.springframework.boot.ResourceBanner import org.springframework.boot.SpringApplication import org.springframework.boot.web.context.WebServerApplicationContext import org.springframework.context.ConfigurableApplicationContext import org.springframework.core.env.ConfigurableEnvironment -import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ResourceLoader import grails.compiler.ast.ClassInjector @@ -95,7 +93,7 @@ class GrailsApp extends SpringApplication { */ GrailsApp(ResourceLoader resourceLoader, Class... sources) { super(resourceLoader, sources) - banner = new ResourceBanner(new ClassPathResource(GRAILS_BANNER)) + banner = new GrailsBanner(GRAILS_BANNER) } @Override diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy new file mode 100644 index 00000000000..107e29315ed --- /dev/null +++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.boot + +import groovy.transform.CompileStatic + +import org.springframework.boot.Banner +import org.springframework.boot.SpringBootVersion +import org.springframework.core.SpringVersion +import org.springframework.core.env.Environment +import org.springframework.core.io.ClassPathResource + +import grails.util.BuildSettings + +@CompileStatic +class GrailsBanner implements Banner { + + private static final int FALLBACK_BANNER_WIDTH = 80 + private static final int VERSIONS_MARGIN = 4 + private static final String VERSIONS_SEPARATOR = ' | ' + + private final String asciiArt + private final Map versions + + GrailsBanner(String grailsBannerFile) { + def bannerResource = new ClassPathResource(grailsBannerFile) + def appNameResolver = { Environment env -> env.getProperty('info.app.name') ?: 'application' } + def appVersionResolver = { Environment env -> env.getProperty('info.app.version') ?: 'unknown' } + asciiArt = bannerResource.exists() ? bannerResource.inputStream.text : '' + versions = [ + (appNameResolver): appVersionResolver, + 'Grails': BuildSettings.grailsVersion, + 'Groovy': GroovySystem.version, + 'JVM': System.getProperty('java.version'), + 'Spring Boot': SpringBootVersion.version, + 'Spring': SpringVersion.version + ] + } + + @Override + void printBanner(Environment environment, Class sourceClass, PrintStream out) { + int bannerWidth = longestLineLength(asciiArt) ?: FALLBACK_BANNER_WIDTH + def versionPairs = versions.collectEntries { + [(resolveValue(it.key, environment)): resolveValue(it.value, environment)] + } + out.println() + out.println(asciiArt) + buildVersionRows(versionPairs, bannerWidth).stream().forEach(out::println) + } + + private static String resolveValue(Object value, Environment environment) { + value instanceof Closure ? value(environment) : value + } + + private static int longestLineLength(String text) { + text.readLines()*.size()?.max() ?: 0 + } + + private static List buildVersionRows(Map versions, int bannerWidth) { + def maxWidth = bannerWidth - VERSIONS_MARGIN * 2 + def rows = [] + def currentRow = new StringBuilder() + def countInRow = 0 + versions.each { + String value = "$it.key: $it.value" + int proposedLength = currentRow.size() + (countInRow > 0 ? VERSIONS_SEPARATOR.size() : 0) + value.size() + boolean wouldOverflow = proposedLength > maxWidth + if (wouldOverflow) { + rows << currentRow.center(bannerWidth) + currentRow.length = 0 + } + if (currentRow.size() > 0) { + currentRow << VERSIONS_SEPARATOR + } + currentRow << value + countInRow++ + } + if (countInRow > 0) { + rows << currentRow.center(bannerWidth) + } + rows + } +} From 62a789f0b849f7f1e289a1b0f9cda09766a560d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Sat, 15 Nov 2025 15:24:21 +0100 Subject: [PATCH 2/9] Include Java vendor in Grails banner version info (#15230) --- grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy index 107e29315ed..003784a7508 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy @@ -43,11 +43,12 @@ class GrailsBanner implements Banner { def appNameResolver = { Environment env -> env.getProperty('info.app.name') ?: 'application' } def appVersionResolver = { Environment env -> env.getProperty('info.app.version') ?: 'unknown' } asciiArt = bannerResource.exists() ? bannerResource.inputStream.text : '' + def javaVendor = System.getProperty('java.vendor') versions = [ (appNameResolver): appVersionResolver, 'Grails': BuildSettings.grailsVersion, 'Groovy': GroovySystem.version, - 'JVM': System.getProperty('java.version'), + 'JVM': System.getProperty('java.version')+(javaVendor ? "-${javaVendor}": ''), 'Spring Boot': SpringBootVersion.version, 'Spring': SpringVersion.version ] From fb59c771e1ef65d37c13c9309dc7e7bdccb0d409 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Sat, 15 Nov 2025 15:41:42 +0100 Subject: [PATCH 3/9] fix: rework `printBanner` No need to store the versions or use resolvers. They are only used in the `printBanner` method. --- .../groovy/grails/boot/GrailsBanner.groovy | 44 ++++++++----------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy index 003784a7508..efbf3c9357b 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy @@ -35,38 +35,30 @@ class GrailsBanner implements Banner { private static final int VERSIONS_MARGIN = 4 private static final String VERSIONS_SEPARATOR = ' | ' - private final String asciiArt - private final Map versions + private final String bannerFile - GrailsBanner(String grailsBannerFile) { - def bannerResource = new ClassPathResource(grailsBannerFile) - def appNameResolver = { Environment env -> env.getProperty('info.app.name') ?: 'application' } - def appVersionResolver = { Environment env -> env.getProperty('info.app.version') ?: 'unknown' } - asciiArt = bannerResource.exists() ? bannerResource.inputStream.text : '' - def javaVendor = System.getProperty('java.vendor') - versions = [ - (appNameResolver): appVersionResolver, + GrailsBanner(String bannerFile) { + this.bannerFile = bannerFile + } + + @Override + void printBanner(Environment environment, Class sourceClass, PrintStream out) { + def bannerResource = new ClassPathResource(bannerFile) + def asciiArt = bannerResource.exists() ? bannerResource.inputStream.text : '' + def bannerWidth = longestLineLength(asciiArt) ?: FALLBACK_BANNER_WIDTH + def versions = [ + (environment.getProperty('info.app.name') ?: 'application'): environment.getProperty('info.app.version') ?: 'unknown', + 'JVM': System.getProperty('java.vendor') + ' ' + System.getProperty('java.version'), 'Grails': BuildSettings.grailsVersion, 'Groovy': GroovySystem.version, - 'JVM': System.getProperty('java.version')+(javaVendor ? "-${javaVendor}": ''), 'Spring Boot': SpringBootVersion.version, 'Spring': SpringVersion.version ] - } - - @Override - void printBanner(Environment environment, Class sourceClass, PrintStream out) { - int bannerWidth = longestLineLength(asciiArt) ?: FALLBACK_BANNER_WIDTH - def versionPairs = versions.collectEntries { - [(resolveValue(it.key, environment)): resolveValue(it.value, environment)] - } out.println() out.println(asciiArt) - buildVersionRows(versionPairs, bannerWidth).stream().forEach(out::println) - } - - private static String resolveValue(Object value, Environment environment) { - value instanceof Closure ? value(environment) : value + buildVersionRows(versions, bannerWidth) + .forEach { out.println(it) } + out.println() } private static int longestLineLength(String text) { @@ -80,8 +72,8 @@ class GrailsBanner implements Banner { def countInRow = 0 versions.each { String value = "$it.key: $it.value" - int proposedLength = currentRow.size() + (countInRow > 0 ? VERSIONS_SEPARATOR.size() : 0) + value.size() - boolean wouldOverflow = proposedLength > maxWidth + def proposedLength = currentRow.size() + (countInRow > 0 ? VERSIONS_SEPARATOR.size() : 0) + value.size() + def wouldOverflow = proposedLength > maxWidth if (wouldOverflow) { rows << currentRow.center(bannerWidth) currentRow.length = 0 From 4babe75c3a2bad2baef0d631fcf9224691b2c977 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 17 Nov 2025 11:42:58 +0100 Subject: [PATCH 4/9] feat: make GrailsBanner configurable - Allow overriding behavior - Add conditional output of art and versions --- .../main/groovy/grails/boot/GrailsApp.groovy | 3 +- .../groovy/grails/boot/GrailsBanner.groovy | 79 ++++++++++++------- ...itional-spring-configuration-metadata.json | 15 ++++ 3 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 grails-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json diff --git a/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy b/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy index 2fd34aee6d0..3aac0c6d0f0 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsApp.groovy @@ -59,7 +59,6 @@ import org.grails.plugins.support.WatchPattern @CompileStatic class GrailsApp extends SpringApplication { - private static final String GRAILS_BANNER = 'grails-banner.txt' private static final String SPRING_PROFILES = 'spring.profiles.active' private static boolean developmentModeActive = false @@ -93,7 +92,7 @@ class GrailsApp extends SpringApplication { */ GrailsApp(ResourceLoader resourceLoader, Class... sources) { super(resourceLoader, sources) - banner = new GrailsBanner(GRAILS_BANNER) + banner = new GrailsBanner() } @Override diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy index efbf3c9357b..3e014deb62a 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy @@ -31,55 +31,74 @@ import grails.util.BuildSettings @CompileStatic class GrailsBanner implements Banner { - private static final int FALLBACK_BANNER_WIDTH = 80 - private static final int VERSIONS_MARGIN = 4 - private static final String VERSIONS_SEPARATOR = ' | ' + private static final int FALLBACK_BANNER_WIDTH = 0 - private final String bannerFile - - GrailsBanner(String bannerFile) { - this.bannerFile = bannerFile - } + String bannerFile = 'grails-banner.txt' + int upperPadding = 1 + int lowerPadding = 1 + int versionsMargin = 4 + String versionsSeparator = ' | ' @Override void printBanner(Environment environment, Class sourceClass, PrintStream out) { + + def bannerWidth = FALLBACK_BANNER_WIDTH + + upperPadding.times { out.println() } + + if (shouldDisplayArt(environment)) { + def art = createBannerArt(environment) + bannerWidth = longestLineLength(art) ?: FALLBACK_BANNER_WIDTH + out.println(art) + } + + if (shouldDisplayVersions(environment)) { + buildVersionRows(createBannerVersions(environment), bannerWidth) + .forEach { out.println(it) } + } + + lowerPadding.times { out.println() } + } + + protected String createBannerArt(Environment environment) { def bannerResource = new ClassPathResource(bannerFile) - def asciiArt = bannerResource.exists() ? bannerResource.inputStream.text : '' - def bannerWidth = longestLineLength(asciiArt) ?: FALLBACK_BANNER_WIDTH - def versions = [ - (environment.getProperty('info.app.name') ?: 'application'): environment.getProperty('info.app.version') ?: 'unknown', - 'JVM': System.getProperty('java.vendor') + ' ' + System.getProperty('java.version'), - 'Grails': BuildSettings.grailsVersion, - 'Groovy': GroovySystem.version, - 'Spring Boot': SpringBootVersion.version, - 'Spring': SpringVersion.version + bannerResource.exists() ? bannerResource.inputStream.text : '' + } + + protected Map createBannerVersions(Environment environment) { + [ + (environment.getProperty('info.app.name') ?: 'application'): environment.getProperty('info.app.version') ?: 'unknown', + 'JVM': System.getProperty('java.vendor') + ' ' + System.getProperty('java.version'), + 'Grails': BuildSettings.grailsVersion, + 'Groovy': GroovySystem.version, + 'Spring Boot': SpringBootVersion.version, + 'Spring': SpringVersion.version ] - out.println() - out.println(asciiArt) - buildVersionRows(versions, bannerWidth) - .forEach { out.println(it) } - out.println() } - private static int longestLineLength(String text) { - text.readLines()*.size()?.max() ?: 0 + protected boolean shouldDisplayArt(Environment environment) { + environment.getProperty('grails.banner.display.art', Boolean, true) + } + + protected boolean shouldDisplayVersions(Environment environment) { + environment.getProperty('grails.banner.display.versions', Boolean, true) } - private static List buildVersionRows(Map versions, int bannerWidth) { - def maxWidth = bannerWidth - VERSIONS_MARGIN * 2 + protected List buildVersionRows(Map versions, int bannerWidth) { + def maxWidth = bannerWidth - versionsMargin * 2 def rows = [] def currentRow = new StringBuilder() def countInRow = 0 versions.each { String value = "$it.key: $it.value" - def proposedLength = currentRow.size() + (countInRow > 0 ? VERSIONS_SEPARATOR.size() : 0) + value.size() + def proposedLength = currentRow.size() + (countInRow > 0 ? versionsSeparator.size() : 0) + value.size() def wouldOverflow = proposedLength > maxWidth if (wouldOverflow) { rows << currentRow.center(bannerWidth) currentRow.length = 0 } if (currentRow.size() > 0) { - currentRow << VERSIONS_SEPARATOR + currentRow << versionsSeparator } currentRow << value countInRow++ @@ -89,4 +108,8 @@ class GrailsBanner implements Banner { } rows } + + private static int longestLineLength(String text) { + text.readLines()*.size()?.max() ?: 0 + } } diff --git a/grails-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/grails-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000000..2be53afe8bd --- /dev/null +++ b/grails-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,15 @@ +{ + "groups": [], + "properties": [ + { + "name": "grails.banner.display.art", + "type": "java.lang.Boolean", + "defaultValue": true + }, + { + "name": "grails.banner.display.versions", + "type": "java.lang.Boolean", + "defaultValue": true + } + ] +} \ No newline at end of file From 506122bbbf9201a6018f92eecebe7840c0eb81fd Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 17 Nov 2025 13:19:02 +0100 Subject: [PATCH 5/9] feat: allow overriding banner versions formatting --- .../groovy/grails/boot/GrailsBanner.groovy | 86 +++++++++++++------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy index 3e014deb62a..c621b4102fb 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy @@ -19,6 +19,7 @@ package grails.boot import groovy.transform.CompileStatic +import groovy.transform.Immutable import org.springframework.boot.Banner import org.springframework.boot.SpringBootVersion @@ -36,8 +37,6 @@ class GrailsBanner implements Banner { String bannerFile = 'grails-banner.txt' int upperPadding = 1 int lowerPadding = 1 - int versionsMargin = 4 - String versionsSeparator = ' | ' @Override void printBanner(Environment environment, Class sourceClass, PrintStream out) { @@ -53,7 +52,7 @@ class GrailsBanner implements Banner { } if (shouldDisplayVersions(environment)) { - buildVersionRows(createBannerVersions(environment), bannerWidth) + createVersionsFormatter().format(createBannerVersions(environment), bannerWidth) .forEach { out.println(it) } } @@ -84,32 +83,69 @@ class GrailsBanner implements Banner { environment.getProperty('grails.banner.display.versions', Boolean, true) } - protected List buildVersionRows(Map versions, int bannerWidth) { - def maxWidth = bannerWidth - versionsMargin * 2 - def rows = [] - def currentRow = new StringBuilder() - def countInRow = 0 - versions.each { - String value = "$it.key: $it.value" - def proposedLength = currentRow.size() + (countInRow > 0 ? versionsSeparator.size() : 0) + value.size() - def wouldOverflow = proposedLength > maxWidth - if (wouldOverflow) { - rows << currentRow.center(bannerWidth) - currentRow.length = 0 + protected VersionsFormatter createVersionsFormatter() { + new DefaultVersionFormatter() + } + + private static int longestLineLength(String text) { + text.readLines()*.size()?.max() ?: 0 + } + + @FunctionalInterface + interface VersionsFormatter { + + /** + * @param versions insertion-ordered map (e.g., LinkedHashMap) of label -> value + * @param bannerWidth total banner width in characters + * @return formatted lines to print + */ + List format(Map versions, int bannerWidth) + } + + @CompileStatic + class DefaultVersionFormatter implements VersionsFormatter { + + VersionsFormatOptions options = new VersionsFormatOptions() + + @Override + List format(Map versions, int bannerWidth) { + def columnWidth = bannerWidth - options.margin * 2 + List rows = [] + def currentRow = new StringBuilder() + def countInRow = 0 + + versions.each { k, v -> + String item = "$k: $v" + def proposedLength = currentRow.length() + (countInRow > 0 ? options.separator.size() : 0) + item.size() + def wouldOverflow = (countInRow > 0 && proposedLength > columnWidth) + def hitCountLimit = (options.maxItemsPerRow > 0 && countInRow >= options.maxItemsPerRow) + + if (wouldOverflow || hitCountLimit) { + rows << currentRow.center(bannerWidth) + currentRow.length = 0 + countInRow = 0 + } + + if (currentRow.size() > 0) { + currentRow << options.separator + } + currentRow << item + countInRow++ } - if (currentRow.size() > 0) { - currentRow << versionsSeparator + + if (countInRow > 0) { + rows << currentRow.center(bannerWidth) } - currentRow << value - countInRow++ - } - if (countInRow > 0) { - rows << currentRow.center(bannerWidth) + + return rows } - rows } - private static int longestLineLength(String text) { - text.readLines()*.size()?.max() ?: 0 + @Immutable + @CompileStatic + class VersionsFormatOptions { + int margin = 4 + int maxItemsPerRow = 0 // 0 or negative = unlimited + String separator = ' | ' } } From 2598cef29fb20b962a1d202e0f694b4b855f598f Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 18 Nov 2025 17:06:53 +0100 Subject: [PATCH 6/9] feat: add map constructor to `GrailsBanner` --- grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy index c621b4102fb..f18f602afcc 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy @@ -20,6 +20,7 @@ package grails.boot import groovy.transform.CompileStatic import groovy.transform.Immutable +import groovy.transform.MapConstructor import org.springframework.boot.Banner import org.springframework.boot.SpringBootVersion @@ -30,6 +31,7 @@ import org.springframework.core.io.ClassPathResource import grails.util.BuildSettings @CompileStatic +@MapConstructor(noArg = true) class GrailsBanner implements Banner { private static final int FALLBACK_BANNER_WIDTH = 0 From e22fe02f006b191883f88a8857fa8e0ca3f64814 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Wed, 19 Nov 2025 12:55:26 +0100 Subject: [PATCH 7/9] feat: add more banner customization options --- .../groovy/grails/boot/GrailsBanner.groovy | 309 +++++++++++++++--- ...itional-spring-configuration-metadata.json | 28 +- 2 files changed, 293 insertions(+), 44 deletions(-) diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy index f18f602afcc..a7cdde6b08d 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy @@ -19,8 +19,9 @@ package grails.boot import groovy.transform.CompileStatic -import groovy.transform.Immutable import groovy.transform.MapConstructor +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType import org.springframework.boot.Banner import org.springframework.boot.SpringBootVersion @@ -30,97 +31,264 @@ import org.springframework.core.io.ClassPathResource import grails.util.BuildSettings +/** + * The default Grails application banner. + * + * @since 7.1 + */ @CompileStatic @MapConstructor(noArg = true) class GrailsBanner implements Banner { private static final int FALLBACK_BANNER_WIDTH = 0 + private static final String DEFAULT_BANNER_FILE = 'grails-banner.txt' - String bannerFile = 'grails-banner.txt' - int upperPadding = 1 - int lowerPadding = 1 + String bannerFile = DEFAULT_BANNER_FILE + int bannerPaddingTop = 1 + int bannerPaddingBottom = 1 + int artPaddingBottom = 0 + /** + * Prints the banner to the specified PrintStream. + * + * @param environment the current environment + * @param sourceClass the source class + * @param out the PrintStream to print to + */ @Override void printBanner(Environment environment, Class sourceClass, PrintStream out) { def bannerWidth = FALLBACK_BANNER_WIDTH - upperPadding.times { out.println() } - + bannerPaddingTop.times { out.println() } if (shouldDisplayArt(environment)) { def art = createBannerArt(environment) bannerWidth = longestLineLength(art) ?: FALLBACK_BANNER_WIDTH out.println(art) } - + artPaddingBottom.times { out.println() } if (shouldDisplayVersions(environment)) { createVersionsFormatter().format(createBannerVersions(environment), bannerWidth) .forEach { out.println(it) } } - - lowerPadding.times { out.println() } + bannerPaddingBottom.times { out.println() } } + /** + * Creates the banner art to be displayed. + * + * @param environment the current environment + * @return the banner art + */ protected String createBannerArt(Environment environment) { - def bannerResource = new ClassPathResource(bannerFile) - bannerResource.exists() ? bannerResource.inputStream.text : '' + if (bannerFile != DEFAULT_BANNER_FILE) { + // Banner file was programmatically set, use it directly + def customBannerResource = new ClassPathResource(bannerFile) + if (customBannerResource.exists()) { + return customBannerResource.inputStream.text + } + } else { + // Use configured banner file or default + def configBannerFile = environment.getProperty('grails.banner.art.file', String, DEFAULT_BANNER_FILE) + def bannerResource = new ClassPathResource(configBannerFile) + if (bannerResource.exists()) { + return bannerResource.inputStream.text + } + } + return '' } - protected Map createBannerVersions(Environment environment) { - [ - (environment.getProperty('info.app.name') ?: 'application'): environment.getProperty('info.app.version') ?: 'unknown', - 'JVM': System.getProperty('java.vendor') + ' ' + System.getProperty('java.version'), - 'Grails': BuildSettings.grailsVersion, - 'Groovy': GroovySystem.version, - 'Spring Boot': SpringBootVersion.version, - 'Spring': SpringVersion.version - ] + /** + * Creates a map of versions to be displayed in the banner. + * + * @param env the current env + * @return a map of version labels to version values + */ + @SuppressWarnings('GrMethodMayBeStatic') + protected Map createBannerVersions(Environment env) { + def defaultIncluded = (DefaultVersionOption.values()).collect { it.key } + def sortOrder = findConfiguredVersions(env, 'grails.banner.versions.order') { it in VersionOption.values()*.key } + def configExcluded = findConfiguredVersions(env, 'grails.banner.versions.exclude') { it in DefaultVersionOption.values()*.key } + def configIncluded = findConfiguredVersions(env, 'grails.banner.versions.include') { it in OptionalVersionOption.values()*.key } + def includedVersions = defaultIncluded + .tap { removeAll(configExcluded) } + .tap { addAll(configIncluded) } + .unique() + if (sortOrder) { + includedVersions.sort { a, b -> + def indexA = sortOrder.indexOf(a) + def indexB = sortOrder.indexOf(b) + if (indexA == -1 && indexB == -1) { + return 0 + } else if (indexA == -1) { + return 1 + } else if (indexB == -1) { + return -1 + } else { + return indexA <=> indexB + } + } + } + includedVersions.collectEntries { key -> + switch (VersionOption.fromString(key)) { + case VersionOption.APP: + [(env.getProperty('info.app.name') ?: 'app'): env.getProperty('info.app.version') ?: 'unknown'] + break + case VersionOption.JVM: + ['JVM': System.getProperty('java.vendor') + ' ' + System.getProperty('java.version')] + break + case VersionOption.GRAILS: + ['Grails': BuildSettings.grailsVersion] + break + case VersionOption.GROOVY: + ['Groovy': GroovySystem.version] + break + case VersionOption.SPRING_BOOT: + ['Spring Boot': SpringBootVersion.version] + break + case VersionOption.SPRING: + ['Spring': SpringVersion.version] + break + case VersionOption.SPRING_SECURITY: + ['Spring Security': findVersion('org.springframework.security.core.SpringSecurityCoreVersion')] + break + case VersionOption.TOMCAT: + ['Tomcat': findVersion('org.apache.catalina.util.ServerInfo')] + break + case VersionOption.JETTY: + ['Jetty': findVersion('org.eclipse.jetty.util.Jetty')] + break + case VersionOption.UNDERTOW: + ['Undertow': findVersion('io.undertow.Undertow')] + break + default: + null + } + } as Map + } + + /** + * Finds the implementation version of the specified class. + * + * @param className the fully qualified class name + * @return the implementation version, or 'unknown' if not found + */ + private static String findVersion(String className) { + try { + def pkg = Class.forName(className).package + return pkg?.implementationVersion ?: 'unknown' + } catch (ClassNotFoundException ignore) { + return 'unknown' + } + } + + /** + * Finds the configured versions from the environment. + * + * @param env the current environment + * @param propertyName the property name to look for + * @param filter the filter closure + * @return a list of configured versions + */ + private static List findConfiguredVersions( + Environment env, + String propertyName, + @ClosureParams( + value = SimpleType, + options = ['java.lang.String'] + ) Closure filter) { + env.getProperty(propertyName, List, [] as List).findAll(filter) } - protected boolean shouldDisplayArt(Environment environment) { - environment.getProperty('grails.banner.display.art', Boolean, true) + /** + * Determines whether to display the banner art. + * + * @param env the current environment + * @return true if the banner art should be displayed, false otherwise + */ + @SuppressWarnings('GrMethodMayBeStatic') + protected boolean shouldDisplayArt(Environment env) { + env.getProperty('grails.banner.art.display', Boolean, true) } - protected boolean shouldDisplayVersions(Environment environment) { - environment.getProperty('grails.banner.display.versions', Boolean, true) + /** + * Determines whether to display the version information. + * + * @param env the current environment + * @return true if the version information should be displayed, false otherwise + */ + @SuppressWarnings('GrMethodMayBeStatic') + protected boolean shouldDisplayVersions(Environment env) { + env.getProperty('grails.banner.versions.display', Boolean, true) } + /** + * Creates the versions formatter to format the version information. + * + * @return the versions formatter + */ protected VersionsFormatter createVersionsFormatter() { new DefaultVersionFormatter() } + /** + * Calculates the length of the longest line in the given text. + * + * @param text the text to analyze + * @return the length of the longest line + */ private static int longestLineLength(String text) { text.readLines()*.size()?.max() ?: 0 } + /** + * Strategy interface for formatting version information + * into printable lines. + */ @FunctionalInterface interface VersionsFormatter { /** - * @param versions insertion-ordered map (e.g., LinkedHashMap) of label -> value - * @param bannerWidth total banner width in characters - * @return formatted lines to print + * Formats the version information into a list of banner lines. + * + * @param versions An insertion-ordered map (e.g., LinkedHashMap) + * mapping human-readable labels to version values. + * The iteration order defines the order of the + * formatted output. + * @param bannerWidth Total banner width in characters + * @return a list of lines to print, without line-termination characters */ - List format(Map versions, int bannerWidth) + List format(Map versions, int bannerWidth) } + /** + * The default implementation of the VersionsFormatter. + */ @CompileStatic - class DefaultVersionFormatter implements VersionsFormatter { + @MapConstructor(noArg = true) + static class DefaultVersionFormatter implements VersionsFormatter { - VersionsFormatOptions options = new VersionsFormatOptions() + int margin = 4 + int maxItemsPerRow = 0 // 0 or negative = unlimited + String itemSeparator = ' | ' + String pairSeparator = ': ' + /** + * Formats the version information into centered lines that fit + * within the banner width. + */ @Override - List format(Map versions, int bannerWidth) { - def columnWidth = bannerWidth - options.margin * 2 + List format(Map versions, int bannerWidth) { + def columnWidth = bannerWidth - margin * 2 List rows = [] def currentRow = new StringBuilder() def countInRow = 0 versions.each { k, v -> - String item = "$k: $v" - def proposedLength = currentRow.length() + (countInRow > 0 ? options.separator.size() : 0) + item.size() + String item = "$k${pairSeparator}$v" + def proposedLength = currentRow.length() + (countInRow > 0 ? itemSeparator.size() : 0) + item.size() def wouldOverflow = (countInRow > 0 && proposedLength > columnWidth) - def hitCountLimit = (options.maxItemsPerRow > 0 && countInRow >= options.maxItemsPerRow) + def hitCountLimit = (maxItemsPerRow > 0 && countInRow >= maxItemsPerRow) if (wouldOverflow || hitCountLimit) { rows << currentRow.center(bannerWidth) @@ -129,7 +297,7 @@ class GrailsBanner implements Banner { } if (currentRow.size() > 0) { - currentRow << options.separator + currentRow << itemSeparator } currentRow << item countInRow++ @@ -143,11 +311,70 @@ class GrailsBanner implements Banner { } } - @Immutable + /** + * Enumeration of supported version options. + */ @CompileStatic - class VersionsFormatOptions { - int margin = 4 - int maxItemsPerRow = 0 // 0 or negative = unlimited - String separator = ' | ' + enum VersionOption { + APP, + JVM, + GRAILS, + GROOVY, + SPRING_BOOT, + SPRING, + SPRING_SECURITY, + TOMCAT, + JETTY, + UNDERTOW; + + final String key + + VersionOption() { + this.key = name().toLowerCase().replace('_', '-') + } + + static VersionOption fromString(String value) { + try { + return valueOf(value.toUpperCase().replace('-', '_')) + } catch (IllegalArgumentException ignore) { + return null + } + } + } + + /** + * Enumeration of default version options. + */ + @CompileStatic + enum DefaultVersionOption { + APP, + JVM, + GRAILS, + GROOVY, + SPRING_BOOT, + SPRING; + + final String key + + DefaultVersionOption() { + this.key = name().toLowerCase().replace('_', '-') + } + } + + /** + * Enumeration of optional version options. + */ + @CompileStatic + enum OptionalVersionOption { + SPRING_SECURITY, + TOMCAT, + JETTY, + UNDERTOW; + + final String key + + OptionalVersionOption() { + this.key = name().toLowerCase().replace('_', '-') + } } } diff --git a/grails-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/grails-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 2be53afe8bd..e4ca458afdd 100644 --- a/grails-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/grails-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,15 +1,37 @@ { - "groups": [], "properties": [ { - "name": "grails.banner.display.art", + "name": "grails.banner.art.display", + "description": "Whether to display the Grails banner art.", "type": "java.lang.Boolean", "defaultValue": true }, { - "name": "grails.banner.display.versions", + "name": "grails.banner.art.file", + "description": "The file path on the classpath to the Grails banner art to display.", + "type": "java.lang.String", + "defaultValue": "grails-banner.txt" + }, + { + "name": "grails.banner.versions.display", + "description": "Whether to display version information in the Grails banner.", "type": "java.lang.Boolean", "defaultValue": true + }, + { + "name": "grails.banner.versions.include", + "description": "A list of optional versions to include in the banner.", + "type": "java.util.List" + }, + { + "name": "grails.banner.versions.exclude", + "description": "A list of versions to exclude from the banner.", + "type": "java.util.List" + }, + { + "name": "grails.banner.versions.order", + "description": "A sorted list of versions to put first in the banner versions.", + "type": "java.util.List" } ] } \ No newline at end of file From b5be89f2ffe40cdbbf271ec17a7e078359338717 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Wed, 19 Nov 2025 13:04:08 +0100 Subject: [PATCH 8/9] docs: document the banner functionality --- .../conf/applicationClass/customizing.adoc | 125 ++++++++++++++++++ .../src/en/guide/introduction/whatsNew.adoc | 7 +- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/grails-doc/src/en/guide/conf/applicationClass/customizing.adoc b/grails-doc/src/en/guide/conf/applicationClass/customizing.adoc index 3363bf86c40..abab83a105a 100644 --- a/grails-doc/src/en/guide/conf/applicationClass/customizing.adoc +++ b/grails-doc/src/en/guide/conf/applicationClass/customizing.adoc @@ -54,3 +54,128 @@ class Application extends GrailsAutoConfiguration { ... } ---- + +[id=customizing-the-banner] +=== Customizing the Banner + +The banner that is printed on application startup can be customized in several ways. You can use the Spring Boot way and provide your own banner text file by placing a file named `banner.txt` in the `src/main/resources` directory of your application. This will replace the default Grails banner described below. + +To turn off the banner completely, set the `spring.main.banner-mode` property to `off` in the `application.yml` file: +[source,yaml] +---- +spring: + main: + banner-mode: "off" +---- + +The default Grails banner is configurable via the following configuration properties: +[source,yaml] +---- +grails: + banner: + art: + display: true # Whether to display the banner art (default: true). + file: 'banner.txt' # Path to a custom banner file on the classpath (default: null). + versions: + display: true # Whether to display version information (default: true). + include: + # Include optional versions (default: []). + # These will trigger class loading to determine the version + # and show 'unknown' if the class is not found. + # Valid options are: + - spring-security + - tomcat + - jetty + - undertow + exclude: + # Exclude any of the versions included by default (default: []). + # Valid options are: + - app + - grails + - groovy + - jvm + - spring-boot + - spring + order: + # Optional ordering in which to display the versions in the banner. + # You can specify any number of the options listed below + # to move them to the top of the list in the specified order. + # If no config is specified, the default order is: + - app + - jvm + - grails + - groovy + - spring-boot + - spring + - spring-security + - tomcat + - jetty + - undertow +---- + +It is also possible to customize the Grails banner by overriding any or all of the following methods of the `GrailsBanner` class and assigning it to the `banner` property of the `GrailsApp` class: +[source,groovy] +---- +import grails.boot.GrailsApp +import grails.boot.banner.GrailsBanner +import grails.boot.config.GrailsAutoConfiguration +import org.springframework.core.env.Environment + +class Application extends GrailsAutoConfiguration { + + static void main(String[] args) { + def app = new GrailsApp(Application) + app.banner = new GrailsBanner( + // You can specify any path on the classpath here, + // typically stored in src/main/resources. + // This will take precedence over the 'grails.banner.art.file' config value + bannerFile: 'my-banner.txt', // default: 'grails-banner.txt' (provided by Grails) + + // Number of blank lines to print for padding + paddingTop: 10, // default: 1 + artPaddingBottom: 1, // default: 0 + paddingBottom: 10 // default: 1 + ) { + + @Override + String createBannerArt(Environment env) { + // This will take precedence over the bannerFile property + 'MY CUSTOM BANNER!' + } + + @Override + Map createBannerVersions(Environment env) { + // Return a map of version names to version values + super.createBannerVersions(env) + ['Some version': '1.0.0'] + } + + @Override + boolean shouldDisplayArt(Environment env) { true } + + @Override + boolean shouldDisplayVersions(Environment env) { true } + + @Override + GrailsBanner.VersionsFormatter createVersionsFormatter() { + // You can provide your own implementation of the VersionsFormatter interface: + def functionalInterfaceExample = { Map versions, int bannerWidth -> + // Return a list of strings representing the lines printed, + // in this case, one single line with all versions, comma-separated + [versions.collect { k, v -> "$k: $v" }.join(', ')] + } as GrailsBanner.VersionsFormatter + + // or customize the default implementation: + def customizingDefaultExample = new GrailsBanner.DefaultVersionsFormatter( + margin: 10, // min spaces to the left and right before breaking, default: 4 + maxItemsPerRow: 3, // 0 or negative = unlimited, default: 0 + itemSeparator: ' <> ', // default: ' | ' + pairSeparator: '-> ' // default: ': ' + ) + + return functionalInterfaceExample + } + } + app.run(args) + } +} +---- diff --git a/grails-doc/src/en/guide/introduction/whatsNew.adoc b/grails-doc/src/en/guide/introduction/whatsNew.adoc index bd984f31105..ad1b55d2f9e 100644 --- a/grails-doc/src/en/guide/introduction/whatsNew.adoc +++ b/grails-doc/src/en/guide/introduction/whatsNew.adoc @@ -74,7 +74,7 @@ The `@Scaffold` annotation was added to customize scaffolding generation for con ==== Bootstrap 5.3.3 support Bootstrap 5.3.3 support. -Saffolding and Fields tags now optionally support boostrap classes. +Scaffolding and Fields tags now optionally support boostrap classes. ==== Prioritization of AutoConfiguration over bean overriding. @@ -89,5 +89,8 @@ With the removal of Micronaut, and the fixes to the asset pipeline plugin, Grail The `g:form` tag now automatically provides csrf protection when Spring Security CSRF is enabled. +==== Grails Banner versions and customization (Grails 7.1+) - +A Grails banner was introduced in Grails 7 that is displayed on application startup. +From Grails 7.1 onwards, this banner now shows versions of foundational dependencies +and can be xref:conf.adoc#customizing-the-banner[customized]. From c5cbdac65e86850b336e19867ea74213af15fc03 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Wed, 19 Nov 2025 13:15:21 +0100 Subject: [PATCH 9/9] style: remove unnecessary semicolons --- grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy index a7cdde6b08d..f4163944747 100644 --- a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy +++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy @@ -325,7 +325,7 @@ class GrailsBanner implements Banner { SPRING_SECURITY, TOMCAT, JETTY, - UNDERTOW; + UNDERTOW final String key @@ -352,7 +352,7 @@ class GrailsBanner implements Banner { GRAILS, GROOVY, SPRING_BOOT, - SPRING; + SPRING final String key @@ -369,7 +369,7 @@ class GrailsBanner implements Banner { SPRING_SECURITY, TOMCAT, JETTY, - UNDERTOW; + UNDERTOW final String key