目標
- servlet的生命周期
- servletConfig與ServletContext定義,及其二者的區別
- 監聽器
- 過濾器
- 請求響應封裝器
- 異步處理
- 模擬服務器推播
1. servlet的生命周期
為什么java總是討論生命周期?因為Java的世界里所有的東西都是對象,是對象就有初始化、執行、銷毀的過程。
那servlet的生命周期,也就是它什么時候被創建,什么時候執行,什么時候被銷毀?
說這之前,先說一下servlet的定義吧,也就是它的祖籍關系!!(別忘了,我們真正使用的是HttpServlet)
HttpServlet實現了GenericServlet類,GenericServlet又實現了Servlet接口和ServletConfig接口,現在分別的解釋一下這幾個類和接口都是干嘛的?
- servlet原來是用來和各種網絡協議交互的,因為網絡協議不同,所以實現方式也就不同,所以它是個接口
- GenericServlet抽象類簡單的實現了servlet接口(簡單也就是不夠落地,例如就沒有doXXX等函數),它還實現了一些系統需求功能,比如實現(其實是一種封裝,ServletConfig對象是做為參數傳進來的,GenericServlet按照ServletConfig接口的規范,重新封裝了對象)了ServletConfig接口(初始化參數,它是),還有類似log日志的功能,這些都是系統需求。
- servlet的基本功能和系統需求功能都實現了,那剩下的就是具體的協議交互處理了,HttpServlet就實現了具體的協議交互,里面就有了doXXX等函數
接下來,我們主要針對ServletConfig和ServletContext分別進行詳細描述。
--------------------GenericServlet--------------------
GenericServlet的用途上面已經說了,現在來看一下它的代碼,它是如何封裝ServletConfig的
這里有個問題:
servlet是java的類,初始化為什么不用構造函數,而是用init函數
The init() method creates and loads the servlet. But the servlet instance is first created through the constructor (done by Servlet container). We cannot write constructors of a servlet class with arguments in servlet (It will throw Exception). So, They provided a init() method that accepts an ServletConfig object as an argument. ServletConfig object supplies a servlet with information about its initialization (init) parameters. Servlet class cannot declare a constructor with ServletConfig object as a argument and cannot access ServletConfig object.
More info at: http://java.sun.com/j2ee/tutorial/1_3-fcs/doc/Servlets6.html
servlet是一個類,他是由web容器來創建和初始化的,所以構造函數不能由開發人隨意定義,web容器會迷糊的(結果就是會拋出異常),所以web容器會自動得給servlet添加一個空的構造器。那servlet需要參數怎么辦?而且不同的servlet參數也不一樣!好吧,參數不一樣,我們用ServletConfig對象來解決,然后servlet類提供了一個init函數,讓我們可以把參數對象ServletConfig傳進去。
2. servletConfig與ServletContext定義,及其二者的區別
--------------------ServletConfig--------------------
Jsp/Servlet容器初始化一個Servlet類型的對象時,會為這個對象創建一個ServletConfig對象,在這個ServletConfig對象中包含了Servlet的初始化參數信息。此外,ServletConfig對象還在內部引用了ServletContext對象。Jsp/Servlet容器在調用Servlet對象的init(ServletConfig config)方法時,會把ServletConfig類型的對象做為參數傳遞給servlet對象,也相當于把ServletContext傳遞給了servlet對象。以下是ServletConfig定義的方法:
getInitParamter(String name)
getInitParamterNames()
getServletContext()
getServletName()
下面這個是一個具體的代碼舉例:
這里有一個問題:
在GenericServlet中,為什么要有兩個init函數?
請仔細看一下GenericServlet的實現,一個有參的init函數參數是固定的,就是ServletConfig對象做為參數,如果這個可以被繼承并重寫,那web容器怎么給servlet傳ServletConfig參數?所以這個帶參數的init函數不能重寫,那就再來一個無參的init函數吧,做為初始化參數!
通常使用標注來設置初始化參數,之后若想改變這些參數,用web.xml重新設置,就可以覆蓋標注。而且不用修改代碼,重新編譯。
--------------------ServletContext--------------------
ServletContext是servlet與servlet容器之間的直接通信接口!Servlet容器在啟動一個webapp時,會為它創建一個ServletContext對象,即servlet上下文環境。每個webapp都有唯一的ServletContext對象。同一個webapp的所有servlet對象共享一個ServletContext,servlet對象還可以通過ServletContext來訪問容器中的各種資源。
webapp?一個項目就是一個webapp,每個項目都配備一個web.xml,web容器會為每個項目產生一個ServletContext對象。
ServletContext提供的方法分為以下幾種類型:
用于在webapp范圍內共享數據
setAttribute(String name,Java.lang.Object object)
getAttribute(String name)
getAttributeNames()
removeAttribute(String name)
訪問當前webapp的資源
getContextpath() webapp的URL
getInitParameter(String name) 容器的指定初始化參數值
getInitParameterNames() 容器的所有初始化參數
getServletContextName() webapp名稱
getRequestDispather(String path) 返回一個向其它web組件轉發請求的RequestDispather對象
訪問servlet容器的相關信息
getContext(String uripath) 根據參數指定的url,返回當前servlet容器中其它web應用的ServletContext對象
訪問web容器的相關信息
getMajorVersion() servlet容器支持的java servlet API的主版本號
getMinorVersion() 上面的次版本號
getServerInfo() 返回servlet容器的名字和版本
訪問服務器端的文件系統資源
getRealPath(String path) 根據參數指定的虛擬路徑,返回文件系統中的真實路徑
getResource(Sting path) 返回一個映射到參數指定路徑的url
getResourceStream(String path) 返回一個用于讀取參數指定的文件的輸入流
getMimeType(String file) 返回參數指定的MIME
輸出日志
log(String msg) 向servlet的日志文件寫日志
log(String message, java.lang.Throwable throwable):向servlet的日志文件中寫錯誤日志,以及異常的堆棧信息。
ServletContext對象的獲取方式
servlet2.5版本與之前:
javax.servlet.http.HttpSession.getServletContext()
javax.servlet.jsp.PageContext.getServletContext()
javax.servlet.ServletConfig.getServletContext() 這就是我們這里使用的方法
servlet3.0新增方法
javax.servlet.ServletRequest.getServletContext()
----------------servletConfig與ServletContext的區別---------------------
從作用范圍來說,ServletConfig作用于某個特定的Servlet,即從該Servlet實例化,那么就開始有效,但是該Servlet之外的其他Servlet不能訪問;ServletContext作用于某個webapp,即在一個webapp中相當于一個全局對象,在Servlet容器啟動時就已經加載,對于不同的webapp,有不同的ServletContext。
3.監聽器
監聽器類似于php的鉤子函數,不同的是java主要用監聽器來健康對象的生命周期和屬性變化等。
既然是監聽器,那就要有監聽的對象目標(不能像php一樣,隨意的下鉤子):
- ServletContext對象
- HttpSession對象
- HttpServletRequest對象
寫一個監聽器也是兩部分組成:編碼和監聽器聲明
- ServletContext對象------------------------------------------------
-
ServletContextListener
這是生命周期監控器,大白話就是監聽創建和銷毀行為的。
定義:
Paste_Image.png
聲明:servlet3.0 前必須web.xml聲明
Paste_Image.png
應用:
Paste_Image.png
這里可以看到,當觸發監聽器之后,在監聽器中可以獲得ServletContext對象,然后在該對象中讀取或設置應用內共享數據。
-
某些特定的程序設置,不能在運行期間動態設置,需要web應用程序初始化的時候進行,例如HttpSession的一些cookie設置(cookie在瀏覽器端的失效時間),可以采用web.xml設置,也可以采用監聽器的方式
Paste_Image.png
-
ServletContextAttributeListener
關于屬性監控器,監控的行為就是該屬性的添加、修改、移除。
Paste_Image.png
標注聲明方式:
Paste_Image.png
web.xml聲明方式:
Paste_Image.png
- HttpSession對象
與HttpSession相關的監聽器有四個:
HttpsessionListener 監聽生命周期
HttpSessionAttributeListener 監聽屬性修改
HttpSessionBindingListenner 監聽對象綁定(不用聲明)
HttpSessionActivateionListener 監聽跨jvm轉移
前兩個是比較簡單的,都是需要代碼編寫和聲明注冊兩部分工作,這里主要介紹一下后兩個。
HttpSessionBindingListenner
它是對象綁定監聽器,通俗的講,就是當一個對象被作為參數賦值給session之后會觸發這個監聽器。這個監聽器是由對象直接集成該監聽器接口,在對象的類中直接實現的,所以不需要標注或web.xml聲明。
HttpSessionActivateionListener
它是對象遷移監聽器,如果應用程序的對象分布在多個jvm中,就涉及到對象的遷移,在遷移之前會進行序列化,這時候會觸發監聽器的sessionWillPassivate()函數,到了目標jvm中,還需要反序列化,就會觸發監聽器的sessionDidActivate()函數。(這個是抄的)
- HttpServletRequest對象
與請求相關的監聽器有三個:
ServletRequestListener 生命周期監聽器
ServletRequestAttributeListener 屬性監聽器
AsyncListener servlet3.0中新增,異步處理時會用到
4. 過濾器
-
過濾器定義----------------------------
在容器調用servlet的service()方法前,servelt并不會知道有請求的到來,而在servlet的service()方法運行之后,容器真正對瀏覽器進行http響應之前,瀏覽器也不會知道servlet真正的響應是什么。過濾器是介于servlet之前,可攔截過濾瀏覽器對servlet的請求,也可以改變servlet對瀏覽器的響應。
想想已經開發好應用程序的主要業務功能了,但現在產品又出了新的需求:
- 針對所有的servlet,測試想要了解從請求到響應之間的時間差
- 針對某些特定的頁面,客戶希望只有特定的幾個用戶才可以瀏覽
- 基于安全方面的考慮,用戶輸入的特定字符必須過濾并替換為無害字符
- 請求與響應的編碼從Big5改為UTF-8
- 過濾器實現--------------------------
先來看一個計算servlet消耗時間的過濾器
標注數字的位置:
- 這是FilterConfig,和ServletContext相似,是為過濾器提供初始化參數的對象
- 在doFilter函數中,會去判斷是調用下一個過濾器還是調用service()函數
-
過濾器聲明----------------------------
聲明就是兩種方式,上面代碼中是標注方式,這里看一下web.xml方式(其實和聲明servlet很相似)
<filter>
<filter-name>FristFilter</filter-name>
<filter-class>filter.FirstFilter</filter-class>
#FilterConfig的設置的初始化參數
<init-param>
<param-name>param1</param-name>
<param-value>hello world</param-value>
<init-param>
<init-param>
<param-name>param2</param-name>
<param-value>good</param-value>
<init-param>
</filter>
<filter-mapping>
<filter-name>FristFilter</filter-name>
<url-pattern>/res.jsp</url-pattern>
</filter-mapping>
如果在web.xml中聲明多個過濾器,按照聲明的先后順序形成過濾器鏈。
- 過濾器觸發時機----------------------------
@WebFilter(
filterName="some",
urlPatterns={"/some"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.Error,
DispatcherType.ASYNC
}
)
可以觸發的時機就是這些,如果想觸發RequestDispatcher.forward()內部轉發過來的請求過濾器,就需要設置DispatcherType.FORWARD,其它同理。默認是DispatcherType.REQUEST。
5. 請求響應封裝器
- 請求封裝器-----------------------------
如果我們已經有一個寫好的項目,項目中都是用getParameter()直接獲取參數。我們現在希望把參數值中的敏感詞過濾掉,但又不希望修改原來的代碼,這里如何實現?(如果可以修改源碼,我們就可以在過濾器中使用setAttribute函數,在代碼中getAttribute參數就可以)
HttpServletRequest對象有getParameter()函數,但是沒有setParameter()函數。
解決辦法:
- 我們自己實現一個HttpServletRequest接口,然后添加一個setAttribute()函數。(不過這個接口內容很多,不好實現啊)
- 系統已經為我們準備好了一個實現類(HttpServletRequestWrapper),我們只要繼承它之后重寫getParameter()函數,讓這個函數直接返回過濾后的參數值!
來看看我們繼承這個HttpServletRequestWrapper類之后,做了什么?
用請求封裝器結合過濾器來實現我們的業務需求:
在這里試想一下,我們原來講過的GET參數的編碼問題,如果是在servlet里面接收參數之后再轉碼,那每個servlet都需要實現轉碼程序,如果這里用請求封裝器去實現,在servlet直接getParameter()就是轉碼之后的參數值了,是不是很方便,POST同理!
- 響應封裝器-----------------------------
在servlet中,如果我們希望給瀏覽器做響應時,對響應數據進行壓縮,正常情況下,我們會調用PrintWriter或ServletOutputStream進行輸出,可是目前這兩個對象的輸出函數都不支持壓縮,因此我們需要針對這兩個對象和HttpServletResponse對象進行重新封裝。
以ServletOutputStream舉例,下面的代碼先封裝一下這個對象:
public class GZIPServletOS extends ServletOutputStream {
private GZIPOutputStream gzipOS;
public GZIPServletOS(ServletOutputStream servletOS) throws IOException {
gzipOS = new GZIPOutputStream(servletOS);
}
// 由于OutputStream的所有輸出底層都是調用wirte(int b)方法的,因此
// 只有讓write(int b)具有壓縮功能,那么OutputStream的所有輸出就都具有壓縮功能了
@Override
public void write(int b) throws IOException {
// TODO Auto-generated method stub
gzipOS.write(b); // 輸出時用封裝的GZIPOutputStream的write壓縮輸出
}
// 對于壓縮輸出流,字節流中前后數據是相互依賴壓縮的,傳輸只能按照壓縮快一塊一塊傳輸
// 不能將一塊分成多個部分傳輸,因此GZIPOutputStream提供finish方法手動控制緩沖區的輸出
// finish類似flush方法,但不過finish并不是將整個緩沖區中所有內容輸出
// 而是將緩沖區中現有的所有完整的塊輸出,末尾不完整的塊不輸出,繼續接受壓縮字節
// 記住!壓縮流只能以壓縮塊為單位進行輸出
public void finish() {
if (gzipOS != null) // 必須在非空的時候才能使用finish,否則會拋出異常
gzipOS.finish();
}
}
接下來我們看看HttpServletResponse的封裝器:
public class CompressWrapper extends HttpServletResponseWrapper {
// 基于OutputStream和PrintWriter的規則設計封裝器
// 在J2SE標準下PrintWriter用封裝的OutputStream進行輸出
// PrintWriter在創建時也是利用OutputStream的:偽代碼
// OutputStream os = new OutputStream
// PrintWriter out = new PrintWriter(os)
// 因此J2SE標準規定:如果out封裝了os,那么輸出時就只能其中一個
// 用os輸出時就不得使用out輸出,用out輸出的時候就不能用os輸出
// 混用就直接拋出IllegalStateException
// 這兩個成員的設計就符合J2SE標準
private GZIPServletOS gzipServletOS; // OutputStream
private PrintWriter out; // PrintWriter
public CompressWrapper(HttpServletResponse resp) {
super(resp);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
// TODO Auto-generated method stub
if (out != null) { // 用os進行輸出時out不能占用os
throw new IllegalStateException();
}
if (gzipServletOS == null) {
gzipServletOS = new GZIPServletOS(getResponse().getOutputStream());
}
return gzipServletOS; // 多態返回,向上隱式轉換
}
@Override
public PrintWriter getWriter() throws IOException {
// TODO Auto-generated method stub
if (gzipServletOS != null) { // os已經被占用就不能在使用out了
throw new IllegalStateException();
}
if (out == null) {
gzipServletOS = new GZIPServletOS(getResponse().getOutputStream());
OutputStreamWriter osw = new OutputStreamWriter(
gzipServletOS, getResponse().getCharacterEncoding());
out = new PrintWriter(osw);
}
return out;
}
@Override
public void setContentLength(int len) {
// TODO Auto-generated method stub
// 不實現此方法內容,因為真正的輸出會被壓縮
}
public void finish() { // 再對finish進行包裝
gzipServletOS.finish();
}
}
最后加上過濾器,我們來看看是如何實現輸出壓縮的,壓縮步驟分為三步:
- 檢查請求的accept-encoding標頭是否有gzip字符串,即判斷瀏覽器是否有壓縮響應的需求;
- 如果有這樣的需求,就得設置響應的content-encoding標頭為gzip;
- 接著就是壓縮響應封裝、doFilter、手動沖刷壓縮緩沖區了;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String encodings = ((HttpServletRequest)request).getHeader("accept-encoding");
if (encodings != null && encodings.indexOf("gzip") > -1) {
CompressWrapper respWrapper = new CompressWrapper((HttpServletResponse)response);
respWrapper.setHeader("content-encoding", "gzip");
chain.doFilter(request, respWrapper);
respWrapper.finish();
}
else {
chain.doFilter(request, response);
}
}
5. 異步處理(針對的是異步上下文對象,不是多線程)
如果瀏覽器請求了一個比較耗時的servlet,那就會一直等待直到服務器返回響應。在Servlet3.0中引入了異步處理的機制,允許servlet重新啟動一個線程去處理耗時業務邏輯,原來web容器啟動的線程可以直接返回響應,新啟動的異步線程后續也可以響應瀏覽器。
- AsyncContext
這個對象里面包含的是發起異步任務時的上下文環境,它提供了一些工具方法(dispatch,獲取request,response等),獲取該對象的方法:
AsyncContext startAsync()
AsyncContext startAsync(ServletRequest,ServletResponse)
第一個方法是采用當前servlet的ServletRequest,ServletResponse對象,第二個方法是由自己封裝新的ServletRequest,ServletResponse對象。下面我們來看看具體的代碼:
servlet類
@WebServlet(urlPatterns="/async", asyncSupported=true)
public class AsyncServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
response.setContentType("text/html;charset=GBK");
PrintWriter out = response.getWriter();
out.println("進入Servlet的時間:" + new Date() + ".<br/>");
out.flush();
AsyncContext acontext = request.startAsync();
acontext.setTimeout(20*1000);
acontext.start(new Executor(acontext));
out.println("結束Servlet的時間:" + new Date() + ".<br/>");
out.flush();
}
}
Executor做了什么!
public class Executor implements Runnable {
private AsyncContext context;
public Executor(AsyncContext context) {this.context = context;}
public void run(){
try {
Thread.sleep(5000);
ServletRequest request = context.getRequest();
List<String> books = new ArrayList<String>();
books.add("book1"); books.add("book2"); books.add("book3");
request.setAttribute("books", books);
context.dispatch("/async.jsp");
} catch (Exception e) {
e.printStachTrace();
}
}
}
在Executor中,讓線程睡眠5秒來模擬耗時的業務邏輯,最后調用AsyncContext的dispatch方法把請求轉發到指定的jsp頁面。被異步請求的頁面需要指定session="false",表明不會重新創建session,下面是async.jsp的內容:
<%@ page contentType="text/html;chaset=GBK" language="java" session="fasle" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<ul>
<c:forEach items="${books}" var="book" >
<li>${book}</li>
</c:forEach>
</ul>
<%
out.println("業務調用結束的時間:" + new Date());
request.getAsyncContext().complete();//完成異步調用
%>
這里說明異步也可以延時響應客戶端。請忽略jsp頁面的寫法,后續再講解,它使用JSTL標簽庫。
以上是異步調用的代碼,我們需要對servlet進行聲明,告知web容器該servlet支持異步:
- @WebServlet中指定asyncSupported=true
- web.xml中配置
<servlet>
<servlet-name>async</servlet-name>
<servlet-class>com.abc.AsyncServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>async</servlet-name>
<url-pattern>/async</url-pattern>
</servlet-mapping>
對于支持異步調用的Servlet來說,當Servlet以異步方式啟用新線程后,該Servlet的執行不會被阻塞,該Servlet將可以向客戶端瀏覽器生成相應——當新線程執行完成后,新線程生成的相應將再次被送往客戶端瀏覽器。
當servlet啟動異步線程之后,對于該線程是否執行成功,是否遇到問題我們就不知道了,為了調試代碼,我們可以用Servlet3.0支持的異步監聽器AsyncListener來實現對異步任務線程的監控,該監聽器提供的方法如下:
- onStartAsync(AsyncEvent event):當異步調用開始時觸發該方法
- onComplete(AsyncEvent event):當異步調用結束時觸發該方法
- onError(AsyncEvent event):當異步調用出錯時觸發該方法
- onTimeout(AsyncEvent event):當異步調用超時時觸發該方法
監聽器代碼
public class MyAsyncListener implements AsyncListener {
public void onComplete(AsyncEvent event) {
System.out.println("異步調用完成:" + new Date());
}
public void onError(AsyncEvent event) {
System.out.println("異步調用出錯:" + new Date());
}
public void onStartAsync(AsyncEvent event) {
System.out.println("異步調用開始:" + new Date());
}
public void onTimeout(AsyncEvent event) {
System.out.println("異步調用超時:" + new Date());
}
}
通過AsyncContext來注冊監聽器
AsyncContext acontext = request.startAsync();
acontext.addListener(new MyAsyncListener());
6. 模擬服務器推播
開發web如果要實現服務器主動推送信息至瀏覽器,基本上只有幾種方式:
- websocket
- 瀏覽器輪詢,每隔一個時間段請求一次服務器
- comet技術
java在servlet3.0之后,實現了異步機制,在上一節,我們已經看異步任務的簡單實現方式,這節就是利用異步機制實現,大致分為這幾部分:
- 多線程異步執行的程序
- 接收請求的servlet
- 發起請求的頁面
多線程異步執行程序:
這里是Runnable多線程方式,實現了一個ServletContext的生命周期監聽器,在這個監聽器里面,循環處理異步任務上下文ArrayList,這個ArrayList的添加是在接收請求的servlet中實現的。
接收請求的servlet:
這里就是接收請求的servlet,取得AsyncContext上下文對象,然后添加至asyncs隊列中。這里如果需要監控多線程背后什么怎么運行的,可以采用上一節的異步監聽方式。
請求頁面這里就不發了,就是ajax異步請求servlet,然后得到數據之后,動態更新到DOM里就可以了!