JavaWeb 學習總結

目錄

一、基本概念

1.1 WEB開發相關知識

​ web,網頁的意思;用於便是伺服器主機上供外界訪問的資源

​ Internet上供外界訪問的web資源分為

  • 靜態 web:指web頁面中供人們瀏覽的數據始終是不變的
    • 靜態web開發技術:HTML
  • 動態 web:指 web 頁面中供人們瀏覽的數據是由程式產生的,不同時間點訪問web頁面看到的內容各不相同
    • 常用動態web開發技術:JSP/Servlet、asp、php

在java中,動態web資源開發技術統稱為JavaWeb

1.2 WEB 應用程式

​ web應用程式(web應用)指供瀏覽器訪問的程式,例如有多個 .html web資源,這多個web資源放在一個目錄中,並對外提供服務,這就組成了一個web應用

​ 一個web由多個靜態 web 資源和動態 web 資源組成,如:html、css、js 文件、jsp、文件、java程式、支援jar包、配置文件等

Web應用開發好後,若想提供外界訪問,需要把web應用所在的目錄交給 web 伺服器管理(如Tomcat),這個過程稱為虛擬目錄映射

1.3 靜態 web

​ *.html 是網頁源文件,直接訪問伺服器上的這些內容,可以獲取網頁上的資訊;整個靜態web操作的過程如下:

image-20200409183123296

​ 訪問靜態web程式的過程中,用戶使用客戶端瀏覽器(Chrome、FireFox等),經過網路(NetWord)連接到伺服器上,使用HTTP協議發送一個請求(Request),伺服器接收到來自客戶端的請求,告知 WEB 伺服器客戶端要所要請求的頁面, WEB 伺服器接收到所有請求後,根據用戶的需要,從文件系統(存放所有靜態頁面的磁碟)去除內容,通過 WEB 伺服器返回給客戶端,客戶端接收到內容之後經過瀏覽器渲染解析,得到顯示的效果。

靜態 WEB 的缺點:

  1. Web頁面中的內容無法動態更新,所有用戶每個時刻訪問看見的內容都是一樣的

    靜態 web 也可以加上一些動態的特效,使用JavaScript 或 VBScript(微軟拿來對標js的),但是這些特效都是基於瀏覽器實現的,就像css設置網頁樣式一樣,js設置一些特效、行為,所以在伺服器上沒有任何變化(可以看出區分動態和靜態web並不是看網頁動或者不動

  2. 無法連接資料庫(數據無法持久化),無法實現和用戶的交互

1.4 動態 web

​ 同樣,動態 web 並不是至頁面會動,主要的特性是:web 頁面的展示效果因人而異(如:淘寶的『千人千面』),且動態 web 具有交互性,其頁面內容可以動態更新(靜態網站內容發生變化往往有一個『更新首頁』的功能,手動更新web頁面),動態 web 的操作過程如下:

image-20200409195916875

​ 同靜態 web 一樣,使用客戶端瀏覽器通過網路連接伺服器,並通過HTTP協議發送請求(Request),不同的是所有請求都先經過一個WEB Server Plugin(伺服器插件)來處理,區分請求的是靜態資源(*.html)還是 動態資源。

​ 靜態資源直接交給 web 伺服器,之後web伺服器從文件系統中取出內容,發送回客戶端;

​ 而 動態資源(*.jsp、*.asp、*.php),會先將請求轉交給 web Container(web 容器),在 web Container 中連接資料庫,在資料庫中取出數據等一系列操作後 動態拼接頁面的展示內容,然後將展示內容交給 web 伺服器,響應返回給客戶端瀏覽器解析執行。

動態 web 缺點:假如伺服器的動態web資源出現了錯誤,就需要重新編寫後台程式(java來說就是servlet),重新發布,即 停機維護

​ 優點就是彌補了靜態 web 的不足

動態 web 的實現方式:

  • ASP、ASP.NET

    ​ 出自微軟,asp已淘汰,ASP.NET效率好,但受限於平台,C#上用得多

  • PHP

    ​ 世界上最好的程式語言…

  • JAVA Servlet/JSP

    ​ B/S架構的語言,不受平台約束,支援多執行緒處理方式

    B/S 為瀏覽器和伺服器 ; C/S 為 客戶端和伺服器

二、WEB伺服器

1.1 簡介

​ Web 伺服器是指連接在Internet上的某種類型的電腦程式(給向其發送請求的瀏覽器提供文檔的程式),伺服器處理該請求並將文件回饋到該瀏覽器上,附帶的資訊(響應頭)告訴瀏覽器如何查看該文件(文件類型、字符集等)。

image-20200409203856151

​ 伺服器是一種波動程式,只有當Internet上運行在其他電腦中的瀏覽器發出請求時,伺服器才會響應。

image-20200409203905212

2.2 Web 伺服器

​ 常見的 IIS 是Windows自帶的,而 Tomcat 伺服器是實現了 Java EE 的最小的 web 伺服器,性能穩定,開源免費,JavaWeb良品

不管什麼 web 資源,想要被遠程電腦訪問,都必須有一個與之對應的網路通訊程式,當用戶來訪問時,這個網路通訊程式讀取 web 資源數據,並返回給來訪者

​ 而Tomcat(web伺服器)就是這樣一個程式,負責完成底層的網路通訊,開發者只用關注web資源如何寫

image-20200409205539036

三、Tomcat伺服器

1.1 埠的配置

​ Tomcat 的所有配置都放在 conf 文件夾之中,裡面的 server.xml 文件是配置的核心文件。

​ 如果想修改 Tomcat 伺服器的啟動埠,則可以在 server.xml 配置文件中的 Connector 節點進行的埠修改。

埠修改後重啟 Tomcat 生效

1.2 虛擬目錄的映射方式

  1. 讓Tomcat伺服器自動映射

    ​ Tomcat 伺服器會自動管理 webapps 目錄下的所有web應用,並映射成虛擬目錄,即webapps中的 web 應用,外界可以直接訪問;

    ​ 所以可以直接將一個web應用整個地 複製 到 webapps 目錄下,然後通過對應的路徑去訪問此web應用。

    修改配置後需要重啟Tomcat伺服器

  2. 在 servlet.xml 文件的 元素中進行配置

1 <Host name="localhost"  appBase="webapps"
2              unpackWARs="true" autoDeploy="true"
3              xmlValidation="false" xmlNamespaceAware="false">
    <!--下面這條就是配置的內容-->
5          <Context path="/servlet" docBase="D:\JavaWeb\servlet" />
6  </Host>

​ Context 表示上下文,代表整個 JavaWeb 應用

​ path:用來配置虛擬目錄,就是瀏覽器地址欄中輸入的訪問地址,必須以 ‘ / ‘ 開頭

​ docBase:配置虛擬目錄對應的硬碟上的web應用資源的路徑

這裡的path 和 docBase 與 Java 中的 Servlet程式 配置web.xml 訪問類似

  1. 在 tomcat 伺服器的 \ conf\Catalina\localhost 目錄下新建一個以 .xml 文件,文件名就為虛擬目錄(即上面一種方式的 path),然後只用在xml文件中配置 docBase 即可實現映射:(xml文件名為 path ,其中配置 docBase)
<Context docBase="D:\JavaWeb\servlet" />

此種方式修改配置文件後不用重啟Tomcat伺服器

1.3 配置虛擬主機

​ 一個主機只能放一個網站,配置多個虛擬主機就可以放多個網站

​ Tomcat 配置虛擬主機:修改 conf文件夾 –> server.xml 配置文件,使用元素進行配置,一對 host 表示一個虛擬主機(就像配置多個Servlet程式一樣)

<Host name="www.bibi.com" appBase="D:\">
</Host>

​ 其中 name 此 host 代表的主機,這台主機管理著 appBase 目錄下所有的 web 應用(即 JavaWeb 目錄並不是代表一個web項目的根目錄,而是一個存放的一個或多個web應用的文件下,就像Tomcat伺服器的 webapps 目錄一樣,下面可以放多個web應用)

1.4 Windows 系統中註冊域名

​ 配置的主機(網站)要想通過域名被外部訪問,必須在DNS伺服器 或 Windows系統中註冊訪問網站時用的域名,即修改 hosts 文件:”C:\Windows\System32\drivers\etc

image-20200409222603350

​ 編輯此文件,將想要設置域名和IP綁定在一起,如下就可以通過 //web:8080 去訪問web應用了(埠根據自己設置的)

image-20200409222759409

1.5 打包JavaWeb應用

​ 開發完一個 JavaWeb應用後,使用 jar 命令將其打包成一個 war 包(java的包為 jar,JavaWeb為war),用法:

image-20200410080510423

​ 而後將 war 包放到 Tomcat 伺服器的 webapps 目錄下,運行伺服器,Tomcat 伺服器在部署 web 應用時會將此 war 包進行解壓,生成 web 應用的文件夾

1.6 Tomcat 體系結構

image-20200410081107263

​ Tomcat 伺服器的啟動時基於 servlet.xml 配置文件,Tomcat 啟動時會先啟動一個 Server ,Server 會啟動 Service(所有請求響應操作都會經過它),Service 裡面啟動多個 Connector(連接器),連接器不處理請求,負責將用戶請求交給 Engine(引擎),由 Engine 解析請求,並將請求交給用戶要訪問的 Host ,Host 也解析請求,查看用戶要訪問主機下的哪個 web 應用,一個 Context(上下文)代表一個 web 應用

servlet.xml 中的配置資訊:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 伺服器 -->
<Server port="8005" shutdown="SHUTDOWN">
<!-- 服務 -->
  <Service name="Catalina">
	<!-- HTTP協議的連接器 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
	<!-- HTTP11 應該就是HTTPS協議的連接器 -->
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
               maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS" 
                keystoreFile="conf/.keystore" keystorePass="123456"/>
	<!-- AJP協議的連接器 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
	<!-- 引擎 -->
    <Engine name="Catalina" defaultHost="localhost">
		<!-- Host 本地主機 -->
       <Host name="localhost"  appBase="webapps"
             unpackWARs="true" autoDeploy="true">
         <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                prefix="localhost_access_log." suffix=".txt"
                pattern="%h %l %u %t "%r" %s %b" />
       </Host>
	   <!-- Host 增加的虛擬主機 -->
       <Host name="web" appBase="D:\servlet\hello">
			<!-- web 應用(上下文對象就代表一個web應用) -->
         <Context path="" docBase="D:\servlet\hello"/>
       </Host>
     </Engine>
   </Service>
 </Server>

1.7 互聯網上的加密原理

​ Tomcat 伺服器啟動時會啟動多個 Connector(連接器),而 Tomcat 伺服器的連接器分為密連接器和非加密連接器

加密方式:

  1. 對稱加密(密鑰加密)

    ​ 採用單秘鑰的加密方式,傳輸數據的加密方和解密數據的接收方使用統一秘鑰進行加解密,所以如何把秘鑰安全地傳遞到解密者手上至關重要

    ​ 常用的對稱加密有:DES、IDEA、RC2、RC4、SKIPJACK、RC5、AES 演算法等

  2. 非對稱加密

    ​ 非對稱加密需要兩個秘鑰:」公共秘鑰「 和 」私有秘鑰「,公鑰和私鑰是一對,如果用公鑰進行加密,那麼只有對應的私鑰才能解密,如果用私鑰進行加密,也只有對應的公鑰才能解密

    ​ 由於加密和解密使用的是兩個不用的秘鑰,所以稱為非對稱秘鑰,實現機密資訊交換的基本過程為:甲方生成一對秘鑰,並將其中一個作為公共秘鑰對外開放,得到該公鑰的乙方,使用此秘鑰對要加密的資訊進行加密,而後傳遞給甲方,甲方接收到被公鑰加密的數據後,使用自己的另一個秘鑰,即私鑰進行解密;同樣的,甲方可以使用乙方的公共秘鑰,對數據進行加密簽名,而後發送給乙方,乙方使用自己的私鑰進行解密驗簽;

    ​ 簡單說就是發送方使用接收方的公鑰進行加密,並發送給接收方,接收方收到數據後使用自己的私鑰進行解密;即時數據被第三方截取,由於通過數字的手段的加密是一個不可逆的過程,沒有對應的私鑰也無法解密;

    ​ 但還是存在安全問題:加入A要給B發送數據,那麼B需要生成一對秘鑰,並將公鑰發送給A用來加密數據,但過程中被C截取了,而C也使用B的公鑰加密數據,再發給B,B接收到數據後就暈了,分不清這條數據是A發的還是C發的;另一個問題是:在B給A發送公鑰的過程中,被C截取後,C生成一對秘鑰,將公鑰發送給A,A以為這個就是B的公鑰,就使用此公鑰對資訊進行加密,發送給B,過程中被C截獲,C可以直接用私鑰進行解密,獲取數據,而B收到數據後,使用私鑰確無法解密;

    ​ 所以,非對稱式加密需要確定拿到公鑰一定是自己想要的,解決辦法:一個第三方機構 CA機構(證書授權中心)來擔保,過程:A想給B發送數據,先將公鑰發給 CA機構,CA拿到公鑰後跑到B家裡問:公鑰是不是你發的?(確認公鑰),B確認後 CA 就會給B公鑰做擔保,生成一份數字證書給B,數字證書包含了CA的擔保認證簽名和B的公鑰,B拿到CA的數字認證後,就發給A,A拿到數字證書後,看到上面有CA的簽名,就可以確定當前拿到的是B的公鑰,那麼就可以放心使用公鑰加密數據,而後發給B了

     ### 1.8 HTTPS 連接器
    

​ 了解了加密原理後,看看瀏覽器與伺服器交互過程的加密:瀏覽器想要將加密數據發送給伺服器,伺服器會向瀏覽器發送一個數字證書,而後瀏覽器就使用數字證書中的公鑰對數據進行加密,再發送給伺服器,伺服器接收後使用私鑰進行解密

​ 所以要實現瀏覽器與伺服器的加密傳輸,先需要給伺服器生成一個數字證書,在配置一下伺服器,讓伺服器收到瀏覽器的請求後,會向瀏覽器出示它的數字證書

  1. 生成 Tomcat 伺服器的數字證書

    1. jdk的bin目錄下有一個keytool.ext 程式就是java提供的製作數字證書的工具

      使用 keytool 生成一個名字為 tomcat 的數字證書,存放在 .keystore 這個秘鑰中

      keytool -genkey -alias tomcat -keyalg RSA
      

image-20200410142321188

生成完後作業系統的用戶目錄下就會生成一個 .keystore 文件

image-20200410142422199

使用命令:keytool -list -keystore .keystore 查看 .keystore 秘鑰庫裡面的所有證書

image-20200410142810600

  1. 配置 HTTPS 連接器

    ​ 將生成的. keystore 密鑰庫文件拷貝到 Tomcat 伺服器的 conf 目錄下

    ​ 修改 server.xml 文件,配置 https 連接器,程式碼如下:

    1  <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
    2                maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
    3                clientAuth="false" sslProtocol="TLS" 
    4                keystoreFile="conf/.keystore" keystorePass="123456"/>
    

      在 server.xml 文件中配置了一個埠是 8443 的加密連接器,瀏覽器訪問 8443 埠的連接器時,將會以加密的方式來訪問 web 伺服器,這個連接器收到瀏覽器的請求後,將會向瀏覽器出示一份數字證書,瀏覽器再用數字證書裡面的公鑰來加密數據,keystoreFile=”conf/.keystore” 用來指明密鑰庫文件的所在路徑,伺服器從密鑰庫中提取證書時需要密碼,keystorePass=”123456″ 指明密鑰庫的訪問密碼

      使用 “//localhost:8443/” 訪問 8443 的加密連接器

    image-20200410143244215

    ​ 由於密鑰庫裡面的證書是我們手工生成的,沒有經過 CA 的認證,所以使用 “//localhost:8443/” 訪問 8443 的加密連接器,瀏覽器會出現 “證書錯誤,導航已阻止”,瀏覽器認為當前要訪問的這個主機是不安全的,不推薦繼續訪問,點擊img就可以繼續訪問了,如下圖所示:

    image-20200410143316897

  2. 安裝數字證書

    ​ 為了讓瀏覽器信任我們生成的數字證書,需要將數字證書安裝到瀏覽器中,以 IE8 瀏覽器為例進行證書安裝說明,安裝步驟如下:

    image-20200410143526359

    image-20200410143535359

    image-20200410143555912

    image-20200410143607161

    image-20200410143715092

    image-20200410143741288

    ​ 證書安裝成功後,重啟瀏覽器後就可以正常訪問了

  3. 刪除數字證書

    ​ 以 IE8 為例進行說明,操作步驟如下:工具 —–>Internet 選項

    image-20200410144006980

    ​ 刪除之後重啟瀏覽器即可

四、HTTP協議

4.1 簡介

  1. 什麼是HTTP協議

    ​ HTTP 是 HyperText Transfer Protocol 超文本傳輸協議,是TCP/IP 協議個一個應用層協議,用於定義 web瀏覽器 和 web伺服器 之間交換數據的過程,規定了客戶端與web伺服器之間的通訊格式

  2. HTTP協議的版本

    • HTTP/1.0:客戶端與 web 伺服器建立連接後,只能獲得一個 web 資源

    • HTTP/2.0:允許客戶端與 web 伺服器建立連接後,在一個連接上獲取多個 web 應用

4.2 HTTP 請求

客戶端連上伺服器後,向伺服器請求某個web資源,稱之為客戶端向伺服器發送了一個 HTTP 請求

一個完整的 HTTP 請求包含:一個請求行、若干個消息頭、及實體內容

訪問百度:客戶端 –> 發請求(Request) –> 伺服器

image-20200410162746105

  1. 請求行

    ​ 上面的稱為請求行,可看出請求方式為 GET,請求方式有 POST、GET、HEAD、OPTIONS、DELETE、TRACE、PUT,常用的:GET、POST

    ​ 瀏覽器向伺服器發送請求默認都是 GET 方式,可通過修改表單提交方式改為POST

    ​ 它們都用於向 web 伺服器請求某個 web 資源,區別表現在數據傳遞上:可以在瀏覽器地址欄輸入地址時,在請求的 URL 地址後 加 ?key1=value1&key2=value2... HTTP/1.1表示給伺服器發送數據,其屬性及值在地址欄中可見,所以不能用於傳遞敏感資訊,且URL地址欄後能附帶的參數是有限的,不能1k(不安全,高效)

    ​ POST 方式:可無限制地在請求的實體內容中向伺服器發送數據,且內容不會顯示在URL地址欄中(安全,不高效)

  2. 消息頭

    常用消息頭:

    accept:瀏覽器通過這個頭告訴伺服器,它所支援的數據類型(格式:大文本/小文本 如:text/html)

    Accept-Charset:瀏覽器通過這個頭告訴伺服器它支援哪種字符集

    Accept-Encoding:瀏覽器通過這個頭告訴伺服器,支援的壓縮格式

    Accept-Language:瀏覽器通過這個告訴伺服器,它的語言環境

    Host:瀏覽器通過這個頭告訴伺服器,向訪問哪台主機

    if-Modified-Since:瀏覽器通過這個頭告訴伺服器,快取數據的時間

    Referer:瀏覽器通過這個頭告訴伺服器,客戶機是哪個頁面來的(訪問來源)

    Connection:瀏覽器通過這個頭告訴伺服器,請求完成後是斷開連接還是保持連接

    例如百度的:

    image-20200410165645730

Upgrade-Insecure-Requests:客戶端向伺服器端發送訊號表示它支援 upgrade-insecure-requests 的升級機制

User-Agent:識別發起請求的用戶代理軟體的應用類型、作業系統、軟體開發商以及版本號

4.3 HTTP 響應

  1. 一個 HTTP 響應代表伺服器向客戶端回送數據,包括:一個狀態行、若干個消息頭、以及實體內容

image-20200410172906263

image-20200410172607318

  1. 狀態行

    ​ 包含:HTTP 版本號、狀態碼、原因敘述

    狀態碼用於表示伺服器對請求的處理結果,分為 5 類:

    image-20200410173218964

  2. 常用響應頭(消息頭)

    Location:伺服器通過這個頭,告訴瀏覽器跳到哪裡

    Server:伺服器通過這個頭,告訴瀏覽器伺服器的型號

    Content-Encoding:伺服器通過這個頭,告訴瀏覽器數據的壓縮格式

    Content-Length:伺服器通過這個頭,告訴瀏覽器回送數據的長度

    Content-Language:伺服器通過這個頭,告訴瀏覽器語言環境

    Content-Type:伺服器通過這個頭,告訴瀏覽器回送數據的類型

    Refresh:伺服器通過這個頭,告訴瀏覽器定時刷新

    Content-Dispostion:伺服器通過這個頭,告訴瀏覽器以下載的方式打開數據

    Transfer-Encoding:伺服器通過這個頭,告訴瀏覽器數據是以分塊的形式回送

    Expires:-1 以下為 控制瀏覽器不要快取

    Cache-Control:no-cache

    Pragma:no-cache

4.4 在服務端設置響應頭來控制客戶端瀏覽器的行為

  1. 設置 Location 響應頭,實現請求重定向
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** 重定向
 * @author YH
 * @create 2020-04-10 18:24
 */
public class Location extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //    通過響應頭能設置一系列的瀏覽器行為
        //通過響應對象:設置狀態行 的 狀態碼 實現重定向(狀態碼位於狀態行,不是響應頭,所以設置方法與響應頭不同)
        resp.setStatus(302);
        //通過響應對象:設置響應頭告訴瀏覽器跳轉到哪裡
        //在這裡使用 / 代表webapps目錄,所以不能使用相對路徑在當前的web應用,即request
        resp.setHeader("location","location.html");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  1. 設置 Content-Encoding 響應頭,告訴瀏覽器數據的壓縮格式
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;

/**
 * @author YH
 * 這個小程式用於演示:
 *  1.使用gzipOutputStream流佬壓縮數據
 *  2.設置響應頭Content-Encoding來告訴瀏覽器,伺服器發送回去的數據壓縮後的格式
 * @create 2020-04-10 18:58
 */
public class Encoding extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String data = null;
        for (int i = 0; i < 5000; i++) {
            data += "abcdefghijk";
        }
        System.out.println("原始數據大小為:" + data.getBytes().length);
		
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        GZIPOutputStream gout = new GZIPOutputStream(bout);
        //將資源寫入輸入流
        gout.write(data.getBytes());
        //gout流資源如果不關閉,就會佔用bout流,使得下面也無法獲取bout的數據
        gout.close();
        //得到壓縮後的數據
        byte[] g = bout.toByteArray();
//通過響應對象:設置Content-Encoding響應頭
        resp.setHeader("Content-Encoding","gzip");
        resp.setHeader("Content-Length",String.valueOf(g.length));
        //將資源寫入網頁
        resp.getOutputStream().write(g);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  1. 設置 Content-type 響應頭,指定回送數據的類型
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;

/** 設置 Content-type 響應頭,指定回送數據的類型
 * @author YH
 * @create 2020-04-10 19:12
 */
public class Content_type extends HttpServlet {
      /**
      * 瀏覽器能接收(Accept)的數據類型有:
      * application/x-ms-application,
      * image/jpeg,
      * application/xaml+xml,
      * image/gif,
      * image/pjpeg,
      * application/x-ms-xbap,
      * application/vnd.ms-excel,
      * application/vnd.ms-powerpoint,
      * application/msword,
      */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        通過響應頭,能設置一系列的瀏覽器的行為
        //通過響應對象:設置 Content-type響應頭,指定回送數據的類型
        //回送圖片
        resp.setHeader("Content-type","image/jpeg");

        //讀取圖片(通過java操作外部資源通常都需要進行IO讀/寫操作,即讓java現先獲得資源,再去使用資源)
        //通過獲取web應用的Context上下文對象,獲取整個web應用的資源,將web根目錄下的png圖片資源轉為輸入流
        InputStream inputStream = this.getServletContext().getResourceAsStream("web技術體系.png");

        //ServletOutputStream 底層是 OutputBuffer 底層有時間詳細了解下-----------------
        ServletOutputStream outputStream = resp.getOutputStream();

        //設置快取
        int len = 0;
        byte[] buffer = new byte[1024];
        while((len = inputStream.read(buffer)) > 0){
            outputStream.write(buffer,0,len);
        }

//        resp.setHeader("Content-type","text/plain;charset=utf-8"); //同時設置文本字符集
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  1. 設置 Refresh 響應頭,讓瀏覽器定時刷新
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** 設置 Refresh 響應頭,讓瀏覽器定時刷新
 * @author YH
 * @create 2020-04-10 19:28
 */
public class Refresh extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        通過響應頭,可以設置一系列的瀏覽器行為
        //通過響應對象:設置 Refresh 響應頭, 2 秒刷新一次
//        resp.setHeader("Refresh","2");
        //設置 Refresh 響應頭,讓瀏覽器每隔三秒跳轉到 百度(類似登陸後隔幾秒跳轉)
        resp.setHeader("Refresh","3;url=//www.baidu.com");
        resp.getWriter().write("hhhhhhhhhh");
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

  1. 設置 Content-Dispostion 響應頭,讓瀏覽器下載文件
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;

/** 設置 Content-Disposition 響應頭,讓瀏覽器下載文件
 * @author YH
 * @create 2020-04-10 19:40
 */
public class Disposition extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("執行到這裡");

        //        通過響應頭,可以設置一系列瀏覽器的行為
//        下載網頁上的內容需要幾個步驟:1、文在文件的路徑 2、根據路徑獲取文件名 3、最後通過響應頭設置要下載的文件名
        //獲取將被下載的文件路徑
        String filePath = "D:\\javacode\\javaweb_Servlet2\\response\\src\\main\\webapp\\哈哈11.xml";
//        通過一些方法,從路徑中提取出 文件名:
        //通過獲取最後一個 / 的腳標 + 1 確定文件名的第一個字元索引,而後從此索引開始截取字元串,就是文件名
        String fileName = filePath.substring(filePath.lastIndexOf("\\") + 1);
        //通過響應對象:設置 content-disposition 響應頭; URLEncoder.encode() 方法用於將字元串轉換成特定編碼的
        resp.setHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode(fileName,"utf-8"));
//        如果只有上面這些步驟,那麼下載到的回事空文件;還需要將文件內的數據,以Stream流的形式寫入文件中
        //創建文件輸入流,讀取下載文件的數據(注意:java要操作任何外部資源都要先獲取(讀 ),才能進行其他操作(寫))
        //注意此處的文件流傳遞的參數為 文件路徑
        FileInputStream fileInputStream = new FileInputStream(filePath);

        //獲取響應對象的輸出流,用於將下載文件數據寫入下載文件內
        ServletOutputStream outputStream = resp.getOutputStream();
        int len;
        byte[] buffer = new byte[1024];
        //將文件輸入流中的數據讀取快取數組中
        while((len = fileInputStream.read(buffer)) > 0){
            outputStream.write(buffer,0,len);
        }
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

五、瀏覽器與伺服器交互

5.1 交互過程(訪問網站的過程)

image-20200409232149609

​ 當在瀏覽器中輸入URL地址 //web:8080/servlet/hello.jsp 訪問伺服器上的 hello.jsp 這個web資源的過程中進行的操作:

  1. 瀏覽器根據主機名「web」 去作業系統的 Host 文件中查找主機名對應的IP地址
  2. 如果在 Host 文件中沒有找到相對應的 IP 地址,就去互聯網上的DNS域名伺服器上查找是否有「web」這台主機對應的 IP
  3. 瀏覽器查找到 」web「這台主機對應的IP地址後,就使用 IP 地址鏈接上 WEB 伺服器(沒找到返回」無法訪問此網站「提示)
  4. 瀏覽器連接到web伺服器後,使用HTTP協議向伺服器發送請求,同時向 web 伺服器以 Stream流 的形式傳輸數據,告訴 web 伺服器要訪問伺服器裡面的哪個 web應用(servlet) 下的 web資源 (hello.jsp)
  5. 伺服器就收到瀏覽器傳輸的數據(請求行、請求頭)後,開始等待 web伺服器 將瀏覽器要訪問的 web資源 傳輸給它
  6. 伺服器接收到瀏覽器傳輸的數據後,開始解析接收到的數據,伺服器解析 “GET /servlet/hello.jsp HTTP/1.1” 裡面的內容時知道客戶端瀏覽器要訪問的是 **servlet應用裡面的 heelo.jsp 這個 Web 資源,然後伺服器就去讀取 hello.jsp 這個 Web 資源裡面的內容,將讀到的內容再以 Stream(流) 的形式傳輸給瀏覽器
  7. 瀏覽器拿到伺服器傳輸給它的數據之後,就可以顯示在瀏覽器內給用戶看,這就是瀏覽器和伺服器的交互過程。

5.2 JavaWeb應用的組成結構

  開發 JavaWeb 應用時,不同類型的文件有嚴格的存放規則,否則不僅可能會使 web 應用無法訪問,還會導致 web 伺服器啟動報錯

image-20200409234440313

  WebRoot :Web 應用所在目錄,一般情況下虛擬目錄要配置到此文件夾當中

    ┝WEB-INF:此文件夾必須位於 WebRoot 文件夾裡面,而且必須以這樣的形式去命名,字母都要大寫

      ┝web.xml:配置文件,有格式要求,此文件必須以這樣的形式去命名,並且必須放置到 WEB-INF 文件夾中

​ web.xml 的格式可在Tomcat 目錄下的 webapps\ROOT\WEB-INF 這個目錄下的 web.xml 文件中的格式為模板,往往是最新的配置版本,如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="//xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="//xmlns.jcp.org/xml/ns/javaee
                      //xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0"
  metadata-complete="true">

</web-app>

​ 這就是 web.xml 文件的格式

六、Maven

​ Maven 翻譯為”專家”、”內行”,是 Apache 下的一個純 Java 開發的開源項目。基於項目對象模型(縮寫:POM)概念,Maven利用一個中央資訊片斷能管理一個項目的構建、報告和文檔等步驟。

Maven 是一個項目管理工具,可以對 Java 項目進行構建、依賴管理

​ Maven 使用約定大於配置的原則,所以要遵守一定的目錄結構

image-20200411081850890

Maven 下載地址://maven.apache.org/download.cgi

​ 需要配置環境變數

6.1 Maven POM

​ POM(Project Object Model,項目對象模型)是 Maven 工程的基本工作單元(核心):pom.xml 文件,包含了項目的基本資訊,基礎配置:

<project xmlns = "//maven.apache.org/POM/4.0.0"
    xmlns:xsi = "//www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation = "//maven.apache.org/POM/4.0.0
    //maven.apache.org/xsd/maven-4.0.0.xsd">
 
    <!-- 模型版本 -->
    <modelVersion>4.0.0</modelVersion>
    <!-- 公司或者組織的唯一標誌,並且配置時生成的路徑也是由此生成, 如com.companyname.project-group,maven會將該項目打成的jar包放本地路徑:/com/companyname/project-group -->
    <groupId>com.companyname.project-group</groupId>
 
    <!-- 項目的唯一ID,一個groupId下面可能多個項目,就是靠artifactId來區分的 -->
    <artifactId>project</artifactId>
 
    <!-- 版本號 -->
    <version>1.0</version>
</project>

添加依賴:

<!--該元素描述了項目相關的所有依賴。 這些依賴組成了項目構建過程中的一個個環節。它們自動從項目定義的倉庫中下載。要獲取更多資訊,請看項目依賴機制。 -->
<dependencies>
    <dependency>
        ......
    </dependency>
</dependencies>

Maven 倉庫://mvnrepository.com/

6.2 配置阿里雲鏡像

  1. 打開Maven根目錄下的conf文件下的 settings.xml

image-20200411091418495

  1. 配置鏡像,加速下載依賴等速度,在 mirrors 標籤內配置如下:
<mirror>
    <id>aliyunmaven</id>
    <mirrorOf>*</mirrorOf>
    <name>阿里雲公共倉庫</name>
    <url>//maven.aliyun.com/repository/public</url>
</mirror>

6.3 本地倉庫

在 localRepository 標籤內進行配置如下(地址為本地倉庫路徑):

<localRepository>D:\java_JDK\apache-maven-3.6.3\maven-repo</localRepository>

配置成功會在指定路徑下生成一個文件夾,如下:

image-20200411092212461

6.4 在IDEA下使用Maven創建web項目

image-20200411093329592

image-20200411094022531

image-20200411095654341

​ 創建一個Module作為web應用:

image-20200411095909300

image-20200411100232615

image-20200411102053084

創建完成,可以看到目錄下自動配置了webapps目錄即WEB-INF文件:

image-20200411102212449

由於創建web時會進行一系列初始化操作,下載一些依賴,需要稍作等待,特別是第一時間較長,看見控制台如下所示,表示下載完畢:

image-20200411102509478

web.xml也進行了自動配置:

​ 需要注意的是:自動配置時可能使用的是IDEA默認的配置,需要手動改一下(一勞永逸的辦法是修改默認的配置,方法在後面創建完項目後介紹)

七、Servlet 開發

7.1 Servlet調用流程圖(生命周期)

總的五大步驟:

image-20200430101922476

載入

初始化:init() ,默認在 servlet 被載入時並實例化後執行

服務:service() ,最終具體體現在 doGet()/doPost() 兩個方法

銷毀:destroy(),servlet 被系統回收時執行

卸載

具體流程:

Servlet調用流程圖

Servlet 容器部分:

image-20200428144113035

小結:   

  1. Servlet何時創建
  默認第一次訪問servlet時創建該對象(調用init()方法,設置 屬性可修改為 Tomcat啟動時執行 init() 方法)
  2. Servlet何時銷毀
伺服器關閉servlet就銷毀了(調用destroy()方法)
  3. 每次訪問必須執行的方法

         public void service(ServletRequest req, ServletResponse resp)

7.2 IDEA中開發Servlet

​ Servlet 介面有兩個默認實現類:GenericServlet、HttpServlet

​ HttpServlet 重寫了 service 方法,且方法體內的程式碼會自動判斷用戶的請求方式,GET 調用 doGET(),POST調用 doPOST() 方法,因此我們只需重寫 doGET() 和 doPOST() 方法,不用去重寫 service()。

創建方式:

image-20200411165554820

然後設置名字即可

​ 但這裡不過多贅述,實際還是使用 Maven ,主要關注點應放在配置上。

7.2 Servlet 開發注意項

  1. Servlet 訪問 URL 映射配置

    ​ 每一個 servlet 就是一個 Servlet程式,都需要在 web.xml 中進行配置,如:

創建一個讓瀏覽器定時刷新的 servlet 程式:

public class Refresh extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        通過響應頭,可以設置一系列的瀏覽器行為
        //通過響應對象:設置 Refresh 響應頭, 2 秒刷新一次
//        resp.setHeader("Refresh","2");
        //設置 Refresh 響應頭,讓瀏覽器每隔三秒跳轉到 百度(類似登陸後隔幾秒跳轉)
        resp.setHeader("Refresh","3;url=//www.baidu.com");
        resp.getWriter().write("hhhhhhhhhh");
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

就需要到 WEB-INF 目錄下的 web.xml 配置文件添加配置:

image-20200411170347114

<servlet>
    <servlet-name>Refresh</servlet-name>
    <servlet-class>com.zuh8.servlet.Refresh</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh</url-pattern>
</servlet-mapping>

是一對標籤,共同給此 web 應用配置資訊

  • 兩個 必須相同,常用定義的類名

  • 用於指定此 web 程式的全類名

  • 表示UTL訪問路徑 “/” 代表當前 web 應用的根路徑

    ​ 進行訪問:

    image-20200411171129556

當前 web 應用的根路徑為 localhost:8080/response/,此時這個 servlet 程式就被訪問了

  • 同一個 servlet 程式,可以被映射到多個 URL 上,即多個 元素的
<servlet>
    <servlet-name>Refresh</servlet-name>
    <servlet-class>com.zuh8.servlet.Refresh</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh222</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh333</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh444</url-pattern>
  </servlet-mapping>

此時就可以通過多個路徑訪問這一個 servlet 程式了:

​ localhost:8080/response/refresh

​ localhost:8080/response/refresh222

​ localhost:8080/response/refresh333

​ localhost:8080/response/refresh444

  • Servlet 訪問 URL 使用 * 通配符映射 

在 Servlet 映射到的 URL 中也可以使用 * 通配符,但是只能有兩種固定的格式:一種格式是 “*. 擴展名”,另一種格式是以正斜杠(/)開頭並以 “/*” 結尾。例如:

image-20200411172506926

當輸入的路徑符合多個匹配規則時,「誰長得更像就找誰」

對於 servlet 來說構建路徑(src 目錄)、webapps/WebContent 都屬於根目錄

web.xml 中 / 代表 web 應用根目錄(//localhost:8888/ServletDemo/)

jsp 中 / 代表伺服器根路徑//localhost:8888/)

  1. Servlet 類與普通 Java 類的區別

    ​ Servlet 是一個供其他 Java 程式(Servlet引擎)調用的類,不能獨立運行,完全由 Servlet 引擎來控制和調度。

    ​ 針對客戶端的多次 Servlet 請求,伺服器只會創建創建一個 servlet 實例對象,即首次收到 Servlet 請求後,創建的 servlet 對象會駐留在記憶體中,為後繼其他請求服務,直至 web 容器(Tomcat)退出,servlet 實例才會被銷毀。

    ​ 在 Servlet 整個生命周期內,Servlet 的 init() 方法只會被調用一次;而對一個 Servlet 的每次訪問請求都會導致 servlet 的 service() 被調用,對於每次訪問請求 Servlet 引擎都會創建一個新的 HttpServeltRequest 請求對象(包括請求資訊) 和一個新的 HTTPServletResponse 響應對象(目前不存在響應資訊,空的),然後將這兩個對象作為參數傳遞給 service() 方法,service 方法再根據請求的方式,調用 doXXX() 方法(也就是我們重寫的方法)。

    ​ 如果在 web.xml 中的 元素內配置一個 元素,那麼 web 應用程式在啟動時就會裝載並創建 Servlet 的實例對象、以及調用 Servlet 實例對象的 init() 方法(正常的 servlet 是通過配置的 URL 訪問,並向 Servlet 發送請求才會觸發引擎去創建 servlet 實例的),配置舉例:

    <servlet>
        <servlet-name>invoker</servlet-name>
        <servlet-class>
            org.apache.catalina.servlets.InvokerServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    

    用途:為 web 應用寫一個 initServlet(進行一些初始操作的程式),這個 servlet 程式在 web 應用啟動時裝載創建,可用來給整個 web 應用創建愛必要的資料庫表和數據等。

  2. 預設 Servlet

    ​ 如果某個 Servlet 的映射路徑為一個證斜杠(/),那麼這個 servlet 就成為當前 web 應用的預設 servlet。

    ​ 凡是

    訪問存在的 web 資源,最終都是訪問到預設 servlet 效果:

    public class Default extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("我是預設 servlet 我被進入了");
            resp.getWriter().print("Hi baby!");
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            this.doGet(req, resp);
        }
    }
    

image-20200411191429215

控制台輸出:

image-20200411191645172

不僅如此!通過 URL 訪問靜態 HTML 或圖片時,實際上也是在訪問 Servlet

訪問 web 應用下的靜態 HTML 和 圖片,示例:

image-20200411192556588

結果就是一頓亂入:

image-20200411192705754

​ 那麼如果我們沒有定義預設 servlet 程式,訪問靜態頁面時可以不用調用預設 servlet 了嗎?其實在 Tomcat 安裝目錄下的 conf/web.xml 文件中,註冊了一個名稱為 org.apache.catalina.servlets.DefaultServlet 的 servlet,並將這個 Servlet 設置為了預設 Servlet,摘取如下:

<servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
...
<!-- The mapping for the default servlet -->
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

​ 我嘗試將 Tomcat 的這一段程式碼注釋,然後運行:

場面頓時和諧:

Application Server was not connected before run configuration stop,reason:
javax.management.InstanceNotFoundException:Catalina:type=Server

​ 找不到實例異常,連接失敗了,無論我本地是否再定義預設都不能啟動伺服器了,看來老東家的東西不能亂碰。

  1. Servlet 的執行緒安全問題

    ​ 當多個客戶端並發訪問用一個 servlet 時,web 伺服器會為每一個客戶端的訪問請求創建一個執行緒,並在這個執行緒上調用 servlet 的 service() 方法,因此 service 方法內如果訪問了統一資源的話可能出現執行緒安全問題。

    ​ 執行緒安全問題時出現來多個執行緒同時操作一個數據源的情況下發生的,如下面的情況就不屬於操作同一個數據源:

    @Override
    public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
        int i = 0;
        i++;
    }
    

    ​ 多個執行緒並發調用 doGet() 時,i 不會存在執行緒安全的問題,因為 i 在 doget() 中,屬於局部變數,每個執行緒都有一份,不存在共享,如果下面這種情況就會存在執行緒安全問題了:

    public class ServleTest extends HttpServlet {
        int i = 0;
        @Override
        public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
            i++;
        }
    }
    

    ​ 此時 i 屬於本 servlet 程式的全局變數,所有 doGet() 方法共享一個 i ,一旦發生執行緒阻塞,就會出現執行緒安全的情況。

    ​ 而且瀏覽器訪問情況還特殊,使用加鎖(synchronized)的方式實現執行緒同步,如果同時有100個人同時訪問這個 servlet,那麼這100個人就要按照先後順序排隊輪流訪問。

    • Sun公司提供的解決辦法:讓 servlet 去實現一個 SingThreadModel(單執行緒模型) 介面,實現它的 servlet 程式,Servlet 引擎將以單執行緒模式來調用其 service 方法(已過時!!!)

    SingThreadModel 是一個標記介面(沒有定義任何方法和常量的介面),僅用於給對象做標誌,常見的有 Serializable(可序列化標誌介面),Cloneable(對象可被克隆介面)

補充:Servlet API 與源碼解析

自定義 Servlet 類繼承樹:

image-20200430104523463

需要注意 ServletConfig 介面中聲明的的兩個方法:

  • getServletContext():獲取 Servlet 上下文對象

    通過獲取到的 ServletContext 對象,可調用如下方法:

    • getContextPath():相對路徑
    • getRealkPath():絕對路徑
    • get/setAttribute():獲取設置屬性值
    • getInitParamter(String name):在當前 web 容器範圍內,獲取初始化參數(注意與下面一個區分)
  • getInitParameter(String name):在當前 Servlet 範圍內,獲取初始化參數(注意與上面一個區分)

  • Servlet 3.0 方式為註解方式(web.xml 配置方式為 Servlet 2.5,需要注意的是:註解只隸屬於某一個具體的 servlet,因此無法為整個 web 容器設置初始化參數,任需要通過 web.xml 方式設置

7.3 ServletConfig 對象

  1. 在 web.xml 中配置 servlet 元素時,可以使用一個或多個 標籤為 servlet 配置一些初始化參數

如:

<servlet>
    <servlet-name>InitParam</servlet-name>
    <servlet-class>com.zuh8.servlet.InitParam</servlet-class>
    <init-param>
      <param-name>username</param-name>
      <param-value>root</param-value>
    </init-param>
    <init-param>
      <param-name>password</param-name>
      <param-value>123456</param-value>
    </init-param>
    <init-param>
      <param-name>Charset</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
  </servlet>

標籤使用 key-value 的形式配置參數

  1. 通過 ServletConfig 獲取 servlet 的初始化參數

    ​ 當 servlet 配置了初始化參數後,web 容器在創建 servlet 實例對象時,會自動將這些初始化參數封裝到 ServletConfig 對象中,並在調用 servlet 的 init() 方法時自動將 ServletConfig 對象傳遞給 servlet。進而,我們通過 ServletConfig 對象就能獲取到當前 servlet 的初始化參數資訊;

    示例:

    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Enumeration;
    
    /**
     * @author YH
     * @create 2020-04-11 21:30
     */
    public class InitParam extends HttpServlet {
        /**
         * 定義 ServletConfig 對象,用於接收配置的初始化參數
         */
        private ServletConfig config;
    
        /**
         * 當servlet配置了初始化參數後,web容器在創建servlet實例對象時,
         * 會自動將初始化配置封裝到ServletConfig對象中,再調用servlet的
         * init()方法將ServletConfig對象傳遞給servlet
         * 所以我們通過ServletConfig就可以獲取到此servlet的初始化參數資訊
         */
        @Override
        public void init(ServletConfig config) throws ServletException {
            //將 ServletConfig 對象傳遞給此servlet程式
            this.config = config;
        }
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //控制響應回送的數據編碼
            resp.setCharacterEncoding("UTF-8");
            //設置瀏覽器解碼所使用的編碼
            resp.setHeader("Content-type","text/html;UTF-8");
    
            //查詢指定 key(name) 值的參數
            String paramName = config.getInitParameter("username");
    
            //獲取所有初始化參數的 name 集合(key值集合)
            Enumeration<String> initParameter = config.getInitParameterNames();
            //遍歷讀取並查詢其 值(value)
            while(initParameter.hasMoreElements()){
                String s = initParameter.nextElement();
                resp.getWriter().print(s + ":" + config.getInitParameter(s) + "<br/>");
            }
            resp.getWriter().print("單獨查詢:<br/>");
            resp.getWriter().print("username:" + paramName );
        }
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            this.doGet(req, resp);
        }
    }
    

    image-20200412095012779

    有關編碼問題此節點末介紹

    ​ 上面的方式難免有些繁瑣,我們看一下它們的繼承樹:

    image-20200412104842228

    image-20200412104917878

    ​ 原來「爺爺類」里有直接獲取 ServletConfig 對象和 ServletText 對象(下面就要用的)的方法,所以上面獲取 ServletConfig 對象的操作我們可以這樣:

    @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //獲取ServletConfig對象
            ServletConfig config = this.getServletConfig();
            ...
    

