Spring Cloud Config Client Without Spring Boot
We have an existing spring web app deployed as a WAR file into Amazon Elastic Beanstalk. Currently we load properties files as http resources to give us a single source of property placeholder config resolution. Im investigating replacing this with the new spring cloud configuration server to give us the benefits of git versioning etc.
However the documentation (http://cloud.spring.io/spring-cloud-config/spring-cloud-config.html) only seems to describe a Spring Boot client application. Is it possible to set up the Spring Cloud Config Client in an existing web app? Do I need to manually set up the Bootstrap parent application context etc - are there any examples of this? Our current spring configuration is XML based.
Refrenced: https://wenku.baidu.com/view/493cf9eba300a6c30d229f49.html
Root WebApplicationContext
and the Servlet WebApplicationContext
uses Environment and initializes PropertySources based on the spring profile. For non-spring boot apps, we need to customize these to get the properties from Config Server and to refresh the beans whenever there is a property change. Below are the changes that needs to happen to get the config working in SpringMVC. You will also need a system property for spring.profile.active
-
Create a
CustomBeanFactoryPostProcessor
and setlazyInit
on all bean definitions to true to initialize all bean lazily i.e. beans are initialized only upon a request.@Component public class AddRefreshScopeProcessor implements BeanFactoryPostProcessor, ApplicationContextAware { private static ApplicationContext applicationContext; @SuppressWarnings("unchecked") @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { String[] beanNames = applicationContext.getBeanDefinitionNames(); for(int i=0; i<beanNames.length; i++){ BeanDefinition beanDef = beanFactory.getBeanDefinition(beanNames[i]); beanDef.setLazyInit(true); beanDef.setScope("refresh"); } } @Override public void setApplicationContext(ApplicationContext context) throws BeansException { applicationContext = context; } /** * Get a Spring bean by type. * * @param beanClass * @return */ public static <T> T getBean(Class<T> beanClass) { return applicationContext.getBean(beanClass); } /** * Get a Spring bean by name. * * @param beanName * @return */ public static Object getBean(String beanName) { return applicationContext.getBean(beanName); } }
-
Create a custom class extending
StandardServletEnvironment
and overriding theinitPropertySources
method to load additional PropertySources (from config server).public class CloudEnvironment extends StandardServletEnvironment { @Override public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) { super.initPropertySources(servletContext,servletConfig); customizePropertySources(this.getPropertySources()); } @Override protected void customizePropertySources(MutablePropertySources propertySources) { super.customizePropertySources(propertySources); try { PropertySource<?> source = initConfigServicePropertySourceLocator(this); propertySources.addLast(source); } catch ( Exception ex) { ex.printStackTrace(); } } private PropertySource<?> initConfigServicePropertySourceLocator(Environment environment) { ConfigClientProperties configClientProperties = new ConfigClientProperties(environment); configClientProperties.setUri("http://localhost:8888"); configClientProperties.setProfile("dev"); configClientProperties.setLabel("master"); configClientProperties.setName("YourApplicationName"); System.out.println("##################### will load the client configuration"); System.out.println(configClientProperties); ConfigServicePropertySourceLocator configServicePropertySourceLocator = new ConfigServicePropertySourceLocator(configClientProperties); return configServicePropertySourceLocator.locate(environment); } }
-
Create a custom
ApplicatonContextInitializer
and override theinitialize
method to set thecustom Enviroment
instead of theStandardServletEnvironment
.public class ConfigAppContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { applicationContext.setEnvironment(new CloudEnvironment()); } }
-
Modify
web.xml
to use this custom context initializer for bothapplication context
andservlet context
.<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextInitializerClasses</param-name> <param-value>com.my.context.ConfigAppContextInitializer</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextInitializerClasses</param-name> <param-value>com.my.context.ConfigAppContextInitializer</param-value> </context-param> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/dispatcher-servlet.xml</param-value> </context-param>
-
To refresh the beans created a refresh endpoint you will also need to refresh the
application Context
.@Controller public class RefreshController { @Autowired private RefreshAppplicationContext refreshAppplicationContext; @Autowired private RefreshScope refreshScope; @RequestMapping(path = "/refreshall", method = RequestMethod.GET) public String refresh() { refreshScope.refreshAll(); refreshAppplicationContext.refreshctx(); return "Refreshed"; } }
RefreshAppplicationContext.java
@Component
public class RefreshAppplicationContext implements ApplicationContextAware {
private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void refreshctx(){
((XmlWebApplicationContext)(applicationContext)).refresh();
}
}
I have similar requirement; I have a Web Application that uses Spring XML configuration to define some beans, the value of the properties are stored in .property files. The requirement is that the configuration should be loaded from the hard disk during the development, and from a Spring Cloud Config server in the production environment.
My idea is to have two definition for the PropertyPlaceholderConfigurer; the first one will be used to load the configuration from the hard disk :
<bean id="resources" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" doc:name="Bean">
<property name="locations">
<list>
<value>dcm.properties</value>
<value>post_process.properties</value>
</list>
</property>
</bean>
The second one will load the .properties from the Spring Config Server :
<bean id="resources" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" doc:name="Bean">
<property name="locations">
<list>
<value>http://localhost:8888/trunk/dcm-qa.properties</value>
</list>
</property>
</bean>
Everything that "just works" with Spring Boot is actually no more than some configuration. It's all just a Spring application at the end of the day. So I believe you can probably set everything up manually that Boot does for you automatically, but I'm not aware of anyone actually trying this particular angle. Creating a bootstrap application context is certainly the preferred approach, but depending on your use case you might get it to work with a single context if you make sure the property source locators are executed early enough.
Non Spring (or non Spring Boot) apps can access plain text or binary files in the config server. E.g. in Spring you could use a @PropertySource
with a resource location that was a URL, like http://configserver/{app}/{profile}/{label}/application.properties
or http://configserver/{app}-{profile}.properties
. It's all covered in the user guide.
I found a solution for using spring-cloud-zookeeper without Spring Boot, based on the idea provided here https://wenku.baidu.com/view/493cf9eba300a6c30d229f49.html
It should be easily updated to match your needs and using a Spring Cloud Config Server (the CloudEnvironement class needs to be updated to load the file from the server instead of Zookeeper)
First, create a CloudEnvironement class that will create a PropertySource (ex from Zookeeper) :
CloudEnvironement.java
public class CloudEnvironment extends StandardServletEnvironment {
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
super.customizePropertySources(propertySources);
try {
propertySources.addLast(initConfigServicePropertySourceLocator(this));
}
catch (Exception ex) {
logger.warn("failed to initialize cloud config environment", ex);
}
}
private PropertySource<?> initConfigServicePropertySourceLocator(Environment environment) {
ZookeeperConfigProperties configProp = new ZookeeperConfigProperties();
ZookeeperProperties props = new ZookeeperProperties();
props.setConnectString("myzookeeper:2181");
CuratorFramework fwk = curatorFramework(exponentialBackoffRetry(props), props);
ZookeeperPropertySourceLocator propertySourceLocator = new ZookeeperPropertySourceLocator(fwk, configProp);
PropertySource<?> source= propertySourceLocator.locate(environment);
return source ;
}
private CuratorFramework curatorFramework(RetryPolicy retryPolicy, ZookeeperProperties properties) {
CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
builder.connectString(properties.getConnectString());
CuratorFramework curator = builder.retryPolicy(retryPolicy).build();
curator.start();
try {
curator.blockUntilConnected(properties.getBlockUntilConnectedWait(), properties.getBlockUntilConnectedUnit());
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
return curator;
}
private RetryPolicy exponentialBackoffRetry(ZookeeperProperties properties) {
return new ExponentialBackoffRetry(properties.getBaseSleepTimeMs(),
properties.getMaxRetries(),
properties.getMaxSleepMs());
}
}
Then create a custom XmlWebApplicationContext class : it will enable to load the PropertySource from Zookeeper when your webapplication start and replace the bootstrap magic of Spring Boot:
MyConfigurableWebApplicationContext.java
public class MyConfigurableWebApplicationContext extends XmlWebApplicationContext {
@Override
protected ConfigurableEnvironment createEnvironment() {
return new CloudEnvironment();
}
}
Last, in your web.xml file add the following context-param for using your MyConfigurableWebApplicationContext class and bootstraping your CloudEnvironement.
<context-param>
<param-name>contextClass</param-name>
<param-value>com.kiabi.config.MyConfigurableWebApplicationContext</param-value>
</context-param>
If you use a standard property file configurer, it should still be loaded so you can have properties in both a local file and Zookeeper.
For all this to work you need to have spring-cloud-starter-zookeeper-config and curator-framework jar in your classpath with their dependancy, if you use maven you can add the following to your pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-zookeeper-dependencies</artifactId>
<version>1.1.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-config</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
</dependencies>