一、基本概念

Servlet是运行在Web服务器上的小程序,通过http协议和客户端进行交互。

这里的客户端一般为浏览器,发送http请求(request)给服务器(如Tomcat)。服务器接收到请求后选择相应的Servlet进行处理,并给出响应(response)。

 

从这里可以看出Servlet并不是独立运行的程序,而是以服务器为宿主,由服务器进行调度的。通常我们把能够运行Servlet的服务器称作Servlet容器,如Tomcat。

这里Tomcat为什么能够根据客户端的请求去选择相应的Servlet去执行的呢?答案是:Servlet规范。因为Servlet和Servlet容器都是遵照Servlet规范去开发的。简单点说:我们要写一个Servlet,就需要直接或间接实现javax.servlet.Servlet。并且在web.xml中进行相应的配置。Tomcat在接收到客户端的请求时,会根据web.xml里面的配置去加载、初始化对应的Servlet实例。这个就是规范,就是双方约定好的。

二、样例分析

在进一步解释Servlet原理、分析源码之前,我们先介绍下如何在JavaWeb中使用Servlet。方法很简单:1.编写自己的Servlet类,这里可以使用开发工具(STS、Myeclipse等)根据向导快速的生成一个Servlet类。2.在web.xml中配置servlet。这里的知识很简单,所以不做过多赘述。直接上代码。(这里需要注意的是,servlet3.0之后提供了注解WebServlet的方式配置servlet,这里就不做介绍了,感兴趣的可以自行去百度,只是配置的形式不同而已,没有本质区别。所以下文还是为web.xml为例)

TestServlet.java

 1 public class TestServlet extends HttpServlet {
 2     private static final long serialVersionUID = 1L;
 3 
 4     public TestServlet() {
 5     }
 6 
 7     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 8         response.getWriter().append("Served at: ").append(request.getContextPath());
 9     }
10 
11     protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
12         doGet(request, response);
13     }
14 }

web.xml

1 <servlet>
2     <servlet-name>TestServlet</servlet-name>
3     <servlet-class>com.nantang.servlet.TestServlet</servlet-class>
4 </servlet>
5 <servlet-mapping>
6     <servlet-name>TestServlet</servlet-name>
7     <url-pattern>/test</url-pattern>
8 </servlet-mapping>

启动Tomcat,浏览器访问/test。将会访问TestServlet。返回客户端请求的上下文路径。

这里需要扩展的有几点:

1.如果一个servlet需要映射多个url-pattern,那么就在<servlet-mapping></servlet-mapping>标签下写多个<url-pattern></url-pattern>,如:

1 <servlet-mapping>
2     <servlet-name>TestServlet</servlet-name>
3     <url-pattern>/test1</url-pattern>
4     <url-pattern>/test2</url-pattern>
5 </servlet-mapping>

2.对于不同的servlet,不允许出现相同的url-pattern。

3.如果不同的servlet,它们的url-patter存在包含关系,那么容器会调用更具象的servlet去处理客户端请求。比如有两个servlet,servlet1的url-pattern是"/*",servlet2的url-pattern是"/test"。那么这个时候如果客户端调用的url是http://localhost:8080/demo/test,容器会使用servlet2去处理客户端的请求。虽然说"/*""/test"都匹配客户请求的url,但是容器会选择更贴切的。这里不会出现多个servlet处理同一个请求的现象。

三、源码分析

上面说过,我们自己编写的Servlet类都必须直接或间接实现javax.servlet.Servlet。可是上面的例子TestServlet继承的是HttpServlet,那是因为HttpServlet间接的实现了javax.servlet.Servlet。下面是HttpServlet的继承层级(类图中的方法并没有一一列举,因为下面会逐一解释):

下面我们由上往下层层分析:

1 ServletContext

一个web应用对应一个ServletContext实例,关于ServletContext的详细介绍,可以参考另一篇博文ServletContext

2.ServletConfig

ServletConfig实例是由servlet容器构造的,当需要初始化servlet的时候,容器根据web.xml中的配置以及运行时环境构造出ServletConfig实例,并通过回调servlet的init方法传递给servlet(这个方法后面会讲到)。所以一个servlet实例对应一个ServletConfig实例

 

 1 public interface ServletConfig {
 2 
 3     public String getServletName();
 4 
 5     public ServletContext getServletContext();
 6 
 7     public String getInitParameter(String name);
 8 
 9     public Enumeration getInitParameterNames();
10 }

