Tomcat10

EUNEIR發表於2024-03-13

概述

Java:

  • JavaSE 標準版
  • JavaEE 企業版
  • JavaME 微型版

Servlet就是JavaEE的13種規範之一

Web系統通訊原理

  1. 輸入URL
  2. 域名解析器解析 http://110.242.68.3:80/index.html
  3. 傳送http協議到110.242.68.3主機,定位80埠
  4. 伺服器將index.html檔案傳送給瀏覽器
  5. 瀏覽器解析並顯示

從瀏覽器到伺服器:請求 Request

從伺服器到瀏覽器:響應 Response

WEB伺服器軟體

  • 應用伺服器:實現了JavaEE的所有規範(13個規範)
    • JBOSS
    • WebLogic
    • WebSphere
  • WEB伺服器:實現了JavaEE中的Servlet + JSP規範
    • Tomcat
    • jetty

應用伺服器是包含WEB伺服器的,例如:JBOSS中內嵌了一個Tomcat伺服器


Tomcat

Welcome to The Apache Software Foundation!

image-20230208131609431

Tomcat是開源免費的輕量級WEB伺服器,還有一個名字:catalina

image-20230208133638695

解壓後:

image-20230208134221591

  • bin:命令檔案存放目錄,例如啟動Tomcat、關閉Tomcat
  • conf:配置檔案存放目錄,server.xml檔案可以配置埠號,預設埠號8080
  • lib:核心程式目錄 jar包
  • logs:日誌目錄,啟動等資訊都會在這個目錄中生成日誌檔案
  • temp:臨時目錄,儲存臨時檔案
  • webapps:存放webapp,自帶了一些應用
  • work:存放JSP檔案翻譯之後的Java檔案和class檔案

啟動Tomcat:

image-20230208134405605

bat檔案是windows作業系統專用的批處理檔案,這種檔案中可以編寫大量的windows的dos命令

sh檔案時linux環境的shell檔案


startup.bat檔案:image-20230208135143528

實際上執行了catalina.bat檔案

catalina.bat檔案:image-20230208135536661

其中需要指定 %JAVA_HOME%

org.apache.catalina.startup.Bootstrap的main方法:

image-20230428153154408

tomcat是Java語言寫的,啟動tomcat就是執行main方法

在startup.bat檔案中需要指定CATALINA_HOME:

image-20230208141833737

嘗試執行startup.bat檔案:

image-20230208141920079

需要將bin目錄新增到環境變數PATH當中:

image-20230208142529633

此時tomcat啟動成功(指定 JAVA_HOME ):

image-20230208142155282

此時在cmd輸入 startup shutdown就可以啟動關閉tomcat伺服器,但是shutdown是windows的關機命令,可以在bin目錄中將其改為stop


  • 配置Tomcat伺服器:
    • JAVA_HOME
    • CATALINA_HOME
    • PATH = %JAVA_HOME%\bin;%CATALINA_HOME%\bin
  • 啟動Tomcat伺服器:startup
  • 關閉Tomcat伺服器:stop(shutdown衝突)

測試Tomcat伺服器是否啟動成功:瀏覽器位址列輸入URL:http://ip地址:埠號

也就是:http://localhost:8080,出現如下介面就能啟動成功了:

image-20230208143405193

解決CMD中Tomcat的亂碼問題

"Tomcat\apache-tomcat-10.0.12\conf\logging.properties"

image-20230429092022898

Windows的控制檯是GBK的編碼方式

在Idea中配置Tomcat就需要將GBK改回UTF-8

實現第一個WEB程式

  • 第一步:找到CATALINA_HOME\webapps目錄,因為所有的webapp都放在該目錄下
  • 第二步:在CATALINA_HOME\webapps目錄下新建子目錄:oa
    • 這個oa就是web應用的名字
  • 第三步:在oa下新建資原始檔,例如:index.html
image-20230208144318184

這個介面就是從tomcat伺服器中獲取的資源

也可以在頁面中使用超連結跳轉:

image-20230208145305108

注意:超連結中的ip地址和埠號是可以省略的:/oa/login.html,也就是絕對路徑,以/開始,就是以webapps目錄開始

如果要訪問以下檔案:image-20230208145752837

<a href="/oa/test/debug/d.html">

BS系統結構的角色和協議

現在有一個靜態的表格:

image-20230208150751460

如果想要從資料庫中查詢資訊展示到網頁上,就需要在Java程式中透過JDBC連線資料庫,資料庫中有多少條記錄頁面上就顯示多少條記錄,這種技術被稱為動態網頁技術(頁面的資料是動態的,根據資料庫中資料的變化而變化)

BS結構系統的通訊原理2

  • 參與的角色:

    • 瀏覽器軟體開發團隊

    • WEB Server開發團隊 Tomcat、jetty、WebLogic、JBOSS、WebSphere

    • DB Server開發團隊 Oracle、MySQL

    • WEB app開發團隊 oa crm

  • 角色和角色之間需要遵守哪些規範:

    • WEBapp和WEBServer之間有一套規範:JavaEE規範之一:Servlet規範
      • Servlet規範的作用是:app和server解耦合,app可以在任意的WEB伺服器之間執行
      • Servlet就是Java程式和Server之間的一套介面規範
    • Browser和WebServer之間有一套協議:HTTP協議
    • Webapp開發團隊和DBServer開發團隊之間有一套協議:JDBC協議

BS結構系統的角色和協議

伺服器的本質是:

  1. 解析請求資源
  2. 將請求資訊封裝為request、建立response物件
  3. 建立servlet物件
  4. 呼叫service方法

模擬Servlet本質

  • SUN公司:制定Servlet規範

    package javax.servlet;
    
    /*
        SUN公司制定的規範/介面
    * */
    public interface Servlet {
        /**
         * 提供服務的方法
         */
        void service();
    }
    
  • WebApp開發者

    package com.eun.servlet;
    
    import javax.servlet.Servlet;
    /*
    * WebApp開發者
    * */
    public class UserListServlet implements Servlet {
        @Override
        public void service() {
            System.out.println("UserListServlet Service");
        }
    }
    
    public class BankServlet implements Servlet {
        @Override
        public void service() {
            System.out.println("BankServlet Service");
        }
    }
    
    public class UserLoginServlet implements Servlet {
        @Override
        public void service() {
            System.out.println("UserLoginServlet Service");
        }
    }
    
    
  • Tomcat伺服器開發者

    package org.apache;
    
    import java.util.Scanner;
    
    public class Tomcat {
        public static void main(String[] args) {
            System.out.println("org.apache.Tomcat server startup");
    
            //Scanner模擬使用者請求
            /*
            使用者訪問伺服器是透過瀏覽器上的請求路徑URL,URL不同執行的Servlet也是不同的
                /userlist  ->  UserListServlet
                /bank  -> BankServlet
            * */
            System.out.print("輸入訪問路徑:");
            Scanner s = new Scanner(System.in);
            String path = s.next(); //獲取到了請求路徑
    
            //Tomcat應該透過請求路徑找到對應的Servlet
            //請求路徑path和xxxServlet之間的關係由誰指定?
            
        }
    }
    

    請求路徑和XXXServlet之間應該有一個對照關係

我們透過配置檔案指定請求路徑和Servlet之間的對應關係:

/bank=com.eun.servlet.BankServlet
/list=com.eun.servlet.UserListServlet
/login=com.eun.servlet.UserLoginServlet

透過請求路徑path作為key就能拿到value,只需要解析配置檔案就行了

所以在Tomcat伺服器當中:

        //Scanner模擬使用者請求
        /*
        使用者訪問伺服器是透過瀏覽器上的請求路徑URL,URL不同執行的Servlet也是不同的
            /userlist  ->  UserListServlet
            /bank  -> BankServlet
        * */
        System.out.print("輸入訪問路徑:");
        Scanner s = new Scanner(System.in);
        String path = s.next(); //獲取到了請求路徑

        //Tomcat應該透過請求路徑找到對應的Servlet
        //請求路徑path和xxxServlet之間的關係由誰指定?
        ResourceBundle bundle = ResourceBundle.getBundle("web");

        String serv = bundle.getString(path);

        Class<?> clazz = Class.forName(serv);
        Object obj = clazz.newInstance();
        if (obj instanceof Servlet servletInstance) {
            servletInstance.service();
        }

拿到請求路徑後透過屬性配置檔案獲取到value值,透過反射機制建立value例項;Servlet開發時需要遵守規範,也就是都實現了Servlet介面,就可以將Object型別轉換為Servlet型別,呼叫介面中對應的方法

對於我們來說,只需要做兩件事:

  1. 編寫類實現Servlet介面
  2. 將編寫的類配置到配置檔案中,在配置檔案中指定 請求路徑 和 類名 的關係

Tomcat伺服器是已經寫好的,這也就意味著:配置檔案 web.xml 的名稱和路徑都是規定好的

嚴格來說Servlet並不只是一個介面,同時規定了:

  • webapp的目錄結構
  • webapp的配置檔名稱和路徑
  • webapp的Java程式放在哪裡

第一個Servlet

webapp
	|----WEB-INF
		|----classes
		|----lib
		|----web.xml
	|----html
	|----css
	|----javascript
	...

遵循Servlet規範的webapp就可以在不同的Web伺服器中執行

  • 規範了介面
  • 規範了類
  • 規範了配置檔名稱、路徑、內容
  • 規範了目錄結構

開發步驟:

  1. webapps新建目錄 crm

  2. crm中新建目錄 WEB-INF

  3. WEB-INF下新建目錄 classes,存放編譯後的 .class 檔案

  4. WEB-INF下新建目錄 lib ,存放第三方jar包(MySQL)

  5. WEB-INF下新建檔案 web.xml,配置檔案,描述請求連線和Servlet的對應關係

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

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
  version="5.0"
  metadata-complete="false">
  
</web-app>
  1. 定義類 繼承Servlet介面

    • 該介面是JavaEE規範的,不在JDK當中

    • Tomcat面向Servlet介面進行呼叫,該介面就在Tomcat當中

      image-20230428171826774

      解壓之後:

      image-20230428171917434

      完整類名是:"jakarta\servlet\Servlet"

      在JavaEE7中的類名是:javax.servlet.Servlet,從 JakartaEE9 開始(Tomcat10)實現的就是jakarta\servlet\Servlet介面

JavaEE最高版本是JavaEE8,JavaEE被Oracle捐獻給Apache,Apache將 JavaEE 改為了 JakartaEE

JavaEE8對應的Servlet類名是 javax.servlet.Servlet, JakartaEE 9對應的類名是 jakarta\servlet\Servlet

image-20230428172938410
Users of Tomcat 10 onwards should be aware that, as a result of the move from Java EE to Jakarta EE as part of the transfer of Java EE to the Eclipse Foundation, the primary package for all implemented APIs has changed from javax.* to jakarta.*. This will almost certainly require code changes to enable applications to migrate from Tomcat 9 and earlier to Tomcat 10 and later. A migration tool has been developed to aid this process.

Servlet核心介面:

.Java原始碼在哪裡都可以,只需要將編譯後的.class檔案放在 webapps/crm/WEB-INF/classes當中

image-20230429090124244

Tomcat的lib目錄下有一個servlet.jar,需要將這個類路徑配到classpath當中:

CLASSPATH=.;D:\Tomcat\apache-tomcat-10.0.12\lib\servlet-api.jar

需要注意的是,配置這個環境變數只是讓java程式可以正常編譯生成class檔案,與Tomcat的執行沒有關係;Tomcat知道自身的jar包在lib目錄下。

將寫好的檔案複製到WEB-INF下的classes當中:

image-20230429093058841

註冊Servlet:在web.xml中編寫配置資訊,將 請求路徑 和 Servlet類名 關聯在一起

image-20230429093006639
    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>cn.eun.demo.HelloServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>

        <url-pattern>/abc</url-pattern>
    </servlet-mapping>

啟動Tomcat伺服器,輸入url-pattern

image-20230429093241004

傳送請求後,Tomcat伺服器會根據請求路徑url-pattern找到servlet-name,根據servlet-name建立servlet-class的例項

main方法在Tomcat伺服器當中。

Tomcat啟動的時候呼叫main方法,我們只需要編寫Servlet的實現類,並且註冊Servlet就可以了。

如果瀏覽器上編寫的路徑太複雜,可以使用超連結

注意:HTML頁面只能放在WEB-INF之外

image-20230429094246825

可以省略協議、IP、埠號:

image-20230429094320844

但是在Request請求中訪問的還是localhost:8080/crm/index.html

  • 瀏覽器傳送請求,到最終伺服器呼叫servlet的方法的過程

瀏覽器傳送請求,Tomcat伺服器接收到請求擷取路徑/crm/abc;Tomcat伺服器找到crm專案,在crm下WEB-INF下web.xml檔案中查詢/abc對應的servlet-class,內部反射建立該類物件,呼叫該物件的service方法

響應到瀏覽器

image-20230429101548766

response物件可以獲取輸出流,輸出流的write方法可以返回內容給瀏覽器,該流:

  • 不需要flush
  • 不需要close

可以設定響應內容型別為普通的HTML程式碼:

response.setContentType("text/html")

但是這個操作只能在獲取輸出流getWriter之前執行:

image-20230429102350896

public void service(ServletRequest request, ServletResponse response) 
    throws ServletException, IOException {
    	reponse.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.print("<h1>Hello First Servlet<h1>");
}

Servlet連線資料庫

需要將驅動放在crm/WEB-INF/lib下

