## Tomcat 整体结构

Tomcat 最顶层的容器是 Server,代表整个服务器。
Tomcat 核心组件有两个,分别是负责接收和反馈外部请求的连接器 `Connector`,以及负责处理请求的容器 `Container`。连接器和容器一起构成对外提供的 Web 服务 `Service`,一个 Tomcat 服务器可以有多个`Service`。
#### Connector
每个 `Service` 可以有多个连接器,其主要功能如下:
1、监听网络端口,接收和响应网络请求。
2、网络字节流处理。将受到的网络字节流装换成标准的 ServletRequest 给容器,同时将容器返回的 ServletResponse 在转换成字节流。
#### Container
Container 内部由 Engine、Host、Context 和 Wrapper 四个容器组成,用于管理和调用 Servlet 相关逻辑。每个 Service 包含一个容器,容器有一个引擎(Engine)和多个虚拟主机(Host),每个虚拟主机又可以管理多个 Web 应用(Context)。Wrapper 是最底层的容器,负责对 Servlet 进行封装,负责实例的创建和销毁。
配置文件 server.xml 中的结构和上面的描述所对应,该配置文件在 Tomcat 安装目录 conf 文件夹下。
```xml
<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.JasperListener" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<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">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="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"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
```
## 嵌入式 Tomcat 的使用
引入依赖包
```xml
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.38</version>
</dependency>
```
下面依据整体结构中的描述,完成 Tomcat 的创建并启动。
首先新建测试类,并在其 main 方法中构建 Tomcat 实例并设置基本目录。按照如下设置后,Tomcat 启动后会在当前项目路径下创建名为 `tomcat.temp` 的目录,即使不设置也会按照规则指定一个默认目录,具体可以查看注释。
```java
Tomcat tomcat = new Tomcat();
tomcat.setBaseDir(System.getProperty("user.dir") + "/tomcat.temp");
```
构建顶层容器 Server 和 Service
```java
Server server = tomcat.getServer();
StandardService service = new StandardService();
server.addService(service);
```
`addService()` 可以添加多个 Service,以上代码执行后 Server 中其实包含 2 个 Service,因为调用 `getServer()` 方法时已经创建了一个 Service,如果我们只需要一个 Service 的话,只需要如下
```java
Service service = tomcat.getService();
```
下面开始构建 Service 中的核心组件 Connector,这里创建俩个实例,分别监听 10001 端口和 10002 端口
```java
Connector connector = new Connector(); // 可以指定协议
connector.setPort(10001);
connector.setURIEncoding("UTF-8");
Connector connector2 = new Connector();
connector2.setPort(10002);
connector2.setURIEncoding("UTF-8");
service.addConnector(connector);
service.addConnector(connector2);
```
Service 中另一个核心组件 Container,Engine、Host、Context 和 Wrapper 容器层级由高到低,所以我们也按这个顺序创建
```java
Engine engine = new StandardEngine();
engine.setDefaultHost("lu - host - 1"); // 默认 host 名称必须有,默认都是 localhost
engine.setName("lu - engine - 1"); // 相当于给 engine 指定个名称
service.setContainer(engine);
StandardHost host = new StandardHost();
host.setName("lu - host - 1"); // 指定 host 名称,这个名称与上面 defaultHost 对应
engine.addChild(host);
```
继续创建 Context 并配置启动监听器
```java
Context context = tomcat.addContext(host, "/", System.getProperty("user.dir") + "/target");
context.addLifecycleListener(new ContextConfig());
```
创建 Wrapper,这里可以参照 web.xml 中关于 Servlet 的配置
```xml
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>test.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/my</url-pattern>
</servlet-mapping>
```
```java
Wrapper myservlet = new StandardWrapper();
context.addChild(myservlet);
//myservlet.setServlet(new MyServlet());
myservlet.setServletClass(MyServlet.class.getName());
myservlet.setName("myServlet");
myservlet.addMapping("/my");
myservlet.addMapping("/test"); // 通过 /my 和 /test 这俩个映射路径都可以找到 MyServlet
myservlet.setLoadOnStartup(1);
myservlet.setOverridable(true);
```
启动并设置主线程保持活动
```java
tomcat.start();
tomcat.getServer().await();
```
另外还需要自定义一个 Servlet 便于测试
```java
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String result = param;
String aka = req.getParameter("aka");
System.out.println(aka);
resp.getWriter().write("request get is ok! " + result);
}
}
```
最后启动 main 方法,访问 http://localhost:10001/test?aka=444,返回成功,另外通过 10002 端口同样成功。
## SpringBoot 如何使用 Tomcat
从 SpringApplication.run() 方法可以一直追踪到调用了AbstractApplicationContext.class 的 refresh() 方法,其中 onRefresh() 是个钩子方法,如果是继承 ServletWebServerApplicationContext 类,那么就会执行其重写方法:
```java
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
```
其中 createWebServer() 方法如下:
```java
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
```
继续查看 factory.getWebServer() ,TomcatServletWebServerFactory.class 中:
```java
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
```
可以发现与上面自己的 demo 大致相同。

嵌入式 Tomcat 初体验