[Java] Servlet工作原理之一:体系结构及其容器

时间:2022-09-13 01:03:53

一、Servlet体系结构

在 servlet-api.jar (2.5) 中有两个包:javax.servlet 和 javax.servlet.http

[Java] Servlet工作原理之一:体系结构及其容器           [Java] Servlet工作原理之一:体系结构及其容器

1 Servlet、GenericServlet及HttpServlet

Servlet 是一个接口,其方法如下:

  • public void init(ServletConfig config);
  • public void service(ServletRequest req, ServletResponse res);
  • public void destroy();
  • public String getServletInfo(); // 返回servlet的信息,如作者、版本和版权
  • public ServletConfig getServletConfig(); // 获取servlet配置属性对象

GenericServlet 实现了 Servlet接口,是一个通用的、不特定于任何协议的 Servlet

  • public void init()
  • public void init(ServletConfig config)
  • public abstract void service(ServletRequest req, ServletResponse res)
  • public void destroy()
  • public void log(String msg) // 将消息写入日志,利用ServletContext的方法写入
  • public void log(String message, Throwable t)
  • public String getInitParameter(String name) // 获取初始化参数,利用ServletConfig的方法获取
  • public Enumeration getInitParameterNames()
  • public String getServletName()
  • public String getServletInfo() // 返回servlet的信息,如作者、版本和版权
  • public ServletConfig getServletConfig()
  • public ServletContext getServletContext()

HttpServlet 继承于 GenericServlet,针对于 HTTP 协议的类

  • public void service(ServletRequest req, ServletResponse res)
  • protected void service(HttpServletRequest req, HttpServletResponse resp)
  • protected void doGet(...)、doPost(...)、doHead(...)、doPut(...)、doDelete(...)、doOptions(...)、doTrace(...)
  • protected long getLastModified(HttpServletRequest req) // 最后修改时间,应该重写实现这个方法

2 ServletConfig、ServletContext

ServletConfig 对象储存了 Servlet 的一些配置属性,在 Servlet 执行 init() 方法时传入,其方法如下:

  • public String getServletName(); // 获取Servlet名称
  • public String getInitParameter(String name); // 获取初始化参数值
  • public Enumeration getInitParameterNames(); // 获取所有参数名称
  • public ServletContext getServletContext();

另外在 ServletConfig 中还有一个 ServletContext,它定义了有关 Servlet 容器的方法,其方法如下:

  • public String getContextPath(); // 返回web项目的路径
  • public String getRealPath(String path);
  • public URL getResource(String path); // 返回webapp下的文件路径对应的URL
  • public Set getResourcePaths(String path); // 返回path路径下的目录或文件
  • public InputStream getResourceAsStream(String path); // 返回path路径的资源
  • public ServletContext getContext(String uripath);
  • public RequestDispatcher getRequestDispatcher(String path);
  • public RequestDispatcher getNamedDispatcher(String name);
  • public String getMimeType(String file); // 返回指定文件的类型,如 text/html、image/gif
  • public String getServerInfo(); // 返回Servlet容器的名称和版本
  • public String getServletContextName(); // 返回这个web应用程序名称
  • public String getInitParameter(String name);
  • public Enumeration getInitParameterNames();
  • public Enumeration getAttributeNames(); // 返回Servlet容器的所有属性
  • public Object getAttribute(String name); // 返回Servlet容器的指定属性
  • public void setAttribute(String name, Object object);
  • public void removeAttribute(String name);
  • public void log(String msg); // 将消息写入到日志文件中
  • public void log(String message, Throwable throwable);
  • public int getMajorVersion(); // 返回这个容器支持的Servlet主版本,如2.5返回2
  • public int getMinorVersion(); // 返回这个容器支持的Servlet小版本,如2.5返回5

3 ServletRequest、ServletResponse

当请求达到时,容器将 ServletRequest 和 ServletResponse 传递给 Servlet。