Idea

  • 整合開發工具很多,其中目前使用比較多的是:

    • IntelliJ IDEA(這個居多,IDEA在提示功能方面要強於Eclipse,也就是說IDEA使用起來比Eclipse更加智慧,更好用。JetBrain公司開發的。收費的。)
    • Eclipse(這個少一些),Eclipse目前還是有團隊使用,只不過處於減少的趨勢,自己從事工作之後,可能會遇到。Eclipse是IBM團隊開發的。Eclipse寓意是“日食”。“日食”表示將太陽吃掉。太陽是SUN。IBM團隊開發Eclipse的寓意是吞併SUN公司,但是2009年的時候SUN公司被Oracle公司併購了。IBM並沒有成功併購SUN公司。
  • 使用IDEA整合開發工具開發Servlet

    • 第一步:New Project(我比較習慣先建立一個Empty Project【空工程】,然後在空工程下新建Module【模組】,這不是必須的,只是一種習慣,你可以直接新建非空的Project),這個Empty Project起名為:javaweb(不是必須的,只是一個名字而已。一般情況下新建的Project的名字最好和目錄的名字一致。)
    • 第二步:新建模組(File --> new --> Module...)
      • 這裡新建的是一個普通的JavaSE模組(這裡先不要新建Java Enterprise模組)
      • 這個Module自動會被放在javaweb的project下面。
      • 這個Module起名:servlet01
    • 第三步:讓Module變成JavaEE的模組。(讓Module變成webapp的模組。符合webapp規範。符合Servlet規範的Module)
      • 在Module上點選右鍵:Add Framework Support...(新增框架支援)
      • 在彈出的視窗中,選擇Web Application(選擇的是webapp的支援)
      • 選擇了這個webapp的支援之後,IDEA會自動給你生成一個符合Servlet規範的webpp目錄結構。
      • 重點,需要注意的:在IDEA工具中根據Web Application模板生成的目錄中有一個web目錄,這個目錄就代表webapp的根
    • 第四步(非必須):根據Web Application生成的資源中有index.jsp檔案,這裡先刪除這個index.jsp檔案。
    • 第五步:編寫Servlet(StudentServlet)
      • class StudentServlet implements Servlet
      • 這個時候發現Servlet.class檔案沒有。怎麼辦?將CATALINA_HOME/lib/servlet-api.jar和jsp-api.jar新增到classpath當中(這裡的classpath說的是IDEA的classpath)
        • File --> Project Structrue --> Modules --> + --> Add JARS....
      • 實現jakarta.servlet.Servlet介面中的5個方法。
    • 第六步:在Servlet當中的service方法中編寫業務程式碼(我們這裡連線資料庫了。)
    • 第七步:在WEB-INF目錄下新建了一個子目錄:lib(這個目錄名可不能隨意,必須是全部小寫的lib),並且將連線資料庫的驅動jar包放到lib目錄下。
    • 第八步:在web.xml檔案中完成StudentServlet類的註冊。(請求路徑和Servlet之間對應起來)
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
    
        <servlet>
            <servlet-name>studentServlet</servlet-name>
            <servlet-class>com.bjpowernode.javaweb.servlet.StudentServlet</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>studentServlet</servlet-name>
            <url-pattern>/servlet/student</url-pattern>
        </servlet-mapping>
        
    </web-app>
    
    • 第九步:給一個html頁面,在HTML頁面中編寫一個超連結,使用者點選這個超連結,傳送請求,Tomcat執行後臺的StudentServlet。

      • student.html

      • 這個檔案不能放到WEB-INF目錄裡面,只能放到WEB-INF目錄外面。

      • student.html檔案的內容

      • <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>student page</title>
        </head>
        <body>
            <!--這裡的專案名是 /xmm ,無法動態獲取,先寫死-->
            <a href="/xmm/servlet/student">student list</a>
        </body>
        </html>
        
    • 第十步:讓IDEA工具去關聯Tomcat伺服器。關聯的過程當中將webapp部署到Tomcat伺服器當中。

      • IDEA工具右上角,綠色小錘子右邊有一個:Add Configuration
      • 左上角加號,點選Tomcat Server --> local
      • 在彈出的介面中設定伺服器Server的引數(基本上不用動)
      • 在當前視窗中有一個Deployment(點選這個用來部署webapp),繼續點選加號,部署即可。
      • 修改 Application context為:/xmm
    • 第十一步:啟動Tomcat伺服器

      • 在右上角有綠色的箭頭,或者綠色的小蟲子,點選這個綠色的小蟲子,可以採用debug的模式啟動Tomcat伺服器。
      • 我們開發中建議適用debug模式啟動Tomcat
    • 第十二步:開啟瀏覽器,在瀏覽器位址列上輸入:http://localhost:8080/xmm/student.html

Servlet物件的生命週期

Servlet物件的生命週期是Tomcat負責的,Tomcat也被稱為 Web容器(Web Container)

  • 思考:手動new的Servlet物件受Web容器管理嗎?

不受Web容器管理的。

web容器底層應該有一個HashMap這樣的集合,在這個集合當中儲存了Servlet物件和請求路徑之間的關係

WEB容器中的Map集合

  • 伺服器啟動時Servlet物件是否被建立?

給定兩個Servlet,並且提供無參構造方法:

image-20230430204321353 image-20230430204331137

發現在啟動時沒有執行無參構造:

image-20230430204408222

也就是說Servlet並不在Tomcat啟動時建立物件,Tomcat啟動時會解析web.xml檔案,並且將請求路徑和類名放在map集合當中

  • 指定伺服器啟動時建立Servlet物件
image-20230430204759501 image-20230430204828399

這個整數是建立的優先順序,數字越小優先順序越高

image-20230430204954213

就會先建立B,再建立A

image-20230430205029825
  • Servlet物件的生命週期
public class AServlet implements Servlet {
    public AServlet() {
        System.out.println("AServlet的無參構造執行了");
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("AServlet's init method execute");
    }

    @Override
    public void destroy() {
        System.out.println("AServlet's destroy method execute");
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("AServlet's service method execute");
    }
}

預設情況下啟動Tomcat時,沒有任何輸出

當第一次傳送請求:

image-20230430210806031

AServlet物件被例項化、init方法執行、service方法執行

第一次傳送請求會執行構造方法init()service()

第二次傳送請求:

image-20230430211149087

service方法執行

也就說明:Servlet物件是單例的,構造方法只會執行一次

物件儲存在堆記憶體當中,Servlet物件的成員變數會有執行緒安全問題

  • Servlet物件是單例(但是Servlet類並不符合單例模式,也被稱為 ‘假單例’ ,單例模式的構造方法是私有化的)
  • 第一次傳送請求執行無引數構造方法,該方法也執行一次
  • 只要傳送請求,service方法一定會被呼叫

關閉伺服器時,控制檯輸出了:

image-20230430212043178

關閉伺服器時destory方法被呼叫,並且只呼叫一次

伺服器關閉需要銷燬Servlet物件的記憶體,在銷燬之前會自動呼叫該物件的destory方法。

Servlet物件可能開啟了流、連線等資源,在關閉伺服器之前需要在destory方法中進行銷燬。


  • 如果指定有參構造(無參構造不提供)
image-20230430213117983

在傳送請求時:

image-20230430213219986

伺服器內部錯誤的狀態碼都是500

結論:Servlet中不建議提供構造方法Servlet規範提供了init方法來代替構造方法(初始化資料庫連線池等)

介面卡改造Servlet

Servlet介面中的getServletInfogetServletConfig是不常用的,可以透過介面卡模式改造:

抽象類繼承Servlet,對不常用的方法進行空實現,常用的方法不覆蓋(將實現的負擔轉移到下層)

public abstract class ServletApdater implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

對於Servlet app來說,只需要實現常用的方法就可以了:

public class ApdaterDemo extends ServletApdater{

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

    }
}

改造ServletApdater

對於init方法:

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

是Tomcat伺服器呼叫的,其中的ServletConfig型別物件的建立和傳遞都是Tomcat伺服器完成的。

虛擬碼:

public class Tomcat{
    main(String[] args){
        Class clazz = Class.forName("com.eun.servlet.LoginServlet");
        Servlet servlet = (Servlet)clazz.getConstructor().newInstance();
        
        ServletConfig servletConfig = new org.apache.catalina.core.StandardWrapperFacade();
        
        servlet.init(servletConfig);
    }
}

可以在init方法中使用servletConfig物件

image-20230430223002982

也就是說建立的其實是StandardWrapperFacade物件

StandardWrapperFacade實現了ServletConfig介面,方法引數servletConfig是一個區域性變數

但是ServletConfig需要在getServlet()中返回,如果是區域性變數的話init方法結束記憶體就被釋放,需要將ServletConfig設定為成員變數

image-20230430223922553

因為父類中的ServletConfig方法被設定為私有的,只能透過公開方法訪問。

  • 如果子類要對父類中的init方法進行覆蓋,但是一定要保證servletConfig物件的初始化:

保證物件的初始化:

image-20230430225014661

但是這樣做子類不能覆蓋,可以再過載一個init方法(模板方法設計模式):

image-20230430225149268

子類只需要實現過載的init方法就可以了。

為了保護ServletConfig的初始化,使用final限定該方法不能被重寫,在該方法執行的過程中呼叫另一個可以被重寫的方法就可以了。

最終的介面卡類:

public abstract class ServletApdater implements Servlet {

    private ServletConfig servletConfig;
    @Override
    public final void init(ServletConfig servletConfig) throws ServletException {
        this.servletConfig = servletConfig;
        this.init();
    }
    public abstract void init();