7.4 ServletContext 對象

​ Web 容器在啟動時,會為每個 web 應用程式都創建一個 ServletContext 對象,它代表此 web 應用。

​ ServletConfig 對象裡面維護了 ServletContext 對象的引用,所以可以通過 ServletConfig 對象(getServletContext() 方法)獲取 ServletContext 對象。

​ 一個 web 應用中所有的 servlet 共享一個 ServletContext 對象,那麼通過 ServletContext 對象就可以實現各個 servlet 之間的通訊;ServletConfig 也被稱為 context 對象。

應用:

  1. 多個 servlet 通過 ServletContext 實現數據共享

程式碼:

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** ServletContext 數據共享演示:存入數據
 * @author YH
 * @create 2020-04-12 11:06
 */
public class ServletContextSet extends HttpServlet {
    /**
     * 獲取 ServletContext 對象,存入屬性值數據
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //向網頁輸出的內容包含中文,需要解決字符集的問題
        resp.setCharacterEncoding("utf-8");
        resp.setHeader("Content-type","text/html;charset=utf-8");

        ServletContext context = this.getServletContext();
        //Servlet中的屬性值存儲是以鍵值對(key-value)的形式
        context.setAttribute("username1","李雷");
        context.setAttribute("username2","韓梅梅");
        resp.getWriter().print("我向ServletContext中存入數據了!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;

/** ServletContext 數據共享演示:獲取數據
 * @author YH
 * @create 2020-04-12 11:16
 */
public class ServletContextGet extends HttpServlet {
    /**
     * 獲取ServletContext對象,讀取屬性
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //向網頁輸出的內容包含中文,需要解決字符集的問題
        resp.setCharacterEncoding("utf-8");
        resp.setHeader("Content-type","text/html;charset=utf-8");

        ServletContext context = this.getServletContext();

        String username1 = (String)context.getAttribute("username1");
        String username2 = (String)context.getAttribute("username2");
        resp.getWriter().print(username1 + " and " + username2);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

先訪問 用於獲取屬性的 servlet URL為 /get,結果:

image-20200412114443208

說明此時 ServletContext 對象中沒有我們要獲取的屬性,我們再運行負責存入屬性的 servlet 程式,URL為 /set,如下:

image-20200412115945444

再來訪問 /get

image-20200412124244737

至此,一個 servlet 獲取到另一個 servlet 添加的數據,實現的了交互

  1. 獲取 web 應用的初始化參數

程式碼:

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-12 13:54
 */
public class ContextParam extends HttpServlet {
    /**
     * 獲取整個 web 工程的初始化參數
     * */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext context = this.getServletContext();
        String url = context.getInitParameter("url");
        resp.getWriter().print(url);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

web.xml 增加:

<!--作用於整個web工程的初始化參數-->
    <context-param>
        <param-name>url</param-name>
        <param-value>jdbc:mysql://localhost:3306/test</param-value>
    </context-param>

訪問效果:

image-20200412140300911

