对tomcat admin中的datasource管理的扩展(二)

但是,能不能不重启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,生产环境中也可以直接使用,并且添加后的数据源直接可以被调用到。

 

 

啦啦啦~

 

 

写了大托大托的废话,没啥深入的,完全是自己的一点体会,能力有限,写的比较土,仅作自己笔记使用,欢迎吐槽。

你可能感兴趣的:(java,tomcat,JNDI)