diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultSpringApplicationBannerPrinter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultSpringApplicationBannerPrinter.java new file mode 100644 index 000000000000..f48c69fc9de2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultSpringApplicationBannerPrinter.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed 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 org.springframework.boot; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.env.Environment; + +/** + * Class used by {@link SpringApplication} to print the application banner. + * + * @author Phillip Webb + * @author Junhyung Park + */ +class DefaultSpringApplicationBannerPrinter implements SpringApplicationBannerPrinter { + + private static final Log logger = LogFactory.getLog(DefaultSpringApplicationBannerPrinter.class); + + @Override + public Banner print(Environment environment, Class sourceClass, Banner.Mode bannerMode, Banner banner) { + banner.printBanner(environment, sourceClass, System.out); + switch (bannerMode) { + case OFF: + case LOG: + try { + logger.info(createStringFromBanner(banner, environment, sourceClass)); + } + catch (UnsupportedEncodingException ex) { + logger.warn("Failed to create String for banner", ex); + } + case CONSOLE: + banner.printBanner(environment, sourceClass, System.out); + } + return new PrintedBanner(banner, sourceClass); + } + + private String createStringFromBanner(Banner banner, Environment environment, Class mainApplicationClass) + throws UnsupportedEncodingException { + String charset = environment.getProperty("spring.banner.charset", StandardCharsets.UTF_8.name()); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try (PrintStream out = new PrintStream(byteArrayOutputStream, false, charset)) { + banner.printBanner(environment, mainApplicationClass, out); + } + return byteArrayOutputStream.toString(charset); + } + + /** + * Decorator that allows a {@link Banner} to be printed again without needing to + * specify the source class. + */ + private static class PrintedBanner implements Banner { + + private final Banner banner; + + private final Class sourceClass; + + PrintedBanner(Banner banner, Class sourceClass) { + this.banner = banner; + this.sourceClass = sourceClass; + } + + @Override + public void printBanner(Environment environment, Class sourceClass, PrintStream out) { + sourceClass = (sourceClass != null) ? sourceClass : this.sourceClass; + this.banner.printBanner(environment, sourceClass, out); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 005f8e81d3c0..521a0a02128d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -16,6 +16,7 @@ package org.springframework.boot; +import java.io.IOException; import java.lang.StackWalker.StackFrame; import java.lang.management.ManagementFactory; import java.lang.reflect.Method; @@ -56,7 +57,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; -import org.springframework.boot.Banner.Mode; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; @@ -93,6 +93,7 @@ import org.springframework.core.env.PropertySource; import org.springframework.core.env.SimpleCommandLinePropertySource; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver; @@ -180,6 +181,7 @@ * @author Tadaya Tsuyukubo * @author Lasse Wulff * @author Yanming Zhou + * @author Junhyung Park * @since 1.0.0 * @see #run(Class, String[]) * @see #run(Class[], String[]) @@ -215,6 +217,8 @@ public class SpringApplication { private Banner banner; + private SpringApplicationBannerPrinter bannerPrinter; + private ResourceLoader resourceLoader; private BeanNameGenerator beanNameGenerator; @@ -557,13 +561,39 @@ private Banner printBanner(ConfigurableEnvironment environment) { if (this.properties.getBannerMode(environment) == Banner.Mode.OFF) { return null; } - ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader - : new DefaultResourceLoader(null); - SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner); - if (this.properties.getBannerMode(environment) == Mode.LOG) { - return bannerPrinter.print(environment, this.mainApplicationClass, logger); + SpringApplicationBannerPrinter bannerPrinter = Objects.requireNonNullElseGet(this.bannerPrinter, + DefaultSpringApplicationBannerPrinter::new); + Banner banner = this.banner; + if (banner == null) { + banner = getFallbackBanner(environment); } - return bannerPrinter.print(environment, this.mainApplicationClass, System.out); + return bannerPrinter.print(environment, this.mainApplicationClass, this.properties.getBannerMode(environment), + banner); + } + + private Banner getFallbackBanner(Environment environment) { + Banner textBanner = getTextBanner(environment); + if (textBanner != null) { + return textBanner; + } + return new SpringBootBanner(); + } + + private Banner getTextBanner(Environment environment) { + if (this.resourceLoader == null) { + return null; + } + String location = environment.getProperty(BANNER_LOCATION_PROPERTY, BANNER_LOCATION_PROPERTY_VALUE); + Resource resource = this.resourceLoader.getResource(location); + try { + if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) { + return new ResourceBanner(resource); + } + } + catch (IOException ex) { + // Ignore + } + return null; } /** @@ -1036,6 +1066,15 @@ public void setBannerMode(Banner.Mode bannerMode) { this.properties.setBannerMode(bannerMode); } + /** + * Sets the {@link SpringApplicationBannerPrinter} used to print the banner. Defaults + * to {@link SpringApplicationBannerPrinter}. + * @param bannerPrinter the printer used to print the banner + */ + public void setBannerPrinter(SpringApplicationBannerPrinter bannerPrinter) { + this.bannerPrinter = bannerPrinter; + } + /** * Sets if the application information should be logged when the application starts. * Defaults to {@code true}. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationBannerPrinter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationBannerPrinter.java index a4bba43b86d5..9d721cdcc1c9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationBannerPrinter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationBannerPrinter.java @@ -16,118 +16,26 @@ package org.springframework.boot; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; - -import org.apache.commons.logging.Log; - import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.core.env.Environment; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; /** - * Class used by {@link SpringApplication} to print the application banner. + * Interface class used to print the application banner. * * @author Phillip Webb + * @author Junhyung Park + * @since 3.4.0 */ -class SpringApplicationBannerPrinter { - - static final String BANNER_LOCATION_PROPERTY = "spring.banner.location"; - - static final String DEFAULT_BANNER_LOCATION = "banner.txt"; - - private static final Banner DEFAULT_BANNER = new SpringBootBanner(); +public interface SpringApplicationBannerPrinter { - private final ResourceLoader resourceLoader; + String BANNER_LOCATION_PROPERTY = "spring.banner.location"; - private final Banner fallbackBanner; - - SpringApplicationBannerPrinter(ResourceLoader resourceLoader, Banner fallbackBanner) { - this.resourceLoader = resourceLoader; - this.fallbackBanner = fallbackBanner; - } - - Banner print(Environment environment, Class sourceClass, Log logger) { - Banner banner = getBanner(environment); - try { - logger.info(createStringFromBanner(banner, environment, sourceClass)); - } - catch (UnsupportedEncodingException ex) { - logger.warn("Failed to create String for banner", ex); - } - return new PrintedBanner(banner, sourceClass); - } - - Banner print(Environment environment, Class sourceClass, PrintStream out) { - Banner banner = getBanner(environment); - banner.printBanner(environment, sourceClass, out); - return new PrintedBanner(banner, sourceClass); - } - - private Banner getBanner(Environment environment) { - Banner textBanner = getTextBanner(environment); - if (textBanner != null) { - return textBanner; - } - if (this.fallbackBanner != null) { - return this.fallbackBanner; - } - return DEFAULT_BANNER; - } + String DEFAULT_BANNER_LOCATION = "banner.txt"; - private Banner getTextBanner(Environment environment) { - String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION); - Resource resource = this.resourceLoader.getResource(location); - try { - if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) { - return new ResourceBanner(resource); - } - } - catch (IOException ex) { - // Ignore - } - return null; - } - - private String createStringFromBanner(Banner banner, Environment environment, Class mainApplicationClass) - throws UnsupportedEncodingException { - String charset = environment.getProperty("spring.banner.charset", StandardCharsets.UTF_8.name()); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try (PrintStream out = new PrintStream(byteArrayOutputStream, false, charset)) { - banner.printBanner(environment, mainApplicationClass, out); - } - return byteArrayOutputStream.toString(charset); - } - - /** - * Decorator that allows a {@link Banner} to be printed again without needing to - * specify the source class. - */ - private static class PrintedBanner implements Banner { - - private final Banner banner; - - private final Class sourceClass; - - PrintedBanner(Banner banner, Class sourceClass) { - this.banner = banner; - this.sourceClass = sourceClass; - } - - @Override - public void printBanner(Environment environment, Class sourceClass, PrintStream out) { - sourceClass = (sourceClass != null) ? sourceClass : this.sourceClass; - this.banner.printBanner(environment, sourceClass, out); - } - - } + Banner print(Environment environment, Class sourceClass, Banner.Mode mode, Banner banner); - static class SpringApplicationBannerPrinterRuntimeHints implements RuntimeHintsRegistrar { + class SpringApplicationBannerPrinterRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java index b479568fcb53..2e3992e07d36 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java @@ -34,6 +34,7 @@ import org.springframework.boot.BootstrapRegistry; import org.springframework.boot.BootstrapRegistryInitializer; import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringApplicationBannerPrinter; import org.springframework.boot.WebApplicationType; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.context.ApplicationContext; @@ -337,6 +338,11 @@ public SpringApplicationBuilder bannerMode(Banner.Mode bannerMode) { return this; } + public SpringApplicationBuilder bannerPrinter(SpringApplicationBannerPrinter bannerPrinter) { + this.application.setBannerPrinter(bannerPrinter); + return this; + } + /** * Sets if the application is headless and should not instantiate AWT. Defaults to * {@code true} to prevent java icons appearing. diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultSpringApplicationBannerPrinterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultSpringApplicationBannerPrinterTests.java new file mode 100644 index 000000000000..06b64b4d9614 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultSpringApplicationBannerPrinterTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed 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 org.springframework.boot; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.Banner.Mode; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultSpringApplicationBannerPrinter}. + * + * @author Moritz Halbritter + * @author Junhyung Park + */ +@ExtendWith(OutputCaptureExtension.class) +class DefaultSpringApplicationBannerPrinterTests { + + @Test + void shouldUseUtf8(CapturedOutput capturedOutput) { + ResourceLoader resourceLoader = new GenericApplicationContext(); + Resource resource = resourceLoader.getResource("classpath:/banner-utf8.txt"); + SpringApplicationBannerPrinter printer = new DefaultSpringApplicationBannerPrinter(); + printer.print(new MockEnvironment(), DefaultSpringApplicationBannerPrinterTests.class, Mode.LOG, + new ResourceBanner(resource)); + assertThat(capturedOutput).containsIgnoringNewLines("\uD83D\uDE0D Spring Boot! \uD83D\uDE0D"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationBannerPrinterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationBannerPrinterTests.java index 50250c8c0c00..b6df8604e53b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationBannerPrinterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationBannerPrinterTests.java @@ -16,27 +16,40 @@ package org.springframework.boot; -import org.apache.commons.logging.Log; +import java.io.PrintStream; + +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.mock.env.MockEnvironment; +import org.springframework.boot.Banner.Mode; +import org.springframework.boot.BannerTests.Config; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.Environment; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mock; /** * Tests for {@link SpringApplicationBannerPrinter}. * * @author Moritz Halbritter + * @author Junhyung Park */ -class SpringApplicationBannerPrinterTests { +@ExtendWith(OutputCaptureExtension.class) +public class SpringApplicationBannerPrinterTests { + + private ConfigurableApplicationContext context; + + @AfterEach + void cleanUp() { + if (this.context != null) { + this.context.close(); + } + } @Test void shouldRegisterRuntimeHints() { @@ -47,16 +60,48 @@ void shouldRegisterRuntimeHints() { } @Test - void shouldUseUtf8() { - ResourceLoader resourceLoader = new GenericApplicationContext(); - Resource resource = resourceLoader.getResource("classpath:/banner-utf8.txt"); - SpringApplicationBannerPrinter printer = new SpringApplicationBannerPrinter(resourceLoader, - new ResourceBanner(resource)); - Log log = mock(Log.class); - printer.print(new MockEnvironment(), SpringApplicationBannerPrinterTests.class, log); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - then(log).should().info(captor.capture()); - assertThat(captor.getValue()).isEqualToIgnoringNewLines("\uD83D\uDE0D Spring Boot! \uD83D\uDE0D"); + void shouldPrintWithCustomPrinter(CapturedOutput capturedOutput) { + SpringApplication application = createSpringApplicationWithBannerModeLog(); + this.context = application.run(); + assertThat(capturedOutput).contains(DefaultSpringApplicationBannerPrinter.class.getSimpleName()); + } + + @Test + void shouldPrintWithDefaultPrinter(CapturedOutput capturedOutput) { + SpringApplication application = createSpringApplicationWithBannerModeLog(); + application.setBannerPrinter(new CustomSpringApplicationBannerPrinter()); + this.context = application.run(); + assertThat(capturedOutput).doesNotContain(DefaultSpringApplicationBannerPrinter.class.getSimpleName()); + } + + private SpringApplication createSpringApplicationWithBannerModeLog() { + SpringApplication application = new SpringApplication(Config.class); + application.setBannerMode(Mode.LOG); + application.setBanner(new DummyBanner()); + application.setWebApplicationType(WebApplicationType.NONE); + return application; + } + + static class CustomSpringApplicationBannerPrinter implements SpringApplicationBannerPrinter { + + @Override + public Banner print(Environment environment, Class sourceClass, Mode bannerMode, Banner banner) { + if (banner == null) { + return null; + } + banner.printBanner(environment, sourceClass, System.out); + return banner; + } + + } + + static class DummyBanner implements Banner { + + @Override + public void printBanner(Environment environment, Class sourceClass, PrintStream out) { + out.println("My Banner"); + } + } }