[Java]Sevlet

0 前言

對於Java程式設計師而言,Web伺服器(如Tomcat)是後端開發繞不過去的坎。簡單來看,瀏覽器發送HTTP請求給伺服器,伺服器處理後發送HTTP響應給瀏覽器。

Web伺服器負責對請求進行處理。HTTP請求和響應本質上可以看成是有一定格式的字元串,而在網路中傳輸的數據都需要轉換成二進位格式。Web伺服器一方面需要將傳入的二進位數據解析成程式能理解的HTTP請求對象,另一方面需要將處理後的響應包裝成相應的響應對象,並轉換成二進位數據傳出。

對於Java程式設計師而言,以上這兩個過程都由Web伺服器幫我們自動完成,我們僅僅需要關注具體的業務。同時,Java也為該處理流程提供了規範,即Servlet介面。Maven依賴如下:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>

因此,我們的主要工作就是根據不同的請求,實現對應的servlet。然後,伺服器就可以通過下圖的執行流程,完成對瀏覽器的響應。

Tomcat處理流程

為了實現具體的servlet,我們需要繼承HttpServlet抽象類,並實現其中的某些方法,其類層次結構如下:

HttpServlet

1 Servlet介面

Servlet介面位於javax.servlet包中,是Servlet開發的頂層介面。

public interface Servlet{}

1.1 介面介紹

介面文檔注釋如下:

1、Defines methods that all servlets must implement.

2、A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web clients, usually across HTTP, the 
HyperText Transfer Protocol.

3、To implement this interface, you can write a generic servlet that extends
	- javax.servlet.GenericServlet 
	- or an HTTP servlet that extends javax.servlet.http.HttpServlet.
		
4、This interface defines methods to initialize a servlet, to service requests, and to remove a servlet from the server. These are known as life-cycle 
methods and are called in the following sequence:
	① The servlet is constructed, then initialized with the init method.
	② Any calls from clients to the service method are handled.
	③ The servlet is taken out of service, then destroyed with the destroy method, then garbage collected and finalized.
	
5、In addition to the life-cycle methods, this interface provides the getServletConfig method, which the servlet can use to get any startup information, 
and the getServletInfo method, which allows the servlet to return basic information about itself, such as author, version, and copyright.

1、介面功能

定義了所有servlets必須實現的方法。

2、什麼是servlet

在Web伺服器中,用於處理客戶端HTTP請求的Java對象,可以類比於Spring MVC中的Controller、Netty中的Handler

3、使用方法

為了定義我們自己的servlet,有兩種方法(通常使用第2種方法):

  1. 繼承javax.servlet.GenericServlet
  2. 對於處理Http請求的servlet,繼承javax.servlet.http.HttpServlet

4、生命周期

介面中定義了初始化servlet、處理請求和從伺服器中移除servlet的方法。這些方法被稱為生命周期方法,其執行流程如下:

  1. servlet構造完成後,執行init()方法進行初始化;
  2. 執行service()方法處理客戶端發送來的請求;
  3. 當需要移除servlet時,執行destroy()方法,進行相應的垃圾收集工作。

5、其他方法

除了生命周期方法,介面還定義了其他很實用的方法:

  • getServletConfig():獲取所有啟動資訊;
  • getServletInfo():獲取servlet的作者、版本和版權等資訊。

1.2 方法詳解

Servlet中僅定義了3個生命周期方法和2個獲取資訊的方法:

  • init()
  • service()
  • destroy()
  • getServletConfig()
  • getServeltInfo()

1、 init()

init()方法的完整簽名為:void init(ServletConfig) throws ServletException

文檔:

1、Called by the servlet container to indicate to a servlet that the servlet is being placed into service.

2、The servlet container calls the init method exactly once after instantiating the servlet. The init method must complete successfully before the servlet 
can receive any requests.

3、The servlet container cannot place the servlet into service if the init method:
	① Throws a ServletException
	② Does not return within a time period defined by the Web server

4、Params:
	config – a ServletConfig object containing the servlet's configuration and initialization parameters
	
5、Throws:
	ServletException – if an exception has occurred that interferes with the servlet's normal operation

如前所述,當servlet構造完成後,可以通過init()方法對其進行初始化。我們僅需要在該方法中定義相應的初始化邏輯,實際調用由servlet container控制。

需要注意的是:在實例化servlet後,servlet container僅會調用1次init()方法。並且只有當init()方法完全執行完畢之後,才會進行後續的接收客戶端請求過程。

由於只有init()成功執行完畢,才能進行後續的服務,所以init()的成功執行至關重要。當出現下列2種情況時,init()方法會執行出錯,影響後續的服務流程:

  1. 拋出ServletException異常;
  2. 沒有在Web伺服器規定的時間內返回。如執行耗時的I/O操作、執行緒休眠時間過長等(我們儘可能要避免這些操作)。

init()方法會接收一個ServletConfig對象,該對象中包含servlet的配置和初始化參數等資訊,用於後續具體的初始化邏輯。

init()方法會拋出一個ServletException異常,該異常表示servlet執行過程中會遇到的一般異常。

2、service()

service()方法的完整簽名:void service(ServletRequest, ServletResponse) throws ServletException, IOException

文檔:

1、Called by the servlet container to allow the servlet to respond to a request.

2、This method is only called after the servlet's init() method has completed successfully.

3、The status code of the response always should be set for a servlet that throws or sends an error.

4、Servlets typically run inside multithreaded servlet containers that can handle multiple requests concurrently. Developers must be aware to synchronize 
access to any shared resources such as files, network connections, and as well as the servlet's class and instance variables. 

4、Params:
	req – the ServletRequest object that contains the client's request
	res – the ServletResponse object that contains the servlet's response

5、Throws:
	ServletException – if an exception occurs that interferes with the servlet's normal operation
	IOException – if an input or output exception occurs