  1. 用 ServletContext 實現請求轉發

程式碼:

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-12 14:13
 */
public class ServletContextDispatcher1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //  擴展:指定getBytes() 的解碼格式,進行解碼,避免亂碼問題
        resp.getOutputStream().write("---------------dis1---------------".getBytes("utf-8"));
        
        //現獲取請求轉發對象;再實現轉發
        RequestDispatcher requestDispatcher = this.getServletContext().getRequestDispatcher("/dis2");
        requestDispatcher.forward(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-12 14:14
 */
public class ServletContextDispatcher2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        resp.setCharacterEncoding("utf-8");
//        resp.setHeader("content-type","text/html;charset=utf-8");

        //  擴展:指定getBytes() 的解碼格式,進行解碼,避免亂碼問題
        resp.getOutputStream().write("---------------dis2---------------".getBytes("utf-8"));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

訪問 1 的URL ,顯示的確實 2 的 servlet 內容:

image-20200412143921930

  1. 使用 ServletContext 讀取資源文件

程式碼:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Properties;

/**
 * 使用 ServletContext 讀取資源文件
 * @author YH
 * @create 2020-04-12 15:39
 */
public class ServletContextReader extends HttpServlet {
    /**
     * 讀取本地數據要使用流
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setHeader("content-type","text/html;charset=utf-8");

        readConfig1(resp);
        resp.getWriter().print("<hr/>");
        readConfig2(resp);
        resp.getWriter().print("<hr/>");
        readConfig3(resp);
        resp.getWriter().print("<hr/>");
        readConfig4(resp);
    }

    /**
     *
     * @param resp
     * @throws IOException
     */
    private void readConfig1(HttpServletResponse resp) throws IOException {
        //獲取文件路徑(getRealPath()方法獲取的是當前項目的路徑,傳遞的參數是基於此路徑的相對路徑),即獲取相對於整個web應用(ServletContext)的路徑,並通過路徑創建文件輸入流
        String path = this.getServletContext().getRealPath("WEB-INF/classes/db1.properties");

        InputStream in = new FileInputStream(path);
        Properties prop = new Properties();
        //從輸入流中讀取屬性列表(將屬性文件中的數據載入Properties對象)
        prop.load(in);
        function(prop,resp);
    }

    /**
     *
     * @param resp
     */
    private void readConfig2(HttpServletResponse resp) throws IOException {
        //直接將文件資源轉為輸入流的方式
        InputStream in = this.getServletContext().getResourceAsStream("WEB-INF/classes/db2.properties");
        Properties prop = new Properties();
        prop.load(in);
        function(prop,resp);
    }

    /**
     *
     * @param resp
     */
    private void readConfig3(HttpServletResponse resp) throws IOException {
        InputStream in = this.getServletContext().getResourceAsStream("WEB-INF/classes/db3.properties");
        Properties prop = new Properties();
        prop.load(in);
        function(prop,resp);
    }

    /**
     * 讀取src目錄下webapp
     * @param resp
     */
    private void readConfig4(HttpServletResponse resp) throws IOException {
        InputStream in = this.getServletContext().getResourceAsStream("WEB-INF/classes/db4.properties");
        Properties prop = new Properties();
        prop.load(in);
        function(prop,resp);
    }

    /**
     * 抽取上面重複程式碼的部分聲明的方法
     * @param prop
     * @param resp
     */
    private void function(Properties prop,HttpServletResponse resp) throws IOException {
        String driver = prop.getProperty("driver");
        String url = prop.getProperty("url");
        String username = prop.getProperty("username");
        String password = prop.getProperty("password");
        resp.getWriter().println("讀取src目錄下包下的db2屬性文件");
        resp.getWriter().println(
                //設置數據顯示的格式
                MessageFormat.format(
                        "driver={0} , url={1} , username={2} , password={3}",
                        driver,url,username,password));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

效果:

image-20200412201104778

需要注意的是:使用Maven後(我是在Maven上的) web 應用的路徑是基於target 目錄下的 web項目目錄(上面程式碼找路徑坑了我半天,還是要細心點),如圖:

image-20200412205036789

也就是說使用 getRealPath() 方法獲取的是當前 web 工程的根路徑,就是/serveltContext 獲取它下面的資源相對於他來操作(帶參的 getRealPath() 方法傳遞的參數是基於此路徑的相對路徑)

  1. 使用類載入器讀取資源文件

    ​ 通過類獲取類載入器,然後用類載入器調用 getResourceAsStream() 資源裝換流方法,獲取輸入流對象,截取上面例子的部分程式碼來做修改:

    public class ServletContextReader extends HttpServlet {
        private void readConfig2(HttpServletResponse resp) throws IOException {
            //獲取類載入器
            ClassLoader loader = ServletContextReader.class.getClassLoader();
            //用類載入器讀取配置文件
            InputStream in = loader.getResourceAsStream("WEB-INF/classes/db2.properties");
            Properties prop = new Properties();
            prop.load(in);
            function(prop,resp);
        }
    }
    

類載入器讀取資源文件不適合讀取大文件,可能出現 jvm 記憶體溢出的情況。

記一個讀取文件名的方法:

...
public void testMethod(){
    String filePath = this.getServletContext().getRealPath("WEB-INF/classes/db1.properties");
    String fileName = filePath.substring(filePath.lastIndex("\\") + 1);
    /**妙就妙在filePath.lastIndex("\\") + 1
    * 獲取最後一個 \ 符號的索引 +1,正是文件名第一個 "d"的索引
    * 再substring從這個索引截取到末尾就是文件名了
    */
}
...

7.5 客戶端快取 Servlet 的輸出

​ 對於不經常變化的數據,在servlet中可以為其設置合理的快取時間值,以避免瀏覽器頻繁向伺服器發送請求,提升伺服器的性能。例如:

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ServletDemo5 extends HttpServlet {
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			 throws ServletException, IOException {
		 String data = "abcddfwerwesfasfsadf";
		 /**
		  * 設置數據合理的快取時間值,以避免瀏覽器頻繁向伺服器發送請求,提升伺服器的性能
		  * 這裡是將數據的快取時間設置為1天
		  */
		 response.setDateHeader("expires",System.currentTimeMillis() + 24 * 3600 * 1000);
		 response.getOutputStream().write(data.getBytes());
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			 throws ServletException, IOException {

		 this.doGet(request, response);
	}
}

八、HttpServletResponse 對象

image-20200413084004996

​ HTTPServletRequest 對象代表伺服器的響應,封裝了向客戶端發送數據、響應頭、響應狀態碼的方法。

查看 API 相關方法:

  1. 負責向客戶端(瀏覽器)發送數據的方法

image-20200413084923288

分別用於返迴響應中的二進位數據(位元組流)和 正文數據(字元流),只能選擇其一,同時使用會報錯

  1. 負責向客戶端(瀏覽器)發送響應頭的方法

image-20200413085744436

image-20200413085838288

  1. 負責向客戶端(瀏覽器)發送狀態碼的方法

image-20200413090040444

  1. 響應狀態碼的常量

    ​ 當要向客戶端發送響應碼時,應避免直接使用數字,而使用這些常量

    2xx:請求響應都成功(200)

    3xx:請求重定向(302)

    4xx:請求的資源不存在(404)

    5xx:伺服器內部發生錯誤(如程式碼錯誤500、網關錯誤502)

狀態碼 200 對應的常量

image-20200413090210143

狀態碼 302 對應的常量

image-20200413090347171

狀態碼 404 對應的常量

image-20200413090324875

狀態碼 500 對應的常量

image-20200413090443458

8.1 Servlet 中文亂碼問題

​ 查看我的另一篇博文: Servlet 中文亂碼問題解析及詳細解決方法

8.2 HTTPServletResponse常見應用

  1. 使用 OutputStream 向瀏覽器輸出中文數據
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** OutputStream響應輸出字元編碼問題
 * @author YH
 * @create 2020-04-13 9:39
 */
public class ServletOutputStreamTest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        /**
         * 使用位元組輸出流輸出中文字元,需要一個字元轉成位元組的過程
         * getBytes()方法將data字元串解碼為位元組,默認按照本地編碼格式進行解碼(中系統默認GBK)
         * 可傳遞參數指定解碼類型:utf-8,此時就是按照UTF-8字符集進行解碼
         * 那麼此時流中的數據時UTF-8解碼後的數據,響應將返回這些數據,就需要告訴瀏覽器它改用什麼
         * 編碼格式來打開這些數據,怎麼告訴瀏覽器呢? 這就需要設置相關響應頭了
         * setHeader()方法第一個參數指定響應頭,第二個參數設置此響應頭的值(設置了文本類型並指定的打開此文本的編碼格式)
         * 還有一個
         */
        resp.setHeader("Content-type","text/html;charset=utf-8");
        ServletOutputStream outputStream = resp.getOutputStream();

        String data = "中國";
        outputStream.write(data.getBytes("utf-8"));
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

效果:

image-20200413102447802

  1. 使用 PrintWriter 向瀏覽器輸出中文數據
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author YH
 * @create 2020-04-13 15:13
 */
public class PrintWriterTest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String data = "中國";
        //關鍵:涉及中文就涉及編碼問題
        //設置ContentType響應頭資訊,告訴瀏覽器要用utf-8字符集解碼
        resp.setContentType("text/html;charset=utf-8");

        //方式一:設置字元以UTF-8的格式回送個瀏覽器
//        resp.setCharacterEncoding("UTF-8");

        //方式二:通過向網頁傳遞 HTML元素標籤設置字符集
        resp.getWriter().write("<mete http-equiv='content-type' content='text/html;charset=utf-8'/>");

        //輸出到瀏覽器
        PrintWriter writer = resp.getWriter();
        writer.write(data);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

image-20200413152302470

​ 以及方式二向瀏覽器傳遞的 HTML 元素程式碼:

響應體內容:

image-20200413152926174

頁面 HTML 程式碼:

image-20200413153013081

OutputStream 可以輸出任何數據;PrintWriter 只能輸出字元,但也省去了字元轉位元組數組那一步,使用方便些

  1. 開發過程中,無論使用何種輸出方式,瀏覽器能夠顯示出來的只能是字元串形式的數據,也就是說直接輸出數字、字元瀏覽器都不能顯示出來。

  2. 文件下載

    實現文件下載的思路:

      1.獲取要下載的文件的絕對路徑

      2.獲取要下載的文件名

      3.設置content-disposition響應頭控制瀏覽器以下載的形式打開文件

      4.獲取要下載的文件輸入流

      5.創建數據緩衝區

      6.通過response對象獲取OutputStream流

      7.將FileInputStream流寫入到buffer緩衝區

      8.使用OutputStream將緩衝區的數據輸出到客戶端瀏覽器

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;

/**
 * @author YH
 * @create 2020-04-13 16:00
 */
public class WebDownload extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.獲取資源的絕對路徑(getRealPath()獲取的是項目根目錄,傳遞參數就是將參數拼接到獲取的根目錄後)
        String filePath = this.getServletContext().getRealPath("阿里前端工程師必讀手.pdf");
        System.out.println("==" + filePath + "==");
        //2.獲取文件名
        //一個絕妙的獲取文件名的方法,filePath.lastIndexOf("\\") + 1 直接獲取到文件名的首索引
        //URLEncoder.encode設置文件名的編碼格式(解決中文文件名亂碼)
        String fileName = filePath.substring(filePath.lastIndexOf("\\") + 1);
        //3.設置content-disposition響應頭以下載的形式打開文件
        resp.setHeader("content-disposition","attachment;filename=" + URLEncoder.encode(fileName,"UTF-8"));
        //4.獲取要下載的文件輸入流
        FileInputStream fileInputStream = new FileInputStream(filePath);
        //5.創建緩衝區
        int len = 0;
        byte[] buffer = new byte[1024];
        //6.通過響應對象獲取 OutputStream 流
        ServletOutputStream outputStream = resp.getOutputStream();
        //7.將文件輸入流中的數據寫入緩衝區
        while((len = fileInputStream.read(buffer)) > 0){
            //8.將數據寫入響應輸出流
            outputStream.write(buffer,0,len);
        }
        //關閉資源
        fileInputStream.close();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

image-20200413173053100

注意:

在編寫下載文件功能時,要使用 OutputStream 流,避免使用 PrintWriter 流,因為 OutputStream 流是位元組流,可以處理任意類型的數據,而 PrintWriter 流是字元流,只能處理字元數據,如果用字元流處理位元組數據,會導致數據丟失

5.生成隨機圖片驗證碼

​ 生成圖片需要使用一個圖片類:BufferedImage 類

程式碼:

![GIF](../GIF.gifimport javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/** 生成隨機驗證碼隨機
 * @author YH
 * @create 2020-04-13 17:43
 */
public class RandomBufferedImage extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //設置響應頭,然瀏覽器 3 秒刷新一次
        resp.setHeader("refresh","3");

        //1.在記憶體中創建一張圖片
        BufferedImage image = new BufferedImage(80, 30, BufferedImage.TYPE_INT_RGB);
        //2.得到圖片
//        image.getGraphics();
        Graphics2D g = image.createGraphics();
        g.setColor(Color.WHITE);//設置背景圖片顏色
        g.fillRect(0,0,80,20);//填充背景顏色
        //3.向圖片上寫數據
        g.setColor(Color.BLUE);//設置圖片上字體的顏色
        g.setFont(new Font(null,Font.BOLD,20));
        g.drawString(makeNum(),0,20);
        //4.設置響應頭,控制瀏覽器以圖片的方式打開
        resp.setHeader("content-type","image/jpeg");
        //5.設置響應頭,控制瀏覽器不快取圖片
        resp.setDateHeader("expries", -1);
        resp.setHeader("Cache-Control", "no-cache");
        resp.setHeader("Pragma", "no-cache");
        //6.將圖片寫給瀏覽器
        ImageIO.write(image,"jpg",resp.getOutputStream());
    }

    /**
     * 生成隨機數
     * @return
     */
    private String makeNum(){
        Random random = new Random();
        String ranNum = random.nextInt(9999999) + "";
        StringBuffer sb = new StringBuffer();
        //如果位數不足 7 位 補零
        for (int i = 0; i < 7 - ranNum.length(); i++) {
            sb.append("0");
        }
        ranNum += sb.toString();

        return ranNum;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

GIF

  1. 其它常用的響應頭設置

    • 設置 HTTP響應頭控制瀏覽器禁止換出當前文檔內容
    response.setDateHeader("expries", -1);
    response.setHeader("Cache-Control", "no-cache");
    response.setHeader("Pragma", "no-cache");
    
    • 設置 HTTP 響應頭控制瀏覽器定時刷新網頁 (refresh)
    response.setHeader("refresh", "3");//設置refresh響應頭控制瀏覽器每隔3秒鐘刷新一次
    
    • 通過 response 實現請求重定向:一個 web 資源受到請求後,通知客戶端去訪問另外一個 web 資源,這稱之為請求重定向

      應用場景:用戶登錄,用戶訪問登錄頁面,登錄成功後跳轉到某個頁面,就是一個請求重定向的過程

      實現方式:response.sendRedircet(String location)

      sendRedirect() 內部實現原理:使用 response 設置 302 狀態碼和設置 location 響應頭實現重定向

    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /** 設置請求重定向的兩種方式
     * @author YH
     * @create 2020-04-13 20:09
     */
    public class SendRedirect extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //1.調用 sendRedirect 方法實現請求重定向
            resp.sendRedirect("ranImg");
    
            //2.使用 response 設置 Location 響應頭並設置狀態碼
    //        resp.setHeader("Location","download");
    //        resp.setStatus(HttpServletResponse.SC_FOUND);
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            this.doGet(req, resp);
        }
    }
    

    GIF

8.3 Web 工程中 URL 地址寫法

​ 在 javaweb 中正斜杠 “/”:如果 “/” 是給伺服器用的,則代表當前的 web 工程,如果 “/” 是給瀏覽器用的,則代表 webapps 目錄

  • “/” 代表當前 web 工程的常見應用:
  1. ServletContext.getRealPath(String path) 獲取指定虛擬路徑(資源訪問路徑)的在伺服器文件系統上的絕對路徑
/**
* ServletContext.getRealPath("/download/1.JPG")是用來獲取伺服器上的某個資源,
* 那麼這個"/"就是給伺服器用的,"/"此時代表的就是web工程
 * ServletContext.getRealPath("/download/1.JPG")表示的就是讀取web工程下的download文件夾中的1.JPG這個資源
* 只要明白了"/"代表的具體含義,就可以很快寫出要訪問的web資源的絕對路徑
*/
this.getServletContext().getRealPath("/download/1.JPG");
  1. 在伺服器端 forward 到其他頁面
/**
* 2.forward
* 客戶端請求某個web資源,伺服器跳轉到另外一個web資源,這個forward也是給伺服器用的,
* 那麼這個"/"就是給伺服器用的,所以此時"/"代表的就是web工程
*/
this.getServletContext().getRequestDispatcher("/index.jsp").forward(request, response);
  • “/” 代表 webapps 目錄的常見應用:
  1. 使用 sendRedirect 實現請求重定向
response.sendRedirect("/JavaWeb_HttpServlet/index.jsp");

  伺服器發送一個 URL 地址給瀏覽器,瀏覽器拿到 URL 地址之後,再去請求伺服器,所以這個 “/” 是給瀏覽器使用的,此時 “/” 代表的就是 webapps 目錄,”/JavaWeb_HttpServlet/index.jsp” 這個地址指的就是 “webapps\JavaWeb_HttpServlet\index.jsp”

  response.sendRedirect(“/ 項目名稱 / 文件夾目錄 / 頁面”); 這種寫法是將項目名稱寫死在程式中的做法,不靈活,萬一哪天項目名稱變了,此時就得改程式,所以推薦使用下面的靈活寫法:

response.sendRedirect("/JavaWeb_HttpServlet/index.jsp");

  這種寫法改成

response.sendRedirect(request.getContextPath()+"/index.jsp");

  request.getContextPath() 獲取到的內容就是 “/JavaWeb_HttpServlet,這樣就比較靈活了,使用 request.getContextPath() 代替 “/ 項目名稱”,推薦使用這種方式,靈活方便!

  1. 使用超鏈接跳轉
<a href="/JavaWeb_HttpServlet/index.jsp">跳轉到首頁</a>

  這是客戶端瀏覽器使用的超鏈接跳轉,這個 “/” 是給瀏覽器使用的,此時 “/” 代表的就是 webapps 目錄。

  使用超鏈接訪問 web 資源,絕對路徑的寫法推薦使用下面的寫法改進:

<a href="${pageContext.request.contextPath}/index.jsp">跳轉到首頁</a>

  這樣就可以避免在路徑中出現項目的名稱,使用 ${pageContext.request.contextPath} 取代 “/JavaWeb_HttpServlet”

  1. Form 表單提交

對於 form 表單提交中 action 屬性絕對路徑的寫法,也推薦使用如下的方式改進:

<form action="${pageContext.request.contextPath}/servlet/CheckServlet" method="post">
         <input type="submit" value="提交">
</form>

綜合範例:

<%@page language="java" import="java.util.*" pageEncoding="UTF-8" %>
<!DOCTYPE HTML PUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
    <title>"/"代表webapps目錄的常見應用場景</title>
    <%--使用絕對路徑的方式引用js腳本--%>
    <script type="text/javascript" src="${pageContext.request.contextPath}/js/index.js"></script>
    <%--${pageContext.request.contextPath}與request.getContextPath()寫法是得到的效果是一樣的--%>
    <script type="text/javascript" src="<%=request.getContextPath()%>/js/login.js"></script>
    <%--使用絕對路徑的方式引用css樣式--%>
    <link rel="stylesheet" href="${pageContext.request.contextPath}/css/index.css" type="text/css"/>
</head>
<body>
<%--form表單提交--%>
<form action="${pageContext.request.contextPath}/servlet/CheckServlet" method="post">
    <input type="submit" value="提交">
</form>
<%--超鏈接跳轉頁面--%>
<a href="${pageContext.request.contextPath}/index.jsp">跳轉到首頁</a>
</body>
</html>

8.4 response 細節擴展

​ Servlet 程式向 ServletOutputStream 或 PrintWriter 對象中寫入的數據將被 Servlet 引擎從 response 裡面獲取,Servlet 引擎將這些數據當做響應消息的正文,然後再與響應狀態行和各響應頭組合後輸出到客戶端。

​ Servlet 程式的 service 方法結束後,Servlet 引擎將檢查 getWriter 或 getOutputStream 方法返回的輸出流是否已經調用過 close 方法,如果沒有,Servlet 引擎將調用 close 方法關閉該輸出流。

Servlet 開發常見問題

Servlet 中文亂碼問題解析及詳細解決方法

8.5 通過 Servlet 生成驗證碼圖片

  1. 創建一個 DrawImage servlet ,用來生成驗證碼圖片
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/** 生成多種類型的內容驗證碼
 * @author YH
 * @create 2020-04-14 7:30
 */
public class DrawImage extends HttpServlet {
    private static final long serialVersionUID = 342349254353430483L;
    //圖片寬
    public static final int WIDTH = 120;
    //圖片高
    public static final int HEIGHT = 30;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //接收客戶端傳遞的createTypeFlag標識(生成那種驗證碼)
        String createTypeFlag = req.getParameter("createTypeFlag");

        //1.記憶體中創建一張圖片
        BufferedImage image = new BufferedImage(WIDTH, HEIGHT,BufferedImage.TYPE_INT_RGB);
        //2.得到圖片
        Graphics2D g = image.createGraphics();
        //3.設置圖片的背景色
        setBackGround(g);
        //4.設置圖片的邊框
        setBorder(g);
        //5.在圖片上畫干擾線
        drawRandomLine(g);
        //6.寫在圖片上的隨機數
        //生成中文驗證碼
//        String randomChar = drawRandomNum(g,"ch");
//        //生成數字字母組合驗證碼
//        String randomChar = drawRandomNum(g,"nl");
//        //生成數字驗證碼
//        String randomChar = drawRandomNum(g,"n");
//        //生成字母驗證碼
//        String randomChar = drawRandomNum(g,"l");
        //客戶端傳遞參數選擇驗證方式
        String randomChar = drawRandomNum(g,createTypeFlag);
        //7.將隨機數存在session中
        req.getSession().setAttribute("checkcode",randomChar);
        //8.設置響應頭通知瀏覽器以圖片的形式打開
        resp.setContentType("image/jpeg");
        //9.設置響應頭通知瀏覽器不要快取
        resp.setDateHeader("expries",-1);
        resp.setHeader("Cach-Contro1","no-cache");
        resp.setHeader("Pragma","no-cache");
        //10.將圖片寫給瀏覽器
        ImageIO.write(image,"jpg",resp.getOutputStream());
    }

    /**
     * 設置背景顏色
     * @param g
     */
    private void setBackGround(Graphics2D g){
        //設置顏色
        g.setColor(Color.white);
        g.fillRect(0,0,WIDTH,HEIGHT);
    }

    /**
     * 設置邊框顏色
     * @param g
     */
    private void setBorder(Graphics2D g){
        //設置顏色
        g.setColor(Color.BLUE);
        //設置邊框寬度,要繪製的矩形寬高減掉邊框寬度,防止撐開原盒子(前端內容)
        g.drawRect(1,1,WIDTH - 2,HEIGHT -2);
    }

    /**
     * 在圖片上畫隨機數
     * @param g
     */
    private void drawRandomLine(Graphics2D g){
        //設置顏色
        g.setColor(Color.GREEN);
        //設置線條數量
        int lineNum = 5;
        //畫線條
        for (int i = 0; i < lineNum; i++) {
            //確定線條起始和結束坐標(在寬高的範圍內)
            int x1 = new Random().nextInt(WIDTH);
            int y1 = new Random().nextInt(HEIGHT);
            int x2 = new Random().nextInt(WIDTH);
            int y2 = new Random().nextInt(HEIGHT);
            g.drawRect(x1,y1,x2,y2);
        }
    }


    private String drawRandomNum(Graphics2D g,String... createTypeFlag){
        //設置顏色
        g.setColor(Color.RED);
        //設置字體
        g.setFont(new Font("微軟雅黑",Font.BOLD,20));
        //常用的漢字
        String baseChineseChar = "\u7684\u4e00\u4e86\u662f\u6211\u4e0d\u5728\u4eba\u4eec\u6709\u6765\u4ed6\u8fd9\u4e0a\u7740\u4e2a\u5730\u5230\u5927\u91cc\u8bf4\u5c31\u53bb\u5b50\u5f97\u4e5f\u548c\u90a3\u8981\u4e0b\u770b\u5929\u65f6\u8fc7\u51fa\u5c0f\u4e48\u8d77\u4f60\u90fd\u628a\u597d\u8fd8\u591a\u6ca1\u4e3a\u53c8\u53ef\u5bb6\u5b66\u53ea\u4ee5\u4e3b\u4f1a\u6837\u5e74\u60f3\u751f\u540c\u8001\u4e2d\u5341\u4ece\u81ea\u9762\u524d\u5934\u9053\u5b83\u540e\u7136\u8d70\u5f88\u50cf\u89c1\u4e24\u7528\u5979\u56fd\u52a8\u8fdb\u6210\u56de\u4ec0\u8fb9\u4f5c\u5bf9\u5f00\u800c\u5df1\u4e9b\u73b0\u5c71\u6c11\u5019\u7ecf\u53d1\u5de5\u5411\u4e8b\u547d\u7ed9\u957f\u6c34\u51e0\u4e49\u4e09\u58f0\u4e8e\u9ad8\u624b\u77e5\u7406\u773c\u5fd7\u70b9\u5fc3\u6218\u4e8c\u95ee\u4f46\u8eab\u65b9\u5b9e\u5403\u505a\u53eb\u5f53\u4f4f\u542c\u9769\u6253\u5462\u771f\u5168\u624d\u56db\u5df2\u6240\u654c\u4e4b\u6700\u5149\u4ea7\u60c5\u8def\u5206\u603b\u6761\u767d\u8bdd\u4e1c\u5e2d\u6b21\u4eb2\u5982\u88ab\u82b1\u53e3\u653e\u513f\u5e38\u6c14\u4e94\u7b2c\u4f7f\u5199\u519b\u5427\u6587\u8fd0\u518d\u679c\u600e\u5b9a\u8bb8\u5feb\u660e\u884c\u56e0\u522b\u98de\u5916\u6811\u7269\u6d3b\u90e8\u95e8\u65e0\u5f80\u8239\u671b\u65b0\u5e26\u961f\u5148\u529b\u5b8c\u5374\u7ad9\u4ee3\u5458\u673a\u66f4\u4e5d\u60a8\u6bcf\u98ce\u7ea7\u8ddf\u7b11\u554a\u5b69\u4e07\u5c11\u76f4\u610f\u591c\u6bd4\u9636\u8fde\u8f66\u91cd\u4fbf\u6597\u9a6c\u54ea\u5316\u592a\u6307\u53d8\u793e\u4f3c\u58eb\u8005\u5e72\u77f3\u6ee1\u65e5\u51b3\u767e\u539f\u62ff\u7fa4\u7a76\u5404\u516d\u672c\u601d\u89e3\u7acb\u6cb3\u6751\u516b\u96be\u65e9\u8bba\u5417\u6839\u5171\u8ba9\u76f8\u7814\u4eca\u5176\u4e66\u5750\u63a5\u5e94\u5173\u4fe1\u89c9\u6b65\u53cd\u5904\u8bb0\u5c06\u5343\u627e\u4e89\u9886\u6216\u5e08\u7ed3\u5757\u8dd1\u8c01\u8349\u8d8a\u5b57\u52a0\u811a\u7d27\u7231\u7b49\u4e60\u9635\u6015\u6708\u9752\u534a\u706b\u6cd5\u9898\u5efa\u8d76\u4f4d\u5531\u6d77\u4e03\u5973\u4efb\u4ef6\u611f\u51c6\u5f20\u56e2\u5c4b\u79bb\u8272\u8138\u7247\u79d1\u5012\u775b\u5229\u4e16\u521a\u4e14\u7531\u9001\u5207\u661f\u5bfc\u665a\u8868\u591f\u6574\u8ba4\u54cd\u96ea\u6d41\u672a\u573a\u8be5\u5e76\u5e95\u6df1\u523b\u5e73\u4f1f\u5fd9\u63d0\u786e\u8fd1\u4eae\u8f7b\u8bb2\u519c\u53e4\u9ed1\u544a\u754c\u62c9\u540d\u5440\u571f\u6e05\u9633\u7167\u529e\u53f2\u6539\u5386\u8f6c\u753b\u9020\u5634\u6b64\u6cbb\u5317\u5fc5\u670d\u96e8\u7a7f\u5185\u8bc6\u9a8c\u4f20\u4e1a\u83dc\u722c\u7761\u5174\u5f62\u91cf\u54b1\u89c2\u82e6\u4f53\u4f17\u901a\u51b2\u5408\u7834\u53cb\u5ea6\u672f\u996d\u516c\u65c1\u623f\u6781\u5357\u67aa\u8bfb\u6c99\u5c81\u7ebf\u91ce\u575a\u7a7a\u6536\u7b97\u81f3\u653f\u57ce\u52b3\u843d\u94b1\u7279\u56f4\u5f1f\u80dc\u6559\u70ed\u5c55\u5305\u6b4c\u7c7b\u6e10\u5f3a\u6570\u4e61\u547c\u6027\u97f3\u7b54\u54e5\u9645\u65e7\u795e\u5ea7\u7ae0\u5e2e\u5566\u53d7\u7cfb\u4ee4\u8df3\u975e\u4f55\u725b\u53d6\u5165\u5cb8\u6562\u6389\u5ffd\u79cd\u88c5\u9876\u6025\u6797\u505c\u606f\u53e5\u533a\u8863\u822c\u62a5\u53f6\u538b\u6162\u53d4\u80cc\u7ec6";
        //數字和字母的組合
        String baseNumLetter = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        //數字的組合
        String baseNum = "0123456789";
        //字母的組合
        String baseLetter = "ABCDEFGHIGKLMNOPQRSTUVWXYZ";
        //createTypeFlag可變參數底層實現為數組,所以createTypeFlag[0]為第一個參數索引
        String baseChar = baseNumLetter;
        if(createTypeFlag.length > 0 && createTypeFlag[0] != null ) {
            switch (createTypeFlag[0]) {
                case "ch":
                    baseChar = baseChineseChar;
                    break;
                case "nl":
                    baseChar = baseNumLetter;
                    break;
                case "n":
                    baseChar = baseNum;
                    break;
                case "l":
                    baseChar = baseLetter;
                    break;
            }
        }
        return createRandomChar(g,baseChar);
    }

    /**
     * 創建隨機字元
     * @param g
     * @param baseChar
     */
    private String createRandomChar(Graphics2D g,String baseChar){
        StringBuffer sb = new StringBuffer();
        // x 坐標的值
        int x = 5;
        String ch = "";
        //控制字數
        for (int i = 0; i < 4; i++) {
            //設置字體旋轉角度
            int degree = new Random().nextInt() % 30;
            ch = baseChar.charAt(new Random().nextInt(baseChar.length())) + "";
            sb.append(ch);
            //正向角度
            g.rotate(degree * Math.PI / 180,x,20);
            //寫入內容
            g.drawString(ch,x,20);
            //反向角度
            g.rotate(-degree * Math.PI / 180,x,20);
            x += 30;
        }
        return sb.toString();
    }
}

顯示各種形式的驗證碼:

image-20200414094831222

  1. 在 from 表單中使用驗證碼圖片
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
 <head>
   <title>在Form表單中使用驗證碼</title>
   <script type="text/javascript">
   //刷新驗證碼
   function changeImg(){
       document.getElementById("randomImage").src="${pageContext.request.contextPath}/drawImage?"+Math.random();
    }
    </script>
  </head>
  <body>
        <form action="${pageContext.request.contextPath}/check" method="post">
            驗證碼:<input type="text" name="validateCode"/>
            <img alt="驗證碼看不清,換一張" src="${pageContext.request.contextPath}/drawImage" id="randomImage" onclick="changeImg()">
            <a href="javascript:void(0)" onclick="changeImg()">看不清,換一張</a>
            <br/>
            <input type="submit" value="提交">
        </form>
  </body>
</html>

效果如下:

GIF

  1. 伺服器端對 from 表單提交上來的驗證碼處理
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-14 11:09
 */
public class CheckServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //獲取瀏覽器提交的用戶輸入
        String createTypeFlag = req.getParameter("validateCode");
        //獲取 DrawImage servlet 生成的驗證碼內容
        String random = (String)req.getSession().getAttribute("checkcode");
//        this.getServletContext().setAttribute("createTypeFlag",createTypeFlag);
        System.out.println(random.equals(createTypeFlag) ? "輸入正確" : "輸出錯誤");
    }
}

GIF

九、HttpServletRequest 對象

​ HttpServletRequest 對象代表客戶端的請求,當客戶端通過 HTTP 協議訪問伺服器時,HTTP 請求頭中的所有資訊都封裝在這個對象中,通過這個對象提供的方法可以獲得所有的請求資訊。

9.1 常用方法

