一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

【图解】JavaWeb:Servlet 教程

时间:2015-09-07 编辑:简简单单 来源:一聚教程网

一、Web服务器

从事web开发的人,会很清楚一个东西叫 Web服务器,比如J2EE开发―Tomcat,Jetty,.NET开发―IIS等。HTTP服务器是使用 HTTP(超文本传输协议) 与客户机浏览器进行信息交流。下面就是HTTP服务器简单交互图:

1_thumb3

HTTP服务器是Web服务器的一种,也是开发最常见的,自然还有其他方式进行信息交互,比如FTP文件服务器…

    Web服务器是可以向发出请求的浏览器提供文档的程序。其核心过程为

        连接过程 ― 请求过程 ― 应答过程 ― 关闭连接

这让我想到了Tomcat架构的一张图:

image001



二、Tomcat 简单说几句

如图,Tomcat 包含了核心服务模块:Connector连接模块 和 Container 容器。Tomcat Server 核心是一个 Servlet/JSP Container。对每一个HTTP请求,过程如下

    ― 获取连接
    ― Servlet来分析请求(HttpServletRequest)
    ― 调用其service方法,进行业务处理
    ― 产生相应的响应(HttpServletResponse)
    ― 关闭连接

如图:
Tomcat相关

蓝色线指向过程是请求,绿色线指向过程是响应过程。也就是上面Web服务器核心过程:“连接过程 ― 请求过程 ― 应答过程 ― 关闭连接”


三、我第一个Servlet

什么是Servlet?(每次都会不停的问自己,这是什么“What”?紧接着应该是什么用“How”吧)

在 JavaEE 6文档中,介绍如下

    “Servlet 是运行在Web服务器的Java小程序。Servlet可以获取并针对Web客户端的请求作出响应。一般情况下,通过HTTP,即超文本传输协议,进行传输通信。”

    A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web clients, usually across HTTP, the HyperText Transfer Protocol.

所以,Servlet 是Web服务器核心工作的抽象。它不单单只是实现HttpServlet,可能实现有FtpServlet(这个我猜的)等。相对较多的Web开发,知道的肯定是HttpServlet。

补充,在Servlet规范是这样写道的:

    Serlvet是基于Java技术的Web组件,容器托管的,用于生产动态内容。它也是基于平台无关的Java类格式,被编译为平台无关的字节码,可以被基于Java技术的web server动态加载并运行。这里容器,有时候也称为servlet 引擎。

在 JavaEE 6文档中,是这样介绍HttpServlet:

    “HttpServlet 提供了一个能被继承后创建一个适应Web网站的Http Servlet的抽象类。”

    Provides an abstract class to be subclassed to create an HTTP servlet suitable for a Web site.

光说不练假把式,练一个“Hello,Servlet/JSP World!”:
   

import java.io.IOException;
import java.io.PrintWriter;
 
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*
 * Copyright [2015] [Jeff Lee]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
/**
 * @author Jeff Lee
 * @since 2015-6-25 19:46:45
 *  HelloWrold案例
 */
@WebServlet(urlPatterns = "/helloWorld.html")
public class HelloWorldServletT extends HttpServlet{
     
    private static final long serialVersionUID = 1L;
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException{
        // 获取输出打印对象
        PrintWriter out = resp.getWriter();
        out.println("Hello,Servlet/JSP World!");
    }
}


右键该HelloWorldServletT.java文件 ― Run As ― Run On Server ― 选择Tomcat服务器 ― Finish即可

image

image

等待片刻,你可看到网页上如下输出。这就是客户端从HttpServlet获取到的响应:
image
三、分析源码

@WebServlet(urlPatterns = "/helloWorld.html")

@WebServlet 注解用于声明一个HttpServlet的配置。其中,urlPatters = “/helloWorld.html”,urlPatterns复数形式,说明至少一个URL必须被申明。它和另一个value必须存在一个,但不能同时存在。如果要匹配多个URL路径的话,如下:

@WebServlet(urlPatterns = { "/helloWorld01.html", "/helloWorld02.html" }

下面有个@Override,重写了父类HttpServlet的doGet方法。我们先看看父类HttpServlet。HttpServlet是一个抽象类,它提供了以下方法:

    ― doGet  , 服务于 HTPP GET 请求
    ― doPost , 服务于 HTTP POST 请求
    ― doPut  , 服务于 HTTP PUT 请求
    ― doDelete,服务于 HTTP DELETE 请求
    …

如图:

image

对于不同的请求,HttpServlet的子类必须相应的实现至少一个方法,通常来说,会是其中一个,这样代码比较清晰。那父类的doGet方法做了什么工作呢?

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);
        }
    }


这里就简单的获取了下HTTP协议及Http Local信息,然后可以协议是否是1.1,做出分别是405或者400HTTP状态码的响应。