service()方法的功能是處理客戶端發送的請求。該方法會在特定的時刻由service container調用,我們僅僅需要定義好相應的處理邏輯即可。

service()只有在init()方法完成後才會被調用。

無論該servlet是否會拋出或發送異常,我們都要記得設置response的狀態碼。

由於servlet通常是運行在多執行緒的servlet container中,會同時接收到多個請求,所以開發人員必須在service()方法中注意對共享資源的訪問。例如文件、網路連接以及servlet的類變數和成員變數等。

service()會接收ServletRequestServletResponse兩個參數。前者代表客戶端的請求對象,包含HTTP請求的各種資訊,開發人員根據該對象包含的資訊進行相應的業務處理;後者代表服務端的響應對象,包含HTTP響應的各種資訊,開發人員需要通過該對象進行響應。

service()會拋出ServletExceptionIOException兩種異常。前者代表servlet執行過程中的一般性異常,後者代表業務處理過程中IO操作可能會拋出的異常。

3、destroy()

destroy()方法的完整簽名:void destroy()

文檔:

1、Called by the servlet container to indicate to a servlet that the servlet is being taken out of service. This method is only called once all threads 
within the servlet's service method have exited or after a timeout period has passed. After the servlet container calls this method, it will not call the 
service method again on this servlet.
2、This method gives the servlet an opportunity to clean up any resources that are being held (for example, memory, file handles, threads) and make sure 
that any persistent state is synchronized with the servlet's current state in memory.

destroy()方法的簽名比較簡單,既沒有接收參數,也沒有返回參數和拋出異常。

servlet被移除前servlet container會調用destroy()方法,主要功能是在servlet銷毀前釋放相關資源(例如鎖、記憶體、文件和執行緒等),以保證系統中的狀態同步,使系統正常運行。

需要注意的是:destroy()方法會等到此servletservice()方法中的所有執行緒退出或超時後才執行,並且僅執行1次。當destroy()被調用後,servlet中的所有方法都不可能再被調用了,亦即該servlet沒用了。

4、getServletConfig()

getServletConfig()方法的完整簽名為:ServletConfig getServletConfig()

文檔:

1、Returns a ServletConfig object, which contains initialization and startup parameters for this servlet. The ServletConfig object returned is the one 
passed to the init method.

2、Implementations of this interface are responsible for storing the ServletConfig object so that this method can return it. The GenericServlet class, 
which implements this interface, already does this.

3、Returns:
	the ServletConfig object that initializes this servlet

getServletConfig()的語義相當清晰,獲取初始化servlet的配置資訊。

getServletConfig()方法會返回一個ServletConfig對象,該對象包含了servlet的初始化和啟動參數等資訊。之前我們介紹過,init()方法需要接收一個ServletConfig對象,其實這兩個ServletConfig對象是同一個對象。

實現Servlet介面需要保存這個ServletConfig對象以便於傳入init()和通過getServletConfig()返回。在GenericServlet類中已經幫我們實現好這個功能。所以當我們繼承GenericServletHttpServlet時,不再需要自己實現此方法,直接使用即可。

5、getServletInfo()

getServletInfo()方法的完整簽名為:String getServletInfo()

文檔:

1、Returns information about the servlet, such as author, version, and copyright.

2、The string that this method returns should be plain text and not markup of any kind (such as HTML, XML, etc.).

3、Returns:
	a String containing servlet information

getServletInfo()方法會返回一個字元串,包含servlet的作者、版本和版權等資訊。

該字元串可以有多種格式,如text、HTML和XML等。

2 ServletConfig介面

ServletConfig介面位於javax.servlet包中,用來定義servlet配置資訊對象所具備的方法。

public interface ServletConfig{}

2.1 介面介紹

文檔:

A servlet configuration object used by a servlet container to pass information to a servlet during initialization.

ServletConfig定義了servlet的配置對象所具有的方法,用來獲取各種配置資訊。在servlet初始化過程中,servlet container會將此對象傳入init()方法以進行初始化。

2.2 方法詳解

ServletConfig介面中定義了4個方法,分別用來獲取servlet的名字、上下文、初始化參數和初始化參數名:

  • getServletName()
  • getServletContext()
  • getInitParameter()
  • getInitParameterNames()

1、getServletName()

getServletName()方法的完整簽名為:String getServletName()

文檔:

1、Returns the name of this servlet instance. The name may be provided via server administration, assigned in the web application deployment descriptor, 
or for an unregistered (and thus unnamed) servlet instance it will be the servlet's class name.

2、Returns:
	the name of the servlet instance

getServletName()方法會返回一個字元串,表示servlet實例的的名字。

servlet的名字可以由伺服器管理員提供,並在Web應用程式部署的描述符中配置。

如果沒有對servlet手動指定名字,則默認為該servlet的類名。

2、getServletContext()

getServletContext()方法的完整簽名為:ServletContext getServletContext()

文檔:

1、Returns a reference to the ServletContext in which the caller is executing.

2、Returns:
	a ServletContext object, used by the caller to interact with its servlet container

getServletContext()方法會返回當前正在callerServletContext對象的引用。通過ServletContext對象可以與servlet container進行交互。

3、getInitParameter()

getInitParameter()方法的完整簽名為:String getInitParameter(String)

文檔:

1、Returns a String containing the value of the named initialization parameter, or null if the parameter does not exist.

2、Params:
	name – a String specifying the name of the initialization parameter

3、Returns:
	a String containing the value of the initialization parameter

getInitParameter()方法接收初始化參數的名字(字元串),並返回對應初始化參數的值(字元串)。如果沒有該初始化參數,則返回null

4、getInitParameterNames()

getInitParameterNames()方法的完整簽名為:Enumeration<String> getInitParameterNames()

文檔:

