摘要:本文介绍在tomcat中部署项目的几种方式以及内部相关的类。
众所周知在tomcat中部署一个项目是一件很轻松的事情,我们总结一下在tomcat中部署项目的四种方式。
-
第一种方式是我们最常见的,直接把war包丢到
webapp
目录下即可,或者将war包解压出来的文件夹放到webapp
目录下也可以。 -
第二种方式是修改
server.xml
文件,在Host
标签下新增Context
标签,例如下面:<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Context path="/test" docBase="G:apache-tomcat-8.0.30myapps"></Context> </Host>
-
第三种方式是在
conf/Catalina/localhost
中添加xxx.xml
,xxx
指的是你要访问的路径,例如添加的是test.xml
,那么最终的访问路径就是localhost:8080/test/
,文件内容是如下:<Context path="/test" docBase="G:apache-tomcat-8.0.30myapps"></Context>
-
第四种方式其实已经脱离了原有的默认形式,那就是在
server.xml
添加一个host,因为在tomcat中默认的host是localhost
,所以新加了一个host以后,所有的操作都需要类似之前默认的localhost
。在server.xml
新加一个自定义host,如下:<Host name="abc" appBase="G:apache-tomcat-8.0.30 - test-deploymyapps" unpackWARs="true" autoDeploy="true"></Host>
添加了以后需要在
hosts
文件中设置新添加的host127.0.0.1 abc
设置好后,访问路径就是
http://abc:8080/项目名称/
需要注意的是:第三种方式配置的Context
标签,如果docBase
配置的是相对路径,那么解压出来的war包会放到默认的webapp
目录下。如果配置的是绝对路径文件夹,那么该文件夹里面的内容是war包解压出来的文件夹,如果是配置的绝对路径war包,那么该war包会默认解压到webapp
目录下,解压出来的名称同path
名称(例如上面path
配置的是/test
,如果docBase
配置绝对路径war包,那么最后会把这个war包解压到webapp
目录下,文件夹名字叫test
)。
好了,外部配置的方式我们都搞清楚了,现在我们要去查看下tomcat在内部是如何实现发布项目的,下面分享下我第一次在不查询任何资料的时候是如何查到相关类的相关方法的:
1、我的想法:在tomcat内部存在很多默认的应用,比如ROOT
,example
等,tomcat肯定要发布这些项目,所以我先启动tomcat,查看日志,找到一些关键字如下:
Deploying web application directory F:coooooooooooooooooooolTomcatSourcewebappsdocs
搜索关键字Deploying web application directory
,结果如下:
看来日志是tomcat统一写到properties文件中的,再继续根据key来查找,随便找个hostConfig.deploy
,全局搜索如下:
可以看出来 代码中没有任何引用,再查另外一个keyhostConfig.deployDir
,结果如下:
因为符合条件的就一个,我们点进去,找到的是一个HostConfig
类的deployDirectory(ContextName cn, File dir)
方法,我们继续查找这个方法的引用,过程我就不一一赘述,最后查处的结果指向的是lifecycleEvent(LifecycleEvent event)
,如下:
/**
* Process the START event for an associated Host.
*
* @param event The lifecycle event that has occurred
*/
@Override
public void lifecycleEvent(LifecycleEvent event) {
// Identify the host we are associated with
try {
host = (Host) event.getLifecycle();
if (host instanceof StandardHost) {
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
setContextClass(((StandardHost) host).getContextClass());
}
} catch (ClassCastException e) {
log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
//11111
check();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
//22222222
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}
可以查询到,最后指向的分别是check()
和start()
方法,我们可以得出一个大概的结论,发布项目是在tomcat启动的时候进行的,是触发了某个组件的生命周期事件来完成的。那么具体是哪个类的生命周期事件呢?其实看方法注释就能看出来了,associated Host
,那么是跟Host
有关的生命周期事件,也就是StandardHost
。我们查看下StandardHost
的start()
方法。
@Override
protected synchronized void startInternal() throws LifecycleException {
// Set error report valve
//111111
String errorValve = getErrorReportValveClass();
if ((errorValve != null) && (!errorValve.equals(""))) {
try {
boolean found = false;
Valve[] valves = getPipeline().getValves();
for (Valve valve : valves) {
if (errorValve.equals(valve.getClass().getName())) {
found = true;
break;
}
}
if(!found) {
Valve valve =
(Valve) Class.forName(errorValve).newInstance();
getPipeline().addValve(valve);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString(
"standardHost.invalidErrorReportValveClass",
errorValve), t);
}
}
//222222
super.startInternal();
}
可以看到标注1到标注2的地方都在给StandardHost
安装Valve
,标注2的地方调用了父类的startInternal()
,也就是ContainerBase
,startInternal()
方法如下,因为我们在ContainerBase
类的讲解中讲过,这里删减了大部分代码,如下:
@Override
protected synchronized void startInternal() throws LifecycleException {
// Start our child containers, if any
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<Future<Void>>();
for (int i = 0; i < children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}
boolean fail = false;
for (Future<Void> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString("containerBase.threadedStartFailed"), e);
fail = true;
}
}
if (fail) {
throw new LifecycleException(
sm.getString("containerBase.threadedStartFailed"));
}
// Start the Valves in our pipeline (including the basic), if any
if (pipeline instanceof Lifecycle)
((Lifecycle) pipeline).start();
//111111111
setState(LifecycleState.STARTING);
// Start our thread
threadStart();
}
这个startInternal()
方法,我们在ContainerBase
文章中没有提到标注1处的方法,我们查看下源码:
//此处代码 来自LifecycleBase
protected synchronized void setState(LifecycleState state)
throws LifecycleException {
setStateInternal(state, null, true);
}
private synchronized void setStateInternal(LifecycleState state,
Object data, boolean check) throws LifecycleException {
if (log.isDebugEnabled()) {
log.debug(sm.getString("lifecycleBase.setState", this, state));
}
if (check) {
// Must have been triggered by one of the abstract methods (assume
// code in this class is correct)
// null is never a valid state
if (state == null) {
invalidTransition("null");
// Unreachable code - here to stop eclipse complaining about
// a possible NPE further down the method
return;
}
// Any method can transition to failed
// startInternal() permits STARTING_PREP to STARTING
// stopInternal() permits STOPPING_PREP to STOPPING and FAILED to
// STOPPING
if (!(state == LifecycleState.FAILED ||
(this.state == LifecycleState.STARTING_PREP &&
state == LifecycleState.STARTING) ||
(this.state == LifecycleState.STOPPING_PREP &&
state == LifecycleState.STOPPING) ||
(this.state == LifecycleState.FAILED &&
state == LifecycleState.STOPPING))) {
// No other transition permitted
invalidTransition(state.name());
}
}
this.state = state;
String lifecycleEvent = state.getLifecycleEvent();
//111111
if (lifecycleEvent != null) {
fireLifecycleEvent(lifecycleEvent, data);
}
}
protected void fireLifecycleEvent(String type, Object data) {
lifecycle.fireLifecycleEvent(type, data);
}
public void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);
LifecycleListener interested[] = listeners;
for (int i = 0; i < interested.length; i++)
interested[i].lifecycleEvent(event);
}
点进去才发现,原来调用的是LifecycleBase
类中的方法,这个我们在之前的文章中也讲解过,查看标注1的地方可以看到,就是在这里触发了生命周期的方法,对StandardHost
的所有listener
触发一下start
事件。
那么StandardHost
的listener
和HostConfig
类是如何连接起来的呢。思来想去看看,先找找看构造函数,set方法都没有找到相关的设置。那有可能是server.xml
中解析出来的配置,我们就需要去查看下Catalina
类里面的load()
方法中的Digester digester = createStartDigester();
方法了。
digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
在createStartDigester()
方法中,找到了配置Host
标签的方法,查看HostRuleSet
相关方法如下:
@Override
public void addRuleInstances(Digester digester) {
digester.addObjectCreate(prefix + "Host",
"org.apache.catalina.core.StandardHost",
"className");
digester.addSetProperties(prefix + "Host");
digester.addRule(prefix + "Host",
new CopyParentClassLoaderRule());
//1111
digester.addRule(prefix + "Host",
new LifecycleListenerRule
("org.apache.catalina.startup.HostConfig",
"hostConfigClass"));
digester.addSetNext(prefix + "Host",
"addChild",
"org.apache.catalina.Container");
digester.addCallMethod(prefix + "Host/Alias",
"addAlias", 0);
//Cluster configuration start
digester.addObjectCreate(prefix + "Host/Cluster",
null, // MUST be specified in the element
"className");
digester.addSetProperties(prefix + "Host/Cluster");
digester.addSetNext(prefix + "Host/Cluster",
"setCluster",
"org.apache.catalina.Cluster");
//Cluster configuration end
digester.addObjectCreate(prefix + "Host/Listener",
null, // MUST be specified in the element
"className");
digester.addSetProperties(prefix + "Host/Listener");
digester.addSetNext(prefix + "Host/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
digester.addRuleSet(new RealmRuleSet(prefix + "Host/"));
digester.addObjectCreate(prefix + "Host/Valve",
null, // MUST be specified in the element
"className");
digester.addSetProperties(prefix + "Host/Valve");
digester.addSetNext(prefix + "Host/Valve",
"addValve",
"org.apache.catalina.Valve");
}
可以看到在标注1的地方就提到了HostConfig
类,查看其内部源码(begin()
方法):
@Override
public void begin(String namespace, String name, Attributes attributes)
throws Exception {
Container c = (Container) digester.peek();
Container p = null;
Object obj = digester.peek(1);
if (obj instanceof Container) {
p = (Container) obj;
}
String className = null;
// Check the container for the specified attribute
if (attributeName != null) {
String value = attributes.getValue(attributeName);
if (value != null)
className = value;
}
// Check the container's parent for the specified attribute
if (p != null && className == null) {
String configClass =
(String) IntrospectionUtils.getProperty(p, attributeName);
if (configClass != null && configClass.length() > 0) {
className = configClass;
}
}
// Use the default
if (className == null) {
className = listenerClass;
}
//11111
// Instantiate a new LifecyleListener implementation object
Class<?> clazz = Class.forName(className);
LifecycleListener listener =
(LifecycleListener) clazz.newInstance();
//22222222
// Add this LifecycleListener to our associated component
c.addLifecycleListener(listener);
}
可以很清晰的看到在标注1的地方新建了一个实例(也就是HostConfig
实例),然后将这个实例添加到了listener
数组里了,既然这样我们推测HostConfig
肯定要实现LifecycleListener
,再回去查看下HostConfig
的类定义:
public class HostConfig implements LifecycleListener
果然是这样,这样整个流程就比较清晰了,我们梳理下。
- 在
Catalina
启动的时候,解析server.xml
的时候会给StandardHost
添加一个starting
事件的监听器,也就是一个HostConfig
实例。 - 在
StanardHost
启动的时候(调用start()
),触发该监听器开始deploy项目。
所以我们只需要查看HostConfig
类的相关方法即可知道tomcat是如何发布项目的。