解讀 Servlet 原始碼:GenericServlet,ServletConfig,ServletContext

2023-03-20 18:07:33

解讀 Servlet 原始碼:GenericServlet,ServletConfig,ServletContext

每博一文案

人活著,就得隨時準備經受磨難。他已經看過一些書,知道不論是普通人還是了不起的人,都要在自己的一生中經歷許多磨難。磨難使人堅強。
人和社會、一切鬥爭的總結局也許都是中庸而已。與其認真、不如隨便、採菊東籬下、悠然見南山。有錢就尋一醉、無錢就尋一睡、與過無爭、隨遇而安。
hellip;…人哪,活著是這麼的苦!一旦你從幸福的彼岸被拋到苦難的此岸,你真是處處走頭無路;而現在你才知道,在天堂與地獄之間原來也只有一步之遙!
倘若流落在他鄉異地,生活中的一切都將失去保障,得靠自己一個人去對付冷酷而嚴峻的現實了
青年,青年,無論受怎樣的挫折和打擊,都要咬著牙關挺住,因為你們完全有機會重建生活;只要不灰心喪氣,每一次挫折就只不過是通往新境界的一塊普通的絆腳石,而絕不會置人於死命。
你永遠要寬恕眾生,不論他有多壞,甚至他傷害過你,你一定要放下,才能得到真正的快樂。
也許人生僅有那麼一兩個輝煌的瞬間————甚至一生都可能在平淡無奇中度過......不過,每個人的生活同樣也是一個世界,即是最平凡的人,也得要為他那個世界的存在而戰鬥。這個意義上來說,在這些平凡的世界裡,也沒有一天是平靜的。因此,大多數
普通人不會像飄飄欲仙的老莊,時常把自己看作是一粒塵埃————儘管地球在浩渺的宇宙中也只不過是一粒塵埃罷了。
                                       ——————《平凡的世界》

@

1. Servlet物件的生命週期

什麼是Servlet 物件的生命週期 ?

Servlet 物件的生命週期表示:一個Servlet物件從出生在最後的死亡,整個過程是怎樣的 。

Servlet物件是由誰來維護的?

  • Servlet 物件的建立,Servlet物件上方法的呼叫,以及Servlet 物件的銷燬,JavaWeb程式設計師都無權干預的。
  • Servlet 物件的生命週期是由 Tomcat 伺服器(web Server)全權負責的
  • Tomcat 伺服器通常又被我們稱之為 WEB容器 (WEB Container) 。WEB 容器來管理 Servlet物件的死活

我們自己new的Servlet物件受WEB容器的管理嗎?

我們自己 new 的 Servlet 物件是不受 WEB容器 管理的。

因為: WEB容器 建立的 Servelt 物件,這些Servlet 物件都會被放到一個集合當中(HashMap) ,只有放到這個 HashMap 集合中的 Servlet 才能夠被 WEB容器管理,自己 new 的 Servlet 物件不會被 WEB容器管理,因為我們自己 new 的Servlet物件並不沒有存放到 WEB 容器當中。

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

1.1 Servlet 五 個方法的呼叫週期

Servlet 必須重寫的五方法分別為:init(),service(ServletRequest request, ServletResponse response),getServletConfig(),getServletInfo(),destroy(),還有一個無參構造器 什麼時候建立的,什麼時候呼叫的,什麼時候銷燬的。

package com.RainbowSea.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class AServlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {
        
    }

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

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {

    }

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

    @Override
    public void destroy() {

    }
}




執行如下程式碼,啟動 Tomcat 伺服器:並存取該AServlet ,通過在瀏覽器其當中輸入: http://127.0.0.1:8080/blog01/A

package com.RainbowSea.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class AServlet implements Servlet {

    public AServlet(){
        System.out.println("AServlet 的無參構造器的呼叫");
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("AServlet 中的 init 的呼叫執行");
    }

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

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        System.out.println("AServlet 中的 service 的方法的呼叫執行");
    }

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

    @Override
    public void destroy() {
        System.out.println("AServlet 中的 destroy 方法的執行呼叫");
    }
}

<?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>
<!--        兩個 name 值要保持一致-->
        <servlet-name>AServlet</servlet-name>
        <servlet-class>com.RainbowSea.servlet.AServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AServlet</servlet-name>
<!--        注意: / 開始-->
        <url-pattern>/A</url-pattern>
    </servlet-mapping>
</web-app>

執行結果如下:

重點:

  • 當我們在預設普通設定的情況下,啟動Tomcat 伺服器的時候,我們編寫的 AServlet 物件並沒有被範例化(因為並沒有呼叫我們 AServlet 構造器建立物件)。
  • 當用戶存取該資源,第一次向我們的 AServlet 傳送請求的時候。我們的 Servlet物件就被範例化了(這裡是 AServlet 的構造器被執行了,並且執行的是該 AServlet 無參構造器。 )
  • 之後,當 Servlet 物件被建立出來了,也就是這裡的 AServlet 物件,Tomcat 伺服器馬上就呼叫了 AServlet 物件當中的 init()物件方法 ,(注意:init()方法在執行的時候,我們的 Servlet(這裡的 AServlet)物件就已經存在了,已經被建立出來了,因為 init() 是物件方法,需要通過建立物件才能被呼叫(特殊的 :反射除外))。
  • 使用者傳送第一次請求的時候,init()方法執行完,Tomcat 伺服器馬上呼叫了 Servlet (這裡的AServlet 物件)當中的 servelt() 方法。
  • 注意:當用戶繼續傳送第二次請求(重新整理該,頁面的資源,重新傳送新的請求)。或者第三次,或者第四次請求的時候,Servlet物件並沒有新建,還是使用之前建立好的Servlet物件,直接呼叫該Servlet物件的service方法結果如下:

  • 根據上述結果:我們可以知道如下資訊:
    • Servlet 物件使單例的 (單範例的。但是要注意:Servlet 物件使單範例的,但是 Servlet 類並不符合單例模式 的設計。因為真正的單例模式的構造器private私有的 ,所以我們稱之為 假單例 ,之所以單例是因為 Servelt 物件的建立,我們 javaWeb程式設計師是管不著的,這個物件的建立只能是 Tomcat 伺服器或其他伺服器來說的算的。Tomcat 只會建立一個,所以導致了單例,但是屬於假單例。)
    • 無參構造器,只會執行一次,該無參構造器是通過反射機制呼叫的(通過從 web.xml 中的設定資訊獲取到其中對應類的:全限定類名 建立物件),
    • init() 方法只在第一次使用者傳送請求的時候執行,init物件方法也只被 Tomcat 伺服器呼叫一次。
    • 只要使用者傳送一次請求:service() 方法必然會被 Tomcat 伺服器呼叫一次,傳送 100 次請求,servlect() 方法就會被呼叫 100次。
  • 我們關閉 Tomcat 伺服器,控制檯上顯示如下結果