1、Returns the names of the servlet's initialization parameters as an Enumeration of String objects, or an empty Enumeration if the servlet has no 
initialization parameters.

2、Returns:
	an Enumeration of String objects containing the names of the servlet's initialization parameters

getInitParamterNames()方法會返回包含servlet所有初始化參數的Enumeration集合對象。如果servlet沒有初始化參數,則會返回空的Enumeration對象。

通過對該Enumeration對象進行遍歷,即可得到各初始化參數,後續也可進行相應的操作。

3 Serializable介面

serializable介面位於JDKjava.io包中,是一個標識介面(沒有方法),表明實現此介面的類可以進行序列化。

public interface Serializable{}

3.1 介面介紹

1、Serializability of a class is enabled by the class implementing the java.io.Serializable interface.

2、Warning: Deserialization of untrusted data is inherently dangerous and should be avoided. Untrusted data should be carefully validated according to the 
"Serialization and Deserialization" section of the Secure Coding Guidelines for Java SE. Serialization Filtering describes best practices for defensive 
use of serial filters. 

實現Serializable表明該類是可序列化的。

反序列化不受信任的數據是極度危險的,我們應該避免那樣做。

3、Classes that do not implement this interface will not have any of their state serialized or deserialized. All subtypes of a serializable class are 
themselves serializable. The serialization interface has no methods or fields and serves only to identify the semantics of being serializable.

4、To allow subtypes of non-serializable classes to be serialized, the subtype may assume responsibility for saving and restoring the state of the 
supertype's public, protected, and (if accessible) package fields. The subtype may assume this responsibility only if the class it extends has an 
accessible no-arg constructor to initialize the class's state. It is an error to declare a class Serializable if this is not the case. The error will be 
detected at runtime.

5、During deserialization, the fields of non-serializable classes will be initialized using the public or protected no-arg constructor of the class. A no-
arg constructor must be accessible to the subclass that is serializable. The fields of serializable subclasses will be restored from the stream.

如果一個類沒有實現Serializable介面,那麼無論在什麼情況下它都不能序列化或反序列化。一個實現了Serializable介面的類的所有子類都是可序列化和反序列化的。Serializable介面中沒有定義方法和屬性,它只是一個標識介面,用來表示某類是否可以序列化。

假設父類沒有實現Serializable介面,而子類為了能夠實現序列化功能,它需要在序列化或反序列化過程中保存或恢復父類中非私有(publicprotecteddefault)的屬性。為了實現這一目標,需要完成兩個要求:

  1. 子類是可序列化的:實現Serializable介面,並且其屬性都是可序列化的。
  2. 父類提供一個無參構造器,其能夠完成父類中屬性的初始化。

如果子類不能滿足以上要求,則會在運行時報錯。

反序列化時,不可序列化的父類的屬性會使用其無參構造器進行初始化,並且該無參構造器必須是子類能訪問的(public/protected)。而可序列化子類的屬性則直接從數據流中獲得。

6、When traversing a graph, an object may be encountered that does not support the Serializable interface. In this case the NotSerializableException will 
be thrown and will identify the class of the non-serializable object.

在遍歷graph時,可能會遇到集合中的對象沒有實現Serializable介面的情況。此時會拋出NotSerializableException異常,並標記該類為不可序列化的。

7、Classes that require special handling during the serialization and deserialization process must implement special methods with these exact signatures:
   - private void writeObject(java.io.ObjectOutputStream out) throws IOException
   - private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
   - private void readObjectNoData() throws ObjectStreamException;
   
8、The writeObject method is responsible for writing the state of the object for its particular class so that the corresponding readObject method can 
restore it. The default mechanism for saving the Object's fields can be invoked by calling out.defaultWriteObject. The method does not need to concern 
itself with the state belonging to its superclasses or subclasses. State is saved by writing the individual fields to the ObjectOutputStream using the 
writeObject method or by using the methods for primitive data types supported by DataOutput.

9、The readObject method is responsible for reading from the stream and restoring the classes fields. It may call in.defaultReadObject to invoke the 
default mechanism for restoring the object's non-static and non-transient fields. The defaultReadObject method uses information in the stream to assign 
the fields of the object saved in the stream with the correspondingly named fields in the current object. This handles the case when the class has evolved 
to add new fields. The method does not need to concern itself with the state belonging to its superclasses or subclasses. State is restored by reading 
data from the ObjectInputStream for the individual fields and making assignments to the appropriate fields of the object. Reading primitive data types is 
supported by DataInput.

10、The readObjectNoData method is responsible for initializing the state of the object for its particular class in the event that the serialization 
stream does not list the given class as a superclass of the object being deserialized. This may occur in cases where the receiving party uses a different 
version of the deserialized instance's class than the sending party, and the receiver's version extends classes that are not extended by the sender's 
version. This may also occur if the serialization stream has been tampered; hence, readObjectNoData is useful for initializing deserialized objects 
properly despite a "hostile" or incomplete source stream.

如果某些實現了Serializable介面的類在序列化或反序列化過程中需要對數據進行特殊處理,則可以在該類中實現如下方法:

  • private void writeObject(java.io.ObjectOutputStream) throws IOException
  • private void readObject(java.io.ObjectInputStream) throws IOException, ClassNotFoundException
  • private void readObjectNoData() throws ObjcetStreamException

通過在這些方法中定義特定處理邏輯,即可在序列化或反序列化過程中對數據進行特殊處理。例如實現數據流傳輸中的單例模式就是利用這些方法。

writeObject()方法負責在序列化過程中將對象屬性轉換成數據流,而readObject()方法則負責在反序列化過程中將數據流轉換成實際對象。序列化時,保存對象屬性的默認機制是調用out.defaultWriteObject()方法,該方法並不關心屬性是屬於父類還是子類的。使用writeObject()方法或DataOutput提供的寫出基本數據類型的方法可以對象的屬性分別寫出到ObjectOutputStream中。