ServletRequest 接口的方法如下:

  • public Enumeration getAttributeNames();
  • public Object getAttribute(String name);
  • public void setAttribute(String name, Object o);
  • public void removeAttribute(String name);
  • public void setCharacterEncoding(String env); // 设置请求体的编码类型,读取参数前使用
  • public String getCharacterEncoding(); // 获取请求体的编码类型
  • public String getContentType(); // 请求体的类型
  • public int getContentLength(); // 请求体的长度,长度未知则返回-1
  • public ServletInputStream getInputStream(); // 请求体的字节流
  • public BufferedReader getReader(); // 请求体的字符流
  • public String getParameter(String name); // 名为name的参数值
  • public String[] getParameterValues(String name); // 名为name的参数值,是一个数组
  • public Enumeration getParameterNames(); // 所有参数名称
  • public Map getParameterMap(); // 所有参数的名称和值
  • public String getProtocol(); // 请求的协议版本,如 HTTP/1.1
  • public String getScheme(); // 请求的协议方式,如 http https ftp
  • public String getServerName(); // 服务端的主机、服务器名或服务器IP地址
  • public int getServerPort(); // 服务端的端口号
  • public String getRemoteHost(); // 客户端或最终代理的主机名称
  • public String getRemoteAddr(); // 客户端或最终代理的IP地址
  • public int getRemotePort(); // 客户端或最终代理的端口号
  • public String getLocalName();
  • public String getLocalAddr();
  • public int getLocalPort();
  • public Locale getLocale(); // 返回请求头Accept-Language设置的语言环境
  • public Enumeration getLocales();
  • public boolean isSecure(); // 是否使用HTTPS等安全通道进行的请求
  • public RequestDispatcher getRequestDispatcher(String path);

ServletResponse 接口的方法如下:

  • public String getCharacterEncoding(); // 获取响应体的编码类型
  • public void setCharacterEncoding(String charset); // 设置响应体编码类型
  • public String getContentType(); // 获取响应体的类型
  • public void setContentType(String type); // 设置响应体的类型
  • public void setContentLength(int len); // 设置响应体长度
  • public ServletOutputStream getOutputStream(); // 获取响应体的字节流
  • public PrintWriter getWriter(); // 获取响应体的字符流
  • public int getBufferSize(); // 返回实际缓冲大小,不使用缓冲则为0
  • public void setBufferSize(int size); // 设置响应体缓冲大小
  • public void flushBuffer(); // 将缓冲区内容写入到客户端
  • public void resetBuffer(); // 清除缓冲区数据,如果缓冲已经被写入客户端,则抛异常
  • public void reset(); // 清除缓冲区的数据、状态码及响应头,如果已经写入客户端,则抛异常
  • public boolean isCommitted(); // 响应是否已提交
  • public void setLocale(Locale loc);
  • public Locale getLocale();

HttpServletRequest 和 HttpServletResponse 接口分别继承自 ServletRequest 和 ServletResponse,在其基础上

HttpServletRequest 接口增加的方法如下:

  • public String getAuthType();
  • public Cookie[] getCookies();
  • public long getDateHeader(String name);
  • public int getIntHeader(String name);
  • public String getHeader(String name);
  • public Enumeration getHeaders(String name);
  • public Enumeration getHeaderNames();
  • public String getMethod();
  • public String getPathInfo();
  • public String getPathTranslated();
  • public String getContextPath();
  • public String getQueryString();
  • public String getRemoteUser();
  • public boolean isUserInRole(String role);
  • public java.security.Principal getUserPrincipal();
  • public String getRequestedSessionId();
  • public String getRequestURI();
  • public StringBuffer getRequestURL();
  • public String getServletPath();
  • public HttpSession getSession(boolean create);
  • public HttpSession getSession();
  • public boolean isRequestedSessionIdValid();
  • public boolean isRequestedSessionIdFromCookie();
  • public boolean isRequestedSessionIdFromURL();

HttpServletResponse 接口增加的方法如下:

  • public void addCookie(Cookie cookie);
  • public boolean containsHeader(String name);
  • public String encodeURL(String url);
  • public String encodeRedirectURL(String url);
  • public void sendError(int sc, String msg) throws IOException;
  • public void sendError(int sc) throws IOException;
  • public void sendRedirect(String location) throws IOException;
  • public void setDateHeader(String name, long date);
  • public void addDateHeader(String name, long date);
  • public void setHeader(String name, String value);
  • public void addHeader(String name, String value);
  • public void setIntHeader(String name, int value);
  • public void addIntHeader(String name, int value);
  • public void setStatus(int sc);