    @Override
    public ServletConfig getServletConfig() {
        return servletConfig;
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

子類只需要實現init(過載)和service方法就可以了。

ServletApdater並不需要我們寫,jakarta.servlet.GenericServlet已經寫好了:

只是其中的過載init並非指定為抽象的,可以不覆蓋。

並且其中的init(ServletConfig)方法沒有final修飾,這是不好設計的。

在繼承GenericServlet時注意覆蓋的一定是無參的init:

image-20230430230213474

ServletConfig

Jakarta.servlet.ServletConfig是Servlet規範的一部分,規定Tomcat在建立Servlet物件呼叫init方法時必須傳入配置資訊物件

GenericServlet中的方法:

image-20230501103212155 image-20230501100155842
  • 誰實現了ServletConfig介面?傳遞的ServletConfig物件是誰?
image-20230501150811611

Tomcat伺服器傳遞的引數是StandardWrapperFacade實現類(由Tomcat實現)

如果把Tomcat伺服器換成jetty伺服器,輸出ServletConfig的結果時就不是這個結果了,但是都實現了ServletConfig這個規範

  • 對於兩個Servlet例項來說,傳遞的並不是同一個ServletConfig物件:
public class ConfigTestServlet extends GenericServlet {
    public void service(ServletRequest request, ServletResponse response){
        ServletConfig servletConfig = this.getServletConfig();
        System.out.println("ConfigTest01的ServletConfig物件是:" + servletConfig);
        //ConfigTest01的ServletConfig物件是:org.apache.catalina.core.StandardWrapperFacade@51a63c19
    }
}

public class ConfigTest02 extends GenericServlet {
    public void service(ServletRequest request, ServletResponse response){
        ServletConfig servletConfig = this.getServletConfig();
        System.out.println("ConfigTest02的ServletConfig物件:" + servletConfig);
        //ConfigTest02的ServletConfig物件:org.apache.catalina.core.StandardWrapperFacade@56738124
    }
}
  1. ServletConfig物件和Servlet是一對一

  2. ServletConfig物件是在呼叫init方法之前建立的。


ServletConfig物件是Servlet物件的配置資訊物件

配置資訊有:

image-20230501152901855

也就是Web.xml檔案中的配置資訊內容。

例如獲取<servlet-name>

@Override
public void service(ServletRequest request, ServletResponse response){
    response.setContentType("text/html");
    PrintWriter writer = response.getWriter();
    ServletConfig servletConfig = this.getServletConfig();
    System.out.println(servletConfig);//org.apache.catalina.core.StandardWrapperFacade@1ac0d76b
    writer.println("ConfigTest01的ServletConfig物件是:" + servletConfig + "<br>");

    writer.println("servletConfig.getServletName(): " + "<servlet-name>" + 
                   servletConfig.getServletName() + "</servlet-name>");
        
        // servletConfig.getServletName(): <servlet-name>ConfigTestServlet</servlet-name>      
}

對於getInitParameterNames方法,在web.xml檔案中是可以指定初始化配置資訊的:

image-20230501154412061

這些初始化配置資訊被Tomcat封裝到了ServletConfig物件當中,透過該物件就可以獲取這些配置資訊

image-20230501154824190

可以遍歷Enumeration集合:

image-20230501155614012 image-20230501155551548

注意:配置檔案web.xml重寫之後一定要restart server

注意:GenericServlet實現了ServletConfig介面,其中重寫了這4個方法,也就是說在我們呼叫的時候並不需要透過getServletConfig方法獲取ServletConfig物件,直接透過this呼叫就可以了:

image-20230501160220957

只要是繼承了GenericServlet的子類都可以透過this呼叫ServletConfig的4個方法

呼叫的方法:

image-20230501160309811

ServletContext

jakarta.servlet.ServletContext是Servlet規範的一部分

ServletConfig介面中有方法:

public ServletContext getServletContext(){}

返回的也是一個介面型別:

        //第一種:透過ServletConfig獲取ServletContext:
        //ServletContext application = this.getServletConfig().getServletContext();

        //第二種:透過GenericServlet獲取ServletContext
        ServletContext application = this.getServletContext();
        System.out.println("servletContext = " + application);
        //servletContext = org.apache.catalina.core.ApplicationContextFacade@30e37e9e

對於兩個Servlet app來說:

image-20230501162635811

兩個不同的Servlet物件獲取的竟然是同一個ServletContext

ServletContext是Tomcat實現的,實現類:org.apache.catalina.core.ApplicationContextFacade

image-20230501163502453
  • ServletContext在Web伺服器啟動時建立,由Web伺服器建立,ServletContext在伺服器關閉時銷燬
  • 對於一個webapp來說,只有一個ServletContext(可以理解為對應了xml檔案)
  • ServletContext被稱為 Servlet物件的上下文物件、Servlet物件的環境物件、應用域
  • 放在ServletContext物件中的資料一定是所有物件共享的
  • 注意:Tomcat是一個容器,一個容器當中可以放多個webapp,一個webapp對應一個ServletContext物件

後面還會學習其他域:請求域、會話域;如果所有使用者共享一份資料,這個資料很少被修改並且資料量很少可以放在應用域當中

實際上嚮應用域當中繫結資料,相當於把資料放在了快取當中,然後使用者訪問的時候直接從快取中獲取,減少IO操作,可以極大提升系統效能

常用方法

  • 獲取上下文的初始化引數:
public String getInitParameter(String name)
public Enumeration<String> getInitParameterNames()

在web.xml檔案中可以配置上下文的初始化引數

上下文初始化引數:所有Servlet物件都是共享的、應用級配置資訊

例如:

image-20230501170133424 image-20230501170013492 image-20230501171241198

注意:多個context-param必須配置到不同的選項當中

  • 獲取應用上下文的根路徑
public String getContextPath()
image-20230501171824600

可以動態獲取應用的根路徑

應用在最終部署的名稱是不確定的,在程式中必須動態獲取

  • 獲取檔案的絕對路徑
public String getRealPath()
image-20230501172310043

其中的/index.html的位置:

image-20230501172343292

應用的根路徑就是web目錄,/就是指代根路徑(預設的起點就是從應用的根路徑開始啟動)

可以遮蔽作業系統的差異

  • 記錄專案日誌
public void log(String message)
public void log(String message,Throwable t)

預設情況下,日誌會自動記錄到:

CATALINA_HOME/logs

但是Idea可以建立多個Tomcat,日誌會預設記錄到Idea相關的目錄下

image-20230501175359383

檢視日誌記錄:

image-20230501175505948

每一個資料夾都是一個Tomcat副本,都是參照apache-tomcat-10.0.12生成的

image-20230501175644907

其中的檔案型別:

image-20230501180155292

如果使用log(String msg,Throwable t)

int age = 17;
if (age < 18){
    application.log("age lt 18",new Throwable("age lt exception"));
}
image-20230501180455091

但是控制檯沒有發生異常,只是模擬記錄

  • 以流的形式獲取根路徑下的資源
public InputStream getResourceAsStream(String path)

這裡的path和getRealPath(String path)的path引數寫法是一樣的

  • 嚮應用域中讀寫資料

ServletContext又被稱為應用域,如果所有使用者共享一份資料,這個資料很少被修改並且資料量很少,可以放在應用域當中

  1. 資料量小:資料量太大會佔用過多的堆記憶體,並且此物件生命週期與伺服器同步,只有伺服器關閉這個物件才會被銷燬(影響伺服器效能)
  2. 修改很少:共享資料的修改操作必然會存在併發帶來的安全問題

應用域相當於一個快取,放在快取中的資料下次使用時不需要再次獲取。

public void setAttribute(String name,Object value);
public Object getAttribute(String name);
public void removeAttribute(String name)

例如:在AServlet物件中寫入User,在BServlet中讀取User:

//AServlet:
User user = new User("jack", "123");
application.setAttribute("userObj",user);

//BServlet:
Object userObj = servletContext.getAttribute("userObj");
writer.println(userObj + "<br>");

先訪問a,再訪問b:

image-20230501183347961

先訪問b,輸出的就是null


以後編寫Servlet類的時候,實際上不會繼承GenericServlet類的,BS架構的系統是基於Http超文字傳輸協議的,在Servlet規範當中提供了一個類:HttpServlet,專門為HTTP協議準備的Servlet類;我們編寫的Servlet類要繼承HttpServlet,使用HttpServlet處理Http協議更便捷

classDiagram Servlet <|.. GenericServlet GenericServlet <|.. HttpServlet

對於HttpServlet,假設我們使用者第一次發生請求:

  1. 執行GenericServlet的init(ServletConfig config)方法

  2. 內部this.init()

  3. 執行子類service(ServletRequest request,SevletResponse response)

    image-20230501190046978

由於ServletRequest的繼承結構:

classDiagram ServletRequest <|-- HttpServletRequest

在HttpServletRequest的實現類RequestFacade引數傳入的時候,正常情況下會先執行HttpServletRequest引數對應的方法,但這樣做是不符合設計思想的,Tomcat底層一定有機制保證service(ServletRequest request,ServletResponse response)先執行

image-20230501190140284
  1. 執行service(HttpServletRequest request,HttpServletResponse response)

    image-20230501190313952

HttpServlet

  • 快取機制
  1. 堆記憶體的字串常量池
  2. 堆記憶體的整數型常量池
  3. 執行緒池:在Tomcat伺服器啟動的時候,會先建立好N多個執行緒Thread物件,然後將執行緒物件放到集合當中,稱為執行緒池。使用者傳送請求過來之後,需要有一個對應的執行緒來處理這個請求,這個時候執行緒物件就會直接從執行緒池中拿,效率比較高。所有的WEB伺服器,或者應用伺服器,都是支援多執行緒的,都有執行緒池機制。
  4. 連線池:JVM是一個程序,MySQL資料庫是一個程序,程序和程序之間建立連線開啟通道是很耗費資源的,可以提前建立好N個Connection物件
  5. Redis
  6. ServletContext物件的應用域

Http協議

Http協議是w3c指定的超文字傳輸協議,B/S架構的系統需要遵守Http協議

對請求協議的測試,請求協議的格式有get/post:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>http test</title>
</head>
<body>

<!--html中的路徑是要帶上應用的根路徑的-->
<h1>get請求:</h1>
<form action="http://localhost:8080/servlet05/getServlet" method="get">
    使用者名稱:<input type="text" name="username"><br>
    密碼:<input type="password" name="pwd">
    <input type="submit" value="login">
</form>

<h1>post請求:</h1>
<form action="http://localhost:8080/servlet05/postServlet" method="post">
    使用者名稱:<input type="text" name="username"><br>
    密碼:<input type="password" name="pwd">
    <input type="submit" value="login">
</form>
</body>
</html>

處理的service方法:

public class PostServlet extends GenericServlet {
    @Override
    public void service(ServletRequest request, ServletResponse response){
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        String msg = """
                <!DOCTYPE html>
                <html lang="en">
                <head>
                    <meta charset="UTF-8">
                    <title>fromPostServlet</title>
                </head>
                <body>
                    <h1>from post Servlet</h1>
                    """
                +
                """
                        """
                +
        """
        </body>
        </html>
        """;
        writer.write(msg);
    }
}
  • 檢視協議的內容

chrome -> F12 -> network

Http協議包括:

  • 請求協議:瀏覽器向伺服器傳送請求的具體格式,包括四部分

    • 請求行

      • 請求方式:7種方式,GET、POST、DELETE、PUT、HEAD、OPTIONS、TRACE
      • URI:統一資源識別符號,代表網路中某個資源的名字,不能定位到該資源 /servlet05/index.html
        • URL:統一資源定位符,透過URL是可以定位到該資源的 http://localhost:8080/servlet05/index.html
      • 協議版本號:HTTP/1.1
    • 請求頭:請求主機、主機埠、瀏覽器資訊、平臺資訊、cookie資訊等

    • 空白行:分隔請求頭和請求體

    • 請求體:向伺服器傳送的具體資料

    • 具體報文

      • get:在請求行上傳送資料,資料掛載在URI後面,資料回顯在位址列
      GET /servlet05/getServlet?username=jack&pwd=123 HTTP/1.1                        /*請求行*/
      Accept:                                                                         /*請求頭*/  text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
      Accept-Encoding: gzip, deflate, br
      Accept-Language: zh-CN,zh;q=0.9
      Connection: keep-alive
      Cookie: Webstorm-3a48b5d7=0f49d683-9f8a-41cb-b29b-af0cf5bba784; Idea-20cff30e=c7317713-3c11-4ec2-93f2-5bfe89b01897; b-user-id=c1429b2f-d0fa-ab23-b39c-3283219755ab
      Host: localhost:8080
      Referer: http://localhost:8080/servlet05/index.html
      Sec-Fetch-Dest: document
      Sec-Fetch-Mode: navigate
      Sec-Fetch-Site: same-origin
      Sec-Fetch-User: ?1
      Upgrade-Insecure-Requests: 1
      User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
      sec-ch-ua: "Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99"
      sec-ch-ua-mobile: ?0
      sec-ch-ua-platform: "Windows"
      																				 /*空白行*/
      																				 /*請求體*/
      

      get只能傳送普通的字串,字串的長度有限制(不同瀏覽器限制不同),無法傳送大資料量

      • post:在請求體中傳送資料,不會回顯到位址列上
      POST /servlet05/postServlet HTTP/1.1                                             /*請求行*/
      Accept:                                                                          /*請求頭*/ text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
      Accept-Encoding: gzip, deflate, br
      Accept-Language: zh-CN,zh;q=0.9
      Cache-Control: max-age=0
      Connection: keep-alive
      Content-Length: 25
      Content-Type: application/x-www-form-urlencoded
      Cookie: Webstorm-3a48b5d7=0f49d683-9f8a-41cb-b29b-af0cf5bba784; Idea-20cff30e=c7317713-3c11-4ec2-93f2-5bfe89b01897; b-user-id=c1429b2f-d0fa-ab23-b39c-3283219755ab
      Host: localhost:8080
      Origin: http://localhost:8080
      Referer: http://localhost:8080/servlet05/index.html
      Sec-Fetch-Dest: document
      Sec-Fetch-Mode: navigate
      Sec-Fetch-Site: same-origin
      Sec-Fetch-User: ?1
      Upgrade-Insecure-Requests: 1
      User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
      sec-ch-ua: "Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99"
      sec-ch-ua-mobile: ?0
      sec-ch-ua-platform: "Windows"
      																				/*空白行*/
      username=zhangsan&pwd=123                                  						/*請求體*/
      

      post可以傳送任何型別的資料,包括普通字串、流媒體等資訊;可以傳送大資料量(檔案上傳)

  • 目前為止,只有表單中的method屬性被指定為post時才能傳送POST請求,其他全為GET請求

  • 瀏覽器位址列直接請求、超連結、form的預設提交都是get

提交資訊的格式:name=value&name=value

重點:name就是form表單提交的輸入域的name,value就是控制元件的value

  • 響應協議:伺服器向瀏覽器傳送響應的具體格式,包括四部分

    • 狀態行:三部分組成

      • 協議版本號 HTTP/1.1
      • 狀態碼:
        • 200 請求響應成功,正常結束;
        • 404 訪問資源不存在,前端錯誤;
        • 405 前端傳送的請求方式與後端的處理方式不一致,例如前端傳送post請求,後端按照get請求處理就會有405錯誤
        • 500 伺服器端程式出現異常,後端錯誤
        • 以4開始的一般是前端錯誤,5開始的一般是後端錯誤
      • 狀態描述資訊:
        • ok 正常結束成功
        • not found 資源找不到
    • 響應頭

      • 響應內容型別、響應內容長度、響應時間、日期...
    • 空白行:分隔響應頭和響應體

    • 響應體:響應的正文,字串

    • 具體報文

      HTTP/1.1 200 ok                                                              //狀態行
      Content-Type: text/html;charset=UTF-8										 /*	
      Content-Length: 162
      Date: Tue, 02 May 2023 01:56:15 GMT											   響應頭
      Keep-Alive: timeout=20
      Connection: keep-alive														 */
      																			 //空白行
      <!DOCTYPE html>																 //響應體
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>fromGetServlet</title>
      </head>
      <body>
          <h1>from get Servlet</h1>
      </body>
      </html>
      
      

get和post的區別

  • get請求傳送資料的時候,資料會掛在URI的後面,並且在URI後面新增一個“?”,"?"後面是資料。這樣會導致傳送的資料回顯在瀏覽器的位址列上。(get請求在“請求行”上傳送資料)

    • http://localhost:8080/servlet05/getServlet?username=zhangsan&userpwd=1111

      這種方式是不對的,使用者名稱和密碼都應該用post傳送

  • post請求傳送資料的時候,在請求體當中傳送。不會回顯到瀏覽器的位址列上。也就是說post傳送的資料,在瀏覽器位址列上看不到。(post在“請求體”當中傳送資料)

  • get請求只能傳送普通的字串。並且傳送的字串長度有限制,不同的瀏覽器限制不同。

  • get請求無法傳送大資料量。

  • post請求可以傳送任何型別的資料,包括普通字串,流媒體等資訊:影片、聲音、圖片。

  • post請求可以傳送大資料量,理論上沒有長度限制。

  • get請求在W3C中是這樣說的:get請求比較適合從伺服器端獲取資料

  • post請求在W3C中是這樣說的:post請求比較適合向伺服器端傳送資料

  • get請求是絕對安全的,因為get請求只是為了從伺服器上獲取資料

  • post請求是危險的,因為post請求是向伺服器提交資料,如果這些資料中留有後門是危險的。

所以一般情況下攔截請求的時候都會選擇攔截(監聽)post請求

  • get請求是支援快取的,post請求不支援快取
    • 訪問一張圖片,訪問過後會將圖片放在瀏覽器的快取當中,如果第二次請求路徑沒有發生變化就從瀏覽器的快取中拿取

任何一個get請求的最終響應結果都會被瀏覽器快取起來,在瀏覽器的快取當中,一個get請求的路徑對應一個資源

實際上,只要傳送get請求,瀏覽器做的第一件事就是從本地快取中查詢資源,找不到才會向伺服器傳送請求

post請求之後的響應結果不會被瀏覽器快取起來,這個快取沒有意義。

假設有需求:不希望get請求查詢快取,每一次都去伺服器上查詢資源。

只要每一次請求對應的請求路徑不同就可以了,可以在路徑後加一個系統毫秒數(時間戳):

http://localhost:8080/servlet05/index.html?t=系統毫秒數

系統毫秒數是隨時發生變化的,一定會從伺服器端獲取。

GET請求和POST請求如何選擇?

  • 怎麼選擇GET請求和POST請求呢?衡量標準是什麼呢?你這個請求是想獲取伺服器端的資料,還是想向伺服器傳送資料。如果你是想從伺服器上獲取資源,建議使用GET請求,如果你這個請求是為了向伺服器提交資料,建議使用POST請求。
  • 大部分的form表單提交,都是post方式,因為form表單中要填寫大量的資料,這些資料是收集使用者的資訊,一般是需要傳給伺服器,伺服器將這些資料儲存/修改等。
  • 如果表單中有敏感資訊,還是建議適用post請求,因為get請求會回顯敏感資訊到瀏覽器位址列上。(例如:密碼資訊)
  • 做檔案上傳,一定是post請求。要傳的資料不是普通文字。
  • 其他情況都可以使用get請求。

模板方法設計模式

當前有兩個類:

image-20230502111129243 image-20230502111215112

核心演算法day和大部分程式碼都是相同的,只有doSome方法的內容不同

可以改進:

image-20230502111548665

final修飾的核心演算法受到了保護,可能出現不同行為的方法設定為抽象的,這就是模板類

模板方法定義核心的演算法骨架,具體的實現步驟延遲到子類當中實現

HttpServlet

jakarta.servlet.http.HttpServlet

HttpServlet:專門為HTTP協議準備的Servlet類,使用HttpServlet處理Http協議更便捷

  • http包下的類和介面:jakarta.servlet.http.*

    • jakarta.servlet.http.HttpServlet (HTTP協議專用的Servlet類,抽象類)

    • jakarta.servlet.http.HttpServletRequest (HTTP協議專用的請求物件)

    • jakarta.servlet.http.HttpServletResponse (HTTP協議專用的響應物件)

  1. HttpServletRequest物件(簡稱request)封裝了請求協議的全部內容,Web伺服器將請求協議中的資料解析出來封裝到request物件當中
  2. HttpServletResponse物件:專門用來響應Http協議到瀏覽器的

執行過程:

//1. 透過無引數構造方法建立物件
public HelloServlet() {

}

//2. 執行HttpServlet的init(ServletConfig config)方法
//   執行父類GenericServlet的 init(ServletConfig config)方法 模板方法
@Override
public void init(ServletConfig config) throws ServletException {
    this.config = config; // 區域性
    this.init();
}
//    內部呼叫init()方法(供子類覆蓋)
public void init() throws ServletException {
    // NOOP by default
}

//3. 執行HttpServlet的service(ServletRequest request,ServletResponse response)方法
@Override
public void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException {

    HttpServletRequest  request;
    HttpServletResponse response;

    try {
        //介面型別轉換
        request = (HttpServletRequest) req;
        response = (HttpServletResponse) res;
    } catch (ClassCastException e) {
        throw new ServletException(lStrings.getString("http.non_http"));
    }
    service(request, response); //呼叫引數型別一致的方法
}
//   呼叫 service(HttpServletRequest request,HttpServletResponse response)
/*模板方法*/
protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
		//獲取請求方式 
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince;
                try {
                    ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                } catch (IllegalArgumentException iae) {
                    // Invalid date header - proceed as if none was set
                    ifModifiedSince = -1;
                }
                if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);

        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);

        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);

        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);

        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);

        } else {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);

            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }

注意:在HttpServlet中模板方法是protected void service(HttpServletRequest req, HttpServletResponse resp)

其中doGet(req, resp)等方法的實現延遲到子類中完成

正常情況下,傳送GET請求就重寫doGet方法,傳送POST請求就重寫doPost方法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    
<a href="/servlet06/hello">hello(get)</a>
    
<br>
    
<form action="/servlet06/hello" method="post"><input type="submit" value="hello(post)"></form>
    
</body>
</html>
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response){
        PrintWriter writer = response.getWriter();
        writer.write("<h1>doGet</h1>");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response){
        PrintWriter writer = response.getWriter();
        writer.write("<h1>doPost</h1>");
    }

這樣做是沒有問題的。

但是如果傳送到是GET請求,而後臺重寫的是doPost方法:

image-20230502131921073

因為此時訪問的是父類HttpServlet中的doGet()方法,在父類中的這個方法是:

image-20230502132154040

假設此處是使用者登入Servlet,要求前端必須使用POST請求,如果使用GET就報錯,在後端就只重寫doPost方法

總結:405錯誤是因為HttpServlet中的doXxx()方法執行了

如果直接重寫service(HttpServletRequest req, HttpServletResponse resp)方法,是無法感知到405錯誤的

405代表了前端傳送的請求不是伺服器需要的請求方式

有些程式設計師為了避免405將doGet和doPost方法都重寫了,這樣做是不好的。

Servlet開發步驟
  • 第一步:編寫一個Servlet類,直接繼承HttpServlet
  • 第二步:重寫doGet方法或者重寫doPost方法,到底重寫誰後端需求決定。
  • 第三步:將Servlet類配置到web.xml檔案當中。
  • 第四步:準備前端的頁面(form表單),form表單中指定請求路徑即可。

Web站點的Welcome Page

設定了Welcome Page後,訪問這個webapp時沒有指定任何資源路徑會預設訪問歡迎頁面

一般的訪問方式:http://localhost:8080/login/index.html

如果訪問的是:http://localhost:8080/login 只是這個站點,沒有指定具體的路徑預設訪問Welcome Page

image-20230502135502424

透過http://localhost:8080/servlet07訪問的就是指定的Welcome Page /loginPage.html

  • 一個Webapp是可以設定多個歡迎頁面的,越上方的優先順序越高
    <welcome-file-list>
        <welcome-file>loginPage.html</welcome-file>
        <welcome-file>dir1/dir2/backLoginPage.html</welcome-file>
    </welcome-file-list>

