Java Web基礎 --- Servlet 綜述(實踐篇)
摘要:
伴隨 J2EE 6一起釋出的Servlet 3.0規範是Servlet規範歷史上最重要的變革之一,它的許多新的特性都極大的簡化了 Java Web 應用的開發。本文從一個簡單的 Servlet 例子開始,說明了如何開發、配置一個 Servlet。此外,還重點敘述了Servlet的一些新特性,包括Servlet 非同步處理、Servlet 非阻塞IO 以及 Servlet 檔案上傳等內容,以便我們對Servlet有一個更全面的瞭解。
本篇主要介紹 Servlet 實踐方面的知識,更多關注於Servlet的新特性:
- Servlet 例項;
- Servlet 配置;
- Servlet 非同步處理;
- Servlet 非阻塞IO;
- Servlet 檔案上傳。
更多關於Servlet理論方面的介紹見我的上一篇博文《Servlet 綜述(理論篇)》,其具體包括以下幾個方面的內容:
- 為什麼會有 Servlet;
- Servlet 是什麼;
- Servlet 如何實現預期效果;
- Servlet 的作用原理;
- Servlet 與 併發;
- Servlet 與 Java Web 應用的結構演變歷程;
- Servlet 與 MVC 的聯絡。
版權宣告:
本文原創作者:書呆子Rico
作者部落格地址:http://blog.csdn.net/justloveyou_/
一. 從一個簡單的 Servlet 例子說起
我們看下面這個簡單的示例:
Servlet:
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
//獲取請求引數
String param1 = request.getParameter("name");
String param2 = request.getParameter("gentle");
//獲取Servlet引數並放到request中
String age = this.getServletConfig().getInitParameter("age");
request.setAttribute("age",age);
// 此處進行業務邏輯處理
//根據處理結果轉發到相應的表現層進行顯示
request.getRequestDispatcher("/showInfo.jsp").forward(request, response);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}
web.xml 配置檔案片段:
<context-param>
<param-name>campus</param-name>
<param-value>NEU</param-value>
</context-param>
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.edu.tju.rico.servlet.TestServlet</servlet-class>
<init-param>
<param-name>age</param-name>
<param-value>24</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/servlet/test</url-pattern>
</servlet-mapping>
顯示邏輯:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<html>
<head>
<title>showInfo</title>
</head>
<body>
請求引數: Name: <%= request.getParameter("name")%><br>
Gentle: <%= request.getParameter("gentle")%><br>
<br>
----------------我是分割線--------------------<br>
<br>
Web應用初始化引數: <%= application.getInitParameter("campus")%><br>
<br>
----------------我是分割線--------------------<br>
<br>
TestServlet 初始化引數: ${requestScope.age}<br>
<br>
</body>
</html>
開發一個Servlet程式時,如果其是基於HTTP協議的,那麼我們一般繼承 HttPServlet 抽象類並重寫 doGet() 和 doPost() 方法,或者直接重寫 service() 方法去處理Http請求。
更多關於 JSP技術的細節見我的其他兩篇部落格: 《Java Web基礎 — Jsp 綜述(上)》 和 《Java Web基礎 — Jsp 綜述(下)》。
二. Servlet 的配置
為了讓 Servlet 能夠響應使用者請求,還必須將 Servlet 配置到我們的Web應用中。進一步地,如果我們沒有為Servlet配置URL,那麼該Servlet將不能響應使用者請求。從 J2EE 6 (Servlet 3.0) 開始,配置 Servlet 的方式共有兩種:
在 web.xml 中進行配置;
在對應的Servlet類中使用@WebServlet註解進行配置。
我們在這裡主要說明使用@WebServlet註解進行配置Servlet,使用 web.xml 配置的方法與該種方式只是在形式不同,作用方式是一樣的,此不贅述。
一旦我們使用 @WebServlet 配置了Servlet,那我們就不用在 web.xml 進行再次配置了,並且不能在web.xml中將 metadata-complete 屬性設定為true。 支援的常用屬性如下表所示:
屬性名 | 是否必需 | 型別 | 描述 |
---|---|---|---|
name | 否 | String | 指定 Servlet 的 name 屬性,等價於<servlet-name>標籤,預設取值為Servlet類的全限定名 |
value | 否 | String[] | 該屬性等價於 urlPatterns 屬性,這兩個屬性不能同時使用 |
loadOnStartup | 否 | int | 指定Servlet的載入時機和順序,等價於<load-on-startup>標籤 |
initParam | 否 | WebInitParam[] | 指定一組 Servlet 初始化引數,等價於<init-param>標籤 |
asyncSupported | 否 | boolean | 宣告 Servlet 是否支援非同步操作模式,等價於<async-supported>標籤 |
description | 否 | String | 該Servlet的描述資訊,等價於<description>標籤 |
displayName | 否 | String | 該Servlet的顯示名,通常配合工具使用,等價於<display-name>標籤 |
將上述示例使用@WebServlet註解配置,如下:
@WebServlet(name = "Test",
urlPatterns = { "/servlet/test" },
initParams = { @WebInitParam(name = "age", value = "24") })
public class TestServlet extends HttpServlet {
...
}
三. Servlet的新特性:非同步處理
1、Servlet 不支援非同步處理會帶來哪些痛點?
在本篇的姊妹篇《Java Web基礎 — Servlet 綜述(實踐篇)》中,我們已經提到Servlet容器處理請求的方式。對於每個到達Web容器的請求,Web容器會為其分配一條執行執行緒來專門負責該請求,直到迴應完成前,該執行執行緒都不會被釋放回Web容器的執行緒池。 我們知道,執行執行緒會耗用系統資源,若某些請求需要長時間處理(例如長時間運算、等待某個資源),就會長時間佔用執行執行緒,若這類的請求很多,許多執行執行緒都被長時間佔用,對於整個系統而言就會是個效能負擔,甚至造成應用的效能瓶頸。
特別地,基本上一些需長時間處理的請求,通常客戶端也較不在乎請求後要有立即的迴應,若可以,讓這類請求先釋放容器分配給該請求的執行執行緒,讓容器可以有機會將執行執行緒資源分配給其它的請求,這樣可以減輕系統負擔。這樣,原先釋放了容器所分配執行執行緒的請求,其迴應將被延後,直到處理完成(例如長時間運算完成、所需資源已獲得)再行對客戶端的迴應。
2、如何使用 Servlet 3.0 去支援對耗時事務的非同步處理?
1)、Servlet 3.0 對非同步處理的支援
在 Servlet 3.0 之前的規範中,如果Servlet作為控制器呼叫了一個耗時的業務方法,那麼 Servlet 必須等到業務方法完全返回之後才能生成響應,這使得 Servlet 對業務方法的呼叫是一種阻塞式呼叫,因此效率比較低。Servlet 3.0 規範引入了非同步處理來解決問題,非同步處理允許Servlet重新發起一個執行緒去呼叫耗時的業務方法,這樣就可以避免等待。
Servlet 3.0 的非同步處理是通過 AsyncContext 類來處理的,Servlet 可以通過 ServletRequest 的如下兩個方法開啟非同步呼叫、建立 AsyncContext 物件。在這裡,AsyncContext 物件代表非同步處理的上下文。
- AsyncContext startAsync()
- AsyncContext startAsync(ServletRequest,ServletRequest)
這兩個方法都會返回 AsyncContext 物件,前者會直接利用原有的請求與響應物件來建立AsyncContext物件,後者則允許你傳入自行建立的請求、響應物件。 在呼叫了startAsync()方法取得AsyncContext物件之後,這次的響應就會被延後,並釋放容器所分配的執行執行緒。
我們可以通過AsyncContext的getRequest()、 getResponse()方法取得請求、響應物件,此次對客戶端的響應將暫緩至呼叫AsyncContext的complete()方法或dispatch()方法為止,前者表示響應完成,後者表示將呼叫指定URL對應的內容進行響應。特別需要注意的是,dispatch()前後仍是同一個請求,並且被非同步請求dispatch的目標頁面必須指定:session=”false”。如果我們要支援 Servlet 的非同步處理,我們的 Servlet 就必須能夠支援非同步處理。也就是說,如果我們使用@WebServlet來標註的話,則必須將其asyncSupported屬性設為true,如下所示:
@WebServlet(urlPatterns = "/some.do", asyncSupported = true )
public class AsyncServlet extends HttpServlet {
...
}
特別需要注意的是,如果Servlet將支援非同步處理,並且其前端有過濾器,那麼過濾器也必須表明其支援非同步處理,如果使用@WebFilter註解的方式,同樣是需要設定其asyncSupported屬性為true,如下所示:
@WebFilter(urlPatterns = "/some.do", asyncSupported = true )
public class AsyncFilter implements Filter{
...
}
2)、非同步處理例項
(1) 進行非同步處理的Servlet類:
@WebServlet(urlPatterns = "/async",asyncSupported=true )
public class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
request.setAttribute("param1", "我在非同步處理前被設定...");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<title>非同步呼叫示例</title>");
out.println("進入Servlet的時間:" + new java.util.Date() + ".<br/>");
//建立AsyncContext物件,開始非同步呼叫
final AsyncContext async = request.startAsync(); // 在區域性(匿名)內部類直接使用,必須設為 final
// 設定非同步呼叫的請求時長
async.setTimeout(10 * 1000);
// 啟動執行緒去處理耗時任務
async.start(new Runnable() { // 匿名內部類
@Override
public void run() {
try {
Thread.sleep(5000);
HttpServletRequest req = (HttpServletRequest) async
.getRequest();
req.setAttribute("param2", "我在耗時任務處理執行緒中被設定...");
async.dispatch("/async.jsp");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
out.println("結束Servlet的時間:" + new java.util.Date() + ".<br/>");
out.flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}
(2) 表現層:
<%-- 被非同步請求dispatch的目標頁面必須指定:session="false" --%>
<%@ page contentType="text/html; charset=utf-8" language="java" session="false"%>
<div style="background-color:#ffffdd;height:80px;">
param1:${param1}<br />
param2:${param2}<br />
<%
out.println("業務呼叫結束的時間:" + new java.util.Date());
%>
</div>
(3) 結果展示頁面
四、Servlet 3.1 支援非阻塞 IO
1、Servlet 不支援非阻塞 IO會帶來哪些痛點?
Servlet 3.0 允許非同步請求處理,但僅限於傳統I/O,這大大限制了程式的可擴充套件性。我們知道,在應用程式中,一種典型的做法是,通過while迴圈讀取Servlet輸入流(Servlet InputStream),如下所示:
public class TestServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
ServletInputStream input = request.getInputStream();
byte[] b = new byte[1024];
int len = -1;
while ((len = input.read(b)) != -1) {
. . .
}
}
}
事實上,Servlet 底層的 IO 是通過以下兩個 IO 流支撐的:
ServletInputStream:Servlet 用於讀取資料的輸入流;
ServletOutputStream:Servlet 用於輸出資料的輸出流。
以 Servlet 讀取資料為例,傳統的讀取方式採用阻塞式IO —— 當Servlet讀取瀏覽器提交的資料時,如果資料暫時不可用,或者資料沒有讀取完成,Servlet當前所有執行緒將會被阻塞,無法繼續執行下去。另外,如果傳入的資料受到阻塞或流傳輸的速度慢於伺服器讀取的速度,則伺服器執行緒就需要一直等待資料的到來。同樣的情形在向Servlet輸出流(Servlet OutputStream)寫資料的時候也會出現。Servlet 3.1 提供的非阻塞IO進行輸入、輸出,可以更好地提升效能。
2、如何使用 Servlet 3.0 去支援非阻塞 IO?
上述問題可以通過新增Servlet 3.1(JSR 340,作為Java EE7釋出的一部分)提供的事件監聽器ReadListener和WriteListener介面進行解決。通過 ServletInputStream.setReadListener 和 ServletOutputStream.setWriteListener 可以註冊監聽器。監聽器中提供了一些回撥方法,在資料讀寫不受阻塞的時候進行觸發。以 ReadListener 為例,實現ReadListener事件監聽器需要實現如下三個方法:
onDataAvailable():當有資料可用時激發該方法;
onAllDataRead():當所有資料讀取完成時激發該方法;
onError(Throwable t):讀取資料出現錯誤時激發該方法。
1)、Servlet 3.1 使用非阻塞IO步驟
在 Servlet 3.1 中使用非阻塞IO步驟可分為三步:
(1) 呼叫 ServletRequest 的startAsync()方法開啟非同步處理模式;
(2) 通過 ServletRequest 獲取 ServletInputStream,併為 ServletInputStream 設定監聽器(ReadListener 實現類);
(3) 實現 ReadListener 介面來實現監聽器,在該監聽器的方法中以非阻塞方式讀取資料。
改進後的doGet方法如下所示:
AsyncContext context = request.startAsync();
ServletInputStream input = request.getInputStream();
input.setReadListener(new MyReadListener(input, context));
setXXXListner方法指出採用非阻塞I/O而不是傳統I/O。onReadListener可以通過ServletInputStream進行註冊,同樣地,oneWritelistener可以通過ServletOutputStream進行註冊。特別地,新增加的ServletInputStream.isReady方法和ServletInputStream.isFinished方法用於檢測非阻塞I/O的讀取狀態,而ServletOutputStream.canWrite方法用於檢測資料是否能夠無阻塞地寫入。
2)、Servlet 3.1 使用非阻塞IO例項
(1) 請求提交表單 form.html:
<html>
<head>
<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title> </title>
</head>
<body>
<form action="asyn" method="post">
使用者名稱:<input type="text" name="name"/><br/>
密碼:<input type="text" name="pass"/><br/>
<input type="submit" value="提交">
<input type="reset" value="重設">
</form>
</body>
</html>
(2) 在 Servlet中使用非阻塞IO:
@WebServlet(urlPatterns = "/async",asyncSupported=true )
public class AsyncServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
request.setAttribute("param1", "我在非同步處理前被設定...");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<title>非阻塞IO示例</title>");
out.println("進入Servlet的時間:" + new java.util.Date() + ".<br/>");
//建立AsyncContext物件,開始非同步呼叫
final AsyncContext async = request.startAsync();
// 設定非同步呼叫的請求時長
async.setTimeout(10 * 1000);
final ServletInputStream input = request.getInputStream();
// 為輸入流注冊監聽器
input.setReadListener(new ReadListener() {
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onDataAvailable() throws IOException {
System.out.println("資料可用!!");
try
{
// 暫停5秒,模擬讀取資料是一個耗時操作。
Thread.sleep(5000);
StringBuilder sb = new StringBuilder();
int len = -1;
byte[] buff = new byte[1024];
// 採用原始IO方式讀取瀏覽器向Servlet提交的資料
while (input.isReady() && (len = input.read(buff)) > 0)
{
String data = new String(buff , 0 , len);
sb.append(data);
}
System.out.println(sb);
// 將資料設定為request範圍的屬性
async.getRequest().setAttribute("data" , sb.toString());
// 轉發到檢視頁面
async.dispatch("/asyn.jsp");
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
@Override
public void onAllDataRead() throws IOException {
System.out.println("資料讀取完成");
}
});
out.println("結束Servlet的時間:" + new java.util.Date() + ".<br/>");
out.flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}
(3) 表現層:
<%@ page contentType="text/html; charset=utf-8" language="java" session="false"%>
<div style="background-color:#ffffdd;height:80px;">
瀏覽器提交資料為:${data}<br/>
<%=new java.util.Date()%>
</div>
(4) 結果展示:
更多關於Servlet使用、實踐方面的介紹以及Servlet新特性的總結見我的下一篇博文《Servlet 綜述(實踐篇)》。
五、Servlet 3.0 支援檔案上傳
Servlet 3.0之前的版本中,檔案上傳是個挺讓人頭疼的問題,雖然有第三方框架(Apache Commons)來實現,但使用起來還是比較麻煩。在Servlet 3.0中,這些問題將不復存在,Servlet 3.0對檔案上傳提供了直接支援,配合Servlet 3.0中基於Annotations的配置,大大簡化上傳件的操作。
在使用表單上傳檔案時,我們需要使用@MultipartConfig註解去修飾對應的Servlet。此外,我們一方面需要在表單裡使用<input type=”file” …/>檔案域,另一方面必須要為表單域設定 enctype 屬性,其有三個值:
- application/x-www-form-urlencoded:表單資料被編碼為名稱/值對,這是預設的編碼方式;
- multipart/form-data:以二進位制流的方式處理表單資料,一般用於傳輸二進位制檔案,如圖片、視訊等;
- text/plain:不編碼特殊字元,適用於通過表單傳送郵件。
下面跟進這個例子來體會其給我們帶來的便捷。
(1) 檔案提交表單:
<%@ page contentType="text/html; charset=utf-8" language="java" errorPage="" %>
<head>
<title> 檔案上傳 </title>
</head>
<body>
<form method="post" action="upload" enctype="multipart/form-data">
檔名:<input type="text" id="name" name="name" /><br/>
選擇檔案:<input type="file" id="file" name="file" /><br/>
<input type="submit" value="上傳" /><br/>
</form>
</body>
</html>
(2) 檔案上傳Servlet:
@WebServlet(name="upload" , urlPatterns={"/upload"})
@MultipartConfig
public class UploadServlet extends HttpServlet
{
public void service(HttpServletRequest request ,
HttpServletResponse response)
throws IOException , ServletException
{
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
request.setCharacterEncoding("utf-8");
// 獲取普通請求引數
String name = request.getParameter("name");
out.println("普通的name引數為:" + name + "<br/>");
// 獲取檔案上傳域
Part part = request.getPart("file");
// 獲取上傳檔案的檔案型別
out.println("上傳檔案的的型別為:"
+ part.getContentType() + "<br/>");
//獲取上傳檔案的大小。
out.println("上傳檔案的的大小為:" + part.getSize() + "<br/>");
// 獲取該檔案上傳域的Header Name
Collection<String> headerNames = part.getHeaderNames();
// 遍歷檔案上傳域的Header Name、Value
for (String headerName : headerNames)
{
out.println(headerName + "--->"
+ part.getHeader(headerName) + "<br/>");
}
// 獲取包含原始檔名的字串
String fileNameInfo = part.getHeader("content-disposition");
// 提取上傳檔案的原始檔名
String fileName = fileNameInfo.substring(
fileNameInfo.indexOf("filename=\"") + 10 , fileNameInfo.length() - 1);
// 將上傳的檔案寫入伺服器
part.write(getServletContext().getRealPath("/uploadFiles")
+ "/" + fileName ); // ①
System.out.println(getServletContext().getRealPath("/uploadFiles"));
}
(3) 結果展示:
六、拾遺增補
除上面提到的內容,Servlet 還引入了其他新的特性(如下所述),此不贅述。
- Servlet 3.0 為 Web 模組化提供了支援;
- Servlet 3.1 可以強制更改 Session ID,具體由 HttpServletRequest 的 changeSessionId()方法完成。
引用
《輕量級 JavaEE 企業應用實戰(第四版)》
Servlet3.0: 簡介AsyncContext
使用 Servlet 3.1 的非堵塞 I/O 實現可伸縮的應用
相關文章
- 【Java基礎】ServletJavaServlet
- GitOps 應用實踐系列 - 綜述Git
- Java 集合框架綜述,這篇讓你吃透!Java框架
- DevOps全面綜述:從概念到實踐dev
- Java集合框架綜述Java框架
- 效能優化開篇綜述優化
- JVM篇1:[-結構綜述-]JVM
- React 應用實踐(基礎篇)React
- Java 執行緒綜述Java執行緒
- 剖析虛幻渲染體系(01)- 綜述和基礎
- Java 集合框架(一)—— 介面綜述Java框架
- 【集合框架】Java集合框架綜述框架Java
- java基礎學習:JavaWeb之ServletJavaWebServlet
- Servlet基礎Servlet
- Web頁面技術綜述(包括fastm)WebAST
- Java基礎篇Java
- Java Web系列:Java Web 專案基礎JavaWeb
- 多執行緒基礎練習實踐篇執行緒
- JAVA精髓(基礎篇)Java
- 計算器製作JAVA版(綜述)Java
- Java之JSP和Servlet基礎知識。JavaJSServlet
- 夯實Java基礎系列1:Java物件導向三大特性(基礎篇)Java物件
- [Java基礎]Java總結篇Java
- ORACLE函式介紹第八篇 綜述Oracle函式
- Java Web系列:JDBC 基礎JavaWebJDBC
- Spring綜述Spring
- API安全綜述API
- Servlet超基礎PrintWriter ??Servlet
- 連載:阿里巴巴大資料實踐—資料建模綜述阿里大資料
- GraphQL 基礎實踐
- 【JAVA】【面試】【基礎篇】- 集合Java面試
- Java基礎-面相物件篇Java物件
- java基礎篇之多型Java多型
- Java基礎-併發篇Java
- Java Web(一) Servlet詳解!!JavaWebServlet
- JAVA基礎:JSP與Servlet的區別(轉)JavaJSServlet
- Google I/O 2018 : Web 現狀綜述GoWeb
- Anchor-free目標檢測綜述 -- Dense Prediction篇