Sunday, June 13, 2010

Spring 3: Scheduling, Components, PostBeanProcessors and bending the rules.

This blog entry should come with one of those warnings "Do not attempt this at home, the following is done by trained professionals".. bla bla bla

To begin, I, being a developer always push the limits and bend the rules of what we should / or should not do. I set out to "bend" something in Spring for a change. I work in a 24/7/365 live type environment, where there are always people using the system and any downtime causes many reports and cascading pain flowing down the leadership hierarchy in a large corporate environment. Now sometimes in this environment even with tons of effort, signoffs, QA's, UAT's, Training something goes live, or happens in live that needs to be changed / switched off.

Now using Spring for injecting all that needs to be injected is awesome, but it does require that the application context be recreated / instantiated to pickup configuration changes. The following code changes that...

As with the method cache aspect, I will make the code available at my google code project:
Source Code

So we want to configure a field in a Spring managed bean to be available to be changed while the application is running.

I have specified an annotation @LiveConfig:
package javaitzen.spring.liveconfig;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) 
@Target({ElementType.FIELD })
@Documented
public @interface LiveConfig {
  String configLocation() default "liveconfig.properties";
}


To pick up all the @LiveConfig annotations, I created a BeanPostProcessor , that will be called when the ApplicationContext instantiates and keep track of all the @LiveConfig available to it.

package javaitzen.spring.liveconfig;

import java.lang.reflect.Field;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class LiveConfigBeanProcessor implements BeanPostProcessor {
    private Log logger = LogFactory.getLog(this.getClass());

    @Autowired
    private ConfigLoader loader;
    public final Object postProcessBeforeInitialization(final Object bean, final String beanName) {

        Field[] fields = bean.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(LiveConfig.class)) {

                try {
                    field.setAccessible(true);
                    LiveConfig ann = field.getAnnotation(LiveConfig.class);
                    LiveConfigData lcd = new LiveConfigData();
                    lcd.setFile(ann.configLocation());
                    lcd.setBean(bean);
                    lcd.setField(field);
                    loader.addConfig(field.getName(), lcd);
                    loader.load();
                } catch (Exception e) {
                    logger.warn(e);
                }
            }
        }
        return bean;
    }

    public Object postProcessAfterInitialization(final Object bean, final String beanName) {
        return bean;
    }

}


So now when the ApplicationContext loads, it will add LiveConfigData to the ConfigLoader. This config loader uses the new Spring 3 Scheduling annotations to fire every 30 seconds asynchronously.

package javaitzen.spring.liveconfig;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * The Class ConfigLoaderImpl.
 */
@Component
public class ConfigLoaderImpl implements ConfigLoader {

    private Map< String, LiveConfigData > liveConfigs = new HashMap< String, LiveConfigData >();

    /**
     * Instantiates a new config loader impl.
     */
    public ConfigLoaderImpl() {

    }

    /**
     * Adds the config.
     * 
     * @param field
     *            the field
     * @param data
     *            the data
     */
    public void addConfig(final String field, final LiveConfigData data) {
        liveConfigs.put(field, data);
    }

    /**
     * Load.
     * 
     * @see javaitzen.spring.liveconfig.ConfigLoader#load()
     */
    @Override
    @Scheduled(fixedDelay = 30000)
    @Async
    public void load() {

        FileInputStream in = null;
        try {
            for (Entry< String, LiveConfigData > liveConfig : liveConfigs.entrySet()) {
                LiveConfigData lcd = liveConfig.getValue();
                Properties props = new Properties();
                in = new FileInputStream(lcd.getFile());
                props.load(in);
                for (Entry< Object, Object > entry : props.entrySet()) {
                    String key = (String) entry.getKey();
                    String value = (String) entry.getValue();
                    LiveConfigData data = liveConfigs.get(key);
                    data.getField().set(data.getBean(), value);
                }
            }
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }
}

The Application Context, is surprisingly simple. The context:component-scan picks up the classes annotated with @Component (ConfigLoaderImpl and the BeanPostProcessor). The task:annotation-driven lets Spring know to look for @Scheduled. The loader is @Autowired into the BeanPostProcessor.




 
 

    



For demostration purposes I just created a simple little WorkManager structure to show how to configure the fields and have a running application to test when changing the configuration changes. It also shows how to get config from a file that is not the default "liveconfig.properties"

package javaitzen.spring.liveconfig.service;

import java.util.Date;

import javaitzen.spring.liveconfig.LiveConfig;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class TheWorkManager implements WorkManager {

    @LiveConfig
    private String filePickupURL;

    @LiveConfig
    private String enableDetail;

    @LiveConfig(configLocation = "duplicate.properties")
    private String allowDuplicates;

    @Autowired
    private Work worker;

    /**
     * process.
     * 
     * @see javaitzen.spring.liveconfig.service.WorkManager#process()
     */
    @Scheduled(fixedDelay = 10000)
    public void process() {
        System.out.println("processing: " + new Date());
        for (int i = 0; i < 3; i++) {
            worker.doWork("" + i, filePickupURL, enableDetail, allowDuplicates);
        }
    }

}

4 comments:

Popular Posts

Followers