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的配置信息列出來:
<?xmlversion="1.0"encoding="UTF-8" ?>
<!DOCTYPEstrutsPUBLIC
"-//Apache Software Foundation//DTD Struts
Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<packagename="default"extends="struts-default" >
<!-- 在這里添加Action定義 -->
<!-- 簡單文件下載 -->
<actionname="download"class="example.FileDownloadAction">
<resultname="success"type="stream">
<paramname="contentType">text/plain</param>
<paramname="inputName">inputStream</param>
<paramname="contentDisposition">attachment;filename="struts2中文.txt"</param>
<paramname="bufferSize">4096</param>
</result>
</action>
<!-- 文件下載,支持中文附件名 -->
<actionname="download2"class="example.FileDownloadAction2">
<!-- 初始文件名 -->
<paramname="fileName">Struts中文附件.txt</param>
<resultname="success"type="stream">
<paramname="contentType">text/plain</param>
<paramname="inputName">inputStream</param>
<!-- 使用經過轉碼的文件名作為下載文件名,downloadFileName屬性
對應action類中的方法
getDownloadFileName() -->
<paramname="contentDisposition">attachment;filename="${downloadFileName}"</param>
<paramname="bufferSize">4096</param>
</result>
</action>
<!-- 下載現有文件 -->
<actionname="download3"class="example.FileDownloadAction3">
<paramname="inputPath">/download/系統說明.doc</param>
<!-- 初始文件名 -->
<paramname="fileName">系統說明.doc</param>
<resultname="success"type="stream">
<paramname="contentType">application/octet-stream;charset=ISO8859-1</param>
<paramname="inputName">inputStream</param>
<!-- 使用經過轉碼的文件名作為下載文件名,downloadFileName屬性
對應action類中的方法 getDownloadFileName() -->
<paramname="contentDisposition">attachment;filename="${downloadFileName}"</param>
<paramname="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/ϵͳ˵Ã÷.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">attachment;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,image/gif代表GIF圖片,image/jpeg代表JPG圖片
inputName
下載文件的來源流,對應著action類中某個類型為Inputstream的屬性名,例如取值為inputStream的屬性需要編寫getInputStream()方法
contentDisposition
文件下載的處理方式,包括內聯(inline)和附件(attachment)兩種方式,而附件方式會彈出文件保存對話框,否則瀏覽器會嘗試直接顯示文件。取值為:
attachment;filename="struts2.txt",表示文件下載的時候保存的名字應為struts2.txt。如果直接寫filename="struts2.txt",那么默認情況是代表inline,瀏覽器會嘗試自動打開它,等價于這樣的寫法:inline;
filename="struts2.txt"
bufferSize
下載緩沖區的大小
。在這里面,contentType屬性和contentDisposition分別對應著HTTP響應中的頭Content-Type和Content-disposition頭。好,我們先來看看這個例子,發布運行項目后鍵入測試地址:http://localhost:8080/struts2hello/download.action,將會看到瀏覽器彈出一個文件保存對話框,如圖12.12所示。
圖12.12 文件下載對話框(IE 7和Firefox 3)
如果此時使用某些工具來探測瀏覽器返回的HTTP頭,將會看到下列內容:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-disposition:
attachment;filename="struts2.txt"
Content-Type: text/plain
Transfer-Encoding: chunked
Date: Sun, 02 Mar 2008 02:58:25 GMT
。所以用來下載的action配置中,只有兩個是和瀏覽器有關的:contentType和contentDisposition。關于contentType的取值,如果是未知的文件類型,或者說出現了瀏覽器不能打開的文件,例如.bean文件,或者說這個action是用來做動態文件下載的,事先并不知道未來的文件類型是什么,那么我們可以把它的值設置成為:application/octet-stream;charset=ISO8859-1,注意一定要加入charset,否則某些時候會導致下載的文件出錯;有人說這時也可以設置成為application/x-download,根據筆者的實踐,這個頭也能正常工作,然而個別時候會出現瀏覽器無法識別的問題。而contentDisposition,如果其取值是filename="struts2.txt",或者是inline;
filename="struts2.txt",運行后你可以看到瀏覽器直接顯示了文件的內容:
Struts 2 下載示例,而不再彈出對話框提示用戶保存文件到硬盤上。所以讀者如果想確保文件是被下載而不是被打開,務必使用格式attachment;filename="struts2.txt",不要丟了attachment;這個類型信息。
至此,關于文件下載的技術內容,已經告一段落。然而做中文系統,不可避免的要解決中文附件的下載問題。關于這個內容,也無權威的資料可查,我們只能用實踐中得到的解決方案來處理。也許有讀者以為將filename屬性設置為filename=”struts2中文.txt”就能解決問題了,好,就來試試,把contentDisposition修改成:
<param name="contentDisposition">attachment;filename="struts2中文.txt"</param>
。再次鍵入地址進行測試,看看顯示的結果,如圖12.13所示。唉,真是完全不給面子!IE壓根就不能顯示出來文件名,草草敷衍了download_action了事。Firefox稍好點,還出來了一個對話框,但是很顯然,那個顯示的struts2--txt絕對不是我們日思夜想的struts2中文.txt。怎么辦?解決方法是有,那就是用ISO8859-1編碼來顯示這個中文字符,可以閱讀12.8參考資料一節中的JSP
文件下載的相對完整代碼(解決中文問題和Weblogic報錯)這篇文章,可以這樣認為,所有的文件下載代碼都是基于同樣的純Servlet的方式來進行的。如果是Java代碼,我們可以這樣做:
圖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">attachment;filename="${downloadFileName}"</param>
<param
name="bufferSize">4096</param>
</result>
</action>
。其中特殊的代碼就是${downloadFileName},它的效果相當于運行的時候將action對象的屬性的取值動態的填充在${}中間的部分,我們可以認為它等價于+action. getDownloadFileName()。
好了,現在讓我們重新發布然后運行這個項目,鍵入地址:
http://localhost:8080/struts2hello/download2.action
進行訪問,可以看到運行結果完全正確,如圖12.14所示。
圖 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">attachment;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目錄下的文件。