From 5141045f6f64b1ec265aeba2b9bdb647f655fc21 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 9 Aug 2024 15:00:05 +0200 Subject: [PATCH] Add properties class to bind to in SpringApplication Closes gh-40592 --- .../boot/ApplicationProperties.java | 152 ++++++++++++++++++ .../boot/SpringApplication.java | 84 ++++------ .../boot/SpringApplicationTests.java | 28 ++-- .../SpringBootServletInitializerTests.java | 4 +- 4 files changed, 203 insertions(+), 65 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationProperties.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationProperties.java new file mode 100644 index 00000000000..d51696bc2fe --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationProperties.java @@ -0,0 +1,152 @@ +/* + * 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.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.Banner.Mode; + +/** + * Spring application properties. + * + * @author Moritz Halbritter + */ +class ApplicationProperties { + + /** + * Whether bean definition overriding, by registering a definition with the same name + * as an existing definition, is allowed. + */ + private boolean allowBeanDefinitionOverriding; + + /** + * Whether to allow circular references between beans and automatically try to resolve + * them. + */ + private boolean allowCircularReferences; + + /** + * Mode used to display the banner when the application runs. + */ + private Banner.Mode bannerMode = Banner.Mode.CONSOLE; + + /** + * Whether to keep the application alive even if there are no more non-daemon threads. + */ + private boolean keepAlive; + + /** + * Whether initialization should be performed lazily. + */ + private boolean lazyInitialization = false; + + /** + * Whether to log information about the application when it starts. + */ + private boolean logStartupInfo = true; + + /** + * Whether the application should have a shutdown hook registered. + */ + private boolean registerShutdownHook = true; + + /** + * Sources (class names, package names, or XML resource locations) to include in the + * ApplicationContext. + */ + private Set sources = new LinkedHashSet<>(); + + /** + * Flag to explicitly request a specific type of web application. If not set, + * auto-detected based on the classpath. + */ + private WebApplicationType webApplicationType; + + boolean isAllowBeanDefinitionOverriding() { + return this.allowBeanDefinitionOverriding; + } + + void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) { + this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding; + } + + boolean isAllowCircularReferences() { + return this.allowCircularReferences; + } + + void setAllowCircularReferences(boolean allowCircularReferences) { + this.allowCircularReferences = allowCircularReferences; + } + + Mode getBannerMode() { + return this.bannerMode; + } + + void setBannerMode(Mode bannerMode) { + this.bannerMode = bannerMode; + } + + boolean isKeepAlive() { + return this.keepAlive; + } + + void setKeepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; + } + + boolean isLazyInitialization() { + return this.lazyInitialization; + } + + void setLazyInitialization(boolean lazyInitialization) { + this.lazyInitialization = lazyInitialization; + } + + boolean isLogStartupInfo() { + return this.logStartupInfo; + } + + void setLogStartupInfo(boolean logStartupInfo) { + this.logStartupInfo = logStartupInfo; + } + + boolean isRegisterShutdownHook() { + return this.registerShutdownHook; + } + + void setRegisterShutdownHook(boolean registerShutdownHook) { + this.registerShutdownHook = registerShutdownHook; + } + + Set getSources() { + return this.sources; + } + + void setSources(Set sources) { + this.sources = new LinkedHashSet<>(sources); + } + + WebApplicationType getWebApplicationType() { + return this.webApplicationType; + } + + void setWebApplicationType(WebApplicationType webApplicationType) { + this.webApplicationType = webApplicationType; + } + +} 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 c418c27c19c..bce52cbc427 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 @@ -208,14 +208,8 @@ public class SpringApplication { private final Set> primarySources; - private Set sources = new LinkedHashSet<>(); - private Class mainApplicationClass; - private Banner.Mode bannerMode = Banner.Mode.CONSOLE; - - private boolean logStartupInfo = true; - private boolean addCommandLineProperties = true; private boolean addConversionService = true; @@ -228,12 +222,8 @@ public class SpringApplication { private ConfigurableEnvironment environment; - private WebApplicationType webApplicationType; - private boolean headless = true; - private boolean registerShutdownHook = true; - private List> initializers; private List> listeners; @@ -244,21 +234,15 @@ public class SpringApplication { private Set additionalProfiles = Collections.emptySet(); - private boolean allowBeanDefinitionOverriding; - - private boolean allowCircularReferences; - private boolean isCustomEnvironment = false; - private boolean lazyInitialization = false; - private String environmentPrefix; private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT; private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; - private boolean keepAlive; + final ApplicationProperties properties = new ApplicationProperties(); /** * Create a new {@link SpringApplication} instance. The application context will load @@ -289,7 +273,7 @@ public class SpringApplication { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); - this.webApplicationType = WebApplicationType.deduceFromClasspath(); + this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath()); this.bootstrapRegistryInitializers = new ArrayList<>( getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); @@ -317,7 +301,7 @@ public class SpringApplication { */ public ConfigurableApplicationContext run(String... args) { Startup startup = Startup.create(); - if (this.registerShutdownHook) { + if (this.properties.isRegisterShutdownHook()) { SpringApplication.shutdownHook.enableShutdownHookAddition(); } DefaultBootstrapContext bootstrapContext = createBootstrapContext(); @@ -335,7 +319,7 @@ public class SpringApplication { refreshContext(context); afterRefresh(context, applicationArguments); startup.started(); - if (this.logStartupInfo) { + if (this.properties.isLogStartupInfo()) { new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup); } listeners.started(context, startup.timeTakenToStarted()); @@ -383,9 +367,10 @@ public class SpringApplication { private Class deduceEnvironmentClass() { Class environmentType = this.applicationContextFactory - .getEnvironmentType(this.webApplicationType); + .getEnvironmentType(this.properties.getWebApplicationType()); if (environmentType == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) { - environmentType = ApplicationContextFactory.DEFAULT.getEnvironmentType(this.webApplicationType); + environmentType = ApplicationContextFactory.DEFAULT + .getEnvironmentType(this.properties.getWebApplicationType()); } if (environmentType == null) { return ApplicationEnvironment.class; @@ -402,7 +387,7 @@ public class SpringApplication { applyInitializers(context); listeners.contextPrepared(context); bootstrapContext.close(context); - if (this.logStartupInfo) { + if (this.properties.isLogStartupInfo()) { logStartupInfo(context.getParent() == null); logStartupInfo(context); logStartupProfileInfo(context); @@ -414,15 +399,15 @@ public class SpringApplication { beanFactory.registerSingleton("springBootBanner", printedBanner); } if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) { - autowireCapableBeanFactory.setAllowCircularReferences(this.allowCircularReferences); + autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences()); if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) { - listableBeanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); + listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding()); } } - if (this.lazyInitialization) { + if (this.properties.isLazyInitialization()) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } - if (this.keepAlive) { + if (this.properties.isKeepAlive()) { context.addApplicationListener(new KeepAlive()); } context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context)); @@ -452,7 +437,7 @@ public class SpringApplication { } private void refreshContext(ConfigurableApplicationContext context) { - if (this.registerShutdownHook) { + if (this.properties.isRegisterShutdownHook()) { shutdownHook.registerApplicationContext(context); } refresh(context); @@ -489,9 +474,10 @@ public class SpringApplication { if (this.environment != null) { return this.environment; } - ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(this.webApplicationType); + ConfigurableEnvironment environment = this.applicationContextFactory + .createEnvironment(this.properties.getWebApplicationType()); if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) { - environment = ApplicationContextFactory.DEFAULT.createEnvironment(this.webApplicationType); + environment = ApplicationContextFactory.DEFAULT.createEnvironment(this.properties.getWebApplicationType()); } return (environment != null) ? environment : new ApplicationEnvironment(); } @@ -556,12 +542,12 @@ public class SpringApplication { } /** - * Bind the environment to the {@link SpringApplication}. + * Bind the environment to the {@link ApplicationProperties}. * @param environment the environment to bind */ protected void bindToSpringApplication(ConfigurableEnvironment environment) { try { - Binder.get(environment).bind("spring.main", Bindable.ofInstance(this)); + Binder.get(environment).bind("spring.main", Bindable.ofInstance(this.properties)); } catch (Exception ex) { throw new IllegalStateException("Cannot bind to SpringApplication", ex); @@ -569,13 +555,13 @@ public class SpringApplication { } private Banner printBanner(ConfigurableEnvironment environment) { - if (this.bannerMode == Banner.Mode.OFF) { + if (this.properties.getBannerMode() == Banner.Mode.OFF) { return null; } ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader : new DefaultResourceLoader(null); SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner); - if (this.bannerMode == Mode.LOG) { + if (this.properties.getBannerMode() == Mode.LOG) { return bannerPrinter.print(environment, this.mainApplicationClass, logger); } return bannerPrinter.print(environment, this.mainApplicationClass, System.out); @@ -589,7 +575,7 @@ public class SpringApplication { * @see #setApplicationContextFactory(ApplicationContextFactory) */ protected ConfigurableApplicationContext createApplicationContext() { - return this.applicationContextFactory.create(this.webApplicationType); + return this.applicationContextFactory.create(this.properties.getWebApplicationType()); } /** @@ -967,7 +953,7 @@ public class SpringApplication { * @since 2.0.0 */ public WebApplicationType getWebApplicationType() { - return this.webApplicationType; + return this.properties.getWebApplicationType(); } /** @@ -978,7 +964,7 @@ public class SpringApplication { */ public void setWebApplicationType(WebApplicationType webApplicationType) { Assert.notNull(webApplicationType, "WebApplicationType must not be null"); - this.webApplicationType = webApplicationType; + this.properties.setWebApplicationType(webApplicationType); } /** @@ -989,7 +975,7 @@ public class SpringApplication { * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding(boolean) */ public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) { - this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding; + this.properties.setAllowBeanDefinitionOverriding(allowBeanDefinitionOverriding); } /** @@ -1000,7 +986,7 @@ public class SpringApplication { * @see AbstractAutowireCapableBeanFactory#setAllowCircularReferences(boolean) */ public void setAllowCircularReferences(boolean allowCircularReferences) { - this.allowCircularReferences = allowCircularReferences; + this.properties.setAllowCircularReferences(allowCircularReferences); } /** @@ -1010,7 +996,7 @@ public class SpringApplication { * @see BeanDefinition#setLazyInit(boolean) */ public void setLazyInitialization(boolean lazyInitialization) { - this.lazyInitialization = lazyInitialization; + this.properties.setLazyInitialization(lazyInitialization); } /** @@ -1030,7 +1016,7 @@ public class SpringApplication { * @see #getShutdownHandlers() */ public void setRegisterShutdownHook(boolean registerShutdownHook) { - this.registerShutdownHook = registerShutdownHook; + this.properties.setRegisterShutdownHook(registerShutdownHook); } /** @@ -1048,7 +1034,7 @@ public class SpringApplication { * @param bannerMode the mode used to display the banner */ public void setBannerMode(Banner.Mode bannerMode) { - this.bannerMode = bannerMode; + this.properties.setBannerMode(bannerMode); } /** @@ -1057,7 +1043,7 @@ public class SpringApplication { * @param logStartupInfo if startup info should be logged. */ public void setLogStartupInfo(boolean logStartupInfo) { - this.logStartupInfo = logStartupInfo; + this.properties.setLogStartupInfo(logStartupInfo); } /** @@ -1173,7 +1159,7 @@ public class SpringApplication { * @see #getAllSources() */ public Set getSources() { - return this.sources; + return this.properties.getSources(); } /** @@ -1188,7 +1174,7 @@ public class SpringApplication { */ public void setSources(Set sources) { Assert.notNull(sources, "Sources must not be null"); - this.sources = new LinkedHashSet<>(sources); + this.properties.setSources(sources); } /** @@ -1203,8 +1189,8 @@ public class SpringApplication { if (!CollectionUtils.isEmpty(this.primarySources)) { allSources.addAll(this.primarySources); } - if (!CollectionUtils.isEmpty(this.sources)) { - allSources.addAll(this.sources); + if (!CollectionUtils.isEmpty(this.properties.getSources())) { + allSources.addAll(this.properties.getSources()); } return Collections.unmodifiableSet(allSources); } @@ -1333,7 +1319,7 @@ public class SpringApplication { * @since 3.2.0 */ public boolean isKeepAlive() { - return this.keepAlive; + return this.properties.isKeepAlive(); } /** @@ -1344,7 +1330,7 @@ public class SpringApplication { * @since 3.2.0 */ public void setKeepAlive(boolean keepAlive) { - this.keepAlive = keepAlive; + this.properties.setKeepAlive(keepAlive); } /** diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 0bce832b570..1ac19f48010 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -56,6 +56,7 @@ import org.springframework.beans.factory.support.BeanDefinitionOverrideException import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.boot.Banner.Mode; import org.springframework.boot.BootstrapRegistry.InstanceSupplier; import org.springframework.boot.SpringApplication.SpringApplicationRuntimeHints; import org.springframework.boot.availability.AvailabilityChangeEvent; @@ -119,6 +120,7 @@ import org.springframework.core.metrics.StartupStep; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -140,7 +142,6 @@ import static org.mockito.ArgumentMatchers.isA; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.never; @@ -284,7 +285,7 @@ class SpringApplicationTests { SpringApplication application = spy(new SpringApplication(ExampleConfig.class)); application.setWebApplicationType(WebApplicationType.NONE); this.context = application.run("--spring.main.banner-mode=log"); - then(application).should(atLeastOnce()).setBannerMode(Banner.Mode.LOG); + assertThatBannerModeIs(application, Banner.Mode.LOG); assertThat(output).contains("o.s.b.SpringApplication"); } @@ -293,7 +294,7 @@ class SpringApplicationTests { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); this.context = application.run("--spring.config.name=bindtoapplication"); - assertThat(application).hasFieldOrPropertyWithValue("bannerMode", Banner.Mode.OFF); + assertThatBannerModeIs(application, Mode.OFF); } @Test @@ -302,7 +303,7 @@ class SpringApplicationTests { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); this.context = application.run(); - assertThat(application).hasFieldOrPropertyWithValue("bannerMode", Banner.Mode.OFF); + assertThatBannerModeIs(application, Mode.OFF); } @Test @@ -311,7 +312,7 @@ class SpringApplicationTests { application.setDefaultProperties(Collections.singletonMap("spring.main.banner-mode", false)); application.setWebApplicationType(WebApplicationType.NONE); this.context = application.run(); - assertThat(application).hasFieldOrPropertyWithValue("bannerMode", Banner.Mode.OFF); + assertThatBannerModeIs(application, Mode.OFF); } @Test @@ -319,7 +320,7 @@ class SpringApplicationTests { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); this.context = application.run("--spring.main.banner-mode=false"); - assertThat(application).hasFieldOrPropertyWithValue("bannerMode", Banner.Mode.OFF); + assertThatBannerModeIs(application, Mode.OFF); } @Test @@ -1554,6 +1555,11 @@ class SpringApplicationTests { return Thread.getAllStackTraces().keySet(); } + private void assertThatBannerModeIs(SpringApplication application, Mode mode) { + Object properties = ReflectionTestUtils.getField(application, "properties"); + assertThat(properties).hasFieldOrPropertyWithValue("bannerMode", mode); + } + static class TestEventListener implements SmartApplicationListener { private final Class eventType; @@ -1613,8 +1619,6 @@ class SpringApplicationTests { private boolean useMockLoader; - private Banner.Mode bannerMode; - TestSpringApplication(Class... primarySources) { super(primarySources); } @@ -1642,14 +1646,8 @@ class SpringApplicationTests { return this.loader; } - @Override - public void setBannerMode(Banner.Mode bannerMode) { - super.setBannerMode(bannerMode); - this.bannerMode = bannerMode; - } - Banner.Mode getBannerMode() { - return this.bannerMode; + return this.properties.getBannerMode(); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java index bb8e7deb81b..f7704330341 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializerTests.java @@ -52,6 +52,7 @@ import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.env.PropertySource; import org.springframework.mock.web.MockServletContext; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.StandardServletEnvironment; @@ -119,7 +120,8 @@ class SpringBootServletInitializerTests { new WithConfigurationAnnotation().onStartup(this.servletContext); assertThat(this.servletContext.getAttribute(LoggingApplicationListener.REGISTER_SHUTDOWN_HOOK_PROPERTY)) .isEqualTo(false); - assertThat(this.application).hasFieldOrPropertyWithValue("registerShutdownHook", false); + Object properties = ReflectionTestUtils.getField(this.application, "properties"); + assertThat(properties).hasFieldOrPropertyWithValue("registerShutdownHook", false); } @Test