Tomcat之启动流程(源码分析)

Tomcat之启动流程

Tomcat启动的main方法为Bootstrap,源码如下:

java 复制代码
//Bootstrap#main
Bootstrap daemon;
Bootstrap bootstrap = new Bootstrap();
bootstrap.init();
daemon = bootstrap;
...
daemon.setAwait(true);
daemon.load(args);
daemon.start();

1. Bootstrap#init方法

init方法主要做了两件事:

  1. 初始化类加载器。
  2. 反射生成了Catalina实例,并调用了setParentClassLoader方法。
java 复制代码
//Bootstrap#init方法
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
​
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
​
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
​
catalinaDaemon = startupInstance;

1.1 初始化类加载器

java 复制代码
//Bootstrap#initClassLoaders
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
    commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);

2. Bootstrap#load

通过反射,调用Catalina的load方法(为什么要通过反射?)

java 复制代码
String methodName = "load";
Method method =catalinaDaemon.getClass().getMethod(methodName, paramTypes);
method.invoke(catalinaDaemon, param);

Catalina的load方法主要分为三步:

  1. 初始化一些文件路径、文件名命名空间
  2. 根据文件解析出Server(通过Digester),并且进行实例化
  3. 调用Server的init方法,Server的是实现为StandardServer。
java 复制代码
//Catalina#load
initDirs();
initNaming();
​
Digester digester = createStartDigester();
InputSource inputSource = null;
InputStream inputStream = null;
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
​
//...
 getServer().init();

StandardServer的init方法(因为继承了LifecycleBase,所以实际会调用initInternal,后面的方法也是同理),主要是调用Service的init方法,Service的实现为StandardService。(当然也会扫描类加载器的资源,这里忽略)

java 复制代码
//StandardServer#initInternal
for (Service service : services) {
    service.init();
}

StandardService的init方法主要做一下事情:

  1. Engine调用init方法,Engine的是实现为StandardEngine
  2. MapperListener调用init方法。(并没有做特殊的事情)
  3. Connector调用init的方法,如果有多个则循环调用。
java 复制代码
//StandardService#initInternal
engine.init();
​
mapperListener.init();
​
for (Connector connector : connectors) {
    connector.init();
}

StandardEngine中的init则会调用父类ContainerBase的init,主要是初始化一个启动线程池。

java 复制代码
//ContainerBase#initInternal
BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
startStopExecutor = new ThreadPoolExecutor(
                getStartStopThreadsInternal(),
                getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
                startStopQueue,
                new StartStopThreadFactory(getName() + "-startStop-"));
startStopExecutor.allowCoreThreadTimeOut(true);

Connector的init方法主要是:

  1. 创建CoyoteAdapter
  2. 调用ProtocolHandler的init方法(并没有特殊逻辑),内部会调用Endpoint的init方法
java 复制代码
//Connector#init
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);
protocolHandler.init();

Endpoint是一个接口,抽象类为AbstractEndpoint,init方法内会调用bindWithCleanup方法,然然后内部会调用bind抽象方法,不同是实现类处理不一样。以NioEndpoint为例:主要是创建ServerSocketChannel,然后绑定端口。

java 复制代码
//以NioEndpoint#bind
public void bind() throws Exception {
    initServerSocket();
    setStopLatch(new CountDownLatch(1));
    // Initialize SSL if needed
    initialiseSsl();
}
java 复制代码
//NioEndpoint#initServerSocket
protected void initServerSocket() throws Exception {
    serverSock = ServerSocketChannel.open();
    socketProperties.setProperties(serverSock.socket());
    InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
    serverSock.socket().bind(addr,getAcceptCount());
  
    serverSock.configureBlocking(true);
    
}

3. Bootstrap#start

通过反射调用Catalina的start方法

java 复制代码
//Bootstrap#start
Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
method.invoke(catalinaDaemon, (Object [])null);

Catalina的start方法,主要有三个步骤:

  1. 调用Server的start方法.
  2. 设置shutdown hook。
  3. 主线程await();
java 复制代码
//Catalina#start
getServer().start();
 
if (shutdownHook == null) {
    shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
java 复制代码
if (await) {
    await();
    stop();
}

3.1 Server#start

Server的start方法主要是调用Service的start方法,因为继承了LifecycBase,所以内部实现为startInternal方法。下面有些类的分析也是同理。

java 复制代码
//StandardServer#startInternal
for (Service service : services) {
    service.start();
}

Service的start方法,主要是做了以下三件事:

  1. 调用Engine的start方法
  2. 调用MapperListener的start方法
  3. 遍历调用Connector的start方法。
java 复制代码
//StandardService#startInternal
engine.start();
​
mapperListener.start();
​
for (Connector connector: connectors) {
    connector.start();
}

3.1.1 容器start方法

Engine的start方法,主要是调用父类的ContainerBase的startInternal方法,主要做了以下事情:

  1. 找到子容器,然后添加到线程中执行,StartChild内部的run方法就是调用child.start()方法。我们知道容器级别为:Engine->Host->Context->Wrapper,所以会产生递归调用,遍历着把子容器都给start一下。然后调用Future.get方法,等待子容器处理完start方法。
  2. 调用Pipeline(实现类为StandardPipeline)的start方法。
  3. 启动后台线程。后台线程会定时扫描,实现热加载和热部署,是一个单线程。虽然方法是在ContainerBase里,但是启动后台线程是要有条件的,必须backgroundProcessorDelay大于0,该参数默认为-1,只有StandardEngine中设置成了10
java 复制代码
//ContainerBase#startInternal
//1. 找到子容器,然后调用start方法
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
    results.add(startStopExecutor.submit(new StartChild(child)));
}
for (Future<Void> result : results) {
    result.get();
}
//2. 
if (pipeline instanceof Lifecycle) {
    ((Lifecycle) pipeline).start();
}
//3. 启动容器后台线程,做热加载和热更新
threadStart();

Host(实现类为StandardHost)中的start没有特殊的,会调用父类ContainerBase的startInternal方法,会继续找子容器调用start方法

在Context容器中,start方法的执行逻辑会比较复杂:

创建一个Loader,内部会包含一个类加载器,并且调用start方法。

java 复制代码
//StandardContext#startInternal
WebappLoader webappLoader = new WebappLoader();
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
​
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
    ((Lifecycle) loader).start();
}

找子容器(Wrapper),然后调用start方法。

java 复制代码
for (Container child : findChildren()) {
    if (!child.getState().isAvailable()) {
        child.start();
    }
}

调用ServletContainerInitializer接口的onStartup方法

java 复制代码
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :initializers.entrySet()) {
    entry.getKey().onStartup(entry.getValue(),getServletContext());
}

找子容器需要在启动的时候load的,子容器Wrapper内部加载Servlet,并调用Servlet#init方法。这种就是在启动的时候加载,默认情况下,是延迟加载。

java 复制代码
loadOnStartup(findChildren());

Wrapper容器中,并没有特殊的地方。

3.1.2 Connector#start

Connector内部会调用ProtocolHandler的start方法

java 复制代码
//Connector#start
protocolHandler.start();

ProtocolHandler是一个接口,抽象类为AbstractProtocol,内部是实现主要逻辑:

  1. Endpoint调用start方法
  2. 启动AsyncTimeout 超时线程。
java 复制代码
//AbstractProtocol#start
endpoint.start();
​
asyncTimeout = new AsyncTimeout();
Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");
timeoutThread.start();

Endpoint是一个接口,抽象类为AbstractEndpoint,内部会调用startInternal,由不同的实现类实现。以NioEndpoint为例。主要逻辑如下:

  1. 创建线程池
  2. 初始化连接数限制。
  3. 启动Poller线程,不同实现可能逻辑有点差异
  4. 启动Acceptor线程。
java 复制代码
//NioEndpoint#startInternal
//1. 创建线程池
createExecutor();
//2. 初始化连接限制
initializeConnectionLatch();
//3. 启动Poller线程
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-Poller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
​
//4. 启动Acceptor线程
startAcceptorThread();
java 复制代码
//NioEndpoint#createExecutor
public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

3.2 shutdown hook

runnable接口的run方法,主要是调用Catalina的stop

java 复制代码
//Catalina$CatalinaShutdownHook#run
Catalina.this.stop();

Catalina#stop方法,主要是调用Server的stop和destroy

java 复制代码
//Catalina#stop
Server s = getServer();
LifecycleState state = s.getState();
if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
                    && LifecycleState.DESTROYED.compareTo(state) >= 0) {
    // Nothing to do. stop() was already called
} else {
    s.stop();
    s.destroy();
}

3.3 主线程await

内部实际会上调用Server的await方法。默认的情况情况下,port是有值的,会创建一个ServerSocket,然后主线程阻塞在accept()方法中。

java 复制代码
//StandardServer#await
awaitSocket = new ServerSocket(port, 1,
                    InetAddress.getByName(address));
while (!stopAwait) {
    ServerSocket serverSocket = awaitSocket;
    Socket socket = serverSocket.accept();
}

在Catalina中的stopServer方法中(可以通过命令stop,会调用该方法),除了会对Server调用Stop之外,如果port大于0,还会创建一个Socket,然后发送SHUTDOWN字符串。

java 复制代码
//Catalina#stopServer
Socket socket = new Socket(s.getAddress(), s.getPort();
OutputStream stream = socket.getOutputStream();
String shutdown = s.getShutdown();
for (int i = 0; i < shutdown.length(); i++) {
    stream.write(shutdown.charAt(i));
}
stream.flush();

4. 参考资料

  1. Tomcat源码8.5.x分支
相关推荐
壹米饭10 分钟前
Java程序员学Python学习笔记一:学习python的动机与思考
java·后端·python
全栈派森18 分钟前
机器学习第五课: 深度神经网络
后端·神经网络·机器学习
白露与泡影42 分钟前
springboot + nacos + k8s 优雅停机
spring boot·后端·kubernetes
菜鸟谢1 小时前
windows xp 下载 sp0 sp1 sp2 sp3 sp4
后端
AirMan1 小时前
你真的懂 MySQL 的一致性读和当前读的区别吗?
后端·面试
David爱编程1 小时前
容器性能优化实战指南——防止“吃爆”服务器就靠这些招!
后端·docker·容器
Android洋芋1 小时前
GitHub项目部署的终极指南:从零到一掌握Docker实战
后端
林太白1 小时前
Next.js超简洁完整篇
前端·后端·react.js
前端付豪1 小时前
汇丰登录风控体系拆解:一次 FaceID 被模拟攻击的调查纪实
前端·后端·架构
无名之逆2 小时前
大三自学笔记:探索Hyperlane框架的心路历程
java·开发语言·前端·spring boot·后端·rust·编程