從上述控制檯上顯示的結果,我們可以知道:當我們關閉伺服器的時候 destroy()方法被呼叫了

  • destroy()方法是在什麼時候被呼叫的 ?
  • 在伺服器關閉的時候。

  • 因為伺服器關閉的時候:要銷燬 AServlet 物件的記憶體資訊。

  • 所以伺服器在銷燬 AServlet 物件記憶體之前,Tomcat 伺服器會自動呼叫 AServlet 物件中的 destroy() 方法。

問題:destroy() 方法呼叫的時候,Servlet 物件銷燬了還是沒有銷燬 ???

答:destroy() 方法執行的時候,AServlet 物件還在,沒有被銷燬,因為 destroy() 方法是物件方法。呼叫該方法的話需要通過物件才能呼叫的(反射除外),destroy()方法執行結束之後,AServlet 物件的記憶體才會被 Tomcat 釋放。

注意點: 對應 Servlet 類我們不要編寫構造器。

當我們Servlet 類中編寫了一個有引數的構造器,那麼如果我們沒有手動再編寫一個無參構造器的話,無參構造器就會消失。

如果一個 Servlet 無參構造器消失的會,如何:結果如下:

報錯:500 錯誤。500 是一個 HTTP 協定的錯誤狀態碼。 500 一般情況下是因為伺服器端的 java 程式出現了異常 。(伺服器端的錯誤基本上都是 500 錯誤,伺服器內部錯誤)

  • 如果一個 Servlet 類沒有無參構造器的話,會導致 500 錯誤,反射機制無法通過 無參構造器建立物件 。導致 無法範例化 Servlet 物件。
  • 所以,一定要注意:在 Servlet 開發當中,不建議 程式設計師來定義構造器,因為定義不當,一不小心就會導致無法範例化 Servlet物件了。

思考:Servlet 的無引數構造器是在物件第一次建立的時候執行的,並且只會執行一次。而 init()方法也是在物件第一次拆建立的時候執行的,並且也只會執行一次。那麼這個無參構造器可以代替 init()方法嗎 ?

答:不能。

Servlet 規範中有要求,作為 javaWeb 程式設計師,編寫 Servlet 類的時候,不建議手動編寫構造器,因為編寫構造器,很容易讓無引數構造器消失。這個操作可能會導致 Servlet 物件無法範例化,所以 init() 方法是有存在的必要的。

1.2 Servlet 常用的三個方法使用總結

package com.RainbowSea.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class AServlet implements Servlet {
    // init 被翻譯為初始化
    // init 方法只會被執行一次,基本上和 Servlet構造器的呼叫同時執行,在Servlet 物件第一次被建立只會執行
    // init 方法通常是完成初始化操作的。
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("AServlet is init method execute!");
    }


    // service 方法:是處理使用者請求的核心方法
    // 只要使用者傳送一次請求,service 方法必然會執行一次
    // 傳送100次請求,service方法執行100次
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        System.out.println("AServlet is service method execute");
    }

    // destroy ()方法也是隻執行一次
    // Tomcat 伺服器在銷燬AServlet 物件之前會呼叫一次destroy 方法
    // destroy()方法在執行的時候,AServlet 物件的記憶體還沒有被銷燬,即將被銷燬,因為destroy 是物件方法,需要通過物件呼叫
    // destroy 方法中可以編寫銷燬前的準備
    // 比如:伺服器關閉的時候,AServelt 物件開啟了一些資源,這些資源可能是流,也可能是資料庫連線
    // 那麼關閉伺服器的時候,要關閉這些流,關閉這些資料庫連線,那麼這些關閉資源的程式碼舊可以寫到destroy()
    @Override
    public void destroy() {
        System.out.println("AServlet is destroy method execute");
    }
}

關於Servlet類中方法的說明:

  • 無參構造器:只會執行呼叫一次。
  • init() 方法:init 被翻譯為初始化,init ()方法只會被執行一次;基本上和 Servlet構造器的呼叫同時執行;在Servlet 物件第一次被建立只會執行。init 方法通常是完成初始化操作的。
  • service() 方法:service() 方法是處理使用者請求的核心方法。只要使用者傳送一次請求,service 方法必然會執行一次;傳送100次請求,service方法執行100次。
  • destroy() 方法:destroy 方法中可以編寫銷燬前的準備;比如:伺服器關閉的時候,AServelt 物件開啟了一些資源,這些資源可能是流,也可能是資料庫連線;那麼關閉伺服器的時候,要關閉這些流,關閉這些資料庫連線,那麼這些關閉資源的程式碼舊可以寫到destroy()。

init、service、destroy方法中使用最多的是哪個方法?

  • 使用最多就是service()方法,service方法是一定要實現的,因為service方法是處理使用者請求的核心方法。

  • 什麼時候使用init方法呢?

    init方法很少用;通常在init方法當中做初始化操作,並且這個初始化操作只需要執行一次。例如:初始化資料庫連線池,初始化執行緒池....

  • 什麼時候使用destroy方法呢?

    destroy方法也很少用;通常在destroy方法當中,進行資源的關閉。馬上物件要被銷燬了,還有什麼沒有關閉的,抓緊時間關閉資源。還有什麼資源沒儲存的,抓緊時間儲存一下。

Servlet物件更像一個人的一生:

  • Servlet的無引數構造方法執行:標誌著你出生了。
  • Servlet物件的init方法的執行:標誌著你正在接受教育。
  • Servlet物件的service方法的執行:標誌著你已經開始工作了,已經開始為人類提供服務了。
  • Servlet物件的destroy方法的執行:標誌著臨終。有什麼遺言,抓緊的。要不然,來不及了。

1.3 補充:讓啟動Tomcat 伺服器的時候會呼叫構造器

從上述內容我們知道了,當 Tomcat 伺服器啟動的時候,我們的構造器實際上還沒有呼叫,也就是物件還沒有被範例化建立出來。但是我們想在啟動 Tomcat 伺服器的時候就呼叫構造器。可以使用如下方式:

在我們想讓Tomcat 伺服器啟動的時候呼叫哪個類的構造器,就在該類當中的 web.xml 加上如下:標籤 <load-on-startup>(填寫數值)</load-on-startup> : 的作用就是啟動伺服器的時候就會建立該物件:數值越小的越先被建立

<!--<load-on-startup> 的作用就是啟動伺服器的時候就會建立該物件:數值越小的越先被建立-->
<load-on-startup>2</load-on-startup>

舉例:

<?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>
        <!--        兩個 name 值要保持一致-->
        <servlet-name>AServlet</servlet-name>
        <servlet-class>com.RainbowSea.servlet.AServlet</servlet-class>
        <!--        的作用就是啟動伺服器的時候就會建立該物件:數值越小的越先被建立-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>AServlet</servlet-name>
        <!--        注意: / 開始-->
        <url-pattern>/A</url-pattern>
    </servlet-mapping>


    <servlet>
        <!--        兩個 name 保持一致-->
        <servlet-name>BServlet</servlet-name>
        <servlet-class>com.RainbowSea.servlet.BServlet</servlet-class>
        <!--        的作用就是啟動伺服器的時候就會建立該物件:數值越小的越先被建立-->
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>BServlet</servlet-name>
        <!--        注意 / 開始-->
        <url-pattern>/B</url-pattern>
    </servlet-mapping>
</web-app>

2. GenericServlet

2.1 介面卡

我們編寫一個Servlet 類直接實現了 這個 Servlet 介面 存在什麼缺點沒有:?
有的:我們編寫的 Servlet 類實現了該 Servlet 介面中所有的方法。但是我們其實只需要其中的 service()方法 ,其他方法大部分情況下是不需要使用的。簡單粗暴的實現了我們所有的方法,但是其中大部分的方法我們是不需要的,這樣就顯的我們的程式碼很醜陋了:

所以:我們不想要重寫所以的方法,而是重寫我們需要用的方法。這樣該怎麼做呢?

我們可以使用 23種設計模式中的一種:介面卡模式(Adapter)

介面卡模式的理解:舉例

比如:如果我們的手機充電時直接插入到 220V 的電壓上,手機會直接報廢的,因為手機無法接受如此高的電壓,怎麼辦呢?
我們可以該手機配備一個電源介面卡——充電器,手機連線充電器(介面卡),介面卡連線 220V的電壓,這樣就問題解決了。同理的還有很多:電腦的充當器。充電熱水壺等等。

編寫一個 GenericServlet (abstract )抽象類介面卡。
這個介面卡是一個abstract抽象類。:我們該類實現了 Servlet 介面中所有的方法,但是其中我們常用的方法 service() 定義為是抽象方法,因為如果不想實現的方法,可以定義將該方法定義為抽象方法就不用重寫了,而是交給繼承的子類重寫就好了。因為重寫方法只能存在於抽象類,介面當中,所以我們這個介面卡就為了一個抽象類。這樣我們以後編寫的所以 Servlet 類就只需要繼承 GenericServlet 抽象類,並且只要重寫其中的 service() 抽象方法即可。但是我們又可以使用 Servlet 介面中的方法,因為我們的父類別GenericServlet 抽象類 實現了 Servlet 介面中的所有方法。如有必要,我們編寫的Servlet 類也可以重寫GenericServlet 抽象類中的方法,該重寫的方法也是重寫Servlet 介面中的方法。

具體編寫如下:

abstract class GenericServlet 抽象類介面卡

package com.RainbowSea.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;


/**
 * 編寫一個標準通用的Servlet
 * 以後所有的Servlet 類都不要直接實現Servlet 介面了。
 * 以後所有的Servlet 類都繼承 GenericServlet 類。
 * GenericServlet 就是一個介面卡
 */
public abstract class GenericServlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {

    }

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

    /**
     * 抽象方法:這個方法最常用,所以要求子類必須實現Service 方法。
     */
    @Override
    public abstract void service(ServletRequest request, ServletResponse response) throws ServletException, IOException;

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

    @Override
    public void destroy() {

    }
}

以後編寫的Servlet 都繼承該 GenericServlet 抽象類介面卡

package com.RainbowSea.servlet;


import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class AServlet extends GenericServlet {

    /**
     * 只需要重寫我們常用的 service 父類別中的抽象方法即可
     */
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        System.out.println("AServlet 處理請求中....");
    }
}

package com.RainbowSea.servlet;


import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class BServlet extends GenericServlet {

    /**
     * 只需要重寫我們常用的 service 父類別中的抽象方法即可
     */
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        System.out.println("BServlet 處理請求中....");
    }
}



問題:提供了 GenericServlet 類,init()方法還好執行嗎?

答:還會執行,執行 GenericServlet 中被重寫的 init()方法。

問題:init()方法是誰呼叫的 ?

Tomcat 伺服器呼叫的。

問題:init()方法中的 ServletConfig 物件是誰建立的 ? 有是誰傳過來的?

是由 Tomcat伺服器 建立的,也是由 Tomcat 伺服器傳過來的。都是Tomcat 伺服器乾的。

Tomcat 伺服器先建立了 ServleConfig 物件,然後呼叫 init()方法,將ServleConfig 物件傳給了 init()方法

Tomcat 執行init()方法的虛擬碼

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

    }
public class Tomcat {
    public static void main(String[] args){
        // .....
        // Tomcat伺服器虛擬碼
        // 建立LoginServlet物件(通過反射機制,呼叫無引數構造方法來範例化LoginServlet物件)
        Class clazz = Class.forName("com.bjpowernode.javaweb.servlet.LoginServlet");
        Object obj = clazz.newInstance();
        
        // 向下轉型
        Servlet servlet = (Servlet)obj;
        
        // 建立ServletConfig物件
        // Tomcat伺服器負責將ServletConfig物件範例化出來。
        // 多型(Tomcat伺服器完全實現了Servlet規範)
        ServletConfig servletConfig = new org.apache.catalina.core.StandardWrapperFacade();
        
        // 呼叫Servlet的init方法
        servlet.init(servletConfig);
        
        // 呼叫Servlet的service方法
        // ....
        
    }
}

2.2 模板方法設計模式

思考: init 方法中的ServileConfig 物件是小貓咪建立好的,這個ServletConfig物件目前在init 方法的引數上,屬於區域性變數。那麼ServletConfig 物件肯定以後要在Service 方法中使用,怎麼才能保證ServletConfig 物件在Service方法中能夠使用呢?

答: 可以定義一個類的屬性,再通過 init 方法的,將該ServleConfig 賦值上去

具體程式碼如下:

