1. Servlet 介紹
1.1 什么是 Servlet
-
Servlet(Server Applet 的縮寫,全稱 Java Servlet): 是用 Java 編寫的服務(wù)器端程序。其主要功能在于交互式地瀏覽和修改數(shù)據(jù),生成動態(tài) Web 內(nèi)容。狹義的 Servlet 是指 Java 語言實現(xiàn)的一個接口,廣義的 Servlet 是指任何實現(xiàn)了這個 Servlet 接口的類,一般情況下,人們將 Servlet 理解為后者。
-
Servlet 運行于支持 Java 的應(yīng)用服務(wù)器中。從原理上講,Servlet 可以響應(yīng)任何類型的請求,但絕大多數(shù)情況下 Servlet 只用來擴展基于 HTTP 協(xié)議的 Web 服務(wù)器。
-
Servlet 是一種實現(xiàn)動態(tài)頁面的技術(shù),是一組由 Tomcat 提供給程序員的 API,幫助程序員簡單高效的開發(fā)一個 web app
1.2 Servlet 的主要工作
- 允許程序員注冊一個類,在 Tomcat 收到的某個特定的 HTTP 請求的時候,執(zhí)行這個類中的一些代碼
- 幫助程序員解析 HTTP 請求,把 HTTP 請求從一個字符串解析成一個 HttpRequest 對象
- 幫助程序員構(gòu)造 HTTP 響應(yīng),程序員只要給指定的 HttpResponse 對象填寫一些屬性字段,Servlet 就會自動的按照 HTTP 協(xié)議的方式構(gòu)造出一個 HTTP 響應(yīng)字符串,并通過 Socket 編寫返回給客戶端
2. Servlet 程序創(chuàng)建步驟
2.1 創(chuàng)建項目
以下使用 IDEA 帶大家編寫一個簡單的 Servlet 程序,主要是讓大家了解一個大致的流程
-
首先使用 IDEA 創(chuàng)建一個 Maven 項目
-
創(chuàng)建好的項目如下
-
通過上圖我們可以看到創(chuàng)建好的項目中有一些目錄結(jié)構(gòu),這是 Maven 項目的標(biāo)準(zhǔn)結(jié)構(gòu),其中
-
src
: 用于存放源代碼和測試代碼的根目錄 -
main
: 用于存放源代碼的目錄 -
test
: 用于存放測試代碼的目錄 -
java
: 用于存放 Java 代碼的目錄 -
resources
: 用于存放依賴的資源文件 -
pom.xml
: 是 Maven 項目的核心配置文件,關(guān)于這個 Maven 項目的相關(guān)屬性,都是在這個 xml 中進行配置
-
2.2 引入依賴
Maven 項目創(chuàng)建完成后,會自動生成一個 pom.xml
文件,我們需要在這個文件中引入 Servlet API 依賴的 jar 包
-
打開中央倉庫,搜索 Servlet,點擊
Java Servlet API
-
選擇對應(yīng) Tomcat 版本的 Servlet(由于我當(dāng)前使用的是 Tomcat 8 系列,所以選擇 Servlet 3.1.0 即可)
-
將中央倉庫提供的該版本的 xml 復(fù)制到項目的
pom.xml
中 -
修改后的
pom.xml
文件如下一個項目中可以有多個依賴,每個依賴都是一個
<dependency>
標(biāo)簽。引入的依賴都要放在一個<dependencies>
的標(biāo)簽中,該標(biāo)簽用于放置項目依賴的 jar 包,Maven 會自動下載該依賴到本地 -
在拷貝的依賴中有幾個參數(shù),分別具有如下含義:
-
groupId
: 表示組織或者公司的 ID -
artifactId
: 表示項目或者產(chǎn)品的 ID -
version
: 表示版本號 -
scope
: 用于指定依賴的作用范圍,包含所在項目的測試、編譯、運行、打包等聲明周期。
-
-
如果你想找到剛剛 Maven 下載到本地的第三方庫,路徑如下
2.3 創(chuàng)建目錄
Web 項目對于目錄結(jié)構(gòu)還有自己的要求,只有 Maven 的標(biāo)準(zhǔn)目錄是不夠的,需要再創(chuàng)建以下目錄并進行配置
-
在 main 目錄下,創(chuàng)建一個
webapp
目錄webapp 目錄就是用于部署到 Tomcat 中的一個重要目錄,里面可以存放一些靜態(tài)資源
-
在 webapp 目錄下,創(chuàng)建一個
WEB-INF
目錄 -
在 WEB-INF 目錄下,創(chuàng)建一個
web.xml
文件Tomcat 通過找到這個 web.xml 文件才能夠正確處理 webapp 中的動態(tài)資源
-
編寫
web.xml
Servlet 中 web.xml 中的內(nèi)容不能是空的,里面的寫法是固定的(這里的寫法專屬于 Servlet),用到的時候可以直接拷貝下面的代碼
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> </web-app>
2.4 編寫代碼
以下編寫一個讓響應(yīng)返回一個自定義字符換的簡單代碼
-
創(chuàng)建一個 TestServlet 類,并且讓它繼承于
HttpServlet
HttpServlet 這個類來自于 pom.xml 中引入的 Servlet API 依賴的 jar 包
-
在 TestServlet 類中重寫
doGet
方法doGet 是 HttpServlet 類中的方法,此處是在子類中重寫了父類的 doGet
-
為了了解 doGet 方法的作用,我們可以看看它的源碼
-
HttpServletRequest
: 表示 HTTP 請求,Tomcat 按照 HTTP 請求的的格式把字符串格式的請求轉(zhuǎn)換成了一個 HttpServletRequest 對象,通過這個對象就可以獲取請求中的信息 -
HttpServletResponse
: 表示 HTTP 響應(yīng),通過代碼可以把響應(yīng)的對象構(gòu)造好,然后 Tomcat 將響應(yīng)返回給瀏覽器 - 通過 doGet 的源碼我們可以大致了解,它的作用是根據(jù)收到的請求通過響應(yīng)返回一個 405 或者 400,那么我們可以重寫這個方法,根據(jù)收到的請求執(zhí)行自己的業(yè)務(wù)邏輯,把結(jié)果構(gòu)造成響應(yīng)對象
-
-
在 doGet 方法中,通過
HttpServletResponse
類的getWriter()
方法往響應(yīng)的 body 中寫入文本格式數(shù)據(jù)resp.getWriter()
會獲取到一個流對象,通過這個流對象就可以寫入一些數(shù)據(jù),寫入的數(shù)會被構(gòu)造成一個 HTTP 響應(yīng)的 body 部分,Tomcat 會把整個響應(yīng)轉(zhuǎn)成字符串,通過 Socket 寫回給瀏覽器 -
需要給 TestServlet 加上一個特定的注解
@WebServlet("/test")
上述助解表示 Tomcat 收到的請求中,URL 的 Servlet Path 路徑為
/test
的請求才會調(diào)用 TestServlet 這個類的代碼,注解中的字符串表示著 URL 的 Servlet Path -
到這里程序的編寫已經(jīng)完成了!但是你可能會疑惑上述代碼不是通過 main 方法作為入口的,這是因為 main 方法已經(jīng)被包含在 Tomcat 中了,我們寫的程序并不能單獨執(zhí)行,而是需要搭配 Tomcat 才能執(zhí)行起來(在 Tomcat 的偽代碼中我們具體分析了這個問題)
2.5 打包程序
在程序編寫好之后,就可以使用 Maven 進行打包
-
首先修改 pom.xml,加入一些必要的配置(打包的類型和打包后的包名)
- packaging 標(biāo)簽中用于設(shè)置打包的類型(如果不修改打包類型則默認為 jar 包,jar 包是普通 Java 程序打包的結(jié)果,里面包含了一些
.class
文件;而部署在 Tomcat 中的壓縮包一般為 war 包,war 包里面是 Java Web 程序,里面除了 .class 文件之外,還包含 HTML、CSS、JavaScript、圖片等等) - finalName 標(biāo)簽中用于設(shè)置打包后的名字(包名很重要,它對應(yīng)著請求中 URL 的 Context Path)
- packaging 標(biāo)簽中用于設(shè)置打包的類型(如果不修改打包類型則默認為 jar 包,jar 包是普通 Java 程序打包的結(jié)果,里面包含了一些
-
執(zhí)行打包操作(打開 Maven 窗口,展開 Lifecycle,雙擊 package 進行打包)
-
打包成功后,可以發(fā)現(xiàn)多了個 target 目錄,該目錄下有一個 testServlet.war 的壓縮包
2.6 部署程序
接下來我們就可以進行程序的部署
-
首先將打好的 war 包拷貝到 Tomcat 的 webapps 目錄下
-
啟動 Tomcat(在 Tomcat 的 bin 目錄中點擊
startup.bat
)
2.7 驗證程序
此時通過瀏覽器訪問 http://127.0.0.1:8080/testServlet/test 就可以看到程序?qū)崿F(xiàn)的結(jié)果了
注意:URL 中的路徑分成了兩個部分 Context Path 和 Servlet Path
- Context Path 這個路徑表示一個 webapp,來源于打包的包名
-
Servlet Path 這個路徑表示一個 webapp 中的一個頁面,來源于對應(yīng)的 Servlet 類
@WebServlet
注解中的內(nèi)容
3. 使用 Smart Tomcat 進行部署
為了簡化上述操作流程,其實是有一些更簡單的方式
- 對于創(chuàng)建項目、引入依賴、創(chuàng)建目錄這三個步驟,其實可以使用項目模板來快速生成,但是由于項目模板加載速度很慢,因此這里并不推薦
- 對于打包程序和部署程序這兩個步驟,其實可以使用 Smart Tomcat 插件來快速實現(xiàn),以下將介紹它的使用方式
3.1 安裝 Smart Tomcat
-
點擊 File → Settings
-
點擊 Plugins,在搜索欄搜索 Smart Tomcat,然后進行安裝即可
3.2 配置 Smart Tomcat
-
點擊 Add Configuration
-
點擊左上角的+號,并選擇 Smart Tomcat
-
主要修改這三個參數(shù)
- Name:這一欄其實可以隨便填
- Tomcat Server:表示 Tomcat 所在的目錄
- Deployment Directory:表示項目發(fā)布目錄
- Context Path:表示項目路徑,默認值是項目名稱
- Servlet Port:表示服務(wù)端口
- Admin Port:表示管理端口
- VM options:表示 JVM 參數(shù)
-
配置好 Smart Tomcat 之后,Add Configuration 就會顯示成 Name 的名字,并且右邊多了個三角形運行的符號
3.3 使用 Smart Tomcat
-
點擊三角形運行 Smart Tomcat,出現(xiàn)如下信息表示程序啟動成功
-
點擊藍色的連接,跳轉(zhuǎn)到項目路徑,再增加 Servlet Path 就可以顯示出該程序的結(jié)果
4. 訪問出錯解決方案
4.1 出現(xiàn) 404
出現(xiàn) 404 原因: 用戶訪問的資源不存在,大概率是 URL 的路徑寫的不正確
錯誤實例1: 少寫了 Context Path 或者 Context Path 寫錯了
錯誤實例2: 少寫了 Servlet Path 或者 Servlet Path 寫錯了
錯誤實例3: web.xml 寫錯了(如清空 web.xml 中的內(nèi)容)
4.2 出現(xiàn) 405
出現(xiàn) 405 原因: 訪問的服務(wù)器不能支持請求中的方法或者不能使用該請求中的方法
錯誤實例1: 沒有重寫 doGet 方法
錯誤實例2: 重寫了 doGet 方法,但是沒有刪除父類的 doGet 方法
4.3 出現(xiàn) 500
出現(xiàn) 500 原因: 服務(wù)器出現(xiàn)內(nèi)部錯誤,往往是 Servlet 代碼中拋出異常導(dǎo)致的
錯誤實例: 代碼中出現(xiàn)空指針異常
4.4 出現(xiàn)“空白頁面”
出現(xiàn)空白頁原因: 響應(yīng)的 body 中并沒有內(nèi)容
錯誤實例: 將 resp.getWriter().write()
操作刪除
4.5 出現(xiàn)“無法訪問此網(wǎng)站”
出現(xiàn)“無法訪問此網(wǎng)站”原因: 一般是不能正確訪問到 Tomcat(可能是 Tomcat 沒啟動,也可能是 IP/端口號寫錯了)
錯誤實例: 注解 @WebServlet
中少寫了 /
4.6 出現(xiàn)中文亂碼問題
響應(yīng)出現(xiàn)中文亂碼問題原因: 使用的編譯器的編碼方式(一般是 utf-8)和瀏覽器的編碼方式不同,瀏覽器默認跟隨系統(tǒng)編碼方式,win10 系統(tǒng)默認是 GBK 編碼
解決方式: 通過響應(yīng)對象的 setContentType()
方法來修改瀏覽器對于響應(yīng)正文的編碼格式
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("吞吞吐吐大魔王");
}
}
5. Servlet 運行原理
在 Servlet 的代碼中,我們并沒有寫 main 方法,那么對應(yīng)的 doGet 代碼是如何被調(diào)用呢?響應(yīng)又是如何返回給瀏覽器的呢?
5.1 Servlet 的架構(gòu)
我們自己實現(xiàn)的 Servlet 是在 Tomcat 基礎(chǔ)上運行的,下圖顯示了 Servlet 在 Web 應(yīng)用程序中的位置
當(dāng)瀏覽器給服務(wù)器發(fā)送請求時,Tomcat 作為 HTTP 服務(wù)器,就可以接收到這個請求。Tomcat 的工作就是解析 HTTP 請求,并把請求交給 Servlet 的代碼來進行進一步的處理。Servlet 的代碼根據(jù)請求計算生成響應(yīng)對象,Tomcat 再把這個響應(yīng)對象構(gòu)造成 HTTP 響應(yīng),返回給瀏覽器。并且 Servlet 的代碼也經(jīng)常會和數(shù)據(jù)庫進行數(shù)據(jù)的傳遞。
5.2 Tomcat 的偽代碼
下面通過 Tomcat 的偽代碼的形式來描述 Tomcat 初始化和處理請求兩部分核心邏輯
-
Tomcat 的初始化流程
class Tomcat { // 用來存儲所有的 Servlet 對象 private List<Servlet> instanceList = new ArrayList<>(); public void start() { // 根據(jù)約定,讀取 WEB-INF/web.xml 配置文件 // 并解析被 @WebServlet 注解修飾的類 // 假定這個數(shù)組里就包含了我們解析到的所有被 @WebServlet 注解修飾的類. Class<Servlet>[] allServletClasses = ...; // 這里要做的的是實例化出所有的 Servlet 對象出來; for (Class<Servlet> cls : allServletClasses) { // 這里是利用 java 中的反射特性做的 // 實際上還得涉及一個類的加載問題,因為我們的類字節(jié)碼文件,是按照約定的 // 方式全部在 WEB-INF/classes 文件夾下存放的,所以 tomcat 內(nèi)部是 // 實現(xiàn)了一個自定義的類加載器(ClassLoader),用來負責(zé)這部分工作。 Servlet ins = cls.newInstance(); instanceList.add(ins); } // 調(diào)用每個 Servlet 對象的 init() 方法,這個方法在對象的生命中只會被調(diào)用這一次 for (Servlet ins : instanceList) { ins.init(); } // 啟動一個 HTTP 服務(wù)器,并用線程池的方式分別處理每一個 Request ServerSocket serverSocket = new ServerSocket(8080); // 實際上 tomcat 不是用的固定線程池,這里只是為了說明情況 ExecuteService pool = Executors.newFixedThreadPool(100); while (true) { Socket socket = ServerSocket.accept(); // 每個請求都是用一個線程獨立支持,這里體現(xiàn)了 Servlet 是運行在多線程環(huán)境下的 pool.execute(new Runnable() { doHttpRequest(socket); }); } // 調(diào)用每個 Servlet 對象的 destroy() 方法,這個方法在對象的生命中只會被調(diào)用這一次 for (Servlet ins : instanceList) { ins.destroy(); } } public static void main(String[] args) { new Tomcat().start(); } }
- Tomcat 的代碼內(nèi)置了 main 方法,當(dāng)我們啟動 Tomcat 的時候,就是從 Tomcat 的 main 方法開始執(zhí)行的
- 被 @WebServlet 注解修飾的類會在 Tomcat 啟動的時候就被獲取到,并集中管理
- Tomcat 通過反射這樣的語法機制來創(chuàng)建被 @WebServlet 注解修飾的類的實例
- 這些實例被創(chuàng)建完之后,就會調(diào)用其中的 init 方法進行初始化
- 這些實例被銷毀之前,就會調(diào)用其中的 destory 方法進行收尾工作
- Tomcat 內(nèi)部也是通過 Socket API 進行網(wǎng)絡(luò)通信
- Tomcat 為了能夠同時處理多個 HTTP 請求,采取了多線程的方式實現(xiàn),因此 Servlet 是運行在多線程環(huán)境下的
-
Tomcat 處理請求流程
class Tomcat { void doHttpRequest(Socket socket) { // 參照我們之前學(xué)習(xí)的 HTTP 服務(wù)器類似的原理,進行 HTTP 協(xié)議的請求解析和響應(yīng)構(gòu)建 HttpServletRequest req = HttpServletRequest.parse(socket); HttpServletRequest resp = HttpServletRequest.build(socket); // 判斷 URL 對應(yīng)的文件是否可以直接在我們的根路徑上找到對應(yīng)的文件,如果找到,就是靜態(tài)內(nèi)容 // 直接使用 IO 進行內(nèi)容輸出 if (file.exists()) { // 返回靜態(tài)內(nèi)容 return; } // 走到這里的邏輯都是動態(tài)內(nèi)容了 // 找到要處理本次請求的 Servlet 對象 Servlet ins = findInstance(req.getURL()); // 調(diào)用 Servlet 對象的 service 方法 // 這里就會最終調(diào)用到我們自己寫的 HttpServlet 的子類里的方法了 try { ins.service(req, resp); } catch (Exception e) { // 返回 500 頁面,表示服務(wù)器內(nèi)部錯誤 } } }
- Tomcat 從 Socket 中讀到的 HTTP 請求是一個字符串,然后 Tomcat 會按照 HTTP 協(xié)議的格式解析成一個 HttpServletRequest 對象
- Tomcat 會根據(jù) URL 中的 Path 判定這個請求是請求一個靜態(tài)資源還是動態(tài)資源。如果是靜態(tài)資源,直接找到對應(yīng)的文件,把文件的內(nèi)容通過 Socket 返回;如果是動態(tài)資源,才會執(zhí)行到 Servlet 的相關(guān)邏輯
- Tomcat 會根據(jù) URL 中的 Context Path 和 Servlet Path 確定要調(diào)用哪個 Servlet 實例的 service 方法
- 通過 service 方法,就會進一步調(diào)用我們重寫的 doGet 或者 doPost 方法等等
-
Servlet 的 service 方法的實現(xiàn)
class Servlet { public void service(HttpServletRequest req, HttpServletResponse resp) { String method = req.getMethod(); if (method.equals("GET")) { doGet(req, resp); } else if (method.equals("POST")) { doPost(req, resp); } else if (method.equals("PUT")) { doPut(req, resp); } else if (method.equals("DELETE")) { doDelete(req, resp); } ...... } }
- Servlet 的 service 方法內(nèi)部會根據(jù)當(dāng)前請求的方式,決定調(diào)用其中的某個 doXXX 方法
- 在調(diào)用 doXXX 方法的時候,會觸發(fā)多態(tài)機制,從而執(zhí)行到我們自己寫的子類的 doXXX 方法
6. Servlet API 詳解
對于 Servlet 主要介紹三個類,分別是 HttpServlet、HttpServletRequest 和 HttpServletResponse。
其中 HttpServletRequest 和 HttpServletResponse 是 Servlet 規(guī)范中規(guī)定的兩個接口,HttpServlet 中并沒有實現(xiàn)這兩個接口的成員變量,它們只是 HttpServlet 的 service 和 doXXX 等方法的參數(shù)。這兩個接口類的實例化是在 Servlet 容器中實現(xiàn)的。
6.1 HttpServlet
核心方法
方法名稱 | 調(diào)用時機 |
---|---|
init |
在 HttpServlet 實例化之后被調(diào)用一次 |
destory |
在 HttpServlet 實例不再使用的時候調(diào)用一次 |
service |
收到 HTTP 請求的時候調(diào)用 |
doGet |
收到 GET 請求的時候調(diào)用(由 service 方法調(diào)用) |
doPost |
收到 POST 請求的時候調(diào)用(由 service 方法調(diào)用) |
doPut/doDelete/doOptions/... |
收到其它對應(yīng)請求的時候調(diào)用(由 service 方法調(diào)用) |
Servlet 的生命周期: Servlet 的生命周期就是 Servlet 對象從創(chuàng)建到銷毀的過程,下面來介紹其生命周期的過程
- Servlet 對象是由 Tomcat 來進行實例化的,并且在實例化完畢之后調(diào)用 init 方法(只調(diào)用一次)
- Tomcat 對于收到的請求,都會通過對應(yīng)的 Servlet 的 service 方法來進行處理(可以調(diào)用多次)
- Tomcat 在結(jié)束之前,會調(diào)用 Servlet 的 destory 方法來進行回收資源(最多調(diào)用一次)
注意: init 和 service 能夠保證在各自的合適時機被 Tomcat 調(diào)用,但是 destory 不一定,它是否能夠被調(diào)用取決于 Tomcat 是如何結(jié)束的
- 如果直接殺死進程,那么就來不及調(diào)用 destory
- 如果通過 Tomcat 的管理端口(默認 8005)進行關(guān)閉,就能夠調(diào)用 destory
處理 GET 請求示例:
直接通過瀏覽器 URL 發(fā)送一個 GET 方法的請求,來對這個請求進行處理
@WebServlet("/get")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("get");
}
}
處理 POST 請求示例:
由于通過瀏覽器 URL 發(fā)送的請求是 GET 方法的請求,因此我們需要通過其它方式來發(fā)送一個 POST 請求然后用于處理。發(fā)送 POST 請求的方式有通過 Ajax、form 表單或者 socket api 進行構(gòu)造,如果單純的用于測試就比較麻煩,這里推薦使用軟件 postman,這是一個很強大的 API 調(diào)試、Http 請求的工具。
@WebServlet("/post")
public class TestServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("post");
}
}
6.2 HttpServletRequest
核心方法
方法 | 描述 |
---|---|
String getProtocol() |
返回協(xié)議的名稱和版本號 |
String getMethod() |
返回請求的 HTTP 方法的名稱 |
String getRequestURL() |
返回請求的 URL,不帶查詢字符串 |
String getRequestURI() |
返回該請求的 URL 的一部分,不帶協(xié)議名、端口號、查詢字符串 |
String getContextPath() |
返回指示請求 URL 中 Context Path 部分 |
String getServletPath() |
返回指示請求 URL 中 ServletPath 部分 |
String getQueryString() |
返回請求首行中 URL 后面的查詢字符串 |
Enumeration getParameterNames() |
返回一個 String 對象的枚舉,包括在該請求中的參數(shù)的名稱 |
String getParameter(String name) |
以字符串形式返回請求參數(shù)的值,如果參數(shù)不存在則返回 null |
String[] getParameterValues(String name) |
返回一個字符串對象的數(shù)組,包括所有給定的請求的參數(shù),如果參數(shù)不存在則返回 null |
Enumeration getHeaderNames() |
返回一個枚舉,包括該請求中所有的頭名 |
String getHeader(String name) |
以字符串形式返回指定的請求頭的值 |
String getCharacterEncoding() |
返回請求正文中使用的字符編碼的名稱 |
String getContentType() |
返回請求正文的 MIME 類型,如果不知道類型則返回 null |
int getContentLength() |
以字節(jié)為單位返回請求正文的長度,并提供輸入流,如果長度未知則返回-1 |
InputStream getInputStream() |
用于讀取請求的正文內(nèi)容,返回一個 InputStream 對象 |
示例1: 通過上述方法返回一個頁面是該請求的具體 HTTP 請求格式
@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 此處返回一個 HTML,在 HTML 中顯示 HttpRequestServlet 類中的一些核心方法
// 把這些 API 的返回結(jié)果通過 StringBuilder 進行拼接
resp.setContentType("text/html;charset=utf-8");
StringBuilder html = new StringBuilder();
html.append(req.getMethod());
html.append(" ");
html.append(req.getRequestURL());
html.append("?");
html.append(req.getQueryString());
html.append(" ");
html.append(req.getProtocol());
html.append("</br>");
Enumeration<String> headerNames = req.getHeaderNames();
while(headerNames.hasMoreElements()){
String headName = headerNames.nextElement();
String header = req.getHeader(headName);
html.append(headName);
html.append(": ");
html.append(header);
html.append("</br>");
}
html.append("</br>");
//InputStream body = req.getInputStream();
resp.getWriter().write(html.toString());
}
}
示例2: 處理 HTTP 請求的 body 中的數(shù)據(jù)格式
-
如果 body 的內(nèi)容格式是
x-www-form-urlencoded
,使用getParameter
進行處理- 此處是要獲取 body 的數(shù)據(jù),由于 GET 方法一般沒有 body,這里使用 POST 方法演示
- 約定 body 的數(shù)據(jù)格式為:x-www-form-urlencoded
- 約定 body 的數(shù)據(jù)內(nèi)容為:username=123&passwd=456
@WebServlet("/postParameter") public class PostParameterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); String username = req.getParameter("username"); String passwd = req.getParameter("passwd"); resp.getWriter().write("username=" + username + "</br>" +"passwd=" + passwd); } }
-
如果 body 的內(nèi)容格式是
json
,首先將整個 body 都讀取出來,再借助第三方庫的方法按照 json 的格式來進行解析,Java 標(biāo)準(zhǔn)庫沒有內(nèi)置對于 json 解析的方法)- 此處是要獲取 body 的數(shù)據(jù),由于 GET 方法一般沒有 body,這里使用 POST 方法演示
- 約定 body 的數(shù)據(jù)格式為:json
- 約定 body 的數(shù)據(jù)內(nèi)容為:
{
username=123,
passwd=456
} - 此處使用 jackson 第三方庫,使用之前需要去 Maven 的中央倉庫將 jackson 的依賴引入 pom.xml 中
- jackson 中的核心類是
ObjectMapper
,通過這個類的readValue(String content, Class<T> valueType)
方法,就可以將 json 字符串轉(zhuǎn)化為一個類的對象(第一個參數(shù)是 json 字符串,第二個參數(shù)是類對象),ObjectMapper 會遍歷定義的類中的每個成員的名稱,去 json 字符串的 key 中查找,如果找到了就將對應(yīng)的值返回給該成員
// 自定義的將 json 字符串轉(zhuǎn)化的類 class UserInfo { public String username; public String passwd; } @WebServlet("/jsonParameter") public class JsonParameterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf-8"); // 1. 先將整個 body 讀取出來 String body = readBody(req); // 2. 按照 json 格式進行解析 ObjectMapper objectMapper = new ObjectMapper(); UserInfo userInfo = objectMapper.readValue(body, UserInfo.class); resp.getWriter().write("username=" + userInfo.username + "</br>" + "passwd=" + userInfo.passwd); } // 定義一個方法來讀取請求中的全部 body private String readBody(HttpServletRequest req) throws IOException { // 1. 先拿到 body 的長度,單位是字節(jié) int contentLength = req.getContentLength(); // 2. 準(zhǔn)備一個字節(jié)數(shù)組,來存放 body 內(nèi)容 byte[] buffer = new byte[contentLength]; // 3. 獲取到 InputStream 對象 InputStream inputStream = req.getInputStream(); // 4. 從 InputStream 對象中讀取到數(shù)據(jù),將數(shù)據(jù)放到字節(jié)數(shù)組中 inputStream.read(buffer); // 5. 將存放 body 內(nèi)容的字節(jié)數(shù)組轉(zhuǎn)換成字符串 return new String(buffer, "utf-8"); } }
6.3 HttpServletResponse
核心方法
方法 | 描述 |
---|---|
void setStatus(int sc) |
為該響應(yīng)設(shè)置狀態(tài)碼 |
void setHeader(String name, String value) |
設(shè)置一個帶有給定的名稱和值的 header,如果 name 已經(jīng)存在,則覆蓋舊的值 |
void addHeader(String name, String value) |
添加一個帶有給定的名稱和值的 header,如果 name 已經(jīng)存在,不覆蓋舊的值,而是添加新的鍵值對 |
void setContentType(String type) |
設(shè)置被發(fā)送到客戶端的響應(yīng)的內(nèi)容類型 |
void setCharacterEncoding(String charset) |
設(shè)置被發(fā)送到客戶端的響應(yīng)的字符編碼,例如 utf-8 |
void sendRedirect(String location) |
設(shè)置 Location 字段,實現(xiàn)重定向 |
PrintWriter getWriter() |
用于往 body 中寫入文本格式數(shù)據(jù) |
OutputStream getOutputStream() |
用于往 body 中寫入二進制格式數(shù)據(jù) |
示例1: 通過代碼,構(gòu)造出不同的響應(yīng)狀態(tài)碼
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
int status = 404;
resp.setStatus(status);
resp.getWriter().write("status=" + status);
}
}
示例2: 在響應(yīng)報頭設(shè)置一個 Refresh 字段,實現(xiàn)字段刷新程序
Refresh 的值表示每秒刷新的時間,當(dāng)程序是毫秒級刷新的時候,可能存在誤差
@WebServlet("/autoRefresh")
public class AutoRefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 給響應(yīng)設(shè)置一個 Refresh 的 header,每隔 1s 鐘刷新一次
resp.setHeader("Refresh", "1");
// 返回一個當(dāng)前的時間,用來顯示刷新的效果
resp.getWriter().write("timestamp=" + System.currentTimeMillis());
}
}
示例3: 實現(xiàn)重定向操作
-
方法一:在響應(yīng)報頭設(shè)置狀態(tài)碼和 Location 來實現(xiàn)重定向
@WebServlet("/redirect") public class RedirectServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 將狀態(tài)碼設(shè)置為 3XX resp.setStatus(302); // 設(shè)置一個 Location,重定向到 CSDN 博客主頁 resp.setHeader("Location", "https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343"); } }
-
方法二:直接使用 sendRedirect() 方法來實現(xiàn)重定向
@WebServlet("/redirect") public class RedirectServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.sendRedirect("https://blog.csdn.net/weixin_51367845?spm=1000.2115.3001.5343"); } }
7. 實現(xiàn)服務(wù)器版表白墻程序
7.1 基本介紹
在之前的文章《【W(wǎng)eb 三件套】 JavaScript WebAPI》中實現(xiàn)過了一個純前端的表白墻代碼,實現(xiàn)后的效果如下。這次將會結(jié)合上述的知識,實現(xiàn)一個服務(wù)器版的表白墻程序
7.2 準(zhǔn)備操作
-
創(chuàng)建好一個 Servlet 項目
-
將之前寫好的純前端的表白墻代碼拷貝到 webapp 目錄下
-
約定好前后端交互的接口,該程序只需約定兩個接口
-
從服務(wù)器獲取全部留言
-
約定請求:方法為 GET,請求路徑為 /message
-
約定響應(yīng):版本號為 HTTP/1.1,狀態(tài)碼為 200 OK,采用 JSON 數(shù)據(jù)格式
-
JSON 具體格式為:
[ ? { ? from: "", ? to: "", ? message: "" ? } ]
-
-
通過客戶端給服務(wù)器新增一個留言
- 約定請求:方法為 POST,請求路徑為 /message
- 約定響應(yīng):版本號為 HTTP/1.1,狀態(tài)碼為 200 OK,提交成功后響應(yīng)頁面顯示“提交成功”
-
-
創(chuàng)建一個
MessageServlet
類,@WebServlet
注解為/message
,對應(yīng)著約定的請求路徑,通過上方的約定完成服務(wù)器段的代碼 -
更改前端的代碼
7.3 代碼實現(xiàn)
后端代碼實現(xiàn):
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.HttpRetryException;
import java.util.ArrayList;
import java.util.List;
// 這個類表示一條消息的詳細情況
class Message{
public String from;
public String to;
public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 通過這個數(shù)組來表示所有的消息
private List<Message> messages= new ArrayList<>();
// 通過這個代碼來完成獲取服務(wù)器所有消息的操作
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
// 獲取到消息列表
// 此處要做的就是把當(dāng)前的 messages 數(shù)組轉(zhuǎn)成 json 格式返回給瀏覽器
ObjectMapper objectMapper = new ObjectMapper();
// 通過 ObjectMapper 的 writeValuesAsString() 方法就可以將一個對象轉(zhuǎn)換成 json 字符串
// 由于這里的 message 是一個 List,那么得到的結(jié)果是一個 json 數(shù)組
String jsonString = objectMapper.writeValueAsString(messages);
resp.getWriter().write(jsonString);
}
// 通過這個代碼來完成新增消息的操作
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
messages.add(message);
resp.getWriter().write("提交成功!");
}
}
前端代碼實現(xiàn):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表白墻</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 600px;
margin: 0 auto;
}
h1 {
text-align: center;
padding: 20px 0;
color: pink;
}
p {
text-align: center;
font-size: 15px;
color: grey;
padding: 5px 0;
}
.row {
display: flex;
height: 40px;
justify-content: center;
align-items: center;
}
.row span {
width: 80px;
}
.row .edit {
width: 250px;
height: 35px;
}
.row .submit {
width: 330px;
height: 40px;
background-color: orange;
color: #fff;
border: none;
}
.row .submit:active {
background-color: grey;
}
</style>
</head>
<body>
<div class="container">
<h1>表白墻</h1>
<p>輸入后點擊提交,將會把消息顯示在在墻上</p>
<div class="row">
<span>誰:</span>
<input type="text" class="edit">
</div>
<div class="row">
<span>對誰:</span>
<input type="text" class="edit">
</div>
<div class="row">
<span>說什么:</span>
<input type="text" class="edit">
</div>
<div class="row">
<input type="button" value="提交" class="submit">
</div>
</div>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
let submitButton = document.querySelector('.submit');
submitButton.onclick = function() {
// 1. 獲取到輸入框里的內(nèi)容
let edits = document.querySelectorAll('.edit');
let from = edits[0].value;
let to = edits[1].value;
let message = edits[2].value;
// 2. 根據(jù)輸入框的內(nèi)容,構(gòu)造 HTML 元素,添加到頁面中
if(from == '' || to == '' || message == '') {
return;
}
let div = document.createElement('div');
div.innerHTML = from + '對' + to + '說:' + message;
div.className = 'row';
let container = document.querySelector('.container');
container.appendChild(div);
// 3. 把上次輸入的內(nèi)容清空
for(let i = 0; i < edits.length; i++){
edits[i].value = '';
}
// 4. 把當(dāng)前新增的消息發(fā)送給服務(wù)器
let body = {
from: from,
to: to,
message: message
};
$.ajax ({
url: "message",
method: "post",
contentType: "application/json;charset=utf8",
// 通過 JSON.stringify 將對象轉(zhuǎn)成字符串
data: JSON.stringify(body),
success: function(data, status){
console.log(data);
}
})
}
// 服務(wù)器版本
// 1. 在頁面加載的時候,從服務(wù)器獲取到消息列表,并顯示在網(wǎng)頁上
function load() {
$.ajax({
method: "get",
url: "message",
success: function(data, status) {
// 此處得到的響應(yīng) data 其實已經(jīng)被 jquery 轉(zhuǎn)成了一個對象數(shù)組
// 但是這里的自動轉(zhuǎn)換有個前提,服務(wù)器響應(yīng)的 header 中 ContentType 是 json
let container = document.querySelector('.container');
for(let message of data){
// 遍歷每個元素,針對每個元素拆功能鍵一個 div 標(biāo)簽
let div = document.createElement('div');
div.className = 'row';
div.innerHTML = message.from + "對" + message.to + " 說:" + message.message;
container.append(div);
}
}
})
}
load();
</script>
</body>
</html>
7.4 持久化存儲
通過上述修改,原本的純前端代碼就加上了服務(wù)器,只要服務(wù)器開啟后,即使刷新網(wǎng)頁,已經(jīng)添加的數(shù)據(jù)也不會消失。但是如果重啟服務(wù)器的話,原本的數(shù)據(jù)就會丟失,為了解決這個問題,就需要讓數(shù)據(jù)能夠持久化存儲。
持久化存儲: 是把數(shù)據(jù)(如內(nèi)存中的對象)保存到可永久保存的存儲設(shè)備中(如磁盤),是一種將程序數(shù)據(jù)在持久狀態(tài)和瞬時狀態(tài)間轉(zhuǎn)換的機制。
持久化存儲機制包括: JDBC 和 文件 IO
接下來將通過增加一個數(shù)據(jù)庫來讓上述表白墻程序可以持久化存儲
-
先建庫建表(可以先創(chuàng)建一個文件,將要建的數(shù)據(jù)庫和表都寫好)
drop database if exits messagewall; create database messagewall; use messagewall; drop table if exits message; create table message ( `from` varchar(50), `to` varchar(50), `message` varchar(1024) );
-
在 pom.xml 文件中引入 mysql 的 jar 包
-
連接數(shù)據(jù)庫,創(chuàng)建一個 DBUtil 類,用于封裝數(shù)據(jù)庫的建立連接和資源釋放操作
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; // 通過這個類來封裝數(shù)據(jù)庫的建立連接操作 public class DBUtil { private static final String URL = "jdbc:mysql://127.0.0.1:3306/messagewall?characterEncoding=utf8&setSSL=false"; private static final String USERNAME = "root"; private static final String PASSWORD = "1234"; private static DataSource dataSource = new MysqlDataSource(); static { ((MysqlDataSource)dataSource).setURL(URL); ((MysqlDataSource)dataSource).setUser(USERNAME); ((MysqlDataSource)dataSource).setPassword(PASSWORD); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet){ if(resultSet != null){ try { resultSet.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } if(statement != null){ try { statement.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } if(connection != null){ try { connection.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } }
-
修改 MessageWall 類的代碼,主要修改的地方有兩處,將原本的 messages 數(shù)組刪除
-
在獲取消息時,可以增加一個
getMessages()
方法,用于拿到數(shù)據(jù)庫中的所有消息// 從數(shù)據(jù)庫獲取到所有消息 private List<Message> getMessages() { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; List<Message> messages = new ArrayList<>(); try { // 1. 和數(shù)據(jù)庫建立連接 connection = DBUtil.getConnection(); // 2. 構(gòu)造 sql String sql = "select * from message"; statement = connection.prepareStatement(sql); // 3. 執(zhí)行 sql resultSet = statement.executeQuery(); // 4. 遍歷結(jié)果集合 while(resultSet.next()){ Message message = new Message(); message.from = resultSet.getString("from"); message.to = resultSet.getString("to"); message.message = resultSet.getString("message"); messages.add(message); } } catch (SQLException throwables) { throwables.printStackTrace(); } finally { DBUtil.close(connection, statement, resultSet); } return messages; }
-
在新增消息是,可以新增一個
addMessage()
方法,用于往數(shù)據(jù)庫存儲一條新消息// 往數(shù)據(jù)庫新增一條消息 private void addMessage(Message message) { Connection connection = null; PreparedStatement statement = null; try { // 1. 和數(shù)據(jù)庫建立連接 connection = DBUtil.getConnection(); // 2. 構(gòu)造 sql String sql = "insert into message values(?, ?, ?)"; statement = connection.prepareStatement(sql); statement.setString(1, message.from); statement.setString(2, message.to); statement.setString(3, message.message); // 3. 執(zhí)行 sql statement.executeUpdate(); } catch (SQLException throwables) { throwables.printStackTrace(); } finally { DBUtil.close(connection, statement, null); } }
到這里為止,一個完整的服務(wù)器表白程序就寫好啦!在我自己擼上面的代碼時,由于連接 MySQL 的 URL 中的端口號寫錯了,導(dǎo)致自己找了很久的 bug,所以大家如果嘗試上述代碼時遇到問題,一定要看清是不是自己哪個地方打錯了!
-
8. Cookie 和 Session
8.1 Cookie 介紹
在之前的文章《HTTP 協(xié)議詳解》中,就介紹過了 Cookie,可以結(jié)合本文的內(nèi)容來搭配理解。
-
Cookie 是什么?
Cookie 是瀏覽器提供的在客戶端存儲數(shù)據(jù)的一種機制(由于瀏覽器禁止了網(wǎng)頁中的代碼直接訪問本地磁盤的文件,因此想要在網(wǎng)頁中實現(xiàn)持久化存儲,就可以通過 Cookie 這樣的機制)
-
Cookie 里面存什么?
Cookie 存儲的數(shù)據(jù)都是程序員自定義的,存儲的數(shù)據(jù)是一個字符串,是鍵值對結(jié)構(gòu)的,鍵值對之間使用
;
分割,鍵和值之間使用=
分割 -
Cookie 從哪里來?
服務(wù)器返回響應(yīng)的時候,可以把要在客戶端保存的數(shù)據(jù)以
Set-Cookie
這個 header 的方式返回給瀏覽器 -
Cookie 到哪里去?
客戶端下次訪問服務(wù)器的時候,就會把之前保存好的 Cookie 再發(fā)送給服務(wù)器
-
Cookie 的典型應(yīng)用場景:
可以使用 Cookie 來保存用戶的登錄信息。比如我們登錄過某個網(wǎng)站后,下次登錄時就不需要重新輸入用戶和密碼了
-
Cookie 的缺陷:
每次請求都要把該域名下所有的 Cookie 通過 HTTP 請求傳給服務(wù)器,因此 Cookie 的存儲容量是有限的。
在了解 Cookie 以后,我們發(fā)現(xiàn) Cookie 是不能夠用于存儲和用戶相關(guān)的直接信息的,一是 Cookie 的存儲容量有限,二是發(fā)送請求時占用帶寬很多,三是不太安全。即這些數(shù)據(jù)不適合保存在客戶端,保存在服務(wù)器是更合適的,通過會話(Session)的方式就能夠保存這些數(shù)據(jù)。
8.2 Session 會話機制介紹
基本介紹:
在計算機中,尤其是在網(wǎng)絡(luò)應(yīng)用中,Session 稱為“會話控制”。Session 對象存儲特定用戶會話所需的屬性及配置信息。當(dāng)用戶在應(yīng)用程序的 Web 頁之間跳轉(zhuǎn)時,存儲在 Session 對象中的變量將不會丟失,而是在整個用戶會話中一直存在下去。當(dāng)用戶請求來自應(yīng)用程序的 Web 頁時,如果該用戶還沒有會話,則 Web 服務(wù)器將自動創(chuàng)建一個 Session 對象。當(dāng)會話過期或被放棄后,服務(wù)器將終止該會話。Session 對象最常見的一個用法就是存儲用戶的首選項。例如,如果用戶指明不喜歡查看圖形,就可以將該信息存儲在 Session 對象中。注意會話狀態(tài)僅在支持 Cookie 的瀏覽器中保留。
會話的本質(zhì):
- 會話的本質(zhì)就是一個哈希表,其中存儲了一些鍵值對結(jié)構(gòu),key 叫做 sessionId,是一個不隨機的、不重復(fù)的、唯一的字符串,value 就是要保存的身份信息,通過 HttpSession 對象來保存。key 和 value 都是 Servlet 自動創(chuàng)建的。
- 每個用戶登錄都會生成一個會話,服務(wù)器會以哈希表的方式將這些會話管理起來
- 一個會話的詳細數(shù)據(jù)通過一個 HttpSession 對象來存儲,并且 HttpSession 對象中存儲的數(shù)據(jù)也是鍵值對結(jié)構(gòu),key 和 value 都是程序員自定義的
接著 Cooike 不適合用于存儲用戶相關(guān)的直接信息來講,由于客戶端不適合存儲這些數(shù)據(jù),服務(wù)器這邊可以通過 Session 會話的方式來進行保存。下面將會以用戶登錄的流程來介紹 Session 會話機制
- 當(dāng)用戶成功登錄之后,服務(wù)器在 Session 中會生成一個新的記錄,并把 sessionId 返回給客戶端(例如 HTTP 響應(yīng)中可以通過 Set-Cookie 字段返回,其中 Cookie 的 key 為 “JSESSION”,value 為服務(wù)器生成的 sessionId 的具體的值)
- 然后客戶端只需要保存這個 sessionId ,當(dāng)后續(xù)再給服務(wù)器發(fā)送請求時,請求中就會帶上 sessionId(例如 HTTP 請求中會帶上 Cookie 字段用于傳遞 Session)
- 服務(wù)器收到請求后,就會根據(jù)請求中的 sessionId 在 Session 中查詢對應(yīng)用戶的身份信息,在進行后續(xù)操作
Session 會話機制的好處:
- 使得客戶端很輕量,不用保存太多數(shù)據(jù)
- 客戶端和服務(wù)器之間傳輸?shù)臄?shù)據(jù)量小,節(jié)省帶寬
- 數(shù)據(jù)都在服務(wù)器存儲,即使客戶端出現(xiàn)問題,數(shù)據(jù)也不會丟失
注意: Servlet 的 Session 默認是保存在內(nèi)存中的,如果重啟服務(wù)器 Session 數(shù)據(jù)將會丟失
8.3 Cookie 和 Session 的區(qū)別
-
Cookie 是客戶端存儲數(shù)據(jù)的一種機制,可以存儲身份信息,也可以存儲其它信息,是鍵值對結(jié)構(gòu)的
-
Session 是服務(wù)器存儲數(shù)據(jù)的一種機制,主要用于存儲身份相關(guān)的信息,是鍵值對結(jié)構(gòu)的
-
Cookie 和 Session 經(jīng)常配合使用,但是不是必須的。
- Cookie 也完全可以保存一些數(shù)據(jù)在客戶端,這些數(shù)據(jù)不一定是用戶身份信息,不一定是 sessionId
- Session 中的 sessionId 也不需要非得通過 Cookie 和 Set-Cookie 來傳遞
8.4 Servlet 中 Cookie 和 Session 的核心方法
HttpServletRequest 類中的相關(guān)方法
方法 | 描述 |
---|---|
HttpSession getSession(參數(shù)) |
在服務(wù)器中獲取會話,參數(shù)如果為 true,當(dāng)不存在會話時會新建會話(包括生成一個新的 sessionId 和 HttpSession 對象),并通過 Set-Cookies 將 sessionId 返回給客戶端;參數(shù)如果為 false,當(dāng)不存在會話時會返回 null。如果存在 sessionId 且合法,就會根據(jù)這個 sessionId 找到對應(yīng)的 HttpSession 對象并返回 |
Cookie[] getCookies() |
返回一個數(shù)組,包含客戶端發(fā)送請求時的所有 Cookie 對象,會自動把 Cookie 中的格式解析成鍵值對 |
HttpServletResponse 類中的相關(guān)方法
方法 | 描述 |
---|---|
void addCookie(Cookie cookie) |
把指定的 cookie 添加到響應(yīng)中 |
HttpSession 類中的相關(guān)方法
- HttpSession是 Java平臺對 session 機制的實現(xiàn)規(guī)范,因為它僅僅是個接口,具體實現(xiàn)為每個 web 應(yīng)用服務(wù)器的提供商。
- 服務(wù)器會為每一個用戶創(chuàng)建一個獨立的 HttpSession,表示為一個會話,并且一個 HttpSession 對象里包含了多個鍵值對,可以往 HttpSession 中存儲需要的數(shù)據(jù)
方法 | 描述 |
---|---|
Object getAttribute(String name) |
該方法返回在 Session 會話中具有指定名稱的對象,如果沒有指定名稱的對象,則返回 null |
void setAttribute(String name, Object value) |
該方法使用指定的名稱綁定一個對象到該 Session 會話中 |
boolean isNew() |
判定當(dāng)前的會話是否是新創(chuàng)建的 |
Cookie 類中的相關(guān)方法
- 這個類描述了一個 Cookie,通過 Cookie 類創(chuàng)建的對象,每個對象就是一個鍵值對
- HTTP 的 Cookie 字段中實際上存儲的是多個鍵值對,每個鍵值對在 Servlet 中都對應(yīng)一個 Cookie 對象
方法 | 描述 |
---|---|
String getName() |
該方法返回 cookie 的名稱(這個值是 Set-Cookie 字段設(shè)置給瀏覽器的,創(chuàng)建之后不能改變) |
String getValue() |
該方法獲取與 Cookie 關(guān)聯(lián)的值 |
void setValue(String newValue) |
該方法設(shè)置與 Cookie 關(guān)聯(lián)的值 |
8.5 實現(xiàn)用戶登錄功能
接下來將使用上述的 Session 和 Cookie 的相關(guān)方法來實現(xiàn)一個用戶登錄功能,并且可以記錄訪問頁面的次數(shù)
登錄功能實現(xiàn)思路:
- 讀取用戶提交的用戶和密碼
- 對用戶密碼進行校驗
- 判定是否登錄成功
- 創(chuàng)建會話,保存自定義信息
- 重定向到指定頁面
登錄功能實現(xiàn)流程:
-
先實現(xiàn)一個登錄頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登錄頁面</title> </head> <body> <form action="login" method="post"> <input type="text" name="username"> <input type="password" name="password"> <input type="submit" value="登錄"> </form> </body> </html>
-
實現(xiàn)一個 Servlet 來處理上面的登錄請求
由于這里是通過 form 表單來構(gòu)造的 post 請求,那么通過 HttpServletRequest 類中的
getParameter()
方法就能夠獲取請求正文中參數(shù)的值import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 1. 從請求中獲取到用戶名和密碼 String username = req.getParameter("username"); String password = req.getParameter("password"); // 2. 對用戶密碼進行校驗 if(username == null || "".equals(username) || password == null || "".equals(password)){ resp.getWriter().write("<h3>賬號或密碼不能為空!</h3>"); return; } // 3. 判斷是否登錄成功(假設(shè)用戶名為 admin,密碼為 1234。不過賬號密碼應(yīng)該用數(shù)據(jù)庫存儲,這里只是用來測試) if(!username.equals("admin") || !password.equals("1234")){ resp.getWriter().write("<h3>賬號或密碼錯誤!</h3>"); return; } // 4. 登錄成功,創(chuàng)建一個會話,用來記錄當(dāng)前用戶的信息 HttpSession session = req.getSession(true); // 通過這個操作,就給會話中新增了一個程序員自定義的信息,訪問次數(shù) session.setAttribute("visitCount", 0); // 5. 把登錄成功的結(jié)果反饋給客戶端(這里的反饋不是簡單的提示“登錄成功”,而是直接跳轉(zhuǎn)到指定頁面) resp.sendRedirect("index"); } }
-
通過實現(xiàn)一個 Servlet 來表示登錄成功后重定向的頁面
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/index") public class IndexServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 只有登錄成功參數(shù)才能是 true,這里是拿參數(shù),所以要填 false HttpSession session = req.getSession(false); // 判斷當(dāng)前用戶是否登錄 if(session == null){ // 可以提示未登錄,也可以重定向到登錄頁面 // resp.getWriter().write("<h3>登錄為空!</h3>"); resp.sendRedirect("login2.html"); return; } // 表示用戶登錄過,獲取會話中的訪問次數(shù) Integer visitCount = (Integer) session.getAttribute("visitCount"); visitCount += 1; session.setAttribute("visitCount", visitCount); resp.getWriter().write("<h3>visitCount = " + visitCount + "</h3>"); } }
-
到這里為止,一個簡單的用戶登錄功能就實現(xiàn)成功了。效果如下
9. 上傳文件操作
上傳文件是日常開發(fā)中的一類常見需求,在 Servlet 中也進行了支持
9.1 Servlet 中上傳文件的核心方法
HttpServletRequest 類中的相關(guān)方法
方法 | 描述 |
---|---|
Part getPart(String name) |
獲取請求中給定 name 的文件 |
Collection<Part> getParts() |
獲取所有的文件 |
Part 類中的相關(guān)方法
方法 | 描述 |
---|---|
String getSubmittedFileName() |
獲取提交的文件名 |
String getContentType() |
獲取提交的文件類型 |
long getSize() |
獲取文件的大小,單位為字節(jié) |
void write(String path) |
把提交的文件數(shù)據(jù)寫入磁盤文件 |
9.2 上傳文件操作實現(xiàn)
-
先寫一個前端頁面,用于上傳文件
- 上傳文件一般使用 post 請求的表單實現(xiàn)
- 通過 form 表單構(gòu)造上傳文件,要加上一個 enctype 字段,它表示 body 中的數(shù)據(jù)格式,它的默認值為:
x-www-form-urlencoded
,這里要修改成:multipart/form-data
,它是上傳文件或者圖片的數(shù)據(jù)格式 - 第一個 input 中 type 的值為 file,它表示第一個輸入框為文件選擇框,name 的值與后端中通過 getPart 獲取指定文件的操作有關(guān)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>上傳文件</title> </head> <body> <form action="upload" method="post" enctype="multipart/form-data"> <input type="file" name="MyFile"> <input type="submit" value="上傳"> </form> </body> </html>
-
寫一個 Servlet 用于處理上傳的文件
- 上傳文件操作還需要給 Servlet 加上一個
@MultipartConfig
注解,否則服務(wù)器代碼無法使用getPart()
方法 -
getPart()
方法中的參數(shù)和 form 表單input="file"
標(biāo)簽的 name 屬性對應(yīng) - 客戶端一次可以提交多個文件,getPart() 方法根據(jù) name 屬性來獲取不同的文件
- 寫入磁盤文件操作的路徑之間可以使用兩個反斜杠
\\
,也可以使用一個正斜杠/
- 寫入磁盤文件操作的路徑最后為保存后的文件名,包括文件后綴
import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import java.io.IOException; @MultipartConfig @WebServlet("/upload") public class UploadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=utf8"); // 通過 getPart 方法獲取到前端傳來的文件 Part part = req.getPart("MyFile"); // 獲取文件名 String fileName = part.getSubmittedFileName(); System.out.println("文件名為: " + fileName); // 獲取提交的文件類型 String fileType = part.getContentType(); System.out.println("文件類型為: " + fileType); // 獲取文件的大小 long fileSize = part.getSize(); System.out.println("文件大小為: " + fileSize); // 把文件數(shù)據(jù)寫入磁盤文件 part.write("C:\\Users\\bbbbbge\\Desktop\\upload.jpg"); resp.getWriter().write("上傳成功!"); } }
- 上傳文件操作還需要給 Servlet 加上一個
到這里為止,一個簡單的文件上傳操作就實現(xiàn)好了,我們可以通過抓包來觀察下文件上傳操作的請求是怎樣的?
文章來源:http://www.zghlxwxcb.cn/news/detail-703017.html
通過抓包操作我們會發(fā)現(xiàn)幾點問題:文章來源地址http://www.zghlxwxcb.cn/news/detail-703017.html
- 正文的大小和我們上傳文件的大小不同,正文的比上傳的文件的字節(jié)數(shù)略大
- 數(shù)據(jù)類型是
multipart/form-data
沒有問題,但是后面多了一串boundary=----WebKitFormBoundaryAl26z0nbP6JzAUGL
,這個 boundary 在 body 中表示一個分隔線,第一條分割線下面是上傳的文件的屬性和文件的內(nèi)容,當(dāng)文件的內(nèi)容結(jié)束時還有第二條分割線 - 由于有這個分割線和文件的一些屬性,因此使得請求中正文的大小比上傳的文件的內(nèi)容略大
到了這里,關(guān)于【Servlet】Servlet 詳解(使用+原理)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!