这样做的最终效果就是修改了项目的运行方式。原先的运行方式是以tomcat为中心,由tomcat来启动和终止项目,现在是由我们的启动程序 为中心,由启动程序来负责启动和终止项目。就相当于现在流行的cs程序一样,有单独的启动脚本,在启动时进行环境预初始化,更新程序以及其它操作,待完成 之后再进行最终的项目启动。
这篇主要讲解如何使用embeded tomcat在代码中进行启动和终止。网上的一般文章均为tomca5.x来做,这里使用了最新的tomcat7,因为tomcat7为embeded开 发,单独发布了org.apache.tomcat.embed包,以进行独立的embed开发。以下是相应的maven包
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | < dependency > < groupid >org.apache.tomcat.embed
|
使用了embed包中的core包,以及用于编译jsp的jasper包,然后是工具类以及进行上场记录的logging-juli包。开始写代码:
1 2 3 4 5 6 7 | //设置工作目录 string catalina_home = "d:/" ; tomcat tomcat = new tomcat(); tomcat.sethostname( "localhost" ); tomcat.setport(startport); //设置工作目录,其实没什么用,tomcat需要使用这个目录进行写一些东西 tomcat.setbasedir(catalina_home); |
上面使用了tomcat类来进行启动类,在tomcat7以前均是使用一个叫embed类来进行启动,在tomcat7之后,embed类被不建 议使用,而建议使用新的tomcat类来进行启动了。然后设置主机名,端口,再设置一个工作目录。这个工作目录可以是任意目录,主要是tomcat需要这 个目录来记录一些东西,比如记录word信息,日志信息(如果配置了日志的话),以及临时文件存储等。
1 2 3 4 5 6 7 8 | //设置程序的目录信息 tomcat.gethost().setappbase( "e:/" ); // add aprlifecyclelistener standardserver server = (standardserver) tomcat.getserver(); aprlifecyclelistener listener = new aprlifecyclelistener(); server.addlifecyclelistener(listener); //注册关闭端口以进行关闭 tomcat.getserver().setport(shutdownport); |
上面的代码,首先设置我们的项目程序所在的appbase,即放项目代码的地方。在通常的tomcat配置中,这个目录一般是webapps。接 着设置一个listener,这个listener主要是负责启动一些比如html native支持程序以及ipv6等信息配置(可以忽略)。接着是配置一个关闭的注册端口,当向这个端口发送信息时,就可以达到关闭tomcat的目的 (后面会讲)。
1 2 3 4 5 6 7 8 9 | //加载上下文 standardcontext standardcontext = new standardcontext(); standardcontext.setpath( "/aa" ); //contextpath standardcontext.setdocbase( "aa" ); //文件目录位置 standardcontext.addlifecyclelistener( new tomcat.defaultwebxmllistener()); //保证已经配置好了。 standardcontext.addlifecyclelistener( new tomcat.fixcontextlistener()); standardcontext.setsessioncookiename( "t-session" ); tomcat.gethost().addchild(standardcontext); |
我们单独使用了一个context来为这个host添加上下文,tomcat本身提供一个方法tomcat.addweb方法来添加项目包,不过 由于这里需要单独设置一个tomcat的sessionname,所以使用与与tomcat.addweb实现类似的方法来添加一个项目包。
以上代码中有两个需要注意的listener,一个是defaultwebxmllistener,这个是由tomcat加载一些默认的配置信 息,比如jspservlet,以及一些繁复的mime/type信息;加上这个,就不需要我们自己去写这么多的配置,因为每个项目都需要这些。这个配置 与tomcat目录下的conf/web.xml中的配置一样,只不过这里是代码化了。第二个是fixcontextlistener,这个主要是在项目 部署完后,将这个上下文设置为configured,表示已经配置好了(不然,tomcat启动时会报错,即相应上下文还未配置好)。
配置ok了之后,就是启动tomcat了:
1 2 | tomcat.start(); tomcat.getserver().await(); |
启动tomcat,并让tomcat在关闭端口上监听。如果没有最后一句,程序将直接结束,保证监听之后,tomcat将一直监听关闭事件,待有关闭事件之后才结束当前程序。所以如果想要关闭当前的tomcat,只需要向关闭端口发送一些信息即可:
1 2 3 4 5 6 7 8 9 | private static void shutdown() throws exception { socket socket = new socket( "localhost" , shutdownport); outputstream stream = socket.getoutputstream(); for ( int i = 0 ;i < shutdown.length();i ) stream.write(shutdown.charat(i)); stream.flush(); stream.close(); socket.close(); } |
这样即可达到关闭tomcat的目的。
实际上看整个项目代码,项目代码的运行,就是一个配置一个基础的server.xml(即tomcat目录下的 conf/server.xml),先配置运行端口,关闭监听端口;然后配置运行的host以及添加一个上下文context,最后就开始运行并开始监 听。对照这个程序,再看一下server.xml中的配置信息,就很容易明白以上这段代码了。
uriencoding
属性部分 org.apache.catalina. strict_servlet_compliance
属性部分为什么写这篇文档?
使 用过hibernate, spring或其他大型组件,写过50个类以上的网络应用程序(web application)的开发者应该知道,当系统中有很多类时,如果开启了tomcat的reloadable=true,那么每当相关文件改变 时,tomcat会停止web app并释放内存,然后重新加载web app.这实在是个浩大的工程。
所以我总是在想如果能有只重载某几个类的功能,将极大的满足我这个即时调试狂。
去年我在论坛上发帖,才发现已经有一些应用服务器具有了这个功能,比如weblogic, websphere, 等等。好像还有一个很酷的名字,叫开发模式。看来我还是孤陋寡闻了点。
当然很多人都是在tomcat上开发,包括我。我很喜欢它的轻小,那些大内存和高cpu消耗的应用服务器不愧为硬件杀手,没理由不改进tomcat :)。
最终实现功能
我没有时间去研究tomcat的文件监听机制,也没时间去把他写成”开发模式”这么完整的功能,我最终实现的是,实现重载功能的测试jsp--很抱歉我还是没办法写得更完整。当然,你可以在这个基础上进行改进。
阅读须知
阅读本文,你应该具备以下知识
jvm 规范有关类加载器的章节
tomcat 类加载机制
java 反射机制
ant
(好象该网址被不定时封锁,有时能上,有时不能)
最好在你的电脑上安装ant,因为tomcat源码包使用ant从互联网获得依赖包。不过我也是修改了一个错误才使它完全编译通过。
当然,你也可以用其他ide工具检查并添加依赖包,在ide中,其实你只需要添加jar直到使org.apache.catalina.loader.webappclassloader无错即可。
修改过程
说明
新添加的代码请添加到java文件的末尾,因为我在说明行数的时候,尽量符合原始行数
web app类加载器
在tomcat中,org.apache.catalina.loader.webappclassloader是web app的类加载器,所以需要修改它实现重载功能。
资源列表
在webappclassloader中,有一个map类型属性resourceentries,它记载了web app中web-inf/classes目录下所加载的类,因此当我们需要重载一个类时,我们需要先将它在resourceentries里删除,我编写了一个方法方便调用:
public boolean removeresourceentry(string name) {
if (resourceentries.containskey(name)) {
resourceentries.remove(name);
return true;
}
return false;
}
是否重载标志
让webappclassloader需要知道加载一个类是否使用重载的方式。所以我建立一个boolean 类型的属性和实现它的getter/setter方法:
private boolean isreload = false;
public boolean isreload() {
return isreload;
}
public void setreload(boolean isreload) {
this.isreload = isreload;
}
动态类加载器
根据jvm类加载器规范,一个类加载器对象只能加载一个类1次,所以重载实际上是创建出另一个类加载器对象来加载同一个类。当然,我们不需要再创建一个webappclassloader,他太大而且加载规则很复杂,不是我们想要的,所以我们创建一个简单的类加载器类org.apache.catalina.loader.dynamicclassloader:
package org.apache.catalina.loader;
import java.net.url;
import java.net.urlclassloader;
import java.security.codesource;
import java.util.*;
/**
* 动态类加载器
*
* @author peter
*
*/
public class dynamicclassloader extends urlclassloader {
/* 父类加载器 */
private classloader parent = null;
/* 已加载类名列表 */
private list classnames = null;
/**
* 构造器
*
* @param parent
* 父类加载器,这里传入的是webappclassloader
*/
public dynamicclassloader(classloader parent) {
super(new url[0]);
classnames = new arraylist();
this.parent = parent;
}
/**
* 从类的二进制数据中加载类.
*
* @param name
* 类名
* @param classdata
* 类的二进制数据
* @param codesource
* 数据来源
* @return 成功加载的类
* @throws classnotfoundexception
* 加载失败抛出未找到此类异常
*/
public class loadclass(string name, byte[] classdata, codesource codesource) throws classnotfoundexception {
if (classnames.contains(name)) {
// system.out.println("此类已存在,调用 loadclass 方法加载.");
return loadclass(name);
} else {
// system.out.println("新类, 记录到类名列表,并用类定义方法加载类");
classnames.add(name);
return defineclass(name, classdata, 0, classdata.length, codesource);
}
}
/* *
* 重载此方法,当要加载的类不在类名列表中时,调用父类加载器方法加载.
* @see java.lang.classloader#loadclass(java.lang.string)
*/
public class loadclass(string name) throws classnotfoundexception {
if (!classnames.contains(name)) {
//system.out.println("不在类名列表中,调用父类加载器方法加载");
return parent.loadclass(name);
}
return super.loadclass(name);
}
}
在webappclassloader中添加dynamicclassloader
添加属性
private dynamicclassloader dynamicclassloader = new dynamicclassloader(this);
添加重建方法,以便需要再次重载时替换掉上次的类加载器对象
public void recreatedynamicclassloader() {
dynamicclassloader = new dynamicclassloader(this);
}
修改调用点
第832行,公开findclass方法
public class findclass(string name) throws classnotfoundexception {
第1569行,添加如下一行代码。
if (isreload) removeresourceentry(name);
第1577行,这里好像是一个bug,具体原因我忘了-_-||
if ((entry == null) || (entry.binarycontent == null))
改为
if ((entry == null) || (entry.loadedclass == null && entry.binarycontent == null))
第1633~1636行
if (entry.loadedclass == null) {
clazz = defineclass(name, entry.binarycontent, 0, entry.binarycontent.length,
codesource);
改为
byte[] classdata = new byte[entry.binarycontent.length];
system.arraycopy(entry.binarycontent, 0, classdata, 0,
classdata.length);
if (entry.loadedclass == null) {
clazz = isreload ?
dynamicclassloader.loadclass(name,
classdata, codesource) :
defineclass(name,
classdata, 0, classdata.length, codesource);
测试代码
test.jsp
我测试用的jsp为$catalina_home/webapps/root/test.jsp,由于webapp里面并不会显式加载tomcat的核心类,所以我们需要用反射代码调用webappclassloader的方法。代码如下:
<%
classloader loader = (thread.currentthread().getcontextclassloader());
class clazz = loader.getclass();
java.lang.reflect.method setreload = clazz.getmethod("setreload", new class[]{boolean.class});
java.lang.reflect.method recreate = clazz.getmethod("recreatedynamicclassloader", null);
java.lang.reflect.method findclass = clazz.getmethod("findclass", new class[]{string.class});
recreate.invoke(loader, null);
setreload.invoke(loader, new object[]{true});
class a = (class)findclass.invoke(loader, new object[]{"org.aclass"});
setreload.invoke(loader, new object[]{false});
a.newinstance();
// 如果你使用下面这行代码,当重编译类时,请稍微修改一下调用它的jsp,让jsp也重新编译
//org.aclass a = (org.aclass)a.newinstance();
// 下面这些代码是测试当一个类不在dynamicclassloader类名列表时的反应
//a.test();
//java.lang.reflect.method test = a.getclass().getmethod("test", null);
//test.invoke(a, null);
%>
org.aclass
package org;
public class aclass {
public aclass() {
// 修改输出内容确认tomcat重新加载了类
system.out.println("aclass v3");
}
public void createbclass() {
new bclass();
}
}
org.bclass
package org;
public class bclass {
public bclass() {
//修改输出内容确认tomcat重新加载了类
system.out.println("bclass v1");
}
}
测试步骤
按照上述步骤修改tomcat源码并编译。
用winzip/winrar/file-roller打开$catalina_home/server/lib/catalina.jar。把前面编译完成后的org.apache.catalina.loader目录下的class文件覆盖jar中同名文件。
编译org.aclass和org.bclass
启动tomcat并在浏览器中打开测试页http://localhost:8080/test.jsp
修改org.aclass中的system.out.println();语句并重编译类。
按下f5按键刷新浏览器。
查看tomcat控制台是否输出了不同的语句?
good luck! :)))
由于程序中使用了jtds驱动来连接数据库。
一段时间后,我发现tomcat的temp文件夹内jtds*.tmp文件(形如jtds424647.tmp)越来越多,容量也越来越大。有一次清理时,据然有几个g多,严重的影响了系统的运行速度。
解决措施:
如果是linux系统
可以指定启动时-djava.io.tmpdir=/tmp
这个目录系统会用cron脚本自动清理文件
如果是windows,需要定期清理tomcat的tmp文件
可以用windows自带的定时任务器建立如下deltmp.bat的文件
@echo off
:start
::启动过程,切换目录
set pwd=�%
cd %1
echo 工作目录是:& chdir
:clean
::主处理过程,执行清理工作
@echo on
rem @for /r %%c in (.log) do @if exist %%c ( rd /s /q %%c & echo 删除目录%%c)
@echo off
@del logs\*.log
@del temp\*.tmp
del /f /s /q e:\osaplatform\web-inf\logs\*.log.*-*
echo "当前目录下的log信息已清除"
goto end
:noclean
::分支过程,取消清理工作
echo "log信息清楚操作已取消"
goto end
:end
::退出程序
cd "%pwd%"
rem pause
tomcat由于运行的时间过长造成内存不宜释放,导致运行性能的降低,定时重启tomcat有利于提升系统的性能,
首先建立如下的bat文件
set java_home=c:\program files\java\jdk1.6.0_06
e:\tomcat6_hb\bin\service.bat install tomcat604
注册为windows服务
然后在建立tomcat.bat,并将此文件放到windows定期任务中
@echo off
net stop tomcat5
rem ping 20个包,实现延时功能
ping 127.0.0.1 -n 20
net start tomcat5
如果需要解除服务,则建立如下文件,直接执行即可
set java_home=c:\program files\java\jdk1.6.0_06
e:\tomcat6_hb\bin\service.bat remove tomcat604
问题原来出在host的appbase设置上,原来的设置是这样的
改成不设置appbase,只对指定的context设置docbase就ok了
<hostname="www.xxx.com"appbase=""
unpackwars="false"autodeploy="true"
xmlvalidation="false"xmlnamespaceaware="false">
<contextdocbase="/usr/local/projects/xxx/web"path=""reloadable="true"workdir="work">
context>
host>
<hostname="admin.xxx.com"appbase=""
unpackwars="false"autodeploy="true"
xmlvalidation="false"xmlnamespaceaware="false">
<contextdocbase="/usr/local/projects/xxx/admin"path=""reloadable="true"workdir="work">
context>
为什么不设置appbase就ok来呢?
我们采取的是集中管理的办法。主要技术:
1.设置context 的crosscontext="true",使得各个web应用的servletcontext是可以互访的
xmlvalidation="false" xmlnamespaceaware="false">
2.主动设置cookies,设置jsessionid为被共享的session的id,统一利用requestsessionid在指定的一个 servletcontext里的一个map查找对于的session,需要存取attribute都对取得的session操作
3.用监听器监听属性的失效
所有context的实现支持如下属性:
属性 | 描述 |
---|---|
backgroundprocessordelay |
这个 值代表在context及其子容器(包括所有的wrappers)上调用backgroundprocess方法的延时,以秒为单位。如果延时值非负,子容器不会被调用,也就是说子容器使用自己的处理线程。如果该值为正,会创建一个新的线程。在等待指定的时间以后,该线程在主机及其 子容器上调用backgroundprocess方法。context利用后台处理session过期,监测类的变化用于重新载入。如果没有指定,该属性的缺省值是-1,说明context依赖其所属的host的后台处理。 |
classname |
实现的java类名。该类必须实现org.apache.catalina.context 接口。如果没有指定,使用标准实现(在下面定义)。 |
cookies |
如果想利用cookies来传递session identifier(需要客户端支持cookies),设为ture。否则为false,这种情况下只能依靠url rewriting传递session identifier。
|
crosscontext |
如果想在应用内调用servletcontext.getcontext()来返回在该虚拟主机上运行的其他web application的request dispatcher,设为true。在安全性很重要的环境中,设为false,使得getcontext()总是返回null。缺省值为false。 |
docbase |
该web应用的文档基准目录(document base,也称为context root),或者是war文件的路径。可以使用绝对路径,也可以使用相对于context所属的host的appbase路径。
|
override |
如果想利用该context元素中的设置覆盖defaultcontext中相应的设置,设为true。缺省情况下使用defaultcontext中的设置。 |
privileged |
设为true,允许context使用container servlets,比如manager servlet。
|
path |
web应用的context路径。catalina将每个url的起始和context path进行比较,选择合适的web应用处理该请求。特定host下的context path必须是惟一的。如果context path为空字符串(""),这个context是所属host的缺省web应用,用来处理不能匹配任何context path的请求。
|
reloadable |
如果希望catalina监视/web-inf/classes/和/web-inf/lib下面的类是否发生变化,在发生变化的时候自动重载web application,设为true。这个特征在开发阶段很有用,但也大大增加了服务器的开销。因此,在发布以后,不推荐使用。但是,你可以使用manager应用在必要的时候触发应用的重载。 |
wrapperclass |
org.apache.catalina.wrapper实现类的名称,用于该context管理的servlets。如果没有指定,使用标准的缺省值。 |