package com.RainbowSea.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;


/**
 * 編寫一個標準通用的Servlet
 * 以後所有的Servlet 類都不要直接實現Servlet 介面了。
 * 以後所有的Servlet 類都繼承 GenericServlet 類。
 * GenericServlet 就是一個介面卡
 */
public abstract class GenericServlet implements Servlet {
    private ServletConfig config ;

    // 這樣我們在我們的子類當中的 service()方法當中也可以使用 Tomcat 伺服器建立的
    // ServletConfig 物件了
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
    }

    // 獲取該 config 物件值
    public ServletConfig getConfig() {
        return config;
    }

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

    /**
     * 抽象方法:這個方法最常用,所以要求子類必須實現Service 方法。
     */
    @Override
    public abstract void service(ServletRequest request, ServletResponse response) throws ServletException, IOException;

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

    @Override
    public void destroy() {

    }
}

舉例:Servale 類在 service ()方法當中獲取到該 ServletConfig 物件值,並使用

package com.RainbowSea.servlet;


import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class AServlet extends GenericServlet {

    /**
     * 只需要重寫我們常用的 service 父類別中的抽象方法即可
     */
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定 response 在瀏覽器頁面當中顯示的格式型別
        // 注意需要在顯示之前,設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.print("AServlet 處理請求中...." + "<br>");
        ServletConfig config = super.getConfig();
        out.print("AServlet 當中 service 獲取到 Tomcat 伺服器建立的ServletConfig物件: " + config);
        
    }
}

重點: 我們通過上述定義了一個 private ServletConfig config 成員屬性,可以獲取到Tomcat 建立的 ServletConfig 物件了,並在 service() 方法當中使用。但是存在一個問題:如果我們的子類要是重寫了,我們父類別介面卡 GenericServlet 類當中的 init()方法的話。因為多型性的緣故:在實際的執行過程中,呼叫的會是我們子類重寫的的 init() 方法,這樣導致的結果就,我們父類別中的 private ServletConfig config 成員屬性的值無法賦值上為 null 了。因為我們父類別當中的 config 成員屬性是通過父類別當中的 init()方法賦值的,現在我們的子類重寫了父類別的 init()方法。不會呼叫我們父類別的 init()進行賦值操作了,從而導致為了 null 值。

解決方式:將我們父類別當中的 init()方法被 final 修飾,這樣我們的子類就無法重寫 init()方法了。

public abstract class GenericServlet implements Servlet {
    private ServletConfig config ;

    // 這樣我們在我們的子類當中的 service()方法當中也可以使用 Tomcat 伺服器建立的
    // ServletConfig 物件了
    @Override
    public final void init(ServletConfig config) throws ServletException {
        this.config = config;
    }
}

上述的解決方式仍然存在一個問題:就是如果我們的子類一定要重寫 init()方法該怎麼辦,現在你父類別當中的 init()方法被 final 修飾了無法,被 子類重寫。

解決方式:
我們使用 23中設計模式當中的 模板方法設計模式:

因為子類想要重寫我們父類別當中的 init()方法,那麼我們就在父類別當中,再建立一個 無引數的init()方法(方法的過載),這個方法讓子類去重寫,但是我們父類別當中還有一個 帶引數的 init(ServletConfig config) 方法,在該帶有引數的init()方法當中呼叫我們這個提供該子類重寫的 init()方法。具體程式碼如下:

package com.RainbowSea.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;


/**
 * 編寫一個標準通用的Servlet
 * 以後所有的Servlet 類都不要直接實現Servlet 介面了。
 * 以後所有的Servlet 類都繼承 GenericServlet 類。
 * GenericServlet 就是一個介面卡
 */
public abstract class GenericServlet implements Servlet {
    private ServletConfig config ;

    // 這樣我們在我們的子類當中的 service()方法當中也可以使用 Tomcat 伺服器建立的
    // ServletConfig 物件了,final 修飾的方法無法重寫
    @Override
    public final void init(ServletConfig config) throws ServletException {
        // 賦值依舊存在,config 不會為 null
        this.config = config;
        // 呼叫子類重寫後的 init()方法
        this.init();
    }
    
    // 用於提供給子類重寫
    public void init() {
        
    }

    public ServletConfig getConfig() {
        return config;
    }
}

舉例:在AServlet 類當中重寫 init()方法,並且執行該重寫的方法:

package com.RainbowSea.servlet;


import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class AServlet extends GenericServlet {

    /**
     * 只需要重寫我們常用的 service 父類別中的抽象方法即可
     */
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定 response 在瀏覽器頁面當中顯示的格式型別
        // 注意需要在顯示之前,設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.print("AServlet 處理請求中...." + "<br>");
        ServletConfig config = super.getConfig();
        out.print("AServlet 當中 service 獲取到 Tomcat 伺服器建立的ServletConfig物件: " + config);

    }

    // 重寫的父類別當中的 init()無參方法
    @Override
    public void init() {
        System.out.println("AServlet 重寫的 init()方法執行了");
    }
}

2.3 補充:GenericServlet 不用我們自己編寫

注意:GenericServlet 抽象類介面卡是不需要我們自己編寫的,Servlet 已經編寫好了,我們只需要使用就可以了。

上述是為了理解 GenericServlet 原始碼,從而自己編寫的 GenericServlet 介面卡的。如下是 Servlet 為我們編寫好的 GenericServlet 原始碼,和我們上面自己編寫的設計結構是一樣的。

package javax.servlet;

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

public abstract class GenericServlet implements Servlet, ServletConfig,
        java.io.Serializable {

    private static final long serialVersionUID = 1L;

    private transient ServletConfig config;


    public GenericServlet() {
        // NOOP
    }


    @Override
    public void destroy() {
        // NOOP by default
    }

    @Override
    public String getInitParameter(String name) {
        return getServletConfig().getInitParameter(name);
    }

   
    @Override
    public Enumeration<String> getInitParameterNames() {
        return getServletConfig().getInitParameterNames();
    }

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

    
    @Override
    public ServletContext getServletContext() {
        return getServletConfig().getServletContext();
    }

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

    
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }

    
    public void init() throws ServletException {
        // NOOP by default
    }

    
    public void log(String message) {
        getServletContext().log(getServletName() + ": " + message);
    }

    
    public void log(String message, Throwable t) {
        getServletContext().log(getServletName() + ": " + message, t);
    }

    
    @Override
    public abstract void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    
    @Override
    public String getServletName() {
        return config.getServletName();
    }
}

3. ServletConfig

