【java高级进阶】之Tomcat深度剖析及性能调优
1、Tomcat系统架构与原理剖析
1.1 浏览器访问服务器的流程
1.1.1 http请求的处理过程图
1.1.2 http请求的处理过程说明
- 浏览器访问服务器使用的是Http协议,用于定义数据通信的格式
- 具体的数据传输使用的是TCP/IP协议
- Http协议对应于应用层协议,Tcp/IP协议对应于传输层/网络层
1.1.3 协议分层
1.1.4 TCP的三次握手和四次挥手
三次握手
四次挥手
1.2 Tomcat系统总体架构
1.2.1 Tomcat请求处理大致过程
流程图
说明
- Tomcat的两个重要身份:HTTP服务器()和Servlet容器
- HTTP服务器接收到请求之后把请求交给Servlet容器来处理
- Servlet容器通过Servlet接口调用业务类
1.2.2 Tomcat Servlet容器处理流程
流程图
说明
- HTTP服务器会把请求信息使用ServletRequest对象封装起来
- 进一步去调用Servlet容器中某个具体的Servlet(根据URL和Servlet的映射关系,找到相应的Servlet)
- 如果Servlet还没有被加载,就用反射机制创建这个Servlet,并调用Servlet的init方法来完成初始化
- 接着调用这个具体Servlet的service方法来处理请求,请求处理结果使用ServletResponse对象封装
- 把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给客户端
1.2.3 Tomcat 系统总体架构
架构图
说明
- Tomcat设计了两个核心组件:连接器(Connector)和容器(Container)来完成Tomcat的两个核心功能:HTTP处理器和Servlet容器
- 连接器(Connector)负责对外交流:处理Socket连接,负责网络字节流与Request和Response对象的转化;对应Tomcat连接器组件Coyote
- 容器(Container)负责内部处理:加载和管理Servlet,以及具体处理Request请求;对应Tomcat Servlet容器Catalina
1.3 Tomcat连接器组件Coyote
1.3.1 Coyote简介
组件图
说明
- Coyote是Tomcat中连接器的组件名称, 是对外的接口。客户端通过Coyote与服务器建立连接、发送请求并接受响应
- Coyote封装了底层的网络通信(Socket请求及响应处理),Coyote负责的是具体协议(应用层)和IO(传输层)相关内容
- Coyote使Catalina容器(容器组件)与具体的请求协议及IO操作方式完全解耦
- Coyote将Socket输入转换封装为Request对象,进一步封装成ServletRequest交由Catalina容器进行处理,处理请求完成后,Catalina返回的ServletResponse封装成Coyote提供的Response对象将结果写入输出流
Tomcat Coyote ⽀持的 IO模型与协议
说明:默认协议是HTTP/1.1,默认IO模型是NIO
1.3.2 Coyote的内部组件及流程
流程图
Coyote 组件及作⽤
组件 | 作用描述 |
---|---|
EndPoint | EndPoint是Coyote通信端点,即通信监听的接口,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint是用来实现TCP/IP协议的 |
Processor | Processor是Coyote协议处理接口,如果说EndPoint是用来实现TCP/IP协议的,那么Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat的Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象 |
ProtocolHandler | Coyote协议接口,通过Endpoint和Processor,实现针对具体协议的处理能力。Tomcat按照协议和I/O提供了6个实现类:AjpNioProtocol、AjpAprProtocol、AjpNio2Protocol、Http11NioProtocol、Http11Nio2Protocol、Http11AprProtocol |
Adapter | 由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类来封装这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,连接器调用CoyoteAdapter的Sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器 |
1.4 Tomcat Servlet容器Catalina
1.4.1 Tomcat模块分层结构图及Catalina位置
模块分层结构图
说明
- Tomcat是一个由一系列可配置(conf/server.xml)的组件构成的Web容器,Catalina是Tomcat的servlet容器
- Tomcat本质上就是一款Servlet容器,因为Catalina才是Tomcat的核心,其他模块都是为Catalina提供支撑的
- 其他支撑模块 :
Coyote模块提供链接通信
Jasper模块提供JSP引擎
Naming提供JNDI服务
Juli提供日志服务
1.4.2 Servlet 容器Catalina的结构
结构图
说明
- Catalina负责解析Tomcat的配置⽂件(server.xml) , 以此来创建服务器Server组件并进⾏管理
- Server服务器表示整个Catalina Servlet容器以及其它组件,负责组装并启动Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接⼝,提供了⼀种优雅的启动和关闭整个系统的⽅式
- Service服务是Server内部的组件,⼀个Server包含多个Service。它将若⼲个Connector组件绑定到⼀个Container
- Container容器,负责处理⽤户的servlet请求,并返回对象给web⽤户的模块
1.4.3 Container组件的具体结构
结构图
说明
- Container的子容器Engine、Host、Context、Wrapper是逐层包含的关系,其中Engine是最顶层,每个service 最多只能有一个Engine, Engine里面可以有多个Host,每个Host下可以有多个Context,每个Context下可以有多个Wrapper
- Engine :引擎,用来管理多个站点,一个Service 最多只能有一个Engine
- Host :代表一个站点,也可以叫虚拟主机,通过配置Host 就可以添加站点
- Context :代表一个应用程序,对应着平时开发的一套程序,或者一个WEB-INF目录以及下面的web.xml 文件
- Wrapper :每个Wrapper 封装着一个servlet
2、Tomcat服务器核心配置详解
核⼼配置在tomcat⽬录下conf/server.xml⽂件
2.1 主要标签结构
<!--Server是根元素,创建1个Server实例,子标签有 Listener、GlobalNamingResources、Service-->
<Server>
<!--定义监听器-->
<Listener/>
<!--定义服务器的全局JNDI资源 -->
<GlobalNamingResources/>
<!--定义1个Service服务,1个Server标签可以有多个Service服务实例-->
<Service/>
</Server>
2.2 Server标签
<!-- port:关闭服务器的监听端口;shutdown:关闭服务器的指令字符串-->
<Server port="8005" shutdown="SHUTDOWN">
<!-- 以日志形式输出服务器、操作系统、JVM的版本信息 -->
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<!-- Security listener. Documentation at /docs/config/listeners.html
<Listener className="org.apache.catalina.security.SecurityListener" />
-->
<!-- 加载(服务器启动)和销毁(服务器停止)APR。如果找不到APR库,则会输出日志,并不影响Tomcat启动 -->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<!-- 避免JRE内存泄漏问题 -->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<!-- 加载(服务器启动)和 销毁(服务器停止)全局命名服务 -->
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<!-- 在Context停止时重建 Executor 池中的线程, 以避免ThreadLocal 相关的内存泄漏 -->
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- GlobalNamingResources 定义了全局命名服务-->
<GlobalNamingResources>
<Resource name="UserDatabase"
auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
...
</Service>
</Server>
2.3 Service标签
<!--
该标签用于创建Service实例,默认使用org.apache.catalina.core.StandardService。
默认情况下,Tomcat仅指定了Service的名称,值为 "Catalina"。
Service子标签为:Listener、Executor、Connector、Engine
其中:
Listener 用于为Service添加生命周期监听器,
Executor 用于配置Service 共享线程池,
Connector 用于配置Service 包含的链接器,
Engine 用于配置Service中链接器对应的Servlet 容器引擎
-->
<Service name="Catalina">
...
</Service>
2.4 Executor标签
<!--
默认情况下,Service并未添加共享线程池配置
如果我们想添加一个线程池, 可以在<Service> 下添加如下配置:
name:线程池名称,用于Connector中指定
namePrefix:所创建的每个线程的名称前缀,一个单独的线程名称为
namePrefix+threadNumber
maxThreads:池中最大线程数
minSpareThreads:活跃线程数,也就是核心池线程数,这些线程不会被销毁,会一致存在
maxIdleTime:线程空闲时间,超过该时间后,空闲线程会被销毁,默认值为6000(1分钟),单位毫秒
maxQueueSize:在被执行前最大线程排队数目,默认为Int的最大值,也就是广义的无限。除非特殊情况,这个值不需要更改,否则会有请求不会被处理的情况发生
prestartminSpareThreads:启动线程池时是否启动minSpareThreads部分线程。默认值为false,即不启动
threadPriority:线程池中线程优先级,默认值为5,值从1到10
className:线程池实现类,未指定情况下,默认实现类为org.apache.catalina.core.StandardThreadExecutor。如果想使用定义线程池首先需要实现org.apache.catalina.Executor接口
-->
<Executor name="commonThreadPool"
namePrefix="thread-exec-"
maxThreads="200"
minSpareThreads="100"
maxIdleTime="60000"
maxQueueSize="Integer.MAX_VALUE"
prestartminSpareThreads="false"
threadPriority="5"
className="org.apache.catalina.core.StandardThreadExecutor"/>
2.5 Connector标签
<!--
默认情况下,server.xml配置了两个链接器,一个支持HTTP协议,一个支持AJP协议
大多数情况下,我们并不需要新增链接器配置,只是根据需要对已有链接器进行优化
port:端口号,Connector用于创建服务端Socket并进行监听,以等待客户端请求链接。如果该属性设置为0,Tomcat将会随机选择一个可用的端口号给当前Connector使用
protocol:当前Connector支持的访问协议。默认为HTTP/1.1,并采自动动切换机制选择一个基于JAVA NIO的链接器或者基于本地APR的链接器(根据本地是否含有Tomcat的本地库判定)
connectionTimeOut:Connector接收链接后的等待超时时间,单位为毫秒。-1 表示不超时
redirectPort:当前Connector 不支持SSL请求, 接收到了一个请求,并且也符合security-constraint 约束,需要SSL传输,Catalina自动将请求重定向到指定的端口
executor:指定共享线程池的名称,也可以通过maxThreads、minSpareThreads等属性配置内部线程池。可以使用共享线程池
URIEncoding:用于指定编码URI的字符编码,Tomcat8.x版本默认的编码为UTF-8, Tomcat7.x版本默认为ISO-8859-1
-->
<!-- HTTP协议 -->
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<!-- AJP协议 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
2.6 Engine标签
<!--
Engine表示Servlet引擎
name:用于指定Engine的名称,默认为Catalina
defaultHost:默认使用的虚拟主机名称,当客户端请求指向的主机无效时,将交由默认的虚拟主机处理,默认为localhost
-->
<Engine name="Catalina" defaultHost="localhost">
...
</Engine>
2.7 Host标签
<!-- Host标签用于配置一个虚拟主机 -->
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
...
</Host>
2.8 Context标签
<Host name="www.abc.com" appBase="webapps" unpackWARs="true" autoDeploy="true">
<!--
Context用于配置一个Web应用
docBase:Web应目录录或者War包的部署路径。可以是绝对路径,也可以是相对于Host appBase的相对路径
path:Web应?的Context路径。如果我们Host名为localhost,则该web应?访问的根路径为:http://localhost:8080/web_demo。
-->
<Context docBase="/Users/yingdian/web_demo" path="/web3"></Context>
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
3、手写实现迷你版Tomcat
3.1 思路
作为一个服务器软件提供服务,可以通过浏览器发送http请求,服务接收到请求进行处理,处理之后的结果可以返回浏览器客户端
- 提供服务,接受请求(Socket通信)
- 请求信息封装Request对象
- 客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
- 封装Response对象,资源返回给客户端浏览器
3.1 代码
- 启动类
Bootstrap.java
package server;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Bootstrap {
// 默认端口号
private int port = 8080;
private Map<String, Servlet> servletMap = new HashMap<String, Servlet>();
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public void start() throws Exception {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("==========> MiniCat start,port:" + port);
// 加载web.xml配置文件
loadServlet();
// 自定义线程池
int corePoolSize = 10;
int maximumPoolSize = 50;
long keepAliveTime = 100L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
// 监听socket请求
while (true) {
Socket socket = serverSocket.accept();
// 创建线程
RequestProcessor requestProcessor = new RequestProcessor(socket, servletMap);
threadPoolExecutor.execute(requestProcessor);
}
}
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.start();
} catch (Exception e) {
e.printStackTrace();
}
}
/** 解析web.xml,封装serveletMap */
private void loadServlet() {
String absolutePath = StaticResoutceUtil.getAbsolutePath("web.xml");
SAXReader reader = new SAXReader();
try {
Document document = reader.read(absolutePath);
Element element = document.getRootElement(); // 获取*节点
List<Element> servletElements = element.selectNodes("//servlet");
for (Element servletElement : servletElements) {
Element servletName = (Element) servletElement.selectSingleNode("//servlet-name");
Element servletClass = (Element) servletElement.selectSingleNode("//servlet-class");
// 获取servlet实例
Servlet servelt = (Servlet) Class.forName(servletClass.getStringValue()).newInstance();
// 根据servlet-name获取对应的servlet-mapping中的url-pattern>
Element servletMapping =
(Element)
element.selectSingleNode(
"/web-app/servlet-mapping[servlet-name='"
+ servletName.getStringValue()
+ "']");
String stringValue = servletMapping.selectSingleNode("url-pattern").getStringValue();
servletMap.put(stringValue, servelt);
}
} catch (DocumentException | ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
- 请求对象
Request.java
和相应对象Response.java
package server;
import java.io.IOException;
import java.io.InputStream;
/** 封装请求信息 */
public class Request {
private String method; // 请求方式(GET/POST)
private String url; // 请求资源
private InputStream inputStream; // 输入流
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public Request() {}
public Request(InputStream inputStream) throws IOException {
this.inputStream = inputStream;
int count = 0;
while (count == 0) {
count = inputStream.available();
}
byte[] bytes = new byte[count];
inputStream.read(bytes);
String inputStr = new String(bytes);
String[] split = inputStr.split("\\n"); // 根据换行符分隔
String firstLine = split[0]; // 获取第一行
String[] strs = firstLine.split(" ");
method = strs[0]; // 方法
url = strs[1]; // 请求资源
System.out.println(method);
System.out.println(url);
}
}
package server;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
/** 封装相应对象 */
public class Response {
private OutputStream outputStream;
public OutputStream getOutputStream() {
return outputStream;
}
public void setOutputStream(OutputStream outputStream) {
this.outputStream = outputStream;
}
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}
/**
* 输出字符串
*
* @param content
* @throws IOException
*/
public void outPut(String content) throws IOException {
outputStream.write(content.getBytes());
}
/**
* 输出静态资源
*
* @param path
* @throws IOException
*/
public void outPutHtml(String path) throws IOException {
String absoluteResourcePah = StaticResoutceUtil.getAbsolutePath(path);
File file = new File(absoluteResourcePah);
if (file.exists() && file.isFile()) {
// 资源存在,输出静态资源
StaticResoutceUtil.outPutStaticResource(new FileInputStream(file), outputStream);
} else {
// 资源不存在,输出404
outPut(HttpProtocolUtil.getHttpHeader404());
}
}
}
- 多线程处理类
RequestProcessor.java
package server;
import java.io.InputStream;
import java.net.Socket;
import java.util.Map;
public class RequestProcessor extends Thread {
private Socket socket;
private Map<String, Servlet> servletMap;
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
public Map<String, Servlet> getServletMap() {
return servletMap;
}
public void setServletMap(Map<String, Servlet> servletMap) {
this.servletMap = servletMap;
}
public RequestProcessor(Socket socket, Map<String, Servlet> servletMap) {
this.socket = socket;
this.servletMap = servletMap;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
int count = 0;
while (count == 0) {
count = inputStream.available();
}
// 封装请求信息
Request request = new Request(inputStream);
// 封装响应信息
Response response = new Response(socket.getOutputStream());
// 判断是否为静态资源
if (servletMap.get(request.getUrl()) == null) {
response.outPutHtml(request.getUrl());
} else {
// 动态资源servlet请求
Servlet httpServlet = servletMap.get(request.getUrl());
httpServlet.service(request, response);
}
socket.close();
} catch (Exception e) {
e.getStackTrace();
}
}
}
- Servlet接口定义
Servlet.java
package server;
public interface Servlet {
/** 初始化方法 */
public void init();
/**
* 执行业务逻辑
*
* @param req
* @param response
*/
public void service(Request req, Response response);
/** 销毁方法 */
public void destory();
}
- HttpServlet抽象类定义
HttpServlet.java
package server;
public abstract class HttpServlet implements Servlet {
@Override
public void init() {}
@Override
public void service(Request req, Response response) {
if ("GET".equals(req.getMethod())) {
doGet(req, response);
} else {
doPost(req, response);
}
}
@Override
public void destory() {}
/**
* 执行get请求
*
* @param req
* @param response
*/
public abstract void doGet(Request req, Response response);
/**
* 执行post请求
*
* @param req
* @param response
*/
public abstract void doPost(Request req, Response response);
}
- 业务类Servlet定义
MyServlet.java
package server;
import java.io.IOException;
public class MyServlet extends HttpServlet {
@Override
public void doGet(Request req, Response response) {
// 执行doGet方法
System.out.println("doGet");
String context = "<p>do Get</p>";
try {
response.outPut(HttpProtocolUtil.getHttpHeader200(context.length()) + context);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(Request req, Response response) {
// 执行doPost方法
System.out.println("doPost");
String context = "<p>do Post</p>";
try {
response.outPut(HttpProtocolUtil.getHttpHeader200(context.length()) + context);
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 工具类之Http协议工具类
HttpProtocolUtil.java
package server;
/** http协议工具类,提供相应头信息,提供200和404状态 */
public class HttpProtocolUtil {
public static String getHttpHeader200(int contentLength) {
return "HTTP/1.1 200 ok \n"
+ "Content-Type: text/html \n"
+ "Content-Length:"
+ contentLength
+ "\n"
+ "\r\n";
}
public static String getHttpHeader404() {
String str404 = "<h1>404 not found</h1>";
return "HTTP/1.1 404 NOT Found \n"
+ "Content-Type: text/html \n"
+ "Content-Length:"
+ str404.length()
+ "\n"
+ "\r\n"
+ str404;
}
}
- 工具类之Http静态资源请求处理工具类
StaticResoutceUtil.java
package server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class StaticResoutceUtil {
/**
* 获取静态资源的绝对路径
*
* @param path
* @return
*/
public static String getAbsolutePath(String path) {
String absolutePath = StaticResoutceUtil.class.getResource("/").getPath();
return absolutePath.replaceAll("\\\\", "/") + path;
}
/**
* 读取静态资源输入流,通过输出流输出
*
* @param inputStream
* @param outputStream
*/
public static void outPutStaticResource(InputStream inputStream, OutputStream outputStream)
throws IOException {
int count = 0;
while (count == 0) {
count = inputStream.available();
}
int resourceSize = count;
// 输出http相应头
outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
// 读取内容输出
long written = 0; // 已经读取的内容长度
int byteSize = 1024; // 计划每次缓冲的长度
byte[] bytes = new byte[byteSize];
// 判断已经读取的内容长度如果小于总长度,需要继续读取
while (written < resourceSize) {
// 判断本次要读取的长度是否小于1024
if (resourceSize - written < byteSize) {
byteSize = resourceSize - (int) written;
bytes = new byte[byteSize];
}
inputStream.read(bytes);
outputStream.write(bytes);
written += byteSize;
outputStream.flush();
}
}
}
-
web.xml
配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>server.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>myServlet</servlet-name>
<url-pattern>/myServlet</url-pattern>
</servlet-mapping>
</web-app>
4、源码构建及核心流程源码剖析
4.1 源码构建
4.1.1 下载源码
4.1.2 源码导入IDE之前准备工作
- 解压tar.gz压缩包,得到目录apache-tomcat-8.5.50-src
- 进入apache-tomcat-8.5.50-src目录,创建一个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>apache-tomcat-8.5.50-src</artifactId>
<name>Tomcat8.5</name>
<version>8.5</version>
<build>
<!-- 指定源目录 -->
<finalName>Tomcat8.5</finalName>
<sourceDirectory>java</sourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>s
<plugins>
<!-- 引入编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<!--tomcat 依赖的基础包-->
<dependencies>
<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>
<dependency>
<groupId>javax.xml.soap</groupId>
<artifactId>javax.xml.soap-api</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
</project>
- 在apache-tomcat-8.5.50-src目录中创建source文件夹
- 将conf、webapps目录移动到刚刚创建的source文件夹中
4.1.3 导入源码工程到IDE并进行配置
- 将源码工程导入到IDEA中
- 给tomcat的源码程序启动类Bootstrap配置VM参数,因为tomcat源码运行也需要加载配置文件
-Dcatalina.home=/tomcat源文件目录/apache-tomcat-8.5.50-src/source
-Dcatalina.base=/tomcat源文件目录/apache-tomcat-8.5.50-src/source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=/tomcat源文件目录//apache-tomcat-8.5.50-src/source/conf/logging.properties
- 运行Bootstrap类的main函数,此时就启动了tomcat,启动时候会去加载所配置的conf目录下的server.xml等配置文件,访问8080会出现如下错误==(错误原因:Jsp引擎Jasper没有被初始化)==
解决办法
在tomcat的源码ContextConfig类中的configureStart方法中增加一行代码将Jsp引擎初始化
// 初始化jsp搜索引擎
context.addServletContainerInitializer(new JasperInitializer(), null);
4.2 核心源码剖析
4.2.1 生命周期接口Lifecycle
什么是Lifecycle
- Tomcat里面有各种各样的组件,每个组件各司其职,组件之间又相互协作共同完成web服务器这样的工程。在这些组件之上,Lifecycle(生命周期机制)至关重要
- Lifecycle,其实就是一个状态机,对组件的由生到死状态的管理
Lifecycle
的方法
public interface Lifecycle {
// 添加监听器
public void addLifecycleListener(LifecycleListener listener);
// 获取所以监听器
public LifecycleListener[] findLifecycleListeners();
// 移除某个监听器
public void removeLifecycleListener(LifecycleListener listener);
// 初始化方法
public void init() throws LifecycleException;
// 启动方法
public void start() throws LifecycleException;
// 停止方法,和start对应
public void stop() throws LifecycleException;
// 销毁方法,和init对应
public void destroy() throws LifecycleException;
// 获取生命周期状态
public LifecycleState getState();
// 获取字符串类型的生命周期状态
public String getStateName();
}
Lifecycle
生命周期接口继承体系
Lifecycle
的基本实现LifecycleBase
的核心方法
- 增加、删除和获取监听器(操作监听器集合lifecycleListeners)
private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
@Override
public void addLifecycleListener(LifecycleListener listener) {
lifecycleListeners.add(listener);
}
@Override
public LifecycleListener[] findLifecycleListeners() {
return lifecycleListeners.toArray(new LifecycleListener[0]);
}
@Override
public void removeLifecycleListener(LifecycleListener listener) {
lifecycleListeners.remove(listener);
}
- 初始化方法
init()
,抽象方法initInternal()
需要组件自行实现
@Override
public final synchronized void init() throws LifecycleException {
// 非NEW状态,不允许调用init()方法
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
// 初始化逻辑之前,先将状态变更为`INITIALIZING`
setStateInternal(LifecycleState.INITIALIZING, null, false);
// 初始化,该方法为一个abstract方法,需要组件自行实现
initInternal();
// 初始化完成之后,状态变更为`INITIALIZED`
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
// 初始化的过程中,可能会有异常抛出,这时需要捕获异常,并将状态变更为`FAILED`
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(
sm.getString("lifecycleBase.initFail",toString()), t);
}
}
- 启动方法
start()
,抽象方法startInternal()
需要组件自行实现
@Override
public final synchronized void start() throws LifecycleException {
// `STARTING_PREP`、`STARTING`和`STARTED时,将忽略start()逻辑
if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {
if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
}
return;
}
// `NEW`状态时,执行init()方法
if (state.equals(LifecycleState.NEW)) {
init();
}
// `FAILED`状态时,执行stop()方法
else if (state.equals(LifecycleState.FAILED)) {
stop();
}
// 不是`INITIALIZED`和`STOPPED`时,则说明是非法的操作
else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}
try {
// start前的状态设置
setStateInternal(LifecycleState.STARTING_PREP, null, false);
// start逻辑,抽象方法,由组件自行实现
startInternal();
// start过程中,可能因为某些原因失败,这时需要stop操作
if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
// 设置状态为STARTED
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(sm.getString("lifecycleBase.startFail", toString()), t);
}
}
- 停止方法
stop()
,抽象方法stopInternal()
需要组件自行实现
@Override
public final synchronized void stop() throws LifecycleException {
// `STOPPING_PREP`、`STOPPING`和STOPPED时,将忽略stop()的执行
if (LifecycleState.STOPPING_PREP.equals(state) || LifecycleState.STOPPING.equals(state) ||
LifecycleState.STOPPED.equals(state)) {
if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStopped", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStopped", toString()));
}
return;
}
// `NEW`状态时,直接将状态变更为`STOPPED`
if (state.equals(LifecycleState.NEW)) {
state = LifecycleState.STOPPED;
return;
}
// stop()的执行,必须要是`STARTED`和`FAILED`
if (!state.equals(LifecycleState.STARTED) && !state.equals(LifecycleState.FAILED)) {
invalidTransition(Lifecycle.BEFORE_STOP_EVENT);
}
try {
// `FAILED`时,直接触发BEFORE_STOP_EVENT事件
if (state.equals(LifecycleState.FAILED)) {
// Don't transition to STOPPING_PREP as that would briefly mark the
// component as available but do ensure the BEFORE_STOP_EVENT is
// fired
fireLifecycleEvent(BEFORE_STOP_EVENT, null);
} else {
// 设置状态为STOPPING_PREP
setStateInternal(LifecycleState.STOPPING_PREP, null, false);
}
// stop逻辑,抽象方法,组件自行实现
stopInternal();
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
if (!state.equals(LifecycleState.STOPPING) && !state.equals(LifecycleState.FAILED)) {
invalidTransition(Lifecycle.AFTER_STOP_EVENT);
}
// 设置状态为STOPPED
setStateInternal(LifecycleState.STOPPED, null, false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(sm.getString("lifecycleBase.stopFail",toString()), t);
} finally {
if (this instanceof Lifecycle.SingleUse) {
// Complete stop process first
setStateInternal(LifecycleState.STOPPED, null, false);
destroy();
}
}
}
- 销毁方法
destroy()
,抽象方法destroyInternal()
需要组件自行实现
@Override
public final synchronized void destroy() throws LifecycleException {
// `FAILED`状态时,直接触发stop()逻辑
if (LifecycleState.FAILED.equals(state)) {
try {
// Triggers clean-up
stop();
} catch (LifecycleException e) {
// Just log. Still want to destroy.
log.warn(sm.getString(
"lifecycleBase.destroyStopFail", toString()), e);
}
}
// `DESTROYING`和`DESTROYED`时,忽略destroy的执行
if (LifecycleState.DESTROYING.equals(state) ||
LifecycleState.DESTROYED.equals(state)) {
if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyDestroyed", toString()), e);
} else if (log.isInfoEnabled() && !(this instanceof Lifecycle.SingleUse)) {
// Rather than have every component that might need to call
// destroy() check for SingleUse, don't log an info message if
// multiple calls are made to destroy()
log.info(sm.getString("lifecycleBase.alreadyDestroyed", toString()));
}
return;
}
// 非法状态判断
if (!state.equals(LifecycleState.STOPPED) &&
!state.equals(LifecycleState.FAILED) &&
!state.equals(LifecycleState.NEW) &&
!state.equals(LifecycleState.INITIALIZED)) {
invalidTransition(Lifecycle.BEFORE_DESTROY_EVENT);
}
try {
// destroy前状态设置
setStateInternal(LifecycleState.DESTROYING, null, false);
// 抽象方法,组件自行实现
destroyInternal();
// destroy后状态设置
setStateInternal(LifecycleState.DESTROYED, null, false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(
sm.getString("lifecycleBase.destroyFail",toString()), t);
}
}
总结:
- 启动方法start()和停止方法stop()相对应
- 初始化方法init()和销毁方法destory()相对应
- LifecycleBase是使用了状态机+模板模式来实现的,模板方法如下
// 初始化方法
protected abstract void initInternal() throws LifecycleException;
// 启动方法
protected abstract void startInternal() throws LifecycleException;
// 停止方法
protected abstract void stopInternal() throws LifecycleException;
// 销毁方法
protected abstract void destroyInternal() throws LifecycleException;
4.2.2 核心流程源码剖析之Tomcat启动流程
- Tomcat启动命令startup.bat(这里分析windows系统的命令),里面执行了catalina.bat
- Tomcat命令catalina.bat(这里分析windows系统的命令),里面配置了Bootstrap
- Tomcat启动流程图
4.2.3 核心流程源码剖析之Tomcat请求处理流程
- 请求流程分析
- 请求处理流程示意图
- Mapper组件体系结构
5、Tomcat类加载机制剖析
5.1 JVM 的类加载机制
5.1.1 概念
- 类的加载:JVM将指定的class文件读取到内存里,并运行该class文件里的Java程序的过程
- 类加载器ClassLoader:是java的运行环境JRE(包括java虚拟机和基础类库)的一部分,负责将java类加载到虚拟机中
5.1.2 类加载器分类
分类
类加载器 | 作用 |
---|---|
引导类加载器(Bootstrap) | c++编写,加载java核心库 java.*,比如rt.jar中的类,构造ExtClassLoader和AppClassLoader |
扩展类加载器(ExtClassLoader) | java编写,加载扩展库 JAVA_HOME/lib/ext?录下的jar中的类,如classpath中的jre,javax.*或者java.ext.dir指定位置中的类 |
应用类加载器(AppClassLoader) | 默认的类加载器,搜索环境变量classpath中指明的路径 |
关系图(父子关系)
5.2 双亲委派机制
5.2.1 什么是双亲委派机制
- 用户自己的类加载器,把加载请求传给父加载器,父加载器再传给其父加载器,一直到加载器树的顶层
- 最顶层的类加载器首先针对其特定的位置加载,如果加载不到就转交给子类
- 如果一直到底层的类加载都没有加载到,那么就会抛出异常ClassNotFoundException
5.2.1 双亲委派机制的作用
- 防止重复加载同一个.class。父类加载器加载过了,子类加载就不会再加载一遍。保证数据安全
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个.class对象。这样
保证了class执行安全
5.3 Tomcat的类加载机制
关系图
tomcat类加载器说明
类加载器 | 说明 |
---|---|
Common类加载器 | 负责加载Tomcat和Web应用都复用的类 |
Catalina类加载器 | 负责加载Tomcat专用的类,而这些被加载的类在Web应用中将不可见 |
Shared类加载器 | 负责加载Tomcat下所有的Web应用程序都复用的类,而这些被加载的类在Tomcat中将不可见 |
WebApp类加载器 | 负责加载具体的某个Web应用程序所使用到的类,而这些被加载的类在Tomcat和其他的Web应用程序都将不可见 |
Jsp类加载器 | 每个jsp页面一个类加载器,不同的jsp页面有不同的类加载器,方便实现jsp页面的热插拔 |
tomcat类加载器为什么打破双亲委派机制
- tomcat中可以有多个webapp,每个webapp有自己的目录和类库
- 比如一个webapp使用类库A1.0版本,一个webapp使用类库A2.0版本
- 父类加载器加载类库A1.0版本,如果使用双亲委派,会由commonClassLoader去加载类库A1.0版本,就不能实现不同webapp使用不同版本的类库
tomcat8.5默认改变了严格的双亲委派机制
- 首先从Bootstrap Classloader加载指定的类
- 如果未加载到,则从/WEB-INF/classes加载
- 如果未加载到,则从/WEB-INF/lib/*.jar 加载
- 如果未加载到,则依次从 System、Common、Shared 加载(在这最后一步,遵从双亲委派机制)
6、Tomcat对Https的支持
6.1 深入理解HTTPS工作原理
6.1.1 https概念
HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 [1] 。HTTPS 在HTTP 的基础下加入SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL
6.1.2 HTTPS和HTTP的主要区别
- HTTPS协议使用时需要到电子商务认证授权机构(CA)申请SSL证书
- HTTP默认使用8080端口,HTTPS默认使用8443端口
- HTTPS则是具有SSL加密的安全性传输协议,对数据的传输进行加密,效果上相当于HTTP的升级版
- HTTP的连接是无状态的,不安全的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全
6.1.3 为什么要使用HTTPS
http协议存在的问题:
- 数据隐私性问题:通信使用明文(不加密),所以可能遭遇窃听
- 数据完整性问题:无法证明报文的完整性,所以可能遭遇篡改
- 身份认证问题:不验证通信方的身份,所以可能遭遇伪装
htts协议解决这些问题
- 数据隐私性问题:内容经过对称加密,每个连接生成一个唯一的加密密钥
- 数据完整性问题:内容传输经过完整性校验
- 身份认证问题:第三方无法伪造服务端(客户端)身份
6.1.4 HTTPS工作原理
-
解决内容可能被窃听的问题—对称加密+非对称加密
具体做法是:发送密文的一方使用对方的公钥进行加密处理“对称的密钥”,然后对方用自己的私钥解密拿到“对称的密钥”,这样可以确保交换的密钥是安全的前提下,使用对称加密方式进行通信。所以,HTTPS采用对称加密和非对称加密两者并用的混合加密机制。 -
解决报文可能遭篡改问题——数字签名
-
解决通信方身份可能被伪装的问题——数字证书
6.1.4 HTTPS工作流程
- Client发起一个HTTPS(比如https://juejin.im/user)的请求,根据RFC2818的规定,Client知道需要连接Server的8443(默认)端口
- Server把事先配置好的公钥证书(public key certificate)返回给客户端
- Client验证公钥证书:比如是否在有效期内,证书的用途是不是匹配Client请求的站点,是不是在CRL吊销列表里面,它的上一级证书是否有效,这是一个递归的过程,直到验证到根证书(操作系统内置的Root证书或者Client内置的Root证书)。如果验证通过则继续,不通过则显示警告信息
- Client使用伪随机数生成器生成加密所使用的对称密钥,然后用证书的公钥加密这个对称密钥,发给Server
- Server使用自己的私钥(private key)解密这个消息,得到对称密钥。至此,Client和Server双方都持有了相同的对称密钥
- Server使用对称密钥加密“明文内容A”,发送给Client
- Client使用对称密钥解密响应的密文,得到“明文内容A”
- Client再次发起HTTPS的请求,使用对称密钥加密请求的“明文内容B”,然后Server使用对称密钥解密密文,得到“明文内容B”
6.2 Tomcat 对 HTTPS 的支持
6.2.1 生成免费的秘钥库文件
到JAVA_HOME\bin目录下执行命令
keytool -genkey -alias test -keyalg RSA -keystore test.keystore
操作指令
6.2.2 配置conf/server.xml
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" schema="https" secure="true" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="tomcat目录\conf\test.keystore" certificateKeystorePassword="设置的密码" type="RSA"/>
</SSLHostConfig>
</Connector>
6.2.3 使用https协议访问8443端⼝(https://localhost:8443)
7、Tomcat性能优化策略
7.1 系统性能衡量指标
- 响应时间:执行某个操作的耗时
- 吞吐量:系统在给定时间内能够支持的事务数量,单位为TPS(Transactions PerSecond的缩写,也就是事务数/秒,一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程)
7.2 Tomcat优化从两个方面进行
7.2.1 JVM虚拟机优化(优化内存模型)
Java虚拟机的运行优化点
- 内存分配优化:内存直接影响服务的运行效率和吞吐量
- 垃圾回收策略优化:垃圾回收机制会不同程度地导致程序运行中断(垃圾回收策略不同,垃圾回收次数和回收效率都是不同的)
内存分配优化
- JVM内存模型回顾
- Java 虚拟机内存相关参数
参数 | 参数作用 | 优化建议 |
---|---|---|
-server | 启动Server,以服务端模式运行 | 服务端模式建议开启 |
-Xms | 最小堆内存 | 建议与-Xmx设置相同 |
-Xmx | 最大堆内存 | 建议设置为可用内存的80% |
-XX:MetaspaceSize | 元空间初始值 | |
-XX:MaxMetaspaceSize | 元空间最大内存 | 默认无限 |
-XX:NewRatio | 年轻代和老年大小比值,取值为整数,默认为2 | 不需要修改 |
-XX:SurvivorRatio | Eden区与Survivor区大小的比值,取值为整数,默认为8 | 不需要修改 |
- 参数调整示例
tomcat的home文件夹下bin/catalina.bat
JAVA_OPTS="-server -Xms2048m -Xmx2048m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
- 调整后查看可使⽤JDK提供的内存映射⼯具
jhsdb --heap --pid
// TODO:
垃圾回收策略优化
- 垃圾回收性能指标
吞吐量:工作时间(排除GC时间)占总时间的百分比,工作时间并不仅是程序运行的时间,还包含内存分配时间。
暂停时间:由垃圾回收导致的应用程序停止响应次数/时间
- 垃圾回收器参数
在bin/catalina.sh的脚本中, 追加如下配置:
JAVA_OPTS="-XX:+UseConcMarkSweepGC"
7.2.2 Tomcat本身配置的优化
- 调整tomcat线程池
- 调整tomcat的连接器
调整tomcat/conf/server.xml 中关于链接器的配置可以提升应⽤服务器的性能
- 禁⽤ AJP 连接器
- 调整 IO 模式
Tomcat8之前的版本默认使用BIO(阻塞式IO),对于每个个请求都要创建一个线程来处理,不适合?并发;Tomcat8以后的版本默认使?NIO模式(非阻塞式IO)
- 动静分离
可以使用Nginx+Tomcat相结合的部署方案,Nginx负责静态资源访问,Tomcat负责Jsp等动态资源访问处理(因为Tomcat不擅长处理静态资源)
本文地址:https://blog.csdn.net/yuyangzhi10/article/details/109701968
上一篇: 蚂蚁Java三面:二叉树+HTTPS加密+自旋锁+缓存穿透(送答案)
下一篇: 过年吃什么菜寓意好