回到HelloWorldServletT.java 这里:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    // 获取输出打印对象
    PrintWriter out = resp.getWriter();
    out.println("Hello,Servlet/JSP World!");
}


表示该HelloWorldServletT会接受Http GET请求,并OOM到HttpServletRequest,并执行里面的逻辑代码和返回响应。 这里从HttpServletResponse对象中获取到输出打印对象PrintWriter,然后输出了“Hello,Servlet/JSP World!”。

完毕!哦还有一点补充补充补充:

    print,这里还好一句话。如果打印个table会很麻烦,因此有一个JSP的东西出现了,是Servlet的HTML化身。

五、深入Servlet 具体过程

又回到这个简单的 Get Servlet代码:
   

public class HelloWorldServletT extends HttpServlet{
     
    private static final long serialVersionUID = 1L;
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException{
        // 获取输出打印对象
        PrintWriter out = resp.getWriter();
        out.println("Hello,Servlet/JSP World!");
    }
}


这过程总结如下:

    ― 从浏览器(Client)获取连接”/helloWorld.html”
    ― Tomcat Connector模块将请求(Request)传递给 Container模块
    ― Container 模块会做以下事情
    ―― 分析HTPP请求信息,组装成HttpServletRequest对象
    ―― 创建新的HttpServletResponse对象
    ―― 根据路由配置,搜索相应的Servlet,并创建一个线程用于处理本次请求。此时线程会将上面Request和Response对象的索引,传递给Servlet

    ― 新线程中的Servlet处理逻辑

    ― 线程结束后,通过HttpServletResponse对象的PrintWriter,返回浏览器一个信息

过程图如下:

Tomcat相关

蓝色线指向过程是请求,绿色线指向过程是响应过程,橙色线指向过程是内部处理过程。

有些面试题会这样问:

    Servlet是线程安全的吗?

    不是,一个servlet实现类只会有一个实例对象,多个线程是可能会访问同一个servlet实例对象的,线程安全问题都是由全局变量及静态变量引起的。

因此,Servlet对象实例化是在以第一次请求此Servlet时,如果访问后,实例对象存在内存中,只会在服务器停止时,它才会消失。它不会随着各个线程结束而结束。因此下次访问Servlet时,Servlet Container会搜索相应的Servlet,如果不存在,Container新建相应的Servlet。这也是我们想要的结果。

再来个恶心的面试题:

    Servlet是单例吗?

    不一定是,在一个ServeltName情况下是的。在多个ServletName匹配到一个Servlet类时,该Servlet不是单例。

六、小结

发现这一博客写的太多,回头一看。可以写成三个文章了。言归正传本文要点如下

    1、简单介绍Web服务器 及 Tomcat容器
    2、第一个Sevlet的开发及使用
    3、深入源码及api介绍使用
    4、总结一次请求及响应的真实过程

    

JavaWeb:Servlet 延续

一、回到 HttpServlet 的 service方法

Servlet 基础接口定义了用于客户端请求处理的service方法。 当请求到达Servlet容器,由Servlet容器路由到一个Servlet实例。

比如说 javax.servlet.http.HttpServlet 类 ,其中有一个 protected void service 方法如下:
   

private static final String METHOD_DELETE = "DELETE";
private static final String METHOD_HEAD = "HEAD";
private static final String METHOD_GET = "GET";
private static final String METHOD_OPTIONS = "OPTIONS";
private static final String METHOD_POST = "POST";
private static final String METHOD_PUT = "PUT";
private static final String METHOD_TRACE = "TRACE";
 
private static final String HEADER_IFMODSINCE = "If-Modified-Since";
 
private static final String LSTRING_FILE =
        "javax.servlet.http.LocalStrings";
private static ResourceBundle lStrings =
        ResourceBundle.getBundle(LSTRING_FILE);
/**
 * HTTP状态码304
 */
public static final int SC_NOT_MODIFIED = 304;
 
/**
 * 接收来自 public service方法的标准HTTP请求,
 * 并将它们分发给此类中定义的doXXX方法。
 */
protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    // 获取请求方法名
    String method = req.getMethod();
    // 如果是GET请求
    if (method.equals(METHOD_GET)) {
        // 上一次修改HttpServletRequest对象的时间
        long lastModified = getLastModified(req);
        // 没有改变
        if (lastModified == -1) {
            doGet(req, resp);
        } else {
            long ifModifiedSince;
            try {
                // 获取请求头中服务器修改时间
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // 获取无效
                ifModifiedSince = -1;
            }
            // 如果请求头服务器修改时间迟
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                // 设置修改HttpServletResponse对象的时间,重新设置浏览器的参数
                              //maybeSetLastModified(resp, lastModified);
                // 调用doGet方法
                                doGet(req, resp);
            } else {
                // 304 HTTP状态码
                resp.setStatus(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);
        // 501 HTTP状态码
        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}


代码逻辑详解如下:

1、HttpServlet的 protected void service方法 用于接受 public service接收的标准HTTP请求。

也就是说,HttpServlet 重写了父类 GenericServlet 的 service方法。如图显示的是该方法,将从容器获取的 ServletRequest 和 ServletResponse 对象强制转化成 用于HTTP处理的 HttpServletRequest 和 HttpServletResponse 对象。然后将两个对象路由给了 HttpServlet的 protected void service方法(图中代码选中处)

image

2、然后根据请求的方法名,分发到此类定义的doXXX方法。如果没有被请求到的话,则返回501 HTTP 状态码。

这样子仿佛明白了什么,也就是说,如果你在 HelloServlet中重写了doGet方法,这里分发到就是HttpServlet的子类HelloServlet的doGet方法。

哦~ 还有,501 HTTP 状态码 ― 未实现(Not implemented)表示服务器不支持实现请求所需要的功能。例如,客户发出了一个服务器不支持的PUT请求。原来如此,所谓死记硬背这些HTTP 状态码有什么用?这样的记忆才是最有效的。

    休息休息,小广告插一下 :(维持生计,O(∩_∩)O~)

    涉及到的代码都会在开源项目 servlet-core-learning 。简介 ― Servlet/JSP学习积累的例子,是Java EE初学者及Servlet/JSP核心技术巩固的最佳实践

大致就是这两步骤。这就是service的工作流程:

    1、接受 public service接收的标准HTTP请求。

    2、分发到定义的doXXX方法

二、GET 请求的处理详解

上面对于GET请求代码处理如下:
   

// 如果是GET请求
if (method.equals(METHOD_GET)) {
    // 上一次修改HttpServletRequest对象的时间
    long lastModified = getLastModified(req);
    // 没有改变
    if (lastModified == -1) {
        doGet(req, resp);
    } else {
        long ifModifiedSince;
        try {
            // 获取请求头中服务器修改时间
            ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
        } catch (IllegalArgumentException iae) {
            // 获取无效
            ifModifiedSince = -1;
        }
        // 如果请求头服务器修改时间迟
        if (ifModifiedSince < (lastModified / 1000 * 1000)) {
            // 设置修改HttpServletResponse对象的时间,重新设置浏览器的参数
          //maybeSetLastModified(resp, lastModified);
            // 调用doGet方法
            doGet(req, resp);
        } else {
            // 304 HTTP状态码
            resp.setStatus(SC_NOT_MODIFIED);
        }
    }
}


这里,

1、定义了 getLastModified(req) 方法。用于获取上一次修改HttpServletRequest对象的时间。如果lastModified为默认的 ?1L,则总是刷新。

    这个getLastModified,是HttpServlet定义了用于支持有条件GET操作。即当客户端通过GET请求获取资源时,当资源自第一次获取那个实际点发生更改后才再次发生数据,否则将使用客户端缓存的数据。

    在一些适当的场合,实现此方法可以更有效的利用网络资源,减少不必要的数据发送。

2、如果getLastModified方法的返回值是一个正数,那就要分以下两种情况考虑:

    (1)如果请求头没有包含If-Modified-Since头字段(应该是第一次访问资源时候) 或者 其getLastModified返回值比If-Modified-Since头字段指定时间新,则调用doGet返回生成 response 和 设置Last-Modified 消息头。

    (2)如果其getLastModified返回值比If-Modified-Since头字段指定时间旧,则返回一个304状态给客户端,表示让客户端继续使用以前缓存的页面。

比如说 304 ,第一次访问 百度 首页时,有些资源会成功获取 返回200。再次F5,有些资源或直接调用客户端的缓存数据,则返回304。

image1_thumb



三、Servlet线程问题

Servlet容器可以并发路由多个请求到 Servlet 的 service方法。为了处理这些请求,Servlet必须在并发及线程安全问题做好处理。上一篇的《 Servlet必会必知 》提到定义全局变量会造成线程安全问题。在开发Servlet时,考虑线程安全问题提出了一下解决:

    1、实现 SingleThreadModel 接口

        Servlet2.4 已经提出不提倡使用。实现此接口,Servlet容器为每个新的请求创建一个单独的Servlet实例。这会有严重性能问题。

    2、同步锁

        使用synchronized关键字,虽然可以保证只有一个线程可以访问被保护区段,已达到保证线程安全。但是系统性能及并发量大大降低。不可取~

    3、避免使用实例变量,即Servlet中全局变量。使用局部变量 (推荐)

        方法中的局部变量分配在栈空间,每个线程有私有的栈空间。因此访问是线程安全的。

我想到了以下一个问题:

    既然Sevlet的全局变量是线程不安全的,那SpringMVC Controller 也一样。那我们在Controller定义个 XXXService 变量会不会造成线程安全呢?


    答:因为这是Spring的一个Service Bean,是线程安全的,所以可以作为单例使用,不会造成线程安全。

热门栏目