<!--路徑最好不要以 / 開始-->

如果第一個找不到就會展示第二個

當檔名稱為 index.html時,Tomcat提前將名稱為index.html的頁面配置為WelcomePage

image-20230502140417583

WelcomPage的配置

  1. 在webapp下的WEB-INF內web.xml檔案中配置(區域性配置)
  2. 在CATALINA_HOME/conf/web.xml檔案中配置(全域性配置)

區域性配置是優先的。

Welcome Page可以是一個Servlet

Welcome Page其實就是伺服器當中的一個資源,可以是靜態的HTML頁面也可以是動態的Java程式

public class WelcomeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();

        writer.write("<h1>Welcome to MyServlet</h1>");
    }
}
image-20230502141536536
  • 放在WEB-INF路徑下的資源是受保護的,不能透過瀏覽器訪問,靜態資源要放在WEB-INF目錄之外

HttpServletRequest介面詳解

image-20230502142022269
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();

        writer.print(request); //org.apache.catalina.connector.RequestFacade@72761ab9
    }

HttpServletRequest是一個介面,全稱是:jakarta.servlet.http.HttpServletRequest

classDiagram ServletRequest <|-- HttpServletRequest HttpServletRequest <|-- RequestFacade

HttpServletRequest的實現類是org.apache.catalina.connector.RequestFacade實現的

image-20230502145051578

這個物件是Tomcat建立的,封裝了Http的請求協議,需要哪些資訊面向介面程式設計就可以了

注意:Request和Response物件每次請求都是不同的,生命週期只有一次請求

常用方法

如何儲存前端的資料?

如果當前有如下的前端介面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Register User</title>
</head>
<body>
<form action="http://localhost:8080/servlet08/request" method="post">
    username:<input type="text" name="username"> <br>
    password:<input type="password" name="pwd"> <br>

    interest:
        smoke<input type="checkbox" name="interest" value="smoke">
        drink<input type="checkbox" name="interest" value="drink">
        perm <input type="checkbox" name="interest" value="perm">

    <input type="submit" value="register">
</form>
</body>
</html>

提交的資料格式:

username=admin&pwd=123&interest=drink&interest=perm

應該採取Map集合儲存,因為都是Key-Value的結構;但是對於interest屬性來說,key重複,value覆蓋

可以將Key儲存String,value儲存String陣列

Map<String, String[]>
    key儲存String
    value儲存String[]
    key				value
    -------------------------------
    username		{"abc"}
    userpwd			{"111"}
    aihao			{"s","d","tt"}

注意:前端表單提交資料的時候,假設提交了120這樣的“數字”,其實是以字串"120"的方式提交的,所以伺服器端獲取到的一定是一個字串的"120",而不是一個數字。(前端永遠提交的是字串,後端獲取的也永遠是字串。)

一般情況下不從Map集合中獲取,效率較低

父類ServletRequest中:

  • 獲取前端提交的資料
Map<String,String[]> getParameterMap();  //獲取map集合
Enumeration<String> getParameterNames(); //獲取所有key
String[] getParameterValues(String name); //根據key獲取map集合的value
String getParameter(String name); //大部分情況下陣列中是一個元素 可以獲取陣列中的這一個元素
//這四個方法和獲取使用者提交的資料有關係
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Map<String, String[]> parameterMap = request.getParameterMap();

        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            String key = entry.getKey();
            String[] value = entry.getValue();
            System.out.println(key + " : " + Arrays.toString(value));
        }
    }
/**
username : [zhangsan]
pwd : [123]
inters : [smoke, smoke]
*/

注意:如果陣列中有多個引數,使用request.getParamter()獲取的結果是陣列中的第一個元素

request 請求域物件

請求域:可以在一次請求範圍內進行共享資料,一般用於請求轉發的多個Servlet中共享資料。

  • 請求域物件比應用域物件的範圍小很多,請求域只在一次請求內有效,一個請求物件request對應一個請求域

  • void setAttribute(String name,Object obj);
    Object getAttribute(String name);
    void removeAttribute(String name);
    
  • 儘量使用小的域物件,佔用的資源較少

在同一個Servlet中進行請求域存取:

public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        Date nowTime = new Date();
        request.setAttribute("sysTime",nowTime);
        
        Object sysTime = request.getAttribute("sysTime");
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.print(sysTime); //Wed May 03 11:03:44 CST 2023
    }
}

這樣做是沒有問題的

在不同的Servlet中進行存取:

//在A中向Request存:
public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Date nowTime = new Date();
        request.setAttribute("sysTime",nowTime);

    }
}

//在B中從Request取:
public class BServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
        	throws ServletException, IOException {
        Object sysTime = request.getAttribute("sysTime");
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.print(sysTime);
    }
}

首先訪問/servlet04/a,將資料存入請求域;然後訪問/servlet04/b,讀取剛才存入的資料

image-20230503110849718

這樣做是讀不到資料的,因為第一次請求結束之後請求域就被銷燬了,/servlet04/b是第二次訪問請求,兩個是完全不同的請求域

不能在AServlet中手動new BServlet的物件,自己new的Servlet物件生命週期不受Tomcat的管理

使用Servlet中的請求轉發機制可以將AServlet和BServlet放在一次請求當中,也就是透過AServlet跳轉到BServlet,

請求轉發機制

執行了AServlet之後,跳轉到BServlet

  • 獲取請求轉發器:
public RequestDispatcher getRequestDispatcher(String path) //path是xml檔案中指定的path

Tomcat透過path從xml檔案中獲取servlet-class。

public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Date nowTime = new Date();
        request.setAttribute("sysTime",nowTime);

        //相當於把 /b這個路徑包裝到請求轉發器當中,實際上是把下一個轉發的資源的路徑告知Tomcat伺服器
        RequestDispatcher requestDispatcher = request.getRequestDispatcher("/b");

        //呼叫requestDispatcher的forward方法進行轉發
        requestDispatcher.forward(request,response);
    }
}

注意:轉發(forward())是一次請求,傳遞的都是同一個request物件,request和response都是要轉發給下一個資源的

這時還是不能直接訪問/servlet04/b,這樣做請求無法轉發到BServlet的request上,必須透過/servlet04/a訪問


  • 多個Servlet間共享資料
  1. 資料放在ServletContext中,但是不建議這樣做
  2. 資料放在Request請求域當中,使用請求轉發機制。

轉發的下一個資源可以是html等靜態資源,只是轉發的路徑必須以 / 開始,不加專案名

public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Date nowTime = new Date();
        request.setAttribute("sysTime",nowTime);
        request.getRequestDispatcher("/index.html").forward(request,response);
    }
}

index.html:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>index html page</h1>
</body>
</html>
image-20230503123035475

常用方法

  • 獲取客戶端IP地址
public String getRemoteAddr();  //獲取客戶端IP地址
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
            String remoteAddr = request.getRemoteAddr(); //獲取客戶端的地址
            System.out.println(remoteAddr);
            InetAddress address = InetAddress.getByName(remoteAddr);
            System.out.println(address.getHostAddress());
            System.out.println(address.getHostName());
    }

127.0.0.1:8080/servlet09/...訪問時:

image-20230503124812637
  • 設定請求體的字符集
public void setCharacterEncoding(String env);

POST請求在請求體中提交資料,setCharacterEncoding()是處理post請求的亂碼問題

有如下html頁面:

image-20230503125547493

在提交的時候指定為POST方式提交,使用者名稱提交為中文;在Tomcat10中是沒有問題的,但是在Tomcat9及以下版本就會出問題,就需要設定字符集

Tomcat10的字符集預設是UTF-8,Tomcat9及之前的字符集是ISO-8859-1

對於response來說也是如此:

response.setContentType("text/html"); //Tomcat9及之前這樣返回的中文也會亂碼
response.setContentType("text/html;charset=UTF-8"); 
  • get請求的亂碼問題

get請求的資料提交是在URI之後進行提交的,也就是在請求行上提交的

image-20230503131854934

這是Connector標籤的相關配置,在conf-server.xml中:

image-20230503132049333

其中的URIEncoding:

image-20230503132126464

就是URI的編碼方式,預設值為UTF-8

但是在Tomcat7及之前,URIEncoding的預設編碼方式是ISO-8859-1

解決方案:修改CATALINA_HOME/conf/server.xml中

  • 獲取應用的根路徑
public String getContextPath();// 獲取應用的根路徑

在一個HttpServlet中,可以透過request獲取應用的根路徑:

image-20230503133035249

也可以透過HttpServlet本身獲取應用的根路徑:

image-20230503133122454

HttpServlet的父類是GenericServlet,GenericServlet implements Servlet,ServletConfig,實現了ServletConfig中的方法:

getServletContext()

getServletContext(){
    return this.getServletConfig().getServletContext();
}
  • 獲取前端請求方式
public String getMethod();
String method = request.getMethod();
System.out.println(method); //GET
  • 獲取請求URI
public String getRequestURI();
String requestURI = request.getRequestURI();
System.out.println(requestURI);//  /servlet04/a
  • 獲取Servlet路徑:不帶專案名
public String getServletPath();
String servletPath = request.getServletPath();
System.out.println("servletPath = " + servletPath); //  servletPath = /a

Servlet的單表練習

使用Servlet完成單表的CRUD操作

image-20230504133122430
  • 實現步驟:

    • 第一步:準備資料庫表,對部門表進行增刪改查
    drop table if exists dept;
    create table dept(
    	deptno int primary key,
        dname varchar(255),
        loc varchar(255)
    );
    insert into dept(deptno,dname,loc) values(10,'銷售部','北京');
    insert into dept(deptno,dname,loc) values(20,'研發部','上海');
    insert into dept(deptno,dname,loc) values(30,'技術部','廣州');
    insert into dept(deptno,dname,loc) values(40,'媒體部','深圳');
    commit;
    select * from dept;
    
    • 第二步:準備介面原型

      新增介面 add.html、修改頁面 edit.html、詳情介面 detail.html 、歡迎頁面 index.html、部門列表頁面 list.html

      保證頁面流轉

    • 第三步:分析系統功能,只要一個操作連線了資料庫就代表了一個功能

      1. 檢視部門列表
      2. 儲存部門
      3. 刪除部門
      4. 檢視部門詳細資訊
      5. 跳轉到修改頁面(佔位符的值也需要查詢資料庫)
      6. 修改部門
    • 第四步:搭建開發環境

      • 建立webapp
      • 新增第三方jar包(MySQL驅動等)
      • JDBC工具類
      • 將HTML頁面放在Web目錄下
    • 實現功能:檢視部門列表

      • 假設從前端開始:從使用者點選按鈕開始

        1. 檢視部門列表:連線資料庫,查詢所有使用者資訊展示到頁面上;修改前端頁面中的超連結,使用者最先點選的就是這個超連結

          image-20230503144937793
        2. 寫web.xml檔案

          image-20230503145431323
        3. DeptListServlet重寫doGet,查詢資料庫中所有的部門

          image-20230503154952459
        4. 此時需要將查詢的結果傳遞到前端頁面上,但是此時只能替換整個網頁:

          image-20230503161030742

          在Java程式中拼接字串,將結果寫出

          image-20230503161209226
        5. 實現 檢視部門詳情

        這一步也需要查詢資料庫,需要跳轉到一個Servlet當中:

        image-20230503161715406
        window.location.href = 'http://localhost:8080/oa/dept/detail'; //前端中都需要加專案名
        

        但是專案名不應該給定,應該動態的獲取:

        "window.location.href = 'http://localhost:8080" + request.getContextPath() + "/dept/detail';"
        

        此時應該傳遞部門編號,在傳送請求的時候:

        image-20230503163520237

        對應了:

        image-20230503163549227

        這樣就需要在建立動態內容時進行額外處理:

        image-20230503163727994 image-20230503163813487

        在請求頭中:image-20230503180432584

        1. 實現刪除功能

        需要向Servlet傳遞要刪除的編號

        image-20230503182512968

        這一步的操作是相同的,在Servlet中根據傳遞的deptno進行刪除,但是在刪除之後應該回到部門列表的頁面,這個操作可以使用請求轉發機制完成

        image-20230503182729061

        這樣做可以獲取最新的資料

        注意:刪除或者新增最好手動提交事務

        也可以根據返回值count進行刪除成功或者失敗的處理。

        1. 新增 功能
        image-20230503190726520

這樣做是有一個問題的,在新增部門頁面提交過來的請求是POST請求,透過請求轉發機制是一個請求,在/dept/list中指定的處理方式是GET,405錯誤

解決方案:

  1. 可以在/dept/list中重寫doPost方法,內部呼叫doGet方法,這是不好的設計
  2. 重定向

實現部門的修改:

image-20230503192718268

點選這個修改,跳轉到修改頁面:

image-20230503192747816

這次實際上是一個查詢操作,根據deptno從資料庫中查出對應的資訊,設為預設值

使用者輸入修改後的資訊 點選確認修改,這次才是一個update的操作

第一次點選修改:從資料庫中查詢資料展示:

image-20230503200641351

注意只能設定為readonly,disabled的資料不會被表單提交

第二次點選修改:向資料庫中更新資料

image-20230503200746510

更新完畢轉發回主頁面

建議從前端向後端一步一步實現,首先要考慮的是使用者點選的是什麼,使用者點選的東西在哪裡

轉發 重定向

  • 在一個web應用中如何完成資源的跳轉?
  1. 轉發:是同一次請求

    request.getRequestDispatcher("/資源路徑").forward(request,response);//獲取請求轉發器物件 呼叫forward方法轉發
    //轉發是一次請求,傳參request就是為了保證在跳轉呼叫相關方法時傳遞的都是同一個request物件,對應了一個請求域
    
image-20230503204806747 轉發
  1. 重定向:由response物件完成操作
response.sendRedirect(request.getContextPath() + "/資源路徑");

重定向的path必須新增專案名:response具有響應能力,自動將路徑響應給瀏覽器,瀏覽器自發的向伺服器傳送了一次全新的請求,瀏覽器傳送請求必須帶專案名

response物件將 /servlet10/b響應給瀏覽器,瀏覽器又自發的向伺服器傳送了一次全新的請求http://localhost:8080/servlet10/b

所以重定向一次瀏覽器一共傳送了兩次請求:

  • 第一次訪問/servlet10/a,重定向response返回 /servlet10/b

    image-20230504135320586
  • 瀏覽器自發的請求/servlet10/b

    image-20230504135356353

最終在位址列上顯示的地址一定是最後一次請求的地址,所以重定向會導致瀏覽器位址列上的地址發生改變

  • 轉發是由Tomcat控制的,從A資源跳轉到B資源,跳轉動作在Tomcat內部完成

  • 重定向完全是由瀏覽器控制下一個資源的走向

重定向無法訪問到第一次請求域中的資料。

重定向

轉發和重定向的選擇:

  • 在上一個servlet當中向request域中繫結了資料,希望在下一個servlet中使用這些資料,使用轉發機制
  • 其餘所有的請求使用重定向

在單表練習中,儲存修改的部門資訊時使用POST請求接收表單資料,儲存完畢之後應該跳轉到部門列表頁面,部門列表只重寫了GET方法,此時就可以使用重定向:

image-20230504140052116

瀏覽器自發的請求是GET請求,就可以避免在部門列表Servlet中重寫doPost方法呼叫doGET

儲存失敗也建議使用重定向:

image-20230504140147502

重定向可以是任何資源

轉發導致的重新整理問題

有如下頁面:

image-20230504143523246

對應的Servlet:

image-20230504143631652

儲存完畢之後透過轉發跳轉到成功頁面

