曾经用php编写代码没有动态配置的概念,代码修改完后直接刷新页面就可以看到效果,不用重新启动server程序。在学习java和开发消息框架的过程中,有一件事情让我觉得特别麻烦,每次更改模块配置后就要重新启动tomcat,这时我才有动态配置的概念——不用重新启动程序让模块配置能立即生效。动态配置不仅在开发、调试过程中需要,节省时间精力,而且在程序上线运行中也重要,我们能及时的更改运行配置而反应业务需要,例如一个url由映射到A模块改为B模块,如果需要停止web服务,那太糟糕了。
我这里区分热部署和动态配置为两个概念,热部署为运行中模块或者对象类的更换,动态配置为当对象配置改变时,配置能立即、尽快的生效。传统上热部署由虚拟机的类加载器来实现,通过开发自己的类加载器来检测对象类的变化来重新加载。我觉得这一来是麻烦,而来是缺乏灵活。我们来看对于消息编程框架如何实现的。
当模块都实例化后,模块是无法知道配置文件改变的,单对每个模块开发检测配置文件功能将繁琐而麻烦,况且配置的改变是根据业务情况需要的,也许后来业务不需要运行中改变。动态配置不应该是业务模块的功能,属于维护方便的辅助。用传统的对象编程实现这个功能比较困难,因为每个对象都是不同的,但在我的消息对象框架中,所有的模块或对象都基于一个公共模板类——TLbaseModule,那么这将很容易的实现。
首先在公共模板类TLbaseModule中增加配置重新载入功能:
case RELOADCONFIG:
setConfig();
initProperty();
reload();
putLog(name+" 重新加载配置",LogLevel.INFO);
上面的代码片段意思是 收到消息 RELOADCONFIG时,重新加载配置。
模块有重新载入配置的功能了,那么下一个问题是模块怎么得到这个消息。我的思路是由单独的线程运行配置文件监听模块,由监听模块监听配置文件,当某个模块的配置文件发生改变时给这个模块发送RELOADCONFIG消息。监听模块代码:
public class TLMonitorConfigModule extends TLBaseModule {
protected TLMsg reloadMsg=createMsg().setAction("reloadConfig");
protected Map<String,String> moduleConfigFiles;
protected Map<String,Long> fileLasttime;
protected boolean ifStop =false;
public TLMonitorConfigModule(){
super();
}
public TLMonitorConfigModule(String name ){
super(name);
}
public TLMonitorConfigModule(String name , TLObjectFactory modulefactory){
super(name,modulefactory);
}
protected void initProperty(){
super.initProperty();
initModulesMap();
}
protected void initModulesMap(){
moduleConfigFiles=new ConcurrentHashMap<>();
fileLasttime=new ConcurrentHashMap<>();
moduleConfigFiles.put(name,configFile);
fileLasttime.put(name, Long.valueOf(0));
String modules[] = params.get("modules").split(";");
for(int i=0;i<modules.length;i++){
modules[i]=modules[i].trim();
moduleConfigFiles.put(modules[i],"");
fileLasttime.put(modules[i], Long.valueOf(0));
}
}
@Override
protected void init() {
putMsg(this,createMsg().setAction("checkModules")
.setParam(SESSIONDEAMON,true)
.setParam(EXCEPTIONHANDLER,new MyUnchecckedExceptionhandler(this,createMsg().setAction("restart")))
.setWaitFlag(false));
putLog("监听配置文件模块启动",LogLevel.INFO);
}
@Override
protected TLMsg checkMsgAction(Object fromWho, TLMsg msg) {
switch (msg.getAction()) {
case"checkModules":
ifStop=false;
checkTask();
break;
case"stop":
ifStop=true;
break;
case"restart":
restart(fromWho,msg);
break;
default:
;
}
return null;
}
private void restart(Object fromWho, TLMsg msg) {
Throwable e = (Throwable) msg.getParam("throwable");
putLog("发生异常,监听配置文件模块重启",LogLevel.ERROR);
putLog(e.getMessage() +e.getCause(),LogLevel.ERROR);
init();
}
protected void checkTask() {
int delay=10;
if(params.get("delay")!=null)
delay= Integer.parseInt(params.get("delay"));
int i =0 ;
while (!ifStop){
try {
i++;
Thread.sleep(delay*1000);
if(i >=10)
{
putLog("监听模块工作中。。。。",LogLevel.INFO);
i=0;
}
checkModules();
} catch (InterruptedException e) {
// e.printStackTrace();
putLog("发生异常,监听配置文件模块重启",LogLevel.INFO);
init();
}
}
}
protected void checkModules() {
for (String moduleName:moduleConfigFiles.keySet())
{
if(ifStop==true)
return;
String configFile=moduleConfigFiles.get(moduleName);
if(configFile==null)
continue;
if(configFile.equals(""))
{
TLBaseModule module= (TLBaseModule) getModuleInFactory(moduleName);
if(module!=null)
{
configFile=module.getConfigFile();
if(configFile==null)
continue;
}
else
continue;
}
Long lasttime=fileLasttime.get(moduleName);
if(lasttime==null)
continue;
if(lasttime==0){
File file = new File(configFile);
long nowTime = file.lastModified();
fileLasttime.put(moduleName,nowTime);
continue;
}
Long nowTime =checkFileTime(configFile,lasttime);
if(nowTime >0)
{
fileLasttime.put(moduleName,nowTime);
putMsg(moduleName,reloadMsg);
putLog(moduleName+"重新配置加载",LogLevel.WARN);
}
}
}
protected Long checkFileTime(String moduleConfigFile, Long lastTime) {
File file = new File(moduleConfigFile);
long newTime = file.lastModified();
if(newTime >lastTime)
return newTime;
else
return Long.valueOf(0);
}
public class MyUnchecckedExceptionhandler implements Thread.UncaughtExceptionHandler {
private IObject module;
private TLMsg msg;
public MyUnchecckedExceptionhandler(IObject module, TLMsg msg){
this.module =module;
this.msg =msg ;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
module.putMsg(module,msg.setParam("throwable" ,e));
}
}
}
模块实例化后在init()方法中启动线程。通过给自身发送启动消息,消息参数为异步(线程方式),模块自动运行,开始检测配置文件,设定每十秒循环一次。当某个模块的配置文件发送改变(根据文件修改时间)时,给这个模块发送重新加载配置文件消息。下面看监听模块的配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<moduleConfig>
<params>
<delay value="4" />
<modules value="moduleFactory;appCenter;cache;website;qqdbserver;abMsgtable;infomationControl;
log;urlMap;velocity;servletCache;clientJsonToMsg;webauth;webUser;memberControl;userInPutFilter;servletCache;informationModule" />
</params>
</moduleConfig>
modules项中定义监听被监听模块 ,由于监听模块也监听自身,因此随时可加入、取消被监听模块,配置方便灵活。当检测被监听模块的配置文件发生改变,给监听模块发送重新加载配置消息,完成动态配置。
监听模块如何启动,在工厂启动项中增加一个模块启动即可,如果取消功能,从配置中删掉即可。
<factoryBoot>
<msg action="getModule" moduleName="monitorConfig" usePreReturnMsg="false" />
</factoryBoot>
上面为在工厂启动中,增加启动消息来启动监听模块。监听模块启动后自动开启新的线程,而不影响主线程,因此对其他模块透明。
下图为一个动态更新例子。程序运行中为方便查看日志,需要动态对模块进行日志输出,比如开启或停止哪个模块的日志,图中更改了日志模块的配置文件log_config.xml ,立即被监听模块检查到而重载入配置。
现在我们来看热部署----动态更新模块类。既然能动态更新配置了,那么动态更新模块自然实现了。在消息编程框架中,所有的模块由模块工厂统一创建,那么思路就是重新由工厂创建一个就行了 。我的消息框架里模块之间互动不是通过对象实例,而是对象名称。一个名称对应哪个具体类不重要,重要是实现功能就行了 。下面为模块工厂配置文件的模块配置项:
<modules>
<!-- 系统模块,不要去掉!以符号.开始的类在默认包下 -->
<module name="broadcast" classfile="cn.tianlong.tlobjcet.base.TLMsgBroadCast" />
<module name="log" classfile="cn.tianlong.tlobjcet.base.TLLog"
configfile="log_config.xml"/>
<module name="monitorConfig" classfile=".servletutils.TLMonitorConfigForThread"
configfile="monitorConfig_config.xml"/>
<module name="msgbus" classfile="cn.tianlong.tlobjcet.base.TLMsgBus" />
对于 log 模块 ,如其他模块调用log模块,(方法:putmsg(“log”,msg)),调用是用的模块名称“log”,现在这个log名字在工厂中对应的是TLLog类,如果想更换类则更改工厂配置。比如更改为 TLNewLog,则“log” 指向新的类,对其他模块不影响,因为其他模块使用的是其名字。那么实现热部署可由以下两步:
第一步:更改模块工厂里面的配置 ,将log 指向新类 TLNewLog
<module name="log" classfile="cn.tianlong.tlobjcet.base.TLNewLog"
configfile="log_config.xml"/>
由于动态配置,模块工厂读取了新的配置。但这时候运行中的模块实例还没有变,log名字还是指向的TLlog实例。我们必须将新类TLNewLog实例化代替过去的实例。这通过下面第二步完成。
第二步:更改模块配置文件 log_confif.xml 刷新实例。
对于配置文件param项参数 ,设置<reload value="true" />,参数意思是,模块重新实例化。
由于配置文件更改,模块开始刷新配置,刷新配置代码reload()方法中:
private void reload()
{
if(params != null && params.get("reload")!=null && (params.get("reload")).equals("true"))
putMsg(moduleFactory,createMsg().setAction("reloadModule").setParam("moduleName",name)) ;
}
模块给模块工厂发送reloadModule的消息重新加载模块,在模块工厂中消息对应的方法:
private void reloadModule(Object fromWho, TLMsg msg) {
String moduleName= (String) msg.getParam("moduleName");
if(moduleName==null)
moduleName=((IObject)fromWho).getName();
Object module = createModule( moduleName,moduleName);
if (module != null)
{
modules.put(moduleName, module);
putToSession(moduleName,module);
TLMsg resetmsg =createMsg().setAction(RESETMODULE).setParam("moduleName",moduleName)
.setParam("module",module);
for (Map.Entry<String, Object> entry : modules.entrySet()) {
if(entry.getKey().equals(name) || entry.getKey().equals(SESSIONDATAMODULE))
continue;
Object moduleObj = entry.getValue();
if(moduleObj instanceof TLBaseModule)
putMsg((IObject) moduleObj,resetmsg);
}
putLog(moduleName+" 模块重新加载",LogLevel.WARN);
}
}
上面代码中,模块工厂重新创建了新模块。由于其他模块还保留着原来模块的实例(为方便,每个模块保存它用过模块的实例),因此要给其他模块发出模块更新的通知消息。对于其他模块来说,这一切都是透明的,还是一样的使用“log“”,但是log指向的对象实例已经变了。这样我们没有增加复杂的代码,通过动态配置与工厂统一创建的功能,将模块更换了 。
对于热部署增加新的模块,因为还没有实例化,无需刷新对象实例,则更加简单,在工厂配置中增加一个module项即可,动态配置生效后就可使用。