41

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.

David Geary
  • 1,756
  • 2
  • 14
  • 23
  • Hi. Any updates on this? I'm in the same situation – K-RAD Jul 28 '15 at 12:54
  • @David Geary ever figure this out? – Selwyn Oct 27 '15 at 12:18
  • Sorry, I've havent looked at this further as it would require a bit a manual effort to get this going in a non spring boot app, duplicating code from spring boot etc. Unfortunately the Spring Cloud stuff seems focused on Spring Boot only. – David Geary Oct 28 '15 at 16:23

5 Answers5

7

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

  1. Create a CustomBeanFactoryPostProcessor and set lazyInit 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);
      }
    }
    
  2. Create a custom class extending StandardServletEnvironment and overriding the initPropertySources 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);
        }
    
      }
    
  3. Create a custom ApplicatonContextInitializer and override the initialize method to set the custom Enviroment instead of the StandardServletEnvironment.

    public class ConfigAppContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        applicationContext.setEnvironment(new CloudEnvironment());
      }
    }
    
  4. Modify web.xml to use this custom context initializer for both application context and servlet 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>
    

  5. 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();
    }
}
Grinish Nepal
  • 3,037
  • 3
  • 30
  • 49
  • I have an issue that performing a test the @Configuration class is not loading the properties, so basically, all the @Value properties are `null`. Do you know what is missing to also load this on a test? – Randy Hector Dec 11 '20 at 11:12
5

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>
Radwan Nizam
  • 131
  • 2
  • 9
2

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.

Dave Syer
  • 56,583
  • 10
  • 155
  • 143
  • 10
    Dave @dave, could you provide more specific information about how to proceed with this? The config server would get a much wider audience if could easily integrate with existing spring mvc applictions. – cmadsen Feb 10 '15 at 08:21
  • 2
    I agree with cmadsen, I don't mind using spring boot for the configuration server itself as that would be a new component in our system, but the configuration client should really be able to be used easily with existing code. Its supposed to be a 'cloud' project after all, so integration with standard spring web apps deployed into established cloud environments such as Elastic Beanstalk would seem to be a common use case. – David Geary Feb 10 '15 at 16:11
  • To start, look at the code that is loaded automatically by boot via https://github.com/spring-cloud/spring-cloud-config/blob/master/spring-cloud-config-client/src/main/resources/META-INF/spring.factories Those classes are the entrypoints into spring-cloud-client. You could get away with ignoring `RefreshAutoConfiguration`, `LifecycleMvcEndpointAutoConfiguration` and `RestartListener`. – spencergibb Feb 10 '15 at 21:11
  • 4
    @dave-syer Your answer is not particularly helpful. Granted, the OP asked whether it is possible, so ‘Yes’ may be correct. In general, the direction Spring takes here is unsatisfying. Vanishing XML configuration and dependence on Spring Boot make adaption a pain. – Michael Piefel Feb 10 '15 at 21:19
  • 3
    has anyone got this to run? sad to see everything is just documented from a spring boot point of view :( – domi Sep 09 '15 at 08:28
  • I'm running into the same issues. I built test Eureka servers and clients with spring boot which was a breeze. Then I deployed a basic Eureka server and tried to annotate an existing MVC app with @EnableDiscoveryClient thinking it would be that easy. Unfortunately it's not. I keep getting errors like No qualifying bean of type [com.netflix.discovery.EurekaClientConfig]. Looks like we're not going to be able to adopt this now... – Bal Sep 11 '15 at 21:12
  • If you built your app with spring boot you should just be able to use the starter poms from spring cloud to get the dependencies right. I can't see why a missing dependency should stop you from using something. – Dave Syer Sep 24 '15 at 06:54
  • Did someone manage to run a Spring Config Client without Spring Boot? – codependent Jan 14 '16 at 13:47
  • 3
    A couple of clients at my company is looking for a "crawl-walk-run" approach in which customers want to migrate to the Config Service without "Bootfying" their apps. This is representing a major pain for initial adoption. – Marcello DeSales Mar 16 '17 at 00:44
  • I am interested on an answer here as well. I have Mule based apps that I would like to configure through Spring Config Server. I can't "bootify" these apps. – cerebrotecnologico Apr 10 '17 at 18:06
2

Posting as an answer because I don't have enough points to comment on Dave Syer's excellent answer. We have implemented his proposed solution in production and it is working as expected. Our newer apps are being written using Boot, while our legacy apps use Spring, but not boot. We were able to use Spring Cloud Config to create a property service that serves properties for both. The changes were minimal. I moved the legacy property files out of the war file to the property service git repository, and changed the property definition from a classpath reference to a URL as Dave describes and inserted our system environment variable just as we did for classpath. It was easy and effective.

<util:properties id="envProperties" location="https://properties.me.com/property-service/services-#{envName}.properties" />
<context:property-placeholder properties-ref="envProperties" ignore-resource-not-found="true" ignore-unresolvable="true" order="0" />
<util:properties id="defaultProperties" location="https://properties.me.com/property-service/services-default.properties" />
<context:property-placeholder properties-ref="defaultProperties" ignore-resource-not-found="true" ignore-unresolvable="true" order="10" />
Tschuss
  • 33
  • 5
  • I found it useful. But my problem is that the servername will be different in each environment. I tried to get the server name from another property file, but it is not working. , I need something like . How can I set config.server.url dynamically? any idea – Vins Dec 14 '17 at 04:02
  • @Vins: You can pass it through a java system property on startup of your app (or in the JVM properties of your servlet container startup script). eg. -Dconfig.server.url=config.somewhere.com – Piers Geyman Mar 23 '18 at 18:25
  • @Tschuss How did you pass security info for access to the spring cloud config server? – Piers Geyman Mar 23 '18 at 18:27
  • @Tschuss : what about config refresh? How did you implement that ? – Swap905 Apr 23 '21 at 10:49
2

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>
loicmathieu
  • 5,181
  • 26
  • 31