gembin

          OSGi, Eclipse Equinox, ECF, Virgo, Gemini, Apache Felix, Karaf, Aires, Camel, Eclipse RCP

          HBase, Hadoop, ZooKeeper, Cassandra

          Flex4, AS3, Swiz framework, GraniteDS, BlazeDS etc.

          There is nothing that software can't fix. Unfortunately, there is also nothing that software can't completely fuck up. That gap is called talent.

          About Me

           

          Apache Common fileUpload API 詳解

          文件上傳組件的應(yīng)用與編寫
          在許多Web站點應(yīng)用中都需要為用戶提供通過瀏覽器上傳文檔資料的功能,例如,上傳郵件附件、個人相片、共享資料等。對文件上傳功能,在

          瀏覽器端提供了較好的支持,只要將FORM表單的enctype屬性設(shè)置為“multipart/form-data”即可;但在Web服務(wù)器端如何獲取瀏覽器上傳的文

          件,需要進行復(fù)雜的編程處理。為了簡化和幫助Web開發(fā)人員接收瀏覽器上傳的文件,一些公司和組織專門開發(fā)了文件上傳組件。本章將詳細介

          紹如何使用Apache文件上傳組件,以及分析該組件源程序的設(shè)計思路和實現(xiàn)方法。
          1.1 準備實驗環(huán)境
          按下面的步驟為本章的例子程序建立運行環(huán)境:
          (1)在Tomcat 5.5.12的<tomcat的安裝目錄>\webapps目錄中創(chuàng)建一個名為fileupload的子目錄,并在fileupload目錄中創(chuàng)建一個名為test.html

          的網(wǎng)頁文件,在該文件中寫上“這是test.html頁面的原始內(nèi)容!”這幾個字符。
          (2)在<tomcat的安裝目錄>\webapps\fileupload目錄中創(chuàng)建一個名為WEB-INF的子目錄,在WEB-INF目錄中創(chuàng)建一個名為classes的子目錄和一個

          web.xml文件,web.xml文件內(nèi)容如下:
          <web-app>
          </web-app>
          (3)要使用Apache文件上傳組件,首先需要安裝Apache文件上傳組件包。在<tomcat的安裝目錄>\webapps\fileupload\WEB-INF目錄中創(chuàng)建一個

          名為lib的子目錄,然后從網(wǎng)址http://jakarta.apache.org/commons/fileupload下載到Apache組件的二進制發(fā)行包,在本書的附帶帶光盤中也

          提供了該組件的二進制發(fā)行包,文件名為commons-fileupload-1.0.zip。從commons-fileupload-1.0.zip壓縮包中解壓出commons-fileupload

          -1.0.jar文件,將它放置進<tomcat的安裝目錄>\webapps\fileupload\WEB-INF\lib目錄中,就完成了Apache文件上傳組件的安裝。
          (4)在<tomcat的安裝目錄>\webapps\fileupload目錄中創(chuàng)建一個名為src的子目錄,src目錄用于放置本章編寫的Java源程序。為了便于對

          Servlet源文件進行編譯,在src目錄中編寫一個compile.bat批處理文件,如例程1-1所示。
          例程1-1 compile.bat



          set PATH=C:\jdk1.5.0_01\bin;%path%
          set CLASSPATH=C:\tomcat-5.5.12\common\lib\servlet-api.jar;C:\tomcat-5.5.12\\webapps\
          fileupload\WEB-INF\lib\commons-fileupload-1.0.jar;%CLASSPATH%
          javac -d ..\WEB-INF\classes %1
          pause



          在compile.bat批處理文件中要注意將commons-fileupload-1.0.jar文件的路徑加入到CLASSPATH環(huán)境變量中和確保編譯后生成的class文件存放

          到<tomcat安裝目錄>\webapps\fileupload\WEB-INF\classes目錄中,上面的CLASSPATH環(huán)境變量的設(shè)置值由于排版原因進行了換行,實際上不

          應(yīng)該有換行。接著在src目錄中為compile.bat文件創(chuàng)建一個快捷方式,以后只要在Windows資源管理器窗口中將Java源文件拖動到compile.bat

          文件的快捷方式上,就可以完成Java源程序的編譯了。之所以要創(chuàng)建compile.bat文件的快捷方式,是因為直接將Java源程序拖動到

          compile.bat批處理文件時,compile.bat批處理文件內(nèi)編寫的相對路徑不被支持。創(chuàng)建完的fileupload目錄中的文件結(jié)構(gòu)如圖1.1所示。
          圖1.1
          (4)啟動Tomcat,在本地計算機的瀏覽器地址欄中輸入如下地址:
          http://localhost:8080/fileupload/test.html
          驗證瀏覽器能夠成功到該網(wǎng)頁文檔。如果瀏覽器無法訪問到該網(wǎng)頁文檔,請檢查前面的操作步驟和改正問題,直到瀏覽器能夠成功到該網(wǎng)頁文

          檔為止。
          (5)為了讓/fileupload這個WEB應(yīng)用程序能自動重新裝載發(fā)生了修改的Servlet程序,需要修改Tomcat的server.xml文件,在該文件的<Host>元

          素中增加如下一個<Context>子元素:
          <Context path="/fileupload" docBase="fileupload" reloadable="true"/>
          保存server.xml文件后,重新啟動Tomcat。

          1.2 Apache文件上傳組件的應(yīng)用
          Java Web開發(fā)人員可以使用Apache文件上傳組件來接收瀏覽器上傳的文件,該組件由多個類共同組成,但是,對于使用該組件來編寫文件上傳

          功能的Java Web開發(fā)人員來說,只需要了解和使用其中的三個類:DiskFileUpload、FileItem和FileUploadException。這三個類全部位于

          org.apache.commons.fileupload包中。
          1.2.1查看API文檔
          在準備實驗環(huán)境時獲得的commons-fileupload-1.0.zip文件的解壓縮目錄中可以看到一個docs的子目錄,其中包含了Apache文件上傳組件中的

          各個API類的幫助文檔,從這個文檔中可以了解到各個API類的使用幫助信息。打開文件上傳組件API幫助文檔中的index.html頁面,在左側(cè)分欄

          窗口頁面中列出了文件上傳組件中的各個API類的名稱,在右側(cè)分欄窗口頁面的底部列出了一段示例代碼,如圖1.2所示。
          圖1.2
          讀者不需要逐個去閱讀圖1.2中列出的各個API類的幫助文檔,而應(yīng)該以圖1.2中的示例代碼為線索,以其中所使用到的類為入口點,按圖索驥地

          進行閱讀,對于示例代碼中調(diào)用到的各個API類的方法則應(yīng)重點掌握。
          1.2.2 DiskFileUpload類
          DiskFileUpload類是Apache文件上傳組件的核心類,應(yīng)用程序開發(fā)人員通過這個類來與Apache文件上傳組件進行交互。下面介紹

          DiskFileUpload類中的幾個常用的重要方法。
          1.setSizeMax方法
          setSizeMax方法用于設(shè)置請求消息實體內(nèi)容的最大允許大小,以防止客戶端故意通過上傳特大的文件來塞滿服務(wù)器端的存儲空間,單位為字節(jié)

          。其完整語法定義如下:
          public void setSizeMax(long sizeMax)
          如果請求消息中的實體內(nèi)容的大小超過了setSizeMax方法的設(shè)置值,該方法將會拋出FileUploadException異常。
          2.setSizeThreshold方法
          Apache文件上傳組件在解析和處理上傳數(shù)據(jù)中的每個字段內(nèi)容時,需要臨時保存解析出的數(shù)據(jù)。因為Java虛擬機默認可以使用的內(nèi)存空間是有

          限的(筆者測試不大于100M),超出限制時將會發(fā)生“java.lang.OutOfMemoryError”錯誤,如果上傳的文件很大,例如上傳800M的文件,在

          內(nèi)存中將無法保存該文件內(nèi)容,Apache文件上傳組件將用臨時文件來保存這些數(shù)據(jù);但如果上傳的文件很小,例如上傳600個字節(jié)的文件,顯然

          將其直接保存在內(nèi)存中更加有效。setSizeThreshold方法用于設(shè)置是否使用臨時文件保存解析出的數(shù)據(jù)的那個臨界值,該方法傳入的參數(shù)的單

          位是字節(jié)。其完整語法定義如下:
          public void setSizeThreshold(int sizeThreshold)
          3. setRepositoryPath方法
          setRepositoryPath方法用于設(shè)置setSizeThreshold方法中提到的臨時文件的存放目錄,這里要求使用絕對路徑。其完整語法定義如下:
          public void setRepositoryPath(String repositoryPath)
          如果不設(shè)置存放路徑,那么臨時文件將被儲存在"java.io.tmpdir"這個JVM環(huán)境屬性所指定的目錄中,tomcat 5.5.9將這個屬性設(shè)置為了

          “<tomcat安裝目錄>/temp/”目錄。
          4. parseRequest方法
          parseRequest 方法是DiskFileUpload類的重要方法,它是對HTTP請求消息進行解析的入口方法,如果請求消息中的實體內(nèi)容的類型不是

          “multipart/form-data”,該方法將拋出FileUploadException異常。parseRequest 方法解析出FORM表單中的每個字段的數(shù)據(jù),并將它們分別

          包裝成獨立的FileItem對象,然后將這些FileItem對象加入進一個List類型的集合對象中返回。parseRequest 方法的完整語法定義如下:
          public List parseRequest(HttpServletRequest req)
          parseRequest 方法還有一個重載方法,該方法集中處理上述所有方法的功能,其完整語法定義如下:
          parseRequest(HttpServletRequest req,int sizeThreshold,long sizeMax,
          String path)
          這兩個parseRequest方法都會拋出FileUploadException異常。
          5. isMultipartContent方法
          isMultipartContent方法方法用于判斷請求消息中的內(nèi)容是否是“multipart/form-data”類型,是則返回true,否則返回false。

          isMultipartContent方法是一個靜態(tài)方法,不用創(chuàng)建DiskFileUpload類的實例對象即可被調(diào)用,其完整語法定義如下:
          public static final boolean isMultipartContent(HttpServletRequest req)
          6. setHeaderEncoding方法
          由于瀏覽器在提交FORM表單時,會將普通表單中填寫的文本內(nèi)容傳遞給服務(wù)器,對于文件上傳字段,除了傳遞原始的文件內(nèi)容外,還要傳遞其

          文件路徑名等信息,如后面的圖1.3所示。不管FORM表單采用的是“application/x-www-form-urlencoded”編碼,還是“multipart/form-data

          ”編碼,它們僅僅是將各個FORM表單字段元素內(nèi)容組織到一起的一種格式,而這些內(nèi)容又是由某種字符集編碼來表示的。關(guān)于瀏覽器采用何種

          字符集來編碼FORM表單字段中的內(nèi)容,請參看筆者編著的《深入體驗java Web開發(fā)內(nèi)幕——核心基礎(chǔ)》一書中的第6.9.2的講解,

          “multipart/form-data”類型的表單為表單字段內(nèi)容選擇字符集編碼的原理和方式與“application/x-www-form-urlencoded”類型的表單是

          相同的。FORM表單中填寫的文本內(nèi)容和文件上傳字段中的文件路徑名在內(nèi)存中就是它們的某種字符集編碼的字節(jié)數(shù)組形式,Apache文件上傳組

          件在讀取這些內(nèi)容時,必須知道它們所采用的字符集編碼,才能將它們轉(zhuǎn)換成正確的字符文本返回。
          對于瀏覽器上傳給WEB服務(wù)器的各個表單字段的描述頭內(nèi)容,Apache文件上傳組件都需要將它們轉(zhuǎn)換成字符串形式返回,setHeaderEncoding 方

          法用于設(shè)置轉(zhuǎn)換時所使用的字符集編碼,其原理與筆者編著的《深入體驗java Web開發(fā)內(nèi)幕——核心基礎(chǔ)》一書中的第6.9.4節(jié)講解的

          ServletRequest.setCharacterEncoding方法相同。setHeaderEncoding 方法的完整語法定義如下:
          public void setHeaderEncoding(String encoding)
          其中,encoding參數(shù)用于指定將各個表單字段的描述頭內(nèi)容轉(zhuǎn)換成字符串時所使用的字符集編碼。
          注意:如果讀者在使用Apache文件上傳組件時遇到了中文字符的亂碼問題,一般都是沒有正確調(diào)用setHeaderEncoding方法的原因。
          1.2.3 FileItem類
          FileItem類用來封裝單個表單字段元素的數(shù)據(jù),一個表單字段元素對應(yīng)一個FileItem對象,通過調(diào)用FileItem對象的方法可以獲得相關(guān)表單字

          段元素的數(shù)據(jù)。FileItem是一個接口,在應(yīng)用程序中使用的實際上是該接口一個實現(xiàn)類,該實現(xiàn)類的名稱并不重要,程序可以采用FileItem接

          口類型來對它進行引用和訪問,為了便于講解,這里將FileItem實現(xiàn)類稱之為FileItem類。FileItem類還實現(xiàn)了Serializable接口,以支持序

          列化操作。
          對于“multipart/form-data”類型的FORM表單,瀏覽器上傳的實體內(nèi)容中的每個表單字段元素的數(shù)據(jù)之間用字段分隔界線進行分割,兩個分隔

          界線間的內(nèi)容稱為一個分區(qū),每個分區(qū)中的內(nèi)容可以被看作兩部分,一部分是對表單字段元素進行描述的描述頭,另外一部是表單字段元素的

          主體內(nèi)容,如圖1.3所示。
          圖 1.3
          主體部分有兩種可能性,要么是用戶填寫的表單內(nèi)容,要么是文件內(nèi)容。FileItem類對象實際上就是對圖1.3中的一個分區(qū)的數(shù)據(jù)進行封裝的對

          象,它內(nèi)部用了兩個成員變量來分別存儲描述頭和主體內(nèi)容,其中保存主體內(nèi)容的變量是一個輸出流類型的對象。當主體內(nèi)容的大小小于

          DiskFileUpload.setSizeThreshold方法設(shè)置的臨界值大小時,這個流對象關(guān)聯(lián)到一片內(nèi)存,主體內(nèi)容將會被保存在內(nèi)存中。當主體內(nèi)容的數(shù)據(jù)

          超過DiskFileUpload.setSizeThreshold方法設(shè)置的臨界值大小時,這個流對象關(guān)聯(lián)到硬盤上的一個臨時文件,主體內(nèi)容將被保存到該臨時文件

          中。臨時文件的存儲目錄由DiskFileUpload.setRepositoryPath方法設(shè)置,臨時文件名的格式為“upload_00000005(八位或八位以上的數(shù)字)

          .tmp”這種形式,F(xiàn)ileItem類內(nèi)部提供了維護臨時文件名中的數(shù)值不重復(fù)的機制,以保證了臨時文件名的唯一性。當應(yīng)用程序?qū)⒅黧w內(nèi)容保存

          到一個指定的文件中時,或者在FileItem對象被垃圾回收器回收時,或者Java虛擬機結(jié)束時,Apache文件上傳組件都會嘗試刪除臨時文件,以

          盡量保證臨時文件能被及時清除。
          下面介紹FileItem類中的幾個常用的方法:
          1. isFormField方法
          isFormField方法用于判斷FileItem類對象封裝的數(shù)據(jù)是否屬于一個普通表單字段,還是屬于一個文件表單字段,如果是普通表單字段則返回

          true,否則返回false。該方法的完整語法定義如下:
          public boolean isFormField()
          2. getName方法
          getName方法用于獲得文件上傳字段中的文件名,對于圖1.3中的第三個分區(qū)所示的描述頭,getName方法返回的結(jié)果為字符串“C:\bg.gif”。

          如果FileItem類對象對應(yīng)的是普通表單字段,getName方法將返回null。即使用戶沒有通過網(wǎng)頁表單中的文件字段傳遞任何文件,但只要設(shè)置了

          文件表單字段的name屬性,瀏覽器也會將文件字段的信息傳遞給服務(wù)器,只是文件名和文件內(nèi)容部分都為空,但這個表單字段仍然對應(yīng)一個

          FileItem對象,此時,getName方法返回結(jié)果為空字符串"",讀者在調(diào)用Apache文件上傳組件時要注意考慮這個情況。getName方法的完整語法

          定義如下:
          public String getName()
          注意:如果用戶使用Windows系統(tǒng)上傳文件,瀏覽器將傳遞該文件的完整路徑,如果用戶使用Linux或者Unix系統(tǒng)上傳文件,瀏覽器將只傳遞該

          文件的名稱部分。
          3.getFieldName方法
          getFieldName方法用于返回表單字段元素的name屬性值,也就是返回圖1.3中的各個描述頭部分中的name屬性值,例如“name=p1”中的“p1”

          。getFieldName方法的完整語法定義如下:
          public String getFieldName()
          4. write方法
          write方法用于將FileItem對象中保存的主體內(nèi)容保存到某個指定的文件中。如果FileItem對象中的主體內(nèi)容是保存在某個臨時文件中,該方法

          順利完成后,臨時文件有可能會被清除。該方法也可將普通表單字段內(nèi)容寫入到一個文件中,但它主要用途是將上傳的文件內(nèi)容保存在本地文

          件系統(tǒng)中。其完整語法定義如下:
          public void write(File file)
          5.getString方法
          getString方法用于將FileItem對象中保存的主體內(nèi)容作為一個字符串返回,它有兩個重載的定義形式:
          public java.lang.String getString()
          public java.lang.String getString(java.lang.String encoding)
          throws java.io.UnsupportedEncodingException
          前者使用缺省的字符集編碼將主體內(nèi)容轉(zhuǎn)換成字符串,后者使用參數(shù)指定的字符集編碼將主體內(nèi)容轉(zhuǎn)換成字符串。如果在讀取普通表單字段元

          素的內(nèi)容時出現(xiàn)了中文亂碼現(xiàn)象,請調(diào)用第二個getString方法,并為之傳遞正確的字符集編碼名稱。
          6. getContentType方法
          getContentType 方法用于獲得上傳文件的類型,對于圖1.3中的第三個分區(qū)所示的描述頭,getContentType方法返回的結(jié)果為字符串

          “image/gif”,即“Content-Type”字段的值部分。如果FileItem類對象對應(yīng)的是普通表單字段,該方法將返回null。getContentType 方法

          的完整語法定義如下:
          public String getContentType()
          7. isInMemory方法
          isInMemory方法用來判斷FileItem類對象封裝的主體內(nèi)容是存儲在內(nèi)存中,還是存儲在臨時文件中,如果存儲在內(nèi)存中則返回true,否則返回

          false。其完整語法定義如下:
          public boolean isInMemory()
          8. delete方法
          delete方法用來清空FileItem類對象中存放的主體內(nèi)容,如果主體內(nèi)容被保存在臨時文件中,delete方法將刪除該臨時文件。盡管Apache組件

          使用了多種方式來盡量及時清理臨時文件,但系統(tǒng)出現(xiàn)異常時,仍有可能造成有的臨時文件被永久保存在了硬盤中。在有些情況下,可以調(diào)用

          這個方法來及時刪除臨時文件。其完整語法定義如下:
          public void delete()
          1.2.4 FileUploadException類
          在文件上傳過程中,可能發(fā)生各種各樣的異常,例如網(wǎng)絡(luò)中斷、數(shù)據(jù)丟失等等。為了對不同異常進行合適的處理,Apache文件上傳組件還開發(fā)

          了四個異常類,其中FileUploadException是其他異常類的父類,其他幾個類只是被間接調(diào)用的底層類,對于Apache組件調(diào)用人員來說,只需對

          FileUploadException異常類進行捕獲和處理即可。

          1.2.5 文件上傳編程實例
          下面參考圖1.2中看到的示例代碼編寫一個使用Apache文件上傳組件來上傳文件的例子程序。
          :動手體驗:使用Apache文件上傳組件
          (1)在<tomcat安裝目錄>\webapps\fileupload目錄中按例程1-1編寫一個名為FileUpload.html的HTML頁面,該頁面用于提供文件上傳的FORM

          表單,表單的enctype屬性設(shè)置值為“multipart/form-data”,表單的action屬性設(shè)置為“servlet/UploadServlet”。
          例程1-1 FileUpload.html



          <html>
          <head>
          <title>upload experiment</title>
          <meta http-equiv="Content-Type" content="text/html; charset=gb2312">
          </head>
          <body>
          <h3>測試文件上傳組件的頁面</h3>
          <form action="servlet/UploadServlet"
          enctype="multipart/form-data" method="post">
          作者:<input type="text" name="author"><br>
          來自:<input type="text" name="company"><br>
          文件1:<input type="file" name="file1"><br>
          文件2:<input type="file" name="file2"><br>
          <input type="submit" value="上載">
          </form>
          </body>
          </html>



          (2)在<tomcat的安裝目錄>\webapps\fileupload\src目錄中按例程1-2創(chuàng)建一個名為UploadServlet.java的Servlet程序,UploadServlet.java

          調(diào)用Apache文件上傳組件來處理FORM表單提交的文件內(nèi)容和普通字段數(shù)據(jù)。
          例程1-2 UploadServlet.java



          import java.io.*;
          import javax.servlet.*;
          import javax.servlet.http.*;
          import org.apache.commons.fileupload.*;
          import java.util.*;

          public class UploadServlet extends HttpServlet
          {
          public void doPost(HttpServletRequest request,
          HttpServletResponse response) throws ServletException,IOException
          {
          response.setContentType("text/html;charset=gb2312");
          PrintWriter out = response.getWriter();

          //設(shè)置保存上傳文件的目錄
          String uploadDir = getServletContext().getRealPath("/upload");
          if (uploadDir == null)
          {
          out.println("無法訪問存儲目錄!");
          return;
          }
          File fUploadDir = new File(uploadDir);
          if(!fUploadDir.exists())
          {
          if(!fUploadDir.mkdir())
          {
          out.println("無法創(chuàng)建存儲目錄!");
          return;
          }
          }

          if (!DiskFileUpload.isMultipartContent(request))
          {
          out.println("只能處理multipart/form-data類型的數(shù)據(jù)!");
          return ;
          }

          DiskFileUpload fu = new DiskFileUpload();
          //最多上傳200M數(shù)據(jù)
          fu.setSizeMax(1024 * 1024 * 200);
          //超過1M的字段數(shù)據(jù)采用臨時文件緩存
          fu.setSizeThreshold(1024 * 1024);
          //采用默認的臨時文件存儲位置
          //fu.setRepositoryPath(...);
          //設(shè)置上傳的普通字段的名稱和文件字段的文件名所采用的字符集編碼
          fu.setHeaderEncoding("gb2312");

          //得到所有表單字段對象的集合
          List fileItems = null;
          try
          {
          fileItems = fu.parseRequest(request);
          }
          catch (FileUploadException e)
          {
          out.println("解析數(shù)據(jù)時出現(xiàn)如下問題:");
          e.printStackTrace(out);
          return;
          }

          //處理每個表單字段
          Iterator i = fileItems.iterator();
          while (i.hasNext())
          {
          FileItem fi = (FileItem) i.next();
          if (fi.isFormField())
          {
          String content = fi.getString("GB2312");
          String fieldName = fi.getFieldName();
          request.setAttribute(fieldName,content);
          }
          else
          {
          try
          {
          String pathSrc = fi.getName();
          /*如果用戶沒有在FORM表單的文件字段中選擇任何文件,
          那么忽略對該字段項的處理*/
          if(pathSrc.trim().equals(""))
          {
          continue;
          }
          int start = pathSrc.lastIndexOf('\\');
          String fileName = pathSrc.substring(start + 1);
          File pathDest = new File(uploadDir, fileName);

          fi.write(pathDest);
          String fieldName = fi.getFieldName();
          request.setAttribute(fieldName, fileName);
          }
          catch (Exception e)
          {
          out.println("存儲文件時出現(xiàn)如下問題:");
          e.printStackTrace(out);
          return;
          }
          finally //總是立即刪除保存表單字段內(nèi)容的臨時文件
          {
          fi.delete();
          }

          }
          }

          //顯示處理結(jié)果
          out.println("用戶:" + request.getAttribute("author") + "<br>");
          out.println("來自:" + request.getAttribute("company") + "<br>");

          /*將上傳的文件名組合成"file1,file2"這種形式顯示出來,如果沒有上傳
          *任何文件,則顯示為"無",如果只上傳了第二個文件,顯示為"file2"。*/
          StringBuffer filelist = new StringBuffer();
          String file1 = (String)request.getAttribute("file1");
          makeUpList(filelist,file1);
          String file2 = (String)request.getAttribute("file2");
          makeUpList(filelist,file2);
          out.println("成功上傳的文件:" +
          (filelist.length()==0 ? "無" : filelist.toString()));

          }

          /**
          *將一段字符串追加到一個結(jié)果字符串中。如果結(jié)果字符串的初始內(nèi)容不為空,
          *在追加當前這段字符串之前先最加一個逗號(,)。在組合sql語句的查詢條件時,
          *經(jīng)常要用到類似的方法,第一條件前沒有"and",而后面的條件前都需要用"and"
          *作連詞,如果沒有選擇第一個條件,第二個條件就變成第一個,依此類推。
          *
          *@param result 要將當前字符串追加進去的結(jié)果字符串
          *@param fragment 當前要追加的字符串
          */
          private void makeUpList(StringBuffer result,String fragment)
          {
          if(fragment != null)
          {
          if(result.length() != 0)
          {
          result.append(",");
          }
          result.append(fragment);
          }
          }
          }



          在Windows資源管理器窗口中將UploadServlet.java源文件拖動到compile.bat文件的快捷方式上進行編譯,修改Javac編譯程序報告的錯誤,直

          到編譯成功通過為止。
          (3)修改<tomcat的安裝目錄>\webapps\fileupload\WEB-INF\classes\web.xml文件,在其中注冊和映射UploadServlet的訪問路徑,如例程1-3

          所示。
          例程1-3 web.xml



          <web-app>
          <servlet>
          <servlet-name>UploadServlet</servlet-name>
          <servlet-class>UploadServlet</servlet-class>
          </servlet>

          <servlet-mapping>
          <servlet-name>UploadServlet</servlet-name>
          <url-pattern>/servlet/UploadServlet</url-pattern>
          </servlet-mapping>
          </web-app>
          (4)重新啟動Tomcat,并在瀏覽器地址欄中輸入如下地址:
          http://localhost:8080/fileupload/FileUpload.html
          填寫返回頁面中的FORM表單,如圖1.4所示,單擊“上載”按鈕后,瀏覽器返回的頁面信息如圖1.5所示。
          圖1.4
          圖1.5(這些圖的標題欄中的it315改為fileupload)
          查看<tomcat安裝目錄>\webapps\it315\upload目錄,可以看到剛才上傳的兩個文件。
          (4)單擊瀏覽器工具欄上的“后退”按鈕回到表單填寫頁面,只在第二個文件字段中選擇一個文件,單擊“上載”按鈕,瀏覽器返回的顯示結(jié)果

          如圖1.6所示。
          圖1.6
          M腳下留心:
          上面編寫的Servlet程序?qū)⑸蟼鞯奈募4嬖诹水斍癢EB應(yīng)用程序下面的upload目錄中,這個目錄是客戶端瀏覽器可以訪問到的目錄。如果用戶

          通過瀏覽器上傳了一個名稱為test.jsp的文件,那么用戶接著就可以在瀏覽器中訪問這個test.jsp文件了,對于本地瀏覽器來說,這個jsp文件

          的訪問URL地址如下所示:
          http://localhost:8080/fileupload/upload/test.jsp
          對于遠程客戶端瀏覽器而言,只需要將上面的url地址中的localhost改寫為Tomcat服務(wù)器的主機名或IP地址即可。用戶可以通過上面的Servlet

          程序來上傳自己編寫的jsp文件,然后又可以通過瀏覽器來訪問這個jsp文件,如果用戶在jsp文件中編寫一些有害的程序代碼,例如,查看服務(wù)

          器上的所有目錄結(jié)構(gòu),調(diào)用服務(wù)器上的操作系統(tǒng)進程等等,這將是一個非常致命的安全漏洞和隱患,這臺服務(wù)器對外就沒有任何安全性可言了



          1.3 Apache文件上傳組件的源碼賞析
          經(jīng)常閱讀一些知名的開源項目的源代碼,可以幫助我們開闊眼界和快速提高編程能力。Apache文件上傳組件是Apache組織開發(fā)的一個開源項目

          ,從網(wǎng)址http://jakarta.apache.org/commons/fileupload可以下載到Apache組件的源程序包,在本書的附帶帶光盤中也提供了該組件的源程

          序包,文件名為commons-fileupload-1.0-src.zip。該組件的設(shè)計思想和程序編碼細節(jié)包含有許多值得借鑒的技巧,為了便于有興趣的讀者學(xué)

          習(xí)和研究該組件的源碼,本節(jié)將分析Apache文件上傳組件的源代碼實現(xiàn)。對于只想了解如何使用Apache文件上傳組件來上傳文件的讀者來說,

          不必學(xué)習(xí)本節(jié)的內(nèi)容。在學(xué)習(xí)本節(jié)內(nèi)容之前,讀者需要仔細學(xué)習(xí)了筆者編著的《深入體驗java Web開發(fā)內(nèi)幕——核心基礎(chǔ)》一書中的第6.7.2節(jié)

          中講解的“分析文件上傳的請求消息結(jié)構(gòu)”的知識。
          1.3.1 Apache文件上傳組的類工作關(guān)系
          Apache文件上傳組件總共由兩個接口,十二個類組成。在Apache文件上傳組件的十二個類中,有兩個抽象類,四個的異常類,六個主要類,其

          中FileUpLoad類用暫時沒有應(yīng)用,是為了以后擴展而保留的。Apache文件上傳組件中的各個類的關(guān)系如圖1.7所示,圖中省略了異常類。
          圖 1.7

          DiskFileUpload類是文件上傳組件的核心類,它是一個總的控制類,首先由Apache文件上傳組件的使用者直接調(diào)用DiskFileUpload類的方法,

          DiskFileUpload類再調(diào)用和協(xié)調(diào)更底層的類來完成具體的功能。解析類MultipartStream和工廠類DefaultFileItemFactory就是DiskFileUpload

          類調(diào)用的兩個的底層類。MultipartStream類用于對請求消息中的實體數(shù)據(jù)進行具體解析,DefaultFileItemFactory類對MultipartStream類解

          析出來的數(shù)據(jù)進行封裝,它將每個表單字段數(shù)據(jù)封裝成一個個的FileItem類對象,用戶通過FileItem類對象來獲得相關(guān)表單字段的數(shù)據(jù)。
          DefaultFileItem是FileItem接口的實現(xiàn)類,實現(xiàn)了FileItem接口中定義的功能,用戶只需關(guān)心FileItem接口,通過FileItem接口來使用

          DefaultFileItem類實現(xiàn)的功能。DefaultFileItem類使用了兩個成員變量來分別存儲表單字段數(shù)據(jù)的描述頭和主體內(nèi)容,其中保存主體內(nèi)容的

          變量類型為DeferredFileOutputStream類。DeferredFileOutputStream類是一個輸出流類型,在開始時,DeferredFileOutputStream類內(nèi)部使

          用一個ByteArrayOutputStream類對象來存儲數(shù)據(jù),當寫入它里面的主體內(nèi)容的大小大于DiskFileUpload.setSizeThreshold方法設(shè)置的臨界值

          時,DeferredFileOutputStream類內(nèi)部創(chuàng)建一個文件輸出流對象來存儲數(shù)據(jù),并將前面寫入到ByteArrayOutputStream類對象中的數(shù)據(jù)轉(zhuǎn)移到文

          件輸出流對象中。這個文件輸出流對象關(guān)聯(lián)的文件是一個臨時文件,它的保存路徑由DiskFileUpload.setRepositoryPath方法指定。
          Apache文件上傳組件的處理流程如圖1.8所示。
          圖1.8
          圖1.8中的每一步驟的詳細解釋如下:
          (1)Web容器接收用戶的HTTP請求消息,創(chuàng)建request請求對象。
          (2)調(diào)用DiskFileUpload類對象的parseRequest方法對request請求對象進行解析。該方法首先檢查request請求對象中的數(shù)據(jù)內(nèi)容是否是

          “multipart/form-data”類型,如果是,該方法則創(chuàng)建MultipartStream類對象對request請求對象中的請求體 進行解析。
          (3)MultipartStream類對象對request請求體進行解析,并返回解析出的各個表單字段元素對應(yīng)的內(nèi)容。
          (4)DiskFileUpload類對象的parseRequest方法接著創(chuàng)建DefaultFileItemFactory類對象,用來將MultipartStream類對象解析出的每個表單

          字段元素的數(shù)據(jù)封裝成FileItem類對象。
          (5)DefaultFileItemFactory工廠類對象把MultipartStream類對象解析出的各個表單字段元素的數(shù)據(jù)封裝成若干DefaultFileItem類對象,然

          后加入到一個List類型的集合對象中,parseRequest方法返回該List集合對象。
          實際上,步驟(3)和步驟(5)是交替同步進行的,即在MultipartStream類對象解析每個表單字段元素時,都會調(diào)用DefaultFileItemFactory

          工廠類把該表單字段元素封裝成對應(yīng)的FileItem類對象。

          1.3.2 Apache文件上傳組件的核心編程問題
          WEB服務(wù)器端程序接收到“multipart/form-data”類型的HTTP請求消息后,其核心和基本的編程工作就是讀取請求消息中的實體內(nèi)容,然后解

          析出每個分區(qū)的數(shù)據(jù),接著再從每個分區(qū)中解析出描述頭和主體內(nèi)容部分。
          在讀取HTTP請求消息中的實體內(nèi)容時,只能調(diào)用HttpServletRequest.getInputStream方法返回的字節(jié)輸入流,而不能調(diào)用

          HttpServletRequest.getReader方法返回的字符輸入流,因為不管上傳的文件類型是文本的、還是其他各種格式的二進制內(nèi)容,WEB服務(wù)器程序

          要做的工作就是將屬于文件內(nèi)容的那部分數(shù)據(jù)原封不動地提取出來,然后原封不動地存儲到本地文件系統(tǒng)中。如果使用

          HttpServletRequest.getReader方法返回的字符輸入流對象來讀取HTTP請求消息中的實體內(nèi)容,它將HTTP請求消息中的字節(jié)數(shù)據(jù)轉(zhuǎn)換成字符后

          再返回,這主要是為了方便要以文本方式來處理本來就全是文本內(nèi)容的請求消息的應(yīng)用,但本程序要求的是“原封不動”,顯然不能使用

          HttpServletRequest.getReader方法返回的字符輸入流對象來進行讀取。
          另外,不能期望用一個很大的字節(jié)數(shù)組就可以裝進HTTP請求消息中的所有實體內(nèi)容,因為程序中定義的字節(jié)數(shù)組大小總是有限制的,但應(yīng)該允

          許客戶端上傳超過這個字節(jié)數(shù)組大小的實體內(nèi)容。所以,只能創(chuàng)建一個一般大小的字節(jié)數(shù)組緩沖區(qū)來逐段讀取請求消息中的實體內(nèi)容,讀取一

          段就處理一段,處理完上一段以后,再讀取下一段,如此循環(huán),直到處理完所有的實體內(nèi)容,如圖1.9所示。
          圖 1.9
          在圖1.9中,buffer即為用來逐段讀取請求消息中的實體內(nèi)容的字節(jié)數(shù)組緩沖區(qū)。因為讀取到緩沖區(qū)中的數(shù)據(jù)處理完后就會被拋棄,確切地說,

          是被下一段數(shù)據(jù)覆蓋,所以,解析和封裝過程必須同步進行,程序一旦識別出圖1.3中的一個分區(qū)的開始后,就要開始將它封裝到一個FileItem

          對象中。
          程序要識別出圖1.3中的每一個分區(qū),需要在圖1.9所示的字節(jié)數(shù)組緩沖區(qū)buffer中尋找分區(qū)的字段分隔界線,當找到一個字段分隔界線后,就

          等于找到了一個分區(qū)的開始。筆者在《深入體驗java Web開發(fā)內(nèi)幕——核心基礎(chǔ)》一書中的第6.7.2節(jié)中已經(jīng)講過,上傳文件的請求消息的

          Content-Type頭字段中包含有用作字段分隔界線的字符序列,如下所示:
          content-type : multipart/form-data; boundary=---------------------------7d51383203e8
          顯然,我們可以通過調(diào)用HttpServletRequest.getHeader方法讀取Content-Type頭字段的內(nèi)容,從中分離出分隔界線的字符序列,然后在字節(jié)

          數(shù)組緩沖區(qū)buffer中尋找分區(qū)的字段分隔界線。content-type頭字段的boundary參數(shù)中指定的字段分隔界線是瀏覽器隨機產(chǎn)生的,瀏覽器保證

          它不會與用戶上傳的所有數(shù)據(jù)中的任何部分出現(xiàn)相同。在這里有一點需要注意,圖1.3中的實體內(nèi)容內(nèi)部的字段分隔界線與content-type頭中指

          定的字段分隔界線有一點細微的差別,前者是在后者前面增加了兩個減號(-)字符而形成的,這倒不是什么編程難點。真正的編程難點在于在

          字節(jié)數(shù)組緩沖區(qū)buffer中尋找分隔界線時,可能會遇到字節(jié)數(shù)組緩沖區(qū)buffer中只裝入了分隔界線字符序列的部分內(nèi)容的情況,如圖1.10所示


          圖1.10

          要解決這個問題的方法之一就是在查找字段分隔界線時,如果發(fā)現(xiàn)字節(jié)數(shù)組緩沖區(qū)buffer中只裝入了分隔界線字符序列的部分內(nèi)容,那么就將

          這一部分內(nèi)容留給字節(jié)數(shù)組緩沖區(qū)buffer的下一次讀取,如圖1.11所示。
          圖1.11
          這種方式讓字節(jié)數(shù)組緩沖區(qū)buffer下一次讀取的內(nèi)容不是緊接著上一次讀取內(nèi)容的后面,而是重疊上一次讀取的一部分內(nèi)容,即從上一次讀取

          內(nèi)容中的分隔界線字符序列的第一個字節(jié)處開始讀取。這種方式在實際的編程處理上存在著相當大的難度,程序首先必須確定字節(jié)數(shù)組緩沖區(qū)

          buffer上一次讀取的數(shù)據(jù)的后一部分內(nèi)容正好是分隔界線字符序列的前面一部分內(nèi)容,而這一部分內(nèi)容的長度是不確定的,可能只是分隔界線

          字符序列的第一個字符,也可能是分隔界線字符序列的前面n-1個字符,其中n為分隔界線字符序列的整個長度。另外,即使確定字節(jié)數(shù)組緩沖

          區(qū)buffer上一次讀取的數(shù)據(jù)的后一部分內(nèi)容正好是分隔界線字符序列的前面一部分內(nèi)容,但它們在整個輸入字節(jié)流中的后續(xù)內(nèi)容不一定就整個

          分隔界線字符序列的后一部分內(nèi)容,出現(xiàn)這種情況的可能性是完全存在,程序必須進行全面和嚴謹?shù)目紤]。
          Apache文件上傳組件的解決方法比較巧妙,它在查找字段分隔界線時,如果搜索到最后第n個字符時,n為分隔界線字符序列的長度,發(fā)現(xiàn)最后n

          個字符不能與分隔界線字符序列匹配,則將最后的n-1個字符留給字節(jié)數(shù)組緩沖區(qū)buffer的下一次讀取,程序再對buffer的下一次讀取的整個內(nèi)

          容從頭開始查找字段分隔界線,如圖1.12所示。
          圖1.12

          Apache文件上傳組件查找字段分隔界線的具體方法,讀者可以請參見MultipartStream類的findSeparator()方法中的源代碼。
          當找到一個分區(qū)的開始位置后,程序還需要分辨出分區(qū)中的描述頭和主體內(nèi)容,并對這兩部分內(nèi)容分開存儲。如何分辨出一個分區(qū)的描述頭和

          主體部分呢?從圖1.3中可以看到,每個分區(qū)中的描述頭和主體內(nèi)容之間有一空行,再加上描述頭后面的換行,這就說明描述頭和主體部分之間

          是使用“\n”、“\r”、“\n”、“\r”這四個連續(xù)的字節(jié)內(nèi)容進行分隔。因此,程序需要把“\n”、“\r”、“\n”、“\r”這四個連續(xù)的

          字節(jié)內(nèi)容作為描述頭和主體部分之間的分隔界線,并在字節(jié)數(shù)組緩沖區(qū)buffer中尋找這個特殊的分隔界線來識別描述頭和主體部分。
          當識別出一個分區(qū)中的描述頭和主體部分后,程序需要解決的下一個問題就是如何將描述頭和主體部分的數(shù)據(jù)保存到FileItem對象中,以便用

          戶以后可以調(diào)用FileItem類的方法來獲得這些數(shù)據(jù)。主體部分的數(shù)據(jù)需要能夠根據(jù)用戶上傳的文件大小有伸縮性地進行存儲,因此,程序要求

          編寫一個特殊的類來封裝主體部分的數(shù)據(jù),對于這個問題的具體實現(xiàn)細節(jié),讀者可參見1.2.4小節(jié)中講解的DeferredFileOutputStream類來了解

          1.3.3 MultipartStream類
          MultipartStream類用來對上傳的請求輸入流進行解析,它是整個Apache上傳組件中最復(fù)雜的類。
          1.設(shè)計思想
          MultipartStream類中定義了一個byte[]類型的boundary成員變量,這個成員變量用于保存圖1.3中的各個數(shù)據(jù)分區(qū)之間的分隔界線,每個分區(qū)

          分別代表一個表單字段的信息。圖1.3中的每個分區(qū)又可以分為描述頭部分和主體部分,MultipartStream類中定義了一個readHeaders()方法來

          讀取描述頭部分的內(nèi)容,MultipartStream類中定義了一個readBodyData(OutputStream output)方法來讀取主體部分的內(nèi)容,并將這些內(nèi)容寫

          入到一個作為參數(shù)傳入進來的輸出流對象中。readBodyData方法接收的參數(shù)output對象在應(yīng)用中的實際類型是DeferredFileOutputStream,這

          個對象又是保存在DefaultFileItem類對象中的一個成員變量,這樣,readBodyData方法就可以將一個分區(qū)的主體部分的數(shù)據(jù)寫入到

          DefaultFileItem類對象中。
          因為圖1.3中的實體內(nèi)容內(nèi)部的字段分隔界線是在content-type頭中指定的字段分隔界線前面增加了兩個減號(-)字符而形成的,而每個字段

          分隔界線與它前面內(nèi)容之間還進行了換行,這個換行并不屬于表單字段元素的內(nèi)容。所以,MultipartStream類中的成員變量boundary中存儲的

          字節(jié)數(shù)組并不是直接從content-type頭的boundary參數(shù)中獲得的字符序列,而是在boundary參數(shù)中指定的字符序列前面增加了四個字節(jié),依次

          是‘\n’、‘\r’、‘-’和‘-’。MultipartStream類中定義了一個readBoundary()方法來讀取和識別各個字段之間分隔界線,有一點特殊的

          是,圖1.3中的第一個分隔界線前面沒有回車換行符,它是無法與成員變量boundary中的數(shù)據(jù)相匹配的,所以無法調(diào)用readBoundary()方法進行

          讀取,而是需要進行特殊處理,其后的每個分隔界線都與boundary中的數(shù)據(jù)相匹配,可以直接調(diào)用readBoundary()方法進行讀取。在本章的后

          面部分,如果沒有特別說明,所說的分隔界線都是指成員變量boundary中的數(shù)據(jù)內(nèi)容。
          RFC 1867格式規(guī)范規(guī)定了描述頭和主體部分必須用一個空行進行分隔,如圖1.3所示,也就是描述頭和主體部分使用“\n”、“\r”、“\n”、

          “\r”這四個連續(xù)的字節(jié)內(nèi)容進行分隔。MultipartStream類的設(shè)計者為了簡化編程,在readHeaders()方法中將“\n”、“\r”、“\n”、

          “\r”這四個連續(xù)的字節(jié)內(nèi)容連同描述頭一起進行讀取。readHeaders()方法在讀取數(shù)據(jù)的過程中,當它發(fā)現(xiàn)第一個‘\n’、‘\r’、‘\n’、

          ‘\r’ 連續(xù)的字節(jié)序列時就會返回,即使主體部分正好也包含了“\n”、“\r”、“\n”、“\r”這四個連續(xù)的字節(jié)內(nèi)容,但是,它們只會被

          隨后調(diào)用的readBodyData方法作為主體內(nèi)容讀取,永遠不會被readHeaders()方法讀取到,所以,它們不會與作為描述頭和主體部分的分隔字符

          序列發(fā)生沖突。
          由于readHeaders()方法讀取了一個分區(qū)中的主體部分前面的所有內(nèi)容(包括它前面的換行),而它與下一個分區(qū)之間的分隔界線前面的換行又

          包含在了成員變量boundary中,這個換行將被readBoundary()方法讀取,所以,夾在readheaders()方法讀取的內(nèi)容和readBoundary()方法讀取

          的內(nèi)容之間的數(shù)據(jù)全部都屬于表單字段元素的內(nèi)容了,因此,讀取分區(qū)中的主體部分的readBodyData(OutputStream output)方法不需要進行特

          別的處理,它直接將讀取的數(shù)據(jù)寫入到DefaultFileItem類對象中封裝的DeferredFileOutputStream屬性對象中即可。
          2. 構(gòu)造方法
          MultipartStream類中的一個主要的構(gòu)造方法的語法定義如下:
          public (InputStream input, byte[] boundary, int bufSize)
          其中,參數(shù)input是指從HttpServetRequest請求對象中獲得的字節(jié)輸入流對象,參數(shù)boundary是從請求消息頭中獲得的未經(jīng)處理的分隔界線,

          bufSize指定圖1.10中的buffer緩沖區(qū)字節(jié)數(shù)組的長度,默認值是4096個字節(jié)。這個構(gòu)造方法的源代碼如下:
          public MultipartStream(InputStream input, byte[] boundary, int bufSize)
          {
          // 初始化成員變量
          this.input = input;
          this.bufSize = bufSize;
          this.buffer = new byte[bufSize];
          this.boundary = new byte[boundary.length + 4];
          this.boundaryLength = boundary.length + 4;
          //buffer緩沖區(qū)中保留給下次讀取的最大字節(jié)個數(shù)
          this.keepRegion = boundary.length + 3;
          this.boundary[0] = 0x0D; //‘\n’的16進制形式
          this.boundary[1] = 0x0A; //‘\r’的16進制形式
          this.boundary[2] = 0x2D; //‘-’的16進制形式
          this.boundary[3] = 0x2D;
          //在成員變量boundary中生成最終的分隔界線
          System.arraycopy (boundary, 0, this.boundary, 4, boundary.length);

          head = 0; // 成員變量,表示正在處理的這個字節(jié)在buffer中的位置指針
          tail = 0; // 成員變量,表示實際讀入到buffer中的字節(jié)個數(shù)
          }

          3. readByte方法
          MultipartStream類中的readByte()方法從字節(jié)數(shù)組緩沖區(qū)buffer中讀一個字節(jié),當buffer緩沖區(qū)中沒有更多的數(shù)據(jù)可讀時,該方法會自動從輸

          入流中讀取一批新的字節(jié)數(shù)據(jù)來重新填充buffer緩沖區(qū)。readByte()方法的源代碼如下:
          public byte readByte () throws IOException
          {
          // 判斷是否已經(jīng)讀完了buffer緩沖區(qū)中的所有數(shù)據(jù)
          if (head == tail)
          {
          head = 0;
          //讀入新的數(shù)據(jù)內(nèi)容來填充buffer緩沖區(qū)
          tail = input.read(buffer, head, bufSize);
          if (tail == -1)
          {
          throw new IOException("No more data is available ");
          }
          }
          return buffer[head++];// 返回當前字節(jié),head++
          }
          其中,head變量是MultipartStream類中定義的一個int類型的成員變量,它用于表示正在讀取的字節(jié)在buffer數(shù)組緩沖區(qū)中的位置;tail變量

          也是MultipartStream類中定義的一個int類型的成員變量,它用于表示當前buffer數(shù)組緩沖區(qū)裝入的實際字節(jié)內(nèi)容的長度。在MultipartStream

          類中主要是通過控制成員變量head的值來控制對buffer緩沖區(qū)中的數(shù)據(jù)的讀取和直接跳過某段數(shù)據(jù),通過比較head與tail變量的值了解是否需

          要向buffer緩沖區(qū)中裝入新的數(shù)據(jù)內(nèi)容。當每次向buffer緩沖區(qū)中裝入新的數(shù)據(jù)內(nèi)容后,都應(yīng)該調(diào)整成員變量head和tail的值。

          4. arrayequals靜態(tài)方法
          MultipartStream類中定義了一個的arrayequals靜態(tài)方法,用于比較兩個字節(jié)數(shù)組中的前面一部分內(nèi)容是否相等,相等返回true,否則返回

          false。arrayequals方法的源代碼如下,參數(shù)count指定了對字節(jié)數(shù)組中的前面幾個字節(jié)內(nèi)容進行比較:
          public static boolean arrayequals(byte[] a, byte[] b,int count)
          {
          for (int i = 0; i < count; i++)
          {
          if (a[i] != b[i])
          {
          return false;
          }
          }
          return true;
          }

          5. findByte方法
          MultipartStream類中的findByte()方法從字節(jié)數(shù)組緩沖區(qū)buffer中的某個位置開始搜索一個特定的字節(jié)數(shù)據(jù),如果找到了,則返回該字節(jié)在

          buffer緩沖區(qū)中的位置,不再繼續(xù)搜索,如果沒有找到,則返回-1。findByte方法的源代碼如下,參數(shù)pos制定了不搜索的起始位置值,value

          是要搜索的字節(jié)數(shù)據(jù):
          protected int findByte(byte value,int pos)
          {
          for (int i = pos; i < tail; i++)
          {
          if (buffer[i] == value)
          {
          return i; // 找到該值,findByte方法返回
          }
          }
          return - 1;
          }
          如果程序需要在buffer緩沖區(qū)中多次搜索某個特定的字節(jié)數(shù)據(jù),那就可以循環(huán)調(diào)用findByte方法,只是在每次調(diào)用findByte方法時,必須不斷

          地改變參數(shù)pos的值,讓pos的值等于上次調(diào)用findByte的返回值,直到findByte方法返回-1時為止,如圖1.13所示。
          圖1.13
          6. findSeparator方法
          MultipartStream類中的findSeparator方法用于從字節(jié)數(shù)組緩沖區(qū)buffer中查找成員變量boundary中定義的分隔界線,并返回分隔界線的第一

          個字節(jié)在buffer緩沖區(qū)中的位置,如果在buffer緩沖區(qū)中沒有找到分隔界線,則返回-1。
          findSeparator方法內(nèi)部首先調(diào)用findByte方法在buffer緩沖區(qū)中搜索分隔界線boundary的第一個字節(jié)內(nèi)容,如果沒有找到,則說明buffer緩沖

          區(qū)中沒有包含分隔界線;如果findByte方法在buffer緩沖區(qū)中找到了分隔界線boundary的第一個字節(jié)內(nèi)容,findSeparator方法內(nèi)部接著確定該

          字節(jié)及隨后的字節(jié)序列是否確實是分隔界線。findSeparator方法內(nèi)部循環(huán)調(diào)用findByte方法,直到找到分隔界線或者findByte方法已經(jīng)查找到

          了buffer緩沖區(qū)中的最后boundaryLength -1個字節(jié)。findSeparator方法內(nèi)部為什么調(diào)用findByte方法查找到buffer緩沖區(qū)中的最后

          boundaryLength-1個字節(jié)時就停止查找呢?這是為了解決如圖1.10所示的buffer緩沖區(qū)中裝入了分隔界線的部分內(nèi)容的特殊情況,所以在

          findSeparator()方法中不要搜索buffer緩沖區(qū)中的最后的boundaryLength -1個字節(jié),而是把buffer緩沖區(qū)中的最后這boundaryLength -1個字

          節(jié)作為保留區(qū),在下次讀取buffer緩沖區(qū)時將這些保留的字節(jié)數(shù)據(jù)重新填充到buffer緩沖區(qū)的開始部分。findSeparator方法的源代碼如下:
          protected int findSeparator()
          {
          int first;
          int match = 0;
          int maxpos = tail - boundaryLength;//在buffer中搜索的最大位置

          for (first = head;(first <= maxpos) && (match != boundaryLength);
          first++)
          {
          //在buffer緩沖區(qū)中尋找boundary的第一個字節(jié)
          first = findByte(boundary[0], first);
          /*buffer中找不到boundary[0]或者boundary[0]位于保留區(qū)中,
          則可以判斷buffer中不存在分隔界線*/
          if (first == -1 || (first > maxpos))
          {
          return -1;
          }
          //確定隨后的字節(jié)序列是否確實是分隔界線的其他字節(jié)內(nèi)容
          for (match = 1; match < boundaryLength; match++)
          {
          if (buffer[first + match] != boundary[match])
          {
          break;
          }
          }
          }
          // 當前buffer中找到boundary,返回第一個字節(jié)所在位置值
          if (match == boundaryLength)
          {
          return first - 1;
          }
          return -1; // 當前buffer中沒找到boundary,返回-1
          }
          圖1.14中描述了findSeparator方法內(nèi)部定義的各個變量的示意圖。
          圖1.14
          findSeparator方法內(nèi)部的代碼主要包括如下三個步驟:
          (1)循環(huán)調(diào)用findByte(boundary[0], first)找到buffer緩沖區(qū)中的與boundary[0]相同的字節(jié)的位置,并將位置記錄在first變量中。
          (2)比較buffer緩沖區(qū)中的first后的boundaryLength-1個字節(jié)序列是否與boundary中的其他字節(jié)序列相同。如果不同,說明這個first變量指

          向的字節(jié)不是分隔界線的開始字節(jié),跳出內(nèi)循環(huán),將first變量加1后繼續(xù)外循環(huán)調(diào)用findByte方法;如果相同,說明在當前緩沖區(qū)buffer中找

          到了分隔界線,內(nèi)循環(huán)正常結(jié)束,此時match變量的值為boundaryLength,接著執(zhí)行外循環(huán)將first變量加1,然后執(zhí)行外循環(huán)的條件判斷,由于

          match != boundaryLength條件不成立,外循環(huán)也隨之結(jié)束。
          (3)判斷match是否等于boundaryLength,如果等于則說明找到了分隔界線,此時返回成員變量boundary的第一個字節(jié)在緩沖區(qū)buffer中位置

          ,由于第(2)中將first加1了,所以這里的返回值應(yīng)該是first-1;如果不等,說明當前緩沖區(qū)huffer中沒有分隔界線,返回-1。

          7. readHeaders方法
          MultipartStream類中的readHeaders方法用于讀取一個分區(qū)的描述頭部分,并根據(jù)DiskFileUpload類的setHeaderEncoding方法設(shè)定的字符集編

          碼將描述頭部分轉(zhuǎn)換成一個字符串返回。
          在調(diào)用readHeaders方法之前時,程序已經(jīng)調(diào)用了findSeparator方法找到了分隔界線和讀取了分隔界線前面的內(nèi)容,此時MultipartStream類中

          的成員變量head指向了buffer緩沖區(qū)中的分隔界線boundary的第一個字節(jié),程序接著應(yīng)調(diào)用readBoundary方法跳過分隔界線及其隨后的回車換

          行兩個字節(jié),以保證在調(diào)用readHeaders方法時,成員變量head已經(jīng)指向了分區(qū)的描述頭的第一個字節(jié)。在readHeaders方法內(nèi)部,直接循環(huán)調(diào)

          用readByte方法讀取字節(jié)數(shù)據(jù),并把讀到的數(shù)據(jù)存儲在一個字節(jié)數(shù)組輸出流中,直到讀取到了連續(xù)的兩次回車換行字符,就認為已經(jīng)讀取完了

          描述頭的全部內(nèi)容,此時成員變量head將指向分區(qū)中的主體內(nèi)容的第一個字節(jié)。readHeaders()方法的源代碼如下:
          public String readHeaders()throws MalformedStreamException
          {
          int i = 0;
          //從下面的代碼看來,這里定義成一個byte即可,不用定義成byte數(shù)組
          byte b[] = new byte[1];
          //用于臨時保存描述頭信息的字節(jié)數(shù)組輸出流
          ByteArrayOutputStream baos = new ByteArrayOutputStream();
          //對描述頭部分的數(shù)據(jù)內(nèi)容過大進行限制處理
          int sizeMax = HEADER_PART_SIZE_MAX;
          int size = 0;
          while (i < 4)
          {
          try
          {
          b[0] = readByte(); }
          catch (IOException e)
          {
          throw new MalformedStreamException("Stream ended unexpectedly " );
          }
          size++;
          //靜態(tài)常量HEADER_SEPARATOR的值為:{0x0D, 0x0A, 0x0D, 0x0A}
          if (b[0] == HEADER_SEPARATOR[i])
          {
          i++;
          }
          else
          {
          i = 0;
          }
          if (size <= sizeMax)
          {
          baos.write(b[0]); // 將當前字節(jié)存入緩沖流
          }
          }
          String headers = null; // 找到HEADER_SEPARATOR后,獲取描述頭
          if (headerEncoding != null)
          {
          try
          {
          headers = baos.toString(headerEncoding);
          }
          catch (UnsupportedEncodingException e)
          {
          headers = baos.toString();
          }
          }
          else
          {
          headers = baos.toString();
          }
          return headers;
          }
          readHeaders方法循環(huán)調(diào)用readByte()方法逐個讀取buffer緩沖區(qū)中的字節(jié),并將讀取的字節(jié)與HEADER_SEPARATOR ={‘\n’,‘\r’,‘\n’

          ,‘\r’}的第一個字節(jié)進行比較,如果這個字節(jié)等于HEADER_SEPARATOR的首字節(jié)‘\n’,則循環(huán)控制因子i加1,這樣,下次調(diào)用readByte()方

          法讀取的字節(jié)將與HEADER_SEPARATOR中的第二字節(jié)比較,如果相等,則依照這種方式比較后面的字節(jié)內(nèi)容,如果連續(xù)讀取到了

          HEADER_SEPARATOR字節(jié)序列,則循環(huán)語句結(jié)束。readHeaders方法將讀取到的每個正常字節(jié)寫入到了一個字節(jié)數(shù)組輸出流中,其中也包括作為描

          述頭與主體內(nèi)容之間的分隔序列HEADER_SEPARATOR中的字節(jié)數(shù)據(jù)。由于readByte()方法會自動移動head變量的值和自動向緩沖區(qū)buffer中載入

          數(shù)據(jù),所以,readHeaders方法執(zhí)行完以后,成員變量head指向分區(qū)主體部分的首字節(jié)。readHeaders方法最后將把存入字節(jié)數(shù)組輸出流中的字

          節(jié)數(shù)據(jù)按指定字符集編碼轉(zhuǎn)換成字符串并返回,該字符串就是描述頭字符串。

          8. readBodyData方法
          MultipartStream類中的readBodyData方法用于把主體部分的數(shù)據(jù)寫入到一個輸出流對象中,并返回寫入到輸出流中的字節(jié)總數(shù)。當調(diào)用

          readBodyData方法前,成員變量head已經(jīng)指向了分區(qū)的主體部分的首字節(jié),readBodyData方法調(diào)用完成后,成員變量head指向分區(qū)分隔界線的

          首字節(jié)。readBodyData方法中需要調(diào)用findSeparator方法找出下一個分區(qū)分隔界線的首字節(jié)位置,才能知道這次讀取的分區(qū)主體內(nèi)容的結(jié)束位

          置。從分區(qū)主體部分的首字節(jié)開始,直到在findSeparator方法找到的下一個分區(qū)分隔界線前的所有數(shù)據(jù)都是這個分區(qū)的主體部分的數(shù)據(jù),

          readBodyData方法需要把這些數(shù)據(jù)都寫到輸出流output對象中。如果findSeparator方法在buffer緩沖區(qū)中沒有找到分區(qū)分隔界線,

          readBodyData方法還必須向buffer緩沖區(qū)中裝入新的數(shù)據(jù)內(nèi)容后繼續(xù)調(diào)用findSeparator方法進行處理。在向buffer緩沖區(qū)中裝入新的數(shù)據(jù)內(nèi)容

          時,必須先將上次保留在buffer緩沖區(qū)中的內(nèi)容轉(zhuǎn)移進新buffer緩沖區(qū)的開始處。readBodyData方法的源代碼如下,傳遞給readBodyData方法

          的參數(shù)實際上是一個DeferredFileOutputStream類對象:
          public int readBodyData(OutputStream output)
          throws MalformedStreamException,IOException
          {
          // 用于控制循環(huán)的變量
          boolean done = false;
          int pad;
          int pos;
          int bytesRead;
          // 寫入到輸出流中的字節(jié)個數(shù)
          int total = 0;
          while (!done)
          {
          pos = findSeparator();// 搜索分隔界線
          if (pos != -1) //緩沖區(qū)buffer中包含有分隔界線
          {
          output.write(buffer, head, pos - head);
          total += pos - head;
          head = pos;//head變量跳過主體數(shù)據(jù),指向分隔界線的首字節(jié)
          done = true;// 跳出循環(huán)
          }
          else //緩沖區(qū)buffer中沒有包含分隔界線
          {
          /*根據(jù)緩沖區(qū)中未被readHeaders方法讀取的數(shù)據(jù)內(nèi)容是否大于圖1.4中的
          保留區(qū)的大小,來決定保留到下一次buffer緩沖區(qū)中的字節(jié)個數(shù)
          */
          if (tail - head > keepRegion)
          {
          pad = keepRegion;
          }
          else
          {
          pad = tail - head;
          }
          output.write(buffer, head, tail - head - pad);
          total += tail - head - pad;//統(tǒng)計寫入到輸出流中的字節(jié)個數(shù)
          /*將上一次buffer緩沖區(qū)中的未處理的數(shù)據(jù)轉(zhuǎn)移到
          下一次buffer緩沖區(qū)的開始位置
          */
          System.arraycopy(buffer, tail - pad, buffer, 0, pad);
          head = 0; //讓head變量指向緩沖區(qū)的開始位置
          //向buffer緩沖區(qū)中載入新的數(shù)據(jù)
          bytesRead = input.read(buffer, pad, bufSize - pad);
          if (bytesRead != -1)
          {
          //設(shè)置buffer緩沖區(qū)中的有效字節(jié)的個數(shù)
          tail = pad + bytesRead;
          }
          else
          {
          /*還沒有找到分隔界線,輸入流就結(jié)束了,輸入流中的數(shù)據(jù)格式
          顯然不正確,保存緩沖區(qū)buffer中還未處理的數(shù)據(jù)后拋出異常
          */
          output.write(buffer, 0, pad);
          output.flush();
          total += pad;
          throw new MalformedStreamException
          ("Stream ended unexpectedly ");
          }
          }
          }
          output.flush();
          return total;
          }

          9. discardBodyData方法
          MultipartStream類中的discardBodyData方法用來跳過主體數(shù)據(jù),它與readBodyData方法非常相似,不同之處在于readBodyData方法把數(shù)據(jù)寫

          入到一個輸出流中,而discardBodyData方法是把數(shù)據(jù)丟棄掉。discardBodyData方法返回被丟掉的字節(jié)個數(shù),方法調(diào)用完成后成員變量head指

          向下一個分區(qū)分隔界線的首字節(jié)。MultipartStream類中定義discardBodyData這個方法,是為了忽略主體內(nèi)容部分的第一個分隔界線前面的內(nèi)

          容,按照MIME規(guī)范,消息頭和消息體之間的分隔界線前面可以有一些作為注釋信息的內(nèi)容,discardBodyData就是為了拋棄這些注釋信息而提供

          的。discardBodyData方法的源代碼如下:
          public int discardBodyData() throws MalformedStreamException,IOException
          {
          boolean done = false;
          int pad;
          int pos;
          int bytesRead;
          int total = 0;
          while (!done)
          {
          pos = findSeparator();
          if (pos != -1)
          {
          total += pos - head;
          head = pos;
          done = true;
          }
          else
          {
          if (tail - head > keepRegion)
          {
          pad = keepRegion;
          }
          else
          {
          pad = tail - head;
          }
          total += tail - head - pad;
          System.arraycopy(buffer, tail - pad, buffer, 0, pad);
          head = 0;
          bytesRead = input.read(buffer, pad, bufSize - pad);
          if (bytesRead != -1)
          {
          tail = pad + bytesRead;
          }
          else
          {
          total += pad;
          throw new MalformedStreamException
          ("Stream ended unexpectedly ");
          }
          }
          }
          return total;
          }
          10. readBoundary方法
          對于圖1.3中的每一個分區(qū)的解析處理,程序首先要調(diào)用readHeaders方法讀取描述頭,接著要調(diào)用readBodyData(OutputStream output)讀取主

          體數(shù)據(jù),這樣就完成了一個分區(qū)的解析。readBodyData方法內(nèi)部調(diào)用findSeparator方法找到了分隔界線,然后讀取分隔界線前面的內(nèi)容,此時

          MultipartStream類中的成員變量head指向了buffer緩沖區(qū)中的分隔界線boundary的第一個字節(jié)。findSeparator方法只負責(zé)尋找分隔界線

          boundary在緩沖區(qū)buffer中的位置,不負責(zé)從buffer緩沖區(qū)中讀走分隔界線的字節(jié)數(shù)據(jù)。在調(diào)用readBodyData方法之后,程序接著應(yīng)該讓成員

          變量head跳過分隔界線,讓它指向下一個分區(qū)的描述頭的第一個字節(jié),才能調(diào)用readHeaders方法去讀取下一個分區(qū)的描述頭。
          MultipartStream類中定義了一個readBoundary方法,用于讓成員變量head跳過分隔界線,讓它指向下一個分區(qū)的描述頭的第一個字節(jié)。對于圖

          1.3中的最后的分隔界線,它比其他的分隔界線后面多了兩個“-”字符,而其他分隔界線與下一個分區(qū)的內(nèi)容之間還有一個回車換行,所以,

          readBoundary方法內(nèi)部跳過分隔界線后,還需要再讀取兩個字節(jié)的數(shù)據(jù),才能讓成員變量head指向下一個分區(qū)的描述頭的第一個字節(jié)。

          readBoundary方法內(nèi)部讀取分隔界線后面的兩個字節(jié)數(shù)據(jù)后,根據(jù)它們是回車換行、還是兩個“-”字符,來判斷這個分隔界線是下一個分區(qū)的

          開始標記,還是整個請求消息的實體內(nèi)容的結(jié)束標記。如果readBoundary方法發(fā)現(xiàn)分隔界線是下一個分區(qū)的開始標記,那么它返回true,否則

          返回false。readBoundary()方法的源代碼如下:
          public boolean readBoundary()throws MalformedStreamException
          {
          byte[] marker = new byte[2];
          boolean nextChunk = false;
          head += boundaryLength; // 跳過分隔界線符
          try
          {
          marker[0] = readByte();
          marker[1] = readByte();
          // 靜態(tài)常量STREAM_TERMINATOR ={‘-’、‘-’}
          if (arrayequals(marker, STREAM_TERMINATOR, 2))
          {
          nextChunk = false;
          }
          // 靜態(tài)常量FIELD_SEPARATOR ={‘/n’、‘/r’}
          else if (arrayequals(marker, FIELD_SEPARATOR, 2))
          {
          nextChunk = true;
          }
          else
          {
          /*如果讀到的既不是回車換行,又不是兩個減號,
          說明輸入流有問題,則拋異常。

          posted on 2008-05-08 14:38 gembin 閱讀(7377) 評論(1)  編輯  收藏 所屬分類: JavaEE

          評論

          # re: Apache Common fileUpload API 詳解 2008-06-18 15:23 歲歲年年

          好文章!  回復(fù)  更多評論   

          導(dǎo)航

          統(tǒng)計

          常用鏈接

          留言簿(6)

          隨筆分類(440)

          隨筆檔案(378)

          文章檔案(6)

          新聞檔案(1)

          相冊

          收藏夾(9)

          Adobe

          Android

          AS3

          Blog-Links

          Build

          Design Pattern

          Eclipse

          Favorite Links

          Flickr

          Game Dev

          HBase

          Identity Management

          IT resources

          JEE

          Language

          OpenID

          OSGi

          SOA

          Version Control

          最新隨筆

          搜索

          積分與排名

          最新評論

          閱讀排行榜

          評論排行榜

          free counters
          主站蜘蛛池模板: 花莲市| 德江县| 布尔津县| 建始县| 普陀区| 弥勒县| 绍兴市| 湾仔区| 新兴县| 错那县| 浑源县| 玉屏| 锦州市| 望城县| 峡江县| 许昌市| 东安县| 阳信县| 溆浦县| 沙洋县| 新丰县| 虹口区| 延长县| 桐梓县| 安远县| 静乐县| 岳池县| 禹城市| 镇宁| 衡山县| 万荣县| 历史| 郯城县| 河间市| 福州市| 张家界市| 吴桥县| 锡林郭勒盟| 和平区| 涞水县| 阳谷县|