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.
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(); } }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.
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
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>
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?