前面已经初步了解了Server组件是什么东西及其作用。在这里进一步对Server组件进行研究学习。
Server组件
从Tomcat的层次结构中已经知道Server组件是Tomcat最顶层组件,它可以当作Tomcat的运行实例的抽象。除此之外,它还包含若干个Service组件、Listener组件以及GlobalNamingResources组件。如下图所示,
Tomcat的整个生命周期存在很多阶段,比如初始化前、初始化中、初始化后、启动前、启动中、启动后、停止前、停止中、停止后、销毁中、销毁后等。为了在Server组件的某阶段执行某些逻辑,于是提供了监听器机制。看了Tomcat9的源码后发现Server组件默认配置了5个监听器组件如下xml所示,书上(Tomcat7)说默认实现了6个监听器,看了一下Tomcat8也是5个监听器,所以应该是Tomcat8以后默认配置5个监听器。接下里就是看每个监听器的实现类,了解一下每个监听器的原理以及作用。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
对于VersionLoggerListener这个监听器,从它的源码来看就是监听并且记录Tomcat启动的时候记录版本的信息。更具体一点就是通过了一个StringManager的对象来管理国际化日志信息输出的问题,例如将日志信息输出成中文。查看StringManager的源码可以发现,其实它采用了懒汉式的单例模式进行设计,并且它在内部实现了一个HashTable容器用来存储不同包和不同的地区的StringManager,保证了线程安全。
**
* 记录Tomcat启动时候的版本信息
*/
public class VersionLoggerListener implements LifecycleListener {
private static final Log log = LogFactory.getLog(VersionLoggerListener.class);
/**
* The string manager for this package.
*/
protected static final StringManager sm = StringManager.getManager(Constants.Package);
接下来就是第二个监听器,AprLifecycleListener监听器了。之前提到过Tomcat有多种不同的网络I/O模式,这个监听器的作用就是初始化APR以及在Tomcat销毁之后的清理APR工作。关于APR模式如何启动可以参考APR模式配置。
/**
* 是接口LifecycleListener的实现类
* 用于初始化APR或销毁APR
*/
public class AprLifecycleListener implements LifecycleListener {
...
}
第三个默认配置的监听器是JreMemoryLeakPreventionListener,翻译一下类名大概就知道这个类是做啥的了。就是为了解决Java运行时可能导致的内存泄漏或者锁定文件问题。这个监听器主要是为了处理由于上下文加载器而导致的内存泄漏问题,Tomcat在重加载一个Web应用时通过实例化一个新的类加载器来实现的,旧的类加载器无法被垃圾回收器回收,导致内存泄漏。例如,如下图所示,当某个上下文加载器为Webappclassloader的线程去加载DriverMananger类,这会导致Webappclassloader被引用,从而导致它不能被回收,从而发生内存泄漏。
为了解决Jre内存泄漏,Tomcat则是尝试通过系统类加载器AppClassLoader去加载这些特殊的Jre类库。这部分的代码如下所示,
public void lifecycleEvent(LifecycleEvent event) {
// Initialise these classes when Tomcat starts
if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {
/*
* 如果不希望当前类加载器去加载JDBC drivers,需要将JDBC drivers添加到classpath中通过
* 系统类加载器去加载,否则会默认使用当前上下文加载器。
*/
if (driverManagerProtection) {
DriverManager.getDrivers();
}
// 先获取当前线程的上下文加载器。
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try
{
//将当前线程的上下文类加载器设置为系统类加载器,然后后面部分均使用系统类加载器去加载类。
Thread.currentThread().setContextClassLoader(
ClassLoader.getSystemClassLoader());
if (appContextProtection) {
ImageIO.getCacheDirectory();
}
//此部分还有很多类需要加载,篇幅太多将其省略
....
} finally {
//最后当加载完类之后,再将当前线程的上下文的类加载器设置为原先的类加载器。
Thread.currentThread().setContextClassLoader(loader);
}
}
}
因为我阅读的是Tomcat9.0的源码,发现JreMemoryLeakPreventionListener这部分的实现源码与书中的源码(Tomcat7.0)有出入,书中提到源码如下所示,根据这一段代码可以发现,JDBC drivers是通过系统类加载器去加载,而在Tomcat9中则需要将JDBC drivers添加到classpath路径下,然后通过系统类加载器去加载,否则就会使用当前线程的上下文加载器。
除了解决内存泄漏问题外,JreMemoryLeakPreventionListener类还解决了锁文件问题。锁文件的情景主要由URLConnection默认的缓存机制导致,在Windows系统下当使用URLConnection的方式读取本地Jar包里面的资源时,它会将资源内存缓存起来,这就导致了该Jar包被锁。此时,如果进行重新部署将会失败,因为被锁的文件无法删除。Tomcat中通过将URLConnection设置为默认不缓存。源代码如下所示,
if (urlCacheProtection) {
try {
//设置为不缓存
JreCompat.getInstance().disableCachingForJarUrlConnections();
} catch (IOException e) {
log.error(sm.getString("jreLeakListener.jarUrlConnCacheFail"), e);
}
}
public void disableCachingForJarUrlConnections() throws IOException {
URL url = new URL("jar:file://dummy.jar!/");
URLConnection uConn = url.openConnection();
uConn.setDefaultUseCaches(false);
}
接下来的的监听器是GlobalResourcesLifecycleListener,它在Tomcat生命周期中的启动和停止时会发生作用。
public void lifecycleEvent(LifecycleEvent event) {
if (Lifecycle.START_EVENT.equals(event.getType())) {
component = event.getLifecycle();
createMBeans();
} else if (Lifecycle.STOP_EVENT.equals(event.getType())) {
destroyMBeans();
component = null;
}
}
从源码中可以看见当事件处于START_EVENT事件时会创建MBeans,处于STOP_EVENT事件时会销毁MBeans。
最后一个监听器就是ThreadLocalLeakPreventionListener,这个监听器时用来解决线程内存泄漏的问题。当Web应用重加载时,把线程池内的所有线程销毁并重新创建。
除了上述5个监听器之外,Tomcat还会监听一个端口号用于关闭Tomcat,默认设置为8005。
<Server port="8005" shutdown="SHUTDOWN">