深入剖析Spring Web源码(五) - DispatcherServlet的实现 - 通用Servlet和HTTP Servlet
2013-04-18 18:00 阅读(215)


1.1.1   通用 Servlet 和 HTTP Servlet

HTTP( Hyper Text Transfer Protocol)是超文本传输协议的缩写,它用于传送 WWW方式的数据,关于HTTP协议的详细内容请参考 RFC2616。 HTTP协议采用了请求 /响应模型。客户端向服务器发送一个请求,请求头包含请求的方法、 URI、协议版本、以及包含请求修饰符、客户信息和内容的类似于 MIME的消息结构。服务器以一个状态行作为响应,相应的内容包括消息协议的版本,成功或者错误编码加上包含服务器信息、实体元信息以及可能的实体内容。

 

HTTP协议支持各种类型的方法,其中包括, GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE。

下图是 Servlet规范的接口和实现类的继承结构,每个类的方法包括实体方法,抽象方法或者占位符方法。抽象方法和占位符方法由子类实现。

 

 

图表 4‑2

 

从上图我们可以看出, Servlet接口定义了 3个重要的接口方法, init()方法是在 Sevlet初始化的时候调用的,提供给 Servlet组件进行初始化自己的机会。与此相对应, detroy()方法是在 Servlet析构时候调用的,提供给Servlet组件进行释放使用过的资源的机会。而 sevice()方法是用来处理每一个 Web容器传递进来的请求与响应的。

 

通用 Servlet实现了 Servlet的接口方法 init(), 方法中保存了 Servlet容器传递过来的 ServletConfig对象。如下图程序片段所示,

public  void  init(ServletConfig config) throws  ServletException {  
    // 保存 Servlet 配置对象,对于处理一个 HTTP 请求的许多操作都需要的 Servlet 配置所包含的信息,例如 ,Servlet 名字, Servlet 配置的参数等等  
     this .config = config;  
   
    // 代理到另外一个无参数的 init 方法,这个方法是一个抽象方法,它是一个占位符,提供给子类重写并初始化的机会  
     this .init();  
}  

通用 Servlet的方法 service()是一个显示定义的抽象方法,要求实现类必须重写这个方法的实现。因为不同的Servlet实现会依赖不同的协议,实现各不相同。 D

 

destroy()是一个方法占位符,子类可以有选择的实现进而进行资源的清理。

 

HTTP Servlet正如我们所愿,实现了通用 Servlet的 service()方法,根据 HTTP请求中所标识的方法,把 HTTP请求派遣到不同的处理方法中。如下图所示,

 

 

图表 4‑3

 

这些不同的方法有不同的实现,这些处理方法中的大部分是占位符,但是,它为 doOptions()和 doTrace()提供了具体实现,因为对于不同的 HTTP Servlet组件,这两个方法的行为基本是不变的。他们都是用于返回服务器信息和调试目的。如下图代码注释,

public  void  service(ServletRequest req, ServletResponse res)  
    throws ServletException, IOException  
{  
    HttpServletRequest  request;  
    HttpServletResponse response;  
   
    try {  
        // 既然是 HTTP 协议绑定的 Serlvet, 强制转换到 HTTP 的领域模型  
        request = (HttpServletRequest) req;  
        response = (HttpServletResponse) res;  
} catch (ClassCastException e) {  
       // 如果传入的 HTTP 请求和 HTTP 响应不是 HTTP 的领域模型,则抛出 Servlet 异常,这个异常会被 Servlet 容器所处理  
        throw new ServletException( "non-HTTP request or response" );  
    }  
   
    // 如果传入的请求和响应是预期的 HTTP 请求和 HTTP 响应,则调用 service() 方法。  
    service(request, response);  
}  
   