  1. 獲取的客戶端資訊
    • getRequestURL() 返回客戶端能發出請求時完整的 RUL
    • getRequestURI() 返回請求行中資源名部分
    • getQueryString() 返回請求行中參數部分
    • getPathInfo() 返回請求 URL 中的額外路徑資訊的路徑資訊;指 URL 中位於 servlet 的路徑之後和查詢參數之前的內容,以”/”開頭
    • getRemoteAddr() 返回發出請求的客戶機的 IP 地址
    • getRemoteHost() 返回發出請求的客戶機的完整主機名
    • getRemotePort() 返回返回客戶機所使用的的網路埠號
    • getLocalAddr() 返回 WEB 伺服器的 IP 地址
    • getLocalName() 返回 WEB 伺服器的主機名

示例:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author YH
 * @create 2020-04-14 14:34
 */
public class RequestMethod extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //因為瀏覽器只能顯示字元串類型的數據,所以將獲取的所有值都轉為字元串
        //獲取客戶端請求時的完整URL
        String url = req.getRequestURL().toString();
        //獲取請求行中資源名部分
        String uri = req.getRequestURI();
        //獲取請求行中參數部分
        String param = req.getQueryString();
        //獲取請求行中servlet路徑之後,參數之前的額外內容
        String pathInfo = req.getPathInfo();
        //獲取獲取客戶機ip
        String address = req.getRemoteAddr();
        //獲取客戶機主機名
        String host = req.getRemoteHost();
        //獲取伺服器網路埠
        int remotePort = req.getRemotePort();
//        String remotePort = String.valueOf(req.getRemotePort());
        //web伺服器ip
        String localAddr = req.getLocalAddr();
        //web伺服器的主機名
        String localName = req.getLocalName();
        //獲取請求的方式
        String method = req.getMethod();
        //返回用戶的登陸資訊
        String user = req.getRemoteUser();

        //設置數據以UTF-8的編碼輸出到客戶端瀏覽器
        resp.setCharacterEncoding("utf-8");
        //設置響應頭,控制瀏覽器以UTF-8的編碼解析收到的響應數據
        resp.setHeader("content-type","text/html;charset=utf-8");

        PrintWriter out = resp.getWriter();

        out.write("獲取得到的用戶資訊如下:");
        out.write("<hr/>");

        out.write("請求的URL地址:" + url);
        out.write("<br/>");
        out.write("請求的資源:" + uri);
        out.write("請求的URL地址中附帶的參數" + param);
        out.write("<br/>");
        out.write("請求的額外部分:" + pathInfo);
        out.write("<br/>");
        out.write("客戶機ip:" + address);
        out.write("<br/>");
        out.write("客戶機主機名:" + host);
        out.write("<br/>");
        out.write("網路埠:" + remotePort);
        out.write("<br/>");
        out.write("web伺服器ip:" + localAddr);
        out.write("<br/>");
        out.write("web伺服器主機名:" + localName);
        out.write("<br/>");
        out.write("客戶端請求方式:" + method);
        out.write("<br/>");
        out.write("用戶登錄資訊:" + user);

    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

效果:

image-20200414154706899

  1. 獲取客戶端請求頭

    • getHeader(String name) 以 String 對象的形式返回指定請求頭的值

    • getHeaders(String name) 以 String 對象的 Enumeration 的形式返回指定請求頭的所有值(有些請求頭參數較多,且可以通過其他請求頭指定其中的某一參數)

    • getHeaderNames() 獲取所有請求頭的名字

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

/**
 * @author YH
 * @create 2020-04-14 15:56
 */
public class RequestLineMethod extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //設置響應頭,控制瀏覽器以UTF-8的編碼解析響應回送的數據
        resp.setHeader("content-type","text/html;charset=utf-8");
        //設置響應數據的編碼方式為UTF-8
        resp.setCharacterEncoding("utf-8");

        PrintWriter out = resp.getWriter();

        //指定響應頭的 name 獲取響應頭資訊
        String content_type = req.getHeader("Accept-Language");
        out.write("獲取到的數據為:");
        out.write("<hr/>");
        out.write("Content-type:" + content_type);
        out.write("<br/>");

        //指定響應頭的的 name 獲取它所有的資訊
        Enumeration<String> headerList = req.getHeaders("Accept");
        while(headerList.hasMoreElements()){
            String headName = headerList.nextElement();
            //根據請求頭的名字獲取對應的請求頭的值
            out.write( "Accept:" + ":" + headName);
            out.write("<br/>");
        }

        //獲取請求中所有的響應頭的name
        Enumeration<String> headerNames = req.getHeaderNames();
            while(headerNames.hasMoreElements()){
                String headNameList = headerNames.nextElement();
                //根據請求頭的名字獲取對應的請求頭的值
                String headValue = req.getHeader(headNameList);
                out.write("<br/>");
                out.write(headNameList + ":" + headValue);
            }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

效果:

image-20200414183953455

  1. 獲得客戶機請求參數(客戶端提交的數據)
    • getParameter(String) 返回指定請求參數的值
    • getParameterValues(String name) 返回包含給定請求參數擁有的所有值的 String 對象數組(如多選框的值)
    • getParametereNames() 返回所有請求參數的集合
    • getParameterMap() 以鍵值對的形式返回請求參數資訊(編寫框架時常用)
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;
import java.util.Map;

/**
 * @author YH
 * @create 2020-04-14 19:18
 */
public class FromSubmit extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //設置客戶端發送請求的數據編碼
        req.setCharacterEncoding("utf-8");
        //設置伺服器端以utf-8的編碼輸出數據
        resp.setCharacterEncoding("utf-8");
        //設置響應頭,控制瀏覽器以UTF-8編碼解析數據
        resp.setContentType("text/html;charset=utf-8");