readObject()方法負責將數據從流中讀出並保存到對象屬性中。其默認機制是調用in.defaultReadObject()方法來恢復對象的non-staticnon-transient屬性。defaultReadObject()方法會根據DataInputStream中的資訊為對應的對象屬性分配數據。如果類新增了屬性欄位,由於在數據流中找不到對應的屬性名,則該欄位不會被恢復。該方法並不關心屬性是屬於父類還是子類的。

當在反序列化過程中,數據流中沒有對象某些父類的資訊時,readObjectNoData()方法負責初始化相應對象。當序列化和反序列化兩端的類的版本不一致時可能會出現這種情況,此時相對於發送端類,接收端類可能新增了幾個父類。另外,如果數據流被篡改也可能會出現這種情況。此時,readObjectNoData()方法可以對數據流進行特殊處理,執行正確的初始化過程。

11、Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method 
with the exact signature:
   ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
   
12、This readResolve method follows the same invocation rules and accessibility rules as writeReplace.

在將對象寫入數據流時,如果需要為序列化類指定一個可替換的對象,則需要實現如下方法:ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException

相反,在將對象從數據流中恢復時,如果需要為反序列化類指定一個可替換的對象,則需要實現如下方法:ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException

13、The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during deserialization to 
verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization. If the 
receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender's class, then deserialization will 
result in an InvalidClassException. A serializable class can declare its own serialVersionUID explicitly by declaring a field named "serialVersionUID" 
that must be static, final, and of type long:
   ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

在序列化過程中需要為每個序列化類關聯一個版本號——serialVersionUID,它可以保證在反序列化過程中發送端和接收端的序列化類是兼容的。如果在反序列化過程中發現接收端和發送端序列化類的版本號不同,則會拋出一個InvalidClassException異常。任何序列化類都可以且應該聲明一個它自己的版本號,可以通過聲明一個屬性:ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;,屬性值是任意的,但必須保持一致。

14、If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value 
for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is strongly recommended 
that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class 
details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization. Therefore, 
to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit 
serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such 
declarations apply only to the immediately declaring class--serialVersionUID fields are not useful as inherited members. Array classes cannot declare an 
explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array 
classes.

如果你沒有顯示聲明一個serialVersionUID,系統會根據多方面因素計算出一個默認的serialVersionUID。具體因素在Java(TM)對象序列化規範中有詳細描述。

強烈建議為每一個序列化類都聲明serialVersionUID,因為serialVersionUID對類的細節十分敏感,很容易會根據編譯器實現的不同而發生變化,從而導致在反序列化過程中發生InvalidClassException異常。因此,為了保證在不同編譯器實現中serialVersionUID的一致性,必須為序列化類聲明明確的serialVersionUID。並且強烈建議使用private修飾符進行修飾,這樣可以保證在子類中對該屬性沒有訪問許可權,從而避免惡意修改。數組不能聲明serialVersionUID,因為它們使用默認計算的值,但是數組序列化是不需要匹配該值的。

4 GenericServlet抽象類

GenericServlet抽象類位於tomcat-embed-core.jar下的javax.servlet包中。

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

4.1 類介紹

1、Defines a generic, protocol-independent servlet. 

GenericServlet類的作用:定義了通用的、獨立於協議的servlet的標準。

2、To write an HTTP servlet for use on the Web, extend javax.servlet.http.HttpServlet instead.

3、GenericServlet implements the Servlet and ServletConfig interfaces. GenericServlet may be directly extended by a servlet, although it's more common to 
extend a protocol-specific subclass such as HttpServlet.

Tomcat為我們提供了一個的專門適用於HTTP協議開發的子類:javax.servlet.http.HttpServlet。在Web開發中,我們應該直接繼承HttpServlet進行開發,而不是使用GenericServlet。(後續會對HttpServlet進行詳細介紹。)

4、GenericServlet makes writing servlets easier. It provides simple versions of the lifecycle methods init and destroy and of the methods in the 
ServletConfig interface. GenericServlet also implements the log method, declared in the ServletContext interface.

5、To write a generic servlet, you need only override the abstract service method.

GenericServletservlet提供了Servlet介面中生命周期方法(init()destroy())、ServletConfig介面中獲取配置的方法以及聲明在ServletContext介面中的日誌方法的簡單實現,從而使得我們編寫servlet更加容易。

需要注意的是:GenericServlet中的service()方法是抽象的,需要我們根據不同的業務自己實現。

4.2 屬性介紹

GenericServlet中包含兩個屬性:

  • private static final long serialVersionUID = 1L;
  • private transient ServletConfig config;

serialVersionUID為序列化版本號,主要為了保證序列化和反序列化過程中的一致性。

ServletConfig表示servlet初始化的配置參數,用於傳入給init()方法進行初始化,以及getServletConfig()方法獲取配置參數。

4.3 方法介紹

GenericServlet中的方法比較多,我們可以按照實現的不同介面或功能進行分類:

  • 實現自Servlet介面:
    • init()
    • service()
    • destroy()
    • getServletConfig()
    • getServletInfo()
  • 實現自ServletConfig介面:
    • getServletName()
    • getServletContext()
    • getInitParameter()
    • getInitParameterNames()
  • ServletContext介面中的日誌方法:
    • log()

4.3.1 Servlet介面中的方法

1、init()

GenericServlet中提供了兩個init()方法:

  • public void init() throws ServletException
  • public void init(ServletConfig config) throws ServletException

其中帶參數的init(ServletConfig)才是Servlet介面中定義的初始化方法,而無參的init()GenericServlet類中新增的方法。

init()方法中的方法體默認為空:

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

init(ServletConfig)方法中做了兩件事:①初始化ServletConfig屬性,②執行init()方法:

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

