[Java] Servlet工作原理之一:体系结构及其容器
一、Servlet体系结构
在 servlet-api.jar (2.5) 中有两个包:javax.servlet 和 javax.servlet.http
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,如图所示
这些标签的实现类可以配置在 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。
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 四种容器,它们的包含关系如下图所示
上述的包含并不是继承关系,而是当子容器创建好后会放入到父容器中。Servlet 被包装成 Wrapper,然后真正管理 Servlet 的是 Context 容器,一个 Context 对应一个 Web 应用。
- Wrapper 封装了具体访问的资源,即 Servlet;
- Context 封装了各个 Wrapper 资源的集合;
- Host 封装了 Context 资源的集合;
- Engine 可以看成是对 Host 的逻辑封装。
我们再来看一下它们的继承关系,这些容器的接口都继承自 Container 接口,为什么要按层次分别封装一个对象呢?为了方便统一管理,在不同层次的配置其作用域是不一样的。
2.1 Engine
Engine 下面拥有多个 Host,即虚拟主机,它的责任就是将用户的请求分配给一个虚拟主机处理。为什么要使用虚拟主机呢?当我们有两个应用时,如下图的 Love 应用和 SDJTU 应用。我们想访问“倪培.我爱你”域名时直接达到 Love 应用,访问“www.sdjtu.net.cn”域名时直接到达 SDJTU 应用,但是如果不设置虚拟主机是无法在一个 Tomcat中做到的。那么,我们可以设置两个虚拟主机,并指定请求到达这个虚拟主机后要去访问的目录。
在 Engine 标签中有几个属性可以填写
- name 定义 Engine 的名字
- className 指定实现 Engine 接口的类,默认是 StandardEngine
- defaultHost 指定处理请求的默认主机
在 Engine 标签里还可以包含以下几个元素
- Logger
- Realm
- Valve
- Host
2.2 Host
Host 代表一个虚拟主机,在它下面有多个 Context,一个 Context 代表一个 Web 应用。
在 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 应用。
在 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 方法将会主要完成以下工作:
- 创建 ContextDigester 对象来解析 XML 配置文件
- 读取默认的 context.xml 配置文件,如果存在则解析它
- 读取默认的 Host 配置文件,如果存在则解析它
- 读取默认的 Context 自身的配置文件,如果存在则解析它
- 设置 Context 的 DocBase
当 ContextConfig 的 init 方法完成后,Context 容器会执行 startInternal 方法,主要包括以下工作
- 创建读取资源文件的对象
- 创建 ClassLoader 对象
- 设置应用的工作目录
- 启动相关的辅助类,如 logger、realm、resources 等
- 修改启动状态,通知感兴趣的观察者
- 子容器的初始化
- 获取 servletContext 并设置必要参数
- 初始化“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 销毁