image-20230504143901678

如果在此處不斷的重新整理,就相當於一直在提交位址列上的:http://localhost:8080/servlet10/save?no=5&name=zhangsan

image-20230504144057302

本質上就是重複傳送了這次請求,使用重定向可以解決這個問題:

image-20230504144234403

重定向之後瀏覽器自發請求的地址:

image-20230504144337576

即使重新整理也是請求的success介面,這個介面在瀏覽器的快取之中

註解式開發

僅僅一個單表查詢,xml檔案中就有如此多的內容;

採用這種方式對於一個大的專案來說web.xml檔案會非常龐大;而且在web.xml檔案中配置的資訊是很少修改的

Servlet3.0後推出了各種基於Servlet的註解式開發

優點:

  • 開發效率高,不需要編寫大量的配置資訊,直接在Java類上使用註解進行標註
  • web.xml檔案變小了(註解 + 配置檔案 組合式開發)

一些不經常變化的配置建議使用註解。

image-20230504161554591

jakarta.servlet.annotation.WebServlet

image-20230504162030207
  • name屬性:指定Servlet的name,等同於<servlet-name>
  • utlPatterns屬性:指定Servlet的url,patterns以s結尾,可以指定多個路徑,也就是指定值可以是陣列
  • value屬性:等同於urlPatterns
  • loadOnStartUp屬性:伺服器啟動階段建立Servlet物件
image-20230504163209179
  • initParams屬性:初始化引數
image-20230504163242587

是註解型別 WebInitParam型別的陣列

image-20230504164608037

對於value屬性來說,因為其他屬性都指定了預設值,在指定註解的時候可以省略,所以value對應的屬性可以省略 value =

image-20230504164727634

但是如果同時指定其他屬性值,value = 就不能省略了。

模板方法解決類爆炸

上例中的開發方式是一個servlet請求對應了一個Servlet類,這樣隨著請求的增多就會出現 “類爆炸”的問題

可以使用模板方法設計模式改造:

image-20230506083615935

接收所有的請求,重寫HttpServlet的Service(HttpServletRequest,HttpServletResponse)方法

這樣就是一個Servlet請求對應一個方法,一個業務模組對應一個Servlet類,部門相關的就對應一個DeptServlet

不建議重寫doGet和doPost方法,可能同時處理這兩種請求

此時註解還有一種更好的寫法:image-20230506084332739

所有的 /dept/開始的請求都走這個Servlet

改進之後的專案:

image-20230506091114212

注意:如果使用模糊匹配在獲取路徑的時候就不能使用getServletPath()

JavaServer Pages

Java程式編寫前端程式碼太麻煩,程式耦合度太高,難於維護;並且修改前端程式碼就要重新編譯Java程式,生成新的class檔案,打一個新的war包,重新發布。

JSP是Java程式,是基於Java語言實現的伺服器端的頁面,本質上還是Servlet。

JSP是一套規範,所有的Web容器都會遵守這套規範,都是按照這套規範進行翻譯的。

每一個Web容器/web伺服器都會內建一個JSP翻譯引擎

image-20230506092040147

其中什麼都沒寫,啟動伺服器:

image-20230506092418047

訪問這個路徑:

image-20230506092523378

底層會將.jsp檔案翻譯生成.java檔案,Tomcat會將.java檔案編譯生成.class檔案,也就是訪問index.jsp底層執行的是index_jsp.class程式

訪問index.jsp,執行的是index_jsp.class中的方法

index_jsp.java:

image-20230506093028586

繼承自HttpJspBase,而HttpJspBase:

image-20230506093207524

繼承自HttpServlet,說明訪問.jsp底層執行的是一個Servlet,JSP實際上就是一個Servlet

  • 訪問index.jsp的時候,會自動翻譯生成index_jsp.java,會自動翻譯生成index_jsp.class,index_jsp類繼承自HttpJspBase,HttpJspBase繼承自Servlet

JSP的生命週期和Servlet完全相同(也都是假單例)

第一次訪問JSP的時候比較慢,會從.jsp -> .java -> .class -> 無參構造 -> init() -> service()

第二次訪問JSP會直接呼叫service方法,比較快

對JSP進行錯誤除錯的時候,應該開啟.java檔案檢查

有如下jsp檔案:

abc

在生成的index_jsp.java的_jspService(HttpServletRequest,HttpServletResponse)中:

image-20230506095012473

index.jsp檔案:

<html>
    <head>
        <title>my first jsp page</title>
    </head>
    <body>
        <h1>my first jsp page</h1>
    </body>
</html>

對應的index_jsp.java檔案:

image-20230506095433770

在jsp檔案中直接編寫文字,都會被翻譯到Servlet類的service方法的out.write(“”)當中,輸出到瀏覽器

如果想在jsp中寫Java程式碼,需要加一些特殊符號,JSP翻譯引擎會根據不同的特殊符號將內容翻譯到.java檔案的不同位置

對於響應的中文內容:

image-20230506100347005

需要對JSP加一個page指令,解決亂碼問題:

image-20230506100548490

在_jsp.java 程式碼中:

image-20230506100629246

JSP預設不是UTF-8

JSP雖然本質上是個Servlet,但是和Servlet的職責不同

Servlet的職責:收集資料

JSP的職責:展示資料

基礎語法

在JSP中編寫Java程式:

<% code line %>

<%%>中編寫的內容被視為Java程式,被翻譯到Servlet類的Service方法內部

<%@page contentType="text/html; UTF-8" %>
<%
    System.out.println("hello jsp");
%>

訪問這個jsp就會在控制檯中輸出hello jsp

image-20230506101213943

這樣直接暴露在service()方法內部,在<%%>中寫程式碼就是在方法體中寫程式碼

image-20230506101504157

會直接退出虛擬機器,Tomcat伺服器立即停止工作

如果在jsp的指令碼塊中出現了語法錯誤:

image-20230506101627130

訪問時錯誤程式碼500:

image-20230506101703118

這個錯誤是在.java -> .class編譯時發生的問題


  • 註釋

JSP的註釋:

<%--
    
--%>

註釋內容不會被翻譯到.java當中;HTML的註釋:

<!--

-->

這個註釋還是會被翻譯到.java檔案當中


  • service()方法之外寫東西:宣告語法
<%!
    
%>
image-20230506103114047 image-20230506103153051

還是編譯報錯

但是這種方法不建議使用,在service方法之外寫的是靜態變數/例項變數,Servlet物件只有一個,多執行緒併發會有執行緒安全問題

輸出語句

向瀏覽器輸出Java變數

<%@page contentType="text/html; UTF-8" %>
<%
    String name = "jack";
    //輸出name到瀏覽器
%>

這段程式碼是翻譯在_jspService()方法內部的:

image-20230507085612779

可以呼叫引數 HttpServletResponse response將這個內容輸出到瀏覽器:

image-20230507085824226

這樣做是沒有問題的,但是在_jspService()方法當中:

image-20230507085943477

在方法內部向瀏覽器輸出時使用的就是變數out:

image-20230507090101089

在JSP中我們也可以直接使用:

image-20230507090141859

這個out是JSP的九大內建物件之一,可以直接使用(只能在service方法內部使用)

image-20230507090411347

異常物件此時還沒有啟用

如果在以下程式中要輸出c:

image-20230507092820297

可以這樣寫:

image-20230507092851534

但是這樣做太麻煩,每次都要呼叫out.write()方法處理

可以直接使用語法:

<%= c %>

可以將其中的內容當作Java程式碼處理後輸出,翻譯為如下程式碼,翻譯到_jspService()方法內部

image-20230507093029467

如果輸出的內容中有變數,使用這種語法可以省去<% out.write() %>的操作

示例:

<%="登入成功,歡迎 " + username%>

改造oa

  • 使用Servlet收集資料,處理業務

  • 使用JSP展示資料

改造index.jsp:

image-20230507095919534

<%= request.getContextPath%>獲取應用根路徑 /oa

注意:

image-20230507100147494

<%= %>當中可以隨意加空格,因為從標籤開始到標籤結束看作一個整體

  • 改造list.jsp

在之前的專案中使用Servlet查詢完畢資料之後在Servlet中拼接字串輸出網頁;

使用jsp就可以在Servlet中查詢資料,將資料封裝在請求域當中,將請求轉發到jsp上展示資料

image-20230507105601592

轉發到:

<%@ page import="com.eun.oa.bean.Dept" %>
<%@ page import="java.util.LinkedHashSet" %>
<%@page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html lang='en'>
<head>
	<meta charset='UTF-8'>
	<title>部門列表頁面</title>
</head>
<body>
	<h1 align='center'>部門列表</h1>
	<hr>
<table border='1px' align='center' width = '50%'>
	<tr>
		<th>序號</th>
		<th>部門編號</th>
		<th>部門名稱</th>
		<th>操作</th>
	</tr>
    
	<%
		LinkedHashSet<Dept> set = (LinkedHashSet) request.getAttribute("depts");
		for (Dept d : set) {
			String deptno = d.getDeptno();;
			String dname = d.getDname();
			String loc = d.getLoc();
	%>

	<tr align='center'>
	<td><%=deptno%></td>
	<td><%=dname%></td>
	<td><%=loc%></td>
	<td>
		<button onclick='del()'>刪除</button>
		<button onclick='edit()'>修改</button>
		<button onclick='detail()'>詳情</button>
		</td>
	</tr>

	<%}%>

</table>
	<hr>
<button onclick='add()'>新增部門</button>
	
	<script !src=''>
		function add() {
			window.location.href = '<%=request.getContextPath()%>/add.jsp';
		}
		function del() {
			window.location.href ='<%=request.getContextPath()%>/del.jsp';
		}
		function edit() {
			window.location.href = '<%=request.getContextPath()%>/edit.jsp';
		}
		function detail() {
			window.location.href = '<%=request.getContextPath()%>/detail.jsp';
		}
	</script>
</body>
</html>

其中展示的核心:

image-20230507105746468

在list_jsp.java 檔案中:

image-20230507110007438

JSP翻譯引擎替我們做了輸出的工作

這個示例很好的體現了:

  • Servlet只負責處理 資料
  • JSP負責展示 資料

JSP檔案的副檔名是可以配置的,在CATALINA_HOME/conf下:

image-20230507110620426 image-20230507110645431

對於修改 和 詳情 來說,實現的功能都是從資料庫裡面獲取資料並展示,只是修改在展示之後多了一個向資料庫中儲存資料的操作,可以考慮將這兩個功能合併:

  • 第一種方法:

給兩個不同的路徑:

image-20230507123113223

兩個路徑指向同一個Servlet:

image-20230507122927044

在Servlet中對ServletPath(或者RequestURI)進行判斷:

image-20230507123014917
  • 第二種方法:

給兩個相同的路徑,不同的query引數:

image-20230507123316543

攔截同一個路徑:

image-20230507123429436

直接獲取query引數進行跳轉:

image-20230507141518548

Session

任何使用者都可以訪問oa系統,對系統中的資料進行增刪改的操作,需要設定一個登入功能;登入成功才能訪問系統

  1. 資料庫中新增user表,儲存登入資訊

    drop table if exists t_user;
    create table t_user(
        id int primary key auto_increment,
        username varchar(255),
        password varchar(255)
    );
    insert into t_user(username, password) values('admin','123456');
    insert into t_user(username, password) values('zhangsan','123456');
    
    
  2. 實現登入頁面,輸入使用者名稱和密碼提交表單(實際開發中儲存的密碼也不能是明文 MD5加密)

  3. 對應Servlet處理對應請求

    登入成功:跳轉到部門列表頁面

    登入失敗:跳轉到失敗頁面

    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        String pwd = request.getParameter("pwd");
        Connection conn = null;
        PreparedStatement prep = null;
        ResultSet set = null;
        try {
            conn = DBUtil.getConnection();
            String sql = "select * from t_user where username = ? and password = ? ";
            prep = conn.prepareStatement(sql);
            prep.setString(1,username);
            prep.setString(2,pwd);
            set = prep.executeQuery();
            if (set.next()){
                response.sendRedirect(request.getContextPath() + "/dept/list");
            } else {
                response.sendRedirect(request.getContextPath() + "/loginError.jsp");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(set,prep,conn);
        }
    }

但是此時還有問題,只要知道部門地址/dept/list還是可以訪問的;如果提交http://localhost:8080/oa/dept/del?deptno=30

可以直接刪除部門資訊,這個登入沒有起到真正攔截的作用;應該設定只處理登入使用者的請求

Session就可以解決這個問題

B/S結構系統的會話跟蹤技術

會話一詞,指聚談、對話。最早出現在歐陽修《與吳正肅公書》一文中:“前約臨行少留會話,終不克遂,至今為恨。”

從開啟一個瀏覽器訪問某個站點,到關閉這個瀏覽器的整個過程中,稱為一次會話。

會話機制最主要的目的是幫助伺服器記住客戶端狀態標識使用者,跟蹤狀態)。目前伺服器與客戶端的通訊都是透過HTTP協議,而HTTP協議是一種無狀態協議,客戶端第二次來訪時,伺服器不知道客戶端之前是否來訪過,Web伺服器本身不能識別出哪些請求是同一個瀏覽器發出的;即:瀏覽器的每一次請求都是完全獨立的。

為了更好的使用者體驗(比如實現購物車),就有了會話機制,伺服器可以記住並區分客戶端,把同一個客戶端的操作歸類在一起。

由於某些原因,HTTP必須保持無狀態,所以會話跟蹤技術就成了對HTTP無狀態協議的一種擴充套件

假如Http協議是有狀態的,1000個人訪問百度,百度伺服器就要維持1000個人的狀態,伺服器壓力很大,無狀態的協議可以降低伺服器的壓力

基於上面的分析(會話是為了唯一標識一個使用者並記錄其狀態),既然一個會話可以幫助伺服器斷定一個客戶端,那麼反向推導得到的結論就是:當伺服器無法斷定客戶端時,一次會話就結束了,無法斷定客戶端的情況:

  • session失效(伺服器)
  • cookie失效(客戶端)

會話的基本原則:雙方共存(session和cookie)

Java中,會話跟蹤常用的有兩種技術:Cookie和Session,並且Session底層依賴於Cookie(也可以使用自定義token)

Cookie是伺服器響應(客戶端必須先訪問伺服器)給客戶端,並且儲存在客戶端的一份資料;下次客戶端訪問伺服器時,自動攜帶Cookie,伺服器根據Cookie就可以區分客戶端。

伺服器如何將Cookie響應給客戶端:

image-20230507183611158

瀏覽器訪問這個Servlet,Servlet透過Response將Cookie以HTTP響應頭的形式傳送給瀏覽器,也就是Set-cookie:

HTTP響應頭都是以鍵值對的形式組成,可以一鍵一值,也可以一鍵多值

image-20230507183758125

瀏覽器在接收到響應頭後,會將他們作為Cookie檔案儲存在客戶端,當後續請求同一個伺服器,在傳送請求時會自動攜帶Cookie資訊

image-20230507184324496

因為現在客戶端已經有Cookie了,以後每次訪問伺服器都會帶上Cookie,如何在伺服器獲取客戶端的Cookie?

image-20230508084332647

注意,雖然請求頭中cookie的內容是name=eun;time=6pm,但是並不需要我們在Servlet中用分號切割字串。只要呼叫request.getCookies();即可得到cookie陣列(自動切割成陣列)

但是,這種方式傳給瀏覽器的Cookie會隨著瀏覽器的關閉而消失,這也就是為什麼登入京東,新增商品進購物車後關閉瀏覽器就需要重新登入的原因

Cookie的兩種型別

  • 會話cookie (session cookie)
  • 永續性cookie (Persistent cookie)