實際上,當servlet載入到servlet container中時,容器在對其進行初始化的過程中調用的是init(ServletConfig)方法,它能夠保證對ServletConfig的保存,避免重寫時的惡意或錯誤程式碼。同時,它後續還會執行init()方法,這樣能夠保證我們自定義的初始化邏輯能夠正常執行。

因此,當開發人員需要自定義servlet的初始化過程時,應當重寫init()方法。而如果必須重寫init(ServletConfig)方法,必須在首行調用super.init(ServletConfig)

這種設計方法能夠將框架邏輯與業務邏輯分隔開來,使開發人員僅需要關注自己的業務邏輯。

2、service()

如前所述,service()方法中執行的是servlet的業務邏輯。因為每個servlet的功能都不同,service()方法需要自定義,因此GenericServlet中的service()方法是抽象的,我們必須對其進行重寫:

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

3、destroy()

destroy()方法主要是為了在servlet移除前釋放init()service()方法中佔用的資源。默認情況下servlet中並沒有使用相關資源,因此它與init()方法一樣,默認也是空的:

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

如果servlet運行時涉及到相關係統資源的佔用,我們必須重寫此方法,在移除前釋放相關資源。

4、getServletConfig()4

getServletConfig()方法返回servlet的配置對象,該對象已經通過init(ServletConfig)保存在config屬性中,我們只需要將該屬性返回即可:

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

5、getServletInfo()

getServletInfo()方法返回servlet的作者、版本和版權等資訊,默認情況下返回空字元串。後續子類可以對其重寫,以輸出實際有意義的資訊:

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

4.3.2 ServletConfig介面中的方法

1、getServletName()

getServletName()返回servlet的名字,由於servlet的所有配置資訊都保存在屬性config中,因此可以通過該屬性進行進一步獲取servlet的名字:

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

2、getServletContext()

getServletContext()返回servletservletContext,也是通過config屬性獲取:

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

3、getInitParameter()

getInitParameter(String)根據初始化參數的名字查找其值,也是通過config屬性獲取:

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

4、getInitParameterNames()

getInitParameterNames()則是獲取所有初始化參數的Enumeration<String>集合,也是通過config屬性獲取:

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

4.3.3 日誌方法

GenericServlet類提供了兩個日誌方法,它們實際上都是使用servletContext提供的日誌方法進行記錄:

  • public void log(String message):以servlet的名字為前綴將資訊寫入日誌。
  • public void log(String message, Throwable t):為可能拋出異常的servlet記錄日誌,以servlet的名字為前綴,並提供了異常對象以追蹤堆棧異常。

它們的源碼如下:

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

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

4.4 總結

通過上面的介紹,我們對GenericServlet類進行總結。它其實只為我們提供了三個功能:

  1. servlet的生命周期方法:init()init(ServletConfig)service()destroy()
  2. 獲取配置資訊的屬性和方法:基於private transient ServletConfig config;
  3. 日誌方法:log(String)log(String, Throwable)

我們只需要記住這三個功能,就能夠對GenericServlet類和一般的servlet功能有很好的整體理解。

5 HttpServlet抽象類

HttpServlet抽象類位於tomcat-embed-core.jar下的javax.servlet包中,是Servlet開發中使用最多的類。

public abstract class HttpServlet extends GenericServlet{}

5.1 類介紹

1、Provides an abstract class to be subclassed to create an HTTP servlet suitable for a Web site.

HttpServlet抽象類為所有基於HTTP的servlet提供了一套標準的模板。這意味著在Web開發中,我們自己實現的servlet必須繼承此類。

2、A subclass of HttpServlet must override at least one method, usually one of these:
	- doGet, if the servlet supports HTTP GET requests
	- doPost, for HTTP POST requests
	- doPut, for HTTP PUT requests
	- doDelete, for HTTP DELETE requests
	- init and destroy, to manage resources that are held for the life of the servlet
	- getServletInfo, which the servlet uses to provide information about itself

HttpServlet的子類中必須實現以下至少一個方法:

  • doGet():用於處理HTTP的GET請求。
  • doPost():用於處理HTTP的POST請求。
  • doPut():用於處理HTTP的PUT請求。
  • doDelete():用於處理HTTP的DELETE請求。
  • init()destroy():用於管理servlet生命周期中所持有的資源。
  • getServletInfo():用於提供servlet的資訊。
3、There's almost no reason to override the service method. service handles standard HTTP requests by dispatching them to the handler methods for each 
HTTP request type (the doMethod methods listed above).

4、Likewise, there's almost no reason to override the doOptions and doTrace methods.

需要注意的是:我們理論上不應該重寫service()方法。因為service()方法中實現了根據不同HTTP請求方式(GET、POST、PUT和DELETE等)進行分發的邏輯,類似於Spring MVC中的DispatchServlet類。

我們也不應該重寫doOptions()doTrace()方法,原因類似。

5、Servlets typically run on multithreaded servers, so be aware that a servlet must handle concurrent requests and be careful to synchronize access to 
shared resources. Shared resources include in-memory data such as instance or class variables and external objects such as files, database connections, 
and network connections.

servlet通常運行在多執行緒伺服器中,我們應該注意處理同步的HTTP請求,同步對共享資源的訪問。共享資源包括記憶體中的對象屬性或類屬性、外部的文件對象、資料庫連接資源和網路連接等。

5.2 靜態屬性介紹