2.1 getServletName

getServletName方法返回servlet实例的名称,这个就是我们在web.xml中<servlet-name>标签中配置的名字,当然也可以在服务器控制台去配置。如果这两个地方都没有配置servlet名称,那么将会返回servlet的类名。

2.2 getServletContext

getServletContext方法返回ServletContext实例,也就是我们上面说的应用上下文。

2.3 getInitParameter和getInitParameterNames

这两个方法是用来获取servlet的初始化参数的,这个参数是在web.xml里面配置的(如下所示)。getInitParameter是根据参数名获取参数值,getInitParameterNames获取参数名集合。

这里需要注意的是当需要配置多个初始化参数时,应该写多个<init-param></init-param>对,而不是在一个<init-param></init-param>对里面写多个<param-name></param-name>和<param-value></param-value>对。

 1 <servlet>
 2     <servlet-name>TestServlet</servlet-name>
 3     <servlet-class>com.nantang.servlet.TestServlet</servlet-class>
 4     <init-param>
 5         <param-name>a</param-name>
 6         <param-value>1</param-value>
 7     </init-param>
 8     <init-param>
 9         <param-name>b</param-name>
10         <param-value>2</param-value>
11     </init-param>
12 </servlet>

3 Servlet

最原始最简单的JaveWeb模型,就是一个servlet容器上运行着若干个servlet用来处理客户端的请求。所以说servlet是JavaWeb最核心的东西,我们的业务逻辑基本上都是通过servlet实现的(虽然现在有各种框架,不用去直接编写servlet,但本质上还是在使用servlet)。

 1 public interface Servlet {
 2     
 3     public void init(ServletConfig config) throws ServletException;
 4 
 5     public ServletConfig getServletConfig();
 6 
 7     public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
 8 
 9     public String getServletInfo();
10 
11     public void destroy();
12 }

所有的servlet都是javax.servlet.Servlet的子类,就像Java里面所有的类都是Object的子类一样。Servlet类规定了每个servlet应该实现的方法,这个是遵循Servlet规范的。但是自定义的servlet一般不用直接实现Servlet,而是继承javax.servlet.GenericServlet或者javax.servlet.http.HttpServlet就行了。我们上面的TestServlet就是继承HttpServlet,这是因为HttpServlet间接实现了Servlet,提供了通用的功能。所以我们在自定义的TestServlet里面只需要专注实现业务逻辑就行了。

Servlet里面有三个比较重要的方法:init、service、destroy。它们被称作是servlet生命周期的方法,它们都是由servlet容器调用。另外两个方法用于获取servlet相关信息的,需要根据业务逻辑进行实现和调用。

3.1 init

init方法是servlet的初始化方法,当客户端第一次请求servlet的时候,JVM对servlet类进行加载和实例化。(如果需要容器启动时就初始化servlet,可以在web.xml配置<load-on-startup>1</load-on-startup>)

这里需要注意的是,servlet会先执行默认的构造函数,然后回调servlet实例的init方法,传入ServletConfig参数。这个参数上面说过,是servlet容器根据web.xml中的配置和运行时环境构造的实例。通过init方法注入到servlet。init方法在servlet的生命周期中只会被调用一次,在客户端的后续请求中将不会再调用。

3.2 service

service方法是处理业务逻辑的核心方法。当servlet容器接收到客户端的请求后,会根据web.xml中配置的<url-pattern>找到相应的servlet,回调service方法处理客户端的请求并给出响应。

3.3 destroy

JDK文档解释这个方法说:这个方法会在所有的线程的service()方法执行完成或者超时后执行。这里只是说明了,当servlet容器要去调用destroy方式的时候,需要等待一会,等待所有线程都执行完或者达到超时的限制。

这里并没有说清楚什么情况下servlet容器会触发这个动作。How Tomcat Works一书中对这个做了解释:当servlet容器关闭或需要更多内存的时候,会销毁servlet。这个方法就使得servlet容器拥有回收资源的能力。