從上面的知識我們知道了 : Tomcat 伺服器先建立了 ServletConfig 物件,然後呼叫init()方法,將ServletConfig物件傳給了init()方法。

先來一波,自問自答:

ServletConfig 是什麼?

package javax.servlet; 顯然 ServletConfig 是一個介面。

誰去實現了這個介面呢 ?

org.apache.catalina.core.StandardWrapperFacade 這個類實現了這個 ServletConfig 介面

StandardWrapperFacade 原始碼如下:

package org.apache.catalina.core;


import java.util.Enumeration;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;


/**
 * Facade for the <b>StandardWrapper</b> object.
 *
 * @author Remy Maucherat
 */
public final class StandardWrapperFacade
    implements ServletConfig {


    // ----------------------------------------------------------- Constructors


    /**
     * Create a new facade around a StandardWrapper.
     * @param config the associated wrapper
     */
    public StandardWrapperFacade(StandardWrapper config) {

        super();
        this.config = config;

    }


    // ----------------------------------------------------- Instance Variables


    /**
     * Wrapped config.
     */
    private final ServletConfig config;


    /**
     * Wrapped context (facade).
     */
    private ServletContext context = null;


    // -------------------------------------------------- ServletConfig Methods


    @Override
    public String getServletName() {
        return config.getServletName();
    }


    @Override
    public ServletContext getServletContext() {
        if (context == null) {
            context = config.getServletContext();
            if (context instanceof ApplicationContext) {
                context = ((ApplicationContext) context).getFacade();
            }
        }
        return context;
    }


    @Override
    public String getInitParameter(String name) {
        return config.getInitParameter(name);
    }


    @Override
    public Enumeration<String> getInitParameterNames() {
        return config.getInitParameterNames();
    }


}

Tomcat 伺服器實現了ServletConfig 介面。

思考: 如果 Tomcat 伺服器換成 了其他的伺服器,比如換成是 Jetty 伺服器,輸出 ServletConfig 物件的時候,還是這個結果嗎?

不一定是一樣的結果了,包含的類名可能和Tomcat 不一樣了,但是他們都實現了 ServletConfig 這個規範。

問題:ServletConfig物件是誰建立的,在什麼時候建立的?

Tomcat 伺服器(WEB伺服器) 建立了ServletConfig 物件,在建立Servlet 物件的時候,同時建立ServletConfig 物件.

問題:ServletConfig 介面到底是幹什麼的? 有什麼用?

Config 是 Configuration單詞的縮寫:n. 佈局,構造;設定

  •  ServletConfig 物件翻譯為: Servlet 物件的設定資訊物件。
    
  •  一個Servlet物件就有一個設定資訊物件:`web.xml <servlet></servlet>`
    
  •  兩個Servlet 物件就有兩個設定資訊物件。
    

ServletConfig物件中建立包裝了 web.xml <servlet></servlet> 標籤設定資訊: Tomcat 小喵咪解析web.xml 檔案,將web.xml檔案中<servlet><init-param></init-param></servlet>標籤中的設定資訊自動包裝到ServletConfig 物件當中

如下:注意:一個Servlet 物件就會有一個 <servlet></servlet> 設定資訊標籤 。如下是 AServlet物件的 xml 設定資訊。

<--以上是 <servlet-name>ConfigTestServlet</servlet-name> <init-param></init-param>
<--是初始化引數,這個初始化引數資訊被小喵咪封裝到 ServletConfig 物件當中 -->
<servlet>
        <servlet-name>AServlet</servlet-name>
        <servlet-class>com.RainbowSea.servlet.AServlet</servlet-class>
        <!-- 這裡是可以設定一個Servlet物件的初始化資訊的 注意:這裡的設定對應的是上述<servlet-name>
        為AServlet-->
        <init-param>
            <param-name>Driver</param-name>
            <param-value>com.mysql.cj.jdbc.Driver</param-value>
        </init-param>
    
        <init-param>
            <param-name>url</param-name>
            <param-value>jdbc:mysql://localhost:3306/dbtest9</param-value>
        </init-param>
    
        <init-param>
            <param-name>user</param-name>
            <param-value>root</param-value>
        </init-param>
</servlet>

3.1 ServletConfig 當中常用的四個方法

public String getServletName();  // 獲取該Servlet物件的名稱
public String getInitParameter(String name); // 通過 <param-name>user</param-name> 標籤當中的name 值獲取到對應  <param-value>root</param-value> 標籤當中的 value 值
public Enumeration<String> getInitParameterNames();  // 一次性獲取到該Servlet物件<init-param>標籤當中設定資訊的 name 值
public ServletContext getServletContext(); // 獲取到ServletContext物件
//  以上的4個方法: 在自己的編寫的Servlet表當中也可以使用this,/super去呼叫(這個Servlet繼承了GenericServet)

3.1.2 通過ServletConfig 物件獲取到 web.xml 組態檔 標籤當中的設定資訊

舉例:獲取到 web.xml 組態檔當中 :<servlet><init-param></init-param></servlet>標籤中的設定資訊。因為該設定資訊是會被動包裝到ServletConfig 物件當中 。這裡我們分別獲取到 AServlet 物件和 BServlet 物件當中的設定資訊。

對應web.xml 資訊如下

<?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>AServlet</servlet-name>
        <servlet-class>com.RainbowSea.servlet.AServlet</servlet-class>
        <!-- 這裡是可以設定一個Servlet物件的初始化資訊的 注意:這裡的設定對應的是上述<servlet-name>
        為AServlet-->
        <init-param>
            <param-name>Driver</param-name>
            <param-value>com.mysql.cj.jdbc.Driver</param-value>
        </init-param>
        <init-param>
            <param-name>url</param-name>
            <param-value>jdbc:mysql://localhost:3306/dbtest9</param-value>
        </init-param>
        <init-param>
            <param-name>user</param-name>
            <param-value>root</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>AServlet</servlet-name>
        <url-pattern>/A</url-pattern>
    </servlet-mapping>


    <servlet>
        <servlet-name>BServlet</servlet-name>
        <servlet-class>com.RainbowSea.servlet.BServlet</servlet-class>
        <init-param>
            <param-name>password</param-name>
            <param-value>root123</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>BServlet</servlet-name>
        <url-pattern>/B</url-pattern>
    </servlet-mapping>
</web-app>