上文中伺服器向瀏覽器響應的cookie就是會話cookie,會話cookie被儲存在瀏覽器的快取中。

永續性Cookie只需要設定Cookie的持久化時間即可

cookie.setMaxAge(10 * 60); //10 min

設定持久化時間 > 0,即可將cookie在客戶端持久化

在客戶端收到的響應報文中,set-cookie多了一個Expires 過期時間欄位

cookie.setMaxAge(0) 刪除客戶端的cookie資訊

Session

cookie是存於客戶端的,如果儲存了敏感資訊,明文傳輸是不安全的,並且資訊太多會影響傳輸效率;所以出現了Session技術

Session是儲存與伺服器的,是一個HashMap集合。

image-20230507190632687

此時,不再把name=eun;time=6pm這樣的資料作為Cookie放在請求頭/響應頭中傳遞了,而是隻給瀏覽器傳遞一個JSESSIONID(實際上也是cookie),真正的資訊儲存在伺服器端的Session物件中,響應給瀏覽器的Cookie只是Session的id,即JSESSONID。下次訪問該網站時,將JSESSONID帶上,就可以在伺服器上找到對應的Session,相當於帶去了使用者資訊

image-20230508085332518

返回的cookie也是區分會話cookie或永續性cookie的。

其實,在伺服器端建立了session物件之後,即使不寫response.addCookie("JSESSIONID"),JSESSIONID也會被作為cookie返回

只要在伺服器端建立了Session,JSESSIONID預設作為Cookie返回

Session序列化

session序列化其實是一個預設行為,比如當前有上千萬個使用者線上,使用者登入資訊都在各自的session中,當伺服器不得不重啟時,為了不讓當前伺服器儲存的session物件丟失,伺服器會將當前記憶體中的session序列化到磁碟當中,等待重啟完畢重新讀回記憶體;這些操作在瀏覽器端的使用者是感知不到的,因為session還在,不需要重新登入

以Tomcat為例,伺服器的Session都會被儲存在work目錄的對應專案下,關閉伺服器時,當前記憶體中的session會被序列化在磁碟中,變成一個叫SESSIONS.ser的檔案

image-20230508090126645

session的鈍化與活化

自從改用Session後,由於Session都存在伺服器端,當線上使用者過多時,會導致Session猛增,無形中加大了伺服器的記憶體負擔。於是,伺服器有個機制:如果一個Session長時間無人訪問,為了減少記憶體佔用,會被鈍化到磁碟上。

也就是說,Session序列化不僅僅是伺服器關閉時才發生,當一個Session長時間不活動,也是有可能被序列化到磁碟中。當該Session再次被訪問時,才會被反序列化。這就是Session的鈍化和活化。

image-20230508090440408

與伺服器關閉時Session的序列化不同的是:1.每個Session單獨一個檔案,而不是SESSIONS.ser。2.即使Session活化回到記憶體,磁碟的檔案也不消失

還有個問題需要解決:Session被序列化了,存在Session中的值怎麼辦?比如之前有這麼一步操作:

HttpSession session= request.getSession();
session.setAttribute("user", new User("eun", 26));

此時Session中有一個User物件,直接序列化User從記憶體中消失,無法隨Session一起序列化到磁碟。如果希望Session中的物件也一起序列化到磁碟,該物件必須實現序列化介面。

Tomcat session

  • 一次會話對應N次請求
  • HttpServletRequest中:
image-20230507145516770

jakarta規範中,session對應的類名:jakarta.servlet.http.HttpSession

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response){
        //獲取session物件
        HttpSession session = request.getSession();
        response.getWriter().print(session);
    }
  • 在同一個瀏覽器中,不管傳送多少次請求得到物件的hashCode都是相同的,也就是多次請求對應一個session物件

  • 如果關閉瀏覽器再開啟進行請求,得到的記憶體地址就變化了,也就是建立了一個新的session物件。

  • 使用Edge瀏覽器訪問:image-20230507151236041

    使用FireFox瀏覽器訪問:image-20230507151311428

    使用Chrome瀏覽器訪問:image-20230507151343239

    伺服器為這三個瀏覽器分別分配了一個session物件

對於OA專案的登入功能:

  • 不能根據(Session是否存在)JSESSION判斷使用者是否登入,即使身份認證失敗,伺服器也會對該次請求建立Session物件(在首頁是JSP的情況下),如果根據(Session是否存在)JSESSIONID判斷下次訪問時還是無法攔截。
  • 使用者資訊不能存入請求域,因為一次請求結束請求物件就被銷燬,下次請求時無法獲取狀態
  • 使用者資訊不能存入應用域,因為這樣做所有使用者的登入狀態都會被共享,且登入狀態永不停止

可以在登入成功之後將使用者資訊存入會話域當中,在每次傳送請求時對會話域中的資訊進行判斷,如果有使用者資訊就響應請求

//從伺服器中獲取當前的session物件,如果session物件不存在就新建
HttpSession session = request.getSession();

//從伺服器中獲取session物件,如果session物件不存在不會新建,返回null
HttpSession session = request.getSession(false);

不同瀏覽器訪問時獲取的不是同一個session物件,關閉瀏覽器再訪問獲取的也不是同一個session物件

伺服器端在一般情況下是不知道瀏覽器是否關閉的,一般採取 “ 超時機制 ”,一段時間後沒有請求會將這個session銷燬了

例如京東商城的長時間未操作跳轉回登入頁面的功能

  1. session物件是儲存在伺服器端的
  2. 一個session對應一個會話,一個會話對應N個請求
image-20230507160649790

web伺服器中有一個session列表,類似於Map集合,Map集合的key是JSESSIONID,value是session物件

使用者傳送第一次請求的時候,伺服器會建立一個新的session物件,同時給session物件生成一個id,web伺服器會將session的id響應給瀏覽器,瀏覽器將id儲存在快取中

image-20230507162220307

使用者傳送第二次請求的時候,會自動將瀏覽器快取中的session id傳送給伺服器,伺服器獲取到session id,從session列表中查詢到對應的session物件

image-20230507162315990

關閉瀏覽器之後,快取清空,傳送的請求不帶有session id,伺服器會建立一個新的session物件

一次會話:從session物件的建立,到session物件的死亡

session物件的銷燬是超時機制完成的,或者是安全退出,手動銷燬

配置session物件的超時機制:

    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>
<!--session物件的超時時間是30分鐘-->

如果瀏覽器沒有關閉,30分鐘內沒有操作session物件被銷燬,代表會話結束

session id 是以 cookie的形式儲存在瀏覽器中
Cookie: JSESSIONID=1DBBDBFA59546E5E350FA3F25B671D02;

瀏覽器只要關閉,cookie就沒有了

cookie是可以禁用的:

image-20230507162902206

伺服器正常傳送cookie給瀏覽器,瀏覽器拒收cookie,這就導致在每次請求時都沒有攜帶cookie,伺服器都會新建一個session物件

伺服器底層的session物件都沒有銷燬,等待會話超時

會話超時預設值:

image-20230507163314758
  • cookie禁用時,session機制還能實現嗎?

可以,使用URL重寫機制:

image-20230507163728699

注意:url後面加的是分號 ;,jsessionid在伺服器的響應報文中可以獲取

獲取到的還是同一個session物件

提高開發成本,每一個請求路徑都要新增sessionid

  • request 請求域 請求級別
  • session 會話域 使用者級別
  • application 應用域 專案級別

改造OA專案

登入成功之後,將登入資訊儲存到session中,如果有使用者資訊就代表使用者登入成功了,session中沒有使用者資訊代表使用者沒有登陸過,跳轉到登入頁面

不能指定為false:此時必須獲取到session物件,有了session物件才能將資料儲存到會話域當中

image-20230507171057518

在訪問頁面時,不能建立session物件,只能獲取,保證在一次會話當中

image-20230507171718213

如果存入應用域,只要有一個使用者登入成功,所有使用者都能查詢到usernmae

不能只根據session是否存在來判斷登入狀態:最先訪問index.jsp介面,JSP會在訪問時就建立session物件,假如判斷session是否為空,即使身份認證失敗也可以訪問所有介面

image-20230508093513738

但是可以在JSP中禁用session,訪問JSP中不生成session

image-20230508093921041

此時伺服器就不會在第一次請求來時建立session物件:

image-20230508094002203

在部門列表介面展示 歡迎 + 使用者名稱:

image-20230508094604392

但是不能加禁用session指令,如果加了只能透過request獲取session

安全退出

獲取session物件,銷燬session

image-20230508095617016

Session的實現原理中,Session id 就是 JSESSIONID,也就是Cookie

在上文中,伺服器響應cookie給瀏覽器,瀏覽器將cookie儲存在快取中(也可以持久化到硬碟檔案上)

問題:

  • cookie如何生成?
  • cookie儲存在哪裡?
  • 瀏覽器什麼時候會傳送cookie、傳送哪些cookie?

cookie和session是會話保持的共存條件,cookie是將會話狀態保持在客戶端上,session是將會話狀態保持在伺服器上

案例一:

JD在未登入的情況下向購物車中放10件商品,然後關閉瀏覽器,再次開啟電腦訪問JD的時候,購物車中的商品還在

在伺服器端將商品編號以cookie的形式儲存在硬碟檔案當中了

會話的狀態:購物車中的10件商品

登入之後:將購物車中的商品儲存到資料庫當中

案例二:

163郵箱30天免登入

選擇30天免登入後伺服器設定cookie的過期時間是30天

cookie中儲存的是加密後的賬戶和密碼,設定cookie的過期時間是30天

image-20230508103556174

ctrl + shift + del 清除瀏覽器快取

在其他瀏覽器是否可以取消30天免登入?

  1. 改密碼,伺服器session中儲存的使用者密碼和實際密碼不同,訪問失敗
  2. 改密碼,原先瀏覽器中儲存的cookie中的加密後的密碼直接失效

Cookie和Session機制都是HTTP協議的一種擴充套件

HTTP協議中規定,cookie是key-value結構

Java中對Cookie提供了一個Cookie類:jakarta.servlet.http.Cookie

只有一個有參構造方法

image-20230508105201101

HttpServletResopnse中有方法:

image-20230508105349816

這個方法將cookie傳送給瀏覽器客戶端。

Http協議規定:當瀏覽器傳送請求時,會自動攜帶該Path下的cookie資料提交給伺服器

image-20230509141042055 image-20230509141021948

在第一次請求時,請求報文中:

image-20230509141125770

攜帶了JSESSIONID,這個session物件是在訪問JSP時建立的

  • 設定cookie的有效期:
image-20230509141700680 image-20230509141645327

BJ是東八區,這個時間要加上8個小時

設定cookie的有效時間 > 0,cookie一定會儲存在硬碟檔案當中

設定cookie的有效時間 = 0:

image-20230509142147467

可以讓瀏覽器刪除cookie

設定cookie的有效時間 < 0,該cookie不會被儲存(不會被儲存到硬碟檔案當中,和沒調這個方法一樣)

  • cookie的路徑

預設情況下,測試在訪問哪些路徑時會攜帶cookie資訊:

第一次訪問localhost:8080/servlet13/cookie/generate,生成cookie:

image-20230509142854857

此時如果開啟新視窗直接訪問:http://localhost:8080/servlet13/cookie/generate,會在請求時攜帶這個cookie資料:

image-20230509143018416

如果開啟新視窗訪問:localhost:8080/servlet13/cookie/,會攜帶cookie資訊

image-20230509143403232

如果訪問:localhost:8080/servlet13/cookie2,這是一個不存在的路徑,但是訪問這個路徑時沒有攜帶cookie

訪問:http://localhost:8080/servlet13/cookie/abc,不存在該路徑,攜帶了cookie

假設第一次生成cookie時請求路徑為:http://localhost:8080/servlet13/cookie/generate

則cookie關聯的預設Path是:http://localhost:8080/servlet13/cookie/以及它的全部子路徑

只要瀏覽器的請求路徑是這個路徑或這個路徑下的子路徑,這次請求都會攜帶這個cookie資訊

設定cookie後:

image-20230509145143303

在響應頭中:

image-20230509145255261

/servlet13及所有子路徑都會攜帶這個cookie資訊

  • 在伺服器接收客戶端發來的cookie
  <a href="<%=request.getContextPath()%>/cookie/generate">伺服器生成cookie,將cookie響應給瀏覽器,瀏覽器儲存cookie</a>

  <br>

  <a href="<%=request.getContextPath()%>/sendCookie">瀏覽器傳送cookie給伺服器</a>

上面的超連結生成cookie,透過response方法將cookie返回給瀏覽器,下面的超連結點選時會攜帶cookie的資料給伺服器,在伺服器端解析:

image-20230509151656754 image-20230509151637623

注意:沒有cookie傳送時getCookies()返回值是null

Cookie實現10天免登入

在登入頁面 index.jsp中需要判斷:

image-20230509193840963
  1. 以cookie形式登入

    • 跳轉到login方法,從cookie中獲取使用者名稱、密碼
  2. 輸入使用者名稱、密碼登入

    • 輸入使用者名稱、密碼,請求到login方法
    • 判斷是否需要設定cookie
image-20230509193908762

如果需要設定cookie:

image-20230509185519129

注意:只有響應給瀏覽器才能被覆蓋

在login方法中根據使用者名稱、密碼進行身份驗證

image-20230509193519814

注意:不能response.flushBuffer()後再sendRedirect(),不能在提交響應內容之後進行重定向操作。

需要注意的是,在設定cookie的時候,一定要設定path:

只要訪問這個應用,瀏覽器就要攜帶這兩個cookie

image-20230509185728326

因為預設的path是 /oa/user/login,如果想達成免登入的效果必須設定為 /oa,也就是系統的根路徑

疑問:

如果修改登入密碼,直接使用reLogin方法重新登入並清除cookie嗎?

JSP

指令

JSP指令:指導JSP翻譯引擎如何工作

<%@taglib prefix=""%> #引入標籤庫的指令 JSTL標籤庫
<%@include file=""%>  #在JSP中完成靜態包含
<%@page%>             # 
  • Page指令
<%page 屬性名=屬性值 屬性名=屬性值 屬性名=屬性值%>

常用屬性:

<%@page contentType="text/html;charset=UTF-8" %>  #設定響應內容型別

<%@page pageEncoding="UTF-8" %> #設定響應時字符集 在上一屬性中已被包含

<%@page import="java.util.HashSet"%> #導包

<%@page errorPage="error.jsp" %>  #當前頁面出現異常之後,跳轉到error.jsp頁面
<%@page isErrorPage="true" %> # 在errorPage中啟用exception
<%
String name = null;
name.toString();//空指標異常
%>

如果沒有設定errorPage,此時訪問就是500 伺服器內部錯誤,設定之後就顯示errorPage

但是,此時JSP中出錯後臺沒有任何異常資訊,後臺不知道錯誤出在哪裡。

在errorPage中可以啟用JSP九大內建物件之一的Exception物件,這個物件就是剛剛發生的異常物件

<%@page isErrorPage="true" %>

<h1>ERROR</h1>

<%
exception.printStackTrace(); //列印異常資訊到控制檯
%>
image-20230509202817693

九大內建物件

	/*4個域物件*/
	final jakarta.servlet.jsp.PageContext pageContext;     //頁面作用域   最小
	final jakarta.servlet.http.HttpServletRequest request; //請求作用域
    jakarta.servlet.http.HttpSession session;			   //會話作用域
    final jakarta.servlet.ServletContext application;	   //應用作用域   最大
	
	/**/
	java.lang.Throwable exception;
    
    /**/
    final jakarta.servlet.ServletConfig config;
	
	/**/
	final java.lang.Object page = this; //HttpJspBase 父類 Servlet

	/**/
	jakarta.servlet.jsp.JspWriter out; /*輸出*/
    final jakarta.servlet.http.HttpServletResponse response; /*響應*/

EL表示式

Expression Language 表示式語言

JSP中混合Java程式碼,維護麻煩;EL表示式可以代替JSP中的Java程式碼,讓JSP看起來更整潔;EL表示式是JSP的一部分

EL表示式出現在JSP主要是:從某個作用域中獲取資料,轉換為字串,輸出到瀏覽器

  1. 從某個域中取資料(pageContext、request、session、application)
  2. 將資料轉換為字串 呼叫toString方法
  3. 將字串輸出到瀏覽器
image-20230509205224949

${}就可以完成這三個工作。

獲取物件的屬性:

image-20230509211034475

注意:EL表示式中不能新增雙引號,如果帶雙引號會認為是普通字串直接輸出到瀏覽器

其中的username並不是屬性名,而是:

userObj.getUsername()
/*去掉get U變為小寫 去掉最後的()*/
${userObj.username}

可以驗證一下:

  • 測試一:

getUsername()方法註釋掉:

image-20230509211246599

在JSP檔案中:

image-20230509211232356

訪問JSP檔案:

image-20230509211350950
  • 測試二:

getUsername()重新命名:

image-20230509211505655

在JSP中:

image-20230509211525828

訪問:image-20230509211551052

  • 測試三:

設定一個getEmail()方法,但是不提供email屬性,返回固定值:

image-20230509211718528

JSP:

image-20230509211744217

訪問:image-20230509211804487

EL表示式中的 . 並不是物件呼叫屬性,而是呼叫get方法

  • 如果訪問的屬性是引用資料型別中的成員變數:
image-20230509212803076 image-20230509212819840

JSP:

image-20230509212846061

只與get方法的方法名有關

域的優先順序

<%@page contentType="text/html;charset=UTF-8" %>
<%
    pageContext.setAttribute("data","pageContext");
    request.setAttribute("data","request");
    session.setAttribute("data","session");
    application.setAttribute("data","application");
%>

${data}

image-20230509213206279

=作用域越小,優先順序越高=

在沒有指定範圍的前提下,預設從小的範圍中取資料

EL表示式有四個隱含的範圍物件:

  • pageScope
  • requestScope
  • sessionScope
  • applicationScope

在<%%>中,使用的是JSP的九大內建物件

在${}中,使用的是四個隱含範圍物件

指定範圍:

image-20230513133305776

在實際開發中,不同域中的名稱一般都是不一樣的。

  • 如果獲取時屬性名輸錯了:
image-20230513192320153

使用兩種方式獲取的結果是不一樣的:

image-20230513192408228

嚴格意義上說EL表示式比域獲取更強大

${username}等同於request.getAttribute("username") != null ? request.getAttribute("username") : ""

表面上是EL表示式,最終還是要變為Java程式碼的

特殊字元

image-20230513193700865

可以使用中括號加字串訪問對應的屬性,這樣也是呼叫get方法獲取

如果不加雙引號,EL表示式取不到資料

  • 特殊情況
image-20230514082531327

在訪問時image-20230514082712434獲取不到資料

image-20230514082811513可以獲取到資料

如果屬性名中含有特殊字元,就必須使用中括號語法

Map集合使用EL表示式

示例

image-20230514083425753 image-20230514091135988

陣列使用EL表示式

image-20230514092013726

EL表示式不會下標越界:

image-20230514092051002

只會顯示空白,不會有任何錯誤

  • 引用型別陣列
image-20230514092421290
  • 集合
    • ArrayList
image-20230514092729589
    • Set
image-20230514093152369

set集合沒有下標,不能使用[下標]獲取 500錯誤

image-20230514093111578

忽略EL表示式

image-20230514093416261

這樣做就可以忽略EL表示式,將其看作普通的字串image-20230514093441210

但是這樣會將整個頁面中的EL表示式全忽略了,如果只忽略其中的某個:

image-20230514093616385 image-20230514093646294

pageContext

pageContext是頁面上下文物件,這個物件可以get其他物件:

image-20230514094116882

比如獲取request物件:

image-20230514094245723

在頁面上顯示:image-20230514094304814

看起來是完全相同的,沒必要呼叫getRequest獲取(但實際上透過pageContext獲取的request物件是ServletRequest)

但是在EL表示式當中,沒有request這個物件(requestScope只代表請求範圍,不等同於request物件)

EL表示式有隱含的物件:pageContext,這個物件與JSP九大內建物件中的pageContext是同一個物件,這些方法是提供給EL表示式中使用的:

image-20230514094756204

獲取時也是透過get方法獲取

但是,這兩個表示式並不是完全等價的:

對於<%pageContext.getRequest()%>來說,獲取到的是ServletRequest物件:

image-20230514095300852

而在EL表示式中${pageContext.request}獲取的是HttpServletRequest物件,EL表示式進行了強制型別轉換

在獲取應用的根路徑時,就可以:

image-20230514094903337

EL表示式隱含物件

  1. pageContext
  2. param
  3. paramValues
  4. initParam
  5. 其他(不是重點)
  • param

獲取請求引數:

image-20230514100618739

可以直接使用隱含物件param獲取:image-20230514100821332

image-20230514100834372
  • paramValues

假設由核取方塊提交的資料:localhost:8080/jsp/6.jsp?hobby=smoke&hobby=drink

如果直接獲取:

image-20230514101042916

request.getParameter()獲取到的應該是第一個提交的資料:

image-20230514101104342

這時如果使用<%%>獲取就需要使用request.getParameterValues("hobby"),使用EL表示式就需要使用${paramValues.hobby}

image-20230514101859578
  • initParam

假設web.xml檔案中配置瞭如下的初始化資訊:

image-20230514103349355

獲取時:

image-20230514103642679

EL表示式中的運算子

  1. 算術運算子

    + - * /

    image-20230514110212430
  2. 關係運算子

    == != >= < <= eq gt lt

    ${"abc" == "abc"}<br> <%--true--%>
    <%
        String s1 = new String("hehe");
        String s2 = new String("hehe");
        request.setAttribute("a",s1);
        request.setAttribute("b",s2);
    %>
    ${a == b} <%--true--%>
    ${a eq b} <%--true--%>
    <%
        Student stu1 = new Student("zhangsan");
        Student stu2 = new Student("zhangsan");
        request.setAttribute("s1",stu1);
        request.setAttribute("s2",stu2);
    %>
    <%-- == eq != 都會呼叫equals方法--%>
    ${s1 == s2} <%--equals is executed true--%>
    ${s1 eq s2} <%--equals is executed true--%>
    ${s1 != s2} <%--equals is executed false--%>
    ${!(s1 eq s2)} <%--equals is executed false--%>
    ${not(s1 eq s2)} <%--equals is executed false--%>
    
    <%--判斷是否為空,如果為空結果是true--%>
    ${empty param.username}
    ${not empty param.username}
    
    ${empty param.pwd == null}<%--true或false永遠不是null--%>
    
    
  3. 邏輯運算子

    ! && || not and or

  4. 條件運算子

    ? :

    image-20230514112216475
  5. 取值運算子

    [] .

  6. empty運算子

    判斷是否為空,為空返回true

JSTL標籤庫

Java Standard Tag Lib

JSTL標籤庫通常結合EL表示式一起使用,目的是讓JSP中的Java程式碼消失

image-20230514122423413
  1. 引入JSTL標籤庫jar包

    Tomcat10後的jar包:

    jakarta.servlet.jsp.jstl-2.0.0.jar

    jakarta.servlet.jsp.jstl-api-2.0.0.jar

  2. 在JSP中引入要使用的標籤庫:使用taglib指令

    JSTL提供了很多標籤,需要使用<%@taglib prefix=""%>指定要使用哪個標籤

    image-20230514122051740

    核心標籤庫

image-20230514122158885

核心標籤庫字首一般都是c,這時c就代表了這個標籤庫

其中的:

image-20230514130754511

如果導錯包會導致不可預知的500錯誤

指向了一個tld檔案:

image-20230514130844909
 <tag>
    <description>
        Catches any Throwable that occurs in its body and optionally
        exposes it.
    </description>
    <name>catch</name> 名字
    <tag-class>org.apache.taglibs.standard.tag.common.core.CatchTag</tag-class> Java類
    <body-content>JSP</body-content>  標籤體中可以出現的內容 JSP標識標籤體中可以出現符合JSP語法的所有內容:EL表示式
    <attribute>
        <description>
Name of the exported scoped variable for the
exception thrown from a nested action. The type of the
scoped variable is the type of the exception thrown. 描述
        </description>
        <name>var</name> 屬性名
        <required>false</required> 是否必須
        <rtexprvalue>false</rtexprvalue> 該屬性值是否支援EL表示式
    </attribute>
  </tag>

<c:catch var=""> var中的內容不是必須的,屬性值不能寫EL表示式 
    JSP...
</c:catch>

tld檔案就是一個xml檔案,描述了 標籤 和 Java類 之間的關係和標籤中屬性值的規範

比如foreach標籤:

image-20230514125609419

描述了items指定要遍歷的集合,並且該屬性值可以使用EL表示式

var屬性:

image-20230514125908230
  1. 在需要的位置使用標籤

示例:request域有如下資料:

image-20230514123211528

要求遍歷並輸出

使用Java程式碼:

image-20230514123426110

使用JSTL標籤庫:

image-20230514130045580 image-20230514131012504

常用的標籤:

  • if標籤
image-20230514133152423

test屬性:必需、支援EL表示式、boolean型別

Snipaste_2023-05-14_13-41-09

顯示:

image-20230514133410027

其他屬性:

Snipaste_2023-05-14_13-41-30

作用:將var指定的v儲存到scope域,v:if條件比較的結果值

  • foreach
image-20230514134631235 image-20230514134639563

var用來指定迴圈中的變數,begin開始,end結束,step步長

底層實際上將i儲存在pageContext中,所以才能使用EL表示式將其取出

image-20230514135216919

varStatus狀態物件的count屬性,以1開始逐一遞增

  • when - otherwise
image-20230514135956706

顯示效果:

image-20230514135939448

總結

jstl中的核心標籤庫core當中有哪些常用的標籤呢?

  • c:if

    • <c:if test="boolean型別,支援EL表示式"></c: if>
  • c:forEach

    • <c:forEach items="集合,支援EL表示式" var="集合中的元素" varStatus="元素狀態物件"> ${元素狀態物件.count} </c: forEach>
    • <c:forEach var="i" begin="1" end="10" step="2"> ${i} </c: forEach>
  • c:choose c:when c:otherwise

        <c:choose>
            <c:when test="${param.age < 18}">
                青少年
            </c:when>
            <c:when test="${param.age < 35}">
                青年
            </c:when>
            <c:when test="${param.age < 55}">
                中年
            </c:when>
            <c:otherwise>
                老年
            </c:otherwise>
        </c:choose>

JSTL改造OA

改造list中的迴圈展示資料:

image-20230514143842812

前端HTML程式碼中,有一個base標籤,這個標籤可以設定整個網頁的基礎路徑,通常出現在head標籤中

image-20230514144732991

因為是以 /結尾的,後續路徑前可以不加 /

image-20230514144254809

base路徑只對頁面中沒有以/開始的路徑有效

但是在某些情況下可能對JS程式碼失效,最好在JS中的請求路徑都以專案名開始

透過EL表示式可以設定為完全動態的路徑:

Snipaste_2023-05-14_15-53-41
<base href="${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.serverPort}${pageContext.request.contextPath}/">

注意不能加空格,最後的/一定要新增

設定Session失效的問題

在之前的專案中,點選 image-20230514152856005是可以清除cookie的,因為在此時呼叫的是setCookie方法,在方法內部:

image-20230514152952365

重置了cookie的路徑為/oa,這樣是可以清空cookie的

但是在image-20230514153050083點選安全退出時,執行的方法是:

image-20230514153126473

這個方法的思路是:透過request物件獲取cookie,將cookie的生命時間設定為0,對應的響應報文:

image-20230514153257993

在點選安全退出時,傳送的請求路徑為:image-20230514153407874

這樣做是刪不掉cookie的

原因在於:

  • cookie:屬性名username,關聯path /oa(在選擇免登入時設定的cookie)
  • cookie:屬性名username,關聯path /oa/user/logout

就會認為這兩個是不同的cookie,透過下方的cookie刪除上面的那個是刪不掉的,透過上面的cookie刪掉下方的是可行的,因為/oa是包含/oa/user/logout

原因也可能在於:在/oa/user/logout中設定的cookie,path被設定為/oa/user/logout,自動被/oa的cookie覆蓋了

解決方法:設定cookie的路徑等於前文中設定的cookie路徑

image-20230514154153922

此時的響應報文:

image-20230514154136932

當前的專案還是存在缺陷的,在DeptServlet中:

image-20230514155748662

首先要獲取session,判斷session是否存在,是否有name屬性

如果在每個業務模組中都進行這些判斷,比如訂單模組,也需要對登入狀態進行判斷

這樣每次都要重複寫這段程式碼,沒有達到程式碼複用的效果

可以在操作前進行過濾:過濾掉沒有登入的使用者。

Filter 過濾器

DeptServlet、OrderServlet、EmpServlet 每一個Servlet都是處理自己相關的業務,在這些Servlet執行之前都要判斷使用者是否登陸了,如果使用者登入就可以繼續操作,如果沒有登入,這段判斷使用者是否登入的程式碼是固定的,並且在每一個Servlet類當中都要重新編寫,顯然程式碼沒有得到重複利用(可能都要解決中文亂碼問題)

可以使用Servlet規範中的Filter解決這個問題

image-20230514164038887

Servlet程式就是最終要執行的目標,可以透過過濾器Filter來新增過濾程式碼,這個過濾程式碼可以新增到Servlet執行之前,也可以新增到Servlet執行之後,Filter可以在目標程式執行之前過濾,也可以在目標程式執行之後過濾

從上圖可知:過濾器也是與使用者的請求路徑有關的。

  • 如果有多個過濾器:
image-20230514164618139

步驟:

  1. 自定義Java類實現介面jakarta.servlet.Filter,實現所有方法

    init和destory方法是default修飾的

    image-20230514165624612

對Filter中的方法測試:

image-20230514165929193

需要在web.xml檔案中配置Filter:

image-20230514170029535

在伺服器啟動時

image-20230514170132928

執行無參構造和init方法,這說明Filter和Servlet還是有區別的

  • Servlet物件預設情況下,在伺服器啟動時不會建立物件
  • Filter物件預設情況下,在伺服器啟動時建立物件
  • Servlet和Filter都是單例項的

伺服器關閉時destory方法執行

image-20230514170431763

此時 /abc路徑不能與 /a.do /b.do路徑匹配上,不會執行後續的servlet

如果將Filter的路徑改為a.do:

image-20230514170858349

這時與AServlet的請求路徑是相同的,再訪問/servlet14/a.do:

image-20230514170954513

AServlet中的doGet方法是沒有執行的,需要在doFilter方法中對其設定:

image-20230514171147041

訪問/a.do

image-20230514171456774

可以觀察到:

image-20230514171749520
  • 對AServlet、BServlet都進行過濾:
image-20230514171926601

也可以進行字首匹配

image-20230514172214088

注意:*.do前面是不能帶 /的,這種寫法叫擴充套件匹配

也可以字尾匹配所有路徑:

Snipaste_2023-05-14_17-24-04
  • Filter的優先順序天生比Servlet優先順序高,如果存在/a的Filter和Servlet,一定先執行Filter

多個Filter的優先順序

web.xml檔案中有如下配置:

image-20230514191321460
  • 訪問 a.do時,兩個過濾器都會進行過濾,哪個過濾器會先進行過濾?
