Struts 2中實現文件下載(修正中文問題)


在BlogJava上已經有一位作者闡述了文件上傳的問題,地址是在Struts 2中實現文件上傳,因此我就不再討論那個話題了。我今天簡單介紹一下Struts 2的文件下載問題。
我們的項目名爲 struts2hello,所使用的開發環境是MyEclipse 6,當然其實用哪個IDE都是一樣的,只要把類庫放進去就行了,文件下載不需要再加入任何額外的包。讀者可以參考文檔:http://beansoft.java-cn.org/myeclipse_doc_cn/struts2_demo.pdf,來了解怎麼下載和配置基本的Struts 2開發環境。
 
爲了便於大家對比,我把完整的struts.xml的配置信息列出來:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
    <package name="default" extends="struts-default"  >
        <!-- 在這裏添加Action定義 -->
        <!-- 簡單文件下載 -->
        <action name="download" class="example.FileDownloadAction">
            <result name="success" type="stream">
                <param name="contentType">text/plain</param>
                <param name="inputName">inputStream</param>
                <param name="contentDisposition">p_w_upload;filename="struts2中文.txt"</param>
                <param name="bufferSize">4096</param>
            </result>
        </action>
        
        <!-- 文件下載,支持中文附件名 -->
        <action name="download2" class="example.FileDownloadAction2">
            <!-- 初始文件名 -->
            <param name="fileName">Struts中文附件.txt</param>
            <result name="success" type="stream">
                <param name="contentType">text/plain</param>
                <param name="inputName">inputStream</param>
                <!-- 使用經過轉碼的文件名作爲下載文件名,downloadFileName屬性
                對應action類中的方法 getDownloadFileName() -->
                <param name="contentDisposition">p_w_upload;filename="${downloadFileName}"</param>
                <param name="bufferSize">4096</param>
            </result>
        </action>
        
        <!-- 下載現有文件 -->
        <action name="download3" class="example.FileDownloadAction3">
            <param name="inputPath">/download/系統說明.doc</param>
            <!-- 初始文件名 -->
            <param name="fileName">系統說明.doc</param>
            <result name="success" type="stream">
                <param name="contentType">application/octet-stream;charset=ISO8859-1</param>
                <param name="inputName">inputStream</param>
                <!-- 使用經過轉碼的文件名作爲下載文件名,downloadFileName屬性
                對應action類中的方法 getDownloadFileName() -->
                <param name="contentDisposition">p_w_upload;filename="${downloadFileName}"</param>
                <param name="bufferSize">4096</param>
            </result>
        </action>
        
    </package>
</struts>
 