獲取BServlet 物件當中的 web.xml 標籤 的設定資訊

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class BServlet extends GenericServlet {

    /**
     * 只需要重寫我們常用的 service 父類別中的抽象方法即可
     */
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到 GenericServlet 父類別介面卡當中的  ServletConfig  config成員屬性
        ServletConfig servletConfig = super.getServletConfig();
        // 獲取該Servlet物件的名稱
        String ServletName = servletConfig.getServletName();
        writer.print(ServletName + "<br>");  // 輸出 Servlet 的物件的名稱

        String name = "password"; // 這裡如果我們直接知道name 為 pawword 
        
        // 通過 <param-name>user</param-name> 標籤當中的name 值獲取到對應  <param-value>root</param-value> 標籤當中的 value 值
        String value = servletConfig.getInitParameter(name);  
        writer.print(name + "--->" + value);  // 頁面輸出
    }
}



獲取AServlet 物件當中的 web.xml 標籤 的設定資訊

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class AServlet extends GenericServlet {

    /**
     * 只需要重寫我們常用的 service 父類別中的抽象方法即可
     */
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到 GenericServlet 父類別介面卡當中的  ServletConfig  config成員屬性
        ServletConfig servletConfig = super.getServletConfig();
        // 獲取該Servlet物件的名稱
        String ServletName = servletConfig.getServletName();
        writer.print(ServletName + "<br>");  // 輸出 Servlet 的物件的名稱

        // 一次性獲取到該Servlet物件<init-param>標籤當中設定資訊的 name 值
        Enumeration<String> names = servletConfig.getInitParameterNames();

        while(names.hasMoreElements()) {  // 判斷是否還有 元素,有返回 true,沒有返回false。和集合當中的迭代器類似
            String name = names.nextElement(); // 取出name 值

            // 通過 <param-name>user</param-name> 標籤當中的name 值獲取到對應  <param-value>root</param-value> 標籤當中的 value 值
            String value = servletConfig.getInitParameter(name);
            writer.print(name + "--->" + value);  // 頁面輸出
            writer.print("<br>");  // 頁面換行

        }

    }

}

3.1.3 獲取到 ServletContext物件

方式一: 通過先獲取到 ServletConfig 物件,再通過 ServletConfig 物件獲取到 ServletContext 物件

使用 ServletConfig 當中的 public ServletContext getServletContext(); // 獲取到ServletContext物件

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class BServlet extends GenericServlet {

    /**
     * 只需要重寫我們常用的 service 父類別中的抽象方法即可
     */
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 獲取到 GenericServlet父類別介面卡當中的 private transient ServletConfig config; 成員屬性
        ServletConfig servletConfig = super.getServletConfig();
        // 獲取到 servletContext 物件
        ServletContext servletContext = servletConfig.getServletContext();
        System.out.println("servletContext的值: " + servletContext);

    }
}



方式二: 直接通過 父類別 GenericServlet 介面卡當中的。this,super.getServletContext() 方法 也可以獲取到ServletContext物件

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class BServlet extends GenericServlet {

    /**
     * 只需要重寫我們常用的 service 父類別中的抽象方法即可
     */
    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();
        System.out.println("servletContext的值: " + servletContext);

    }
}



4. ServletContext

  • ServletContext 是介面,是Servlet規範中的一員。Tomcat伺服器(WEB伺服器) 實現了ServletContext 介面。
  • ServletContext 是一個介面,Tomcat 伺服器對 ServletContext 介面進行了實現。ServletContext 物件的建立也是 Tomcat 伺服器來完成的。啟動webapp的時候建立的。
  • ServletContext 物件在伺服器啟動階段建立的,在伺服器關閉的時候銷燬。這就是 ServletContext 物件的生命週期。
  • ServletContext 物件的環境物件,(Servlet物件的上下文物件)。

重點:

一個ServletContext 表示的就是一個 webapp 當中的 web.xml 檔案當中下設定資訊。而一個 webapp 一般只有一個 web.xml 檔案。所以只要在同一個 webapp 當中,只要在同一個應用當中,所以的Servlet 物件都是共用同一個 ServletContext物件的 。舉例:Tomcat 伺服器中有一個 webapps ,這個 webapps 下存放了多個 webapp 。假設有 100 個 webapp ,那麼就會有 100 個 ServeltContext 物件,因為一個 webapp 對應一個 web.xml 檔案。總之,一個應用,一個 webapp 只有一個 web.xml檔案,則只有一個 ServletContext 物件,所有該應用下/webapp下的 Servlet 物件共用。

如下:我們一個 webapp 下有兩個 Servlet 物件,分別為 BServlet 和 AServlet 物件,但是他們兩個的同時在同一個 webapp下的,只有一個 web.xml 檔案。所以這個兩個 Servlet 物件共用 一個 ServletContext 物件:

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class BServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();
        writer.print("BServlet 下的 servletContext的值: " + servletContext);

    }
}



package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class AServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();
        writer.print("AServlet 下的 servletContext的值: " + servletContext);

    }

}

ServletContext對應顯示生活中的什麼例子呢?

一個教室裡有多個學生,那麼每一個學生就是一個Servlet,這些學生都在同一個教室當中,那麼我們可以把這個教室叫做ServletContext物件。那麼也就是說放在這個ServletContext物件(環境)當中的資料,在同一個教室當中,物品都是共用的。比如:教室中有一個空調,所有的學生都可以操作。可見,空調是共用的。因為空調放在教室當中。教室就是ServletContext物件。

4.1 ServletContext 獲取web.xml 組態檔當中的 <context-param>標籤當中的設定資訊

我們想要獲取到web.xml 組態檔大當中的<context-param>標籤當中編寫的的設定資訊,需要使用到如下兩個方法:

public String getInitParameter(String name); // 通過初始化引數的name獲取value
public Enumeration<String> getInitParameterNames(); // 獲取所有的初始化引數的name
<!--以上兩個方法是ServletContext物件的方法,這個方法獲取的是什麼資訊?是以下的設定資訊-->
<context-param>
    <param-name>pageSize</param-name>
    <param-value>10</param-value>
</context-param>
<context-param>
    <param-name>startIndex</param-name>
    <param-value>0</param-value>
</context-param>
<!--注意:以上的設定資訊屬於應用級的設定資訊,一般一個專案中共用的設定資訊會放到以上的標籤當中。-->
<!--如果你的設定資訊只是想給某一個servlet作為參考,那麼你設定到servlet標籤當中即可,使用ServletConfig物件來獲取。-->

舉例:

如下是 Servle 物件通過,ServletContext物件的上述兩個方法,獲取到 標籤當中編寫的設定資訊:

<?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">

    <!--如下是: context-param 的編寫的設定資訊,該設定資訊,被所有的Servlet共用
可以使用ServletContext物件獲取到 -->
    <context-param>
        <param-name>pageSize</param-name>
        <param-value>10</param-value>
    </context-param>

    <context-param>
        <param-name>startIndex</param-name>
        <param-value>0</param-value>
    </context-param>

    <servlet>
        <servlet-name>AServlet</servlet-name>
        <servlet-class>com.RainbowSea.servlet.AServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AServlet</servlet-name>
        <url-pattern>/A</url-pattern>
    </servlet-mapping>
    
     <servlet>
        <servlet-name>BServlet</servlet-name>
        <servlet-class>com.RainbowSea.servlet.BServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>BServlet</servlet-name>
        <url-pattern>/B</url-pattern>
    </servlet-mapping>
</web-app>
package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class AServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();
        writer.print("AServlet 下的 servletContext的值: " + servletContext + "<br>");

        //  一次性獲取到 <context-param> </context-param>標籤當中所有的 name 值
        Enumeration<String> names = servletContext.getInitParameterNames();

        while(names.hasMoreElements()) { // 判斷該集合是否還有元素,有返回true,沒有返回false
            // 獲取到元素當中的 name值
            String name = names.nextElement();
            // 通過物件 <context-param> </context-param>標籤下的name獲取到對應的value值
            String value = servletContext.getInitParameter(name);
            writer.print(name + "--->" + value);
            writer.print("<br>");
        }

    }

}

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class BServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();
        writer.print("BServlet 下的 servletContext的值: " + servletContext + "<br>");

        //  一次性獲取到 <context-param> </context-param>標籤當中所有的 name 值
        Enumeration<String> names = servletContext.getInitParameterNames();

        while(names.hasMoreElements()) { // 判斷該集合是否還有元素,有返回true,沒有返回false
            // 獲取到元素當中的 name值
            String name = names.nextElement();
            // 通過物件 <context-param> </context-param>標籤下的name獲取到對應的value值
            String value = servletContext.getInitParameter(name);
            writer.print(name + "--->" + value);
            writer.print("<br>");
        }

    }
}



4.2 ServletContext 獲取檔案的絕對路徑

獲取應用的根路徑(非常重要),因為在java原始碼當中有一些地方可能會需要應用的根路徑,這個方法可以動態獲取應用的根路徑。 在java原始碼當中,不要將應用的根路徑寫死,因為你永遠都不知道這個應用在最終部署的時候,會給你的另外取起一個什麼名字。所以我們需要動態獲取。

public String getContextPath(); // 獲取到該專案在 url 上輸入的專案名
public String getRealPath(String path);// 獲取檔案的絕對路徑(真實路徑)
// 獲取到這個 index.html 檔案的絕對路徑
// "/" 表示的是 web 目錄,該方法預設是從web目錄下開始找的。
// 就算你不寫也是,預設從 web目錄下開始找的。
String realPath = servletContext.getRealPath("/index.html");

舉例:

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class AServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();
        // 獲取到這個專案名。
        String contextPath = servletContext.getContextPath();
        writer.print(contextPath + "<br>");
        // 獲取到這個 index.html 檔案的絕對路徑
        // "/" 表示的是 web 目錄,該方法預設是從web目錄下開始找的。
        // 就算你不寫也是,預設從 web目錄下開始找的。
        String realPath = servletContext.getRealPath("/index.html");
        writer.print(realPath);


    }

}

注意:如果想要獲取到web目錄下面的子目錄下的,檔案的絕對路徑的話,需要寫明該檔案是在 web目錄下的哪個子目錄。不然無法找到的。因為 getRealPath() 方法是從 web目錄下開始尋找的。

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class AServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();
        // 獲取到這個專案名。
        String contextPath = servletContext.getContextPath();
        writer.print(contextPath + "<br>");
        // 獲取到這個 index.html 檔案的絕對路徑
        // "/" 表示的是 web 目錄,該方法預設是從web目錄下開始找的。
        // 就算你不寫也是,預設從 web目錄下開始找的。
        // 如果是web目錄下的子目錄的檔案,需要寫明對應的子目錄
        String realPath = servletContext.getRealPath("/test/index.html");
        writer.print(realPath);


    }

}

4.3 ServletContext物件"應用域" 存放資料量小,不易修改的資料資訊

ServletContext物件還有另一個名字:應用域(後面還有其他域,例如:請求域、對談域)
如果所有的使用者共用一份資料,並且這個資料很少的被修改,並且這個資料量很少,可以將這些資料放到ServletContext這個應用域中儲存起來。儲存起來後,可以被其他的 Servlet 物件獲取到。

為什麼是所有使用者共用的資料? 不是共用的沒有意義。

因為ServletContext這個物件只有一個。只有共用的資料放進去才有意義。

為什麼資料量要小?

因為資料量比較大的話,太佔用堆記憶體,並且這個物件的生命週期比較長,伺服器關閉的時候,這個物件才會被銷燬。巨量資料量會影響伺服器的效能。佔用記憶體較小的資料量可以考慮放進去。

為什麼這些共用資料很少的修改,或者說幾乎不修改?

所有使用者共用的資料,如果涉及到修改操作,必然會存線上程並行所帶來的安全問題。所以放在ServletContext物件中的資料一般都是唯讀的。

資料量小、所有使用者共用、又不修改,這樣的資料放到ServletContext這個應用域當中,會大大提升效率。因為應用域相當於一個快取,放到快取中的資料,下次在用的時候,不需要從資料庫中再次獲取,大大提升執行效率。

對於ServletContext物件應用域,的存,取,刪的方法如下:

// 存(怎麼向ServletContext應用域中存資料)
public void setAttribute(String name, Object value); // map.put(k, v)
// 取(怎麼從ServletContext應用域中取資料)
public Object getAttribute(String name); // Object v = map.get(k)
// 刪(怎麼刪除ServletContext應用域中的資料)
public void removeAttribute(String name); // map.remove(k)

舉例:如下我們定義了一個 User 類,將該 User類的範例化物件儲存到 ServletContext 物件當中去,並在不同的Servlet 物件當中取出來。

package com.RainbowSea.servlet;

public class User {
    private String name;
    private String password;
    
    public User() {
        
    }
    
    public User(String name,String password) {
        this.name = name;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

AServlet 物件,存,取

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;


public class AServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();

        // 建立一個 User 範例物件
        User user = new User("Tom","123456");

        // 存
        servletContext.setAttribute("userObj",user); // map<K,V>
        // 取 注意引數是我們 ,setAttribute設定的 K 值
        //Object userObj = servletContext.getAttribute("userObj");
        // 因為我們這裡知道我們儲存的是什麼型別的資料所以可以直接強制型別轉換
        User userObj = (User) servletContext.getAttribute("userObj");

        writer.print(userObj); // 瀏覽器頁面輸出

    }

}

BServlet 物件的取

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class BServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // 設定在瀏覽器頁面顯示的格式型別,必須在輸出前設定
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();

        // 獲取到GenericServlet父類別介面卡當中的 servletContext 物件
        ServletContext servletContext = super.getServletContext();
        // 取 注意引數是我們 ,setAttribute設定的 K 值
        //Object userObj = servletContext.getAttribute("userObj");
        // 因為我們這裡知道我們儲存的是什麼型別的資料所以可以直接強制型別轉換
        User userObj = (User) servletContext.getAttribute("userObj");

        writer.print(userObj); // 瀏覽器頁面輸出
    }
}



兩者瀏覽器顯示的結果:注意要先存取 AServlet (先將資料儲存到 ServletContext物件當去),不然BServlet 存取的話就是 null了

4.4 ServletContext 紀錄檔資訊

我們可以通過 ServletContext 生成紀錄檔資訊:

// 通過ServletContext物件也是可以記錄紀錄檔的
public void log(String message);
public void log(String message, Throwable t);
// 這些紀錄檔資訊記錄到哪裡了?
// localhost.2021-11-05.log
// Tomcat伺服器的logs目錄下都有哪些紀錄檔檔案?
//catalina.2021-11-05.log 伺服器端的java程式執行的控制檯資訊。
//localhost.2021-11-05.log ServletContext物件的log方法記錄的紀錄檔資訊儲存到這個檔案中。
//localhost_access_log.2021-11-05.txt 存取紀錄檔

舉例:

package com.RainbowSea.servlet;


import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class BServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        ServletContext servletContext = super.getServletContext();

        servletContext.log("你好世界");  // 新增紀錄檔資訊

        // 達到某個條件,計入紀錄檔資訊:異常
        int age = 17;
        if( age < 17) {
            servletContext.log("對不起,您未成年,請繞行",new RuntimeException("小屁孩,快走開,不適合你"));
        }
    }
}



5. 總結:

  1. Servlet 物件的生命週期:
    1. 無參構造器:只會執行呼叫一次。
    2. init() 方法:init 被翻譯為初始化,init ()方法只會被執行一次;基本上和 Servlet構造器的呼叫同時執行;在Servlet 物件第一次被建立只會執行。init 方法通常是完成初始化操作的。
    3. service() 方法:service() 方法是處理使用者請求的核心方法。只要使用者傳送一次請求,service 方法必然會執行一次;傳送100次請求,service方法執行100次。
    4. destroy() 方法:destroy 方法中可以編寫銷燬前的準備;比如:伺服器關閉的時候,AServelt 物件開啟了一些資源,這些資源可能是流,也可能是資料庫連線;那麼關閉伺服器的時候,要關閉這些流,關閉這些資料庫連線,那麼這些關閉資源的程式碼舊可以寫到destroy()。
  2. GenericServlet 抽象類介面卡思想以及模板方法設計思想
  3. ServletConfig物件中建立包裝了 web.xml <servlet></servlet> 標籤設定資訊: Tomcat 小喵咪解析web.xml 檔案,將web.xml檔案中<servlet><init-param></init-param></servlet>標籤中的設定資訊自動包裝到ServletConfig 物件當中 。這個是 :一個 Servlet 物件就一 個 ServletConfig 物件,有 100 個 Servlet 物件就有 100 個 ServletConfig 物件。
public String getServletName();  // 獲取該Servlet物件的名稱
public String getInitParameter(String name); // 通過 <param-name>user</param-name> 標籤當中的name 值獲取到對應  <param-value>root</param-value> 標籤當中的 value 值
public Enumeration<String> getInitParameterNames();  // 一次性獲取到該Servlet物件<init-param>標籤當中設定資訊的 name 值
public ServletContext getServletContext(); // 獲取到ServletContext物件
//  以上的4個方法: 在自己的編寫的Servlet表當中也可以使用this,/super去呼叫(這個Servlet繼承了GenericServet)
  1. ServletContext 獲取web.xml 組態檔當中的 <context-param>標籤當中的設定資訊。一個 webapp 就有一個ServleContext 物件,因為一個 webapp 就只有一個 web.xml 組態檔,所有的 Servlet 物件共用 一個 ServletContest 物件。有 100 個 webapp 就有 100 個 web.xml組態檔,就有 100 個 ServletContext 物件。簡單的來說 一個 ServletContext 物件就對應著一個 web.xml 組態檔。
  2. ServletContext 獲取檔案的絕對路徑。
public String getContextPath(); // 獲取到該專案在 url 上輸入的專案名
public String getRealPath(String path);// 獲取檔案的絕對路徑(真實路徑)
// 獲取到這個 index.html 檔案的絕對路徑
// "/" 表示的是 web 目錄,該方法預設是從web目錄下開始找的。
// 就算你不寫也是,預設從 web目錄下開始找的。
String realPath = servletContext.getRealPath("/index.html");
  1. ServletContext物件還有另一個名字:應用域(後面還有其他域,例如:請求域、對談域)如果所有的使用者共用一份資料,並且這個資料很少的被修改,並且這個資料量很少,可以將這些資料放到ServletContext這個應用域中儲存起來。儲存起來後,可以被其他的 Servlet 物件獲取到。
// 存(怎麼向ServletContext應用域中存資料)
public void setAttribute(String name, Object value); // map.put(k, v)
// 取(怎麼從ServletContext應用域中取資料)
public Object getAttribute(String name); // Object v = map.get(k)
// 刪(怎麼刪除ServletContext應用域中的資料)
public void removeAttribute(String name); // map.remove(k)
  1. Servlet 介面的結構圖

jakarta.servlet.Servlet(介面)【爺爺】
jakarta.servlet.GenericServlet implements Servlet(抽象類)【兒子】
jakarta.servlet.http.HttpServlet extends GenericServlet(抽象類)【孫子】
我們以後編寫的Servlet要繼承HttpServlet類。

6. 最後:

限於自身水平,其中存在的錯誤,希望大家給予指教,韓信點兵——多多益善,謝謝大家,江湖再見,後會有期!!!