        PrintWriter out = resp.getWriter();
//第一種方式:
//        String userId = req.getParameter("userId");
//        String username = req.getParameter("username");
//        String password = req.getParameter("password");
//        String sex = req.getParameter("sex");
//        String department = req.getParameter("department");
////        String hobby = req.getParameter("hobby");
//        String[] hobby = req.getParameterValues("hobby");
//        String textarea = req.getParameter("textarea");
//        String hiddenFile = req.getParameter("hiddenFile");
//        String htmlStr = "<table>" +
//                "<tr><td>填寫的編號:</td><td>{0}</td></tr>" +
//                "<tr><td>填寫的用戶名:</td><td>{1}</td></tr>" +
//                "<tr><td>填寫的密碼:</td><td>{2}</td></tr>" +
//                "<tr><td>選中的性別:</td><td>{3}</td></tr>" +
//                "<tr><td>選中的部門:</td><td>{4}</td></tr>" +
//                "<tr><td>選中的興趣:</td><td>{5}</td></tr>" +
//                "<tr><td>填寫的說明:</td><td>{6}</td></tr>" +
//                "<tr><td>隱藏域的內容:</td><td>{7}</td></tr>" +
//                "</table>";
//        String hobbyStr = "";
//        //方式出現數組空指針的技巧
//        for (int i = 0; hobby != null && i < hobby.length; i++) {
//            if(i == hobby.length - 1){
//                hobbyStr += hobby[i];
//            }else{
//                hobbyStr += hobby[i] + ",";
//            }
//        }
//        htmlStr = MessageFormat.format(htmlStr,userId,username,password,sex,department,hobbyStr,textarea,hiddenFile);
//
//        out.write(htmlStr);

//第二種方式:
//        Enumeration<String> names = req.getParameterNames();
//        while(names.hasMoreElements()){
//            String s = names.nextElement();
//            String parameter = req.getParameter(s);
//            out.write(s + ":" + parameter + "<br/>");
//        }

// 第三種方式
        Map<String, String[]> parameterMap = req.getParameterMap();
        for(Map.Entry<String,String[]> entry : parameterMap.entrySet()){
            String paramName = entry.getKey();
            String paramValue = "";
            String[] paraValueArr = entry.getValue();
            for (int i = 0; paraValueArr != null && i < paraValueArr.length; i++) {
                if(i == paraValueArr.length - 1){
                    //最後一個不加 , 號
                    paramValue += paraValueArr[i];
                }else{
                    //不是最後一個加 , 號
                    paramValue += paraValueArr[i] + ",";
                }
            }
            out.write(MessageFormat.format("{0} : {1}<br/>",paramName,paramValue));
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

image-20200415082515156

第一種方式 使用 getParamter() 和 getParamterValues():

使用getParamter()讀取多選框:image-20200415083005305

使用getParamterValues()讀取多選框:image-20200415082758444

第二種方式 使用 getParamterNames():

image-20200415082549897

第三種方式 使用 getParamterMap():

image-20200416082528744

補充:之所以會產生亂碼,就是因為伺服器和客戶端溝通的編碼不一致造成的,因此解決的辦法是:在*客戶端*和伺服器之間設置一個統一的編碼,之後就按照此編碼進行數據的傳輸和接收。

請求亂碼問題詳見://www.cnblogs.com/csyh/p/12691421.html

9.2 Request 對象是實現請求轉發

​ 指一個 web 資源收到客戶端請求後,通知伺服器去調用另外一個 web 資源進行處理

​ 應用場景:MVC 設計模式

  1. 通過 ServletContext 的 getRequestDispatcher(String path) 方法,獲取一個 RequestDispatcher 對象,調用這個對象的 forward 方法可以實現請求轉發
RequestDispatcher reqDispatcher =this.getServletContext().getRequestDispatcher("/test.jsp");
reqDispatcher.forward(request, response);
  1. 通過 request 對象提供的 getRequestDispatche(String path) 方法,獲取一個 RequestDispatcher 對象,調用這個對象的 forward 方法實現請求轉發
request.getRequestDispatcher("/test.jsp").forward(request, response);

​ request 對象同時也是一個域對象(Map 容器),開發人員通過 request 對象在實現轉發時,把數據通過 request 對象帶給其他 web 資源處理

示例:在 request 對象中存入數據,轉發頁面中獲取此數據

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** 將數據存入 request 對象,把 request 對象當做一個 Map 容器來使用
 * @author YH
 * @create 2020-04-16 10:36
 */
public class RequestMapData extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //向 request 對象中存入數據
        req.setAttribute("data","requestData");
        //當瀏覽器訪問此 servlet 後,將請求轉發至前端頁面
        req.getRequestDispatcher("req.jsp").forward(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    使用普通方式取出存儲在 request 對象中的數據
    <h3><%=(String)request.getAttribute("data")%></h3>
    使用EL表達式取出存儲在request對象中的數據
    <h3>${data}</h3>
</body>
</html>

9.3 請求轉發和請求重定向的區別

​ 一個 web 資源收到客戶端請求後,通知伺服器去調用另一個 web 資源進行處理,為請求轉發(307)

​ 一個 web 資源收到客戶端請求後,通知瀏覽器去訪問另一個 web 資源進行處理,為請求重定向(302)

十、Cookie 會話管理

​ 簡單理解:用戶開一個瀏覽器,點擊多個超鏈接訪問伺服器的多個資源,然後關閉瀏覽器,這個整個過程稱為一個會話

​ 有狀態會話:一個同學來過教室,下次再來教室,我們會知道這個同學曾經來過

10.1 會話過程中要解決的問題?

​ 每個用戶在使用瀏覽器與伺服器進行會話的過程中,不可避免各自會產生一些數據,程式要想辦法為每個用戶保存這些數據

10.2 保存會話的兩種技術

​ Cookie 是客戶端技術,是伺服器發送到用戶瀏覽器並保存在本地的一小塊數據,它會在瀏覽器下次向同一伺服器再發起請求時被攜帶並發送到伺服器上。通俗點說:程式把每個用戶的數據以 cookie 的形式寫給用戶各自的瀏覽器,再去訪問伺服器中的 web 資源時,就會帶著各自的數據去。這樣 web 資源處理的就是用戶各自的數據了。

​ 通常用於告知服務端兩個請求是否來自同一瀏覽器,如保持用戶的登錄狀態。

10.2.2 Session

​ Session 是伺服器端技術,利用這個技術,伺服器在運行時可以為每一個用戶的瀏覽器創建一個其獨享的 session 對象,由於 session 為用戶瀏覽器獨享,所以用戶在訪問伺服器的 web 資源時,可以把各自的數據放在各自的 session 中,當用戶再去訪問伺服器中的其他 web 資源時,其他 web 資源再從用戶各自的 session 中取出數據為用戶服務。

​ Java 中的 javax.servlet.http.Cookie 類用於創建一個 Cookie

方法 類型 描述
1 Cookie(String name,String value) 構造方法 實例化 Cookie 對象,傳入 cookie 名稱後 cookie 的值
2 public String getName() 普通方法 取得 Cookie 的名字
3 public String getValue() 普通方法 取得 Cookie 的值
4 public void setValue() 普通方法 設置 Cookie 的值
5 public void setMaxAge(int expiry) 普通方法 設置 Cookie 的最大保存時間,即 Cookie 的有效期,當伺服器給瀏覽器回送一個 cookie 時,如果在伺服器端沒有調用 setMaxAge 方法設置 cookie 的有效期,那麼 cookie 的有效期只在一次會話過程中有效,用戶開一個瀏覽器,點擊多個超鏈接,訪問伺服器多個 web 資源,然後關閉瀏覽器,整個過程稱之為一次會話,當關閉瀏覽器,會話就結束了,此時 cookie 就會失效,如果在伺服器端使用 setMaxAge 方法設置了 cookie 的有效期,比如設置了 30 分鐘,那麼當伺服器把 cookie 發送給瀏覽器時,cookie 就會存儲在客戶端的硬碟上30分鐘,在30分鐘內即時瀏覽器關閉了,cookie 依然存在,只要打開瀏覽器訪問伺服器,瀏覽器都會帶上 cookie ,這樣就可以在伺服器端獲取到客戶端瀏覽器傳遞過來的 cookie 裡面的資訊(這就是 cookie 是否設置 maxAge 的區別),不設置 maxAge,那麼 cookie 就只在一次會話中有效,一旦用戶關閉了瀏覽器(會話),那麼 cookie 就沒有了。瀏覽器是如何做到這一點的呢?我們啟動一個瀏覽器就相當於啟動一個應用程式,而伺服器回送的 cookie 首先是存在瀏覽器的快取中的,當瀏覽器關閉時,瀏覽器的快取自然就沒有了,所以存在快取中的 cookie 也被被清理掉了,而設置了 maxAge,即設置 cookie 的有效期後,瀏覽器在關閉時就會將快取中的 cookie 寫到硬碟上存儲起來,這樣 cookie 就能夠一直存在了
6 public int getMaxAge() 普通方法 獲取 Cookies 的有效期
7 public void setPath(String uri) 普通方法 設置 cookie 的有效路徑,比如把 cookie 的有效路徑設置為 「/ck”,那麼瀏覽器訪問」xdp”目錄下的 web 資源時,都會帶上 cookie,再比如把 cookie 的有效路徑設置為「/ck/yh”,那麼瀏覽器只有在訪問」ck”目錄下的「yh”這個目錄里的 web 資源時才會帶上 cookie一起訪問,而當訪問」ck「目錄下的 web 資源時,瀏覽器是不帶」cookie「的
8 public String getPath() 普通方法 獲取 cookie 的有效路徑
9 public void setDomain(String pattern) 普通方法 設置 cookie 的有效路徑
10 public void getDomain() 普通方法 獲取 cookie 的有效路徑

​ response 介面中也定義了一個 addCookie() 方法,用於在其響應頭中增加一個相應的 Set-Cookie 頭欄位。同樣,request 介面中也定義了一個 getCookies() 方法,它用於獲取客戶端提交的 Cookie

實例 1:使用 cookie 記錄用戶上一次訪問的時間

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

/** cookie實例:獲取用戶的上一次訪問時間
 * @author YH
 * @create 2020-04-19 9:14
 */
public class CookieDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //設置服務端以UTF-8的編碼進行輸出
        resp.setCharacterEncoding("UTF-8");
        //控制瀏覽器以UTF-8的編碼進行輸出
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter out = resp.getWriter();
        //獲取瀏覽器其訪問伺服器傳遞過來的 Cookie 的數組
        Cookie[] cookies = req.getCookies();
        //如果用戶是第一次訪問,那麼cookie為null
        if(cookies != null){
            out.write("上一次的訪問時間為:");
            //對cookie數組進行遍歷,獲取name為lastAccessTime的屬性的值,就是上次訪問時間
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                if(cookie.getName().equals("lastAccessTime")){
                    Long lastAccessTime = Long.parseLong(cookie.getValue());
                    //轉為日期格式
                    Date date = new Date(lastAccessTime);
                    out.write(date.toString());
                }
            }
        }else{
            out.write("你是第一次訪問!");
        }

        //用戶訪問過之後重新設置用戶的訪問時間,存儲到 cookie 中,然後發送到客戶端瀏覽器
        Cookie cookie = new Cookie("lastAccessTime",System.currentTimeMillis() + "");
        //將 cookie 對象添加到response對象中,這樣伺服器在輸出response對象內容時就會把cookie傳遞給瀏覽器
        resp.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

第一次訪問,伺服器通過響應對象回送cookie數據給瀏覽器:

image-20200419110730449

刷新一下,相當於第二次訪問:

image-20200419110958117

實例 2:使用 Cookie 實現記住用戶名

首次訪問註冊頁面:

image-20200420081812814

訪問測試頁面,將用戶提交的用戶名資訊以 Cookie 的形式回傳給客戶端(此時瀏覽器的其他網頁也共享這個 cookie):

image-20200420082235679

再次訪問註冊頁面:

image-20200420083008474

  • 一個 Cookie 只能標識一種資訊,它至少含有一個標識該資訊的名稱(Name)和設置值(Value)
  • 一個 web 站點可以給一個 web 瀏覽器發送多個 Cookie,一個 web 瀏覽器也可以存儲多個 web 站點提供的 Cookie
  • 瀏覽器一般可以存儲300個Cookie(好幾年前的數據),每個站點最多存放20個 Cookie(幾年前的數據),每個 Cookie 的大小最小為 4k(4096位元組)
  • 如果創建一個 Cookie 並將它發送到瀏覽器,默認情況下它是一個會話級別的 cookie(即存儲在瀏覽器的記憶體中),用戶退出瀏覽器之後被刪除。若希望瀏覽器將給 cookie 存儲在磁碟上,則需要使用 maxAge,並給出一個以秒為單位的時間。將最大時效設為 0 則是命令瀏覽器將該 Cookie 刪除
  1. 刪除 Cookie

注意:刪除 cookie 時,path 必須一致,否則不會刪除

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-19 11:37
 */
public class DeleteCookie extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //創建一個名字為 lastAccessTime的cookie
        Cookie cookie = new Cookie("lastAccessTime",System.currentTimeMillis() + "");
        //將cookie的有效期設置為0,命令瀏覽器刪除該cookie
        cookie.setMaxAge(0);
        resp.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  1. Cookie 中存取中文

    ​ 要想在 cookie 中存取中文,那麼必須使用 URLEncoder 類裡面的 encode(String s,String enc) 方法進行中文轉碼,例如:

    Cookie cookie = new Cookie("userName",URLEncoder.encode("雲翯","UTF-8"));
    response.addCookie(cookie);
    

    ​ 在獲取 cookie 中的中文數據時,再使用 URLDecoder 類裡面的 decode(String s,String enc) 進行解碼,例如:

    URLDecoder.decode(cookies[i].getValue(),"UTF-8");
    

十一、Session

​ 在 WEB 開發中,伺服器可以為每個用戶瀏覽器創建一個會話對象(session 對象)一個瀏覽器獨佔一個 session 對象(默認情況下)。因此,在需要保存用戶數據時,伺服器程式可以把用戶數據寫到用戶瀏覽器獨佔的 session 中,當用戶使用瀏覽器訪問其他程式時,其他程式可以從用戶的 session 中取出該用戶的數據,為用戶服務。

  • Cookie 存儲在客戶端,是把用戶的數據寫給用戶的瀏覽器

  • Session 存儲在服務端,是把用戶的數據寫到用戶獨佔的 session 中(同一會話共享一個 session )

  • Session 對象由伺服器創建,開發人員可以調用 request 對象的 getSession 方法得到 session 對象

11.2 Session 實現原理

  1. 伺服器時如何實現一個 session 為一個用戶瀏覽器服務的?

Session 機制:

image-20200420095343794

​ 客戶端第一次訪問服務端時(會通過匹配 JSESSIONID和sessionId判斷是否第一次訪問),服務端會產生一個 session 對象(用於保存該用戶的資訊),且每個 session 對象都有一個唯一的 sessionId(用於區分其他的 session),服務端會產生一個 name=JSESSIONID,Value=服務端 sessionId 的 Cookie,服務端會在響應客戶端的同時,將該 Cookie 發送給客戶端,至此,客戶端就有了一個 Cookie(JSESSIONID);

​ 因此,客戶端的 Cookie 就可以和服務端的 session 一一對應(JSESSIONID – sessionId)。

​ 客戶端再次訪問服務端時:客戶端請求中就會帶上 Cookie,而服務端會先用客戶端 cookie 中的 JSESSIONID 去服務端的 session 中匹配 sessionId,如果沒有匹配到,說明第一次訪問,過程如上;如果過匹配到,說明不是第一訪問,則直接通過對應的 session 獲取用戶資訊,為之服務(如無需再次登錄)

例子:商場存包

​ 顧客(客戶端)

​ 商場(服務端)

​ 顧客第一次來存包處,商場判斷顧客是否有鑰匙,判斷是否是新顧客(沒鑰匙),給新顧客分配一把鑰匙,鑰匙 與 柜子 一一對應;

​ 顧客第二次來存包,顧客有鑰匙,說明是老顧客,則不需要分配鑰匙,客戶憑鑰匙去對應的柜子取包。

可以用如下的程式碼證明:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-19 14:55
 */
public class SessionDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        resp.setContentType("text/html;charset=utf-8");
        //使用request對象的getSession()獲取session,如果session不存在將創建一個
        HttpSession session = req.getSession();
        //將數據存儲到session中
        session.setAttribute("data","加油!奧利給!");
        //使用session對象的getId()獲取session的id
        String id = session.getId();
        //判斷session是不是新創建的
        if(session.isNew()){
            resp.getWriter().print("session創建成功,session的id為:" + id);
        }else{
            resp.getWriter().print("伺服器已存在session,session的id是:" + id);
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

伺服器創建session後,把session的Id以cookie的形式存儲到用戶的客戶端:

image-20200419153250093

刷新瀏覽器,再次向伺服器發送請求,可以看到瀏覽器將session的id存儲到cookie中,一起傳到伺服器:

image-20200419153700015

11.3 session 常用方法

String getId() 獲取sessionId

boolean isNew() 判斷是否是新用戶(第一次訪問)

void invalidate() 使 session 失效(退出登錄、註銷)

設置 session 屬性(存入數據):

​ void setAttribute()

​ Object getAttribute()

void setMaxInactiveInterval(秒):設置最大有效非活動時間(如,多長時間未操作退出登錄)

int getMaxInactiveInterval():獲取最大有效非活動時間

注意:request 請求(作用域)只在同一次有效(如在地址欄再次回車或點刷新屬於二次發送請求,是無效的),而按 F5(強制刷新)不同,瀏覽器會自動重複剛才的行為。

11.4 session 共享問題

​ 同一會話中(用戶在站點上從打開到關閉的一次操作),訪問站點上所有 web 資源時,共享一個 session

實例:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** session共享:網頁登陸示例
 * @author YH
 * @create 2020-04-20 14:24
 */
public class SessionDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setCharacterEncoding("utf-8");
        //模擬資料庫存儲帳號密碼
        String nameData = "YH";
        String pswdData = "123456";
        //進行登陸驗證
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        if (username.equals(nameData) && password.equals(pswdData)){
            //密碼正確,跳轉到登陸成功頁面(驗證登陸成功後可以直接訪問登陸成功頁面)
            //登陸成功創建 session
            req.getSession().setAttribute("username",username);
            req.getSession().setAttribute("password",password);
            //請求轉發至登陸成功頁面,可以獲取到數據且地址欄沒有改變
            req.getRequestDispatcher("login.jsp").forward(req,resp);
        }else{
            //密碼錯誤,重定向回登陸介面(數據會丟失,且地址欄變化)
            System.out.println("密碼錯誤");
            resp.sendRedirect("index.jsp");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
<%--登陸頁面--%>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello World!</h2>
<form action="${pageContext.request.contextPath}/session2" method="get">
    <input type="text" name="username" />
    <input type="password" name="password" />
    <input type="submit" value="註冊" />
</form>
</body>
</html>
<%--登陸成功頁面--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%
    String name = (String)session.getAttribute("username");
    //如果用戶沒有登陸成功而直接訪問此頁面,則name必然為null
    if(name != null){
        response.getWriter().print("登陸成功!用戶名:" + name);
        //10秒無操作註銷session(退出登陸)
        session.setMaxInactiveInterval(10);
    }else{
        //重定向回登錄頁
        response.sendRedirect("index.jsp");
    }
%>
<a href="invalidate.jsp">註銷</a>
<h2>登陸成功!</h2>
</body>
</html>
<%--實現註銷(銷毀 session)功能的頁面--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <%
        //註銷 session
        session.invalidate();
        //返回登陸頁
        response.sendRedirect("login.jsp");
    %>
</body>
</html>

輸入錯誤的帳號密碼,直接跳回登陸頁:

GIF

​ 嘗試未登陸成功的情況下去訪問登陸後所跳轉的頁面,跳回登陸頁:

GIF

輸入正確的登陸帳號密碼:

GIF

​ 登陸成功後,訪問跳轉後的頁面(如訪問其它需要登陸後才能訪問的頁面效果同樣):

GIF

​ 註銷後跳轉回登陸介面,並嘗試訪問登陸成功介面,但跳回登錄頁:

GIF

GIF

10 秒無操作後再次訪問登錄頁,已經退出了:

GIF

小結:

​ 用戶的登陸資訊可以保存在 session 中,通過 session 可以控制用戶登入登出等用戶獨享的資訊,而同一用戶登陸成功後(匹配上 session 後),可以獲取 session 的所有資訊(即 登陸後的頁面共享這個 session)。

11.5 使用 session 防止表單重複提交

​ 場景:遇到網路延遲時,用戶提交表單,伺服器半天沒有反應,那麼用戶可能多次進行提交操作,造成重複提交表單的情況,開發中需要避免。

  1. 表單重複提交的常見應用場景

有如下 jsp 頁面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/form" method="post">
        用戶名:<input type="text" name="username">
        <input type="submit" value="提交">
    </form>
</body>
</html>
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-22 20:06
 */
public class DoFormServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //瀏覽器默認是以UTF-8編碼傳輸到伺服器的,所以需要設置伺服器以UTF-8的編碼進行接收,避免亂碼
        req.setCharacterEncoding("UTF-8");
        String username = req.getParameter("username");
        try{
            //讓當前執行緒沉睡3秒,模擬網路延遲
            Thread.sleep(3*1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(username + "向資料庫中插入數據");

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

場景一:網路延遲使用戶可以多次提交表單(用 Chrome 瀏覽器重複提價也只會顯示一次,IE瀏覽器多次點擊 提交按鈕會顯示多次)

GIF1

場景二:提交表單後出現網路延遲,用戶進行刷新操作,也會重複提交表單(同樣貌似 Chrome 瀏覽器做了優化,反覆刷新也只提交一次,IE 刷新就會提交一次)

GIF2

場景三:場景二中的延遲出現時,進行後退有可能也會出現重複提價表單的情況

  1. 解決辦法

    • 方式一:利用 JavaScript 防止表單重複提交

      針對網路延遲的情況下用戶有時間多次點擊 submit 按鈕按鈕導致表單重複提交,js 的解決思路是:用 JS 控制 form 表單只能提交一次

      檢測是否提交過表單來控制提交:

      <script type="text/javascript">
          var isCommitted = false;//表單是否已經提交標識,默認為false
          function dosubmit(){
              if(isCommitted==false){
                   isCommitted = true;//提交表單後,將表單是否已經提交標識設置為true
                   return true;//返回true讓表單正常提交
               }else{
                   return false;//返回false那麼表單將不提交
               }
           }
      </script>
      

      還可以在表單提交後將按鈕設置為不可用:

      function dosubmit(){
          //獲取表單提交按鈕
          var btnSubmit = document.getElementById("submit");
          //將表單提交按鈕設置為不可用,這樣就可以避免用戶再次點擊提交按鈕
          btnSubmit.disabled= "disabled";
          //返回true讓表單可以正常提交
          return true;
      }
      

      使用 JS 只能解決場景一出現的情況。

  • 方式二:利用 Session 防止表單重複提交

具體思路:

​ 在伺服器生成一個唯一的隨機標識,專業術語稱為 Token(令牌),同時在當前用戶的 session 域中保存這個 Token;然後將 Token 發送到客戶端的 Form 表單中,在 Form 表單中使用隱藏域來存儲這個 Token,表單提交的時候連同這個 Token 一起提交到伺服器,然後在伺服器端判斷提交上來的 Token 與伺服器生成的 Token 是否一致,如果不一致,那就是重複提交了,此時伺服器端就可以不處理重複的表單;如果相同則處理提交的表單,處理完後清除當前用戶的 session 域中存儲的 Token 標識;

​ 伺服器拒絕處理用戶提交的表單請求的情況:

  • 存儲 session 域中的 Token(令牌) 與表單提交的 Token(令牌) 不同 —– 多次點擊提交按鈕;
  • 當前用戶的 session 中不存在 Token(令牌) —– 第一次提交成功後,伺服器會清除當前用戶 session 中存儲的 Token;
  • 用戶提交的表單數據中沒有 Token(令牌) —– 點擊後退時

具體程式碼:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 獲取並向 session 中存儲 token令牌的 servlet程式
 * @author YH
 * @create 2020-04-22 22:02
 */
public class TokenServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //生成 ToKen,創建令牌
        String token = TokenProccessor.getInstance().makeToken();
        System.out.println("在FormServlet中生成的token:"+token);
        //在伺服器使用session保存token(令牌)
        req.getSession().setAttribute("token", token);
        //跳轉到form表單頁面
        req.getRequestDispatcher("/form.jsp").forward(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
import sun.misc.BASE64Encoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;

/**
 * 單例設計模式創建生成 Token令牌 的工具類
 * @author YH
 * @create 2020-04-22 22:04
 */
public class TokenProccessor {
    private TokenProccessor(){}
    private static final TokenProccessor instance = new TokenProccessor();

    /**
     * 返回類對象實例
     * @return
     */
    public static TokenProccessor getInstance(){
        return instance;
    }

    /**
     * 生成Token
     * @return
     */
    public String makeToken(){
        String token = (System.currentTimeMillis() + new Random().nextInt(999999999) + "");
        //數據指紋 128位長 16個位元組 md5
        try{
            MessageDigest md = MessageDigest.getInstance("md5");
            byte md5[] = md.digest(token.getBytes());
            //base64編碼--任意而精緻編碼明文字元
            BASE64Encoder encoder = new BASE64Encoder();
            return encoder.encode(md5);
        }catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 判斷是否重複提交
 * @author YH
 * @create 2020-04-22 20:06
 */
public class DoFormServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //判斷用戶是否重複提交
        boolean b = isRepeatSubmit(req);
        if(b == true){
            System.out.println("請不要重複提交");
            return;
        }
        //移除session中的token
        req.getSession().removeAttribute("token");
        System.out.println("處理用戶提交請求!");

        //瀏覽器默認是以UTF-8編碼傳輸到伺服器的,所以需要設置伺服器以UTF-8的編碼進行接收,避免亂碼
        req.setCharacterEncoding("UTF-8");
        String username = req.getParameter("username");
        try{
            //讓當前執行緒沉睡3秒,模擬網路延遲
            Thread.sleep(3*1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(username + "向資料庫中插入數據");
    }

    /**
     * 判斷客戶端提交上來的令牌和伺服器端生成的令牌是否一致
     * @param req
     * @return true 重複提交 false 沒有重複
     */
    private boolean isRepeatSubmit(HttpServletRequest req) {
        String client_token = req.getParameter("token");
//        1.如果用戶提交的表單數據中沒有token,則用戶是重複提交了表單
        if(client_token == null){
            return true;
        }
        //取出存儲在Session中的token
        String session_token = (String)req.getSession().getAttribute("token");
//        2.如果session中不存在token,則表單重複提交
        if(session_token == null){
            return true;
        }
//        3.存儲在session的Token 與 表單提交的Token不同,則用戶是重複提交了表單
        if(!client_token.equals(session_token)){
            return true;
        }
        return false;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/form" method="post">
<%--        使用隱藏域存儲生成的token--%>
<%--        <input type="hidden" name="token" value="<%=session.getAttribute("token")%>">--%>
<%--    使用EL表達式取出存儲在session中的token--%>
    <input type="hidden" name="token" value="${token}"/>

        用戶名:<input type="text" name="username">
        <input type="submit" value="提交">
    </form>
</body>
</html>

首次訪問伺服器生成token存入專屬的session內:

image-20200422224017221

最終效果:

GIF

從運行效果中可以看到,通過這種方式處理表單重複提交,可以解決上述的場景二和場景三中出現的表單重複提交問題。

補充:cookie 及四種對象作用域

​ 客戶端在第一次請求服務端時,如果服務端發現此請求沒有 JSESSIONID,則會創建一個 name=JSESSIONID 的cookie,並返回給客戶端。

四種範圍對象(小 -> 大)

pageContext JSP 頁面容器 當前頁面有效(頁面跳轉後無效)

request 請求對象 同一次請求有效,其他請求無效(請求轉發一直都是一次請求 則有效,重定向後會丟失原請求數據發起新請求 則無效)

session 會話對象 同一次會話有效(無論怎麼跳轉都有效,關閉/切換瀏覽器後無效;即從 登陸 – 退出 之間 全部有效)

appliation 全局對象 全局有效(整個項目運行期間都有效;重啟服務、其他項目無效)

以上四個對象:通過 setAttribute() 賦值 getAttribute() 獲取屬性值;作用範圍越大的,性能開銷也越大

十二、JavaBean 及 JSP 簡單了解

12.1 JavaBean

Bean:在電腦英語中,有可重用組件的含義

JavaBean:用 Java 語言編寫的可重用組件(JavaBean 所表示的範圍 > 實體類)

​ JavaBean 遵循特定的寫法,特點如下:

  • 這個類必須有一個無參構造器
  • 屬性必須私有化
  • 私有化的屬性必須通過 public 類型的方法暴露給其他程式,且方法的命名需遵循規範
  • 使用層面,Java 可分為 2 大類:
    • 封裝業務邏輯的 JavaBean(如封裝 jdbc 操作的 DAO)
    • 封裝數據的 JavaBean(如對應於資料庫中一張表的實體類)

範例:

package yh.javabean;

public class Person {
    //-----------屬性私有化-----------
    private String name;
    private char sex;
    private int age;
    //-----------無參構造器-----------
    public class(){}
    //-----------設置/獲取屬性的public許可權的方法-----------
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
    public char getSex(){
        return sex;
    }
    public void setSex(char sex){
        this.sex = sex;
    }
    public int getAge(){
        return age;
    }
    public void setAge(int age){
        this.age = age;
    }
}

​ JavaBean 在 J2EE 開發中,通常用於封裝數據,對於遵循以上寫法的 JavaBean 組件,其他程式可以通過反射技術實例化 JavaBean 對象,並且通過反射那些遵循命名規範的方法,從而獲得 JavaBean 的屬性,進而調用其保存數據。

​ JavaBean 的屬性可以是任意類型,且可以有多個屬性。每個屬性需要有對應的 getter、setter 方法(稱為屬性修改器 和 屬性訪問器)。

​ 命名規範:遵循駝峰命名法,get/set 為小寫。

​ 屬性值也可以只有 get 或 set 方法,稱為只讀、只寫屬性。

12.2 JSP 原理

JSP 本質上是 Servlet,瀏覽器向伺服器發送請求時,不管訪問的是什麼資源,起始都是在訪問 Servlet。

​ 所以當訪問一個 jsp 頁面時,其實也是在訪問一個 Servlet,伺服器在執行 jsp 的時候,首先把 jsp 翻譯成一個 Servlet,所以我們訪問 jsp 時,其實不是在訪問 jsp,而是在訪問 jsp 翻譯後的哪個 Servlet。

訪問 jsp 頁面時伺服器執行流程圖:

img

第一次執行:

  1. 客戶端通過電腦連接伺服器,因為是請求是動態的,所以所有的請求交給 WEB 容器來處理
  2. 在容器中找到需要執行的 *.jsp 文件
  3. 之後 *.jsp 文件通過轉換變為 *.java 文件
  4. *.java 文件經過編譯後,形成 *.class 文件
  5. 最終伺服器要執行形成的 *.class 文件

第二次執行:

  1. 因為已經存在了 *.class 文件,所以不在需要轉換和編譯的過程

修改後執行:

​ \1. 源文件已經被修改過了,所以需要重新轉換,重新編譯。

​ 每個 jsp 頁面就是一個 java 源文件,且繼承 HttpJspBase 類,而 HttpJspBase 類又繼承於 HTTPServlet,可見 HttpJspBase 類本身就是一個 servlet,繼承它的 jsp 類也是。

  • 既然本質是 java 中的 Servlet,那麼其中的 HTML 程式碼又是如何實現的呢?

​ 在 jsp 中編寫的 java 程式碼和 html 程式碼都會被翻譯到_jspService 方法中去,java 程式碼原封不動地翻譯為 java 程式碼,而 HTML 程式碼則通過 out.write(“”); 輸出流,向網頁輸出 HTML 程式碼,最終通過瀏覽器解析展現

  • Web 伺服器在調用 jsp 實例時,會給 jsp 提供一些什麼 java 對象?

八大對象:

PageContext pageContext;
HttpSession session;
ServletContext application;
ServletConfig config;
JspWriter out;
Object page = this;
HttpServletRequest request,
HttpServletResponse response

其中 page/request/response 已經完成實例化,其他 5 個對象的實例化方式:

pageContext = _jspxFactory.getPageContext(this, request, response,null, true, 8192, true);
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();

十三、JDBC

13.1簡介

  1. 資料庫驅動

​ 安裝資料庫後,我們的程式是不可以直接進行資料庫連接的,需要對應的數據驅動(就像我們電腦的音效卡、網卡也不是插上就能用的,也需要驅動),通過驅動去和資料庫打交道,如圖:

image-20200423121143850

  1. JDBC

    ​ SUN 公司為了簡化、統一對數據的湊在哦、定義了一套 Java 操作資料庫的規範(介面),稱之為 JDBC(Java DataBase Connection)。這套介面由資料庫廠商去實現,這樣開發人員只需學習 jdbc 介面,並通過 jdbc 載入具體的驅動,就可以操作資料庫。

    image-20200423121303771

    組成 JDBC 的兩個包:java.sql、javax.sql,開發 JDBC 應用還需要導入相應資料庫的驅動。

  2. 相關API

    • DriverManager驅動管理類
    • Connection連接對象介面(通過 DriverManager 獲取對象)
      • createStatement():生成命令對象
      • preparedStatement():生成預編譯命令對象
    • Statement命令對象介面(通過 Connection 獲取對象)
      • executeQuery():執行SQL增刪改查語句,返回首影響行數
      • executeUpdate():執行SQL查詢語句,返回結果集
      • execure():執行任何SQL語句,返回boolean
    • prearedStatement預編譯命令對象介面(Statement 子類,也通過 Connection 獲取對象)
      • executeQuery():執行SQL增刪改查語句,返回首影響行數
      • executeUpdate():執行SQL查詢語句,返回結果集
      • execure():執行任何SQL語句,返回boolean
      • setXxxx(佔位符索引,佔位符的值):設置對應索引的佔位符的值,類型為XX類型
      • setObject(佔位符索引,佔位符的值):設置對應索引的佔位符的值,類型為Object類型
    • CallableStatement : 調用資料庫中的 存儲過程/存儲函數(Statement 子類,也通過 Connection 獲取對象)
    • ResultSet結果集對象介面(通過 Statement 獲取對象)
      • next():下移一行返回當前行是否有值(類似java迭代器)
      • previous():上移一行,返回當前行是否有值
      • getXX(列索引|列名|別名):返回對應列的值,接收類型為XX
      • getObject(列索引|列名|別名):返回對應列的值,接收類型為Object
        afterLast() :移動到resultSet的最後面。

各個 類、API 解釋:

  1. DriverManager 類

    JDBC 程式中 DriverManager 用於載入驅動,並創建與資料庫的連接,這個 API 的常用方法:

    • DriverManager.registerDriver(new Driver())
    • DriverManager.getConnection(url,user,password)

注意:實際開發中並不採用 registerDriver 方法註冊驅動,原因:

  1. 查看 Driver 源碼可以看到,如果採用此種方式,會導致驅動程式註冊兩次,在記憶體中創建兩個 Driver 對象
  2. 程式依賴 mysql 的 api,脫離 mysql 的 jar 包,程式將無法編譯,將來程式切花底層資料庫將會非常麻煩

​ 推薦方式:Class.forName(“com.mysql.jdbc.Driver”);

此種方式不會導致驅動對象在記憶體中重複出現,並且採用此種方式,程式只需要一個字元串,不需要依賴具體的驅動,使程式的靈活性更高。

  1. 資料庫 URL

    URL 用於標識資料庫的位置,通過 URL 地址告訴 JDBC 程式連接哪個資料庫,URL 的寫法為:

    image-20200423135844554

    常用資料庫 URL 地址的寫法:

    • MySQL 寫法:jdbc:mysql://localhost:3306/data
    • Oracle 寫法:jdbc:oracle:thin:@localhost:1521:sid
    • SqlServer 寫法:jdbc:microsoft:sqlserver://localhost:1433; DatabaseName=sid
  2. Connection 類

    JDBC 程式中的 Connection 用於代表資料庫的連接,Connection 是數據編程中最重要的一個對象,客戶端與資料庫所有交互都是通過 Connection 對象完成的,常用方法:

    • createStatement():創建向資料庫發送 SQL 的 statement 對象
    • prepareStatement(sql):創建向資料庫發送預編譯 SQL 的 PrepareStatement 對象
    • prepareCall(sql):創建執行存儲過程的 callableStatement 對象
    • setAutoCommit(boolean autoCommit):設置事務是否自動提交
    • commit():在鏈接上提交事務
    • rollback():在此連接上回滾事務
  3. Statement 類

    JDBC 程式中的 Statement 對選哪個用於向資料庫發送 SQL 語句,常用方法:

    • executeQuery(String sql):用於向資料庫發送查詢語句
    • executeUpdate(String sql):用於向資料庫發送 insert、update 或 delete 語句
    • execute(String sql):用於向資料庫發送任意 SQL 語句
    • addBatch(String sql):把多條 SQL 語句放到一個批處理中
    • executeBatch():向資料庫發送一批 SQL 語句執行
  4. PreparedStatement 類(常用)

​ PreparedStatement 是 Statement 的子類,它的實例對象可以通過 Connection.preparedStatement() 方法獲得,相對於 Statement 對象而言:Statement 對象而言:PreparedStatement 可以避免 sql 注入的問題。
​ Statement 會使資料庫頻繁編譯 sql,可能造成資料庫緩衝區溢出;而 PreparedStatement 可對 SQL 進行預編譯,從而提高資料庫的執行效率。並且 PreparedStatement 對於 SQL 中的參數,允許使用佔位符的形式進行替換,簡化 sql 語句的編寫。

  • 相比 Statement,PreparedStatement 有哪些優勢:
    1. 解決 Statement 的 SQL 語句拼串問題,防止 SQL 注入
    2. PreparedStatment 可以操作 Blod(而進位)數據,Statement不行
    3. PreparedStatement 可以實現更高效的批量操作
  1. ResultSet 類

JDBC 程式中用於表示 SQL 語句執行結果的對象,ResultSet 封裝執行結果時,採用的類似於表格的方式,內部維護了一個指向該表格數據行的游標,初始位置位於第一行數據的前一個,配合 next() 方法,移動游標並判斷是否為空。

​ 既然是結果集,所以提供的都是 get 方法

​ 獲取任意類型的數據:

​ getXxx(int index)

​ getXxx(String columnName)

​ ResultSet 還提供了對結果集進行滾動的方法:

​ next():移動到下一行

​ Previous():移動到前一行

​ absolute(int row):移動到指定行

​ beforeFirst():移動 resultSet 的最前面

​ afterLast():移動到 resultSet 的最後面

  1. 釋放資源

  Jdbc 程式運行完後,切記要釋放程式在運行過程中,創建的那些與資料庫進行交互的對象,這些對象通常是 ResultSet, Statement 和 Connection 對象,特別是 Connection 對象,它是非常稀有的資源,用完後必須馬上釋放,如果 Connection 不能及時、正確的關閉,極易導致系統宕機。Connection 的使用原則是盡量晚創建,盡量早的釋放。

​ 為確保資源釋放程式碼能運行,資源釋放程式碼也一定要放在 finally 語句中。

13.2 使用 JDBC 進行增刪改查

​ JDBC連接步驟:

  1. 讀取配置文件
  2. 註冊加驅動
  3. 創建連接
  4. 執行增刪改查
  5. 關閉資源

將其中1/2/3/5封裝成工具類

對JDBC連接過程通用部分封裝為工具類:

package jdbc;

import java.io.FileInputStream;
import java.sql.*;
import java.util.Properties;

/**
 * JDBC工具類
 * 功能:獲取連接、釋放資源
 * @author YH
 * @create 2020-04-23 16:00
 */
public class JDBCUtils {
    //連接資料庫需要的URL參數
    static String user;
    static String password;
    static String url;
    static String driver;
    //讀取配置文件(屬於共有操作,隨著類載入只執行一次,提升效率)
    static{
        //工具類中對可能出現的異常進行初步處理,省去調用者處理
        try {
            Properties properties = new Properties();
            properties.load(new FileInputStream("JDBC/src/main/resources/data.properties"));
            user = properties.getProperty("user");
            password = properties.getProperty("password");
            url = properties.getProperty("url");
            driver = properties.getProperty("driver");
            //註冊驅動
            Class.forName(driver);
        } catch (Exception e) {
            //將編譯時異常轉為運行時異常(提示的異常資訊更具體)
            throw new RuntimeException(e);
        }
    }

    /**
     * 獲取連接
     * @return
     */
    public static Connection getConnection(){
        try {
            return DriverManager.getConnection(url,user,password);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 功能:釋放資源
     * 沒有用到的資源參數留null
     * @param connection
     * @param statement 可以接收其子類對象
     * @param resultSet
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet){
        try {
            if(connection != null){
                connection.close();
            }
            if(statement != null){
                statement.close();
            }
            if(resultSet != null){
                resultSet.close();
            }
        } catch(SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

配置文件內容:

user=root
password=rootMySQL
url=jdbc:mysql://localhost:3306/class7
driver=com.mysql.jdbc.Driver

JDBC連接MySQL進行CRUD操作:

package jdbc;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 *
 * @author YH
 * @create 2020-04-23 15:42
 */
public class JDBCTest {
    public static void main(String[] args) throws SQLException {
        JDBCTest jdbcTest = new JDBCTest();
//        jdbcTest.select();
        jdbcTest.cud();
    }

    /**
     * 查詢操作
     */
    public void select() throws SQLException {
        Connection connection = JDBCUtils.getConnection();

        System.out.println("連接成功!");

        PreparedStatement statement = null;

        //查詢操作
        try {
            String sql = "SELECT * FROM usertest WHERE id BETWEEN 2 AND ?";
            statement = connection.prepareStatement(sql);
            //設置通配符的值
            statement.setString(1,"6");
            ResultSet set = statement.executeQuery();
            while(set.next()){
                String id = set.getString(1);
                String name = set.getString(2);
                String sex = set.getString(3);
                String age = set.getString(4);
                String password = set.getString(5);
                String telephone = set.getString(6);
                System.out.println("編號\t姓名\t性別\t年齡\t密碼\t電話");
                System.out.println(" " + id + "\t\t" + name + "\t" + sex + "\t\t" + age+"\t\t"
                        + password+"\t" + telephone);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            //關閉資源
            JDBCUtils.close(connection,statement,null);
        }
    }

    /**
     * 增刪改操作
     */
    public void cud(){
        //獲取連接
        Connection connection = JDBCUtils.getConnection();

        System.out.println("連接成功!");

        PreparedStatement cStatement = null;
        PreparedStatement uStatement = null;
        PreparedStatement dStatement = null;
        try {
            //增刪改操作
            //sql語句
            String cSql = "insert into usertest(id,name,sex) values(null,'張三','女'),(null,'李四','男');";
            String uSql = "update usertest set name='麻子',sex='女' where id=1;";
            String dSql = "delete from usertest where id=2;";

            //預編譯SQL語句
            cStatement = connection.prepareStatement(cSql);
            uStatement = connection.prepareStatement(uSql);
            dStatement = connection.prepareStatement(dSql);
            //執行SQL語句
//            int cLine = cStatement.executeUpdate();
//            System.out.println(cLine > 0 ? "創建並添加數據成功!" : "創建失敗!");

//            int uLine = uStatement.executeUpdate();
//            System.out.println(uLine > 0 ? "更新數據成功!" : "更新失敗!");
//
            int dLine = dStatement.executeUpdate();
            System.out.println(dLine > 0 ? "刪除數據成功!" : "刪除失敗");


        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(connection,cStatement,null);
            JDBCUtils.close(null,uStatement,null);
            JDBCUtils.close(null,dStatement,null);
        }
    }
}

原表中數據:

image-20200423210741833

查詢結果:

image-20200423172700026

增、改、刪效果:

image-20200423210956743

image-20200423211038538

image-20200423211120446

13.3 查詢結果集的處理

​ 查詢之所以與 CUD 區別開來關鍵在於查詢返回的是從資料庫獲取的數據,查詢不同的表就返回不同的結果集,我們需要對這些結果集進行妥善處理。

​ 對於查詢結果集可以直接輸出 或 存儲在數組、集合內使用,但是更好的方式是將數據封裝在 JavaBean 對象中,無論是調用還是傳遞都更加方便,且更符合 Java 萬事萬物皆對象的理念。

  • ORM 編程思想(Object Relational mapping 對象關係映射)

    • 一個數據表對應一個 java 類
    • 表中的一條記錄(行)對應 java 類的一個對象
    • 表中的一個欄位(本行的列)對應 java 類的一個屬性
  • Java與SQL對應數據類型轉換表

    Java類型 SQL類型
    boolean BIT
    byte TINYINT
    short SMALLINT
    int INTEGER
    long BIGINT
    String CHAR,VARCHAR,LONGVARCHAR
    byte array BINARY , VAR BINARY
    java.sql.Date DATE
    java.sql.Time TIME
    java.sql.Timestamp TIMESTAMP

1. ResultSet

  • 常用方法:

    • next():判斷下一行是否有數據,true 向下移動一行
    • getXxx():返回指定類型的數據,Xxx 為數據類型
  • 查詢需要調用 PreparedStatement 的 executeQuery() 方法,查詢結果是一個 ResultSet 對象;

  • ResultSet 對象以邏輯表格的形式封裝了執行資料庫操作的結果集,ResultSet 介面由資料庫廠商實現;

  • ResultSet 返回的實際是一張數據表,有一個指針指向數據表的第一條記錄的前面。初始時這個游標位於第一條記錄前(類似迭代器),通過其 next() 方法檢測是否有下一行,有則移動到下一行(相當於 Iterator 的 hasnext() 和 next() 結合,只是沒有返回當前指向數據值,它返回的是 boolean)

1555580152530

  • 當指正指向一行時,可以通過調用 getXxx(int index) 或 getXxx(int columnName) 獲取每一列的值。
    • 如:getInt(1),getString(“name”)

注意:Java 與資料庫的交互涉及到的相關 Java API 中的索引都從 1 開始

2. ResultSetMetaData 元數據

​ 圍繞數據進行解釋說明的資訊的稱為元數據。ResultSetMetaData 可用於獲取關於 ResultSet 對象中列的類型和屬性資訊的對象

​ ResultSetMetadata meta = ResultSet.getMetaData();

​ 常用方法:

  • getColumnCount():返回當前 ResultSet 對象的列數
  • getColumnName(int column):獲得指定列的名稱
  • isNullable(int column):指示指定列中的值是否可以為 null
  • getColumnLabel(int column):獲取指定列的別名
  • getColumnTypeName(int column):獲得指定列的類型
  • isAutoIncrement(int column):指示是否自動為指定列進行編號,這樣這些列仍然是只讀的
  • getColumnDisplaySize(int column):指示指定列的最大標準寬度,以字元為單位

1555579494691

問題1:得到結果集後,如何知道該結果集中有哪些列?列名是什麼?

​ 需要使用一個描述 ResultSet 的對象,即 ResultSetMetadata,配合 next() 通過 meta.getConlumnName(int column) 依次獲得列名(索引從 1 開始)

問題2:關於 ResultSetMetaData

1. 如何獲取 ResultSetMetaData:調用 ResultSet() 的 getMetaData() 方法即可
2. 獲取 ResultSet 中有多少列:調用 ResultSetMetaData 的 getColumnCount() 方法即可
3. 獲取 ResultSet 每一列的別名是什麼:調用 ResultSetMetaData 的 getColumnLabel() 方法

1555579816884

  1. 編寫通用的查詢方法
package jdbc;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 不同表格通用的查詢方法
 * @author YH
 * @create 2020-04-25 11:53
 */
public class ForQuery {
    /**
     * 測試方法
     */
    public static void main(String[] args){
        ForQuery f = new ForQuery();
        String sql = "SELECT user_id id,user_name name,sex,birthday FROM table_1 WHERE user_id=?;";
        Table_1 instance = f.getInstance(Table_1.class, sql, 1);
        System.out.println(instance.toString());
        System.out.println("-------------------------------------");

        String sql2 = "SELECT user_id id,user_name name,sex,birthday FROM table_1;";
        List<Table_1> list = f.getInstances(Table_1.class, sql2);
        //使用Lambda表達式(方法引用)
        list.forEach(System.out::println);
        }
    /**
     * 通用查詢表格單條數據的方法
     * @param clazz 表格對應的類的Class對象
     * @param sql   SQL執行語句
     * @param params  佔位符參數
     * @param <T>   表格要封裝成的對象類型
     * @return
     */
    public <T> T getInstance(Class<T> clazz,String sql,Object... params){
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            //1.獲取連接
            conn = JDBCUtils.getConnection();
            //2.預編譯SQL語句,獲得PreparedStatement對象
            statement = conn.prepareStatement(sql);
            //3.填充佔位符
            for (int i = 0; i < params.length; i++) {
                statement.setObject(i + 1,params[i]);
            }
            //4.執行查詢,獲得結果集
            resultSet = statement.executeQuery();
            //5.獲取結果集對象的元數據對象
            ResultSetMetaData metaData = resultSet.getMetaData();
            //獲取查詢結果集中數據的列數
            int columnCount = metaData.getColumnCount();

            if(resultSet.next()){
                //每有一條查詢記錄,生成一個對應的對象
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //獲取列值
                    Object columnValue = resultSet.getObject(i + 1);
                    //獲取列的別名(需要SQL語句中設置別名,否則獲取的就是原列名,總之就是獲取結果集中體現的列名)
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //6.利用反射獲取本對象的屬性對象,並進行賦值
                    //獲取field屬性對象
                    Field field = clazz.getDeclaredField(columnLabel);
                    //可訪問私有許可權屬性
                    field.setAccessible(true);
                    //給本對象的此屬性設置值
                    field.set(t,columnValue);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,statement,resultSet);
        }
        return null;
    }
    /**
     * 用List的形式返回查詢到的每條數據的對象集合
     * @param clazz 表格對應的類的Class對象
     * @param sql   SQL執行語句
     * @param params  佔位符參數
     * @param <T>   表格要封裝成的對象類型
     * @return
     */
    public <T> List<T> getInstances(Class<T> clazz,String sql,Object... params){
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            //1.獲取連接
            conn = JDBCUtils.getConnection();
            //2.預編譯SQL語句,獲得PreparedStatement對象
            statement = conn.prepareStatement(sql);
            //3.填充佔位符
            for (int i = 0; i < params.length; i++) {
                statement.setObject(i + 1,params[i]);
            }
            //4.執行查詢,獲得結果集
            resultSet = statement.executeQuery();
            //5.獲取結果集對象的元數據對象
            ResultSetMetaData metaData = resultSet.getMetaData();
            //獲取查詢結果集中數據的列數
            int columnCount = metaData.getColumnCount();
            List<T> list = new ArrayList<>();
            while(resultSet.next()){
                //每有一條查詢記錄,生成一個對應的對象
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //獲取列值
                    Object columnValue = resultSet.getObject(i + 1);
                    //獲取列的別名(需要SQL語句中設置別名,否則獲取的就是原列名,總之就是獲取結果集中體現的列名)
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //6.利用反射獲取本對象的屬性對象,並進行賦值
                    //獲取field屬性對象
                    Field field = clazz.getDeclaredField(columnLabel);
                    //可訪問私有許可權屬性
                    field.setAccessible(true);
                    //給本對象的此屬性設置值
                    field.set(t,columnValue);
                }
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,statement,resultSet);
        }
        return null;
    }
}

單條數據查詢效果:

image-20200425142204353

image-20200425142036851

多條數據查詢效果:

image-20200425145944396

image-20200425145906345

​ 需要注意的是:資料庫中的列名往往和我們定義的對象屬性名不同,我們需要將他們對應起來,這個中間橋樑就是:ResultSet 結果集

​ 針對錶的欄位名與類的屬性名不相同的情況:

  1. 必須在聲明 SQL 時,使用類的屬性名來命名欄位的別名

  2. 使用 ResultSetMetaData 時,需要使用 getColumnLabel() 來替換 getColumnName() 獲取列的別名

    如果 SQL 中沒有給欄位起別名,getColumnLabel() 獲取的就是列名

我們要讓查詢結果所組成的結果集的列名與我們自定義類的屬性一一對應,那麼就要將執行的 SQL 語句所查詢的欄位都設置上別名,且與屬性名對應,最終查詢的結果的列名就與屬性名對應上了,而後就可以通過反射直接設置列名對應屬性的值

查詢操作流程圖:

image-20200425104425004

JDBC API 小結

  • 兩種思想

    • 面向介面編程思想

      • 無論是獲取連接,執行 sql 語句等,都是介面引用我們獲取的對象,而實際調用方法的是介面的實現類對象
    • ORM 思想(Object Relational Mapping)

      • 一個數據表對應一個java類
      • 表中的一條記錄對應java類的一個對象
      • 表中的一個欄位對應java類的一個屬性

      sql 需要結合列名和對象的屬性名來寫,要起別名。

  • 兩種技術

    • JDBC 結果集的元數據:ResultSetMetaData
      • 獲取列數:getColumn()
      • 獲取列的別名:getColumn()
    • 通過反射,創建指定類的對象,獲取指定的屬性並賦值

13.4 批量插入

​ Java 支援批量更新機制,運行多條語句一次性提價給資料庫批量處理。通常比單獨提交處理更有效率

JDBC 的批量處理語句包括下面三個方法:

  • addBatch(String):添加需要批量處理的 SQL 語句或是參數;
  • executeBatch():執行批量處理語句
  • clearBatch():清空快取的數據

通常我們會遇到兩種批量執行 SQL 語句的情況:

  • 多條 SQL 語句的批量處理
  • 一個 SQL 語句的批量傳參

實現高效的批量操作

package jdbc;

import org.junit.Test;

import java.sql.Connection;
import java.sql.PreparedStatement;

/**
 * 高效的批量插入操作
 * 1:使用 addBatch() / executeBatch() / clearBatch() 方法
 * 2.mysql 伺服器默認是關閉批處理的,我們需要通過一個參數,讓 mysql 開啟批處理的支援
 *      ?rewriteBatchedStatements=true 下載配置文件的url後面
 * 3.使用更新的 mysql 驅動:mysql-connector-java-5.1.37-bin.jar
 * @author YH
 * @create 2020-04-26 9:27
 */
public class InsertsTest {
    @Test
    public void inserts() throws Exception {
        //記錄程式開始執行的時間戳
        long start = System.currentTimeMillis();

        //獲取連接
        Connection conn = JDBCUtils.getConnection();

        //1.設置不自動提交
        conn.setAutoCommit(false);

        String sql = "INSERT INTO tb_2(name) VALUES(?)";
        PreparedStatement statement = conn.prepareStatement(sql);
        for (int i = 1; i <= 1000000; i++) {
            statement.setString(1,"name_" + i);
            //1.攢sql
            statement.addBatch();
            //每攢500條sql語句執行一次
            if(i % 500 == 0){
                //2.執行sql
                statement.executeBatch();
                //2.清空快取
                statement.clearBatch();
            }
        }
        //2.提交請求
        conn.commit();

        //記錄程式執行完畢的時間戳,並得出最終用時
        long end = System.currentTimeMillis();
        System.out.println("花費時間為:" + (end - start));
    }
}

執行結果:

image-20200426112633237

​ **掉坑警告!不要手賤在 sql 語句後加分號 ; **,調試研究了半天才網上找到的解答,使用批處理時,不要用!,既然不用也可以,為了養成習慣 sql 語句都不要用;結尾了,會出現java.sql.BatchUpdateException異常,如下:

image-20200426110143163

13.5 資料庫事務

  1. 資料庫事務介紹

一組邏輯單元(如 一個或多個DML操作),時數據從一種狀態變換到另一種狀態的過程稱為事務

事務處理(事務操作):保證所有事務都作為一個工作單元來執行,即時出現了故障,也不能改變這種執行方式;當一個事務中執行多個操作時,要麼所有的事務都被提交(commit),則這些修改就永久地保存下來;要麼資料庫管理系統將放棄所有的修改,整個事務回滾(rollback)到最初狀態。

​ 這個過程需要確保一致性,數據的操縱應當是離散的成組的邏輯單元:當它全部完成時,數據的一致性可以保持;而當這個單元中的一部分操作失敗,整個事務應全部視為錯誤,所有起始點以後的操作應全部回退到開始狀態。

  1. JDBC 事務處理
  • 數據一旦提交,就不可回滾

  • 數據什麼時候意味著提交?

    • 當一個連接對象被創建時,默認情況下是自動提交事務,每次執行一個 sql 語句時,如果執行成功,就會向資料庫自動提交,而不能回滾;
    • 關閉資料庫連接,數據就會自動提交。如果多個操作,每個操作使用的是自己單獨的連接,則無法保證事務,即同一個事務的多個操作必須在同一個連接下。
  • JDBC 程式中為了讓多個 SQL 語句作為一個事務執行:

    • 調用 Connection 對象的 setAutoCommit(false); 以取消自動提交事務;
    • 在所有的 SQL 語句都成執行後,調用 commit(); 方法提交事務;
    • 在出現異常時,調用 rollback(); 方法回滾事務;

    若此時 Connection 連接沒有被關閉,還可能被重複使用,那麼需要恢復其自動提交狀態 setAutoCommit(true)。尤其是在使用資料庫連接池技術時,執行 close() 方法前,最好恢復自動提交狀態。

【案例:用戶 AA 向用戶 BB 轉賬 100】

/**
     * JDBC處理數據轉賬案例
     */
    @Test
    public void transfer(){
        Connection conn = null;
        try {
            //1.獲取連接
            conn = JDBCUtils.getConnection();
            //2.取消自動提交事務
            conn.setAutoCommit(false);
            //3.進行資料庫操作
            String subSql = "update user_table set balance=balance-100 where user=?";
            //將執行修改數據的操作封裝成方法來調用
            update(conn,subSql,"AA");

            //模擬出現異常
//            System.out.println(1 / 0);

            String addSql = "update user_table set balance=balance+100 where user=?";
            //將執行修改數據的操作封裝成方法來調用
            update(conn,addSql,"BB");

            //4.沒有出現異常,提交事務
            conn.commit();
        //注意異常接收的類型,如果不能捕獲到出現的異常,也就不能處理,也就執行程式碼塊內的回滾操作
        } catch (Exception e) {
            e.printStackTrace();
            //5.出現了異常,則回滾事務
            try {
                conn.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            //6.關閉資源前恢復每次DML操作的自動提交功能(針對使用資料庫連接池時)
            try {
                conn.setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            //7.關閉資源
            JDBCUtils.close(conn,null,null);
        }
    }

執行增刪改操作的方法:

/**
     * @description 使用事務後的通用更新操作
     * @param conn 連接
     * @param sql 執行的sql語句
     * @param params 佔位符的參數
     */
    public void update(Connection conn,String sql,Object... params){
        PreparedStatement ps = null;
        try {
            ps = conn.prepareStatement(sql);

            //填充佔位符
            for (int i = 0; i < params.length; i++) {
                ps.setObject(i + 1,params[i]);
            }
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,null);
        }
    }
}

擴展:事務的 ACID 屬性

  1. 原子性(Atomicity)

    原子性是指事務是一個不可分割的一個工作單位,事務中的操作要麼都發生,要麼都不發生

  2. 一致性(Consistency)

    事務必須使資料庫從一個一致性狀態變換到另一個一致性狀態

  3. 隔離性(Isolation)

    事務的隔離性是指一個事務的執行不能被其他事務干擾,即一個事務內部的操作及使用的數據對並發的其他事務是隔離的,並發執行的各個事務之間互不干擾

  4. 持久性(Durability)

    持久性是指一個事務一旦被提交,它對資料庫中數據的改變就是永久性的,接下來的其他操作和資料庫故障不應該對其有任何影響

  • 資料庫的並發問題
    • 對於同時運行的多個事務,當這些事務訪問資料庫中相同的數據時,如果沒有採取必要的隔離措施機制,就會導致各種並發問題:
      • 臟讀:對於兩個事務 T1、T2, T1 讀取了已經被 T2 更新但還沒有被提交的欄位。之後, 若 T2 回滾, T1讀取的內容就是臨時且無效的。
      • 不可重複讀: 對於兩個事務T1, T2, T1 讀取了一個欄位, 然後 T2 更新了該欄位。之後, T1再次讀取同一個欄位, 值就不同了。
      • 幻讀: 對於兩個事務T1, T2, T1 從一個表中讀取了一個欄位, 然後 T2 在該表中插入了一些新的行。之後, 如果 T1 再次讀取同一個表, 就會多出幾行。
  • 資料庫事務的隔離性:資料庫系統必須具有隔離並發運行各個事務的能力,使它們不會相互影響,避免各種並發問題。
  • 一個事務與其他事務的隔離程度稱為隔離級別。資料庫規定了多種事務隔離級別,不同隔離級別對應不同的干擾程度,隔離級別越高,數據一致性就越好,但並發性越弱
    • 四種隔離級別:

1555586275271

​ Oracle 支援的 2 種事務隔離級別:READ COMMITED,SERIALIZABLE。 Oracle 默認的事務隔離級別為: READ COMMITED

​ Mysql 支援 4 種事務隔離級別。Mysql 默認的事務隔離級別為: REPEATABLE READ。

  • 在 MySQL 中設置隔離級別

    ​ 每啟動一個 mysql 程式,就會獲得一個單純的資料庫連接,每個資料庫連接都有一個全局變數 @@tx_isolation,表示當前的事務隔離級別。

    • 查看當前的隔離級別:

      SELECT @@tx_isolation;
      
    • 設置當前 mysql 連接的隔離級別:

      set  transaction isolation level read committed;
      
    • 設置資料庫系統的全局隔離級別:

      set global transaction isolation level read committed;
      
    • 補充操作:[ tom 為用戶名]

      • 創建 mysql 資料庫用戶:

        create user tom identified by 'abc123';
        
      • 授予許可權

        #授予通過網路方式登錄的tom用戶,對所有庫所有表的全部許可權,密碼設為abc123.
        grant all privileges on *.* to tom@'%'  identified by 'abc123'; 
        
         #給tom用戶使用本地命令行方式,授予atguigudb這個庫下的所有表的插刪改查的許可權。
        grant select,insert,delete,update on atguigudb.* to tom@localhost identified by 'abc123'; 
        
        

13.6 DAO 及相關實現類

DAO(Data Access Object): 訪問數據資訊的類和介面,包括了對數據的 CRUD(Create Retrival、Update、Delete),而不包括任何業務相關的資訊。也稱為 BaseDAO

作用:為了實現功能的模組化,更有利於程式碼的維護和升級。

操作範例:

針對數據表 customers 進行操作:(如圖為被 CRUD 後的數據)

image-20200427152443593

BaseDAO.java :

package dao;

import jdbc.JDBCUtils;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description 封裝針對數據表通用的操作
 * @author YH
 * @create 2020-04-27 6:47
 */
public class BaseDAO {
    /**
     * @description 通用的增刪改操作(考慮上事務)
     * @param conn 資料庫連接對選哪個
     * @param sql 執行的SQL語句
     * @param params 佔位符參數
     * @return
     */
    public int update(Connection conn,String sql, Object...params) {
        PreparedStatement ps = null;
        try {
            //1.預編譯sql語句,獲取PrepareStatement的實例
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                //2.填充佔位符
                ps.setObject(i + 1,params[i]);
            }
            //執行並返回受影響行數
            return  ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,null);
        }
        return 0;
    }

    /**
     * 通用查詢操作,用於返回封裝數據表中一條記錄的對象(考慮上事務)
     * @param conn 資料庫連接
     * @param clazz 表格JavaBean類的Class對象
     * @param sql 要執行的SQL語句
     * @param params 佔位符參數
     * @param <T> 表格JavaBean類型
     * @return
     */
    public <T> T getInstance(Connection conn,Class<T> clazz,String sql,Object...params){
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            //1.預編譯SQL語句,獲取PrepareStatement實例
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                //2.設置佔位符
                ps.setObject(i + 1,params[i]);
            }
            //3.執行,獲取結果集
            rs = ps.executeQuery();
            //4.獲取結果集元數據對象
            ResultSetMetaData metaData = rs.getMetaData();
            //5.獲取結果集的列數
            int columnCount = metaData.getColumnCount();
            //6.判斷結果集中是否有記錄
            if(rs.next()) {
                //6.1 創建對象實例
                T t = clazz.newInstance();
                //遍歷結果集中的每一列
                for (int i = 0; i < columnCount; i++) {
                    //6.2獲取列值
                    Object columnValue = rs.getObject(i + 1);
                    //6.3獲取列的別名(列名)
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //7.利用反射獲取本對象的field對象
                    Field field = clazz.getDeclaredField(columnLabel);
                    //7.1設置私有屬性可被訪問
                    field.setAccessible(true);
                    //7.2給本對象此屬性賦值
                    field.set(t,columnValue);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,rs);
        }
        return null;
    }
    /**
     * 通用查詢操作,用於返回封裝數據表中所有記錄的對象集合(考慮上事務)
     * @param conn 資料庫連接
     * @param clazz 表格JavaBean類的Class對象
     * @param sql 要執行的SQL語句
     * @param params 佔位符參數
     * @param <T> 表格JavaBean類型
     * @return
     */
    public <T> List<T> getForList(Connection conn, Class<T> clazz, String sql, Object...params){
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            //1.預編譯SQL語句,獲取PrepareStatement實例
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                //2.設置佔位符
                ps.setObject(i + 1,params[i]);
            }
            //3.執行,獲取結果集
            rs = ps.executeQuery();
            //4.獲取結果集元數據對象
            ResultSetMetaData metaData = rs.getMetaData();
            //5.獲取結果集的列數
            int columnCount = metaData.getColumnCount();
            //6.聲明一個存儲JavaBean對象的List
            List<T> list = new ArrayList<>();
            //6.判斷結果集中是否有記錄
            while(rs.next()) {
                //6.1 創建對象實例
                T t = clazz.newInstance();
                //遍歷結果集中的每一列
                for (int i = 0; i < columnCount; i++) {
                    //6.2獲取列值
                    Object columnValue = rs.getObject(i + 1);
                    //6.3獲取列的別名(列名)
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //7.利用反射獲取本對象的field對象
                    Field field = clazz.getDeclaredField(columnLabel);
                    //7.1設置私有屬性可被訪問
                    field.setAccessible(true);
                    //7.2給本對象此屬性賦值
                    field.set(t,columnValue);
                }
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,rs);
        }
        return null;
    }

    /**
     * 用於查詢特殊值的通用方法(如最大值、總數、平均值等)
     * @param conn 資料庫連接
     * @param sql 要執行的SQL語句
     * @param params 佔位符參數
     * @param <E> 返回的數據類型
     * @return
     */
    public <E> E getValue(Connection conn,String sql,Object...params){
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                ps.setObject(i + 1,params[i]);
            }
            rs = ps.executeQuery();
            if(rs.next()){
                return (E) rs.getObject(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,rs);
        }
        return null;
    }
}

CustomerDAO.java :

package dao;

import bean.Customer;

import java.sql.Connection;
import java.util.Date;
import java.util.List;

/**
 * @description 此介面用於規範針對於customers表的常用操作
 * @author YH
 * @create 2020-04-27 10:03
 */
public interface CustomerDAO {
    /**
     * @description 將cust對象添加到資料庫中
     * @param conn
     * @param cust
     */
    void insert(Connection conn, Customer cust);

    /**
     * 通過指定id刪除記錄
     * @param conn
     * @param id
     */
    void deleteById(Connection conn,int id);

    /**
     * 針對記憶體中的cust對象,去修改數據表中指定的記錄
     * @param conn
     * @param cust
     */
    void update(Connection conn,Customer cust);

    /**
     * 通過指定id查詢得到對應的Customer對象
     * @param conn
     * @param id
     * @return
     */
    Customer getCustomerById(Connection conn,int id);

    /**
     * 查詢返回表中所有記錄的對象構成的List集合
     * @param conn
     * @return
     */
    List<Customer> getAll(Connection conn);

    /**
     * 查詢表中所有數據的條目數
     * @param conn
     * @return
     */
    long getCount(Connection conn);

    /**
     * 返回數據表中最大的生日
     * @param conn
     * @return
     */
    Date getMaxBirth(Connection conn);
}

CustomerDAOImp.java

package dao;

import bean.Customer;

import java.sql.Connection;
import java.sql.Date;
import java.util.List;

/**
 * 針對Customer表的具體實現
 * @author YH
 * @create 2020-04-27 10:13
 */
public class CustomerDAOImp extends BaseDAO implements CustomerDAO {

    @Override
    public void insert(Connection conn, Customer cust) {
        //向資料庫插入數據的sql語句
        String sql = "insert into customers(name,email,birth) values(?,?,?)";
        //使用父類BaseDAO通用的更新方法
        update(conn,sql,cust.getName(),cust.getEmail(),cust.getBirth());
    }

    @Override
    public void deleteById(Connection conn,int id){
        String sql = "delete from customers where id=?";
        update(conn,sql,id);
    }

    @Override
    public void update(Connection conn,Customer cust){
        String sql = "update customers set name=?,email=?,birth=? where id=?";
        update(conn,sql,cust.getName(),cust.getEmail(),cust.getBirth(),cust.getId());
    }

    @Override
    public Customer getCustomerById(Connection conn, int id){
        String sql = "select id,name,email,birth from customers where id=?";
        return getInstance(conn,Customer.class,sql,id);
    }

    @Override
    public List<Customer> getAll(Connection conn){
        String sql = "select id,name,email,birth from customers";
        return getForList(conn,Customer.class,sql);
    }

    @Override
    public long getCount(Connection conn){
        String sql = "select count(*) from customers";
        return getValue(conn,sql);
    }

    @Override
    public Date getMaxBirth(Connection conn){
        String sql = "select max(birth) from customers";
        return getValue(conn,sql);
    }

    /**
     * 添加中文時出現SQL異常並提示字元串值不正確,需修改編碼解決
     * @param conn
     * @param character
     */
    public void charcter(Connection conn,String character){
        String sql = "alter table customers change name name varchar(20) character set ?";
        update(conn,sql,character);
    }

    /**
     *
     */

}

測試程式碼:

package junit;

import bean.Customer;
import dao.CustomerDAOImp;
import jdbc.JDBCUtils;
import org.junit.Test;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;

/**
 * @author YH
 * @create 2020-04-27 11:28
 */
public class CustomerDAOImpTest {
    CustomerDAOImp dao = new CustomerDAOImp();

    @Test
    public void testCharset(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            dao.charcter(conn,"utf8");
            System.out.println("編碼修改成功");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testInert(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            Customer cust = new Customer(1, "雲", "[email protected]", new Date(43534646435L));
            dao.insert(conn, cust);
            System.out.println("添加成功!");
        } catch (SQLException e) {
            e.printStackTrace();
        }finally{
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testDeleteById(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            dao.deleteById(conn,1);
            System.out.println("刪除成功");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testUpdate(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            dao.update(conn,new Customer(5,"雲翯","[email protected]",new Date(349803493049L)));
            System.out.println("修改成功");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testGetCustomerById(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            Customer customer = dao.getCustomerById(conn, 5);
            System.out.println(customer.toString());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testGetAll(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            List<Customer> list = dao.getAll(conn);
            list.forEach(System.out::println);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testGetCount(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            System.out.println(dao.getCount(conn));
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testGetMaxBirth(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            System.out.println(dao.getMaxBirth(conn).toString());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

}

個人體會:

  1. 首先封裝一個通用操作的 DAO(或BaseDAO)作為一個實現資料庫和 Java 之間交互的基礎,封裝了針對數據表的通用操作;
  2. 建立針對特定表的規範,即提供對應表格的介面並聲明常用操作的方法;
  3. 最後創建繼承 DAO(或 BaseDAO)、且實現表格對應介面的實現類,進行具體的交互(即 CRUD 等操作)

13.7 資料庫連接池

  • JDBC 資料庫連接池的必要性

    在使用開發基於資料庫的 web 程式時,傳統模式基本是:在主程式(如servlet、beans)中建立連接、進行 sql 操作、斷開資料庫連接,這三個步驟。這種模式開發,存在的問題:

    • 每次向資料庫要求一個連接都要經過將 Connection 載入到記憶體,在驗證用戶名和密碼(花費0.05s~1s),執行完後在斷開連接,這種方式會消耗大量資料庫資源,資料庫連接的資源並沒有得到很好的重複利用。若同時又幾百人甚至幾千人在線可能會導致伺服器崩潰。
    • 對於每一次資料庫連接,使用完成都得斷開。如果未能關閉,將會導致資料庫系統中的記憶體泄漏。
    • 不能控制被創建的連接對象樹。系統資源會被毫無顧忌的分配出去,如連接過多也可能導致記憶體泄漏,伺服器崩潰。
  • 資料庫庫連接池技術
  1. 使用資料庫連接池優化程式性能

    資料庫連接池的基本思想就是為資料庫連接建立一個「緩衝池」。預先在緩衝池中放入一定數量的連接,當需要建立資料庫連接時,只需從「緩衝池」中取出一個,使用完畢之後再放回去。資料庫連接池負責分配、管理和釋放資料庫連接,它允許應用程式重複使用一個現有的資料庫連接,而不是重新建立一個。連接池的最大資料庫連接數量限定了這個連接池能佔有的最大連接數,當應用程式向連接池請求的連接數超過最大連接數量時,這些請求將被加入到等待隊列中。如圖:

    1555593464033

    工作原理:

    1555593598606

    ​ 資料庫在初始化時就創建一定量的連接放到資料庫連接池,通過設置最小資料庫連接數(和最大資料庫連接數來限定資料庫連接池的連接數量,當應用程式向資料庫請求的連接數量大於資料庫連接池中的數量時,這些請求將被加入等待隊列中。

    ​ 資料庫中的最小連接數和最大連接數的設置要考慮的因素:

    • 最小連接數:是連接池一直保持的資料庫連接,所以如果應用程式對資料庫連接的使用量不大,未被使用的資料庫連接將被浪費;
    • 最大連接數:是連接池能申請的最大連接數,如果資料庫連接請求超過次數,後面的資料庫連接請求將被加入到等待隊列中,這會影響後來的資料庫操作;

資料庫連接池的優點:

  1. 資源重用

    資料庫連接得以重用,避免了頻繁創建、釋放連接所要開銷,同時增加了系統運行環境的平穩性。

  2. 更快的反應速度

    資料庫連接池初始化過程中往往已經創建了若干連接備用,所以對於業務請求處理而言,可以直接利用現有的連接,避免了資料庫連接初始化和釋放過程的時間開銷。

  3. 新的資源分配手段

    對於多重應用共享同一資料庫而言,可在應用層通過資料庫連接池的配置,實現某一應用最大可用資料庫連接數的限制,避免某一應用獨佔所有的資料庫資源。

  4. 同一的連接管理,避免資料庫連接泄漏

    可預設佔用超時參數,強制回收被佔用連接,從而避免了常規資料庫連接操作中可能出現的資源泄漏。

  5. 使用開源的資料庫連接池

    開發的資料庫連接池是第三方對 DataSoruce 的實現(編寫連接池需實現 java.sql.DataSource 介面),即連接池的實現,也稱為數據源

    注意:

    • 數據源和資料庫連接不同,數據源無需創建多個,它是產生資料庫連接的工廠,因此整個應用只需要一個數據源即可。

    • 當資料庫訪問結束後,程式還是像以前一樣關閉資料庫連接:conn.close(); 但conn.close()並沒有關閉資料庫的物理連接,它僅僅把資料庫連接釋放,歸還給了資料庫連接池。

    一些開源組織提供的數據源的獨立實現:

    • DBCP
    • C3P0 Tomcat 伺服器默認
    • Druid(德魯伊)
  6. Druid(德魯伊)數據源

    Druid 是阿里開源平台上的一個資料庫連接池實現,有強大的日誌監控功能,據說是目前最好的連接池。

    使用範例:

    import com.alibaba.druid.pool.DruidDataSourceFactory;
    
    import javax.sql.DataSource;
    import java.io.FileInputStream;
    import java.sql.Connection;
    import java.util.Properties;
    /**
     * 使用Druid連接池獲取連接
     * @author YH
     * @create 2020-04-24 10:31
     */
    public class DruidTest {
        public static void main(String[] args) throws Exception{
            Properties pro = new Properties();
            pro.load(new FileInputStream("JDBC/src/main/resources/druid.properties"));
            DataSource ds = DruidDataSourceFactory.createDataSource(pro);
            int i = 0;
            while(i < 100){
                Connection conn = ds.getConnection();
                System.out.println("第" + i + "個連接:" + conn);
                i++;
            }
        }
    }
    
    url=jdbc:mysql://localhost:3306/class7?rewriteBatchedStatements=true
    username=root
    password=rootMySQL
    driverClassName=com.mysql.jdbc.Driver
    initialSize=10
    maxActive=20
    maxWait=100
    filters=wall
    

    可以看出,配置文件中設置了maxActive 最大活躍數後,只能同時獲取20個連接

    image-20200424105154116

如果及時釋放連接:

image-20200424115253063

  1. DruidDataSource 配置屬性:
配置 預設值 說明
name 配置這個屬性的意義在於,如果存在多個數據源,監控的時候可以通過名字來區分開來。如果沒有配置,將會生成一個名字,格式是:”DataSource-” + System.identityHashCode(this). 另外配置此屬性至少在1.0.5版本中是不起作用的,強行設置name會出錯。詳情-點此處
url 連接資料庫的url,不同資料庫不一樣。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 連接資料庫的用戶名
password 連接資料庫的密碼。如果你不希望密碼直接寫在配置文件中,可以使用ConfigFilter。詳細看這裡
driverClassName 根據url自動識別 這一項可配可不配,如果不配置druid會根據url自動識別dbType,然後選擇相應的driverClassName
initialSize 0 初始化時建立物理連接的個數。初始化發生在顯示調用init方法,或者第一次getConnection時
maxActive 8 最大連接池數量
maxIdle 8 已經不再使用,配置了也沒效果
minIdle 最小連接池數量
maxWait 獲取連接時最大等待時間,單位毫秒。配置了maxWait之後,預設啟用公平鎖,並發效率會有所下降,如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖。
poolPreparedStatements false 是否快取preparedStatement,也就是PSCache。PSCache對支援游標的資料庫性能提升巨大,比如說oracle。在mysql下建議關閉。
maxPoolPreparedStatementPerConnectionSize -1 要啟用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改為true。在Druid中,不會存在Oracle下PSCache佔用記憶體過多的問題,可以把這個數值配置大一些,比如說100
validationQuery 用來檢測連接是否有效的sql,要求是一個查詢語句,常用select ‘x’。如果validationQuery為null,testOnBorrow、testOnReturn、testWhileIdle都不會起作用。
validationQueryTimeout 單位:秒,檢測連接是否有效的超時時間。底層調用jdbc Statement對象的void setQueryTimeout(int seconds)方法
testOnBorrow true 申請連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。
testOnReturn false 歸還連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。
testWhileIdle false 建議配置為true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閑時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
keepAlive false (1.0.28) 連接池中的minIdle數量以內的連接,空閑時間超過minEvictableIdleTimeMillis,則會執行keepAlive操作。
timeBetweenEvictionRunsMillis 1分鐘(1.0.14) 有兩個含義: 1) Destroy執行緒會檢測連接的間隔時間,如果連接空閑時間大於等於minEvictableIdleTimeMillis則關閉物理連接。 2) testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明
numTestsPerEvictionRun 30分鐘(1.0.14) 不再使用,一個DruidDataSource只支援一個EvictionRun
minEvictableIdleTimeMillis 連接保持空閑而不被驅逐的最小時間
connectionInitSqls 物理連接初始化的時候執行的sql
exceptionSorter 根據dbType自動識別 當資料庫拋出一些不可恢復的異常時,拋棄連接
filters 屬性類型是字元串,通過別名的方式配置擴展插件,常用的插件有: 監控統計用的filter:stat 日誌用的filter:log4j 防禦sql注入的filter:wall
proxyFilters 類型是List<com.alibaba.druid.filter.Filter>,如果同時配置了filters和proxyFilters,是組合關係,並非替換關係

13.8 Apache-DBUtils 實現 CRUD 操作

​ commons-dbutils 是 Apache 組織提供的一個開源 JDBC 工具類,它是對 JDBC 的簡單封裝

  • API
    • org.apache.commons.dbutils.QueryRunner :提供資料庫操作的一系列重載的 update() 和 query() 操作(類似上面的 BaseDAO);
    • 介面 org.apache.commons.dbutils.ResultSetHandler:此介面用於處理查詢返回的結果集,不同的結果集情形由其 不同的子類來實現(類似上面針對特定表格規範的介面);
    • 工具類:org.apache.commons.dbutils.DbUtils:提供如關閉連接、裝載JDBC驅動程式等常規工作的工具類,裡面的所有方法都是靜態的。主要方法如下
      • public static void close(...) throws java.sql.SQLException:DbUtils類提供了三個重載的關閉方法。這些方法檢查所提供的參數是不是NULL,如果不是的話,它們就關閉Connection、Statement和ResultSet。
      • public static void closeQuietly(...):這一類方法不僅能在Connection、Statement和ResultSet為NULL情況下避免關閉,還能隱藏一些在程式中拋出的SQLEeception。
      • public static void commitAndClose(Connection conn) throws SQLException:用來提交連接的事務,然後關閉連接。
      • public static commitAndCloseQuietly(Connection conn):用來提交連接,然後關閉連接,並且在關閉連接時不拋出SQL異常。
      • public static rollback(Connection conn) throws SQLException:回滾給定連接所做的修改(允許 conn 為 null,因為方法內部做了判斷)
      • public static void rollbackAndClose(Connection conn)throws SQLException:回滾給定連接所做的修改並關閉資源
      • public static void rollbackAndCloseQuietly(Connection):回滾給定連接所做的修改並關閉資源,並且在關閉連接時不拋出SQL異常
      • public static boolean loadDriver(java.lang.String driverClassName):這一方裝載並註冊JDBC驅動程式,如果成功就返回true。使用該方法,你不需要捕捉這個異常ClassNotFoundException。
package dbutils;

import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.junit.Test;

import com.atguigu2.bean.Customer;
import com.atguigu4.util.JDBCUtils;

/*
 * commons-dbutils 是 Apache 組織提供的一個開源 JDBC工具類庫,封裝了針對於資料庫的增刪改查操作
 * 
 */
public class QueryRunnerTest {
	
	//測試插入
	@Test
	public void testInsert() {
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "insert into customers(name,email,birth)values(?,?,?)";
			int insertCount = runner.update(conn, sql, "蔡徐坤","[email protected]","1997-09-08");
			System.out.println("添加了" + insertCount + "條記錄");
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);
		}
	}
	
	//測試查詢
	/*
	 * BeanHander:是ResultSetHandler介面的實現類,用於封裝表中的一條記錄。
	 */
	@Test
	public void testQuery1(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "select id,name,email,birth from customers where id = ?";
			BeanHandler<Customer> handler = new BeanHandler<>(Customer.class);
			Customer customer = runner.query(conn, sql, handler, 23);
			System.out.println(customer);
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);
		}
	}
	
	/*
	 * BeanListHandler:是ResultSetHandler介面的實現類,用於封裝表中的多條記錄構成的集合。
	 */
	@Test
	public void testQuery2() {
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "select id,name,email,birth from customers where id < ?";
			
			BeanListHandler<Customer>  handler = new BeanListHandler<>(Customer.class);

			List<Customer> list = runner.query(conn, sql, handler, 23);
			list.forEach(System.out::println);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);
		}
	}
	
	/*
	 * MapHander:是ResultSetHandler介面的實現類,對應表中的一條記錄。
	 * 將欄位及相應欄位的值作為map中的key和value
	 */
	@Test
	public void testQuery3(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "select id,name,email,birth from customers where id = ?";
			MapHandler handler = new MapHandler();
			Map<String, Object> map = runner.query(conn, sql, handler, 23);
			System.out.println(map);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);	
		}
		
	}
	
	/*
	 * MapListHander:是ResultSetHandler介面的實現類,對應表中的多條記錄。
	 * 將欄位及相應欄位的值作為map中的key和value。將這些map添加到List中
	 */
	@Test
	public void testQuery4(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "select id,name,email,birth from customers where id < ?";
		
			MapListHandler handler = new MapListHandler();
			List<Map<String, Object>> list = runner.query(conn, sql, handler, 23);
			list.forEach(System.out::println);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);	
		}
		
	}
	/*
	 * ScalarHandler:用於查詢特殊值
	 */
	@Test
	public void testQuery5(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			
			String sql = "select count(*) from customers";
			
			ScalarHandler handler = new ScalarHandler();
			
			Long count = (Long) runner.query(conn, sql, handler);
			System.out.println(count);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);	
		}
	}
	@Test
	public void testQuery6(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			
			String sql = "select max(birth) from customers";
			
			ScalarHandler handler = new ScalarHandler();
			Date maxBirth = (Date) runner.query(conn, sql, handler);
			System.out.println(maxBirth);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);	
		}
	}

