屬性和監聽
屬性和監聽
本篇對應《head first servlet and jsp》的第五章。該章介紹了servletConfig和servletContext在不同場景下的應用,並探討了執行緒安全問題和小小的解決方法。
servletConfig
servletConfig和servletContext解決了初始化引數的硬編碼問題。比如我們想輸出一句話,硬編碼中我們是這樣寫的
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才被建立哦???
- 容器呼叫servlet的init()方法 ,傳入一個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應用中部署的各個servlet和JSP都能訪問該ServletContext
❗❗❗如果這個Web應用是分佈的,那麼每個JVM有一個屬於自己的ServletContext並且 99.9 % 99.9\% 99.9%的情況下,ServletContext的內容是相同的
接著一個問題隨之而來,很顯然的可以看到,這些初始化引數都是String鍵值對 ,可想而知String能做的事情非常的有限???如果我想要初始化的是一個Object呢?
不要著急,我們擁有這樣的技術( *^-^)ρ(^0^* )
監聽器
監聽器Listener可以在特定事件發生時出現並幫助我們完成一些事情,有各種各樣的Listener在這裡我們只講解ServletContextListener ,看名知意這個Listener就是專門監聽?ServletContext的,也就是說在ServletContext發生了什麼的時候,這個Listener會完成一些事情。至於是什麼時候呢?下面將會揭曉。
你可能想馬上知道ServletContextListener到底幹了什麼,居然可以讓一個String初始化引數化String為Object ,沒錯,我看到這裡也充滿了好奇❓❓❓
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怎麼化String為Object的
當然在此之前,需要說一說我們需要準備什麼?
假設我們需要初始化一個名為highway的學生,那麼我們需要準備什麼呢❓
- 當然是不能缺少的ServletContextListener ,在本例中該Listener名為StudentListener
- 需要準備一個Student實體類 ,在本例中該實體類就叫Student
- 可能會遲到但永遠不會缺席的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的屬性。屬性Attribute和String初始化引數init-param的區別讓我們能夠完成化String為Object的魔法?。
那麼他們的區別在哪呢?會在下面介紹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>
或許單看程式碼,你還有點糊塗,那麼下面用文字來描述一遍吧。當然我肯定會建議你邊看文字描述邊翻程式碼對應。
- 容器讀部署檔案,註冊Listener ,為每個context-param標籤建立一個String鍵值對
- 容器建立一個ServletContext的例項
- 容器為ServletContext提供String鍵值對的引用,在本例中Key是name,Value是highway。並將這些鍵值對的引用交給ServletContext
- 容器建立一個實現ServletContextListener介面的StudentListener類
- 容器呼叫Listener的contextInitialized()方法 ,傳入新的ServletContextEvent 。這個事件物件有一個ServletContext引用,所以事件處理程式碼可以從事件中得到上下文 ,並從上下文得到上下文初始化引數 。Listener向servletContextEvent要ServletContext的引用
ServletContext servletContext = servletContextEvent.getServletContext();
- 然後從ServletContext的引用中獲得上下文初始化引數name
String name = servletContext.getInitParameter("name");
- Listener使用這個String來構造一個Student物件
Student student = new Student(name);
- Listener將這個Student物件設定為ServletContext的一個Attribute
servletContext.setAttribute("student",student);
- 到此為止,這個ServletContext就完成了(>人<;)那麼剩下的事情我想你已經非常熟悉了,容器建立一個新的servlet ,當然在容器呼叫init()方法時已經建立了一個ServletConfig ,並且這個ServletConfig 裡有個ServletContext的引用
- servletStudy得到一個請求,向ServletContext請求屬性"student"
Student student = (Student)getServletContext().getAttribute("student");
一定要強制型別轉換(>人<;)! 一定要強制型別轉換(>人<;)! 一定要強制型別轉換(>人<;)!
- 獲得student的name ,向HttpServletRequest設定屬性,然後getRequestDispatcher轉發
req.setAttribute("name",student.getName());
req.getRequestDispatcher("student.jsp").forward(req, resp);
到此為止結束(●ˇ∀ˇ●)我相信你應該明白了???如果不明白,那就重複看幾遍吧(≧﹏ ≦)
Attribute是什麼?
在上面的例子中,你可能早就已經迫不及待的想知道Attribute是什麼了吧?
說起來其實非常簡單???
Attribute是一個物件,Attribute可以設定到3個servlet API物件中,分別是ServletContext 、HttpServletRequest 、HttpSession前2種我們在上面的程式碼已經使用過了,還剩下最後一種,那麼什麼是session呢?不要著急,我們**先把Attribute是什麼?**的事情說完?
你可以簡單的理解為是一個對映例項物件種的鍵值對 ,不同的是Key為一個String ,Value則是一個Object 。在實際中,我們並不❌知道也不❌關心具體實現,我們只關心?屬性所在的作用域。
屬性 | 引數 | |
---|---|---|
型別 | 應用/上下文 請求 會話 | 應用/上下文初始化引數 請求引數 servlet初始化引數 |
設定方法 | setAttribute(String name, Object value) | 不能設定應用和servlet初始化引數 對於請求引數可以,但這是另外一回事了 |
返回型別 | Object | String |
獲取方法 | 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應該加給誰了吧?很顯然是要對這個Attribute加synchronized?
那麼怎麼加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
相關文章
- 監聽 watch props物件屬性監聽 或深度監聽物件
- JavaScript監聽屬性改變JavaScript
- Zookeeper(2)---節點屬性、監聽和許可權
- iOS筆記--UIButton常用屬性和監聽方法iOS筆記UI
- 深入理解 Getter和Setter 對物件的屬性監聽物件
- vue監聽input是否為空(監聽值為物件某個屬性)Vue物件
- Vue:watch 監聽多個屬性值的方法Vue
- vue計算屬性 監聽 方法的區別Vue
- 論如何監聽物件某個屬性的變化物件
- Vue計算屬性和偵聽器Vue
- vue2.x版本中Object.defineProperty物件屬性監聽和關聯VueObject物件
- vue原始碼學習:Object.defineProperty 物件屬性監聽Vue原始碼Object物件
- javascript擼來擼去(1)-事件監聽與物件屬性JavaScript事件物件
- Vue中計算屬性和偵聽器Vue
- vue 基礎入門筆記 19:計算屬性和偵聽屬性Vue筆記
- 微信小程式,實現 watch 屬性,監聽資料變化微信小程式
- 【Vue】計算屬性 監聽屬性 元件通訊 動態元件 插槽 vue-cli腳手架Vue元件
- 動態監聽和靜態監聽
- KVO監聽容器類(陣列,字典等)屬性的兩種方法陣列
- vue 監視屬性Vue
- oracle靜態監聽和動態監聽Oracle
- swift 訊息監聽和鍵值監聽(kvo)Swift
- defer 屬性和 async 屬性
- Vue中的計算屬性和偵聽器比較Vue
- 使用 TypeScript 自定義裝飾器給類的屬性增添監聽器 ListenerTypeScript
- java全域性滑鼠鍵盤監聽Java
- 學習筆記:vue為什麼不能監聽陣列屬性的變化筆記Vue陣列
- 【listener】oracle靜態監聽和動態監聽 【轉載】Oracle
- JavaScript私有屬性和靜態屬性JavaScript
- 事件和事件監聽器事件
- Vue3.0的遞迴監聽和非遞迴監聽Vue遞迴
- 【Vue.js 牛刀小試】05:第五章 – 計算屬性與監聽器Vue.js
- 【Vue.js 牛刀小試】05:第五章 - 計算屬性與監聽器Vue.js
- 03-修飾符-監聽屬性-傳送Ajax請求-生命週期鉤子
- Vue入門指南-04 事件機制和事件/按鍵修飾符和過濾器及監聽屬性(快速上手vue)Vue事件過濾器
- 庫和屬性
- 屬性和方法
- 【JS】在連續性監聽事件中,監聽當前狀態是否變化JS事件