<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    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 詳解

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

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

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

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

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

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

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

    提供了該組件的二進制發行包,文件名為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目錄中創建一個名為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環境變量中和確保編譯后生成的class文件存放

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    內存中將無法保存該文件內容,Apache文件上傳組件將用臨時文件來保存這些數據;但如果上傳的文件很小,例如上傳600個字節的文件,顯然

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

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

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

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

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

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

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

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

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

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

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

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

    法用于設置轉換時所使用的字符集編碼,其原理與筆者編著的《深入體驗java Web開發內幕——核心基礎》一書中的第6.9.4節講解的

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

    段元素的數據。FileItem是一個接口,在應用程序中使用的實際上是該接口一個實現類,該實現類的名稱并不重要,程序可以采用FileItem接

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

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

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

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

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

    DiskFileUpload.setSizeThreshold方法設置的臨界值大小時,這個流對象關聯到一片內存,主體內容將會被保存在內存中。當主體內容的數據

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

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

    .tmp”這種形式,FileItem類內部提供了維護臨時文件名中的數值不重復的機制,以保證了臨時文件名的唯一性。當應用程序將主體內容保存

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    表單,表單的enctype屬性設置值為“multipart/form-data”,表單的action屬性設置為“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創建一個名為UploadServlet.java的Servlet程序,UploadServlet.java

    調用Apache文件上傳組件來處理FORM表單提交的文件內容和普通字段數據。
    例程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();

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

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

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

    //得到所有表單字段對象的集合
    List fileItems = null;
    try
    {
    fileItems = fu.parseRequest(request);
    }
    catch (FileUploadException e)
    {
    out.println("解析數據時出現如下問題:");
    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("存儲文件時出現如下問題:");
    e.printStackTrace(out);
    return;
    }
    finally //總是立即刪除保存表單字段內容的臨時文件
    {
    fi.delete();
    }

    }
    }

    //顯示處理結果
    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()));

    }

    /**
    *將一段字符串追加到一個結果字符串中。如果結果字符串的初始內容不為空,
    *在追加當前這段字符串之前先最加一個逗號(,)。在組合sql語句的查詢條件時,
    *經常要用到類似的方法,第一條件前沒有"and",而后面的條件前都需要用"and"
    *作連詞,如果沒有選擇第一個條件,第二個條件就變成第一個,依此類推。
    *
    *@param result 要將當前字符串追加進去的結果字符串
    *@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)單擊瀏覽器工具欄上的“后退”按鈕回到表單填寫頁面,只在第二個文件字段中選擇一個文件,單擊“上載”按鈕,瀏覽器返回的顯示結果

    如圖1.6所示。
    圖1.6
    M腳下留心:
    上面編寫的Servlet程序將上傳的文件保存在了當前WEB應用程序下面的upload目錄中,這個目錄是客戶端瀏覽器可以訪問到的目錄。如果用戶

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

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

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

    器上的所有目錄結構,調用服務器上的操作系統進程等等,這將是一個非常致命的安全漏洞和隱患,這臺服務器對外就沒有任何安全性可言了



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

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

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

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

    不必學習本節的內容。在學習本節內容之前,讀者需要仔細學習了筆者編著的《深入體驗java Web開發內幕——核心基礎》一書中的第6.7.2節

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

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

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

    DiskFileUpload類再調用和協調更底層的類來完成具體的功能。解析類MultipartStream和工廠類DefaultFileItemFactory就是DiskFileUpload

    類調用的兩個的底層類。MultipartStream類用于對請求消息中的實體數據進行具體解析,DefaultFileItemFactory類對MultipartStream類解

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

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

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

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

    時,DeferredFileOutputStream類內部創建一個文件輸出流對象來存儲數據,并將前面寫入到ByteArrayOutputStream類對象中的數據轉移到文

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

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

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

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

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

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

    析出每個分區的數據,接著再從每個分區中解析出描述頭和主體內容部分。
    在讀取HTTP請求消息中的實體內容時,只能調用HttpServletRequest.getInputStream方法返回的字節輸入流,而不能調用

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

    要做的工作就是將屬于文件內容的那部分數據原封不動地提取出來,然后原封不動地存儲到本地文件系統中。如果使用

    HttpServletRequest.getReader方法返回的字符輸入流對象來讀取HTTP請求消息中的實體內容,它將HTTP請求消息中的字節數據轉換成字符后

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

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

    許客戶端上傳超過這個字節數組大小的實體內容。所以,只能創建一個一般大小的字節數組緩沖區來逐段讀取請求消息中的實體內容,讀取一

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

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

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

    等于找到了一個分區的開始。筆者在《深入體驗java Web開發內幕——核心基礎》一書中的第6.7.2節中已經講過,上傳文件的請求消息的

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

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

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

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

    字節數組緩沖區buffer中尋找分隔界線時,可能會遇到字節數組緩沖區buffer中只裝入了分隔界線字符序列的部分內容的情況,如圖1.10所示


    圖1.10

    要解決這個問題的方法之一就是在查找字段分隔界線時,如果發現字節數組緩沖區buffer中只裝入了分隔界線字符序列的部分內容,那么就將

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

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

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

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

    區buffer上一次讀取的數據的后一部分內容正好是分隔界線字符序列的前面一部分內容,但它們在整個輸入字節流中的后續內容不一定就整個

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    “\r”這四個連續的字節內容連同描述頭一起進行讀取。readHeaders()方法在讀取數據的過程中,當它發現第一個‘\n’、‘\r’、‘\n’、

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

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

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

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

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

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

    bufSize指定圖1.10中的buffer緩沖區字節數組的長度,默認值是4096個字節。這個構造方法的源代碼如下:
    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緩沖區中保留給下次讀取的最大字節個數
    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; // 成員變量,表示正在處理的這個字節在buffer中的位置指針
    tail = 0; // 成員變量,表示實際讀入到buffer中的字節個數
    }

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

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

    也是MultipartStream類中定義的一個int類型的成員變量,它用于表示當前buffer數組緩沖區裝入的實際字節內容的長度。在MultipartStream

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

    要向buffer緩沖區中裝入新的數據內容。當每次向buffer緩沖區中裝入新的數據內容后,都應該調整成員變量head和tail的值。

    4. arrayequals靜態方法
    MultipartStream類中定義了一個的arrayequals靜態方法,用于比較兩個字節數組中的前面一部分內容是否相等,相等返回true,否則返回

    false。arrayequals方法的源代碼如下,參數count指定了對字節數組中的前面幾個字節內容進行比較:
    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()方法從字節數組緩沖區buffer中的某個位置開始搜索一個特定的字節數據,如果找到了,則返回該字節在

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

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

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

    個字節在buffer緩沖區中的位置,如果在buffer緩沖區中沒有找到分隔界線,則返回-1。
    findSeparator方法內部首先調用findByte方法在buffer緩沖區中搜索分隔界線boundary的第一個字節內容,如果沒有找到,則說明buffer緩沖

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

    字節及隨后的字節序列是否確實是分隔界線。findSeparator方法內部循環調用findByte方法,直到找到分隔界線或者findByte方法已經查找到

    了buffer緩沖區中的最后boundaryLength -1個字節。findSeparator方法內部為什么調用findByte方法查找到buffer緩沖區中的最后

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

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

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

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

    向的字節不是分隔界線的開始字節,跳出內循環,將first變量加1后繼續外循環調用findByte方法;如果相同,說明在當前緩沖區buffer中找

    到了分隔界線,內循環正常結束,此時match變量的值為boundaryLength,接著執行外循環將first變量加1,然后執行外循環的條件判斷,由于

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

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

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

    碼將描述頭部分轉換成一個字符串返回。
    在調用readHeaders方法之前時,程序已經調用了findSeparator方法找到了分隔界線和讀取了分隔界線前面的內容,此時MultipartStream類中

    的成員變量head指向了buffer緩沖區中的分隔界線boundary的第一個字節,程序接著應調用readBoundary方法跳過分隔界線及其隨后的回車換

    行兩個字節,以保證在調用readHeaders方法時,成員變量head已經指向了分區的描述頭的第一個字節。在readHeaders方法內部,直接循環調

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

    描述頭的全部內容,此時成員變量head將指向分區中的主體內容的第一個字節。readHeaders()方法的源代碼如下:
    public String readHeaders()throws MalformedStreamException
    {
    int i = 0;
    //從下面的代碼看來,這里定義成一個byte即可,不用定義成byte數組
    byte b[] = new byte[1];
    //用于臨時保存描述頭信息的字節數組輸出流
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    //對描述頭部分的數據內容過大進行限制處理
    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++;
    //靜態常量HEADER_SEPARATOR的值為:{0x0D, 0x0A, 0x0D, 0x0A}
    if (b[0] == HEADER_SEPARATOR[i])
    {
    i++;
    }
    else
    {
    i = 0;
    }
    if (size <= sizeMax)
    {
    baos.write(b[0]); // 將當前字節存入緩沖流
    }
    }
    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方法循環調用readByte()方法逐個讀取buffer緩沖區中的字節,并將讀取的字節與HEADER_SEPARATOR ={‘\n’,‘\r’,‘\n’

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

    法讀取的字節將與HEADER_SEPARATOR中的第二字節比較,如果相等,則依照這種方式比較后面的字節內容,如果連續讀取到了

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

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

    數據,所以,readHeaders方法執行完以后,成員變量head指向分區主體部分的首字節。readHeaders方法最后將把存入字節數組輸出流中的字

    節數據按指定字符集編碼轉換成字符串并返回,該字符串就是描述頭字符串。

    8. readBodyData方法
    MultipartStream類中的readBodyData方法用于把主體部分的數據寫入到一個輸出流對象中,并返回寫入到輸出流中的字節總數。當調用

    readBodyData方法前,成員變量head已經指向了分區的主體部分的首字節,readBodyData方法調用完成后,成員變量head指向分區分隔界線的

    首字節。readBodyData方法中需要調用findSeparator方法找出下一個分區分隔界線的首字節位置,才能知道這次讀取的分區主體內容的結束位

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

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

    readBodyData方法還必須向buffer緩沖區中裝入新的數據內容后繼續調用findSeparator方法進行處理。在向buffer緩沖區中裝入新的數據內容

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

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

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

    入到一個輸出流中,而discardBodyData方法是把數據丟棄掉。discardBodyData方法返回被丟掉的字節個數,方法調用完成后成員變量head指

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

    容,按照MIME規范,消息頭和消息體之間的分隔界線前面可以有一些作為注釋信息的內容,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中的每一個分區的解析處理,程序首先要調用readHeaders方法讀取描述頭,接著要調用readBodyData(OutputStream output)讀取主

    體數據,這樣就完成了一個分區的解析。readBodyData方法內部調用findSeparator方法找到了分隔界線,然后讀取分隔界線前面的內容,此時

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

    boundary在緩沖區buffer中的位置,不負責從buffer緩沖區中讀走分隔界線的字節數據。在調用readBodyData方法之后,程序接著應該讓成員

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

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

    readBoundary方法內部跳過分隔界線后,還需要再讀取兩個字節的數據,才能讓成員變量head指向下一個分區的描述頭的第一個字節。

    readBoundary方法內部讀取分隔界線后面的兩個字節數據后,根據它們是回車換行、還是兩個“-”字符,來判斷這個分隔界線是下一個分區的

    開始標記,還是整個請求消息的實體內容的結束標記。如果readBoundary方法發現分隔界線是下一個分區的開始標記,那么它返回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();
    // 靜態常量STREAM_TERMINATOR ={‘-’、‘-’}
    if (arrayequals(marker, STREAM_TERMINATOR, 2))
    {
    nextChunk = false;
    }
    // 靜態常量FIELD_SEPARATOR ={‘/n’、‘/r’}
    else if (arrayequals(marker, FIELD_SEPARATOR, 2))
    {
    nextChunk = true;
    }
    else
    {
    /*如果讀到的既不是回車換行,又不是兩個減號,
    說明輸入流有問題,則拋異常。

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

    評論

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

    好文章!  回復  更多評論   

    導航

    統計

    常用鏈接

    留言簿(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
    主站蜘蛛池模板: 中文字幕亚洲一区| 免费人成网站7777视频| 久久青草亚洲AV无码麻豆| 一级毛片不卡免费看老司机| 日本午夜免费福利视频| 国产精品亚洲专区无码不卡| 国产免费观看青青草原网站| 精品亚洲成A人在线观看青青| 精品久久洲久久久久护士免费| 亚洲AV无码成人网站在线观看| 日日操夜夜操免费视频| 美女羞羞免费视频网站| 国内精品99亚洲免费高清| a毛片免费全部播放完整成| 亚洲天堂一区二区| A在线观看免费网站大全| 亚洲欧美黑人猛交群| 国产国产人免费人成免费视频| 免费又黄又爽又猛大片午夜| 亚洲亚洲人成综合网络| 91制片厂制作传媒免费版樱花| 亚洲人妖女同在线播放| 拔擦拔擦8x华人免费久久| 一级毛片免费不卡| 亚洲一区二区成人| 日本免费无遮挡吸乳视频电影| eeuss免费天堂影院| 久久久久亚洲AV无码观看| 成人免费一区二区三区在线观看| 黄网站色视频免费观看45分钟| 亚洲精品无码专区在线在线播放| 真实国产乱子伦精品免费| 亚洲欧美日韩国产精品一区| 久久亚洲av无码精品浪潮| 222www免费视频| 妇女自拍偷自拍亚洲精品| 亚洲av无码乱码国产精品fc2| 成年人网站在线免费观看| 人妻巨大乳hd免费看| 亚洲成a人片在线观看播放| 全部免费毛片在线|