	/*
	 * 自定義ResultSetHandler的實現類
	 */
	@Test
	public void testQuery7(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			
			String sql = "select id,name,email,birth from customers where id = ?";
			ResultSetHandler<Customer> handler = new ResultSetHandler<Customer>(){

				@Override
				public Customer handle(ResultSet rs) throws SQLException {
//					System.out.println("handle");
//					return null;
//					return new Customer(12, "成龍", "[email protected]", new Date(234324234324L));
					
					if(rs.next()){
						int id = rs.getInt("id");
						String name = rs.getString("name");
						String email = rs.getString("email");
						Date birth = rs.getDate("birth");
						Customer customer = new Customer(id, name, email, birth);
						return customer;
					}
					return null;
				}
			};
			Customer customer = runner.query(conn, sql, handler,23);
			System.out.println(customer);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);
		}
	}
}

十四、Filter 過濾器

14.1 Filter 簡介

​ Filter 也稱為過濾器,通過 Filter 技術可對 web 伺服器管理所有的 web 資源:例如 jsp、Servlet 靜態圖片文件或靜態 HTML 文件等進行攔截,從而實現一些特殊的功能。例如實現 URL 級別的許可權訪問控制、過濾敏感辭彙、壓縮響應資訊等一些高級功能。

​ Servlet API 中提供了一個 Filter 介面,開發 web 應用時,如果編寫的 Java 類實現了這個介面,則把這個 java 類稱之為過濾器 Filter。通過 Filter 技術,開發人員可實現用戶在訪問某個目標資源之前,對訪問的請求和響應進行攔截,如下所示:

