純粹是閑的,在慕課網看了幾集的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,看起來沒什么營養就不看了。