Unverified Commit e7b9f0d6 by Jason Song Committed by GitHub

Merge pull request #937 from nobodyiam/apollo.bootstrap

Support apollo config injection in Spring Boot bootstrap phase
parents 39dc2979 cbb02247
......@@ -38,6 +38,12 @@
<artifactId>spring-context</artifactId>
<optional>true</optional>
</dependency>
<!-- optional spring boot dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<!-- test -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
......
package com.ctrip.framework.apollo.spring.boot;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySourcesProcessor;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED)
@ConditionalOnMissingBean(PropertySourcesProcessor.class)
public class ApolloAutoConfiguration {
@Bean
public ConfigPropertySourcesProcessor configPropertySourcesProcessor() {
return new ConfigPropertySourcesProcessor();
}
}
package com.ctrip.framework.apollo.spring.boot;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySource;
import com.google.common.base.Splitter;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* Inject the Apollo config in Spring Boot bootstrap phase
*
* <p>Configuration example:</p>
* <pre class="code">
* # will inject 'application' namespace in bootstrap phase
* apollo.bootstrap.enabled = true
* </pre>
*
* or
*
* <pre class="code">
* apollo.bootstrap.enabled = true
* # will inject 'application' and 'FX.apollo' namespaces in bootstrap phase
* apollo.bootstrap.namespaces = application,FX.apollo
* </pre>
*/
public class ApolloSpringApplicationRunListener implements SpringApplicationRunListener,
PriorityOrdered {
private static final Logger logger = LoggerFactory.getLogger(ApolloSpringApplicationRunListener.class);
private static final Splitter NAMESPACE_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();
public ApolloSpringApplicationRunListener(SpringApplication application, String[] args) {
//ignore
}
public void starting() {
}
public void started() {
}
public void environmentPrepared(ConfigurableEnvironment environment) {
}
public void contextPrepared(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
String enabled = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "false");
if (!Boolean.valueOf(enabled)) {
logger.debug("Apollo bootstrap config is not enabled for context {}, see property: ${{}}", context, PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
return;
}
logger.debug("Apollo bootstrap config is enabled for context {}", context, PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
//already initialized
return;
}
String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
logger.debug("Apollo bootstrap namespaces: {}", namespaces);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
for (String namespace : namespaceList) {
Config config = ConfigService.getConfig(namespace);
composite.addPropertySource(new ConfigPropertySource(namespace, config));
}
environment.getPropertySources().addFirst(composite);
}
public void contextLoaded(ConfigurableApplicationContext context) {
}
public void finished(ConfigurableApplicationContext configurableApplicationContext,
Throwable throwable) {
}
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
package com.ctrip.framework.apollo.spring.config;
public interface PropertySourcesConstants {
String APOLLO_PROPERTY_SOURCE_NAME = "ApolloPropertySources";
String APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME = "ApolloBootstrapPropertySources";
String APOLLO_BOOTSTRAP_ENABLED = "apollo.bootstrap.enabled";
String APOLLO_BOOTSTRAP_NAMESPACES = "apollo.bootstrap.namespaces";
}
......@@ -31,7 +31,6 @@ import java.util.Iterator;
* @author Jason Song(song_s@ctrip.com)
*/
public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered {
private static final String APOLLO_PROPERTY_SOURCE_NAME = "ApolloPropertySources";
private static final Multimap<Integer, String> NAMESPACE_NAMES = HashMultimap.create();
private ConfigurableEnvironment environment;
......@@ -46,11 +45,11 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
}
protected void initializePropertySources() {
if (environment.getPropertySources().contains(APOLLO_PROPERTY_SOURCE_NAME)) {
if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME)) {
//already initialized
return;
}
CompositePropertySource composite = new CompositePropertySource(APOLLO_PROPERTY_SOURCE_NAME);
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
//sort by order asc
ImmutableSortedSet<Integer> orders = ImmutableSortedSet.copyOf(NAMESPACE_NAMES.keySet());
......@@ -64,7 +63,15 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
composite.addPropertySource(new ConfigPropertySource(namespace, config));
}
}
environment.getPropertySources().addFirst(composite);
// add after the bootstrap property source or to the first
if (environment.getPropertySources()
.contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
environment.getPropertySources()
.addAfter(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, composite);
} else {
environment.getPropertySources().addFirst(composite);
}
}
@Override
......
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration
org.springframework.boot.SpringApplicationRunListener=\
com.ctrip.framework.apollo.spring.boot.ApolloSpringApplicationRunListener
package com.ctrip.framework.apollo;
import com.ctrip.framework.apollo.spring.BootstrapConfigTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
......@@ -35,7 +36,7 @@ import com.ctrip.framework.apollo.util.parser.DurationParserTest;
ConfigIntegrationTest.class, ExceptionUtilTest.class, XmlConfigFileTest.class, PropertiesConfigFileTest.class,
RemoteConfigLongPollServiceTest.class, DateParserTest.class, DurationParserTest.class, JsonConfigFileTest.class,
XmlConfigPlaceholderTest.class, JavaConfigPlaceholderTest.class, XMLConfigAnnotationTest.class,
JavaConfigAnnotationTest.class, ConfigUtilTest.class
JavaConfigAnnotationTest.class, ConfigUtilTest.class, BootstrapConfigTest.class
})
public class AllTests {
......
......@@ -37,6 +37,19 @@ public abstract class AbstractSpringIntegrationTest {
@Before
public void setUp() throws Exception {
doSetUp();
}
@After
public void tearDown() throws Exception {
doTearDown();
}
protected static void mockConfig(String namespace, Config config) {
CONFIG_REGISTRY.put(namespace, config);
}
protected static void doSetUp() {
//as PropertySourcesProcessor has some static states, so we must manually clear its state
ReflectionUtils.invokeMethod(PROPERTY_SOURCES_PROCESSOR_CLEAR, null);
//as ConfigService is singleton, so we must manually clear its container
......@@ -45,15 +58,10 @@ public abstract class AbstractSpringIntegrationTest {
MockInjector.setInstance(ConfigManager.class, new MockConfigManager());
}
@After
public void tearDown() throws Exception {
protected static void doTearDown() {
CONFIG_REGISTRY.clear();
}
protected void mockConfig(String namespace, Config config) {
CONFIG_REGISTRY.put(namespace, config);
}
public static class MockConfigManager implements ConfigManager {
@Override
......
package com.ctrip.framework.apollo.spring;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.google.common.collect.Sets;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Jason Song(song_s@ctrip.com)
*/
@RunWith(Enclosed.class)
public class BootstrapConfigTest {
private static final String TEST_BEAN_CONDITIONAL_ON_KEY = "apollo.test.testBean";
private static final String FX_APOLLO_NAMESPACE = "FX.apollo";
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(ConfigurationWithConditionalOnProperty.class)
@DirtiesContext
public static class TestWithBootstrapEnabledAndDefaultNamespacesAndConditionalOn extends
AbstractSpringIntegrationTest {
private static final String someProperty = "someProperty";
private static final String someValue = "someValue";
@Autowired(required = false)
private TestBean testBean;
@ApolloConfig
private Config config;
@Value("${" + someProperty + "}")
private String someInjectedValue;
private static Config mockedConfig;
@BeforeClass
public static void beforeClass() throws Exception {
doSetUp();
System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "true");
mockedConfig = mock(Config.class);
when(mockedConfig.getPropertyNames()).thenReturn(Sets.newHashSet(TEST_BEAN_CONDITIONAL_ON_KEY, someProperty));
when(mockedConfig.getProperty(eq(TEST_BEAN_CONDITIONAL_ON_KEY), anyString())).thenReturn(Boolean.TRUE.toString());
when(mockedConfig.getProperty(eq(someProperty), anyString())).thenReturn(someValue);
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, mockedConfig);
}
@AfterClass
public static void afterClass() throws Exception {
System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
doTearDown();
}
@Test
public void test() throws Exception {
Assert.assertNotNull(testBean);
Assert.assertTrue(testBean.execute());
Assert.assertEquals(mockedConfig, config);
Assert.assertEquals(someValue, someInjectedValue);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(ConfigurationWithConditionalOnProperty.class)
@DirtiesContext
public static class TestWithBootstrapEnabledAndNamespacesAndConditionalOn extends
AbstractSpringIntegrationTest {
@Autowired(required = false)
private TestBean testBean;
@BeforeClass
public static void beforeClass() throws Exception {
doSetUp();
System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "true");
System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES,
String.format("%s, %s", ConfigConsts.NAMESPACE_APPLICATION, FX_APOLLO_NAMESPACE));
Config config = mock(Config.class);
Config anotherConfig = mock(Config.class);
when(config.getPropertyNames()).thenReturn(Sets.newHashSet(TEST_BEAN_CONDITIONAL_ON_KEY));
when(config.getProperty(eq(TEST_BEAN_CONDITIONAL_ON_KEY), anyString())).thenReturn(Boolean.TRUE.toString());
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, anotherConfig);
mockConfig(FX_APOLLO_NAMESPACE, config);
}
@AfterClass
public static void afterClass() throws Exception {
System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES);
doTearDown();
}
@Test
public void test() throws Exception {
Assert.assertNotNull(testBean);
Assert.assertTrue(testBean.execute());
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(ConfigurationWithConditionalOnProperty.class)
@DirtiesContext
public static class TestWithBootstrapEnabledAndDefaultNamespacesAndConditionalOnFailed extends
AbstractSpringIntegrationTest {
@Autowired(required = false)
private TestBean testBean;
@BeforeClass
public static void beforeClass() throws Exception {
doSetUp();
System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "true");
Config config = mock(Config.class);
when(config.getPropertyNames()).thenReturn(Sets.newHashSet(TEST_BEAN_CONDITIONAL_ON_KEY));
when(config.getProperty(eq(TEST_BEAN_CONDITIONAL_ON_KEY), anyString())).thenReturn(Boolean.FALSE.toString());
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, config);
}
@AfterClass
public static void afterClass() throws Exception {
System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
doTearDown();
}
@Test
public void test() throws Exception {
Assert.assertNull(testBean);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(ConfigurationWithoutConditionalOnProperty.class)
@DirtiesContext
public static class TestWithBootstrapEnabledAndDefaultNamespacesAndConditionalOff extends
AbstractSpringIntegrationTest {
@Autowired(required = false)
private TestBean testBean;
@BeforeClass
public static void beforeClass() throws Exception {
doSetUp();
System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "true");
Config config = mock(Config.class);
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, config);
}
@AfterClass
public static void afterClass() throws Exception {
System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
doTearDown();
}
@Test
public void test() throws Exception {
Assert.assertNotNull(testBean);
Assert.assertTrue(testBean.execute());
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(ConfigurationWithConditionalOnProperty.class)
@DirtiesContext
public static class TestWithBootstrapDisabledAndDefaultNamespacesAndConditionalOn extends
AbstractSpringIntegrationTest {
@Autowired(required = false)
private TestBean testBean;
@BeforeClass
public static void beforeClass() throws Exception {
doSetUp();
Config config = mock(Config.class);
when(config.getPropertyNames()).thenReturn(Sets.newHashSet(TEST_BEAN_CONDITIONAL_ON_KEY));
when(config.getProperty(eq(TEST_BEAN_CONDITIONAL_ON_KEY), anyString())).thenReturn(Boolean.FALSE.toString());
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, config);
}
@AfterClass
public static void afterClass() throws Exception {
doTearDown();
}
@Test
public void test() throws Exception {
Assert.assertNull(testBean);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(ConfigurationWithoutConditionalOnProperty.class)
@DirtiesContext
public static class TestWithBootstrapDisabledAndDefaultNamespacesAndConditionalOff extends
AbstractSpringIntegrationTest {
@Autowired(required = false)
private TestBean testBean;
@BeforeClass
public static void beforeClass() throws Exception {
doSetUp();
Config config = mock(Config.class);
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, config);
}
@AfterClass
public static void afterClass() throws Exception {
System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
doTearDown();
}
@Test
public void test() throws Exception {
Assert.assertNotNull(testBean);
Assert.assertTrue(testBean.execute());
}
}
@EnableAutoConfiguration
@Configuration
static class ConfigurationWithoutConditionalOnProperty {
@Bean
public TestBean testBean() {
return new TestBean();
}
}
@ConditionalOnProperty(TEST_BEAN_CONDITIONAL_ON_KEY)
@EnableAutoConfiguration
@Configuration
static class ConfigurationWithConditionalOnProperty {
@Bean
public TestBean testBean() {
return new TestBean();
}
}
static class TestBean {
public boolean execute() {
return true;
}
}
}
......@@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
......@@ -15,6 +16,7 @@ import javax.annotation.PostConstruct;
/**
* You may set up data like the following in Apollo:
* <pre>
* redis.cache.enabled = true
* redis.cache.expireSeconds = 100
* redis.cache.clusterNodes = 1,2
* redis.cache.commandTimeout = 50
......@@ -24,8 +26,14 @@ import javax.annotation.PostConstruct;
* redis.cache.someList[1] = d
* </pre>
*
* To make <code>@ConditionalOnProperty</code> work properly, <code>apollo.bootstrap.enabled</code> should be set to true
* and <code>redis.cache.enabled</code> should also be set to true.
*
* @see resources/bootstrap.yml
*
* @author Jason Song(song_s@ctrip.com)
*/
@ConditionalOnProperty("redis.cache.enabled")
@ConfigurationProperties(prefix = "redis.cache")
@Component("sampleRedisConfig")
@RefreshScope
......
......@@ -8,12 +8,14 @@ import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.stereotype.Component;
/**
* @author Jason Song(song_s@ctrip.com)
*/
@ConditionalOnProperty("redis.cache.enabled")
@Component
public class SpringBootApolloRefreshConfig {
private static final Logger logger = LoggerFactory.getLogger(SpringBootApolloRefreshConfig.class);
......
apollo:
bootstrap:
enabled: true
# will inject 'application' and 'FX.apollo' namespaces in bootstrap phase
namespaces: application,FX.apollo
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment