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
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

View File

@ -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();

View File

@ -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 {

View File

@ -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

View File

@ -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();

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.ConfigFileApplicationListener,\
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;
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<String, Object> map = new HashMap<String, Object>();
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<String, Object> map = new HashMap<String, Object>();
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<String, Object> map = new HashMap<String, Object>();
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<String, Object> map = new HashMap<String, Object>();
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<String, Object> map = new HashMap<String, Object>();
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);

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");
}
}
}