HttpServlet抽象類中只有靜態屬性,用於實現不同的功能:

  • 序列化版本號:private static final long serialVersionUID = 1L
  • HTTP請求方法標識:
    • GET:private static final String METHOD_GET = "GET";
    • POST:private static final String METHOD_POST = "POST";
    • PUT:private static final String METHOD_PUT = "PUT";
    • DELETE:private static final String METHOD_DELETE = "DELETE";
    • HEAD:private static final String METHOD_HEAD = "HEAD";
    • OPTIONS:private static final String METHOD_OPTIONS = "OPTIONS";
    • TRACE:private static final String METHOD_TRACE = "TRACE";
  • HTTP請求頭部標識:
    • private static final String HEADER_IFMODSINCE = "If-Modified-Since";
    • private static final String HEADER_LASTMOD = "Last-Modified";
  • 配置資源文件的地址以及訪問對象:
    • private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings";
    • private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE);

5.3 方法介紹

HttpServlet沒有重寫GenericSerblet抽象類中已經實現的各種方法,只是實現了service()方法,並且增加了許多針對於HTTP協議的特殊方法。我們可以將這些方法分成以下幾類:

  • 構造函數:HttpServlet()

  • servlet的業務處理方法:service()

  • 各種HTTP請求方式對應的方法:

    • doGet()
    • doPost()
    • doPut()
    • doDelete()
    • doHead()
    • doOptions()
    • doTrace()
  • 各種HTTP請求頭部對應的方法:

    • maybeSetLastModified()
    • getLastModified()
  • 其他方法:

    • sendMethodNotAllowed()
    • getAllDeclaredMethods()

1、HttpServlet()

因為HttpServlet是一個抽象類,所以構造器中沒有實現任何邏輯:

public HttpServlet() {
    // NOOP
}

2、service()

GenericServlet抽象類中的init()方法一樣,HttpServlet抽象類中的service()方法也提供了兩個重載方法:

  • public void service(ServletRequest req, ServletResponse res)
  • protected void service(HttpServletRequest req, HttpServletResponse resp)

其中service(ServletRequest, ServletResponse)是對Servlet介面中的該方法的實現,功能是對請求協議進行初步篩選。當接收到相應的請求時,容器會調用此方法進行處理。此方法默認是對HTTP協議進行處理,因此對參數ServletRequestServletResponse進行了向下轉型。如果轉型失敗,會拋出ServletException異常,否則執行重載的service(HttpServletRequest, HttpServletResponse)方法。

@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, HttpServletResponse)方法中才真正開始了對請求進行處理,它會調用HttpServletRequest.getMethod()方法獲取當前HTTP請求的方式,隨後根據不同的方式分發到對應的doXxx()方法中進行處理。

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);
    }
}

總而言之,兩個service()共同完成了HTTP請求的分發功能,針對不同的請求方法,最終的業務邏輯都在doXxx()方法中。伺服器只需調用service(ServletRequest, ServletRespose)這一個方法,就能完成類型轉換、請求分發的功能。

3、doGet()

重寫此方法可以同時支援HTTP的GET和HEAD請求方式。HEAD方式本質上也是GET方式,只不過它的HTTP響應中只有首部資訊,沒有主體資訊。

我們重寫此方法的流程一般如下:

  1. 通過HttpServletRequest參數設置請求數據的解碼編碼集。
  2. 通過HttpServletRequest參數讀取請求數據;
  3. 處理數據;
  4. 通過HttpServletResponse參數設置響應頭部、輸出編碼集;
  5. 通過HttpServletResponse參數獲取輸出對象,並輸出響應數據。

在讀取請求數據或寫出響應數據前,必須設置好相應的編碼。響應首部必須在響應數據輸出前設置好,因為對於HTTP響應而言,響應首部是先於響應體發送的。

如果請求格式不正確,doGet()方法會返回一個錯誤資訊。

protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
{
    String msg = lStrings.getString("http.method_get_not_supported");
    sendMethodNotAllowed(req, resp, msg);
}

4、doPost()

重寫doPost()方法可用於處理POST請求。

HTTP的POST請求允許客戶端向服務端發送無限長的數據,這尤其對一些安全數據格外重要。

我們重寫此方法的流程一般如下:

  1. 通過HttpServletRequest參數設置請求數據的解碼編碼集。
  2. 通過HttpServletRequest參數讀取請求數據;
  3. 處理數據;
  4. 通過HttpServletResponse參數設置響應頭部、輸出編碼集;
  5. 通過HttpServletResponse參數獲取輸出對象,並輸出響應數據。

讀取請求數據或寫出響應數據前,必須設置好相應的編碼。響應首部必須在響應數據輸出前設置好,因為對於HTTP響應而言,響應首部是先於響應體發送的。

如果請求格式不正確,doPost()方法會返回一個錯誤資訊。

protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    String msg = lStrings.getString("http.method_post_not_supported");
    sendMethodNotAllowed(req, resp, msg);
}

5、doPut()

重寫doPut()方法用於處理HTTP的PUT請求。

HTTP的PUT請求主要用於客戶端向伺服器發送文件。

當重寫doPut()方法時,需要保留相應的請求頭部,包括Content-Length Content-Type Content-Transfer-Encoding Content-Encoding Content-Base Content-Language Content-LocationContent-MD5 Content-Range等。如果重寫的doPUt()方法中不能處理這些請求頭,則必須發送一個錯誤消息:HTTP 501 - Not Implemented,並丟棄這個請求。

doPut()方法並不是安全或冪等的,它可能會對數據產生副作用,因此開發人員要對此負責。執行此方法時,臨時存儲相應的URL在某些情況下很有用。

如果請求格式不正確,doPost()方法會返回一個錯誤資訊。

protected void doPut(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    String msg = lStrings.getString("http.method_put_not_supported");
    sendMethodNotAllowed(req, resp, msg);
}

6、doDelete()

重寫doDelete()方法用於處理HTTP的DELETE請求。

DELETE請求允許客戶端從伺服器上刪除相應的數據。

doDelete()方法並不是安全或冪等的,它可能會對數據產生副作用,因此開發人員要對此負責。執行此方法時,臨時存儲相應的URL在某些情況下很有用。