image-20200428103042792

  • 過濾器位於客戶端和web應用程式之間,用於檢查和修改兩者之間流過的請求和響應;
  • 在請求到達Servlet/JSP之前,過濾器截獲請求;
  • 在響應送給客戶端之前,過濾器截獲響應;
  • 最先截獲客戶端請求的過濾器將最後截獲Servlet/JSP的響應資訊;

多個過濾器形成一個過濾器鏈,過濾器鏈中不同過濾器的先後順序由部署文件web.xml中過濾器映射的順序決定,過程如下:

image-20200428114707920

14.2 Filter 是如何實現攔截

​ Filter 介面中有一個 doFilter 方法,當我們編寫好 Filter,並配置對哪個 web 資源進行攔截後,web 伺服器每次在調用 web 資源的 service 方法之前,都會先調用一下 Filter 的 doFilter() 方法,因此該方法內編寫的編寫的程式碼可以達到如下目的:

  • 調用目標資源之前,讓一段程式碼執行;

  • 是否調用目標資源,即是否讓用戶訪問 web 資源;

  • 調用目標資源後,讓一段程式碼執行;

    web 伺服器在調用 doFilter() 方法時,會傳遞一個 filterChain 對象進來,filterChain 對象是 filter 介面中最重要的一個對象,它也提供了一個 doFilter() 方法,開發人員可以根據需求決定是否調用此方法,調用該方法,web 伺服器就調用 web 資源的 service() 方法,即 web 資源被訪問;反之不會被訪問。

