原文:https://howtodoinjava.com/servlets/complete-java-servlets-tutorial/
Servlet 是一些遵從Java Servlet API的Java類,這些Java類可以響應請求。盡管Servlet可以響應任意類型的請求,但是它們使用最廣泛的是響應web方面的請求。 Servlet必須部署在Java servlet容器才能使用。雖然很多開發者都使用Java Server Pages(JSP)和Java Server Faces(JSF)等Servlet架構,但是這些技術都要在幕後通過Servlet容器把頁面編譯為Java Servlet。也就是說,了解Java Servlet技術的基礎知識對任何Java web開發者來說是很有用的。
在這個教程裡,我們将會通過下面的專題來全面了解Java Servlet技術。
目錄
編寫你的第一個Servletbr/>Servlet生命周期方法
使用@WebServlet注解開發Servlet
打包和部署Servlet到Tomcat伺服器
編寫動态的Servlet響應内容
處理Servlet請求和響應
監聽Servlet容器事件
傳遞Servlet初始化參數
為特定的URL請求添加Servlet過濾器
使用Servlet下載下傳二進制檔案
使用RequestDispatcher.forward()轉發請求到另一個Servlet
使用HttpServletResponse.sendRedirect()重定向請求到另一個Servlet
使用Servlets讀寫Cookie
讓我們一起來一步步地學習Servlet。
編寫你的第一個Servlet
我們的第一個Servlet是一個隻擁有少量代碼的簡單Servlet,目的是讓你隻需關注它的行為。
package com.howtodoinjava.servlets;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyFirstServlet extends HttpServlet {
private static final long serialVersionUID = -1915463532411657451L;
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
// Write some content
out.println("<html>");
out.println("<head>");
out.println("<title>MyFirstServlet</title>");
out.println("</head>");
out.println("<body>");
out.println("<h2>Servlet MyFirstServlet at " + request.getContextPath() + "</h2>");
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
}
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//Do some other work
}
@Override
public String getServletInfo() {
return "MyFirstServlet";
}
}
為了在web容器裡注冊上面的Servlet,你要為你的應用建一個web.xml入口檔案。
<?xml version="1.0"?>
<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_3_0.xsd"
version="3.0">
<welcome-file-list>
<welcome-file>/MyFirstServlet</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>MyFirstServlet</servlet-name>
<servlet-class>com.howtodoinjava.servlets.MyFirstServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyFirstServlet</servlet-name>
<url-pattern>/MyFirstServlet</url-pattern>
</servlet-mapping>
</web-app>
上面的Servlet做了一些重要的事情,你可能想了解的。
MyFirstServlet類繼承了HttpServlet。這個繼承是必須的,因為所有的Servlet必須是要麼繼承了 javax.servlet.GenericServlet 的普通Servlet,要麼是繼承了 javax.servlet.http.HttpServlet 的HTTP Servlet。
重新 doGet() 和 doPost() 方法。這兩個方法都已在 HttpServlet 類裡定義了。當一個GET或POST請求到來時,它就會被映射到相應的方法裡。例如,如果你向這個servlet發送一個HTTP GET請求,doGet()方法就會被調用。
這裡也有一些其他有用的方法。你可以重寫它們來在運作時控制應用。例如getServletInfo()。
HttpServletRequest 和 HttpServletResponse 是所有doXXX()方法的預設參數。我們會在後面的章節裡詳細學習這些對象。
以上所有關于簡單Servlet的内容就是你需要知道的内容。
Servlet生命周期方法
在你的應用加載并使用一個Servlet時,從初始化到銷毀這個Servlet期間會發生一系列的事件。這些事件叫做Servlet的生命周期事件(或方法)。讓我們一起來進一步了解它們。
Servlet生命周期的三個核心方法分别是 init() , service() 和 destroy()。每個Servlet都會實作這些方法,并且在特定的運作時間調用它們。
1) 在Servlet生命周期的初始化階段,web容器通過調用init()方法來初始化Servlet執行個體,并且可以傳遞一個實作 javax.servlet.ServletConfig 接口的對象給它。這個配置對象(configuration object)使Servlet能夠讀取在web應用的web.xml檔案裡定義的名值(name-value)初始參數。這個方法在Servlet執行個體的生命周期裡隻調用一次。
init方法定義與這類似:
public void init() throws ServletException {
//custom initialization code
2) 初始化後,Servlet執行個體就可以處理用戶端請求了。web容器調用Servlet的service()方法來處理每一個請求。service() 方法定義了能夠處理的請求類型并且調用适當方法來處理這些請求。編寫Servlet的開發者必須為這些方法提供實作。如果發出一個Servlet沒實作的請求,那麼父類的方法就會被調用并且通常會給請求方(requester)傳回一個錯誤資訊。
通常,我們不需要重寫(override)這個方法。
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
} else if (method.equals(METHOD_HEAD)) {
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
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);
3) 最後,web容器調用destroy()方法來終結Servlet。如果你想在Servlet的生命周期内關閉或者銷毀一些檔案系統或者網絡資源,你可以調用這個方法來實作。destroy() 方法和init()方法一樣,在Servlet的生命周期裡隻能調用一次。
public void destroy() {
在大多數情況下,你通常不需要在你的Servlet裡重寫這些方法。
擴充閱讀:web伺服器是如何運作的?
使用@WebServlet注解來開發Servlet
如果你不喜歡使用xml配置而喜歡注解的話,沒關系,Servlets API同樣提供了一些注解接口給你。你可以像下面的例子一樣使用 @WebServlet 注解并且不需要在web.xml裡為Servlet注冊任何資訊。容器會自動注冊你的Servlet到運作環境,并且像往常一樣處理它。
import javax.servlet.annotation.WebServlet;
@WebServlet(name = "MyFirstServlet", urlPatterns = {"/MyFirstServlet"})
private static final long serialVersionUID = -1915463532411657451L;
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
//Do some work
}
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//Do some other work
}
如果你在使用IDE(例如eclipse),那麼打包和部署你的應用隻需要一個簡單的步驟。右擊項目> Run As > Run As Server。如果還沒配置伺服器先配置好伺服器,然後就可以準備開幹了。
如果你沒在使用IDE,那麼你需要做一些額外的工作。比如,使用指令提示符編譯應用,使用ANT去生成war檔案等等。但我相信,現在的開發者都在使用IDE來開發。是以我就不在這方面浪費時間了。
當你把我們的第一個Servlet部署到tomcat上并在浏覽器輸入“http://localhost:8080/servletexamples/MyFirstServlet”,你會得到下面的響應。
Java Servlets如此有用的原因之一是Servlet能動态顯示網頁内容。這些内容可以從伺服器本身、另外一個網站、或者許多其他網絡可以通路的資源裡擷取。Servlet不是靜态網頁,它們是動态的。可以說這是它們最大的優勢。
讓我們來舉個Servlet例子,這個Servlet會顯示目前日期和時間給使用者并且會顯示使用者名和一些自定義的資訊。讓我們來為這個功能編寫代碼吧。
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@WebServlet(name = "CalendarServlet", urlPatterns = {"/CalendarServlet"})
public class CalendarServlet extends HttpServlet {
private static final long serialVersionUID = -1915463532411657451L;
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
Map<String,String> data = getData();
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
// Write some content
out.println("<html>");
out.println("<head>");
out.println("<title>CalendarServlet</title>");
out.println("</head>");
out.println("<body>");
out.println("<h2>Hello " + data.get("username") + ", " + data.get("message") + "</h2>");
out.println("<h2>The time right now is : " + new Date() + "</h2>");
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
}
//This method will access some external system as database to get user name, and his personalized message
private Map<String, String> getData()
{
Map<String, String> data = new HashMap<String, String>();
data.put("username", "Guest");
data.put("message", "Welcome to my world !!");
return data;
}
當你在tomcat裡運作上面的Servlet并在浏覽器裡輸入“http://localhost:8080/servletexamples/CalendarServlet”,你會得得下面的響應。
Servlet可以輕松建立一個基于請求和響應生命周期的web應用。它們能夠提供HTTP響應并且可以使用同一段代碼來處理業務邏輯。處理業務邏輯的能力使Servlet比标準的HTML代碼更強大。
現實世界裡的應用,一個HTML網頁表單包含了要發送給Servlet的參數。Servlet會以某種方式來處理這些參數并且 傳回一個用戶端能夠識别的響應。在對象是HttpServlet的情況下,用戶端是web浏覽器,響應是web頁面。<form>的 action屬性指定了使用哪個Servlet來處理表單裡的參數值。
為了擷取請求參數,需要調用 HttpServletRequest 對象的 getParameter() 方法,并且傳遞你要擷取的輸入參數的id給該方法。
String value1 = req.getParameter("param1");
String value1 = req.getParameter("param2");
一旦擷取了參數值,它們就會在需要時被處理。對用戶端的響應和我們上面部分讨論的一樣。我們使用 HttpServletResponse 對象給用戶端發送響應。
request和response處理的基本使用可以是這樣的:
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
String username = request.getParameter("username");
String password = request.getParameter("password");
boolean success = validateUser(username, password);
try {
// Write some content
out.println("<html>");
out.println("<head>");
out.println("<title>LoginServlet</title>");
out.println("</head>");
out.println("<body>");
if(success) {
out.println("<h2>Welcome Friend</h2>");
}else{
out.println("<h2>Validate your self again.</h2>");
}
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
為了發送内容給用戶端,你需要使用從 HttpServletResponse 裡擷取的 PrintWriter 對象。任何寫到這個對象的内容都會被寫進outputstream裡,并會把内容發送回給用戶端。
有時候,知道應用伺服器容器(the application server container)裡某些事件發生的時間是很有用的。這個概念适用于很多情況,但它通常用在開啟應用時初始化應用或者關閉應用時清理應用。可以在應用裡 注冊一個監聽器(listener)來顯示應用什麼時候開啟或者關閉。是以,通過監聽這些事件,Servlet可以在一些事件發生時執行相應的動作。
為了建立一個基于容器事件執行動作的監聽器,你必須建立一個實作 ServletContextListener 接口的類。這個類必須實作的方法有 contextInitialized() 和 contextDestroyed()。這兩個方法都需要 ServletContextEvent 作為參數,并且在每次初始化或者關閉Servlet容器時都會被自動調用。
為了在容器注冊監聽器,你可以使用下面其中一個方法:
1) 利用 @WebListener 注解。
2) 在web.xml應用部署檔案裡注冊監聽器。
3) 使用 ServletContext 裡定義的 addListener() 方法
請注意,ServletContextListener 不是Servlet API裡唯一的監聽器。這裡還有一些其他的監聽器,比如
javax.servlet.ServletRequestListener
javax.servlet.ServletRequestAttrbiteListener
javax.servlet.ServletContextListener
javax.servlet.ServletContextAttributeListener
javax.servlet.HttpSessionListener
javax.servlet.HttpSessionAttributeListener
根據你要監聽的事件選擇他們來實作你的監聽器類。比如,每當建立或銷毀一個使用者session時,HttpSessionListener 就會發出通知。
現在的大多數應用都需要設定一些在應用/控制器(controller)啟動時可以傳遞的配置參數(configuration parameters)。Servlet同樣可以接受初始化參數,并在處理第一個請求前來使用它們來建構配置參數。
顯然,你也可以在Servlet裡寫死配置值。但是這樣做的話,在Servlet發生改動時你需要再次重新編譯整個應用。沒有人喜歡這樣做。
<web-app>
<servlet>
<servlet-name>SimpleServlet</servlet-name>
<servlet-class>com.howtodoinjava.servlets.SimpleServlet</servlet-class>
<!-- Servlet init param -->
<init-param>
<param-name>name</param-name>
<param-value>value</param-value>
</init-param>
</servlet>
設定後,你就可以在代碼裡調用 getServletConfig.getInitializationParameter() 并傳遞參數名給該方法來使用參數。就像下面展示的代碼一樣:
String value = getServletConfig().getInitParameter("name");
Web過濾器在給定的URL被通路時對請求進行預處理并調用相應的功能是很有用的。相 比于直接調用給定URL請求的Servlet,包含相同URL模式的過濾器(filter)會在Servlet調用前被調用。這在很多情況下是很有用的。 或許最大的用處就是執行日志,驗證或者其他不需要與使用者互動的背景服務。
過濾器必須要實作 javax.servlet.Filter 接口。這個接口包含了init(),descriptor()和doFilter()這些方法。init()和destroy()方法會被容器調用。 doFilter()方法用來在過濾器類裡實作邏輯任務。如果你想把過濾器組成過濾鍊(chain filter)或者存在多比對給定URL模式的個過濾器,它們就會根據web.xml裡的配置順序被調用。
為了在web.xml裡配置過濾器,需要使用<filter>和<filter-mapping> XML元素以及相關的子元素标簽。
<filter>
<filter-name>LoggingFilter</filter-name>
<filter-class>LoggingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LogingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
如果你要使用注解來為特定的servlet配置過濾器,你可以使用@WebFilter注解。
幾乎所有的web應用都必須有下載下傳檔案的功能。為了下載下傳一個檔案,Servlet必須提供一個和下載下傳檔案類型比對的響應類型。同樣,必須在響應頭裡指出該響應包含附件。就像下面的代碼。
String mimeType = context.getMimeType( fileToDownload );
response.setContentType( mimeType != null ? mimeType : "text/plain" );
response.setHeader( "Content-Disposition", "attachment; filename="" + fileToDownload + """ );
通過調用 ServletContext.getResourceAsStream() 方法并傳遞檔案路徑給該方法,你可以擷取要下載下傳的檔案(檔案儲存在檔案系統)的引用。這個方法會傳回一個輸入流(InputStream)對 象,我們可以用這個對象來讀取檔案内容。當讀取檔案時,我們建立一個位元組緩存區(byte buffer)從檔案裡擷取資料塊。最後的工作就是讀取檔案内容并且把它們複制到輸出流。我們使用while循環來完成檔案的讀取,這個循環直到讀取了文 件的所有内容才會跳出循環。我們使用循環來讀進資料塊并把它寫進輸出流。把所有資料寫進輸出流後,ServletOutputStream 對象的flush方法就會被調用并且清空内容和釋放資源。
看這段簡單的代碼:
private void downloadFile(HttpServletRequest request, HttpServletResponse response, String fileToDownload) throws IOException
final int BYTES = 1024;
int length = 0;
ServletOutputStream outStream = response.getOutputStream();
ServletContext context = getServletConfig().getServletContext();
String mimeType = context.getMimeType( fileToDownload );
response.setContentType( mimeType != null ? mimeType : "text/plain" );
response.setHeader( "Content-Disposition", "attachment; filename="" + fileToDownload + """ );
InputStream in = context.getResourceAsStream("/" + fileToDownload);
byte[] bbuf = new byte[BYTES];
while ((in != null) && ((length = in.read(bbuf)) != -1)) {
outStream.write(bbuf, 0, length);
}
outStream.flush();
outStream.close();
}
有時候,你的應用需要把一個Servlet要處理的請求轉讓給另外的Servlet來處理并完成任務。而且,轉讓請求時不能重定向用戶端的URL。即浏覽器位址欄上的URL不會改變。
在 ServletContext 裡已經内置了實作上面需求的方法。是以,當你擷取了 ServletContext 的引用,你就可以簡單地調用getRequestDispatcher() 方法去擷取用來轉發請求的 RequestDispatcher 對象。當調用 getRequestDispatcher() 方法時,需要傳遞包含servlet名的字元串,這個Servlet就是你用來處理轉讓請求的Servlet。擷取 RequestDispatcher 對象後,通過傳遞 HttpServletRequest 和HttpServletResponse 對象給它來調用轉發方法。轉發方法負責對請求進行轉發。
RequestDispatcher rd = servletContext.getRequestDispatcher("/NextServlet");
rd.forward(request, response);
盡管有時候,你不想在Servlet發送重定向時通知使用者,就像我們在上面那段看到的一樣。但是在某些情況下,我們确實想要通知使用者。當應用内的特定URL被通路時,你想把浏覽器的URL重定向到另外一個。
要實作這種功能,你需要調用 HttpServletResponse 對象的sendRedirect()方法。
httpServletResponse.sendRedirect("/anotherURL");
這個簡單的重定向,與servlet鍊(servlet chaining)相反,不需要傳遞目标位址的HttpRequest對象。
使用Servlet讀寫Cookie
很多應用都想在用戶端機器裡儲存使用者目前的浏覽曆史。目的是當使用者再次使用應用時,他能夠從上次離開的地方開始浏覽。為了實作這個需求,通常使用cookies。你可以把它看作是儲存在用戶端機器裡的鍵值對基本資料。當使用浏覽器打開應用時,應用可以對這些資料進行讀寫。
為了建立cookie,需要執行個體化一個新的 javax.servlet.http.Cookie 對象并且為它配置設定名稱和值。執行個體化cookie後,可以設定屬性來配置cookie。在這個例子裡,我們使用 setMaxAge() 和 setHttpOnly() 方法來設定cookie的生命周期和防範用戶端腳本。
從Servlet3.0 API開始,已經可以把cookie标記為HTTP only了。這使cookie可以防範用戶端腳本的攻擊,使cookie更加安全。