如果請求格式不正確,doDelete()方法會返回一個錯誤資訊。

protected void doDelete(HttpServletRequest req,
                            HttpServletResponse resp)
        throws ServletException, IOException {
    String msg = lStrings.getString("http.method_delete_not_supported");
    sendMethodNotAllowed(req, resp, msg);
}

7、doHead()

重寫doHead()方法用於處理HTTP的HEAD請求。

客戶端在只需要獲取響應的首部如Content-TypeContent-Length,而不需要響應體時,可以發送HEAD請求。

doHead()方法需要計算響應中的輸出位元組數,從而準確設置Content-Length首部。而如果你重寫了此方法,可以直接設置Content-Length,避免計算過程,從而提高性能。

重寫的doHead()應當是安全並且冪等的。

如果請求格式不正確,doHead()方法會返回一個錯誤資訊。

protected void doHead(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    if (DispatcherType.INCLUDE.equals(req.getDispatcherType())) {
        doGet(req, resp);
    } else {
        NoBodyResponse response = new NoBodyResponse(resp);
        doGet(req, response);
        response.setContentLength();
    }
}

8、doOptions()

doPotions()方法用於處理HTTP的OPTIONS請求。

客戶端發送OPTIONS請求向伺服器詢問其支援的HTTP請求方式,伺服器會將支援的所有請求方式通過Allow首部傳回。

例如,如果servlet只重寫了doGet()方法,那麼它的doOptions()方法會響應如下首部:Allow: GET, HEAD, TRACE, OPTIONS

一般情況下我們不需要重寫doOptions()方法,除非servlet實現了HTTP 1.1之外的新的HTTP請求方法。

doOptions()方法默認實現了返回伺服器所有請求方式的邏輯,其通過反射的方式獲取本類中重寫或者新增的方法,並將所有方法名添加到響應的Allow首部中。

需要注意的是,TRACEOPTIONS請求方式是默認支援的。

protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

    Method[] methods = getAllDeclaredMethods(this.getClass());

    boolean ALLOW_GET = false;
    boolean ALLOW_HEAD = false;
    boolean ALLOW_POST = false;
    boolean ALLOW_PUT = false;
    boolean ALLOW_DELETE = false;
    boolean ALLOW_TRACE = true;
    boolean ALLOW_OPTIONS = true;

    // Tomcat specific hack to see if TRACE is allowed
    Class<?> clazz = null;
    try {
        clazz = Class.forName("org.apache.catalina.connector.RequestFacade");
        Method getAllowTrace = clazz.getMethod("getAllowTrace", (Class<?>[]) null);
        ALLOW_TRACE = ((Boolean) getAllowTrace.invoke(req, (Object[]) null)).booleanValue();
    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException |
            IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        // Ignore. Not running on Tomcat. TRACE is always allowed.
    }
    // End of Tomcat specific hack

    for (int i=0; i<methods.length; i++) {
        Method m = methods[i];

        if (m.getName().equals("doGet")) {
            ALLOW_GET = true;
            ALLOW_HEAD = true;
        }
        if (m.getName().equals("doPost"))
            ALLOW_POST = true;
        if (m.getName().equals("doPut"))
            ALLOW_PUT = true;
        if (m.getName().equals("doDelete"))
            ALLOW_DELETE = true;
    }

    String allow = null;
    if (ALLOW_GET)
        allow=METHOD_GET;
    if (ALLOW_HEAD)
        if (allow==null) allow=METHOD_HEAD;
        else allow += ", " + METHOD_HEAD;
    if (ALLOW_POST)
        if (allow==null) allow=METHOD_POST;
        else allow += ", " + METHOD_POST;
    if (ALLOW_PUT)
        if (allow==null) allow=METHOD_PUT;
        else allow += ", " + METHOD_PUT;
    if (ALLOW_DELETE)
        if (allow==null) allow=METHOD_DELETE;
        else allow += ", " + METHOD_DELETE;
    if (ALLOW_TRACE)
        if (allow==null) allow=METHOD_TRACE;
        else allow += ", " + METHOD_TRACE;
    if (ALLOW_OPTIONS)
        if (allow==null) allow=METHOD_OPTIONS;
        else allow += ", " + METHOD_OPTIONS;

    resp.setHeader("Allow", allow);
}

9、doTrace()

doTrace()用於處理HTTP的TRACE請求。

TRACE的請求首部會保存在其響應首部中一起發回給客戶端,主要用於調試。

我們一般不必重寫doTrace()方法,它默認實現了該邏輯,基本步驟如下:

  1. 通過StringBuilder將請求中的URI、協議以及各種首部資訊按照格式寫入到TRACE首部中;
  2. 設置當前相應的相應首部:ContentTypeContentLength
  3. 輸出響應。
protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {

    int responseLength;

    String CRLF = "\r\n";
    StringBuilder buffer = new StringBuilder("TRACE ").append(req.getRequestURI())
        .append(" ").append(req.getProtocol());

    Enumeration<String> reqHeaderEnum = req.getHeaderNames();

    while( reqHeaderEnum.hasMoreElements() ) {
        String headerName = reqHeaderEnum.nextElement();
        buffer.append(CRLF).append(headerName).append(": ")
            .append(req.getHeader(headerName));
    }

    buffer.append(CRLF);

    responseLength = buffer.length();

    resp.setContentType("message/http");
    resp.setContentLength(responseLength);
    ServletOutputStream out = resp.getOutputStream();
    out.print(buffer.toString());
    out.close();
}

10、maybeSetLastModified()

maybeSetLASTModified()方法是一個私有方法,用於實現在必要時設置Last-Modified首部的操作。

必須在響應數據前調用此方法,因為HTTP首部必須在響應體前發送。