同样地,destroy方法在servlet的生命周期中只会被调用一次。

3.4 getServletConfig

这个方法返回ServletConfig实例,这个对象即为servlet容器回调init方法的时候传入的实例。所以自定义的Servlet一般的实现方式为:在init方法里面把传入的ServletConfig存储到servlet的属性字段。在getServletConfig的实现里返回该实例。这个在后续解释javax.servlet.GenericServlet的源码时,能够看到。

3.5 getServletInfo

返回关于servlet的信息,这个由自定义的servlet自行实现,不过一般建议返回servlet的作者、版本号、版权等信息。

4.GenericServlet

GenericServlet从名字就能看的出来是servlet的一般实现,实现了servlet具有的通用功能,所以我们自定义的servlet一般不需要直接实现Servlet接口,只需要集成GenericServletGenericServlet实现了Servlet和ServletConfig接口。

4.1 GenericServlet对Servlet接口的实现

 1 private transient ServletConfig config;
 2 
 3 public void init(ServletConfig config) throws ServletException {
 4     this.config = config;
 5     this.init();
 6 }
 7 
 8 public void init() throws ServletException {}
 9 
10 public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
11 
12 public void destroy() {}
13 
14 public ServletConfig getServletConfig() {
15     return config;
16 }
17 
18 public String getServletInfo() {
19     return "";
20 }

可以说,GenericServlet对Servlet方法的实现逻辑非常简单。就是把一些必要的逻辑写了下。

1.init方法就是把容器传入的ServletConfig实力存储在类的私有属性conifg里面,然后调用一个init无参的空方法。这么做的意义在于,我们如果想在自定义的servlet类里面在初始化的时候添加些业务逻辑,只需要重写无参的init方法就好了,我们不需要关注ServletConfig实例的存储细节了。

2.service和destroy方法并未实现具体逻辑。

3.getServletConfig就是返回init方法里面存储的config。getServletInfo就是返回空字符串,如果有业务需要,可以在子类里面重写。

4.2 GenericServlet对于ServletConfig接口的实现

 1 public String getServletName() {
 2     ServletConfig sc = getServletConfig();
 3     if (sc == null) {
 4         throw new IllegalStateException(
 5             lStrings.getString("err.servlet_config_not_initialized"));
 6     }
 7     return sc.getServletName();
 8 }
 9 
10 public ServletContext getServletContext() {
11     ServletConfig sc = getServletConfig();
12     if (sc == null) {
13         throw new IllegalStateException(
14             lStrings.getString("err.servlet_config_not_initialized"));
15     }
16     return sc.getServletContext();
17 }
18 
19 public String getInitParameter(String name) {
20     ServletConfig sc = getServletConfig();
21     if (sc == null) {
22         throw new IllegalStateException(
23             lStrings.getString("err.servlet_config_not_initialized"));
24     }
25     return sc.getInitParameter(name);
26 }
27 
28 public Enumeration getInitParameterNames() {
29     ServletConfig sc = getServletConfig();
30     if (sc == null) {
31         throw new IllegalStateException(
32             lStrings.getString("err.servlet_config_not_initialized"));
33     }
34     return sc.getInitParameterNames();
35 }

这四个方法的实现就跟一个模子刻出来的一样,都是取得ServletConfig实例,然后调用相应的方法。其实GenericServlet完全没有必要实现ServletConfig,这么做仅仅是为了方便。当我们集成GenericServlet写自己的servlet的时候,如果需要获取servlet的配置信息如初始化参数,就不需要写形如:“ServletConfig sc = getServletConfig();if (sc == null) ...;return sc.getInitParameterNames();”这些冗余代码了。除此之外,没有别的意义。

5 HttpServlet

HttpServlet是一个针对HTTP协议的通用实现,它实现了HTTP协议中的基本方法get、post等,通过重写service方法实现方法的分派。

 1 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{
 2     HttpServletRequest    request;
 3     HttpServletResponse    response;
 4     try {
 5         request = (HttpServletRequest) req;
 6         response = (HttpServletResponse) res;
 7     } catch (ClassCastException e) {
 8         throw new ServletException("non-HTTP request or response");
 9     }
10     service(request, response);
11 }

