ReloadablePropertiesFactoryBean: Beware of Caching

As Glenn pointed out, reloading properties is futile if the servlet engine returns an unchanged copy of the property file from its resource cache.

We need to find the real file anyway to check its last modification date, so overriding “loadProperties” to read from the file system absolutely makes sense.

Here’s an updated version, which correctly handles reloading of properties also from a ServletContextResource.

Download

5 thoughts on “ReloadablePropertiesFactoryBean: Beware of Caching”

  1. Chris Gilbert proposed a fix for Spring 3.0 in http://www.wuenschenswert.net/wunschdenken/archives/138#comments.
    I could not get his fix to work, so here are my changes to the code from 31.08.2007 (+ some other changes) that passes my unit tests. I have not yet converted to Spring 3.0, so there could be errors that my unit tests don’t detect.
    I have modified this code several times including formatting in Eclipse, so I post the complete class, not a diff.

    ReloadablePropertiesFactoryBean.java:

    package net.wuenschenswert.spring;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Properties;
    
    import org.apache.commons.io.IOUtils;
    import org.apache.log4j.Logger;
    import org.springframework.beans.factory.DisposableBean;
    import org.springframework.beans.factory.FactoryBean;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.core.io.Resource;
    import org.springframework.core.io.support.PropertiesLoaderSupport;
    import org.springframework.util.DefaultPropertiesPersister;
    import org.springframework.util.PropertiesPersister;
    
    /**
     * A properties factory bean that creates a reconfigurable Properties object.
     * When the Properties' reloadConfiguration method is called, and the file has
     * changed, the properties are read again from the file.
     */
    public class ReloadablePropertiesFactoryBean extends PropertiesLoaderSupport
    		implements DisposableBean, FactoryBean<ReconfigurableBean>, InitializingBean {
    
    	private Logger log = Logger.getLogger(getClass());
    
    	// add missing getter for locations
    
    	private Resource[] locations;
    
    	private long[] lastModified;
    	
    	private boolean ignoreResourceNotFound = false;
    	
    	private String fileEncoding;
    	
    	private boolean singleton;
    		
    	private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
    
    	private List<ReloadablePropertiesListener> preListeners;
    	
    	public ReloadablePropertiesFactoryBean() {
    		singleton = true;
    	}
    	
    	@Override
    	public void setLocation(Resource location) {
    		setLocations(new Resource[] { location });
    	}
    
    	@Override
    	public void setLocations(Resource[] locations) {
    		if(locations != null) {
    			this.locations = locations.clone();
    			lastModified = new long[locations.length];
    			super.setLocations(locations);
    		}
    	}
    
    	protected Resource[] getLocations() {
    		return locations;
    	}
    
    	public void setListeners(List listeners) {
    		// early type check, and avoid aliassing
    		this.preListeners = new ArrayList<ReloadablePropertiesListener>();
    		for (Object o : listeners) {
    			preListeners.add((ReloadablePropertiesListener) o);
    		}
    	}
    	
    	  @Override
    	public void setFileEncoding(String encoding) {
    		this.fileEncoding = encoding;
    		super.setFileEncoding(encoding);
    	}
    
    	@Override
    	public void setPropertiesPersister(PropertiesPersister propertiesPersister) {
    		this.propertiesPersister = (propertiesPersister != null ? propertiesPersister
    				: new DefaultPropertiesPersister());
    		super.setPropertiesPersister(this.propertiesPersister);
    	}
    
    	@Override
    	public void setIgnoreResourceNotFound(boolean ignoreResourceNotFound) {
    		this.ignoreResourceNotFound = ignoreResourceNotFound;
    		super.setIgnoreResourceNotFound(ignoreResourceNotFound);
    	}
    
    	private ReloadablePropertiesImpl reloadableProperties;
    
    	private ReloadablePropertiesImpl createInstance() throws IOException {
    		// would like to uninherit from AbstractFactoryBean (but it's final!)
    		
    		if (!isSingleton()) {
    			throw new RuntimeException(
    					"ReloadablePropertiesFactoryBean only works as singleton");
    		}
    		ReloadablePropertiesImpl reloadableProperties = new ReloadablePropertiesImpl();
    		if (preListeners != null) {
    			reloadableProperties.setListeners(preListeners);
    		}
    		return reloadableProperties;
    	}
    
    	public void destroy(){
    		reloadableProperties = null;
    	}
    
    	protected void reload(boolean forceReload) throws IOException {
    		boolean reload = forceReload;
    		for (int i = 0; i < locations.length; i++) {
    			Resource location = locations[i];
    			File file;
    			try {
    				file = location.getFile();
    			} catch (IOException e) {
    				// not a file resource
    				continue;
    			}
    			try {
    				long l = file.lastModified();
    				if (l > lastModified[i]) {
    					lastModified[i] = l;
    					reload = true;
    				}
    			} catch (Exception e) {
    				// cannot access file. assume unchanged.
    				if (log.isDebugEnabled()) {
    					log.debug("can't determine modification time of " + file
    							+ " for " + location, e);
    				}
    			}
    		}
    		if (reload) {
    			doReload();			
    		}
    	}
    
    	private void doReload() throws IOException {
    		reloadableProperties.setProperties(mergeProperties());
    	}
    
    	  /**
    		 * Load properties into the given instance. Overridden to use
    		 * {@link Resource#getFile} instead of {@link Resource#getInputStream},
    		 * as the latter may be have undesirable caching effects on a
    		 * ServletContextResource.
    		 * 
    		 * @param props
    		 *            the Properties instance to load into
    		 * @throws java.io.IOException
    		 *             in case of I/O errors
    		 * @see #setLocations
    		 */
    	@Override
    	protected void loadProperties(Properties props) throws IOException {
    		if (this.locations != null) {
    			for (int i = 0; i < this.locations.length; i++) {
    				Resource location = this.locations[i];
    				if (logger.isInfoEnabled()) {
    					logger.info("Loading properties file from " + location);
    				}
    				InputStream is = null;
    				try {
    					File file = null;
    					try {
    						file = location.getFile();
    					} catch (IOException e) {
    						logger.warn(
    								"Not a file resource, may not be able to reload: "
    										+ location, e);
    					}
    					if (file != null){
    						is = new FileInputStream(file);
    					}
    						
    					else {
    						is = location.getInputStream();
    					}
    						
    					if (location.getFilename().endsWith(XML_FILE_EXTENSION)) {
    						this.propertiesPersister.loadFromXml(props, is);
    					} else {
    						if (this.fileEncoding != null) {
    							this.propertiesPersister
    									.load(props, new InputStreamReader(is,
    											this.fileEncoding));
    						} else {
    							this.propertiesPersister.load(props, is);
    						}
    					}
    				} catch (IOException ex) {
    					if (this.ignoreResourceNotFound) {
    						if (logger.isWarnEnabled()) {
    							logger.warn("Could not load properties from "
    									+ location + ": " + ex.getMessage());
    						}
    					} else {
    						throw ex;
    					}
    				} finally {
    					IOUtils.closeQuietly(is);
    				}
    			}
    		}
    	}
    
    	class ReloadablePropertiesImpl extends ReloadablePropertiesBase implements
    			ReconfigurableBean {
    
    		private static final long serialVersionUID = 8997200726392650775L;
    
    		public void reloadConfiguration() throws IOException {
    			ReloadablePropertiesFactoryBean.this.reload(false);
    		}
    	}
    
    	public ReconfigurableBean getObject() throws Exception {
    		ReconfigurableBean object = singleton ? reloadableProperties : createInstance();
    		reload(true);
    		return object;
    
    	}
    
    	public Class<? extends ReconfigurableBean> getObjectType() {
    		return ReconfigurableBean.class;
    	}
    
    	public boolean isSingleton() {
    		return singleton;
    	}
    
    	public void setSingleton(boolean singleton) {
    		this.singleton = singleton;
    	}
    
    	public void afterPropertiesSet() throws Exception {
    		if(singleton)
                reloadableProperties = createInstance();
    	}
    
    }
    
  2. Thanks alot for the contribution! I’m currently annoyingly busy with work not even related to programming, but it’s becoming obvious to me that this piece of code merits revived attention.

    And I’ll definitely have to get my head around a proper place to host a piece of code – wordpress clearly isn’t. Will keep you posted.

  3. I’ve assembled a minimal mavenized version for your jar just move code in the standard maven file system structure (src/main/java, src/test/java and be sure to move config.properties and dynamic.xml into src/test/resources/net/wu…/spring/example, test1.xml into src/test/resources/net/wu…/spring/).

    This is the pom.xml file

    4.0.0
    net.wuenschenswert
    spring-reloaded
    0.0.1-SNAPSHOT

    org.springframework
    org.springframework.core
    ${spring.version}

    org.springframework
    org.springframework.context
    ${spring.version}

    org.slf4j
    com.springsource.slf4j.log4j
    ${slf4j.version}

    org.slf4j
    com.springsource.slf4j.api
    ${slf4j.version}

    org.apache.commons
    com.springsource.org.apache.commons.logging
    1.1.1

    org.junit
    com.springsource.org.junit
    4.8.1
    test

    org.apache.maven.plugins
    maven-compiler-plugin
    2.3.1

    ${java.source.version}
    ${java.target.version}

    1.6
    1.6
    3.0.2.RELEASE
    1.5.10

  4. Sorry wordpress removed the < &gr;

    <project xmlns=”http://maven.apache.org/POM/4.0.0″ xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd“>
    <modelVersion>4.0.0</modelVersion>
    <groupId>net.wuenschenswert</groupId>
    <artifactId>spring-reloaded</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>org.springframework.core</artifactId>
    <version>${spring.version}</version>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>org.springframework.context</artifactId>
    <version>${spring.version}</version>
    </dependency>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>com.springsource.slf4j.log4j</artifactId>
    <version>${slf4j.version}</version>
    </dependency>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>com.springsource.slf4j.api</artifactId>
    <version>${slf4j.version}</version>
    </dependency>
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>com.springsource.org.apache.commons.logging</artifactId>
    <version>1.1.1</version>
    </dependency>
    <dependency>
    <groupId>org.junit</groupId>
    <artifactId>com.springsource.org.junit</artifactId>
    <version>4.8.1</version>
    <scope>test</scope>
    </dependency>
    </dependencies>
    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.1</version>
    <configuration>
    <source>${java.source.version}</source>
    <target>${java.target.version}</target>
    </configuration>
    </plugin>
    </plugins>
    </build>
    <properties>
    <java.source.version>1.6</java.source.version>
    <java.target.version>1.6</java.target.version>
    <spring.version>3.0.2.RELEASE</spring.version>
    <slf4j.version>1.5.10</slf4j.version>
    </properties>
    </project>

  5. Thanks for the contribution, again!

    I’ve finally come around to pushing the code to github, where it should live more easily:

    http://github.com/axeolotl/SpringPropertiesReloaded

    A github pull request is a smaller obstacle than fighting wordpress XML filtering, I hope.

    In the initial commit I’ve added a pom. The pom uses maven central dependencies instead of Enterprise Bundle Repository, for the simple reason that I didn’t have the repository URL at hand. What do you think, should it be EBR?

Comments are closed.