Struts 2中實(shí)現(xiàn)文件下載(修正中文問(wèn)題)
在BlogJava上已經(jīng)有一位作者闡述了文件上傳的問(wèn)題,地址是在Struts 2中實(shí)現(xiàn)文件上傳,因此我就不再討論那個(gè)話題了。我今天簡(jiǎn)單介紹一下Struts
2的文件下載問(wèn)題。
我們的項(xiàng)目名為 struts2hello,所使用的開(kāi)發(fā)環(huán)境是MyEclipse 6,當(dāng)然其實(shí)用哪個(gè)IDE都是一樣的,只要把類庫(kù)放進(jìn)去就行了,文件下載不需要再加入任何額外的包。讀者可以參考文檔:http://beansoft.java-cn.org/myeclipse_doc_cn/struts2_demo.pdf,來(lái)了解怎么下載和配置基本的Struts 2開(kāi)發(fā)環(huán)境。
為了便于大家對(duì)比,我把完整的struts.xml的配置信息列出來(lái):
<?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定義 -->
<!-- 簡(jiǎn)單文件下載 -->
<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>
<!-- 使用經(jīng)過(guò)轉(zhuǎn)碼的文件名作為下載文件名,downloadFileName屬性
對(duì)應(yīng)action類中的方法
getDownloadFileName() -->
<paramname="contentDisposition">attachment;filename="${downloadFileName}"</param>
<paramname="bufferSize">4096</param>
</result>
</action>
<!-- 下載現(xiàn)有文件 -->
<actionname="download3"class="example.FileDownloadAction3">
<paramname="inputPath">/download/系統(tǒng)說(shuō)明.doc</param>
<!-- 初始文件名 -->
<paramname="fileName">系統(tǒng)說(shuō)明.doc</param>
<resultname="success"type="stream">
<paramname="contentType">application/octet-stream;charset=ISO8859-1</param>
<paramname="inputName">inputStream</param>
<!-- 使用經(jīng)過(guò)轉(zhuǎn)碼的文件名作為下載文件名,downloadFileName屬性
對(duì)應(yīng)action類中的方法 getDownloadFileName() -->
<paramname="contentDisposition">attachment;filename="${downloadFileName}"</param>
<paramname="bufferSize">4096</param>
</result>
</action>
</package>
</struts>
Struts 2中對(duì)文件下載做了直接的支持,相比起自己辛辛苦苦的設(shè)置種種HTTP頭來(lái)說(shuō),現(xiàn)在實(shí)現(xiàn)文件下載無(wú)疑要簡(jiǎn)便的多。說(shuō)起文件下載,最直接的方式恐怕是直接寫一個(gè)超鏈接,讓地址等于被下載的文件,例如:<a href=”file1.zip”>下載file1.zip</a>,之后用戶在瀏覽器里面點(diǎn)擊這個(gè)鏈接,就可以進(jìn)行下載了。但是它有一些缺陷,例如如果地址是一個(gè)圖片,那么瀏覽器會(huì)直接打開(kāi)它,而不是顯示保存文件的對(duì)話框。再比如如果文件名是中文的,它會(huì)顯示一堆URL編碼過(guò)的文件名例如%3457...。而假設(shè)你企圖這樣下載文件:http://localhost:8080/struts2hello/download/系統(tǒng)說(shuō)明.doc,Tomcat會(huì)告訴你一個(gè)文件找不到的404錯(cuò)誤:HTTP Status 404 -
/struts2hello/download/ϵͳ˵Ã÷.doc。雖然目前還沒(méi)發(fā)現(xiàn)直接配置Struts 2來(lái)正確的下載中文名字的附件,不過(guò)好在作者對(duì)JSP中的文件下載比較了解,因此我們另有辦法解決這個(gè)問(wèn)題。另外一個(gè)最大的用途,就是動(dòng)態(tài)的生成并下載文件了,例如動(dòng)態(tài)的下載生成的EXCEL,PDF,驗(yàn)證碼圖片等等。本節(jié)內(nèi)容就依次討論簡(jiǎn)單的下載文件代碼,下載中文附件,最后介紹如何下載已經(jīng)存在的文件。
先說(shuō)文件下載,編寫一個(gè)普通的Action就可以了,只需要提供一個(gè)返回InputStream流的方法,該輸入流代表了被下載文件的入口,這個(gè)方法用來(lái)給被下載的數(shù)據(jù)提供輸入流,意思是從這個(gè)流讀出來(lái),再寫到瀏覽器那邊供下載。這個(gè)方法需要由開(kāi)發(fā)人員自己來(lái)編寫,只需要返回值為InputStream即可。在我們的例子中方法的簽名是:public InputStream getInputStream() throws
Exception,當(dāng)然它也可以是別的名字,例如getDownloadFile()。好了,現(xiàn)在我們所寫的這個(gè)進(jìn)行文件下載的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(),在這個(gè)方法里面我們使用了一個(gè)數(shù)組輸入流來(lái)從字符串轉(zhuǎn)換成的數(shù)組作為數(shù)據(jù)的來(lái)源進(jìn)行讀取。也許方法體中使用這樣的實(shí)現(xiàn)代碼:
return new
java.io.FileInputStream(“c:""test.txt”);//從系統(tǒng)磁盤文件讀取數(shù)據(jù)
這樣會(huì)更直觀一些。
文件下載的第二步,乃是在struts.xml中對(duì)action進(jìn)行配置,其代碼清單如下所示:
<!-- 簡(jiǎn)單文件下載 -->
<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>
。這個(gè)action特殊的地方在于result的類型是一個(gè)流(stream),配置stream類型的結(jié)果時(shí),因?yàn)闊o(wú)需指定實(shí)際的顯示的物理資源,所以無(wú)需指定location屬性,只需要指定inputName屬性,該屬性指向被下載文件的來(lái)源,對(duì)應(yīng)著Action類中的某個(gè)屬性,類型為InputStream。下面則列出了和下載有關(guān)的一些參數(shù)列表:
參數(shù)
說(shuō)明
contentType
內(nèi)容類型,和互聯(lián)網(wǎng)MIME標(biāo)準(zhǔn)中的規(guī)定類型一致,例如text/plain代表純文本,text/xml表示XML,image/gif代表GIF圖片,image/jpeg代表JPG圖片
inputName
下載文件的來(lái)源流,對(duì)應(yīng)著action類中某個(gè)類型為Inputstream的屬性名,例如取值為inputStream的屬性需要編寫getInputStream()方法
contentDisposition
文件下載的處理方式,包括內(nèi)聯(lián)(inline)和附件(attachment)兩種方式,而附件方式會(huì)彈出文件保存對(duì)話框,否則瀏覽器會(huì)嘗試直接顯示文件。取值為:
attachment;filename="struts2.txt",表示文件下載的時(shí)候保存的名字應(yīng)為struts2.txt。如果直接寫filename="struts2.txt",那么默認(rèn)情況是代表inline,瀏覽器會(huì)嘗試自動(dòng)打開(kāi)它,等價(jià)于這樣的寫法:inline;
filename="struts2.txt"
bufferSize
下載緩沖區(qū)的大小
。在這里面,contentType屬性和contentDisposition分別對(duì)應(yīng)著HTTP響應(yīng)中的頭Content-Type和Content-disposition頭。好,我們先來(lái)看看這個(gè)例子,發(fā)布運(yùn)行項(xiàng)目后鍵入測(cè)試地址:http://localhost:8080/struts2hello/download.action,將會(huì)看到瀏覽器彈出一個(gè)文件保存對(duì)話框,如圖12.12所示。
圖12.12 文件下載對(duì)話框(IE 7和Firefox 3)
如果此時(shí)使用某些工具來(lái)探測(cè)瀏覽器返回的HTTP頭,將會(huì)看到下列內(nèi)容:
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
。所以用來(lái)下載的action配置中,只有兩個(gè)是和瀏覽器有關(guān)的:contentType和contentDisposition。關(guān)于contentType的取值,如果是未知的文件類型,或者說(shuō)出現(xiàn)了瀏覽器不能打開(kāi)的文件,例如.bean文件,或者說(shuō)這個(gè)action是用來(lái)做動(dòng)態(tài)文件下載的,事先并不知道未來(lái)的文件類型是什么,那么我們可以把它的值設(shè)置成為:application/octet-stream;charset=ISO8859-1,注意一定要加入charset,否則某些時(shí)候會(huì)導(dǎo)致下載的文件出錯(cuò);有人說(shuō)這時(shí)也可以設(shè)置成為application/x-download,根據(jù)筆者的實(shí)踐,這個(gè)頭也能正常工作,然而個(gè)別時(shí)候會(huì)出現(xiàn)瀏覽器無(wú)法識(shí)別的問(wèn)題。而contentDisposition,如果其取值是filename="struts2.txt",或者是inline;
filename="struts2.txt",運(yùn)行后你可以看到瀏覽器直接顯示了文件的內(nèi)容:
Struts 2 下載示例,而不再?gòu)棾鰧?duì)話框提示用戶保存文件到硬盤上。所以讀者如果想確保文件是被下載而不是被打開(kāi),務(wù)必使用格式attachment;filename="struts2.txt",不要丟了attachment;這個(gè)類型信息。
至此,關(guān)于文件下載的技術(shù)內(nèi)容,已經(jīng)告一段落。然而做中文系統(tǒng),不可避免的要解決中文附件的下載問(wèn)題。關(guān)于這個(gè)內(nèi)容,也無(wú)權(quán)威的資料可查,我們只能用實(shí)踐中得到的解決方案來(lái)處理。也許有讀者以為將filename屬性設(shè)置為filename=”struts2中文.txt”就能解決問(wèn)題了,好,就來(lái)試試,把contentDisposition修改成:
<param name="contentDisposition">attachment;filename="struts2中文.txt"</param>
。再次鍵入地址進(jìn)行測(cè)試,看看顯示的結(jié)果,如圖12.13所示。唉,真是完全不給面子!IE壓根就不能顯示出來(lái)文件名,草草敷衍了download_action了事。Firefox稍好點(diǎn),還出來(lái)了一個(gè)對(duì)話框,但是很顯然,那個(gè)顯示的struts2--txt絕對(duì)不是我們?nèi)账家瓜氲?/span>struts2中文.txt。怎么辦?解決方法是有,那就是用ISO8859-1編碼來(lái)顯示這個(gè)中文字符,可以閱讀12.8參考資料一節(jié)中的JSP
文件下載的相對(duì)完整代碼(解決中文問(wèn)題和Weblogic報(bào)錯(cuò))這篇文章,可以這樣認(rèn)為,所有的文件下載代碼都是基于同樣的純Servlet的方式來(lái)進(jìn)行的。如果是Java代碼,我們可以這樣做:
圖12.13 IE和Firefox下的中文文件下載對(duì)話框
String downFileName = new String(“struts2中文.txt”.getBytes(),
"ISO8859-1");
然后把生成的結(jié)果字符串放到XML文件中就行了,然而它的輸出類似于struts2??.txt,是無(wú)法直接寫道我們的XML配置文件中的。所以,我們想到的的辦法,就是在Action類中寫一個(gè)方法來(lái)做轉(zhuǎn)碼,使它成為某個(gè)屬性,所以要以get開(kāi)頭。然后,再用12.3.8給Action注入?yún)?shù)(param)值一節(jié)的內(nèi)容,將文件名以正常的方式設(shè)置為action類的某個(gè)屬性,最后呢,再利用一個(gè)小小的param參數(shù)取值中的伎倆:${屬性名},它可以直接從action類中動(dòng)態(tài)獲取某個(gè)屬性值。好了,現(xiàn)在讓我們來(lái)看看第二個(gè)文件下載類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;// 初始的通過(guò)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;
}
/** 提供轉(zhuǎn)換編碼后的供下載用的文件名 */
public String getDownloadFileName() {
String downFileName = fileName;
try {
downFileName = new
String(downFileName.getBytes(), "ISO8859-1");
} catch
(UnsupportedEncodingException e) {
e.printStackTrace();
}
return downFileName;
}
}
。這個(gè)類有兩個(gè)屬性,第一個(gè)是fileName,它是需要被指定的下載文件名;第二個(gè)則是動(dòng)態(tài)的僅僅由getDownloadFileName()這個(gè)方法定義的屬性downloadFileName,它的值隨著fileName而動(dòng)態(tài)變動(dòng),僅僅是把它轉(zhuǎn)換成了ISO8859方式的西歐字符集。
接下來(lái)就是如何配置這個(gè)action了,這是關(guān)鍵的地方所在,現(xiàn)在配置一個(gè)新的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>
<!-- 使用經(jīng)過(guò)轉(zhuǎn)碼的文件名作為下載文件名,downloadFileName屬性
對(duì)應(yīng)action類中的方法
getDownloadFileName() -->
<param
name="contentDisposition">attachment;filename="${downloadFileName}"</param>
<param
name="bufferSize">4096</param>
</result>
</action>
。其中特殊的代碼就是${downloadFileName},它的效果相當(dāng)于運(yùn)行的時(shí)候?qū)?/span>action對(duì)象的屬性的取值動(dòng)態(tài)的填充在${}中間的部分,我們可以認(rèn)為它等價(jià)于+action. getDownloadFileName()。
好了,現(xiàn)在讓我們重新發(fā)布然后運(yùn)行這個(gè)項(xiàng)目,鍵入地址:
http://localhost:8080/struts2hello/download2.action
進(jìn)行訪問(wèn),可以看到運(yùn)行結(jié)果完全正確,如圖12.14所示。
圖 12.14 正確顯示了文件下載名的對(duì)話框(IE和Firefox)
在本節(jié)的最后部分,我們來(lái)討論一下如何下載已經(jīng)存在于當(dāng)前Web應(yīng)用目錄下的已經(jīng)存在的文件。一般的網(wǎng)站可能會(huì)把要下載的文件放在某個(gè)固定的目錄下,例如WebRoot/download,在這個(gè)子目錄下,我們放了一個(gè)名為系統(tǒng)說(shuō)明.doc的文件,希望最后我們的action能夠正確的下載這個(gè)文件。要檢驗(yàn)下載是否成功非常簡(jiǎn)單,文件內(nèi)容僅僅是粗體的系統(tǒng)說(shuō)明書這五個(gè)字,而word文件壞一個(gè)字節(jié)的話都是打不開(kāi)的,所以下載后再用word打開(kāi)即可檢驗(yàn)是否成功。現(xiàn)在我們創(chuàng)建第三個(gè)文件下載的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;// 初始的通過(guò)param指定的文件名屬性
private String inputPath;// 指定要被下載的文件路徑
public InputStream getInputStream() throws
Exception {
// 通過(guò)
ServletContext,也就是application 來(lái)讀取數(shù)據(jù)
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;
}
/** 提供轉(zhuǎn)換編碼后的供下載用的文件名 */
public String getDownloadFileName() {
String downFileName = fileName;
try {
downFileName = new
String(downFileName.getBytes(), "ISO8859-1");
} catch
(UnsupportedEncodingException e) {
e.printStackTrace();
}
return downFileName;
}
}
。代碼中被改動(dòng)的部分已經(jīng)用粗斜體的方式顯示出來(lái)了。首先是新加入了一個(gè)名為inputPath的屬性,用來(lái)制定被下載文件的路徑。接著就是ServletActionContext.getServletContext()這段代碼,它的意義我們將在12.6節(jié)詳細(xì)討論,在這里讀者只需要知道它獲取了當(dāng)前Servlet容器的ServletContext,也就是大家常說(shuō)的jsp中的application對(duì)象,然后用它來(lái)打開(kāi)文件的輸入流。
接著要做的就是配置action,它和剛剛配置過(guò)的download2的內(nèi)容差不多,只是多了一個(gè)被下載的資源的路徑屬性。現(xiàn)在我們?cè)?/span>struts.xml中加入這個(gè)新的action定義:
<!-- 下載現(xiàn)有文件 -->
<action name="download3"
class="example.FileDownloadAction3">
<param
name="inputPath">/download/系統(tǒng)說(shuō)明.doc</param>
<!-- 初始文件名 -->
<param name="fileName">系統(tǒng)說(shuō)明.doc</param>
<result name="success"
type="stream">
<param name="contentType">application/octet-stream;charset=ISO8859-1</param>
<param
name="inputName">inputStream</param>
<!-- 使用經(jīng)過(guò)轉(zhuǎn)碼的文件名作為下載文件名,downloadFileName屬性
對(duì)應(yīng)action類中的方法 getDownloadFileName()
-->
<param
name="contentDisposition">attachment;filename="${downloadFileName}"</param>
<param
name="bufferSize">4096</param>
</result>
</action>
。查看粗斜體的部分,首先就是自定了被下載文件的路徑,inputPath,接著就是修改了contentType為二進(jìn)制方式。最后重新發(fā)布項(xiàng)目并運(yùn)行,鍵入地址進(jìn)行訪問(wèn):http://localhost:8080/struts2hello/download3.action
。很好,可以看到文件下載對(duì)話框,保存系統(tǒng)說(shuō)明.doc后再用word打開(kāi)它,內(nèi)容正確。
注意:而這種文件下載方式卻是存在安全隱患的,因?yàn)樵L問(wèn)者如果精通Struts 2的話,它可能使用這樣的帶有表單參數(shù)的地址來(lái)訪問(wèn):http://localhost:8080/struts2hello/download3.action?inputPath=/WEB-INF/web.xml,這樣的結(jié)果就是下載后的文件內(nèi)容是您系統(tǒng)里面的web.xml的文件的源代碼,甚至還可以用這種方式來(lái)下載任何其它JSP文件的源碼。這對(duì)系統(tǒng)安全是個(gè)很大的威脅。作為一種變通的方法,讀者最好是從數(shù)據(jù)庫(kù)中進(jìn)行路徑配置,然后把Action類中的設(shè)置inputPath的方法統(tǒng)統(tǒng)去掉,簡(jiǎn)言之就是刪除這個(gè)方法定義:
public void setInputPath(String value) {
inputPath = value;
}
。而實(shí)際情況則應(yīng)該成為 download3.action?fileid=1類似于這樣的形式來(lái)進(jìn)行。或者呢,讀者可以在execute()方法中進(jìn)行路徑檢查,如果發(fā)現(xiàn)有訪問(wèn)不屬于download下面文件的代碼,就一律拒絕,不給他們返回文件內(nèi)容。例如,我們可以把剛才類中的execute()方法加以改進(jìn),成為這樣:
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();// 真實(shí)文件路徑,去掉里面的..等信息
// 發(fā)現(xiàn)企圖下載不在 /download 下的文件, 就顯示空內(nèi)容
if(!downloadFile.startsWith(downloadDir)) {
return null;
}
return SUCCESS;
}
。這時(shí)候如果訪問(wèn)者再企圖下載web.xml的內(nèi)容,它只能得到一個(gè)空白頁(yè),現(xiàn)在訪問(wèn)者只能下載位于/download目錄下的文件。