Tomcat

一、概述

tomcat:用于处理连接过来的socket请求的

1. 对外处理连接:将收到的字节流转化为自己想要的request和response对象
2. 对内处理servlet,将对应的request请求分发到相应的servket中

tomat分为两大部分:① 连接器处理对外连接 ②容器管理对内的servelet

 

一个tomcat服务可以对应多个service。每个service都有连接器和容器。

tomcat目录配置文件server.xml

<Server port="8006" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"/>

<Connector port="8010" prorocol="AJP/1.3" redirectPort="8443"/>

<Engine name="Catalina" defaultHost="localhost">

<Realm className="org.apache.catalina.realm.LockOutRealm">

<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>

</Realm>
<Host name="localhost" appBase="webapps">
</Host>
</Engine>
</Service>
</Server>

连接器(Connector)

连接器接收到外部的请求传给容器的是ServletRequest对象,而容器传给连接器的是ServletResponse对象,因为在网络传输中传送的是字节流,所以在网络传输过程中是不行的。

连接器的功能需求:

  • Socket连接

  • 读取请求网络中的字节流

  • 根据相应的协议(Http/AJP)解析字节流,生成统一的TomcatRequest对象

  • 将TomcatRequest对象传给容器

  • 将TomcatResponse对象转换为字节流

  • 将字节流返回给客户端

总结为:

  • 网络通信 --- EndPoint

  • 应用层协议解析 --- Processor

  • Tomcat的Request/Response与ServletRequest/ServletResponse对象的转化 --- Adapter

 

容器(Engine)

在Tomcat主要就是装载Servlet,Tomcat的容器设计采用了组合设计模式,组合模式的意义就是使得用户对于单个对象和组合对象的使用具有一致性,即无论添加多少个Context使用就是为了找到其下面的Servlet,而无论添加多少个Host也是为了找个下面的Servlet。

在server.xml可以体现

<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
</Host>
</Engine>

容器中有两个模块,一个是顶层模块Engine,另一个是Host,还有两个模块,一个是context对应的webapp里面的每个应用文件夹,每个文件夹对应一个context,还有一个wrapper对应的是context中的所有的servlet,wrapper管理了访问关系与具体的Servlet的对应。

 

Tomcat请求如何定位