private void maybeSetLastModified(HttpServletResponse resp, long lastModified) {
    if (resp.containsHeader(HEADER_LASTMOD))
        return;
    if (lastModified >= 0)
        resp.setDateHeader(HEADER_LASTMOD, lastModified);
}

11、getLastModified()

getLastModified()方法返回HttpServletRequest對象的最後修改時間。如果不知道修改時間,該方法會返回-1(默認)。

支援HTTP的GET請求並且能夠快速確定其最後修改時間的servlet應該重寫此方法。這會提高瀏覽器或代理伺服器快取的工作效率,減少伺服器和網路資源的負載。

protected long getLastModified(HttpServletRequest req) {
    return -1;
}

12、sendMethodNotAllowed()

sendMethodNotAllowed()是一個私有方法,抽象了伺服器不支援相關請求,從而向客戶端發送錯誤報告的邏輯。

private void sendMethodNotAllowed(HttpServletRequest req, HttpServletResponse resp, String msg) throws IOException {
    String protocol = req.getProtocol();
    // Note: Tomcat reports "" for HTTP/0.9 although some implementations
    //       may report HTTP/0.9
    if (protocol.length() == 0 || protocol.endsWith("0.9") || protocol.endsWith("1.0")) {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
    } else {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
    }
}

13、getAllDeclaredMethods()

getAllDeclaredMethods()是一個私有的靜態方法,獲取所有當前servlet重寫、新增的方法。主要用於doOptions()方法中獲取servlet提供的服務。

private static Method[] getAllDeclaredMethods(Class<?> c) {

    if (c.equals(javax.servlet.http.HttpServlet.class)) {
        return null;
    }

    Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass());
    Method[] thisMethods = c.getDeclaredMethods();

    if ((parentMethods != null) && (parentMethods.length > 0)) {
        Method[] allMethods = new Method[parentMethods.length + thisMethods.length];
        System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length);
        System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length);

        thisMethods = allMethods;
    }

    return thisMethods;
}

5.4 內部類

HttpServlet抽象類中提供了NoBodyResponseNoBodyOutputStream兩個內部類,分別用於對沒有響應體的HTTP響應進行包裝和寫出。

1、NoBodyResponse

NoBodyResponse類對沒有響應體的響應進行了包裝,以便於自動計算響應的長度等。

class NoBodyResponse extends HttpServletResponseWrapper {
    private final NoBodyOutputStream noBody;
    private PrintWriter writer;
    private boolean didSetContentLength;
    
    // file private
    NoBodyResponse(HttpServletResponse r) {
        super(r);
        noBody = new NoBodyOutputStream(this);
    }

    // file private
    void setContentLength() {
        if (!didSetContentLength) {
            if (writer != null) {
                writer.flush();
            }
            super.setContentLength(noBody.getContentLength());
        }
    }


    // SERVLET RESPONSE interface methods

    @Override
    public void setContentLength(int len) {
        super.setContentLength(len);
        didSetContentLength = true;
    }

    @Override
    public void setContentLengthLong(long len) {
        super.setContentLengthLong(len);
        didSetContentLength = true;
    }

    @Override
    public void setHeader(String name, String value) {
        super.setHeader(name, value);
        checkHeader(name);
    }

    @Override
    public void addHeader(String name, String value) {
        super.addHeader(name, value);
        checkHeader(name);
    }

    @Override
    public void setIntHeader(String name, int value) {
        super.setIntHeader(name, value);
        checkHeader(name);
    }

    @Override
    public void addIntHeader(String name, int value) {
        super.addIntHeader(name, value);
        checkHeader(name);
    }

    private void checkHeader(String name) {
        if ("content-length".equalsIgnoreCase(name)) {
            didSetContentLength = true;
        }
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return noBody;
    }

    @Override
    public PrintWriter getWriter() throws UnsupportedEncodingException {

        if (writer == null) {
            OutputStreamWriter w;

            w = new OutputStreamWriter(noBody, getCharacterEncoding());
            writer = new PrintWriter(w);
        }
        return writer;
    }
}

2、NoBodyOutputStream

NoBodyOutputStream對輸出流進行了包裝,濾過了所有數據,用於對沒有響應體的響應進行輸出。

class NoBodyOutputStream extends ServletOutputStream {

    private static final String LSTRING_FILE =
        "javax.servlet.http.LocalStrings";
    private static final ResourceBundle lStrings =
        ResourceBundle.getBundle(LSTRING_FILE);

    private final HttpServletResponse response;
    private boolean flushed = false;
    private int contentLength = 0;

    // file private
    NoBodyOutputStream(HttpServletResponse response) {
        this.response = response;
    }

    // file private
    int getContentLength() {
        return contentLength;
    }

    @Override
    public void write(int b) throws IOException {
        contentLength++;
        checkCommit();
    }

    @Override
    public void write(byte buf[], int offset, int len) throws IOException {
        if (buf == null) {
            throw new NullPointerException(
                    lStrings.getString("err.io.nullArray"));
        }

        if (offset < 0 || len < 0 || offset+len > buf.length) {
            String msg = lStrings.getString("err.io.indexOutOfBounds");
            Object[] msgArgs = new Object[3];
            msgArgs[0] = Integer.valueOf(offset);
            msgArgs[1] = Integer.valueOf(len);
            msgArgs[2] = Integer.valueOf(buf.length);
            msg = MessageFormat.format(msg, msgArgs);
            throw new IndexOutOfBoundsException(msg);
        }

        contentLength += len;
        checkCommit();
    }

    @Override
    public boolean isReady() {
        // TODO SERVLET 3.1
        return false;
    }

    @Override
    public void setWriteListener(javax.servlet.WriteListener listener) {
        // TODO SERVLET 3.1
    }

    private void checkCommit() throws IOException {
        if (!flushed && contentLength > response.getBufferSize()) {
            response.flushBuffer();
            flushed = true;
        }
    }
}
Tags: