在很多项目里面都有后缀名为properties的配置文件,我们一般会把这些文件放到名为conf之类的目录下面,随同jar一起发布。运行时会把conf目录加到jvm的classpath下面去。麻烦的是,程序运行时,我们改动了配置文件,如何让我们的配置文件无需重启程序起作用。我这里有个比较简陋的解决方案,有兴趣的可以看看,应该还可以做些优化。
解决方案的技术思路:
起一个定时器,定时的监控配置的文件的修改时间,如果一旦发现修改,重新装载文件。由于Spring的配置值表达式不支持OGNL类的表达式,于是使用Spring自带的method replace(方法替换)来模拟OGNL类的表达。
代码并不复杂,用到包有asm,cglib,spring2.x,commons-logging4个而已。demo结构如下:
文件简介:
FileListener:监测配置文件修改的接口
FileMonitor:一个TimeTask的子类,检查文件有无改动
ConfigManager: 核心类,里面有个Properties成员装载配置文件信息
ConcreteConfig:配置的“反射”类
Main:测试类
conf/monitor.properties 配置文件
conf/monitorContext.xml Spring配置文件
具体实现代码为:
FileListener.java
import java.io.File; /** * an interface to listen the notifications when the file has been changed * */ public interface FileListener { /** * a notification when the file changed * * @param file * the file which has been changed */ public void onFileChanged(File file); }
FileMonitor.java
import java.io.File; import java.util.TimerTask; /** * a class to monitor if the file has been changed * */ public class FileMonitor extends TimerTask { private FileListener listener; private File file; private long lastModified; /** * constructor * * @param file * a file which will be monitor * @param listener * a listener which will be notified when the file has been * changed */ public FileMonitor(File file, FileListener listener) { if (file == null || listener == null) { throw new NullPointerException(); } this.file = file; this.lastModified = this.file.lastModified(); this.listener = listener; } @Override public void run() { long lastModified = this.file.lastModified(); if (this.lastModified != lastModified) { this.lastModified = lastModified; this.listener.onFileChanged(this.file); } } }
ConfigManager.java
import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Method; import java.net.URISyntaxException; import java.net.URL; import java.util.Properties; import java.util.Set; import java.util.Timer; import org.springframework.beans.factory.support.MethodReplacer; public class ConfigManager implements FileListener, MethodReplacer{ private static final long FILE_MONITOR_INTERVAL = 5000; private static final String MONITOR_CONF_FILE_PATH = "monitor.properties"; private FileMonitor monitor; private Timer timer = new Timer("Timer", true); private Properties properties = new Properties(); private ConfigManager() throws IOException { properties.load(getClass().getResourceAsStream("/monitor.properties")); // monitor the configuration file change monitor = new FileMonitor(getFileByClassPath(MONITOR_CONF_FILE_PATH), this); timer.schedule(monitor, FILE_MONITOR_INTERVAL, FILE_MONITOR_INTERVAL); } private File getFileByClassPath(String filepath) { URL url = getClass().getResource( filepath); if (url == null) { System.err.println("failed to find the file " + filepath); return null; } try { File file = new File(url.toURI()); return file; } catch (URISyntaxException e) { e.printStackTrace(); } return null; } @Override public synchronized void onFileChanged(File file) { Properties newProperties = new Properties(); try { newProperties.load(new FileInputStream(file)); } catch (IOException e) { e.printStackTrace(); return; } Set<String> keys = properties.stringPropertyNames(); for(String key : keys) { String newValue = (String)newProperties.get(key); String oldValue = properties.getProperty(key); System.out.println("newValue:"+newValue+" oldValue:"+oldValue); if(newValue != null) { properties.setProperty(key, newValue); } else { properties.remove(key); } } } public synchronized String getProperty(String key) { return properties.getProperty(key); } @Override public Object reimplement(Object arg0, Method arg1, Object[] arg2) throws Throwable { String methodName = arg1.getName(); String tmp = methodName.substring("get".length()); char ch = tmp.charAt(0); ch = Character.toLowerCase(ch); tmp = ch+tmp.substring(1); return getProperty(tmp); } }
ConcreteConfig.java
public class ConcreteConfig { private String zookeeperQuorum; private String zookeeperPort; /** * @param zookeeperQuorum the zookeeperQuorum to set */ public void setZookeeperQuorum(String zookeeperQuorum) { this.zookeeperQuorum = zookeeperQuorum; } /** * @return the zookeeperQuorum */ public String getZookeeperQuorum() { return zookeeperQuorum; } /** * @param zookeeperPort the zookeeperPort to set */ public void setZookeeperPort(String zookeeperPort) { this.zookeeperPort = zookeeperPort; } /** * @return the zookeeperPort */ public String getZookeeperPort() { return zookeeperPort; } }
Main.java
import org.springframework.context.support.FileSystemXmlApplicationContext; public class Main { public static void main(String[] args) throws InterruptedException { FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext( new String[]{ "classpath:monitorContext.xml" }); ConcreteConfig config = (ConcreteConfig)context.getBean("concreteConfig"); while(true) { System.out.println(config.getZookeeperQuorum()); System.out.println(config.getZookeeperPort()); Thread.sleep(5000); } } }
monitor.properties
zookeeperQuorum = host1:2181,host2:2181,host3:2181 zookeeperPort = 2181
monitorContext.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans default-lazy-init="false"> <bean id="configManager" class="ConfigManager"/> <bean id="concreteConfig" class="ConcreteConfig"> <replaced-method name="getZookeeperQuorum" replacer="configManager"/> <replaced-method name="getZookeeperPort" replacer="configManager"/> </bean> </beans>
测试过程:
在Eclipse里面以Main做主类运行,观察控制台输出。
然后改动monitor.properties,再看控制台输出,可以发现改动很快生效。