14.3 Filter 開發

Filter 開發步驟

  • 編寫 java 類實現 Filter 介面,並實現其 doFilter() 方法;
  • 在 web.xml 文件中使用 元素對編寫的 filter 類進行註冊,並設置它所能攔截的資源;

範例:

web.xml 配置:

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="//xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="//xmlns.jcp.org/xml/ns/javaee
                      //xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0"
         metadata-complete="true">
    
<!--  配置過濾器-->
  <filter>
    <description>FilterDemo</description>
    <filter-name>FilterDemo</filter-name>
    <filter-class>filter.FilterDemo</filter-class>
  </filter>
    
<!--針對一個URL pattern做過濾-->
  <filter-mapping>
    <filter-name>FilterDemo</filter-name>
<!--    表示攔截所有請求-->
    <url-pattern>/*</url-pattern>
  </filter-mapping>
    
<!--  針對一個Servlet做過濾
  <filter-mapping>
    <filter-name>FilterDemo</filter-name>
    <url-pattern>FilterDemo</url-pattern>
  </filter-mapping>-->
</web-app>

Filter 介面實現類:

package filter;

import javax.servlet.*;

import java.io.IOException;

/**
 * Filter 過濾器使用
 * @author YH
 * @create 2020-04-28 11:00
 */
    public class FilterDemo implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //對request和response做一些預處理
        servletRequest.setCharacterEncoding("utf-8");
        servletResponse.setCharacterEncoding("utf-8");
        servletResponse.setContentType("text/html;charset=utf-8");
        System.out.println("FilterDemo過濾器執行前");
        //讓目標資源執行(放行)
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("FilterDemo過濾器執行後");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("過濾器初始化...");
    }

    @Override
    public void destroy() {
        System.out.println("過濾器銷毀");
    }
}

當瀏覽器訪問 web 資源時,會經過過濾器,執行結果:

image-20200428145148767

可以看出 Filter 過濾器的 doFilter() 調用流程基本與 service() 一致。

補充:可以看出,執行了兩次 Filter 的 doFilter() 方法(service() 也是),個人理解是請求的時候調用一次,響應時又調用一次;過程:瀏覽器發送請求 — WEB伺服器(web 容器)— 第一次調用 — 訪問 servlet/jsp 程式 — 第二次調用 — WEB伺服器發送響應 — 瀏覽器接收

  • Filter 鏈

    在一個 web 應用中,可以開發編寫多個 Filter,這些 Filter 組合起來稱之為一個 Filter 鏈。

web 伺服器根據 Filter 在 web.xml 文件中的註冊順序,決定先調用哪個 Filter,第一個 Filter 的 doFilter)() 方法被調用時,web 伺服器會創建一個代表 Filter 鏈的 FilterChain 對象傳遞給該方法,在 doFilter() 方法中,開發人員如果調用了 FilterChain 對象 doFilter() 方法,則 web 伺服器會檢查 FilterChain 對象中是否還有 Filter,如果有,則調用第 2 個 Filter,否則調用目標資源。

過濾器邏輯與Servlet邏輯不同,它不依賴於任何用戶狀態資訊,因為一個過濾器實例可能同時處理多個完全不同的請求。

14.4 Filter 生命周期

  1. 創建

    Filter 的創建和銷毀由 web 伺服器負責,web 應用程式啟動時,web 伺服器將創建 Filter 的實例對象,並調用其 init() 方法,完成對象的初始化功能,從而為後繼的用戶請求做好攔截的準備,filter 對象只會創建一次,init() 方法也只會執行一次。通過 init() 方法的參數,可獲得當前 filter 配置資訊的 FilterConfig 對象。

  2. 銷毀

    ​ Web 容器調用 destroy() 方法銷毀 Filter。destroy() 方法在 Filter 的生命周期中僅執行一次,在 destroy() 方法中可以釋放過濾器使用的資源。

  3. FilterConfig 介面

    ​ 用戶在配置 filter 時,可以使用 為 Filter 配置一些初始化參數,當 web 容器實例化 Filter 對象,調用其 init 方法時,會把封裝了 filter 初始化參數的 filterConfig 對象傳遞進來。因此開發人員在編寫 filter 時,通過 filterConfig 對象的方法,就可獲得:

    ​ String getFilterName():得到 filter 的名稱;

    ​ String getInitParameter(String name):返回在部署描述中指定名稱的初始化參數的值,不存在返回 null;

    ​ Enumeration getInitParameterNames():返回過濾器的所有初始化參數的的名字的枚舉集合;

    ​ public ServletContext getServletContext():返回 Servlet 上下文對象的引用;

範例:利用 FilterConfig 得到 filter 配置資訊

web.xml 文件中的配置:

...
<filter>
  <filter-name>FilterDemo</filter-name>
  <filter-class>filter.FilterDemo</filter-class>+
  <init-param>
    <param-name>YH</param-name>
    <param-value>study java</param-value>
  </init-param>
  <init-param>
    <param-name>content</param-name>
    <param-value>JavaWeb</param-value>
  </init-param>
</filter>
...

filter 實現類程式碼:

package filter;

import javax.servlet.*;

import java.io.IOException;
import java.util.Enumeration;

/**
 * Filter 過濾器使用
 * @author YH
 * @create 2020-04-28 11:00
 */
    public class FilterDemo implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //對request和response做一些預處理
        servletRequest.setCharacterEncoding("utf-8");
        servletResponse.setCharacterEncoding("utf-8");
        servletResponse.setContentType("text/html;charset=utf-8");
        System.out.println("FilterDemo過濾器執行前");
        //讓目標資源執行(放行)
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("FilterDemo過濾器執行後");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("過濾器初始化...");
        //獲取過濾器的名字
        String filterName = filterConfig.getFilterName();
        //獲取在web.xml文件中配置的初始化參數
        String initParam1 = filterConfig.getInitParameter("YH");
        String initParam2 = filterConfig.getInitParameter("content");
        //返回過濾器的所有初始化參數的名字的枚舉集合
        Enumeration<String> initParameterNames = filterConfig.getInitParameterNames();
        System.out.println(filterName);
        System.out.println(initParam1);
        System.out.println(initParam2);
        while(initParameterNames.hasMoreElements()){
            String paramName = (String)initParameterNames.nextElement();
            System.out.println(paramName);
        }
    }

    @Override
    public void destroy() {
        System.out.println("過濾器銷毀");
    }
}

image-20200428205241168

14.5 Filter 的部署

分為兩個步驟:

  1. 註冊 Filter
  2. 映射 Filter
  • 註冊 Filter

    開發好 Filter 之後,需要在 web.xml 文件中進行註冊,這樣才能夠被 web 伺服器調用

    web.xml 配置文件中註冊 Filter 的範例:

    <filter>
        <description>FilterDemo過濾器</description>
        <filter-name>FilterDemo</filter-name>
        <filter-class>filter.FilterDemo</filter-class>+
        <init-param>
          <description>配置FilterDemo過濾器的初始化參數1</description>
          <param-name>YH</param-name>
          <param-value>study java</param-value>
        </init-param>
        <init-param>
          <description>配置FilterDemo過濾器的初始化參數2</description>
          <param-name>content</param-name>
          <param-value>JavaWeb</param-value>
        </init-param>
      </filter>
    

    用於添加描述資訊,該元素的內容可為空,也可以不配置

    用於為過濾器指定一個名字,該元素的內容不能為空

    用於指定過濾器完整的限定類名(全類名)

    用於為過濾器指定初始化參數,其子元素以鍵值對的形式指定 key 和 value;可以使用 FilterConfig 介面對象來訪問初始化參數。

  • 映射 Filter

    <!--針對一個URL pattern做過濾-->
      <filter-mapping>
        <filter-name>FilterDemo</filter-name>
    	<!-- 表示攔截(過濾)所有請求-->
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    
    <!--  針對一個servlet做過濾-->
      <filter-mapping>
        <filter-name>FilterDemo</filter-name>
        <servlet-name>FilterDemo</servlet-name>
      </filter-mapping>
    

    用於設置一個 filter 所負責攔截的資源,一個 Filter 攔截的資源可通過兩種方式來指定:servlet 名稱資源訪問的請求路徑

    用於設置 filter 的註冊名稱,必須與 中聲明的 name 一樣

    設置 filter 所攔截的 web 資源的請求訪問路徑

    設置 filter所 攔截的 servlet 名稱

    指定過濾器所攔截的資源被 Servlet 容器調用的方式,可以是 RRQUEST,INCLUDE,FORWARD 和 ERROR 之一,默認 REQUEST 。可以設置多個 子元素用來指定 Filter 對資源的多種調用方式進行攔截,如下:

    <!--  指定過濾器所攔截指定 Servlet 容器調用方式的資源-->
      <filter-mapping>
        <filter-name>FilterDemo</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
      </filter-mapping>
    

    子元素可以設置的值及其意義:

    • REQUEST:當用戶直接訪問頁面時,Web 容器將會調用過濾器。如果目標資源是通過 RequestDispatcher 的 include() 或 forward() 方法訪問時,那麼該過濾器就不會被調用。

    • INCLUDE:如果目標資源是通過 RequestDispatcher 的 include() 方法訪問時,那麼該過濾器將被調用。除此之外,該過濾器不會被調用。

    • FORWARD:如果目標資源是通過 RequestDispatcher 的 forward() 方法訪問時,那麼該過濾器將被調用,除此之外,該過濾器不會被調用。

    • ERROR:如果目標資源是通過聲明式異常處理機制調用時,那麼該過濾器將被調用。除此之外,過濾器不會被調用。

十五、Listener 監聽器

​ 監聽器在 JavaWeb 開發中用得比較多,監聽器(Listener)在開發中的常見應用:

15.1 統計當前在線人數

​ 在 JavaWeb 應用開發中,有時候我們需要統計當前在線的用戶數,此時就可以使用監聽器技術來實現這個功能了:

package listener;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * 監聽器:統計當前在線人數
 * @author YH
 * @create 2020-04-29 7:18
 */
public class ListenerDemo1 implements HttpSessionListener {
    /**
     * 監聽伺服器被訪問的情況進行增加在線人數
     * @param se
     */
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        ServletContext context = se.getSession().getServletContext();
        Integer onLineCount = (Integer) context.getAttribute("onLineCount");
        if(onLineCount == null){
            context.setAttribute("onLineCount",1);
        } else{
            onLineCount++;
            context.setAttribute("onLineCount",onLineCount);
        }
        System.out.println("連接數:" + onLineCount);
    }

    /**
     * 監聽伺服器被訪問的情況進行減少在線人數
     * @param se
     */
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        ServletContext context = se.getSession().getServletContext();
        Integer onLineCount = (Integer) context.getAttribute("onLineCount");
        if(onLineCount == null){
            context.setAttribute("onLineCount",1);
        } else{
            onLineCount--;
            context.setAttribute("onLineCount",onLineCount);
        }
        System.out.println("連接數:" + onLineCount);
    }
}

web.xml 中的配置

<listener>
  <display-name>ListenerDemo1</display-name>
  <listener-class>listener.ListenerDemo1</listener-class>
</listener>

15.2 自定義 Session 掃描器

​ 當一個 Web 應用創建的 Session 很多時,為了避免 Session 佔用太多的記憶體,我們可以選擇手動將這些記憶體中的 session 銷毀,那麼此時也可以藉助監聽器技術來實現:

package listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.*;

/**
 * 自定義 Session 掃描器
 * @author YH
 * @create 2020-04-29 7:22
 */
public class ListenerDemo2 implements HttpSessionListener, ServletContextListener {
    /**
     * @Field:list
     *      定義一個集合存儲伺服器創建的 HttpSession
     *      LinkeList不是一個執行緒安全的集合
     */
    /*
    * private List<HttpSession> list = new LinkedList<>();
    * 這樣寫涉及到執行緒安全問題,SessionScanerListener 對象在記憶體中只有一個
    * 但sessionCreated 可能被多人調用
    * 當有多個人並發訪問站點時,伺服器同時為這些並發訪問的人創建session
    * 那麼sessionCreate方法在某一時刻內會被幾個執行緒同時調用,幾個現場並發調用sessionCreated方法
    * 但是其內部處理的是向一個集合內添加已經創建好的session,那麼add(session)時就會涉及到
    * 幾個 session同時搶奪集合中的一個位置的情況,所以向集合中添加session時,要確保執行緒是安全的
    * 解決:使用 Collections.synchronizedList(List<T> list)方法將不是執行緒安全的list集合
    * 包裝成執行緒安全的list集合
    * */

    private List<HttpSession> list = null;
            {
        //將LinkedList包裝成執行緒安全的集合
        list = Collections.synchronizedList(new LinkedList<HttpSession>());
    }

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        System.out.println("session被創建了!");
        HttpSession session = se.getSession();
        //加鎖:向集合添加session 和 遍歷集合操作不能同時進行
        synchronized(this){
            list.add(session);
        }
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("session 被銷毀了");
    }

    /**
     * web應用啟動時觸發這個事件
     * @param sce
     */
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("web應用初始化");
        //創建定時器
        Timer timer = new Timer();
        //定時每隔30秒執行任務
        timer.schedule(new MyTask(list,this),0,1000*30);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("web應用關閉");
    }
}

/**
 * 定時要執行的任務
 */
class MyTask extends TimerTask{
    /**
     * 存儲HttpSession的list集合
     */
    private List<HttpSession> list;
    /**
     * 存儲傳遞過來的鎖
     */
    private Object lock;

    public MyTask(List<HttpSession> list,Object lock){
        this.list = list;
        this.lock = lock;
    }

    @Override
    public void run(){
        //將該操作加鎖進行鎖定
        synchronized(lock){
            System.out.println("定時器執行");
            //進行迭代操作
            Iterator<HttpSession> it = list.iterator();
            while(it.hasNext()){
                HttpSession session = it.next();
                // 30秒未有操作銷毀session
                if(System.currentTimeMillis() - session.getLastAccessedTime() > 1000*30){
                    //手動銷毀
                    session.invalidate();
                    //移除集合中已經被銷毀的session
                    it.remove();
                }
            }
        }
    }
}

web.xml 中的配置

<listener>
  <display-name>ListenerDemo2</display-name>
  <listener-class>listener.ListenerDemo2</listener-class>
</listener>

執行結果:

啟動web伺服器時的運行可以看出執行順序:監聽器 — 過濾器 — servle一系列操作

image-20200429095508088

我們開始訪問網頁,新用戶訪問就會創建一個session對象,同時連接數增加:

image-20200429095945106

監視器的定時器 30 秒監控一次,有30秒未操作的session(用戶),就銷毀此session:

image-20200429100145705

監控器的定時任務依舊進行:

image-20200429100401946

關閉伺服器時,過濾器先關閉,再關閉監聽器:

image-20200429100500454

十六、MVC 設計模式與三層架構

16.3 MVC 設計模式

image-20200504081303509

Model:模型,一個功能,用 JavaBean 實現(可細分為 處理業務邏輯的JavaBean 和 封裝數據的JavaBean)

View:視圖,用於展示資訊以及實現用戶交互(通過前端技術實現)

Controller:控制器,接收請求,將請求跳轉到模型進行處理,模型處理完畢後,再將處理的結果返回給請求處(servlet 實現

16. 2 三層架構

​ 與 MVC 設計模式的目標一致:都是為了解耦合、提高程式碼復用度;但是二者對項目的理解角度不同。

image-20200504094237082

項目結構:

image-20200507162908137

表現層往往不需要定義介面

三層組成

​ 表示層 USL (User Show Layer) 一般稱為:視圖層

​ 業務邏輯層 BLL (Business Logic Layer) 一般稱為:Service 層

​ 數據訪問層 DAL (Data Access Layer) DAO層

三層的關係:

​ 上層依賴下層:上層將請求傳遞給下層,下層處理後返回給上層;(依賴:程式碼存在先後的理解,如有A的前提是先有B)

三層優化:

加入介面

​ 面向介面編程:先定義介面-再定義實現類

​ 類命名規範:

​ 介面(interface) :I實體類Service 如:IStudentService

​ 實現類(implements):實體類ServiceImpl 如:StudentServiceImpl

​ 包命名規範:

​ 介面所在包:如 xxx.service

​ 實現類所在包:如 xxx.service.impl (作為介麵包的子包)

​ 並利用多態使用介面類型接收介面實現類的引用 如:介面 i = new 實現類();

使用 DBUtil 解決程式碼冗餘:

範例:學生管理系統

程式碼流程圖:

image-20200505091612041

參考:孤傲蒼狼//www.cnblogs.com/xdp-gacl/tag/

Tags: