⭐️前言
前面简单的介绍了Tomcat的框架架构,大家应该对tomcat有了一个简单的了解,但是作为技术人员还是需要亲自的去分析代码的执行流程,然后跟着debug断点走一遍才会心理踏实。对于一些初级的程序员来说对tomcat有一个了解认知就可以了,但是对于中级程序员来说还是要深入跟进代码才能感觉到别人写的完整架构的魅力。本文就简单的介绍了tomcat源码的构建,然后又debuger跟了tomcat的执行流程走了一遍,感兴趣的可以看着介绍跟着自己走一遍断点了解下。
❤️Tomcat源码构建
Tomcat源码下载官网
进入Tomcat官网后下载源码,本文使用的是Tomcat8.0版本,其实版本不影响阅读源码,主要来看一下Tomcat的运行流程。
1️⃣第一步:下载下来源码之后,解压文件夹,然后在tomcat文件夹主目录创建catalina-home文件夹,然后将conf和webapps移动到新创建的catalina-home文件夹中,另外还需要在文件夹中创建几个项目中需要用到的文件夹分别为:lib、logs、temp和work。最终catalina-home文件夹应该长这样的。
2️⃣第二步:在tomcat文件夹主目录创建一个pom.xml文件,我们知道tomcat是用Java语言编写的,将其转化为maven项目。pom.xml中复制下面内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tomcat</groupId>
<artifactId>Tomcat8.0</artifactId>
<name>Tomcat8.0</name>
<version>8.0</version>
<build>
<finalName>Tomcat8.0</finalName>
<sourceDirectory>java</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>test</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
</dependencies>
</project>
3️⃣第三步:在idea中以maven项目的形式打开tomcat源码,然后使用Ant编译代码,报了这个错误不影响代码启动运行。
4️⃣第四步:增加一个运行application,设置启动类为:org.apache.catalina.startup.Bootstrap,这个类中有个main方法,是tomcat启动的入口,并且配置下面参数指向第一步中建立的catalina-home文件夹。
-Dcatalina.home=F:\apache-tomcat-8.0.53-src\catalina-home -Dcatalina.base=F:\apache-tomcat-8.0.53-src\catalina-home -Djava.endorsed.dirs=F:\apache-tomcat-8.0.53-src\catalina-home/endorsed -Djava.io.tmpdir=F:\apache-tomcat-8.0.53-src\catalina-home/temp -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
5️⃣最后:运行项目,可能项目中有些test包下面的代码缺少class直接屏蔽掉该代码就可以了,代码正常启动,输入localhost:8080进入到我们熟悉的页面,大功告成。👌
❤️Tomcat执行流程
🤍 前期准备
首先通过idea建立JavaWeb项目,idea建立Java项目很简单,直接生成的demo项目就能用。
然后运行web项目,在target下面有生成war部署文件,将文件夹改名后放到构建的源码中webapps下面,然后启动构建的源码,看一下访问是否正常,正常后就可以进行后面的断点调试了,这个步骤很简单就不过多的讲解了。
🤍 tomcat运行流程
这是一幅根据前面讲解的Tomcat架构画出来的简单的请求执行的流程图,先看一下流程,后面咱再打断点跟踪一下执行的过程。
由于源码中的代码太多了,咱就不贴完整源码去分析了,只看关键部分代码,感兴趣的可以根据下面的流程找到对应的地方打断点观察一下:
1️⃣首先找一下EndPoint,请求进来先进入EndPoint,tomcat8中默认的是NIO的通信方式,找到NioEndPoint类,类中有个方法startInternal(),咱直接看这个方法,文中代码又删减,只关注的执行流程。
public void startInternal() throws Exception {
if (!running) {
... //这行有一个启动线程,看一下启动的线程
startAcceptorThreads();
}
}
//启动线程在AbstractEndpoint中的startAcceptorThreads方法中,看创建了一堆接收线程用来监听接收请求
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new Acceptor[count];
for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
...
Thread t = new Thread(acceptors[i], threadName);
...
t.start();
}
}
//既然是线程直接看Acceptor线程类中的run方法
@Override
public void run() {
while (asyncTimeoutRunning) {
...
for (SocketWrapper<S> socket : waitingRequests) {
if (socket.getTimeout() > 0 && (now - access) > socket.getTimeout()) {
... //最终执行了这里
processSocket(socket, SocketStatus.TIMEOUT, true);
}
}
}
}
//又跳回了NioEndpoint中的processSocket方法
protected boolean processSocket(KeyAttachment attachment, SocketStatus status, boolean dispatch) {
try {
SocketProcessor sc = processorCache.pop();
if ( sc == null ) sc = new SocketProcessor(attachment, status);
else sc.reset(attachment, status);
Executor executor = getExecutor();
//最终用线程池执行了SocketProcessor,即使是点进线程去
if (dispatch && executor != null) {
executor.execute(sc);
}
...
}
//点进SocketProcessor 线程的run方法
public void run() {
doRun();
}
private void doRun() {
NioChannel socket = ka.getSocket();
SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
try {
....
if (handshake == 0) {
SocketState state = SocketState.OPEN;
...//调用了这一行
state = handler.process(ka, status);
}
}
}
public SocketState process(SocketWrapper<S> wrapper,
SocketStatus status) {
//这个方法往下走的那一行是
state = processor.process(wrapper);
}
2️⃣ 上面那一堆操作都是建立socket连接,中间做了一堆判断调用处理,下面调用到了Processor来获取Socket中的流信息转换为Tomcat自身封装的Request请求了。默认使用的是HTTP1.1协议AbstractHttp11Processor。
@Override
public SocketState process(SocketWrapper<S> socketWrapper)
throws IOException {
//上面是一堆针对request请求的处理,处理完成的请求通过适配器转换为Servlet Request
getAdapter().service(request, response);
}
3️⃣前面Processor将Socket中读取的流封装成了Tomcat本身的Request对象,但是后面的Servlet要处理的是Servlet Request,所以使用适配器转换一下。适配器使用的是CoyoteAdapter类。
public void service(org.apache.coyote.Request req,
org.apache.coyote.Response res)
throws Exception {
//这上面是一堆Request转换处理的方法,转换处理完调用了这个方法
postParseSuccess = postParseRequest(req, request, res, response);
}
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
//上面又是一堆操作,最终操作到这寻找对应的信息,这是准备工作
connector.getService().getMapper().map(serverName, decodedURI,version, request.getMappingData
());
if (postParseSuccess) {
//前面准备工作都做好了,到这一行就是真正干活了
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
}
4️⃣ 前面已经通过连接器的一系列处理获取到了请求对象Servlet,下面就需要通过容器去查找对应的Host、Context和Wrapper对象了。
//最终后面的map方法通过请求URI查找到了对应的host
public void map(MessageBytes host, MessageBytes uri, String version,
MappingData mappingData) throws IOException {
if (host.isNull()) {
host.getCharChunk().append(defaultHostName);
}
host.toChars();
uri.toChars();
internalMap(host.getCharChunk(), uri.getCharChunk(), version,
mappingData);
}
//进行对象查找并且封装到MappingData中。
private final void internalMap(CharChunk host, CharChunk uri,
String version, MappingData mappingData) throws IOException {
//这里是查找对应的host主机
MappedHost[] hosts = this.hosts;
MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
mappingData.host = mappedHost.object;
//这里是查找对应的context
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
...
mappingData.context = contextVersion.object;
mappingData.contextSlashCount = contextVersion.slashCount;
//到这里去找对应的Wrapper
if (!contextVersion.isPaused()) {
internalMapWrapper(contextVersion, uri, mappingData);
}
}
看一下最终封装到MappingData中的数据,对应的请求解析到了Host、Context、Wrapper。
5️⃣既然前面的准备工作都做好了,找到了对应的Host、Context和Wrapper对象了,下面开始进行上面那副图里面的一串调用了。还是看到前面第三步CoyoteAdapter的操作,下面不是还有一行操作吗?
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
//上面又是一堆操作,最终操作到这寻找对应的信息,这是准备工作
connector.getService().getMapper().map(serverName, decodedURI,version, request.getMappingData
());
if (postParseSuccess) {
//前面准备工作都做好了,到这一行就是真正干活了
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
}
对于这个invoke可以自己点进去看一下,可以看到这是Tomcat架构图中那一层层的剥开啊,从service一直invoke了好几层最终到达了Wrapper的invoke方法,开始真正的调用servlet执行了。看一下StandardWrapperValve中的invoke方法。
public final void invoke(Request request, Response response)
throws IOException, ServletException {
...//前面一大串直接跳过省略了
try {
//看这里找到了对应的Servlet
if (!unavailable) {
servlet = wrapper.allocate();
}
}
...//中间的一大堆代码也跳过省略了,这里是一堆过滤器链
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
try {
if ((servlet != null) && (filterChain != null)) {
if (context.getSwallowOutput()) {
...
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else if (comet) {
filterChain.doFilterEvent(request.getEvent());
} else {
//执行过滤器方法,进去看过滤器里面的方法
filterChain.doFilter(request.getRequest(), response.getResponse());
}
}
}
}
...//后面的一堆处理也跳过了
}
6️⃣这是最终执行方法demo中方法的地方了,看一下过滤器里面的方法,到了这里开始执行到咱写的demo中servlet中的方法了。
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
。。。
internalDoFilter(request,response);
}
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
...
//在这个方法里面有这么一行,到了这一行才是真正执行Sevlet方法
servlet.service(request, response);
}
看最终执行的servlet中的service,可以看到会根据请求的方法不同调用servlet中不同的方法。我们写的servlet是doGet()最终执行了doGet返回想要的结果。
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
doGet(req, resp);
}
...
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}