在我们启动Tomcat的时候,连接器就会进行初始化监听所配置的端口号,这里我们配置的是8080端口对应的协议是HTTP

  • 请求发送到本机的8080端口,被监听的HTTP/1.1的连接器Connector获得

  • 连接器Connector将字节流转换为容器所需要的ServletRequest对象给同级Service下的容器模块Engine进行处理

  • Engine获得请求地址http://localhost:8080/??/??,匹配下面的Host主机

  • 匹配到名为localhost的Host(就算此时请求为具体的ip,没有配置相应的Host,也会交给名为localhost的Host进行处理,因为他是默认的主机)

  • Host匹配到请求地址的路径/***的Context,即在webapp下找到对应的文件夹

  • Context匹配到URL规则为*.do的servlet,对应为某个Servlet类

  • 调用其doGet或者doPost方法

  • Servlet执行完以后将对象返回给Context

  • Context返回给Host

  • Host返回给Engine

  • Engine返回给连接器Connector

  • 连接器Connector将对象解析为字节流发送给客户端

 

EndPoint:是一个通信的端点,就是负责对外实现TCP/IP协议,EndPoint是个接口,具体实体类是AbstractEndpointAbstractEndpoint具体的实现类就有 AprEndpointNio2EndpointNioEndpoint

* AprEndpoint:对应的是APR模式,就是从操作系统级别解决异步IO的问题,大幅度提高服务器的处理和响应性能。启用需要安装一些其他的依赖库。
* Nio2Endpoint:利用代码来实现异步IO
* NioEndpoint:利用了Java的NIO实现非阻塞IO,Tomcat默认是NioEndpoint启动。

NioEndpoint:对于Linux的多路复用器的使用。步骤如下

1. 创建一个Selector,注册各种Channel,然后调用select方法,等待通道中有对应的事件发送
2. 有对应的事件发生了,例读、写事件,就将信息从通道中读取出来。

NioEndpoint实现多路复用采用五个组件。依次是Limitlatch、Acceptor、SocketProcessor、Executor

* LimitLatch:连接控制器,负责控制最大的连接数
* Acceptro:负责接收新的连接,然后返回一个Channel对象给Poller
* Poller:可以将其看成是NIO中Selector,负责监控Channel的状态
* SocketProcessor:可以看成是一个被封装的任务类
* Exceutor:Tomcat自己扩展的线程池,用来执行任务类

 

LimitLatch:主要是用来控制Tomcat所能接收的最大数量连接,如果超过了此连接,那么Tomcat就会将此连接线程阻塞等待,等里面有其他连接释放了再消费此连接。

publicclassLimitLatch{

private static final Log log = LogFactory.getLog(LimitLatch.class);
private class Sync extends AbstractQueuedSynchronizer{
private static final long serialVersionUID = 1L;

public Sync(){}

@Override
protected int tryAcquireShared(int ignored){
long newCount = count.incrementAndGet();
if(!released && newCount > limit){
count.decrementAndGet();
return -1;
} else {
return 1;
}
}

protected boolean tryReleaseShared(int arg){
count.decrementAndGet();
return true;
}
}

private final Sync sync;

//当前连接数
private final AtomicLong count;
//最大连接数
private volatile long limit;

private volatile boolean released = false;
}


内部实现了AbstractQueuedSynchronizer,AQS是一个框架,实现它的类可以自定义控制线程什么时候挂起什么时候释放。limit参数就是控制的最大连接数。我们可以看到AbstractEndpoint调用LimitLatch的countUpOrAwait方法来判断是否能获取连接

public void countUpOrAwait() throws InterruptedException{
if(log.isDebugEnabled()){
log.debug("Counting up["+Thread.currentThread().getName()+"] latch="+getCount());
}
sync.acquireSharedInterruptibly(1);
}

AQS需要用户自己实现AbstractQueuedSynchronizer来定义什么时候获取连接,什么时候释放连接,才能知道什么时候阻塞线程。在上面代码中Sync类重写了tryAcquireShared和tryReleaseShared方法。在tryAcquireShared方法中定义了一旦当前连接数大于设置的最大连接数就会返回-1表示将此线程放入AQS队列中等待。

Acceptor:是接收连接的,Acceptor实现了Runnable接口,在AbstractEndpoint的startAcceptorThreads来新开启线程来执行Acceptor的run方法。

protected void startAcceptorThreads(){
int count = getAcceptorThreadCount();
acceptors = newArrayList<>(count);
for(int i = 0; i < count; i++){
Acceptor<U> acceptor = newAcceptor<>(this);
String threadName = getName() + "-Acceptor-" + i;
acceptor.setThreadName(threadName);
acceptor.add(acceptor);
Thread t = newThread(acceptor,threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start();
}
}

可以设置开启几个Acceptor,默认是一个。而一个端口只能对应一个ServerSocketChannel,ServerSocketChannel在Acceptor<U> acceptor = newAcceptor<>(this);由Endpoint组件初始化的连接。在NioEndpoint的initServerSocket方法中初始化连接。

protected void initServerSocket() throws Exception{
if(!getUseInheritedChannel()){
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(),getPortWithOffset());
serverSock.socket().bind(addr,getAcceptCount());
} else {
Channel ic = System.inheritedChannel();
if(ic instanceof ServerSocketChannel){
serverSock = (ServerSocketChannel) ic;
}

if(serverSock == null){
throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
}
}
serverSock.configureBlocking(true);
}

1.在bind方法中的第二个参数表示操作系统的等待队列长度,即Tomcat不在接受连接时(达到了设置的最大连接数),但是在操作系统层面还是能够接受连接的,此时将此连接信息放入等待队列,那么这个队列的大小就是此参数设置的。
2.ServerSocketChannel被设置成了阻塞的模式,也就是说是以阻塞方式接受连接的。设置成非阻塞模式就必须设置一个Selector不断的轮询,但是接受连接只需要阻塞一个通道即可。

 

每个Acceptor生成PollerEvent对象放入Poller队列中时都是随机取出Poller对象的,Poller中的Queue对象设置成了SynchronizedQueue<PollerEvent>,因为可能有多个Acceptor同时向此Poller的队列中放入PollerEvent对象。

public Poller getPoller0(){
if(pollerThreadCount == 1){
return pollers[0];
} else {
int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
return pollers[idx];
}
}

操作系统级别的连接:在TCP的三次握手中,系统通常会每一个LISTEN状态的Socket维护两个队列,一个是半连接队列(SYN):这些连接已经收到客户端SYN;另一个是全连接队列(ACCEPT):这些链接已经收到客户端的ACK,完成了三次握手,等待被应用调用accept方法取走使用。
所有的Acceptor共用一个连接,在Accptor的run方法中。


@Override
public void run(){
while(endpoint.isRunning()){
try{
//如果到了最大连接数,线程等待
endpoint.countUpOrAwaitConnection();
U socket = null;
}
try{
//调用accept方法获得一个连接
socket = endpoint.serverSocketAccept();
}catch(Exception ioe){
//出异常以后当连接数减掉1
endpoint.countDownConnection();
}
//配置Socket
if(endpoint.isRunning() && !endpoint.isPaused()){
if(!endpoint.setSocketOptions(socket)){
endpoint.closeSocket(socket);
}
} else{
endpoint.destroySocket(socket);
}
}
}

1.运行时会先判断是否达到最大连接数,如果达到了那么就阻塞线程等待,里面调用的是LimitLatch组件判断的
2.配置socket,endpoint.setSocketOptions(socket);

protected boolean setSocketOptions(SocketChannel socket) {

// Process the connection
try{
// 设置Socket为非阻塞模式,供Poller调用
socket.configureBlocking(false);
Socket sock = socket.socket();
socketProperties.setProperties(sock);
NioChannel channel = null;
if(nioChannels != null) {
channel = nioChannels.pop();
}
if(channel == null) {
SocketBufferHandler bufhandler = new SocketBufferHandler(                         socketProperties.getAppReadBufSize(),                       socketProperties.getAppWriteBufSize(),                       socketProperties.getDirectBuffer());

if(isSSLEnabled()) {
channel = new SecureNioChannel(socket, bufhandler,selectorPool, this);
} else{
channel = new NioChannel(socket, bufhandler);
}
} else{
channel.setIOChannel(socket);
  channel.reset();
}
//注册ChannelEvent,其实是将ChannelEvent放入到队列中,然后Poller从队列中取
getPoller0().register(channel);
} catch(Throwable t) {
ExceptionUtils.handleThrowable(t);
try{
  log.error(sm.getString("endpoint.socketOptionsError"),t);
} catch(Throwable tt) {
ExceptionUtils.handleThrowable(tt);
}
  // Tell to close the socket
  return false;
}
return true;
}

将Acceptor与一个Poller绑定起来,然后两个组件通过队列通信,每个Poller都维护着一个SynchronizedQueue队列,ChannelEvent放入到队列中,然后poller从队列中取出事件进行消费。

Poller:是NioEndpoint的内部类,而它也是实现了Runnable接口,可以看到在其类中维护了一个Queue和Selector,本质上Poller就是Selector。

private Selector selector;
private final Synchronized Queue<PollerEvent> events = new SynchronizedQueue<>();

@Override
pubblic void run(){
while(true){
boolean hasEvents = false;
try{
if(!close){
//查看是否有连接进来,如果有就将Channel注册进Selector中
hasEvents = events();
}
if(close){
events();
timeout(0,false);
}
try{
selector.close();
}catch(IOException ioe){
log.error(sm.getString("endpoint.nio.selectorCloseFail"),ioe);
break;
}
}catch(Throwable x){
ExceptionUtils.handleThrowable(x);
log.error(sm.getString("endpoint.nio.selectorLoopError"),x);
continue;
}
if(keyCount == 0){
hasEvents = (hasEvent | events());
}
Iterator<SelectionKey> iterator = KeyCount > 0 ? selector.selectedKeys().iterator() : null;
while(iterator != null && iterator.hasNext()){
SelectionKey sk = iterator.next();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if(socketWrapper == null){
iterator.remove();

}else {
iterator.remove();
processKey(sk,socketWrapper);
}
}
timeout(keyCount,hasEvents);
}
getStopLatch().countDown();
}

调用了events()方法,不断查看队列中是否有Pollerevent事件,有的话就将其取出然后把里面的Channel取出来注册到该Selector中,然后不断轮询所有注册过的Channel查看是否有事件发送。

SocketProcessor:Poller在轮询Channel有事件发生时,就会调用将此事件封装起来,然后交给线程池去执行。负责包装的类就是SocketProcessor。包住类内部实现了Runnable接口,用来定义线程池Executor中线程所执行的任务。将Channel中的字节流转换为Tomcat需要的ServletRequest对象是调用了Http11Processor来进行字节流与对象的转换的。

Executor:是Tomcat定制版的线程池。内部扩展了Java的线程池。

在线程池中最重要的两个参数就是核心线程数和最大线程数。

Java线程池的执行流程
1.如果当前线程小于核心线程数,那么来一个任务就创建一个线程。
2.如果当前线程大于核心线程数,那么就再来任务就将任务放入到任务队列中,所有线程抢任务。
3.如果队列满了,那么就开始创建临时线程。
4.如果总线程数到了最大的线程数,再次获得任务队列,再尝试一次将任务加入队列中。
5.如果此时还是满的,就抛异常。

Tomcat和Java线程池的执行流程差别在于第四步,原生线程池的处理策略是只要当前线程数大于最大线程数,那么就抛异常,而Tomcat的则是如果当前线程数大于最大线程数,就再尝试一次,如果还是满的才抛出异常。

public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try{
super.execute(command);
} catch(RejectedExecutionException rx) {
if(super.getQueue() instanceof TaskQueue) {
//获得任务队列
final TaskQueue queue = (TaskQueue)
super.getQueue();
try{
if(!queue.force(command, timeout, unit)) {
//这个参数就是定义了任务已经提交到了线程池中,但是还没有执行的任务个数
              submittedCount.decrementAndGet();
throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
}
} catch(InterruptedException x) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} else{
      submittedCount.decrementAndGet();
      throw rx;
}
}
}


private final AtomicInteger submittedCount = new AtomicInteger(0);

1.定制的队列继承了LinkedBlockingQueue,而LinkedBlockingQueue队列默认是没有边界的。于是我们就传入了一个参数,maxQueueSize给构造的队列。在Tomcat任务队列默认情况下是无限制的,如果当前线程达到了核心线程数,则开始向队列中添加任务,那么就会一直是添加成功。就不会再创建新的线程。
2.线程池中创建新线程会有两个地方,一个是小于核心线程时,来一个任务创建一个线程。另一个是超过核心线程并且任务队列已满,则会创建临时线程。
3.规定任务队列是否已满,需要设置队列的最大长度,但Tomcat默认情况下是没有设置,所以默认是无限的。所以Tomcat的TaskQueue继承了LinkedBlockingQueue,重写了offer方法,在里面定义了什么时候返回false。

@Override
public boolean offer(Runnable o){
if(parent == null){
return super.offer(o);
}
//如果当前线程数等于最大线程数,此时不能创建新线程,只能添加进任务队列中
if(parent.getPoolSize() == parent.getMaximumPoolSize()){
return super.offer(o);
}
//如果已提交但是未完成的任务数小于等于当前线程数,说明能处理过来,就放入队列中
if(parent.getSubmittedCount() <=(parent.getPoolSize())){
return super.offer(o);
}
//以提交但是未完成的任务数大于当前线程数,如果当前线程数小于最大线程数,就返回false新建线程
if(parent.getPoolSize() < parent.getMaximumPoolSize()){
return false;
}
return super.offer(o);
}

submittedCount:目的就是为了在任务队列长度无限的情况下,让线程池有机会创建新的线程。

 

Java中的类加载器:类加载器负责在程序运行时将Java文件动态加载到JVM中

Java虚拟机存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器是使用C++语言实现的,是虚拟机自身的一部分。

  • 其他的类加载器:这些类加载器都是Java实现,独立于虚拟机外部,并且全部都继承自抽象类Java.lang.ClassLoader,其他的类加载器又分为:

    • ExtensionClassLoader:这个类加载器由ExtClassLoader实现,它负责加载 JAVA_HOME、lib/ext 目录中的所有类,或者被Java.ext.dir系统变量所指定的路径中的所有的类。

    • ApplicationClassLoader:这个类加载器是由AppClassLoader实现的,它负责加载用户类路径(ClassPath)上所指定的所有类,如果应用中没有自定义自己的类加载器,那么一般就是程序中默认的类加载器。

    • 自定义加载器:根据自己的需求,自定义加载特定路径的加载器。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

双亲委派模型:除了顶层的启动类加载器外,其他加载器都应该有自己的父加载器。加载器之间的父子关系不是通过继承来实现的,而是通过设置parent变量来实现的。

工作过程:收到一个类加载器的请求,本身不会先加载此类,而是会先将此请求委派给父类加载器去完成,每个层次都是如此,直到启动类加载器中,只有父类没有加载此文件,子类才会去尝试去加载。

双亲委派模型是为了保证Java程序的稳定运行。

protected Class<?> loadClass(String name, boolean resolve) throwsClassNotFoundException{
synchronized(getClassLoadingLock(name)) {
  // 首先,检查请求类是否被加载过
  Class<?> c = findLoadedClass(name);
  if(c == null) {
long t0 = System.nanoTime();
try{
// 如果没被本类类加载器加载过,先委托给父类进行加载
if(parent != null) {
      c = parent.loadClass(name, false);
} else{
// 如果没有父类,则表明在顶层,就交给BootStrap类加载器加载
          c = findBootstrapClassOrNull(name);
}
// 如果最顶层的类也找不到,那么就会抛出ClassNotFoundException异常
} catch(ClassNotFoundException e) {}
// 如果父类都没有加载过此类,子类才开始加载此类
if(c == null) {
      c = findClass(name);
}
}
if(resolve) {
  resolveClass(c);
}
return c;
}
}

protected Class<?> findClass(String name) throws ClassNotFoundException{
throw new ClassNotFoundException(name);
}

Tomcat中的类加载器

*  Bootstrap:是Java的最高的加载器,用C语言实现,主要用来加载JVM启动时所需要的核心类,例如 `$JAVA_HOME/jre/lib/ext`路径下的类。
* System:会加载CLASSPATH系统变量所定义路径的所有的类。
* Common:会加载Tomcat路径下的lib文件下的所有类。
* Webapp1、Webapp2....:会加载webapp路径下项目中的所有的类。一个项目对应一个WebappClassLoader,这样就实现了应用之间类的隔离。

Tomcat自定义类加载器

* 隔离不同应用:部署在同一个Tomcat中的不同应用A和B,不同应用中使用了同一个框架的不同版本,使用的又是同一个类加载器,那么web应用就会因为jar包覆盖无法启动。
* 灵活性:Web应用之间的类加载器相互独立,那么就可以根据修改不同的文件重建不同的类加载器替换原来的。从而不影响其他应用。
* 性能:在一个Tomcat部署多个应用,多个应用中都有相同的类库依赖,那么可以把这相同的类库让Common类加载器进行加载。

Tomcat自定义WebAppClassLoader,即收到类加载的请求,会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载web应用自己定义的类,我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,tomcat重写了loadClass方法。

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized(getClassLoadingLock(name)) {
Class<?> clazz = null;
// 1. 从本地缓存中查找是否加载过此类
      clazz = findLoadedClass0(name);
if(clazz != null) {
if(log.isDebugEnabled()){
          log.debug(" Returning class from cache");
          }
if(resolve){
              resolveClass(clazz);
          }
return clazz;
}
// 2. 从AppClassLoader中查找是否加载过此类
      clazz = findLoadedClass(name);
if(clazz != null) {
if(log.isDebugEnabled()){
          log.debug(" Returning class from cache");
          }
          if(resolve){
              resolveClass(clazz);
          }
return clazz;
}
String resourceName = binaryNameToPath(name, false);
// 3. 尝试用ExtClassLoader 类加载器加载类,防止Web应用覆盖JRE的核心类
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try{
      URL url;
if(securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
              url = AccessController.doPrivileged(dp);
} else{
          url = javaseLoader.getResource(resourceName);
}
          tryLoadingFromJavaseLoader = (url != null);
} catch(Throwable t) {
      tryLoadingFromJavaseLoader = true;
}
boolean delegateLoad = delegate || filter(name, true);
// 4. 判断是否设置了delegate属性,如果设置为true那么就按照双亲委派机制加载类
if(delegateLoad) {
if(log.isDebugEnabled()){
          log.debug(" Delegating to parent classloader1 "+ parent);
          }
try{
          clazz = Class.forName(name, false, parent);
if(clazz != null) {
if(log.isDebugEnabled()){
                  log.debug(" Loading class from parent");
                  }
if(resolve){
                  resolveClass(clazz);
                  }
return clazz;
}
} catch(ClassNotFoundException e) {
// Ignore
}
}
// 5. 默认是设置delegate是false的,那么就会先用WebAppClassLoader进行加载
if(log.isDebugEnabled()){
      log.debug(" Searching local repositories");
      }
try{
      clazz = findClass(name);
if(clazz != null) {
if(log.isDebugEnabled()){
              log.debug(" Loading class from local repository");
              }
if(resolve){
              resolveClass(clazz);
              }
return clazz;
}
} catch(ClassNotFoundException e) {
// Ignore
}
// 6. 如果此时在WebAppClassLoader没找到类,那么就委托给AppClassLoader去加载
if(!delegateLoad) {
if(log.isDebugEnabled()){
      log.debug(" Delegating to parent classloader at end: "+ parent);
      }try{
      clazz = Class.forName(name, false, parent);
if(clazz != null) {
if(log.isDebugEnabled()){
              log.debug(" Loading class from parent");
              }
if(resolve){
              resolveClass(clazz);
              }
return clazz;
}
} catch(ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}

web应用默认的类加载顺序是(打破了双亲委派规则):
1.先从JVM的BootStrapClassLoader中加载。
2.加载Web应用下/WEB-INF/classes中的类。
3.加载web应用下/WEB-INF/lib/*.jsp中的jar包中的类。
4.加载上面定义的System路径下面的类
5.加载上面定义的Common路径下面的类
配置文件中配置<Loaderdelegate="true"/>,遵循双亲委派规则,顺序:
1.先从JVM的BootStrapClassLoader中加载。
2.加载上面定义的System路径下面的类。
3.加载上面定义的Common路径下面的类。
4.加载Web应用下 /WEB-INF/classes中的类。
5.加载Web应用下 /WEB-INF/lib/*.jap中的jar包中的类。

容器:Tomcat中所有容器中的子容器的父接口都是Container。,所有子容器都需要实现此接口。

Container接口设计
public interface Container extends Lifecycle{
public void setName(String name);
public Container getParent();
public void setParent(Container container);
public void addChild(Container child);
public void removeChild(Container child);
public Container findChild(String name);
}

Tomcat通过设置父子关系,形成一个树形的结构(一父多子)、链式结构(一父一子)来管理。树形结构--设计模式中的组合模式,链式结构--设计模式中的责任链设计模式。
连接器将转换好的ServletRequest给到容器,看CoyoteAdapter中的service方法。在连接器最后一环将解析过的Request给到Adapter运用适配器设计模式解析为ServletRequest对象。

Pipeline管道里面装的是valve,每个valve都是一个处理点,它的invoke就是相对应的处理逻辑。是通过setNext的方法变成链表的方式将valve组织起来。然后将此valve装入Pipeline中。因此每个容器都有一个Pipeline,里面装入系统定义或者自定义的一些拦截点来做一些相应的处理。只要获得了容器中的Pipeline管道中的第一个valve对象,那么后面一系列链条都会执行到。

不同容器之间Pipeline之间通过setBasic方法设置的valve链条的末端节点,它负责调用底层容器的Pipeline第一个valve节点。

Engine容器:实现类是StandardEngine

@Override
publicvoid addChild(Container child) {
if(!(child instanceof Host)){
throw new IllegalArgumentException(sm.getString("standardEngine.notHost"));
}
super.addChild(child);
}

@Override
public void setParent(Container container) {
throw new IllegalArgumentException(sm.getString("standardEngine.notParent"));
}

Engine容器没有父容器,添加父容器会报错,添加子容器只能添加Host容器。

Host容器:是Engine的子容器,一个Host在Engine中代表一个虚拟主机,作用就是运行多个应用,负责安装和展开这个应用,并且标识这个应用以便能够区分它们。Host的子容器通常是Context容器。

<Host name="localhost"  appBase="webapps"
              unpackWARs="true" autoDeploy="true"/>
               
Host容器在启动时,只启动了相对应的valve,在Tomcat的设计中,每个模块都有对应的生命周期:
NEW、INITIALIZING、INITIALIZED、SSTARTING_PREP、STAPTING、STARTED,每个模块状态的变化都会引发一系列动作。
开闭原则:是为了扩展性系统的功能,不能修改系统中现有的类,可以定义新的类。
每个模块状态的变化相当于一个事件的发生,而事件是有相应的监听器的。在监听器中实现具体的逻辑,监听器也可以方便的增加和删除。这就是典型的观察者模式。
Host容器在启动时需要扫描webapps目录下面的所有web应用,创建相应的Context容器。Host的监听器是HostConfig,实现了LifecycleListener接口。
容器中各组件的具体处理逻辑是在监听器中实现的。

Context容器:一个Context对应一个Web应用,Context代表的是Servlet的Context,它具备了Servlet的运行的基本环境。

Context最重要的功能就是管理它里面的Servlet实例,Servlet实例在Context中是以Wrapper出现的。Context准备运行环境是在ContextConfig中lifecycleEvent方法。

@Override
public void lifecycleEvent(LifecycleEvent event) {
// Identify the context we are associated with
try{
  context = (Context) event.getLifecycle();
} catch(ClassCastException e) {
  log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if(event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
  configureStart();
} else if(event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
  beforeStart();
} else if(event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if(originalDocBase != null) {
      context.setDocBase(originalDocBase);
}
} else if(event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
  configureStop();
} else if(event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
  init();
} else if(event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
  destroy();
}
}

Wrapper :代表一个Servlet,包括Servlet的装载、初始化、执行以及资源的回收。Wrapper是最底层的容器,它没有子容器。

Wrapper实现类是StandrdWrapper,主要任务是载入Servlet类,并进行实例化。但是StandardWrapper类并不会调用Servlet的service方法。而是StandardWrapperValue类通过调用StandardWrapper的allocate方法获得相应的servlet,然后通过拦截器的过滤之后才会调用相应的Servlet的service方法。

Tomcat的热加载和热部署

热部署就是在服务器运行时重新部署项目,热加载即在在运行时重新加载class,从而升级应用。

热加载修改class文件能够动态加载:

1. 所以的class文件都是交给由类加载来管理的
2. 如果换了class文件,Tomcat中Context的启动方法中,调用了threadStart的方法。
protected void threadStart() {
backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor().scheduleWithFixedDelay(newContainerBackgroundProcessor(),
//要执行的Runnable
  backgroundProcessorDelay,
  //第一次执行延迟多久
  backgroundProcessorDelay,
  //之后每次隔多久执行一次
  TimeUnit.SECONDS);
  //时间单位
}

在后台开启周期性的任务,使用了Java提供了ScheduledThreadPoolExecutor。能周期性执行任务还有线程池功能,调用了scheduleWithFixedDelay方法,第一个传入的参数就是要执行的任务。

protected class ContainerBackgroundProcessor implements Runnable{
@Override
public void run() {
// 请注意这里传入的参数是 " 宿主类 " 的实例
      processChildren(ContainerBase.this);
}
protected void processChildren(Container container) {
try{
//1. 调用当前容器的 backgroundProcess 方法。
          container.backgroundProcess();
//2. 遍历所有的子容器,递归调用 processChildren,
// 这样当前容器的子孙都会被处理
Container[] children = container.findChildren();
for(int i = 0; i < children.length; i++) {
// 这里会判断子容器如果已经启动了后台线程,那么这里就不会启动了
if(children[i].getBackgroundProcessorDelay() <= 0) {
          processChildren(children[i]);
}
}
} catch(Throwable t) { ... }

监听代码在backgroundProcess实现。
public void backgroundProcess() {

//WebappLoader 周期性的检查 WEB-INF/classes 和 WEB-INF/lib 目录下的类文件
Loader loader = getLoader();
if(loader != null) {
  loader.backgroundProcess();
}
............省略
}

进入loader.backgroundProcess();
public void backgroundProcess() {
//此处判断热加载开关是否开启和监控的文件夹中文件是否有修改
if(reloadable && modified()) {
try{
Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
if(context != null) {
//Context重启
              context.reload();
}
} finally{
if(context != null&& context.getLoader() != null) {
Thread.currentThread().setContextClassLoader(context.getLoader().getClassLoader());
}
}
}
}

Tomcat热加载的步骤:

* 如果发现有文件发生变化,热加载开关开启
* 关闭Context容器
* 重启Context容器

一个Context容器对应一个类加载器。所以在销毁Context容器的时候也连带着将其类加载器一并销毁了。Context在重启的过程中也会创建新的类加载器来加载我们新建的文件。

热部署:

Tomcat在启动的时候会将其目录下webapp中war包解压后然后封装为一个Context供外部访问。那么热部署就是在程序运行时,如果我们修改了War包中的东西。那么Tomcat就会删除之前的War包解压的文件夹,重新解压新的War包。

周期事件的监听器Host容器调用了父类的backgroundProcess方法。Host是HostConfig类的lifecycleEvent方法

@Override
public void lifecycleEvent(LifecycleEvent event) {
if(event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
// 周期事件
      check();
} else if(event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
// 开始之前事件
      beforeStart();
} else if(event.getType().equals(Lifecycle.START_EVENT)) {
// 开始事件
      start();
} else if(event.getType().equals(Lifecycle.STOP_EVENT)) {
// 结束事件
      stop();
}
}


protected void check() {
if(host.getAutoDeploy()) {
// 检查Host下所有已经部署的web应用
DeployedApplication[] apps =deployed.values().toArray(new DeployedApplication[0]);
for(int i = 0; i < apps.length; i++) {
if(!isServiced(apps[i].name)){
          checkResources(apps[i],false);
          }
}
// 检查Web应用是否有变化
if(host.getUndeployOldVersions()) {
      checkUndeploy();
}
// 执行部署
      deployApps();
}
}

热部署的步骤:

* 检查Host管理下的所有web应用
* 如果原来的web应用被删除,就将应用Context容器删除
* 如果有新war包放进来,就部署相应的war包

一个Context容器对应一个类加载器。所以在销毁Context容器的时候也连带着将其类加载器一并销毁了。Context在重启的过程中也会创建新的类加载器来加载我们新建的文件

posted on 2021-11-23 17:37  白糖℃  阅读(124)  评论(0编辑  收藏  举报