From 633dea9d801d5a468f2d41a68fb7c6e00acb4ee5 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 7 Jan 2014 17:52:37 +0000 Subject: [PATCH] Add declarative ApplicationListener --- docs/howto.md | 37 ++++++- ...agementSecurityAutoConfigurationTests.java | 5 +- .../web/DefaultErrorViewIntegrationTests.java | 5 +- .../boot/autoconfigure/SpringJUnitTests.java | 6 +- .../SecurityAutoConfigurationTests.java | 5 +- ...nvironmentDelegateApplicationListener.java | 104 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- ...teApplicationContextInitializerTests.java} | 40 ++----- ...nmentDelegateApplicationListenerTests.java | 100 +++++++++++++++++ 9 files changed, 266 insertions(+), 39 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/listener/EnvironmentDelegateApplicationListener.java rename spring-boot/src/test/java/org/springframework/boot/context/initializer/{EnvironmentDelegateApplicationContextInitializerTest.java => EnvironmentDelegateApplicationContextInitializerTests.java} (70%) create mode 100644 spring-boot/src/test/java/org/springframework/boot/context/listener/EnvironmentDelegateApplicationListenerTests.java diff --git a/docs/howto.md b/docs/howto.md index 0de4953d25..c1bd28ef94 100644 --- a/docs/howto.md +++ b/docs/howto.md @@ -823,7 +823,7 @@ No matter what you set in the environment, Spring Boot will always load `application.properties` as described above. If YAML is used then files with the ".yml" extension are also added to the list by default. -See `ConfigFileApplicationContextInitializer` for more detail. +See `ConfigFileApplicationListener` for more detail. ## Use YAML for External Properties @@ -899,6 +899,41 @@ To do the same thing with properties files you can use `application-${profile}.properties` to specify profile-specific values. +## Customize the Environment or ApplicationContext Before it Starts + +A `SpringApplication` has `ApplicationListeners` and +`ApplicationContextInitializers` that are used to apply customizations +to the context or environment. Spring Boot loads a number of such +customizations for use internally from +`META-INF/spring.factories`. There is more than one way to register +additional ones: + +* programmatically per application by calling the `addListeners` and + `addInitializers` methods on `SpringApplication` before you run it +* declaratively per application by setting + `context.initializer.classes` or `context.listener.classes` +* declarative for all applications by adding a + `MTEA-INF/spring.factories` and packaging a jar file that the + applications all use as a library + +Any `ApplicationContextInitializer` registered programmatically or via +`spring.factories` that is also an `ApplicationListener` will be +automatically cross registered (and vice versa for listeners that are +also initializers). The `SpringApplication` sends some special +`ApplicationEvents` to the listeners (even some before the context is +created), and then registers the listeners for events published by the +`ApplicationContext` as well: + +* `SpringApplicationStartEvent` at the start of a run, but before any + processing except the registration of listeners and initializers. +* `SpringApplicationEnvironmentAvailableEvent` when the `Environment` + to be used in the context is known, but before the context is + created. +* `SpringApplicationBeforeRefreshEvent` just before the refresh is + started, but after bean definitions have been loaded. +* `SpringApplicationErrorEvent` if there is an exception on startup. + + ## Build An Executable Archive with Ant To build with Ant you need to grab dependencies and compile and then diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java index 3597a9f42e..79f9294dff 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java @@ -18,6 +18,8 @@ package org.springframework.boot.actuate.autoconfigure; import org.junit.After; import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringApplicationBeforeRefreshEvent; import org.springframework.boot.TestUtils; import org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; @@ -171,7 +173,8 @@ public class ManagementSecurityAutoConfigurationTests { AnnotationConfigWebApplicationContext context) { TestUtils.addEnviroment(context, "debug:true"); LoggingApplicationListener logging = new LoggingApplicationListener(); - logging.initialize(context); + logging.onApplicationEvent(new SpringApplicationBeforeRefreshEvent( + new SpringApplication(), context, new String[0])); AutoConfigurationReportLoggingInitializer initializer = new AutoConfigurationReportLoggingInitializer(); initializer.initialize(context); context.refresh(); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/DefaultErrorViewIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/DefaultErrorViewIntegrationTests.java index a0dd12aa9d..d41f633416 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/DefaultErrorViewIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/DefaultErrorViewIntegrationTests.java @@ -23,10 +23,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.web.DefaultErrorViewIntegrationTests.TestConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.context.listener.LoggingApplicationListener; +import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -41,7 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. /** * @author Dave Syer */ -@ContextConfiguration(classes = TestConfiguration.class, initializers = { LoggingApplicationListener.class }) +@SpringApplicationConfiguration(classes = TestConfiguration.class) @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration public class DefaultErrorViewIntegrationTests { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SpringJUnitTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SpringJUnitTests.java index c5c581538c..1b7def4d43 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SpringJUnitTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SpringJUnitTests.java @@ -20,13 +20,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.SpringJUnitTests.TestConfiguration; -import org.springframework.boot.context.listener.ConfigFileApplicationListener; +import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.assertEquals; @@ -36,7 +34,7 @@ import static org.junit.Assert.assertNotNull; * @author Dave Syer */ @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = TestConfiguration.class, initializers = ConfigFileApplicationListener.class) +@SpringApplicationConfiguration(classes = TestConfiguration.class) public class SpringJUnitTests { @Autowired diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java index d0adc5d929..9084632526 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java @@ -17,6 +17,8 @@ package org.springframework.boot.autoconfigure.security; import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringApplicationBeforeRefreshEvent; import org.springframework.boot.TestUtils; import org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer; import org.springframework.boot.autoconfigure.ComponentScanDetector; @@ -118,7 +120,8 @@ public class SecurityAutoConfigurationTests { AnnotationConfigWebApplicationContext context) { TestUtils.addEnviroment(context, "debug:true"); LoggingApplicationListener logging = new LoggingApplicationListener(); - logging.initialize(context); + logging.onApplicationEvent(new SpringApplicationBeforeRefreshEvent( + new SpringApplication(), context, new String[0])); AutoConfigurationReportLoggingInitializer initializer = new AutoConfigurationReportLoggingInitializer(); initializer.initialize(context); context.refresh(); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/listener/EnvironmentDelegateApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/context/listener/EnvironmentDelegateApplicationListener.java new file mode 100644 index 0000000000..3c6f1ed8f0 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/listener/EnvironmentDelegateApplicationListener.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2013 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 + * + * http://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.context.listener; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.BeanUtils; +import org.springframework.boot.SpringApplicationEnvironmentAvailableEvent; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ApplicationListener} that delegates to other listeners that are specified under + * a {@literal context.listener.classes} environment property. + * + * @author Dave Syer + * @author Phillip Webb + */ +public class EnvironmentDelegateApplicationListener implements + ApplicationListener, Ordered { + + // NOTE: Similar to org.springframework.web.context.ContextLoader + + private static final String PROPERTY_NAME = "context.listener.classes"; + + private int order = 0; + + private SimpleApplicationEventMulticaster multicaster; + + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof SpringApplicationEnvironmentAvailableEvent) { + List> delegates = getListeners(((SpringApplicationEnvironmentAvailableEvent) event) + .getEnvironment()); + if (delegates.isEmpty()) { + return; + } + this.multicaster = new SimpleApplicationEventMulticaster(); + for (ApplicationListener listener : delegates) { + this.multicaster.addApplicationListener(listener); + } + } + if (this.multicaster != null) { + this.multicaster.multicastEvent(event); + } + } + + @SuppressWarnings("unchecked") + private List> getListeners( + ConfigurableEnvironment env) { + String classNames = env.getProperty(PROPERTY_NAME); + List> listeners = new ArrayList>(); + if (StringUtils.hasLength(classNames)) { + for (String className : StringUtils.commaDelimitedListToSet(classNames)) { + try { + Class clazz = ClassUtils.forName(className, + ClassUtils.getDefaultClassLoader()); + Assert.isAssignable(ApplicationListener.class, clazz, "class [" + + className + "] must implement ApplicationListener"); + listeners.add((ApplicationListener) BeanUtils + .instantiateClass(clazz)); + } + catch (Exception ex) { + throw new ApplicationContextException( + "Failed to load context listener class [" + className + "]", + ex); + } + } + } + AnnotationAwareOrderComparator.sort(listeners); + return listeners; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } +} diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index a05fb79e12..5186b1c5dc 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -9,4 +9,5 @@ org.springframework.boot.context.listener.FileEncodingApplicationListener,\ org.springframework.boot.context.listener.VcapApplicationListener,\ org.springframework.boot.context.listener.ConfigFileApplicationListener,\ org.springframework.boot.context.listener.LoggingApplicationListener,\ -org.springframework.boot.liquibase.LiquibaseServiceLocatorInitializer +org.springframework.boot.liquibase.LiquibaseServiceLocatorInitializer,\ +org.springframework.boot.context.listener.EnvironmentDelegateApplicationListener diff --git a/spring-boot/src/test/java/org/springframework/boot/context/initializer/EnvironmentDelegateApplicationContextInitializerTest.java b/spring-boot/src/test/java/org/springframework/boot/context/initializer/EnvironmentDelegateApplicationContextInitializerTests.java similarity index 70% rename from spring-boot/src/test/java/org/springframework/boot/context/initializer/EnvironmentDelegateApplicationContextInitializerTest.java rename to spring-boot/src/test/java/org/springframework/boot/context/initializer/EnvironmentDelegateApplicationContextInitializerTests.java index e58b88a7d6..613af9476d 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/initializer/EnvironmentDelegateApplicationContextInitializerTest.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/initializer/EnvironmentDelegateApplicationContextInitializerTests.java @@ -16,21 +16,16 @@ package org.springframework.boot.context.initializer; -import java.util.HashMap; -import java.util.Map; - import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.boot.context.initializer.EnvironmentDelegateApplicationContextInitializer; +import org.springframework.boot.TestUtils; import org.springframework.context.ApplicationContextException; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.PropertySource; import org.springframework.web.context.ConfigurableWebApplicationContext; import static org.hamcrest.Matchers.equalTo; @@ -41,7 +36,7 @@ import static org.junit.Assert.assertThat; * * @author Phillip Webb */ -public class EnvironmentDelegateApplicationContextInitializerTest { +public class EnvironmentDelegateApplicationContextInitializerTests { @Rule public ExpectedException thrown = ExpectedException.none(); @@ -51,11 +46,9 @@ public class EnvironmentDelegateApplicationContextInitializerTest { @Test public void orderedInitialize() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); - Map map = new HashMap(); - map.put("context.initializer.classes", MockInitB.class.getName() + "," - + MockInitA.class.getName()); - PropertySource propertySource = new MapPropertySource("map", map); - context.getEnvironment().getPropertySources().addFirst(propertySource); + TestUtils.addEnviroment(context, + "context.initializer.classes:" + MockInitB.class.getName() + "," + + MockInitA.class.getName()); this.initializer.initialize(context); assertThat(context.getBeanFactory().getSingleton("a"), equalTo((Object) "a")); assertThat(context.getBeanFactory().getSingleton("b"), equalTo((Object) "b")); @@ -70,20 +63,15 @@ public class EnvironmentDelegateApplicationContextInitializerTest { @Test public void emptyInitializers() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); - Map map = new HashMap(); - map.put("context.initializer.classes", ""); - PropertySource propertySource = new MapPropertySource("map", map); - context.getEnvironment().getPropertySources().addFirst(propertySource); + TestUtils.addEnviroment(context, "context.initializer.classes:"); this.initializer.initialize(context); } @Test public void noSuchInitializerClass() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); - Map map = new HashMap(); - map.put("context.initializer.classes", "missing.madeup.class"); - PropertySource propertySource = new MapPropertySource("map", map); - context.getEnvironment().getPropertySources().addFirst(propertySource); + TestUtils.addEnviroment(context, + "context.initializer.classes:missing.madeup.class"); this.thrown.expect(ApplicationContextException.class); this.initializer.initialize(context); } @@ -91,10 +79,8 @@ public class EnvironmentDelegateApplicationContextInitializerTest { @Test public void notAnInitializerClass() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); - Map map = new HashMap(); - map.put("context.initializer.classes", Object.class.getName()); - PropertySource propertySource = new MapPropertySource("map", map); - context.getEnvironment().getPropertySources().addFirst(propertySource); + TestUtils.addEnviroment(context, + "context.initializer.classes:" + Object.class.getName()); this.thrown.expect(IllegalArgumentException.class); this.initializer.initialize(context); } @@ -102,10 +88,8 @@ public class EnvironmentDelegateApplicationContextInitializerTest { @Test public void genericNotSuitable() throws Exception { StaticApplicationContext context = new StaticApplicationContext(); - Map map = new HashMap(); - map.put("context.initializer.classes", NotSuitableInit.class.getName()); - PropertySource propertySource = new MapPropertySource("map", map); - context.getEnvironment().getPropertySources().addFirst(propertySource); + TestUtils.addEnviroment(context, "context.initializer.classes:" + + NotSuitableInit.class.getName()); this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("generic parameter"); this.initializer.initialize(context); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/listener/EnvironmentDelegateApplicationListenerTests.java b/spring-boot/src/test/java/org/springframework/boot/context/listener/EnvironmentDelegateApplicationListenerTests.java new file mode 100644 index 0000000000..74fe6be6f1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/listener/EnvironmentDelegateApplicationListenerTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2013 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 + * + * http://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.context.listener; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringApplicationEnvironmentAvailableEvent; +import org.springframework.boot.TestUtils; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +public class EnvironmentDelegateApplicationListenerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private EnvironmentDelegateApplicationListener listener = new EnvironmentDelegateApplicationListener(); + + private StaticApplicationContext context = new StaticApplicationContext(); + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void orderedInitialize() throws Exception { + TestUtils.addEnviroment(this.context, "context.listener.classes:" + + MockInitB.class.getName() + "," + MockInitA.class.getName()); + this.listener.onApplicationEvent(new SpringApplicationEnvironmentAvailableEvent( + new SpringApplication(), this.context.getEnvironment(), new String[0])); + this.context.getBeanFactory().registerSingleton("testListener", this.listener); + this.context.refresh(); + assertThat(this.context.getBeanFactory().getSingleton("a"), equalTo((Object) "a")); + assertThat(this.context.getBeanFactory().getSingleton("b"), equalTo((Object) "b")); + } + + @Test + public void noInitializers() throws Exception { + this.listener.onApplicationEvent(new SpringApplicationEnvironmentAvailableEvent( + new SpringApplication(), this.context.getEnvironment(), new String[0])); + } + + @Test + public void emptyInitializers() throws Exception { + TestUtils.addEnviroment(this.context, "context.listener.classes:"); + this.listener.onApplicationEvent(new SpringApplicationEnvironmentAvailableEvent( + new SpringApplication(), this.context.getEnvironment(), new String[0])); + } + + @Order(Ordered.HIGHEST_PRECEDENCE) + private static class MockInitA implements ApplicationListener { + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) event + .getApplicationContext(); + applicationContext.getBeanFactory().registerSingleton("a", "a"); + } + } + + @Order(Ordered.LOWEST_PRECEDENCE) + private static class MockInitB implements ApplicationListener { + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) event + .getApplicationContext(); + assertThat(applicationContext.getBeanFactory().getSingleton("a"), + equalTo((Object) "a")); + applicationContext.getBeanFactory().registerSingleton("b", "b"); + } + } + +}