上述的四个接口分别有一个包装类的实现,利用了装饰者模式。

4 Filter、FilterConfig、FilterChain

Filter 即过滤器,它是 AOP 思想的一种实现(利用回调函数实现的),通过它我们可以实现权限访问控制、过滤敏感词汇、日志记录等等。为什么要使用 Filter 呢?或者说为什么要使用 AOP 的方式去做这个呢?如果我们不使用 Filter 而直接在 Servlet 的 doGet()、doPost() 方法中实现上述功能也是可以的,但是这样导致了代码冗余,所以我们需要把这些公共的代码抽象出来进行封装。像 OOP 的封装方式针对的是对具有上下关系的对象,而像访问控制、日志等功能并不适合这样的封装,它更像是一种左右关系,所以我们要用 AOP 的方式进行封装。

Filter 可以实现在 Servlet 的 service() 调用的前后执行一段代码,从而实现了公共代码的复用。使用 Filter 与 Servlet 相似,首先要自己编写一个类实现 Filter 接口,然后在 web.xml 中配置好直接该 Filter 对应的 URL。Filter 中有一个 doFilter() 方法,其使用方式大致如下

public class FilterTest implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init");
// 获取过滤器的名字
String filterName = filterConfig.getFilterName();
// 获取其初始化参数,在 web.xml 中指定的
String param1 = filterConfig.getInitParameter("name");
String param2 = filterConfig.getInitParameter("like");
// 返回过滤器的所有初始化参数的名字的枚举集合。
Enumeration<String> paramNames = filterConfig.getInitParameterNames();
System.out.println(filterName);
System.out.println(param1);
System.out.println(param2);
while (paramNames.hasMoreElements()) {
String paramName = (String) paramNames.nextElement();
System.out.println(paramName);
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 执行前的操作
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
System.out.println("before");
// 执行service()方法或下一个过滤器方法
chain.doFilter(request, response); //让目标资源执行,放行
// 执行后的操作
System.out.println("after");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}

编写完 Filter 实现类后还要在 web.xml 文件中对其注册和映射

<!-- filter注册 -->
<filter>
<filter-name>FilterTest</filter-name>
<filter-class>com.filter.FilterTest</filter-class>
<init-param>
<param-name>name</param-name>
<param-value>t</param-value>
</init-param>
<init-param>
<param-name>like</param-name>
<param-value>java</param-value>
</init-param>
</filter>
<!-- filter映射 -->
<filter-mapping>
<filter-name>FilterTest</filter-name>
<url-pattern>*.do</url-pattern>
<!-- 指定过滤器所拦截的 Servlet 名称
<servlet-name></servlet-name> -->
<!-- 指定过滤器所拦截的资源被 Servlet 容器调用的方式,
REQUEST:用户直接访问时调用,即不包括通过RequestDispatcher访问的情况
INCLUDE:通过RequestDispatcher的include()方法访问时调用
FORWARD:通过RequestDispatcher的forward()方法访问时调用
ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用
默认REQUEST,并且可以设置多个<dispatcher>
<dispatcher></dispatcher> -->
</filter-mapping>

我们可以编写多个 Filter,组成了一个 Filter 链。执行顺序与它们在 web.xml 文件中配置顺序有关,先配置则先执行。在上述代码中,我们调用了 FilterChain 对象的 doFilter() 方法,此时会先检查 FilterChanin 对象中是否还有下一个 Filter,如果有则继续调用,如果没有则调用 Servlet 的 service() 方法。

4.1 Filter

Filter 的创建和销毁由其容器负责,容器启动的时候创建 Filter 实例对象,并调用 init() 方法完成初始化,Filter 只会实例化一次。

  • public void init(FilterConfig filterConfig); // 初始化并传入Filter的配置对象
  • public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain); // 执行拦截器的内容
  • public void destroy(); // Filter销毁时调用,当这个方法被调用后,容器还会再调用一次 doFilter() 方法