protected void service(HttpServletRequest req, HttpServletResponse resp)  
    throws ServletException, IOException  
{  
    // 从 HTTP 请求中取得这次请求所使用的 HTTT 方法  
    String method = req.getMethod();  
   
    if (method.equals(METHOD_GET)) {  
       // 如果这次请求使用 GET 方法  
   
        // 取得这个 Servlet 的最后修改的时间  
        long lastModified = getLastModified(req);  
        if (lastModified == -1) {  
            //-1 代表这个 Servlet 不支持最后修改操作,直接调用 doGet() 进行处理 HTTP GET 请求  
            doGet(req, resp);  
        } else {  
            // 如果这个 Servlet 支持最后修改操作,取得请求头中包含的请求的最后修改时间  
            long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);  
   
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {  
            // 如果请求头中包含的修改时间早于这个 Servlet 的最后修改时间,说明这个 Servlet 自从客户上一次 HTTP 请求已经被修改了 , 设置最新修改时间到响应头中  
              maybeSetLastModified(resp, lastModified);  
               
              // 调用 doGet 进行进行处理 HTTP GET 请求  
              doGet(req, resp);  
             } else {  
                // 如果请求头中包含修改时间晚于这个 Servlet 的最后修改时间,说明这个 Servlet 自从请求的最后修改时间后没有更改过,这种情况下,仅仅返回一个 HTTP 响应状态 SC_NOT_MODIFIED  
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);  
             }  
        }  
   
    } else if (method.equals(METHOD_HEAD)) {  
        // 如果这次请求使用 POST 方法  
   
        // 如果这个 Servlet 支持最后修改操作,则设置这个 Servlet 的最后修改时间到响应头中  
        long lastModified = getLastModified(req);  
        maybeSetLastModified(resp, lastModified);  
   
       // 和对 HTTP GET 方法处理不同的是,无论请求头中的修改时间是不是早于这个 Sevlet 的最后修改时间,都会发 HEAD 响应给客户,因为 HTTP HEAD 响应是用来查询 Servlet 头信息的操作  
        doHead(req, resp);  
   
    } else if (method.equals(METHOD_POST)) {  
        // 如果这次请求使用 POST 方法  
        doPost(req, resp);  
   
    } else if (method.equals(METHOD_PUT)) {  
        // 如果这次请求使用 PUT 方法  
        doPut(req, resp);    
   
    } else if (method.equals(METHOD_DELETE)) {  
        // 如果这次请求使用 DELETE 方法  
        doDelete(req, resp);  
   
    } else if (method.equals(METHOD_OPTIONS)) {  
        // 如果这次请求使用 OPTIONS 方法  
        doOptions(req,resp);  
   
    } else if (method.equals(METHOD_TRACE)) {  
        // 如果这次请求使用 TRACE 方法  
        doTrace(req,resp);  
   
    } else {  
       // 如果这次请求是其他未知方法,返回错误代码 SC_NOT_IMPLEMENTED 给 HTTP 响应,并且显示一个错误消息,说明这个操作是没有实现的  
        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);  
    }  
}   

从上面两个方法的实现中我们可以看到, HTTP Servlet根据不同的 HTTP方法进行了 HTTP请求的分发。这样,不同方法的请求会使用不同的处理方法进行处理。事实上 doGet() doPost(), doPut(), doDelete()都是占位符实现,子类应该有选择的重写这些方法来实现真正的服务逻辑。 Spring Web MVC就是通过重写这些方法,开始控制流的实现的。

 

下面是 doGet()方法的代码注释。

protected  void  doGet(HttpServletRequest req, HttpServletResponse resp)  
    throws ServletException, IOException  
{  
    // 取得请求头包含的 HTTP 协议版本  
    String protocol = req.getProtocol();  
   
    // 直接发送错误消息,可见,一个子类需要重写这些占位符方法 doGet(), doPost(), doPut(), doDelete() 中的一个或者多个  
    String msg = lStrings.getString( "http.method_get_not_supported" );  
    if (protocol.endsWith( "1.1" )) {  
        // 如果是 HTTP 1.1, 发送 SC_METHOD_NOT_ALLOWED  
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);  
    } else {  
        // 如果是 HTTP 的更早版本则发送 SC_BAD_REQUEST  
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);  
    }  
} 

doHead()方法的实现通过对 HTTP响应类进行包装,实现了 NoBodyReponse类,这个类忽略了对 HTTP响应体的输出。重用了 doGet()方法的实现,并且保留了 HTTP头信息的输出。所以,如果一个子类 Servlet重写了doGet()方法,这个方法 doHead()是不需要重写的。代码注释如下,

protected  void  doHead(HttpServletRequest req, HttpServletResponse resp)  
    throws ServletException, IOException  
{  
    // 构造一个特殊的响应类,这个类内部忽略了所有的响应体的输出  
    NoBodyResponse response = new NoBodyResponse(resp);  
     
    // 重用 doGet() 处理器罗杰  
    doGet(req, response);  
     
    // 设置响应体的字节大小,尽管响应体并没有输出,但是客户端可能关系这个信息  
    response.setContentLength();     
}   

doPost(), doPut(), doDelete()方法的实现和 doGet()方法的实现是类似的,他们都是一个占位符的实现,子类Servlet需要有选择的进行重写进而实现真正需要的 HTTP服务。 

然而, doOptions()和 doTrace()对任何 Servlet的实现,基本是不变的,他们是用来查询服务器信息和调试所用,他们的实现如下,

