但是,能不能不重启tomcat就有效果呢?
首先需要重启的原因是因为修改了context.xml后,tomcat会自动redeploy app,首先就造成了之前的session都不可用,其次,classloader也发生了变化(由于再次使用到dom4j的SAXReader去读取xml文件时候,会报类型转换异常,但明明是同样的类型,其实原因是虽然类名相同,但是在不同的classloader下,造成了这样的错误,中间我还替换了一个别人自己hack的dom4j.jar但是,虽然解决了这个错,还是有其他一些错误,头晕中,还是换条路吧),同时,我们也知道,如果app的web.xml发生了修改,tomcat也会自动redeploy响应的app,于是乎,就找找tomcat在哪里去监控这些文件是否发生变化吧。
试试看吧,开始看tomcat的source............(看的头晕眼花)
.
.
.
还在看,继续找~~~.......
终于有一天,发现Context接口中有个findWatchedResources,removeWatchedResource这样的方法,看似有点像,那就试试看调用一下removeWatchedResource吧,怎么才能获取到context对象呢,发现tomcat默认实现都在
org.apache.catalina.core包下,自然Context接口的默认实现叫做StandardContext,怎么获取到这个对象的实例呢?
这里就涉及到tomcat的整体架构了,网上好多相关介绍,说的都很详细,我这里涉及到的就是Server->Service->Engin->Host->Context,这样一个架构:
通过Server server = ServerFactory.getServer();这个静态方法就可以获取到最外层的Server对象;server中可以有多个service,因此需要通过domain来匹配,默认domian就是Catalina;service.getContainer()获取到engine;engine中又多个host,需要通过findChild(String name)来找到对应的host,这里我们的name是localhost;host再调用findChildren就可以获得到所有的StandardContext对象了。
Container[] containers=(Container[])host.findChildren(); for(int i=0;i<containers.length;i++){ if(containers[i] instanceof StandardContext){ StandardContext context = (StandardContext)containers[i]; String[] watchedResources=context.findWatchedResources(); for(String watchedResource : watchedResources){ if(watchedResource.indexOf("conf\\context.xml")>0){ log.debug("Remove watchedResource " + watchedResource + " from "+context.getName()); context.removeWatchedResource(watchedResource); } } } }
这段代码就是获取到StandardContext后,去调用removeWatchedResource方法,然后放在我自己的tomcat admin中的一个servlet的init方法中去调用一下,看看结果,哇,貌似可以了,commit change后,tomcat admin的session还在,我可以继续操作,回头看看console,发现还是有app被redeploy了,啥情况,一下就想到了是不是调用的时机不对,部署到admin这个应用之前的应用中的resource都被删除了,之后部署的没有被删除(从日志中发现的),那是不是放到我commit change之前去调用,就可以了呢??试了发现,看似日志里每个应用的resource都删除了,但是所有的app都被redeploy了。
回顾一下,发现,其实之前在servlet的init方法中调用后,只有这个app是没有redeploy的,继续看tomcat source吧。
添加了对org.apache.catalina下的debug等级的日志输出,发现,所有servlet的装载,包括调用init方法都是在Context的start方法中执行的,然后再会调用HostConfig中的start方法,HostConfig的实例其实是作为一个lifecycleListener在StandardHost的init方法中被添加到其lifecycle属性中,当收到Lifecycle.START_EVENT时候,就会去调用这些listener的start方法,这时候就会去把context的warchedResource添加到HostConfig中的deployed里面去,
deployed是个map,值是DeployedApplication实例,这个类是HostConfig的一个内部类,
resource就是被添加到这个类实例的reloadResources属性中去。
那是不是可以获取到这些DeployedApplication实例,在从他们的属性reloadResources中去删除对应的resource呢?
看似不行,HostConfig的deployed属性是protected的,别且之前也说了DeployedApplication是protected内部类。
继续苦闷中。既然是protected,那就是可以自己来继承一下HostConfig嘛?看了下StandardHost里面,获取HostConfig时候貌似还是通过反射来的,类路径是可以set的,于是找到了server.xml中的Host标签,找到name是localhost的,添加了一个属性hostConfigClass="com.elitecrm.tomcat.EliteHostConfig",并且自己实现的EliteHostConfig继承了HostConfig:
public class EliteHostConfig extends HostConfig { private static final Log log = LogFactory.getLog(EliteHostConfig.class); public HashMap<String,DeployedApplication> getDeployed(){ return super.deployed; } public void removeReloadResources(){ HashMap<String,DeployedApplication> deployed=getDeployed(); for(String contextPath:deployed.keySet()){ DeployedApplication app = deployed.get(contextPath); String[] resources = (String[]) app.reloadResources.keySet().toArray(new String[0]); for(int i=0;i<resources.length;i++){ log.debug("Remove resources " + resources[i]+ " for context " + contextPath); if(resources[i].indexOf("conf"+File.separator+"context.xml")>=0){ app.reloadResources.remove(resources[i]); } } } } }
打包成jar,放到了tomcat的lib目录下
嘿嘿,还真的可以:
LifecycleListener[] lls= host.findLifecycleListeners(); for(int i=0;i<lls.length;i++){ LifecycleListener ll=lls[i]; if(ll instanceof EliteHostConfig){ EliteHostConfig hostConfig=(EliteHostConfig)ll; hostConfig.removeReloadResources(); } }
现在,删除resource的代码变成了这样。
在登录我的tomcat admin登录界面时候,调用这个方法,然后,commit change都没问题了,所有app都老老实实,一动不动,爽了。
那这样commit change后的datasource,能不能直接使用了呢?
测试发现不行,继续看tomcat source,查找如何创建对应的resource的。
找到了StandardContext里面有类似:context.getNamingResources().addResourceLink(resourceLink);
这样的代码,但是传递的resourceLink是个ContextResourceLink对象,然后又找到了这个对象的创建:
ContextResourceLink resourceLink = new ContextResourceLink(); resourceLink.setGlobal(resourceLinkName); resourceLink.setName(resourceLinkName); resourceLink.setType("javax.sql.DataSource");
同时发现context.getNamingContextListener().addResourceLink(resourceLink);这样的代码,于是又添加上了:
NamingContextListener ncl = context.getNamingContextListener(); ContextAccessController.setWritable(ncl.getName(), context); ncl.addResourceLink(resourceLink);
这里还不能直接调用addResourceLink或者removeResourceLink,其实这些方法里面是jndi的bind和unbind方法,但是tomcat设置了Context的resource是read only的,所以要先自己去执行的ContextAccessController.setWritable方法,才能去bind或者unbind。
尝试下来,发现只需要对context的NamingResources做addResourceLink,不需要对NamingContextListener去手动bind。
这样,终于算是基本完成了,可以再我的tomcat admin上去添加修改删除数据源,并且可以commit change保存数据到配置文件,并且不会引发app的redeploy,生产环境中也可以直接使用,并且添加后的数据源直接可以被调用到。
啦啦啦~
写了大托大托的废话,没啥深入的,完全是自己的一点体会,能力有限,写的比较土,仅作自己笔记使用,欢迎吐槽。