4.2 FilterConfig

  • public String getFilterName();
  • public ServletContext getServletContext();
  • public String getInitParameter(String name);
  • public Enumeration getInitParameterNames();

4.3 FilterChain

Filter 类的核心就是传递 FilterChain 对象,在 Tomcat 中 FilterChain 的实现类是 ApplicationFilterChain,它在 filters 数组中保存了到最终 Servlet 对象的所有 Filter 对象,当执行完所有 Filter 对象后就会执行 Servlet。

  • public void doFilter(ServletRequest request, ServletResponse response);

5 RequestDispatcher

  • public void forward(ServletRequest request, ServletResponse response)
  • public void include(ServletRequest request, ServletResponse response)

6 Listener

Listener 是基于观察者模式设计的,能够方便的从另一个纵向维度控制程序和数据。在 Servlet 中有两类共6中观察者接口,EventListeners 类型的 ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttrbuteListener,还有 LifecycleListeners 类型的 ServletContextListener、HttpSessionListener,如图所示

[Java] Servlet工作原理之一:体系结构及其容器

这些标签的实现类可以配置在 web.xml 的 <listener> 标签中,也可以在程序中动态的添加。如 Spring 的 org.springframework.web.context.ContextLoaderLister 就实现了一个 ServletContextListener,当容器加载时启动 Spring,如下所示

<!-- spring启动监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- spring配置文件,默认查找 WEB-INF 下的 applicationContext.xml 文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>

下面我们看一下各个 Listener 的具体方法:

ServletContextListener

  • public void contextInitialized(ServletContextEvent sce); //在 Context 容器初始时,Filter 和 Servlet 的 init() 前调用
  • public void contextDestroyed(ServletContextEvent sce); //在 Context 容器销毁时,Filter 和 Servlet 的 destroy() 后调用

ServletRequestListener

  • public void requestInitialized(ServletRequestEvent sre); // HttpServeltRequest 传递到 Servlet 前调用
  • public void requestDestroyed(ServletRequestEvent sre);

HttpSessionListener

  • public void sessionCreated(HttpSessionEvent se);
  • public void sessionDestroyed(HttpSessionEvent se);

HttpSessionBindingListener

  • public void valueBound(HttpSessionBindingEvent event); // 对象被放入 session 时调用
  • public void valueUnbound(HttpSessionBindingEvent event); // 对象被移出 session 时调用

ServletContextAttributeListener

  • public void attributeAdded(ServletContextAttributeEvent scab); //调用 servletContext 的 setAttribute() 时触发
  • public void attributeRemoved(ServletContextAttributeEvent scab); //调用 servletContext 的 removeAttribute() 时触发
  • public void attributeReplaced(ServletContextAttributeEvent scab); //调用 servletContext 的 setAttribute() 替换旧值时触发

ServletRequestAttributeListener

  • public void attributeAdded(ServletRequestAttributeEvent srae);
  • public void attributeRemoved(ServletRequestAttributeEvent srae);
  • public void attributeReplaced(ServletRequestAttributeEvent srae);

HttpSessionAttributeListener

  • public void attributeAdded(HttpSessionBindingEvent se);
  • public void attributeRemoved(HttpSessionBindingEvent se);
  • public void attributeReplaced(HttpSessionBindingEvent se);

HttpSessionActivationListener

  • public void sessionWillPassivate(HttpSessionEvent se); // 通知 session 被钝化
  • public void sessionDidActivate(HttpSessionEvent se); // 通知 session 被激活

7 ServletInputStream、ServletOutputStream

8 ServletException

二、Tomcat 组件

Servlet 不能够独立运行,需要在它的容器中运行,容器管理着它创建到销毁的整个过程。在看 Servlet 的生命周期前,我们先看下 Servlet 的我们最熟悉的一个容器——Tomcat。

[Java] Servlet工作原理之一:体系结构及其容器

Tomcat 有两个重要组件:连接器(Connector)和容器(Engine容器及其子容器),我们结合 server.xml 配置文件来看一下这两个组件。