// 这个方法 doOptions() 设置支持的 HTTP 方法名称到相应头中  
protected void doOptions(HttpServletRequest req, HttpServletResponse resp)  
throws ServletException, IOException  
{  
    // 取得当前这个 Servlet 以及父类 Servlet 声明的所有方法,这些方法不包括本类 HTTP Servlet 所生命的方法  
    Method[] methods = getAllDeclaredMethods( this .getClass());  
     
    // 初始化的时候,假设它不支持任何 HTTP 方法  
    boolean ALLOW_GET = false ;  
    boolean ALLOW_HEAD = false ;  
    boolean ALLOW_POST = false ;  
    boolean ALLOW_PUT = false ;  
    boolean ALLOW_DELETE = false ;  
    boolean ALLOW_TRACE = true ;  
    boolean ALLOW_OPTIONS = true ;  
     
    // 根据子类 Servlet 是否重写了 HTTP Servlet 的占位符方法,判断是否这个 Servlet 实现支持这种 HTTP 方法,例如,如果子类 Servlet 实现了 doGet(), 然后 HTTP GET 方法是支持的  
    for ( int i=0; i<methods.length; i++) {  
        // 遍历得到的所有生命的方法  
        Method m = methods[i];  
         
        // 如果名字是 doGet(), doPost(),  doPut() 或者 doDelete(), 它支持相应的方法  
        if (m.getName().equals( "doGet" )) {  
            ALLOW_GET = true ;  
            ALLOW_HEAD = true ;  
        }  
        if (m.getName().equals( "doPost" ))  
            ALLOW_POST = true ;  
        if (m.getName().equals( "doPut" ))  
            ALLOW_PUT = true ;  
        if (m.getName().equals( "doDelete" ))  
            ALLOW_DELETE = true ;  
         
    }  
     
     // 把支持的 HTTP 方法名称拼接成逗号分割的字符串,例如, “GET, POST”, “GET, POST, PUT, DELETE”  
    String allow = null ;  
    if (ALLOW_GET)  
        if (allow== null ) allow=METHOD_GET;  
    if (ALLOW_HEAD)  
        if (allow== null ) allow=METHOD_HEAD;  
        else allow += ", " + METHOD_HEAD;  
    if (ALLOW_POST)  
        if (allow== null ) allow=METHOD_POST;  
        else allow += ", " + METHOD_POST;  
    if (ALLOW_PUT)  
        if (allow== null ) allow=METHOD_PUT;  
        else allow += ", " + METHOD_PUT;  
    if (ALLOW_DELETE)  
        if (allow== null ) allow=METHOD_DELETE;  
        else allow += ", " + METHOD_DELETE;  
    if (ALLOW_TRACE)  
        if (allow== null ) allow=METHOD_TRACE;  
        else allow += ", " + METHOD_TRACE;  
    if (ALLOW_OPTIONS)  
        if (allow== null ) allow=METHOD_OPTIONS;  
         else allow += ", " + METHOD_OPTIONS;  
     
    // 把支持的方法拼接成的字符串设置到 HTTP 协议的相应头中,这个值的 key 是 "Allow"  
    resp.setHeader( "Allow" , allow);  
}  
   
// 这个方法返回一个字符串到 HTTP 响应体里面,这个字符串包含请求 URL, 版本信息以及请求的头信息,主要是用来调试  
protected void doTrace(HttpServletRequest req, HttpServletResponse resp)  
    throws ServletException, IOException  
{  
     
    int responseLength;  
     
   
    // 连接 URI 字符串和协议版本信息字符串  
    String CRLF = "/r/n" ;  
    String responseString = "TRACE " + req.getRequestURI()+  
        " " + req.getProtocol();  
     
    Enumeration reqHeaderEnum = req.getHeaderNames();  
    // 遍历所有的请求头信息  
    while ( reqHeaderEnum.hasMoreElements() ) {  
        String headerName = (String)reqHeaderEnum.nextElement();  
         
        // 拼接所有的请求头到字符串中,并且使用:分割名值对,每对头信息之间使用回车换行进行分隔  
        responseString += CRLF + headerName + ": " +  
        req.getHeader(headerName);  
    }  
     
    // 附着回车换行符到字符串结尾  
    responseString += CRLF;  
     
    // 取得字符串字节长度信息  
    responseLength = responseString.length();  
     
    // 设置响应类型为 message/http  
    resp.setContentType( "message/http" );  
     
    // 设置响应体的长度  
    resp.setContentLength(responseLength);  
   
    // 输出字符串消息到响应中  
    ServletOutputStream out = resp.getOutputStream();  
    out.print(responseString);  
     
    // 关闭相应流,结束操作  
    out.close();  
    return ;  
}   

从上面的代码注释中,我们可以看到 Servlet规范中的 HTTP Servlet的实现只是一个占位符实现,并不包含完全的服务实现,一些服务的实现是由子类 Servlet完成的。 Spring Web MVC就是通过实现这些占位符方法来派遣HTTP请求到 Spring Web MVC的控制器组件方法的。

 

下面一节我们将深入剖析 Spring Web MVC 的控制器是如何进行派遣和处理 HTTP 请求的。


转自:http://blog.csdn.net/robertleepeak/article/details/5891549







返回顶部