重写的service方法将参数转换成HttpServletRequestHttpServletResponse,并调用自己的另一个重载service方法。

 1 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
 2     String method = req.getMethod();
 3     if (method.equals(METHOD_GET)) {
 4         long lastModified = getLastModified(req);
 5         if (lastModified == -1) {
 6             doGet(req, resp);
 7         } else {
 8             long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
 9             if (ifModifiedSince < (lastModified / 1000 * 1000)) {
10                 maybeSetLastModified(resp, lastModified);
11                 doGet(req, resp);
12             } else {
13                 resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
14             }
15         }
16     } else if (method.equals(METHOD_HEAD)) {
17         long lastModified = getLastModified(req);
18         maybeSetLastModified(resp, lastModified);
19         doHead(req, resp);
20     } else if (method.equals(METHOD_POST)) {
21         doPost(req, resp);        
22     } else if (method.equals(METHOD_PUT)) {
23         doPut(req, resp);            
24     } else if (method.equals(METHOD_DELETE)) {
25         doDelete(req, resp);        
26     } else if (method.equals(METHOD_OPTIONS)) {
27         doOptions(req,resp);        
28     } else if (method.equals(METHOD_TRACE)) {
29         doTrace(req,resp);        
30     } else {
31         String errMsg = lStrings.getString("http.method_not_implemented");
32         Object[] errArgs = new Object[1];
33         errArgs[0] = method;
34         errMsg = MessageFormat.format(errMsg, errArgs);        
35         resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
36     }
37 }

这个方法的的逻辑也很简单,就是解析出客户端的request是哪种中方法,如果是get方法则调用doGet,如果是post则调用doPost等等。这样我们在继承HttpServlet的时候就无需重写service方法,我们可以根据自己的业务重写相应的方法。一般情况下我们的应用基本就是get和post调用。那么我们只需要重写doGet和doPost就行了。

这里需要注意的是3-15行代码,这里对资源(比如页面)的修改时间进行验证,判断客户端是否是第一次请求该资源,或者该资源是否被修改过。如果这两个条件有一个被满足那么就调用doGet方法。否则返回状态304(HttpServletResponse.SC_NOT_MODIFIED),这个状态就是告诉客户端(浏览器),可以只用自己上一次对该资源的缓存。

不过HttpServlet对于判断资源修改时间的逻辑非常简单粗暴:

1 protected long getLastModified(HttpServletRequest req) {
2     return -1;
3 }

方法始终返回-1,这样就会导致每次都会调用doGet方法从服务器取资源而不会使用浏览器的本地缓存。所以如果我们自己的servlet要使用浏览器的缓存,降低服务器的压力,就需要重写getLastModified方法。

最后我们来看一下HttpServlet对http一些方法的实现,在所有的方法中,HttpServlet已经对doOptions和doTrace方法实现了通用的逻辑,所以我们一般不用重写这两个方法,感兴趣的可以自己去看下源码。

这里我们列举下最常用的两个方法doGet和doPost:

 1 protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException{
 2     String protocol = req.getProtocol();
 3     String msg = lStrings.getString("http.method_get_not_supported");
 4     if (protocol.endsWith("1.1")) {
 5         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
 6     } else {
 7         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
 8     }
 9 }
10     
11 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
12     String protocol = req.getProtocol();
13     String msg = lStrings.getString("http.method_post_not_supported");
14     if (protocol.endsWith("1.1")) {
15         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
16     } else {
17         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
18     }
19 }

其实这两个实现里面也没做什么有用的逻辑,所以一般情况下都要重写这两个方法,就像我们最初的TestServlet那样。doHead、doPut、doDelete也是这样的代码模板,所以如果有业务需要的话,我们都要重写对应的方法。

四、总结

讲了这么多,可以这么说:Servlet是JavaWeb里面最核心的组件。只有对它完全融会贯通,才能去进一步去理解上层框架Struts、Spring等。

另外需要明确的是:一个Web应用对应一个ServletContext,一个Servlet对应一个ServletConfig。每个Servlet都是单例的,所以需要自己处理好并发的场景。

 

作者:南唐三少
出处:http://www.cnblogs.com/nantang
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我们最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文链接,否则保留追究法律责任的权利。