参考:
PoetryAndTheDistance :一:Tomcat核心组件及应用架构详解
Hong EuiSung –@gowoonosori : 요청처리 내부구조
tomcatDoc: Class Loader How-To
Http 请求响应实例
Servlet 接口
为了解耦 【http服务器代码】和【业务代码】,Java就定义了一个接口,各种业务类都必须实现这个接口,这个接口就叫 Servlet 接口,有时我们也把实现了 Servlet 接口的业务类叫作 Servlet。
Servlet 接口定义了下面五个方法
public interface Servlet { void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy(); }
- 其中最重要是的 service 方法,具体业务类在这个方法里实现处理逻辑。这个方法有两个参数:ServletRequest 和 ServletResponse。
- ServletRequest 用来封装请求信息,ServletResponse 用来封装响应信息,因此本质上这两个类是对通信协议的封装。
- 比如 HTTP 协议中的请求和响应就是对应了 HttpServletRequest 和 HttpServletResponse 这两个类。你可以通过 HttpServletRequest 来获取所有请求相关的信息,包括请求路径、Cookie、HTTP 头、请求参数等。还可以通过 HttpServletRequest 来创建和获取 Session。而 HttpServletResponse 是用来封装 HTTP 响应的。
- 两个跟生命周期有关的方法 init 和 destroy
-
- Servlet 容器在加载 Servlet 类的时候会调用 init 方法,在卸载的时候会调用 destroy 方法。我们可能会在 init 方法里初始化一些资源,并在 destroy 方法里释放这些资源,
- 比如 Spring MVC 中的 DispatcherServlet,就是在 init 方法里创建了自己的 Servlet WebApplicationContext
- 还有 ServletConfig 这个类,ServletConfig 的作用就是封装 Servlet 的初始化参数。
-
- 你可以在 web.xml 给 Servlet 配置参数,并在程序里通过 getServletConfig 方法拿到这些参数。
有接口一般就有抽象类,抽象类用来实现接口和封装通用的逻辑。
(接口和抽象类都不能实例化,接口只能有方法的声明不能有实现,抽象类可以有方法的实现。接口用来抽象功能,抽象类用来抽象类型)
因此 Servlet 规范提供了 GenericServlet 抽象类,我们可以通过扩展它来实现 Servlet。虽然 Servlet 规范并不在乎通信协议是什么,但是大多数的 Servlet 都是在 HTTP 环境中处理的,因此 Servet 规范还提供了 HttpServlet 来继承 GenericServlet,并且加入了 HTTP 特性。这样我们通过继承 HttpServlet 类来实现自己的 Servlet,只需要重写两个方法:doGet 和 doPost。
HttpServlet 实现的 Servlet 声明的业务方法接口 service 方法里会调用 doGet 和 doPost
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); long lastModified; if (method.equals("GET")) { lastModified = this.getLastModified(req); if (lastModified == -1L) { this.doGet(req, resp); } else { .......省略 } } else if (method.equals("HEAD")) { lastModified = this.getLastModified(req); this.maybeSetLastModified(resp, lastModified); this.doHead(req, resp); } else if (method.equals("POST")) { this.doPost(req, resp); } else if (method.equals("PUT")) { this.doPut(req, resp); } else if (method.equals("DELETE")) { this.doDelete(req, resp); } else if (method.equals("OPTIONS")) { this.doOptions(req, resp); } else if (method.equals("TRACE")) { this.doTrace(req, resp); } else { String errMsg = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[]{method}; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(501, errMsg); } }
Servlet 容器
当客户请求某个资源时:
- HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法
- Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器
- HTTP 服务器会把响应发送给客户端
扩展机制
Servlet 规范提供了两种扩展机制:Filter 和 Listener。
Filter
允许你对请求和响应做一些统一的定制化处理。过滤器的工作原理是这样的:Web 应用部署完成后,Servlet 容器需要实例化 Filter 并把 Filter 链接成一个 FilterChain。当请求进来时,获取第一个 Filter 并调用 doFilter 方法,doFilter 方法负责调用这个 FilterChain 中的下一个 Filter。
比如你可以根据请求的频率来限制访问,或者根据国家地区的不同来修改响应内容。还有 SpringSecurity 的 SessionRepositoryFilter 就 implements 了 Filter,用来校验请求头中的 session
Listener
当 Web 应用在 Servlet 容器中运行时,Servlet 容器内部会不断的发生各种事件,如 Web 应用的启动和停止、用户请求到达等。 Servlet 容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet 容器会负责调用监听器的方法。当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在web.xml中。
比如 Spring 就实现了自己的监听器,来监听 ServletContext 的启动事件,目的是当 Servlet 容器启动时,创建并初始化全局的 Spring 容器。
Servlet容器 与 SpringMVC 的关系
在 SpringMVC 中,有一个名为 DispatcherServlet 的 Servlet,它充当 FrontController,转发、匹配、将请求委托给其他控制器来处理请求。
执行service(),并在方法中执行 dispatch.doService() -> doDispatch(),doDispatch 负责 从 HandlerMapping 中通过 URL 获取handler
在应用启动的时候,Spring 容器会加载这些 Controller 类,并且解析出 URL 对应的处理函数,封装成 Handler 对象,存储到 HandlerMapping 对象中。当有请求到来的时候,DispatcherServlet 从 HanderMapping 中,查找请求 URL 对应的 Handler,然后调用执行 Handler 对应的函数代码,最后将执行结果返回给客户端。
Root WebApplicationContext
- 就是 org.springframework.context.ApplicationContext
- 就是 Spring 的 IOC 容器,包括 Service、datasource、repositories 等 bean
- 通过 Servlet 容器的 Listener 机制,Spring 的 ContextLoaderListener 类实现 ServletContextListener,监听 Servlet 容器的启动事件,创建并初始化这个 spring 全局上下文。
- 管理bean的生命周期并提供 IOC/DI
- Root WebApplicationContext 不能访问 Servlet WebApplicationContext 中的 bean(Controller 里可以访问 Service 对象,Service 对象不可以访问 Controller 对象)
Servlet WebApplicationContext
- 就是 org.springframework.web.context.WebApplicationContext
- 与标准 javax.servlet.ServletContext 配合使用。ServletContext 实例可以做很多事情,例如通过调用 getResourceAsStream() 方法访问 WEB-INF 资源(xml 配置等)。
- 继承自 RootWebApplicationContext 实现的 Context,主要包含 Controller、Intercepter、ViewResolver、HandlerMapping 等bean。
- 之所以有这样的继承关系,是因为一个Servlet容器内部可能会出现多个Servlet,它们要共用 Root WebApplicationContext 中的服务和数据源。
- 延迟加载,当第一个请求到来,Tomcat 发现 DispatcherServlet 还没被实例化,就会调用它的 init 方法,建立容器初始化相关 bean
- 如果一个servlet 上下文的 bean注册了与Application Context和context相同的ID,则使用Servlet Context中声明的bean,查找 bean 时按照 Servlet Context -> Application Context的顺序查找。
public interface WebApplicationContext extends ApplicationContext { ServletContext getServletContext(); }
从服务器启动到请求的步骤
- Web 服务器初始化
- 监听到 Servlet 容器启动。加载 Root WebApplicationContext
- 网络服务器启动
- 客户端第一次向 Web 服务器发出请求
- Web Server 交付给 Servlet 容器
- 创建 Servlet线程
- 如果 DispatcherServlet 没有创建,DispatcherServlet init 初始化 Servlet WebApplicationContext
- 在生成的线程中调用 DisapatcherServlet 的 service() 方法。
- 通过 HandlerMapping 查找 Controller
- 通过 HandlerAdapter 传递给 Controller
- Controller->Service->Response
Filter 与 Intercepter
Filter 是 Servlet 规范的一部分,Servlet 容器 Tomcat 实现了它;Intercepter 是 Spring 实现的。它们的执行顺序是
- Filter.doFilter();
- HandlerInterceptor.preHandle();
- Controller
- HandlerInterceptor.postHandle();
- HandlerInterceptor.afterCompletion();
- Filter.doFilter()
- Servlet 方法返回
Tomcat 配置
Tomcat 总体架构
Tomcat 实现的 2 个核心功能:
- 连接器(Connector):处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
- 容器(Container):加载并管理 Servlet ,以及处理具体的 Request 请求。
所以 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)。连接器负责对外交流,容器负责内部处理
- Service 默认只有一个,也就是一个 Tomcat 实例默认一个 Service。
- Connector:一个 Service 可能多个连接器,接受不同连接协议。
- Container: 多个连接器对应一个容器,顶层容器其实就是 Engine。
Tomcat 为了实现支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。
连接器
连接器主要完成三个功能:
- EndPoint :网络通信。负责提供字节流给 Processor
- Processor(Coyote):应用层协议解析。负责提供 Tomcat Request 对象给 Adapter
- Adapter:Tomcat request/response 与 ServletRequest/ ServeletReponse 的转换。负责提供 ServletRequest对象给容器。
主要处理 网络连接 和 应用层协议 的两个重要部件 EndPoint 和 Processor 组合形成 ProtocoHandler
Tomcat 支持的 I/O 模型有:
- NIO:非阻塞 I/O,采用 Java NIO 类库实现。
- NIO2:异步I/O,采用 JDK 7 最新的 NIO2 类库实现。
- APR:采用 Apache可移植运行库实现,是 C/C++ 编写的本地库。
Tomcat 支持的应用层协议有:
- HTTP/1.1:这是大部分 Web 应用采用的访问协议。
- AJP:用于和 Web 服务器集成(如 Apache)。
- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
容器(Engine)
Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。因为这里正好符合一个 Host 多个 Context, 一个 Context 也包含多个 Servlet,而每个组件都需要统一生命周期管理,所以组合模式设计这些容器
server.xml
<Server port="8005" shutdown="SHUTDOWN"> // 顶层组件,可包含多个 Service,代表一个 Tomcat 实例 <Service name="Catalina"> // 顶层组件,包含一个 Engine ,多个连接器 // 连接器1:http 协议 <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> // 连接器2:AJP 协议 <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> // 容器组件:一个 Engine 处理 Service 所有请求,包含多个 Host <Engine name="Catalina" defaultHost="localhost"> //容器组件:处理指定Host下的客户端请求, 可包含多个 Context <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> // 容器组件:处理特定 Context Web应用的所有客户端请求 <Context> </Context> </Host> </Engine> </Service> </Server>
Wrapper 就是 Servlet。Servlet 的映射可以通过配置文件配置 web.xml。这个里面也可以配置 Filter
<servlet> <servlet-name>DownloadServlet</servlet-name> <servlet-class> com.test.servlet.DownloadServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>DownloadServlet</servlet-name> <url-pattern>/download</url-pattern> </servlet-mapping>
也可以通过注解
@WebServlet(urlPatterns = "/download") public class FetchMessageServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //...... }
Mapper组件里保存了 Web 应用的配置信息。当一个请求到来时,Mapper 组件通过解析请求 URL 里的域名和路径,再到自己保存的 Map 里去查找,就能定位到一个 Servlet。
启动流程
- Tomcat 本质上是一个 Java 程序,因此 startup.sh 脚本会启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap。
- Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina。关于 Tomcat 为什么需要自己的类加载器,后面详细介绍。
- Catalina 是一个启动类,它通过解析 server.xml、创建相应的组件,并调用 Server 的 start 方法。
- Server 组件的职责就是管理 Service 组件,它会负责调用 Service 的 start 方法。
- Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Engine 的 start 方法。
Tomcat 类加载器
类加载前情知识
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。
对于任意一个类,都必须由 加载它的类加载器 和 这个类本身一起共同确立其在Java虚拟机中的唯一性。即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
站在Java虚拟机的角度来看,只存在两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
- 负责加载存放在 <java_home>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,用来加载 JVM 启动时所需要的核心类,比如rt.jar、resources.jar等。
- 其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
- 扩展类加载器(Extension ClassLoader):负责加载<java_home>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
- 用户类加载器(Application ClassLoader):加载用户类路径 (ClassPath)上所有的类库
自JDK 1.2以来,Java一直保 持着三层类加载器、双亲委派的类加载架构。
- 双亲委派模型的工作过程是:
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
- 好处
- 很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载)
- Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。
- 例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
- 反之
- 如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应 用程序将会变得一片混乱。
Tomcat 类加载器
tomcat6 之前
从图中的委派关系中可以看出(父类加载器类库的可以被子类加载器使用,同级类加载器的类库相互隔离)
- CommonClassLoader 用来加载放置在/common目录中的。
- 类库可被Tomcat和所有的Web应用程序共同使用。
- CatalinaClassLoader(也称为 ServerClassLoader)用来加载放置在/server目录中的。
- 类库可被Tomcat使用,对所有的Web应用程序都不可见。
- SharedClassLoader 用来加载放置在/shared目录中的。
- 类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。(实现了两个Web应用程序 所使用的 Java公共类库可以互相共享)
- WebappClassLoader 用来加载放置在/WebApp/WEB-INF目录中的。
- 类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应 用程序都不可见。(实现了两个Web应用程序 所使用的 Java类库可以实现相互隔离)
tomcat6 以后
Java ClassLoader: Bootstrap ClassLoader(加载$JAVA_HOME/jre/lib/目录下核心类库:resources.jar,rt.jar,sunrsasign.jar,jsse.jar,jce.jar,charsets.jar,jfr.jar,以及jre/classes目录下的class) /|\ | Tomcat ClassLoader: ExtClassLoader(加载$JAVA_HOME/jre/lib/ext/目下的所有jar) -------- Bootstrap(加载$JAVA_HOME/jre/lib/ext/目录下的所有jar) /|\ /|\ | | AppClassLoader(加载应用程序classpath目录下的所有jar和class文件) System(加载$CATALINA_HOME/bin/bootstrap.jar,$CATALINA_BASE/bin/tomcat-juli.jar,$CATALINA_HOME/bin/commons-daemon.jar) /|\ | Common(加载$CATALINA_BASE/lib和$CATALINA_HOME/lib下的class,资源和jar文件) /|\ | WebAppClassLoader(多个平级的,加载 WebApp/WEB-INF/classes,WebApp/WEB-INF/lib)
WebAppClassLoade默认不使用“双亲委派机制”(根据 Servlet 规范 2.4 版第 9.7.2 Web 应用程序类加载器第 9.7.2 节中的建议),查找class和资源的顺序如下:
- Bootstrap classes of your JVM
- /WEB-INF/classes of your web application
- /WEB-INF/lib/*.jar of your web application
- System class loader classes
- Common class loader classes
如果 Web 应用程序类加载器配置为 <Loader delegate="true"/>
,则顺序变为双亲委派的顺序
第二段:建议应用类加载器优先加载 Web 应用自己定义的类
Tomcat 实现细节
异步 Servlet