淺析Java源碼之HttpServlet


  純粹是閑的,在慕課網看了幾集的Servlet入門,剛寫了1個小demo,就想看看源碼,好在也不難

  主要是介紹一下里面的主要方法,真的沒什么內容啊~

  源碼來源於apache-tomcat-7.0.52,servlet-api.jar包

 

繼承樹

  首先來看一下HttpServlet類的繼承關系:

  // javax.servlet.http
  public abstract class HttpServlet extends GenericServlet implements java.io.Serializable {
      //...
  }

  // javax.servlet
  public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {
      //...
  }

  先不看HttpServlet本身,它的父類是GenericServlet,該類主要是對Servlet、ServletConfig兩個接口中的部分方法做了簡單實現,並沒有多少東西。

  這里列舉一下ServletConfig與Servlet接口中的方法:

ServletConfig

  public interface ServletConfig {
      // 獲取servlet名字
      public String getServletName();
      // 獲取servlet上下文
      public ServletContext getServletContext();
      // 獲取初始化參數列表
      public String getInitParameter(String name);
      // 獲取初始化參數名
      public Enumeration getInitParameterNames();
  }

Servlet

    public interface Servlet {
        // 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接口中可以簡單看到Servlet的生命周期:constructor、init、service、destroy =>構造、 初始化、處理請求、銷毀。

  值得注意的是,在GenericServlet中,init、destroy方法都未實現:

    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }
    public void destroy() {
    }

  也就是在實際運行中,會根據定義方法來進行初始化與銷毀。

  接下來就看看HttpServlet本身,這里就不一個一個過,挑一些方法來看:

 

1、為什么繼承類需要重寫doGet/doPost

  在看視頻的時候,講課老師提到了我們需要override這兩個方法,看了源碼就明白原因了:

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 獲取請求協議 => HTTP/1.1
        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);
        }
    }

  其余諸如doPost、doPut方法類似,這里就不貼出來了。

  未重寫的方法只是簡單的獲取了請求協議,並根據協議返回一個錯誤提示信息,所以所有繼承的方法都有必要重寫對應的響應方法。

 

2、通用方法service

  在請求處理中,內置了一個通用的方法,名為service。

    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 獲取請求方式
        String method = req.getMethod();
        // 開始匹配響應方法
        if (method.equals(METHOD_GET)) {
            // 獲取請求里lastModified值 默認為-1
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // 未處理緩存相關
                // 直接響應
                doGet(req, resp);
            } else {
                // 獲取請求頭中的If-Modified-Since值
                // private static final String HEADER_IFMODSINCE = "If-Modified-Since";
                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_POST)) {
            // doPost
        }
        // 其余請求方法處理

        // 報錯
        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);
        }
    }

  這個方法總的來說只有兩部:

1、獲取的方法,get?post?

2、匹配對應的響應處理方法,doGet or doPost

  這里唯有get響應有一些復雜,主要原因在於所有頁面的請求默認是get請求,這涉及到協商緩存問題,詳細的可以自己去網上查。

  中間有一個maybeSetLastModified是一個檢測方法,判斷響應頭中是否有設置Last-Modified,如下:

    private void maybeSetLastModified(HttpServletResponse resp, long lastModified) {
        // 如果已有直接返回
        if (resp.containsHeader(HEADER_LASTMOD))
            return;
        // 大於0說明有做處理 設置響應頭的Last-Modified
        if (lastModified >= 0)
            resp.setDateHeader(HEADER_LASTMOD, lastModified);
    }

  這個比較簡單,就不解釋了。

  另外,注意到上面的service方法權限是protected,其實還有看起來一樣的public版本提供了外部訪問途徑,參數不太一樣:

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

  看一下就行了。

 

3、doTrace

  類中還內置了一個特殊方法,可以詳細展示了請求的頭部信息。

    protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int responseLength;
        // 換行
        String CRLF = "\r\n";
        // 地址 + 協議名
        String responseString = "TRACE " + req.getRequestURI() + " " + req.getProtocol();
        // 獲取所有請求頭
        Enumeration reqHeaderEnum = req.getHeaderNames();
        // 遍歷拼接key: value
        while (reqHeaderEnum.hasMoreElements()) {
            String headerName = (String) reqHeaderEnum.nextElement();
            responseString += CRLF + headerName + ": " + req.getHeader(headerName);
        }
        responseString += CRLF;
        responseLength = responseString.length();
        // 這個響應類型查都查不到
        // 表現形式為下載一個文件 內容為拼接的字符串
        resp.setContentType("message/http");
        resp.setContentLength(responseLength);
        // 內置的輸出流 與PrintWriter類似
        ServletOutputStream out = resp.getOutputStream();
        out.print(responseString);
        out.close();
        return;
    }

  這個方法調用后,就不能繼續用視頻里的out.print輸出內容了,如果在doGet中調用此方法,例如:

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doTrace(req,resp);
    }

  將會下載一個名為servlet的文件:

  里面的內容如下:

    TRACE /Myservlet/MyServlet/servlet HTTP/1.1
    host: localhost:8080
    connection: keep-alive
    cache-control: max-age=0
    upgrade-insecure-requests: 1
    user-agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36
    accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
    referer: http://localhost:8080/Myservlet/
    accept-encoding: gzip, deflate, br
    accept-language: zh-CN,zh;q=0.9
    cookie: JSESSIONID=46E569152C4D155D266B790E09604F30

  很明顯,就是請求頭的鍵值對打印信息。

 

4、響應頭Allow

  有一個方法專門設置Allow的響應頭,該字段表明可以處理的請求方式。

  不過在此之前,需要看一下getAllDeclaredMethods方法,該方法獲取繼承鏈(除了根類javax.servlet.http.HttpServlet)上所有類方法:

    private static Method[] getAllDeclaredMethods(Class c) {
        // 該類為終點
        if (c.equals(javax.servlet.http.HttpServlet.class)) {
            return null;
        }
        // 遞歸獲取父類方法
        Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass());
        // 通過反射獲取本類中的方法
        Method[] thisMethods = c.getDeclaredMethods();
        // 如果父類存在方法 拷貝到數組中
        if ((parentMethods != null) && (parentMethods.length > 0)) {
            Method[] allMethods = new Method[parentMethods.length + thisMethods.length];
            System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length);
            System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length);

            thisMethods = allMethods;
        }
        return thisMethods;
    }

  該方法通過反射機制,獲取到本類向上直到HttpServlet類中間的所有方法,用一個Method數組保存起來。

  接下來就可以看這個doOptions是如何設置這個頭信息的:

    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 獲取methods
        Method[] methods = getAllDeclaredMethods(this.getClass());
        // 假設請求方法均為false
        // trace、options默認為true
        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;
        // 遍歷methods
        // 如果存在對應的方法名 說明有重寫方法處理對應的請求
        // getName方法獲取對應的字符串
        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            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;
        }
        // 進行字符串拼接
        String allow = null;
        if (ALLOW_GET)
            if (allow == null)
                allow = METHOD_GET;
        // 很多if
        // ...
        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;
        // 設置頭
        resp.setHeader("Allow", allow);
    }
}

  很簡單,遍歷methods,有對應的方法,就將對應的控制變量設為true,最后進行拼接,設置響應頭Allow。

  測試代碼如下:

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        this.doOptions(req,resp);
        out.println("123");
    }

  打開網頁,查看Network中的Headers,可以看到:

 

  基本上講完了,里面還有兩個內部類:NoBodyResponse、NoBodyOutputStream,看起來沒什么營養就不看了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM