屬性和監聽

小莫神和他的的發表於2020-11-09

屬性和監聽

本篇對應《head first servlet and jsp》的第五章。該章介紹了servletConfigservletContext在不同場景下的應用,並探討了執行緒安全問題和小小的解決方法。

servletConfig

servletConfigservletContext解決了初始化引數的硬編碼問題。比如我們想輸出一句話,硬編碼中我們是這樣寫的

PrintWriter out = response.getWriter();
out.printlin("I love servlet!!!");

眾所周知,硬編碼是一個非常不優雅的寫法???比如過了一段時間,我們學習了spring之後再也不用像現在這樣麻煩地寫servlet了。我想改成I love spring呢?(●’◡’●)

這個時候我們想到使用servletConfig ,通過xml部署檔案來解決硬編碼的問題

<servlet>
    <servlet-name>servletStudy</servlet-name>
    <servlet-class>com.highway.servlet.user.servletStudy</servlet-class>
    <!--我們通過init-param標籤設定初始化引數 -->
    <init-param>
      <param-name>speak</param-name>
      <param-value>I love servlet</param-value>
    </init-param>
  </servlet>

然後我們的Java程式碼就可以寫成這樣了

out.printlin(getServletConfig().getInitParameter("speak"));

這樣是不是顯得非常優雅了呢( •̀ ω •́ )y

需要注意的是init-param標籤寫在一個servlet標籤裡,從這一點說明了servletConfig只對一個servlet有效。servletConfig也被稱為servlet初始化引數servlet初始化引數只會在servlet被容器初始化時有且僅有一次被讀取,當這個servlet例項化後永遠也不可能再回頭讀取servlet初始化引數了(除非你重新部署該專案)

我們接下來看看容器時怎麼讀取servlet初始化引數的吧

servletConfig的誕生

  • 首先容器會讀取該servlet的部署描述檔案,包括servlet初始化引數 。換句話說就是這個servlet標籤裡的所有內容
  • 容器為這個servlet建立一個ServletConfig例項
  • 容器為每個init-param建立一個String鍵值對根據該例,Key是speak,Value是I love servlet
  • 容器向ServletConfig提供init-param鍵值對的引用 ,注意是引用哦,因為是String嘛?
  • 容器建立servlet 。這個時候servlet才被建立哦???
  • 容器呼叫servletinit()方法 ,傳入一個ServletConfig的引用
public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
}

public void init() throws ServletException {}

該方法繼承自GenericServlet

servletConfig的生命之旅到此結束了(●’◡’●)

下面將引出servletContext

ServletContext

當另外一個servlet也想大喊一聲“I love servlet”時,我們就要跑到這個servlet的標籤裡再寫一遍init-param ,這樣一來程式碼複用又變得很差。那這個時候我們該怎麼辦呢?

我們需要一些全域性性的東西,這個東西就是上下文初始化引數用英文的講法就是ServletContext

ServletContext同樣通過xml部署檔案進行部署

<context-param>
    <param-name>speak</param-name>
    <param-value>I love servlet</param-value>
</context-param>

可以看到context-param標籤並不需要寫在一個servlet標籤裡,只需要寫在web-app標籤裡就行了。

所以其實跟servletConfig來看其實區別只是在於一個是全域性一個是區域性而已。可以看到Java程式碼都非常的相似。

//getServletConfig()
out.printlin(getServletConfig().getInitParameter("speak"));
//getServletContext()
out.printlin(getServletContext().getInitParameter("speak"));

而這個區別其實從方法名上來看非常好理解的?

❗❗❗每個servlet有一個ServletConfig,每個Web應用有一個ServlentContext

ServletContext的誕生

  • 容器讀部署檔案,為每個context-param標籤建立一個String鍵值對
  • 容器建立一個ServletContext的例項
  • 容器為ServletContext提供String鍵值對的引用
  • Web應用中部署的各個servletJSP都能訪問該ServletContext

❗❗❗如果這個Web應用是分佈的,那麼每個JVM有一個屬於自己的ServletContext並且 99.9 % 99.9\% 99.9%的情況下,ServletContext的內容是相同的

接著一個問題隨之而來,很顯然的可以看到,這些初始化引數都是String鍵值對 ,可想而知String能做的事情非常的有限???如果我想要初始化的是一個Object呢?

不要著急,我們擁有這樣的技術( *^-^)ρ(^0^* )

監聽器

監聽器Listener可以在特定事件發生時出現並幫助我們完成一些事情,有各種各樣的Listener在這裡我們只講解ServletContextListener ,看名知意這個Listener就是專門監聽?ServletContext的,也就是說在ServletContext發生了什麼的時候,這個Listener會完成一些事情。至於是什麼時候呢?下面將會揭曉。

你可能想馬上知道ServletContextListener到底幹了什麼,居然可以讓一個String初始化引數StringObject ,沒錯,我看到這裡也充滿了好奇❓❓❓

ServletContextListener

ServletContextListener負責監聽ServletContext ,同時ServletContextListener是一個介面,實現這個介面需要覆蓋2個方法

package javax.servlet;

import java.util.EventListener;

public interface ServletContextListener extends EventListener {
    //當context初始化時呼叫該函式
    void contextInitialized(ServletContextEvent var1);
	//當context銷燬時呼叫該函式
    void contextDestroyed(ServletContextEvent var1);
}

只要覆蓋了這2個方法就能在ServletContext 初始化時和銷燬時做一些事情了。

我們用一個例子看看Listener怎麼化StringObject

當然在此之前,需要說一說我們需要準備什麼?

假設我們需要初始化一個名為highway的學生,那麼我們需要準備什麼呢❓

  1. 當然是不能缺少的ServletContextListener ,在本例中該Listener名為StudentListener
  2. 需要準備一個Student實體類 ,在本例中該實體類就叫Student
  3. 可能會遲到但永遠不會缺席的servlet ??? ,在本例中該servlet名為servletStudy

在此之前,先看看xml部署檔案然後再來看看具體的實現

<web-app>
<!--  上下文初始化引數-->
  <context-param>
    <param-name>name</param-name>
    <param-value>highway</param-value>
  </context-param>
<!--  註冊監聽器-->
  <listener>
    <listener-class>com.highway.listener.StudentListener</listener-class>
  </listener>  
 <!--配置servlet-->
  <servlet>
    <servlet-name>servletStudy</servlet-name>
    <servlet-class>com.highway.servlet.user.servletStudy</servlet-class>
  </servlet>
  <!--配置servlet-mapping-->
  <servlet-mapping>
    <servlet-name>servletStudy</servlet-name>
    <url-pattern>/study.do</url-pattern>
  </servlet-mapping>  
</web-app>

看完配置檔案之後大概瞭解一下架子了吧,那麼我們按照順序進行準備吧,首先我們準備一個ServletContextListener

package com.highway.listener;

import com.highway.pojo.Student;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class StudentListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //獲取ServletContext
        ServletContext servletContext = servletContextEvent.getServletContext();
        //從ServletContext中獲得param-name==name的值
        String name = servletContext.getInitParameter("name");
        //這是化String為Object的關鍵
        Student student = new Student(name);
        //將student設定為servletContext的Attribute
        servletContext.setAttribute("student",student);
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

可以看到通過這樣的方式new出來了一個Student ,所以其實並沒有你想的那麼神奇哦???

Student

package com.highway.pojo;

import java.io.Serializable;

public class Student implements Serializable {
    private String name;

    public Student() {
    }

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

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

可以看到,在StudentListener中呼叫的有參構造,我們例項出了一個名為highway的學生,並且通過setAttribute()方法把這個例項設定為了ServletContext的屬性。屬性AttributeString初始化引數init-param的區別讓我們能夠完成化StringObject的魔法?‍。

那麼他們的區別在哪呢?會在下面介紹Attribute的時候再說,現在我們不需要關注他,只要知道這樣就能把這個Object設定到ServletContext上,然後讓所有的servlet訪問到這個Object

現在將目光移回到我們最後一樣東西servlet

package com.highway.servlet.user;

import com.highway.pojo.Student;

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

public class servletStudy extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //從Context拿到這個student,注意的是一定要強制型別轉換!強制型別轉換!強制型別轉換!
        Student student = (Student)getServletContext().getAttribute("student");
        //又是一個setAttribute,不用著急,現在來說還不重要
        req.setAttribute("name",student.getName());
        //轉發至student.jsp
        req.getRequestDispatcher("student.jsp").forward(req, resp);
    }

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

來看看student.jsp幹了啥吧。其實就是把這個學生的名字列印出來(●ˇ∀ˇ●)(●ˇ∀ˇ●)(●ˇ∀ˇ●)

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>主頁</title>
</head>
<body>
<h2>Hello World!</h2><br/>
<div class="name">${name}</div><br/>
</body>
</html>

或許單看程式碼,你還有點糊塗,那麼下面用文字來描述一遍吧。當然我肯定會建議你邊看文字描述邊翻程式碼對應。

  1. 容器讀部署檔案,註冊Listener ,為每個context-param標籤建立一個String鍵值對
  2. 容器建立一個ServletContext的例項
  3. 容器為ServletContext提供String鍵值對的引用,在本例中Key是name,Value是highway。並將這些鍵值對的引用交給ServletContext
  4. 容器建立一個實現ServletContextListener介面的StudentListener
  5. 容器呼叫ListenercontextInitialized()方法 ,傳入新的ServletContextEvent 。這個事件物件有一個ServletContext引用,所以事件處理程式碼可以從事件中得到上下文 ,並從上下文得到上下文初始化引數ListenerservletContextEventServletContext的引用
ServletContext servletContext = servletContextEvent.getServletContext();
  1. 然後從ServletContext的引用中獲得上下文初始化引數name
String name = servletContext.getInitParameter("name");
  1. Listener使用這個String來構造一個Student物件
Student student = new Student(name);
  1. Listener將這個Student物件設定為ServletContext的一個Attribute
servletContext.setAttribute("student",student);
  1. 到此為止,這個ServletContext就完成了(>人<;)那麼剩下的事情我想你已經非常熟悉了,容器建立一個新的servlet ,當然在容器呼叫init()方法時已經建立了一個ServletConfig ,並且這個ServletConfig 裡有個ServletContext的引用
  2. servletStudy得到一個請求,向ServletContext請求屬性"student"
Student student = (Student)getServletContext().getAttribute("student");

一定要強制型別轉換(>人<;)! 一定要強制型別轉換(>人<;)! 一定要強制型別轉換(>人<;)!

  1. 獲得studentname ,向HttpServletRequest設定屬性,然後getRequestDispatcher轉發
req.setAttribute("name",student.getName());
req.getRequestDispatcher("student.jsp").forward(req, resp);

到此為止結束(●ˇ∀ˇ●)我相信你應該明白了???如果不明白,那就重複看幾遍吧(≧﹏ ≦)

Attribute是什麼?

在上面的例子中,你可能早就已經迫不及待的想知道Attribute是什麼了吧?

說起來其實非常簡單???

Attribute是一個物件,Attribute可以設定到3個servlet API物件中,分別是ServletContextHttpServletRequestHttpSession前2種我們在上面的程式碼已經使用過了,還剩下最後一種,那麼什麼是session呢?不要著急,我們**先把Attribute是什麼?**的事情說完?

你可以簡單的理解為是一個對映例項物件種的鍵值對 ,不同的是Key為一個StringValue則是一個Object 。在實際中,我們並不❌知道也不❌關心具體實現,我們只關心?屬性所在的作用域。

屬性引數
型別應用/上下文
請求
會話
應用/上下文初始化引數
請求引數
servlet初始化引數
設定方法setAttribute(String name, Object value)不能設定應用和servlet初始化引數
對於請求引數可以,但這是另外一回事了
返回型別ObjectString
獲取方法getAttribute(String name)getInitParameter(String name)

執行緒安全問題

上下文屬性是執行緒安全的嗎?

執行緒安全問題應該說跟Attribute本身沒有什麼關係,那麼問題出在哪呢?你一定會覺得詫異?

執行緒安全跟這個Attribute在哪個作用域有關(應用/上下文、請求、會話)

經過這麼多的學習,相信你一定知道,該Web應用中的所有servlet都能訪問這個共有的ServletContext ,那麼執行緒A設定了一個Attribute ,這當然沒有出現“所謂的”執行緒安全問題,但是如果這個時候我們加入一個執行緒B呢?(當然,這裡我預設你已經學習多執行緒執行緒同步的相關知識)

很顯然這個時候執行緒不安全的問題就有可能出現了。

這時你肯定在想,那麼我們應該怎麼做呢????

我們可以在服務方法上加一個synchronized

public class servletStudy extends HttpServlet {
    @Override
    protected synchronized void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Student student = (Student)getServletContext().getAttribute("student");
        req.setAttribute("name",student.getName());
        req.getRequestDispatcher("student.jsp").forward(req, resp);
    }

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

這個方法解決了執行緒不安全的問題嗎??停下來想一想。

答案是沒有。

服務方法上加一個synchronized只是解決了一個servlet一次只會處理一個服務方法 ,但是其他的servlet還是可以訪問到這個Attribute的哦(#°Д°)

到這裡你應該明白synchronized應該加給誰了吧?很顯然是要對這個Attributesynchronized?

那麼怎麼加synchronized也是需要思考?的一個問題。

只有當處理這些上下文屬性的所有其他程式碼也對ServletContext同步時才能奏效,如果有一段程式碼沒有請求鎖?的話,那麼這個程式碼就能自由地訪問上下文屬性 ,這也表示我們的設計功虧一簣???

通過這個例子來說明如何加鎖?

public class StudentListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ServletContext servletContext = servletContextEvent.getServletContext();
        String name = servletContext.getInitParameter("name");
        Student student = new Student(name);
        synchronized(servletContext){
            servletContext.setAttribute("student",student);
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

會話屬性是執行緒安全的嗎?

遺憾的是,會話屬性任然不是執行緒安全的。ヾ(≧へ≦)〃

會話是什麼呢?具體的會在會話章節進行介紹,這裡就不花筆墨說了

還有啥是執行緒安全的呢

只有請求屬性和區域性變數是執行緒安全的!

為了保證執行緒安全,我們常常採用這2種辦法

  • 把變數宣告為服務方法中的區域性變數,而不是一個例項變數
  • 在最合適的作用域使用Attribute

相關文章