Struts 2中對文件下載做了直接的支持,相比起自己辛辛苦苦的設置種種HTTP頭來說,現在實現文件下載無疑要簡便的多。說起文件下載,最直接的方式恐怕是直接寫一個超鏈接,讓地址等於被下載的文件,例如:<a href=”file1.zip”>下載file1.zip</a>,之後用戶在瀏覽器裏面點擊這個鏈接,就可以進行下載了。但是它有一些缺陷,例如如果地址是一個圖片,那麼瀏覽器會直接打開它,而不是顯示保存文件的對話框。再比如如果文件名是中文的,它會顯示一堆URL編碼過的文件名例如%3457...。而假設你企圖這樣下載文件:http://localhost:8080/struts2hello/download/系統說明.doc,Tomcat會告訴你一個文件找不到的404錯誤:HTTP Status 404 - /struts2hello/download/Ïμí3ËμÃ÷.doc。雖然目前還沒發現直接配置Struts 2來正確的下載中文名字的附件,不過好在作者對JSP中的文件下載比較瞭解,因此我們另有辦法解決這個問題。另外一個最大的用途,就是動態的生成並下載文件了,例如動態的下載生成的EXCEL,PDF,驗證碼圖片等等。本節內容就依次討論簡單的下載文件代碼,下載中文附件,最後介紹如何下載已經存在的文件。
先說文件下載,編寫一個普通的Action就可以了,只需要提供一個返回InputStream流的方法,該輸入流代表了被下載文件的入口,這個方法用來給被下載的數據提供輸入流,意思是從這個流讀出來,再寫到瀏覽器那邊供下載。這個方法需要由開發人員自己來編寫,只需要返回值爲InputStream即可。在我們的例子中方法的簽名是:public InputStream getInputStream() throws Exception,當然它也可以是別的名字,例如getDownloadFile()。好了,現在我們所寫的這個進行文件下載的Action類example.FileDownloadAction 的源代碼清單如下:
package example;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import com.opensymphony.xwork2.Action;
public class FileDownloadAction implements Action {
public InputStream getInputStream() throws Exception {
return new ByteArrayInputStream("Struts 2 下載示例".getBytes());
}
public String execute() throws Exception {
return SUCCESS;
}
}
。注意這裏唯一特殊的方法就是getInputStream(),在這個方法裏面我們使用了一個數組輸入流來從字符串轉換成的數組作爲數據的來源進行讀取。也許方法體中使用這樣的實現代碼:
return new java.io.FileInputStream(“c://test.txt”);//從系統磁盤文件讀取數據
這樣會更直觀一些。
文件下載的第二步,乃是在struts.xml中對action進行配置,其代碼清單如下所示:
<!-- 簡單文件下載 -->
<action name="download" class="example.FileDownloadAction">
<result name="success" type="stream">
<param name="contentType">text/plain</param>
<param name="inputName">inputStream</param>
<param name="contentDisposition">p_w_upload;filename="struts2.txt"</param>
<param name="bufferSize">4096</param>
</result>
</action>
。這個action特殊的地方在於result的類型是一個流(stream),配置stream類型的結果時,因爲無需指定實際的顯示的物理資源,所以無需指定location屬性,只需要指定inputName屬性,該屬性指向被下載文件的來源,對應着Action類中的某個屬性,類型爲InputStream。下面則列出了和下載有關的一些參數列表:
參數
說明
contentType
內容類型,和互聯網MIME標準中的規定類型一致,例如text/plain代表純文本,text/xml表示XML,p_w_picpath/gif代表GIF圖片,p_w_picpath/jpeg代表JPG圖片
inputName
下載文件的來源流,對應着action類中某個類型爲Inputstream的屬性名,例如取值爲inputStream的屬性需要編寫getInputStream()方法
contentDisposition
文件下載的處理方式,包括內聯(inline)和附件(p_w_upload)兩種方式,而附件方式會彈出文件保存對話框,否則瀏覽器會嘗試直接顯示文件。取值爲:
p_w_upload;filename="struts2.txt",表示文件下載的時候保存的名字應爲struts2.txt。如果直接寫filename="struts2.txt",那麼默認情況是代表inline,瀏覽器會嘗試自動打開它,等價於這樣的寫法:inline; filename="struts2.txt"
bufferSize
下載緩衝區的大小
。在這裏面,contentType屬性和contentDisposition分別對應着HTTP響應中的頭Content-TypeContent-disposition頭。好,我們先來看看這個例子,發佈運行項目後鍵入測試地址:http://localhost:8080/struts2hello/download.action,將會看到瀏覽器彈出一個文件保存對話框,如圖12.12所示。
clip_p_w_picpath002
clip_p_w_picpath004
圖12.12 文件下載對話框(IE 7和Firefox 3)
如果此時使用某些工具來探測瀏覽器返回的HTTP頭,將會看到下列內容:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-disposition: p_w_upload;filename="struts2.txt"
Content-Type: text/plain
Transfer-Encoding: chunked
Date: Sun, 02 Mar 2008 02:58:25 GMT
。所以用來下載的action配置中,只有兩個是和瀏覽器有關的:contentTypecontentDisposition。關於contentType的取值,如果是未知的文件類型,或者說出現了瀏覽器不能打開的文件,例如.bean文件,或者說這個action是用來做動態文件下載的,事先並不知道未來的文件類型是什麼,那麼我們可以把它的值設置成爲:application/octet-stream;charset=ISO8859-1 ,注意一定要加入charset,否則某些時候會導致下載的文件出錯;有人說這時也可以設置成爲application/x-download,根據筆者的實踐,這個頭也能正常工作,然而個別時候會出現瀏覽器無法識別的問題。而contentDisposition,如果其取值是filename="struts2.txt",或者是inline; filename="struts2.txt",運行後你可以看到瀏覽器直接顯示了文件的內容:
Struts 2 下載示例,而不再彈出對話框提示用戶保存文件到硬盤上。所以讀者如果想確保文件是被下載而不是被打開,務必使用格式p_w_upload;filename="struts2.txt",不要丟了p_w_upload;這個類型信息。
至此,關於文件下載的技術內容,已經告一段落。然而做中文系統,不可避免的要解決中文附件的下載問題。關於這個內容,也無權威的資料可查,我們只能用實踐中得到的解決方案來處理。也許有讀者以爲將filename屬性設置爲filename=”struts2中文.txt”就能解決問題了,好,就來試試,把contentDisposition修改成:
<param name="contentDisposition">p_w_upload;filename="struts2中文.txt"</param>
。再次鍵入地址進行測試,看看顯示的結果,如圖12.13所示。唉,真是完全不給面子!IE壓根就不能顯示出來文件名,草草敷衍了download_action了事。Firefox稍好點,還出來了一個對話框,但是很顯然,那個顯示的struts2--txt絕對不是我們日思夜想的struts2中文.txt。怎麼辦?解決方法是有,那就是用ISO8859-1編碼來顯示這個中文字符,可以閱讀12.8參考資料一節中的JSP 文件下載的相對完整代碼(解決中文問題和Weblogic報錯)這篇文章,可以這樣認爲,所有的文件下載代碼都是基於同樣的純Servlet的方式來進行的。如果是Java代碼,我們可以這樣做:
clip_p_w_picpath006
clip_p_w_picpath008
圖12.13 IE和Firefox下的中文文件下載對話框
String downFileName = new String(“struts2中文.txt”.getBytes(), "ISO8859-1");
然後把生成的結果字符串放到XML文件中就行了,然而它的輸出類似於struts2??.txt,是無法直接寫道我們的XML配置文件中的。所以,我們想到的的辦法,就是在Action類中寫一個方法來做轉碼,使它成爲某個屬性,所以要以get開頭。然後,再用12.3.8 給Action注入參數(param)值一節的內容,將文件名以正常的方式設置爲action類的某個屬性,最後呢,再利用一個小小的param參數取值中的伎倆:${屬性名},它可以直接從action類中動態獲取某個屬性值。好了,現在讓我們來看看第二個文件下載類FileDownloadAction2的代碼:
package example;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import com.opensymphony.xwork2.Action;
public class FileDownloadAction2 implements Action {
private String fileName;// 初始的通過param指定的文件名屬性
public InputStream getInputStream() throws Exception {
return new ByteArrayInputStream("Struts 2 下載示例".getBytes());
}
public String execute() throws Exception {
return SUCCESS;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
/** 提供轉換編碼後的供下載用的文件名 */
public String getDownloadFileName() {
String downFileName = fileName;
try {
downFileName = new String(downFileName.getBytes(), "ISO8859-1");
catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return downFileName;
}
}
。這個類有兩個屬性,第一個是fileName,它是需要被指定的下載文件名;第二個則是動態的僅僅由getDownloadFileName()這個方法定義的屬性downloadFileName,它的值隨着fileName而動態變動,僅僅是把它轉換成了ISO8859方式的西歐字符集。
接下來就是如何配置這個action了,這是關鍵的地方所在,現在配置一個新的action,名爲download2,其源代碼如下:
<!-- 文件下載,支持中文附件名 -->
<action name="download2" class="example.FileDownloadAction2">
<!-- 初始文件名 -->
<param name="fileName">Struts中文附件.txt</param>
<result name="success" type="stream">
<param name="contentType">text/plain</param>
<param name="inputName">inputStream</param>
<!-- 使用經過轉碼的文件名作爲下載文件名,downloadFileName屬性
對應action類中的方法 getDownloadFileName() -->
<param name="contentDisposition">p_w_upload;filename="${downloadFileName}"</param>
<param name="bufferSize">4096</param>
</result>
</action>
。其中特殊的代碼就是${downloadFileName},它的效果相當於運行的時候將action對象的屬性的取值動態的填充在${}中間的部分,我們可以認爲它等價於action. getDownloadFileName()
好了,現在讓我們重新發布然後運行這個項目,鍵入地址:
http://localhost:8080/struts2hello/download2.action 進行訪問,可以看到運行結果完全正確,如圖12.14所示。
clip_p_w_picpath010
clip_p_w_picpath012
圖 12.14 正確顯示了文件下載名的對話框(IE和Firefox)
在本節的最後部分,我們來討論一下如何下載已經存在於當前Web應用目錄下的已經存在的文件。一般的網站可能會把要下載的文件放在某個固定的目錄下,例如WebRoot/download,在這個子目錄下,我們放了一個名爲系統說明.doc的文件,希望最後我們的action能夠正確的下載這個文件。要檢驗下載是否成功非常簡單,文件內容僅僅是粗體的系統說明書這五個字,而word文件壞一個字節的話都是打不開的,所以下載後再用word打開即可檢驗是否成功。現在我們創建第三個文件下載的Action類,名爲example. FileDownloadAction3,其源代碼清單如下所示:
package example;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import org.apache.struts2.ServletActionContext;
import com.opensymphony.xwork2.Action;
public class FileDownloadAction3 implements Action {
private String fileName;// 初始的通過param指定的文件名屬性
private String inputPath;// 指定要被下載的文件路徑
public InputStream getInputStream() throws Exception {
// 通過 ServletContext,也就是application 來讀取數據
return ServletActionContext.getServletContext().getResourceAsStream(inputPath);
}
public String execute() throws Exception {
return SUCCESS;
}
public void setInputPath(String value) {
inputPath = value;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
/** 提供轉換編碼後的供下載用的文件名 */
public String getDownloadFileName() {
String downFileName = fileName;
try {
downFileName = new String(downFileName.getBytes(), "ISO8859-1");
catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return downFileName;
}
}
。代碼中被改動的部分已經用粗斜體的方式顯示出來了。首先是新加入了一個名爲inputPath的屬性,用來制定被下載文件的路徑。接着就是ServletActionContext.getServletContext()這段代碼,它的意義我們將在12.6節詳細討論,在這裏讀者只需要知道它獲取了當前Servlet容器的ServletContext,也就是大家常說的jsp中的application對象,然後用它來打開文件的輸入流。
接着要做的就是配置action,它和剛剛配置過的download2的內容差不多,只是多了一個被下載的資源的路徑屬性。現在我們在struts.xml中加入這個新的action定義:
<!-- 下載現有文件 -->
<action name="download3" class="example.FileDownloadAction3">
<param name="inputPath">/download/系統說明.doc</param>
<!-- 初始文件名 -->
<param name="fileName">系統說明.doc</param>
<result name="success" type="stream">
<param name="contentType">application/octet-stream;charset=ISO8859-1</param>
<param name="inputName">inputStream</param>
<!-- 使用經過轉碼的文件名作爲下載文件名,downloadFileName屬性
對應action類中的方法 getDownloadFileName() -->
<param name="contentDisposition">p_w_upload;filename="${downloadFileName}"</param>
<param name="bufferSize">4096</param>
</result>
</action>
。查看粗斜體的部分,首先就是自定了被下載文件的路徑,inputPath,接着就是修改了contentType爲二進制方式。最後重新發布項目並運行,鍵入地址進行訪問:http://localhost:8080/struts2hello/download3.action 。很好,可以看到文件下載對話框,保存系統說明.doc後再用word打開它,內容正確。
注意:而這種文件下載方式卻是存在安全隱患的,因爲訪問者如果精通Struts 2的話,它可能使用這樣的帶有表單參數的地址來訪問:http://localhost:8080/struts2hello/download3.action?inputPath=/WEB-INF/web.xml,這樣的結果就是下載後的文件內容是您系統裏面的web.xml的文件的源代碼,甚至還可以用這種方式來下載任何其它JSP文件的源碼。這對系統安全是個很大的威脅。作爲一種變通的方法,讀者最好是從數據庫中進行路徑配置,然後把Action類中的設置inputPath的方法統統去掉,簡言之就是刪除這個方法定義:
public void setInputPath(String value) {
inputPath = value;
}
。而實際情況則應該成爲 download3.action?fileid=1 類似於這樣的形式來進行。或者呢,讀者可以在execute()方法中進行路徑檢查,如果發現有訪問不屬於download下面文件的代碼,就一律拒絕,不給他們返回文件內容。例如,我們可以把剛纔類中的execute()方法加以改進,成爲這樣:
public String execute() throws Exception {
// 文件下載目錄路徑
String downloadDir = ServletActionContext.getServletContext().getRealPath("/download");
// 文件下載路徑
String downloadFile = ServletActionContext.getServletContext().getRealPath(inputPath);
java.io.File file = new java.io.File(downloadFile);
downloadFile = file.getCanonicalPath();// 真實文件路徑,去掉裏面的..等信息
// 發現企圖下載不在 /download 下的文件, 就顯示空內容
if(!downloadFile.startsWith(downloadDir)) {
return null;
}
return SUCCESS;
}
。這時候如果訪問者再企圖下載web.xml的內容,它只能得到一個空白頁,現在訪問者只能下載位於/download目錄下的文件。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章