Add declarative ApplicationListener

This commit is contained in:
Dave Syer 2014-01-07 17:52:37 +00:00
parent 73c2216732
commit 633dea9d80
9 changed files with 266 additions and 39 deletions

View File

@ -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 load `application.properties` as described above. If YAML is used then
files with the ".yml" extension are also added to the list by default. 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 ## 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 `application-${profile}.properties` to specify profile-specific
values. 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 ## Build An Executable Archive with Ant
To build with Ant you need to grab dependencies and compile and then To build with Ant you need to grab dependencies and compile and then

View File

@ -18,6 +18,8 @@ package org.springframework.boot.actuate.autoconfigure;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationBeforeRefreshEvent;
import org.springframework.boot.TestUtils; import org.springframework.boot.TestUtils;
import org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer; import org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
@ -171,7 +173,8 @@ public class ManagementSecurityAutoConfigurationTests {
AnnotationConfigWebApplicationContext context) { AnnotationConfigWebApplicationContext context) {
TestUtils.addEnviroment(context, "debug:true"); TestUtils.addEnviroment(context, "debug:true");
LoggingApplicationListener logging = new LoggingApplicationListener(); LoggingApplicationListener logging = new LoggingApplicationListener();
logging.initialize(context); logging.onApplicationEvent(new SpringApplicationBeforeRefreshEvent(
new SpringApplication(), context, new String[0]));
AutoConfigurationReportLoggingInitializer initializer = new AutoConfigurationReportLoggingInitializer(); AutoConfigurationReportLoggingInitializer initializer = new AutoConfigurationReportLoggingInitializer();
initializer.initialize(context); initializer.initialize(context);
context.refresh(); context.refresh();

View File

@ -23,10 +23,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.web.DefaultErrorViewIntegrationTests.TestConfiguration; import org.springframework.boot.actuate.web.DefaultErrorViewIntegrationTests.TestConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.context.annotation.Configuration;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@ -41,7 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
/** /**
* @author Dave Syer * @author Dave Syer
*/ */
@ContextConfiguration(classes = TestConfiguration.class, initializers = { LoggingApplicationListener.class }) @SpringApplicationConfiguration(classes = TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration @WebAppConfiguration
public class DefaultErrorViewIntegrationTests { public class DefaultErrorViewIntegrationTests {

View File

@ -20,13 +20,11 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringJUnitTests.TestConfiguration; 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.ApplicationContext;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -36,7 +34,7 @@ import static org.junit.Assert.assertNotNull;
* @author Dave Syer * @author Dave Syer
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestConfiguration.class, initializers = ConfigFileApplicationListener.class) @SpringApplicationConfiguration(classes = TestConfiguration.class)
public class SpringJUnitTests { public class SpringJUnitTests {
@Autowired @Autowired

View File

@ -17,6 +17,8 @@
package org.springframework.boot.autoconfigure.security; package org.springframework.boot.autoconfigure.security;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationBeforeRefreshEvent;
import org.springframework.boot.TestUtils; import org.springframework.boot.TestUtils;
import org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer; import org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer;
import org.springframework.boot.autoconfigure.ComponentScanDetector; import org.springframework.boot.autoconfigure.ComponentScanDetector;
@ -118,7 +120,8 @@ public class SecurityAutoConfigurationTests {
AnnotationConfigWebApplicationContext context) { AnnotationConfigWebApplicationContext context) {
TestUtils.addEnviroment(context, "debug:true"); TestUtils.addEnviroment(context, "debug:true");
LoggingApplicationListener logging = new LoggingApplicationListener(); LoggingApplicationListener logging = new LoggingApplicationListener();
logging.initialize(context); logging.onApplicationEvent(new SpringApplicationBeforeRefreshEvent(
new SpringApplication(), context, new String[0]));
AutoConfigurationReportLoggingInitializer initializer = new AutoConfigurationReportLoggingInitializer(); AutoConfigurationReportLoggingInitializer initializer = new AutoConfigurationReportLoggingInitializer();
initializer.initialize(context); initializer.initialize(context);
context.refresh(); context.refresh();

View File

@ -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<ApplicationEvent>, 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<ApplicationListener<ApplicationEvent>> delegates = getListeners(((SpringApplicationEnvironmentAvailableEvent) event)
.getEnvironment());
if (delegates.isEmpty()) {
return;
}
this.multicaster = new SimpleApplicationEventMulticaster();
for (ApplicationListener<ApplicationEvent> listener : delegates) {
this.multicaster.addApplicationListener(listener);
}
}
if (this.multicaster != null) {
this.multicaster.multicastEvent(event);
}
}
@SuppressWarnings("unchecked")
private List<ApplicationListener<ApplicationEvent>> getListeners(
ConfigurableEnvironment env) {
String classNames = env.getProperty(PROPERTY_NAME);
List<ApplicationListener<ApplicationEvent>> listeners = new ArrayList<ApplicationListener<ApplicationEvent>>();
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<ApplicationEvent>) 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;
}
}

View File

@ -9,4 +9,5 @@ org.springframework.boot.context.listener.FileEncodingApplicationListener,\
org.springframework.boot.context.listener.VcapApplicationListener,\ org.springframework.boot.context.listener.VcapApplicationListener,\
org.springframework.boot.context.listener.ConfigFileApplicationListener,\ org.springframework.boot.context.listener.ConfigFileApplicationListener,\
org.springframework.boot.context.listener.LoggingApplicationListener,\ org.springframework.boot.context.listener.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorInitializer org.springframework.boot.liquibase.LiquibaseServiceLocatorInitializer,\
org.springframework.boot.context.listener.EnvironmentDelegateApplicationListener

View File

@ -16,21 +16,16 @@
package org.springframework.boot.context.initializer; package org.springframework.boot.context.initializer;
import java.util.HashMap;
import java.util.Map;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; 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.ApplicationContextException;
import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.StaticApplicationContext; import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; 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 org.springframework.web.context.ConfigurableWebApplicationContext;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
@ -41,7 +36,7 @@ import static org.junit.Assert.assertThat;
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
public class EnvironmentDelegateApplicationContextInitializerTest { public class EnvironmentDelegateApplicationContextInitializerTests {
@Rule @Rule
public ExpectedException thrown = ExpectedException.none(); public ExpectedException thrown = ExpectedException.none();
@ -51,11 +46,9 @@ public class EnvironmentDelegateApplicationContextInitializerTest {
@Test @Test
public void orderedInitialize() throws Exception { public void orderedInitialize() throws Exception {
StaticApplicationContext context = new StaticApplicationContext(); StaticApplicationContext context = new StaticApplicationContext();
Map<String, Object> map = new HashMap<String, Object>(); TestUtils.addEnviroment(context,
map.put("context.initializer.classes", MockInitB.class.getName() + "," "context.initializer.classes:" + MockInitB.class.getName() + ","
+ MockInitA.class.getName()); + MockInitA.class.getName());
PropertySource<?> propertySource = new MapPropertySource("map", map);
context.getEnvironment().getPropertySources().addFirst(propertySource);
this.initializer.initialize(context); this.initializer.initialize(context);
assertThat(context.getBeanFactory().getSingleton("a"), equalTo((Object) "a")); assertThat(context.getBeanFactory().getSingleton("a"), equalTo((Object) "a"));
assertThat(context.getBeanFactory().getSingleton("b"), equalTo((Object) "b")); assertThat(context.getBeanFactory().getSingleton("b"), equalTo((Object) "b"));
@ -70,20 +63,15 @@ public class EnvironmentDelegateApplicationContextInitializerTest {
@Test @Test
public void emptyInitializers() throws Exception { public void emptyInitializers() throws Exception {
StaticApplicationContext context = new StaticApplicationContext(); StaticApplicationContext context = new StaticApplicationContext();
Map<String, Object> map = new HashMap<String, Object>(); TestUtils.addEnviroment(context, "context.initializer.classes:");
map.put("context.initializer.classes", "");
PropertySource<?> propertySource = new MapPropertySource("map", map);
context.getEnvironment().getPropertySources().addFirst(propertySource);
this.initializer.initialize(context); this.initializer.initialize(context);
} }
@Test @Test
public void noSuchInitializerClass() throws Exception { public void noSuchInitializerClass() throws Exception {
StaticApplicationContext context = new StaticApplicationContext(); StaticApplicationContext context = new StaticApplicationContext();
Map<String, Object> map = new HashMap<String, Object>(); TestUtils.addEnviroment(context,
map.put("context.initializer.classes", "missing.madeup.class"); "context.initializer.classes:missing.madeup.class");
PropertySource<?> propertySource = new MapPropertySource("map", map);
context.getEnvironment().getPropertySources().addFirst(propertySource);
this.thrown.expect(ApplicationContextException.class); this.thrown.expect(ApplicationContextException.class);
this.initializer.initialize(context); this.initializer.initialize(context);
} }
@ -91,10 +79,8 @@ public class EnvironmentDelegateApplicationContextInitializerTest {
@Test @Test
public void notAnInitializerClass() throws Exception { public void notAnInitializerClass() throws Exception {
StaticApplicationContext context = new StaticApplicationContext(); StaticApplicationContext context = new StaticApplicationContext();
Map<String, Object> map = new HashMap<String, Object>(); TestUtils.addEnviroment(context,
map.put("context.initializer.classes", Object.class.getName()); "context.initializer.classes:" + Object.class.getName());
PropertySource<?> propertySource = new MapPropertySource("map", map);
context.getEnvironment().getPropertySources().addFirst(propertySource);
this.thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
this.initializer.initialize(context); this.initializer.initialize(context);
} }
@ -102,10 +88,8 @@ public class EnvironmentDelegateApplicationContextInitializerTest {
@Test @Test
public void genericNotSuitable() throws Exception { public void genericNotSuitable() throws Exception {
StaticApplicationContext context = new StaticApplicationContext(); StaticApplicationContext context = new StaticApplicationContext();
Map<String, Object> map = new HashMap<String, Object>(); TestUtils.addEnviroment(context, "context.initializer.classes:"
map.put("context.initializer.classes", NotSuitableInit.class.getName()); + NotSuitableInit.class.getName());
PropertySource<?> propertySource = new MapPropertySource("map", map);
context.getEnvironment().getPropertySources().addFirst(propertySource);
this.thrown.expect(IllegalArgumentException.class); this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("generic parameter"); this.thrown.expectMessage("generic parameter");
this.initializer.initialize(context); this.initializer.initialize(context);

View File

@ -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<ContextRefreshedEvent> {
@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<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) event
.getApplicationContext();
assertThat(applicationContext.getBeanFactory().getSingleton("a"),
equalTo((Object) "a"));
applicationContext.getBeanFactory().registerSingleton("b", "b");
}
}
}