Servlet 是 Java Web 開發的起點,幾乎所有的 Java Web 框架都是基於 Servlet 的封裝,其中最主要的就是 Servlet 和 Filter 介面。我重新學習了一遍 Servlet,對 Java Web 開發有了更深的理解。
1. Servlet 是什麼
從 API 可以看出,Servlet 本質是一套介面(Interface)。那麼介面的本質是什麼?是規範、是協議,所以我們常說要面向介面程式設計,而不是面向實現。介面是連線 Servlet 和 Servlet 容器(Tomcat、Jetty 等)的關鍵。
Servlet 介面定義了一套處理網路請求的規範,所有實現 Servlet 的類都需要實現它的五個方法,其中最主要的是兩個生命週期方法 init 和 destroy,還有一個處理請求的 service 方法。也就是說,所有實現 Servlet 介面的類,或者說,所有想要處理網路請求的類,都需要回答這三個問題:
- 你初始化時要做什麼
- 你銷燬時要做什麼
- 你接收到請求時要做什麼
這是 Java EE 給的一種規範!就像阿西莫夫的機器人三大定律一樣,是規範!
看一下 Servlet 的介面定義,即 Servlet 和 Servlet 容器的規範。我們最關心的就是 service 方法,在這裡處理請求。
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
複製程式碼
2. Servlet 如何工作
Servlet 想要工作離不開 Servlet 容器,比如我們最常用的 Tomcat。它監聽了某個埠,http 請求過來後,容器根據 url 等資訊,確定要將請求交給哪個 Servlet 去處理,然後呼叫 Servlet 的 service 方法,service 方法返回一個 response 物件,容器將 respose 物件解析之後封裝成一個 http 響應返回客戶端。
3. Servlet 體系結構
Servlet 規範類有這麼幾個:
- Servlet
- ServletContext
- ServletConfig
- ServletRequest
- ServletResponse
Servlet 執行模式是典型的「握手型」互動。舉個例子:
買早點的場景。找到一家早點鋪(SerletContext 開始),看到牌面上寫著可以加肉鬆(ServletConfig),就告訴老闆我要加肉鬆的煎餅果子,拿出手機掃碼支付了五塊錢(ServletRequest)。老闆嫻熟地給我攤好,然後遞給我(ServletResponse),我就美滋滋地離開了(ServletContext 結束)。
引用開源中國紅薯的一段話,原文 在這裡。
為什麼我這麼強調 HttpServletRequest 和 HttpServletResponse 這兩個介面,因為 Web 開發是離不開 HTTP 協議的,而 Servlet 規範其實就是對 HTTP 協議做物件導向的封裝,HTTP協議中的請求和響應就是對應了 HttpServletRequest 和 HttpServletResponse 這兩個介面。
你可以通過 HttpServletRequest 來獲取所有請求相關的資訊,包括 URI、Cookie、Header、請求引數等等,別無它路。因此當你使用某個框架時,你想獲取 HTTP 請求的相關資訊,只要拿到 HttpServletRequest 例項即可。而 HttpServletResponse介面是用來生產 HTTP 回應,包含 Cookie、Header 以及回應的內容等等。
4. Servlet 實踐
我們來寫一個簡單的 Servlet,在 doGet 方法列印所有請求的資訊。
public class HelloWorld extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
System.out.println("init helloworld: " + config);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("********doGet********");
resp.setContentType("text/html;charset=UTF-8");
resp.setCharacterEncoding("UTF-8");
System.out.println("method: " + req.getMethod());
System.out.println("charsetEncoding: " + req.getCharacterEncoding());
System.out.println("contentType: " + req.getContentType());
System.out.println("contentLength: " + req.getContentLength());
System.out.println("requestUrl: " + req.getRequestURL());
System.out.println("servletPath: " + req.getServletPath());
System.out.println("contextPath: " + req.getContextPath());
System.out.println("requestUri: " + req.getRequestURI());
System.out.println("locale: " + req.getLocale());
System.out.println("authType: " + req.getAuthType());
System.out.println("scheme: " + req.getScheme());
System.out.println("protocol: " + req.getProtocol());
System.out.println("serverPort: " + req.getServerPort());
System.out.println("remoteHost: " + req.getRemoteHost());
System.out.println("remoteAddr: " + req.getRemoteAddr());
System.out.println("remoteUser: " + req.getRemoteUser());
System.out.println("requestedSessionId: " + req.getRequestedSessionId());
System.out.println("pathInfo: " + req.getPathInfo());
System.out.println("isSecure: " + req.isSecure());
System.out.println("servletName: " + req.getServerName());
System.out.println("pathTranslated: " + req.getPathTranslated());
System.out.println("++headers++");
Enumeration headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String paramName = (String) headerNames.nextElement();
String paramValue = req.getHeader(paramName);
System.out.println("name: " + paramName + ", value: " + paramValue);
}
System.out.println("--headers--");
System.out.println("++parameters++");
Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String name = parameterNames.nextElement();
String value = req.getParameter(name);
System.out.println("name: " + name + ", value: " + value);
}
System.out.println("--parameters--");
System.out.println("++attributes++");
Enumeration<String> attributeNames = req.getAttributeNames();
while (attributeNames.hasMoreElements()) {
String name = attributeNames.nextElement();
Object value = req.getAttribute(name);
System.out.println("name: " + name + ", value: " + value);
}
System.out.println("--attributes--");
System.out.println("++cookies++");
Cookie[] cookies = req.getCookies();
for (Cookie cookie : cookies) {
System.out.println("name: " + cookie.getName() + ", value: " + URLDecoder.decode(cookie.getValue(), "utf-8"));
}
System.out.println("--cookies--");
PrintWriter writer = resp.getWriter();
try {
writer.println("<h1>Hello world!</h1>");
writer.flush();
} finally {
writer.close();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("--------doPost--------");
doGet(req, resp);
}
@Override
public void destroy() {
super.destroy();
System.out.println("destroy helloworld");
}
}
複製程式碼
在 web.xml 配置上面的 Servlet。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>HelloWorld</servlet-name>
<servlet-class>com.richie.servlet.HelloWorld</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloWorld</servlet-name>
<url-pattern>/helloworld</url-pattern>
</servlet-mapping>
</web-app>
複製程式碼
配置好 Tomcat,執行輸出,然後使用 postman 傳送 post 請求 http://localhost:8080/helloworld,並加上引數username=admin&password=123。
--------doPost--------
********doGet********
method: POST
charsetEncoding: null
contentType: application/x-www-form-urlencoded
contentLength: 23
requestUrl: http://localhost:8080/helloworld
servletPath: /helloworld
contextPath:
requestUri: /helloworld
locale: zh_CN
authType: null
scheme: http
protocol: HTTP/1.1
serverPort: 8080
remoteHost: 0:0:0:0:0:0:0:1
remoteAddr: 0:0:0:0:0:0:0:1
remoteUser: null
requestedSessionId: F17A359B0544082FC6A6C5F62E672E8A
pathInfo: null
isSecure: false
servletName: localhost
pathTranslated: null
++headers++
name: host, value: localhost:8080
name: connection, value: keep-alive
name: content-length, value: 23
name: cache-control, value: max-age=0
name: origin, value: http://localhost:8080
name: upgrade-insecure-requests, value: 1
name: content-type, value: application/x-www-form-urlencoded
name: user-agent, value: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
name: accept, value: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
name: referer, value: http://localhost:8080/
name: accept-encoding, value: gzip, deflate, br
name: accept-language, value: zh-CN,zh;q=0.9,en;q=0.8
name: cookie, value: __utmz=111872281.1521468435.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); Idea-734b2b82=47daaaf7-bc69-41ca-9234-dffa6c217ef8; _ga=GA1.1.2085956305.1521468435; Webstorm-717d1cc9=b6b7f7ea-d8d3-4891-8e20-0dca54d5cbd2; __utmc=111872281; __utma=111872281.2085956305.1521468435.1529898141.1530148517.11; SESSION=12913786-3c46-421d-ac2c-02c9c29ae03d; JSESSIONID=F17A359B0544082FC6A6C5F62E672E8A
--headers--
++parameters++
name: username, value: admin
name: password, value: 123
--parameters--
++attributes++
--attributes--
++cookies++
name: __utmz, value: 111872281.1521468435.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)
name: Idea-734b2b82, value: 47daaaf7-bc69-41ca-9234-dffa6c217ef8
name: _ga, value: GA1.1.2085956305.1521468435
name: Webstorm-717d1cc9, value: b6b7f7ea-d8d3-4891-8e20-0dca54d5cbd2
name: __utmc, value: 111872281
name: __utma, value: 111872281.2085956305.1521468435.1529898141.1530148517.11
name: SESSION, value: 12913786-3c46-421d-ac2c-02c9c29ae03d
name: JSESSIONID, value: F17A359B0544082FC6A6C5F62E672E8A
--cookies--
複製程式碼
可以看出,http 請求的基本資訊都能取到,每個方法都有它的含義,具體可以參考 菜鳥教程 上的解釋。
5. Filter 過濾器
Filter 和 Servlet 一樣重要,它可以實現許多功能,比如敏感詞過濾、使用者驗證等。它也是一個介面,和 Servlet 類似,有 init 和 destroy 方法,最重要的是 doFilter 方法。
Filter 主要用於對使用者請求進行預處理,也可以對 HttpServletResponse 進行後處理。使用 Filter 的完整流程:Filter 對使用者請求進行預處理,接著將請求交給 Servlet 進行處理並生成響應,最後 Filter 再對伺服器響應進行後處理。
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
default void destroy() {
}
}
複製程式碼
除此之外,規範類還有 FilterChain、FilterConfig,Filter 使用了責任鏈的設計模式,傳遞的物件就 FilterChain。
6. Filter 工作原理
當我們寫好 Filter,並配置對哪個 web 資源進行攔截後,web 伺服器每次在呼叫 Servlet 的 service 方法之前, 都會先呼叫一下 filter 的 doFilter 方法。因此,在該方法內編寫程式碼可達到如下目的:
-
呼叫目標資源之前,讓一段程式碼執行。
-
是否呼叫目標資源(即是否讓使用者訪問 web 資源)。
-
呼叫目標資源之後,讓一段程式碼執行。
web 伺服器在呼叫 doFilter 方法時,會傳遞一個 FilterChain 物件進來,FilterChain 物件是 Filter 介面中最重要的一個物件,它也提供了一個 doFilter 方法,開發人員可以根據需求決定是否呼叫此方法。
7. Filter 實踐
簡單實現一個攔截器,列印它的生命週期。
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("init logFilter: " + filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("log doFilter pre");
// 一定要呼叫 filterChain 的 doFilter 方法,繼續傳遞事件
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("log doFilter after");
}
@Override
public void destroy() {
System.out.println("destroy logFilter");
}
}
複製程式碼
然後配置 web.xml,一般把 filter 配置在所有的 servlet 前面,/* 表示匹配所有的請求。
<filter>
<filter-name>LogFilter</filter-name>
<filter-class>com.richie.servlet.LogFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LogFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
複製程式碼
執行後輸出,我們可以對請求和響應進行預處理和後處理,
log doFilter pre
Servlet 處理的方法
log doFilter after
複製程式碼
這篇文章詳細闡釋了 Filter 的有關內容,推薦看看 Java 中的 Filter 過濾器。
另外還有 Listener 監聽器的內容,後面再寫吧。
多囉嗦幾句。其實客戶端和服務端的通訊,本質上是一種 IO 操作。使用者輸入網址後(略去查詢 DNS ),瀏覽器從某個埠發出資料包,裡面有目標地址、請求引數等等(output)。然後經過網路一層層傳遞,跨越萬水千山,到達伺服器被 80 埠捕獲到了(input),交給 Servlet 容器處理,根據請求資訊拿到資料返回給客戶端(output)。這是一對多的資料交換,如果請求資料的人多了,服務端的壓力其實蠻大的。
更細一點說,客戶端和服務端的通訊是一種程式間的通訊。執行在 A 主機上的 A1 程式和執行在 B 主機上的 B1 程式進行通訊,它是基於 Socket 的通訊,埠是一個重要的標識。
(全文完)