1 连接器(Connector)

首先向 Tomcat 发送的请求可以分为两类:

  • Tomcat 作为应用服务器:请求来自前端的 Web 服务器,如 Nginx、Apache、IIS 等。
  • Tomcat 作为独立服务器:请求来自浏览器。

这些不同的请求需要不同的连接器来接收,在 Service 中有一个引擎和多个连接器,以适应不同情况。常见的连接器有四种:HTTP连接器、SSL连接器、AJP连接器、proxy连接器。在定义连接器时可以配置的属性有很多,连接器公用属性如下:

  • className 指定实现 Connector 接口的类
  • enableLookups 是否通过request.getRemoteHost()获取客户端的主机名,默认true
  • redirectPort 如果连接器的协议是HTTP,当收到HTTPS请求时,转发到此端口

HttpConnector 的属性:

  • className 指定实现 Connector 接口的类
  • port 监听端口,默认8080
  • address 指定监听地址,默认为所有地址
  • bufferSize 设置由端口创建的输入流缓存大小,默认2048byte
  • protocol 连接器使用的协议,默认HTTP/1.1
  • maxThreads 支持的最大并发连接数,默认200
  • connectionTimeout 等待客户端发送请求的超时时间,默认60000,即1分钟
  • acceptCount 设置等待队列的最大长度,默认为10。当tomcat所有处理线程均繁忙时,新链接被放置于等待队列中

JkConnector 的属性:

  • className 指定实现 Connector 接口的类
  • port 设定AJP端口号
  • protocol 必须为 AJP/1.3

2 容器(Engine容器及其子容器)

在 Tomcat 中有 Engine、Host、Context 及 Wrapper 四种容器,它们的包含关系如下图所示

[Java] Servlet工作原理之一:体系结构及其容器

上述的包含并不是继承关系,而是当子容器创建好后会放入到父容器中。Servlet 被包装成 Wrapper,然后真正管理 Servlet 的是 Context 容器,一个 Context 对应一个 Web 应用。

  • Wrapper 封装了具体访问的资源,即 Servlet;
  • Context 封装了各个 Wrapper 资源的集合;
  • Host 封装了 Context 资源的集合;
  • Engine 可以看成是对 Host 的逻辑封装。

我们再来看一下它们的继承关系,这些容器的接口都继承自 Container 接口,为什么要按层次分别封装一个对象呢?为了方便统一管理,在不同层次的配置其作用域是不一样的。

[Java] Servlet工作原理之一:体系结构及其容器

2.1 Engine

Engine 下面拥有多个 Host,即虚拟主机,它的责任就是将用户的请求分配给一个虚拟主机处理。为什么要使用虚拟主机呢?当我们有两个应用时,如下图的 Love 应用和 SDJTU 应用。我们想访问“倪培.我爱你”域名时直接达到 Love 应用,访问“www.sdjtu.net.cn”域名时直接到达 SDJTU 应用,但是如果不设置虚拟主机是无法在一个 Tomcat中做到的。那么,我们可以设置两个虚拟主机,并指定请求到达这个虚拟主机后要去访问的目录。

[Java] Servlet工作原理之一:体系结构及其容器

在 Engine 标签中有几个属性可以填写

  • name 定义 Engine 的名字
  • className 指定实现 Engine 接口的类,默认是 StandardEngine
  • defaultHost 指定处理请求的默认主机

在 Engine 标签里还可以包含以下几个元素

  • Logger
  • Realm
  • Valve
  • Host

2.2 Host

Host 代表一个虚拟主机,在它下面有多个 Context,一个 Context 代表一个 Web 应用。

[Java] Servlet工作原理之一:体系结构及其容器

在 Host 标签中的几个属性

  • name 定义 Host 的名字
  • className 指定实现 Host 接口的类,默认是 StandardHost
  • appBase 指定虚拟主机的目录,默认是 webapps
  • unpackWARs 是否先展开war文件再运行。如果为 false 将直接运行 war 文件
  • autoDeploy 表示是否支持热部署
  • alias 用来指定主机别名
  • deployOnStartup 是否在启动时自动发布目录下的所有Web应用

在 Host 标签中还可以包含以下几个元素

  • Logger
  • Realm
  • Valve
  • Host

2.3 Context

Context 代表运行在虚拟主机上的单个 Web 应用。

[Java] Servlet工作原理之一:体系结构及其容器

在 Context 标签中的几个属性

  • className 指定现实 Context 接口的类,默认是 StandardContext 类
  • path 配置Web应用对应的URL,即跟在域名后面的内容
  • docBase 指定要执行的Web应用
  • reloadable 当项目下的 class 文件被更新时,是否重新加载Web应用
  • cookies 指定是否通过 Cookies 来支持 Session,默认为 true
  • useNaming 指定是否支持 JNDI,默认值为 ture

在 Context 标签中的元素

  • Logger
  • Realm
  • Resource
  • ResourceParams

2 Tomcat 启动过程

Tomcat 从 7.0 开始增加了一个启动类 org.apache.catalina.startup.Tomcat。通过这个类的实例调用 start() 方法就可以启动 Tomcat,还可以通过这个对象增加和修改 Tomcat 的配置参数,来动态的添加 Context、Servlet 等。

Tomcat 的启动是基于观察者模式设计的,所有的容器都继承了 Lifecycle 接口,由它来管理容器的生命周期,所有容器的修改和状态改变都会由它去通知已经注册的观察者(Listener)。

当 Context 容器初始化状态为 init 时,添加到 Context 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口,它是在调用了 Tomcat.addWebapp 时被加入到 StandardContext 容器的,这个类将会负责整个 Web 应用的配置解析工作。ContextConfig 的 init 方法将会主要完成以下工作:

  1. 创建 ContextDigester 对象来解析 XML 配置文件
  2. 读取默认的 context.xml 配置文件,如果存在则解析它
  3. 读取默认的 Host 配置文件,如果存在则解析它
  4. 读取默认的 Context 自身的配置文件,如果存在则解析它
  5. 设置 Context 的 DocBase

当 ContextConfig 的 init 方法完成后,Context 容器会执行 startInternal 方法,主要包括以下工作

  1. 创建读取资源文件的对象
  2. 创建 ClassLoader 对象
  3. 设置应用的工作目录
  4. 启动相关的辅助类,如 logger、realm、resources 等
  5. 修改启动状态,通知感兴趣的观察者
  6. 子容器的初始化
  7. 获取 servletContext 并设置必要参数
  8. 初始化“load on startup”的 Servlet

Web 应用的初始化是在 ContextConfig 的 configureStart 方法中实现的,应用初始化主要是解析 web.xml 文件。web.xml 文件中的配置会被解析成 WebXml 对象,然后这些配置会放入 Context 中,并且 Servlet 配置会被包装成 StandardWrapper 并作为子容器添加到 Context 中。

三、Servlet 生命周期

前面我们知道 Servlet 由 Tomcat 解析,并被包装成 Wrapper 添加在 Context 容器中,下面就要进行 Servlet 的实例化。

1 创建实例

创建 Servlet 实例的方法是从 StandardWrapper 的 loadServlet() 方法开始的。loadServlet() 方法获取了 servletClass,然后将它交给了 InstanceManager 去创建一个基于 ServletClass.class 的对象。

Servlet 并不是单例的,但一般只会有一个实例,即一个<servlet>标签对应一个实例。另外如果 Servlet 没有配置<servlet-mapping>标签,则无法通过请求时创建,只能配置 load-on-startup 使其在容器启动时便创建。

2 初始化

初始化 Servlet 是在 StandardWrapper 对象的 initServlet() 方法中,这个方法会去调用 Servlet 的 init() 方法,同时把 StandardWrapperFacade 对象作为 ServletConfig 传递进去。

3 处理请求

客户端发出 Http 请求,Tomcat 接收到请求后将信息封装进了 HttpRequest 对象,接着创建一个 HttpResponse 对象,然后调用 HttpServlet 对象的 service() 方法,把 HttpRequest 对象与 HttpRespnse 对象传入进去。当执行完 service() 方法后,Tomcat 把响应传递给客户端。

4 销毁