一、基本概念

Servlet是运行在Web服务器上的小程序,通过http协议和客户端进行交互。
这里的客户端一般为浏览器,发送http请求(request)给服务器(如Tomcat)。服务器接收到请求后选择相应的Servlet进行处理,并给出响应(response)。

 
servlet

从这里可以看出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

public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L; public TestServlet() {
} protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().append("Served at: ").append(request.getContextPath());
} protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}

web.xml

<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.nantang.servlet.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>

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

 
测试结果

这里需要扩展的有几点:
1.如果一个servlet需要映射多个url-pattern,那么就在<servlet-mapping></servlet-mapping>标签下写多个<url-pattern></url-pattern>,如:

<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/test1</url-pattern>
<url-pattern>/test2</url-pattern>
</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的继承层级(类图中的方法并没有一一列举,因为下面会逐一解释):

 
Servlet源码

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

1 ServletContext

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

2.ServletConfig

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

 
Servlet和ServletContext的关系
public interface ServletConfig {

    public String getServletName();

    public ServletContext getServletContext();

    public String getInitParameter(String name);

    public Enumeration getInitParameterNames();
}

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>对。

<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.nantang.servlet.TestServlet</servlet-class>
<init-param>
<param-name>a</param-name>
<param-value>1</param-value>
</init-param>
<init-param>
<param-name>b</param-name>
<param-value>2</param-value>
</init-param>
</servlet>

3 Servlet

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

public interface Servlet {

    public void init(ServletConfig config) throws ServletException;

    public ServletConfig getServletConfig();

    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

    public String getServletInfo();

    public void destroy();
}

所有的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相关信息的,需要根据业务逻辑进行实现和调用。

 
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接口,只需要集成GenericServlet。GenericServlet实现了Servlet和ServletConfig接口。

4.1 GenericServlet对Servlet接口的实现

private transient ServletConfig config;

public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
} public void init() throws ServletException {} public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public void destroy() {} public ServletConfig getServletConfig() {
return config;
} public String getServletInfo() {
return "";
}

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

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

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

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

4.2 GenericServlet对于ServletConfig接口的实现

public String getServletName() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletName();
} public ServletContext getServletContext() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletContext();
} public String getInitParameter(String name) {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getInitParameter(name);
} public Enumeration getInitParameterNames() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getInitParameterNames();
}

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

5 HttpServlet

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

 
分派
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}

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

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}

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

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

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

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

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

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

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

protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
} protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_post_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}

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

四、总结

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

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

JavaWeb--Servlet 详解的更多相关文章

  1. JavaWeb Cookie详解

    代码地址如下:http://www.demodashi.com/demo/12713.html Cookie的由来 首先我们需要介绍一下,在Web开发过程中为什么会引入Cookie.我们知道Http协 ...

  2. Servlet详解

    原文出处:http://blog.csdn.net/q547550831/article/details/50458456 Servlet详解 基本概述 Session在计算机中,尤其是在网络应用中, ...

  3. Java Servlet详解(体系结构+注解配置+生命周期)

    Java Servlet详解(注解配置+生命周期) 什么是Servlet : (Server applet)? 顾名思义:服务端的小程序 Servlet只是一个接口,定义了Java被浏览器访问到(To ...

  4. JavaWeb配置详解(结合框架SpringMVC)

    详解 先说一说常识性的东西,我们的JavaWeb程序运行一开始走的是web.xml文件,这是我们的核心文件,可以说没有web.xml文件我们就无法运行项目,这个文件长什么样子,读者自己新建一个web项 ...

  5. Java Web(一) Servlet详解!!

    这篇文章到上一篇,距离的有点遥远呀,隔了大概有两个月把,中间在家过了个年,哈哈~ 现在重新开始拾起,最近在看一本个人觉得很棒的书,<Java Web 整合开发王者归来>,现在写的这一系列基 ...

  6. (转)Java Web(一) Servlet详解!!

    https://www.cnblogs.com/whgk/p/6399262.html 这篇文章到上一篇,距离的有点遥远呀,隔了大概有两个月把,中间在家过了个年,哈哈~ 现在重新开始拾起,最近在看一本 ...

  7. JavaWeb Session详解

    代码地址如下:http://www.demodashi.com/demo/12756.html 记得把这几点描述好咯:代码实现过程 + 项目文件结构截图 + ## Session的由来 上一篇博文介绍 ...

  8. web开发(一)-Servlet详解

    在网上看见一篇不错的文章,写的详细. 以下内容引用那篇博文.转载于<http://www.cnblogs.com/whgk/p/6399262.html>,在此仅供学习参考之用. 一.什么 ...

  9. JavaWeb笔记一、Servlet 详解

    一.创建一个 Servlet 简单实现类 1.创建一个 HelloServlet 类(测试 Servlet 接口方法) 1 //创建一个 HelloServlet 类并实现 Servlet 接口 2 ...

  10. Mac下Intellij IDea发布JavaWeb项目 详解三 (为所有Module配置Tomcat Deployment 并测试web 网页 配置Servlet)

    step4 为所有项目配置Deployment 4.1 如图 4.2 [+][Artifact] 4.3 将这里列出的所有内容选中后,点[OK] 4.4 选完是这样,表示,这三个java ee 项目会 ...

随机推荐

  1. python网络爬虫(12)去哪网酒店信息爬取

    目的意义 爬取某地的酒店价格信息,示例使用selenium在Firefox中的使用. 来源 少部分来源于书.python爬虫开发与项目实战 构造 本次使用简易的方案,模拟浏览器访问,然后输入字段,查找 ...

  2. 第二章 单表查询 T-SQL语言基础(3)

    单表查询(3) 2.6 处理字符数据 字符数据的查询处理,内容包括:类型,排序规则,运算符和函数,以及模式匹配. 2.6.1 数据类型 SQL Server支持两种字符数据类型----普通字符和Uni ...

  3. Django框架——进阶之AJAX

    <script>$("#b1").on("click", function () { // 点击 id是b1的按钮要做的事儿 var i1 = $( ...

  4. 红色警戒2CE修改教程

    在大学的时候特别喜欢玩游戏,尤其偏爱单机游戏.在玩一些单机游戏的时候,特意使用了一些修改工具.本来是打算做成一个系列的,但是现在由于时间问题,仅介绍一些.(大概包括rimworld,饥荒,放逐之城,缺 ...

  5. simhash算法:海量千万级的数据去重

    simhash算法:海量千万级的数据去重 simhash算法及原理参考: 简单易懂讲解simhash算法 hash 哈希:https://blog.csdn.net/le_le_name/articl ...

  6. Spring Cloud,Docker

    Spring Cloud 先决条件 Spring cloud 基于spring boot,spring,java Spring Cloud解决的问题 分布式微服务架构和微服务监控.注册于发现.跟踪等一 ...

  7. Django 数据库模块 单独使用

    pip install django pip install psycopg2 pip install mysqlclient Entity.py from django.db import mode ...

  8. 为Qtcreator 编译的程序添加管理员权限

    (1)创建资源文件 myapp.rc 1 24 uac.manifest (2)创建文件uac.manifest <?xml version="1.0" encoding=& ...

  9. 小米Air安装Arch Linux之图形界面配置(Gnome 和 sway)持续更新中……

    0. 前言 上一篇文章简单讲述了在小米Air上安装Arch Linux的经验,但是安装完后基本系统后,还需要额外的配置才能进到日常使用.下文简单列举一些步骤. 1. 参考网站 主要还是参考ARCH W ...

  10. 全面解析Java日期时间API

    时区 GMT(Greenwich Mean Time):格林尼治时间,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间. UTC(Universal Time ...