image-20230514201045263

可以看到,MyFilter中的doFilter方法先執行,Filter2中的doFilter方法後執行

如果將web.xml中的位置進行更換:(只更換filter-mapping的位置就可)

image-20230514201459810

執行順序就改變了:

image-20230514201604468

過濾器是有優先順序的,filter-mapping的順序決定了Filter執行的優先順序

或者使用@WebFilter({"*.do"}):

Snipaste_2023-05-14_20-23-19 Snipaste_2023-05-14_20-23-42

這時的執行順序:

image-20230514202359426

MyFilter1先執行

如果將MyFilter1改名為MyFilterB,MyFilter2改名為MyFilterA:

image-20230514202521423

執行的順序就改變了,說明如果使用註解,過濾器執行的順序是按照字典序(一般都是配置在xml檔案中)

FilterA和FilterB,先執行FilterA

Filter 的生命週期:

  • 預設在伺服器啟動時建立,執行init
  • 每次請求執行doFilter -> chain.doFilter(request,response)
  • 停止時銷燬

Filter的優先順序比Servlet高

責任鏈設計模式

public class Test {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }

    private static void m1() {
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 over");
    }

    private static void m2() {
        System.out.println("m2 begin");
        m3();
        System.out.println("m2 over");
    }

    private static void m3() {
        System.out.println("target is executed");
    }
}

這樣做的話,在編譯階段就確定了呼叫關係;如果想改變呼叫關係,必須修改Java原始碼,也就要重新編譯、測試、釋出,違背OCP

過濾器最大的優點:編譯階段沒有確定呼叫順序,Filter的呼叫順序是配置到web.xml檔案當中的,只要修改web.xml中filter-mapping的順序就可以調整Filter的執行順序,顯然Filter的執行順序是在程式執行階段動態組合的,這就是責任鏈設計模式

  • 核心思想:在程式執行階段,動態的組合程式的呼叫順序

使用過濾器改造oa

image-20230514205605875

目前寫成 /*,表示所有的請求均攔截,即使是正常的登入請求也會被攔截(無限重定向)

Snipaste_2023-05-14_20-57-49

不能攔截的情況

  • 已經登入,不能攔截
  • 要去登入 (/user/login),不能攔截
  • 訪問WelcomeServlet(訪問index.jsp),不能攔截
image-20230514213657839

Listener 監聽器

在Servlet中,所有監聽器介面都是以 “Listener”結尾,監聽器實際上是Servlet規範留下的 “ 特殊時刻 ”,在某個特殊時刻想執行一段程式碼,就要用到對應的監聽器

Servlet規範中提供的監聽器:

  • jakarta.servlet包下:

    • ServletContextListener
    • ServletContextAttributeListener
    • ServletRequestListener
    • ServletRequestAttributeListener
  • jakarta.servlet.http包下:

    • HttpSessionListener

    • HttpSessionAttributeListener 監聽session域中資料的變化

    • HttpSessionBindingListener 不需要加WebListener註解

      User類實現了HttpSessionBindingListener 監聽器,在存入session時會觸發HttpSessionBindingListener 的方法和HttpSessionAttributeListener 中的方法

    • HttpSessionIdListener 監聽session id 的改變

    • HttpSessionActivationListener 監聽session物件的鈍化或活化

域物件監聽器

以ServletContextListener為例(監聽ServletContext域物件的狀態),實現監聽器的步驟:

  1. 編寫類實現ServletContextListener介面,並且實現裡面的方法。

    image-20230515091945300

    這兩個方法都有ServletContextEvent物件

  2. 在web.xml檔案中對ServletContextEvent進行配置

    image-20230515092151236

    也可以不使用配置檔案,使用註解:

    image-20230515092233416
  3. 所有監聽器中的方法都不需要手動呼叫,由伺服器自動呼叫

    • 某個特殊事件發生的時候,被Web伺服器自動呼叫
    image-20230515092513658

示例:

image-20230515092632540

在伺服器啟動時執行contextInitialized()方法,關閉時執行contextDestroyed()方法

如果希望在伺服器啟動時執行某段程式,可以將程式寫在這個方法中

HttpSessionListener、ServletRequestListener都是和這個類很相似的:

image-20230515093508864

對於HttpSessionListener,其中的destroyed方法會在session物件被銷燬(或者是手動呼叫invalidate()方法)

對Session進行測試:

image-20230515095125891
  • 如果請求訪問的是一個普通的servlet:
image-20230515102201905

此時傳送請求:image-20230515102233809

說明session物件未建立。

如果在doGet方法中手動獲取session物件:

Snipaste_2023-05-15_10-26-05 image-20230515102537743

此時會建立session物件,但是sessionid此時還未進行分配。

  • 如果訪問的是一個啟用session的jsp http://localhost:8080/servlet15/index.jsp
Snipaste_2023-05-15_10-27-50

會建立session物件


域資料監聽器

HttpSessionAttributeListener、ServletRequestAttributeListener、ServletContextAttributeListener都是監聽域物件存取資料的監聽器,以HttpSessionAttributeListener為例:

image-20230515094420335

測試程式:

image-20230515103220594

執行過程:

image-20230515103259180

HttpSessionBindingListener

當前有兩個類:

image-20230515103809312 image-20230515103824690

測試程式:

image-20230515104752955

控制檯在Cat物件被存入後輸出 Cat data is bound

說明:

  • HttpSessionBindingListener :監聽實現該介面的物件,該物件向session域中存取才會觸發
  • HttpSessionAttributeListener:監聽session物件,對session域進行操作時觸發

記錄線上使用者人數

如果用session物件的數量統計線上人數的話,只要訪問index.jsp就會建立一個session物件;同一個使用者也可能有很多個不同的session物件;不夠精確。

對於當前的OA專案來說,使用者首先訪問index.jsp頁面,此時會建立session物件,只有在登入成功後才會將username儲存到session域當中;如果關閉瀏覽器再次訪問,不管是使用cookie登入還是賬號密碼登入,正常情況下此次訪問不能記錄為一次新的訪問;但是這樣做一定會建立一個新的session物件,所以完整的做法是:維護一個集合,儲存當前登入的使用者的使用者名稱(假定唯一)

想精確(排除相同使用者)就使用HashSet,大致估計可以使用int count

使用者登入成功的標誌:User型別物件(使用者名稱)儲存在session當中

集合的選擇:HashSet 去重、效率高,可以避免在每次新增前判斷集合中是否存在這個元素,如果存在預設就不會新增該元素

image-20230515120606942
  1. 監聽session域,每當有新的add請求傳送時,判斷集合中是否存在這個使用者名稱,如果沒有就將這個使用者名稱儲存到集合中;

    image-20230515120624104

    採用這種方式,如果在客戶端登陸後清除cookie再次登入,會建立一個新的session物件,users集合中已經儲存了使用者名稱,會試圖將使用者名稱再次加入users集合當中。

  2. 在登入成功後,判斷集合中是否存在這個使用者名稱,如果沒有就將這個使用者名稱儲存到集合中;

    image-20230515121128229

在點選 退出系統 / session失效時,將集合中對應的使用者名稱刪除,線上人數就是集合的元素個數

image-20230515121218990

集合在實際開發中通常儲存在應用域當中,但對集合進行操作要注意執行緒安全問題


HttpSessionBindingListener實現線上人數統計

HttpSessionBindingListener在實際開發中更適合做這件事情,因為使用者一定會對應一個bean實體類

application併發操作會存線上程安全問題

這樣做可以將使用者登入/退出都設定在同一個類當中,內聚性更高,但是這種方法無法避免同一個使用者多次登入的情況

即使不清除cookie,每次登入都會進行這個操作

image-20230515124418358

但是可能存在問題

  1. 安全退出是可以監聽到的,session過期是否可以被監聽到?

  2. 這個程式碼是存線上程安全問題的

MVC架構模式

假設當前要處理轉賬功能:

/**
 * 不使用MVC架構模式的前提下,完成銀行賬戶轉賬
 */
@WebServlet("/transfer")
public class AccountTransferServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        double money = Double.parseDouble(request.getParameter("money"));
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        Connection connection = null;
        PreparedStatement prep = null;
        ResultSet set = null;
        try {
            connection = DBUtil.getConnection();

            String selectBalanceFromAct = "select balance from t_act where actno = ?";
            prep = connection.prepareStatement(selectBalanceFromAct);
            prep.setString(1,fromActno);
            set = prep.executeQuery();
            if (set.next()){
                double balance = set.getDouble("balance");
                System.out.println(balance);
                if (balance < money){
                    /*餘額不足*/
                    throw new MoneyNotEnoughException("餘額不足");
                }
                /*餘額充足*/
                connection.setAutoCommit(false); /*關閉自動提交*/
                String updateFromAct = "update t_act set balance = balance - ? where actno = ?";
                prep = connection.prepareStatement(updateFromAct);
                prep.setDouble(1,money);
                prep.setString(2,fromActno);
                int count = prep.executeUpdate();
                
                String updateToAct = "update t_act set balance = balance + ? where actno = ?";
                prep = connection.prepareStatement(updateToAct);
                prep.setDouble(1,money);
                prep.setString(2,toActno);
                count += prep.executeUpdate();

                if (count != 2){
                    throw new AppException("App異常");
                }
                connection.commit();
                out.print("轉賬成功");
            }
        } catch (Exception e) {
            out.print(e.getMessage());
            try {
                if (connection != null)
                    connection.rollback();
            } catch (SQLException exc) {
                exc.printStackTrace();
            }
        } finally {
            DBUtil.close(set,prep,connection);
        }


    }
}

AccountTransferServlet 負責了:

  1. 資料的接收
  2. 核心業務邏輯
  3. 資料庫的CRUD
  4. 資料的展示

問題:

  • 程式碼的複用性太差,如果單獨查詢餘額還需要再寫一個Servlet,此時的查詢餘額的方法耦合在整個doPost內部
    • 原因:不符合單一職責原則,沒有進行職能分工,程式碼和程式碼之間的耦合度太高
  • 程式碼擴充套件性差
    • 不符合單一職責原則
  • 資料庫和業務邏輯混合,應該分別處理

理論基礎

分層的原因:希望達到單一職責,程式碼的耦合度降低,擴充套件力提高,元件的可複用性增強

  • M Model : 資料/業務,處理業務/資料
  • V View : 檢視/展示,展示資料
  • C Controller : 控制器,核心

MVC是透過控制器C排程M、V完成業務的處理

image-20230515143634512

抽取層

image-20230515202904093
  • AccountDao類:處理資料的CRUD
  1. Dao Data Access Object(資料訪問物件)
  2. DAO實際上是一種設計模式,屬於JavaEE的設計模式之一(不是23種設計模式)
  3. DAO只負責表的CRUD,沒有任何的業務邏輯在裡面
  4. 一般情況下,一張表對應一個DAO物件
public class AccountDao {
    int insert();
    int deleteByActno();
    int update();
    Account selectByActno();
    List<Account> selectAll();
}

在資料庫中查到的是一條記錄,對應到Java中應該是一個物件:將零散的資料封裝為一個物件

public class Account {
    private long id;
    private String actno;
    private double balance;
}

但是在資料庫中查詢到的balance可能是null,如果null賦值給double型別會報錯,可以使用包裝類進行改進:

public class Account {
    private Long id;
    private String actno;
    private Double balance;
}

這個bean類也被稱為:

  • pojo 物件 ,Plain Ordinary Java Object 簡單的Java物件

  • domain物件,領域模型物件

最終得到的DAO類:

public class AccountDao {
    /*插入賬戶資訊*/
    public int insert(Account account){
        int count = 0;
        
        return count;
    }
    /*根據主鍵刪除*/
    public int deleteById(Long id){
        int count = 0;

        return count;
    }
    /*根據賬號刪除*/
    public int deleteByActno(String actno){
        int count = 0;

        return count;
    }
    /*更新記錄*/
    public int update(Account account){
        int count = 0;

        return count;
    }
    /*根據賬號查詢*/
    public Account selectByActno(String actno){
        return new Account();
    }
    /*查詢所有賬戶資訊*/
    public List<Account> selectAll(){
        return null;
    }
}
image-20230515195643528

DAO中的程式碼和業務沒有任何關係,只是對資料進行增刪改查的。

  • AccountService:解決核心業務邏輯

AccountService:專門處理Account業務的類,在該類只專注業務,也可以叫:AccountBiz

該類中的方法名一定要體現處理哪些業務

image-20230515204412272
  • AccountServlet:Controller,負責排程
image-20230515205936833

Controller中有Service,Service中有Dao

這就是三層架構

image-20230515210519052

其中的三層架構:

image-20230515210912258

對於MVC模式:

image-20230515211235953

其中的Model包含了:

  • pojo、bean、domain
  • Service
  • Dao

Model呼叫Service、Dao的時候需要使用pojo

image-20230515212337343

SSM:

  • Spring:整個專案所有物件的建立以及維護物件和物件之間的關係
  • SpringMVC:體現了MVC架構模式
  • MyBatis:持久層框架

接下來需要解決事務的問題。

解決MVC的事務問題

image-20230516133016817

如果此時發生異常,轉賬失敗但是fromAct賬戶會減少對應的金額

service中一個方法就是一個完整的業務流程,控制事務應該在方法執行時開啟事務,所有操作成功之後提交事務

image-20230516133422791

事務是在service層進行控制的,一般情況下一個業務方法對應一個事務

image-20230516140556255

這樣還不能達成想要的效果,因為Dao層中的連線物件並不是service層中關閉了自動提交的connection物件

設定conn為Dao層方法的引數:

image-20230516142508700

但是這樣做Dao層方法都帶有conn引數,顯然是不合適的

image-20230516142546865

第二種思路:

但是這樣做在service層就需要新增JDBC的程式碼,這是不好的做法,另一種思路是在Dao中不關閉資料庫連線,封裝commit和rollback方法供service層呼叫,只適用於目前的情況

image-20230516141213336

這種方法就需要額外增加一個Connection區域性變數

Snipaste_2023-05-16_13-56-52

在service層中:

image-20230516135822877

ThreadLocal

在第一種思路的基礎上進行改進,去掉Dao層方法中的conn引數

  • service方法和Dao層方法的呼叫都是在同一個執行緒當中,使conn物件繫結到執行緒上

image-20230516143049317

可以設定一個Map集合,將執行緒和Connection繫結在一起

image-20230516160407904

封裝在DBUtil當中:

image-20230516160539332

在Dao,Service中呼叫:

image-20230516161422349image-20230516161440340

MyThread類在java.lang.ThreadLocal中已經實現了,改造完成的DBUtil工具類:

image-20230516162245881

在close方法中需要進行額外的處理:

image-20230516162357259

關閉連線之後,如果不從執行緒池中移除,下次獲取到的還是關閉後的連線,無法進行資料庫操作

Tomcat伺服器內建了一個執行緒池,核心執行緒是不會銷燬的,一定要關閉連線

根本原因是Tomcat伺服器支援執行緒池的;

image-20230516184339498

在這兩個查詢方法中不能關閉connection物件,否則在後面的update方法中不能獲取到同一個資料庫連線物件

不同功能的類放在不同的包下

三層架構:

image-20230516185303217

MVC:

image-20230516185510610

面向介面程式設計

層與層之間是面向介面程式設計的,最終的結構:

image-20230516190908917

在web層呼叫service層時,使用多型:

Snipaste_2023-05-16_19-10-02

service層呼叫dao層,也使用多型:

image-20230516191039488

不足

  1. 事務:在service層控制了事務,service方法中的事務控制程式碼太突兀了

    動態代理機制

  2. 雖然面向介面程式設計了,但是image-20230516191223288

使用類名,耦合度太高了。