struts2 檔案上傳和下載,以及部分原始碼解析

beifengwang發表於2014-08-26

struts2 檔案上傳 和部分原始碼解析,以及一般上傳原理

(1) 單檔案上傳

一.簡介

Struts2並未提供自己的請求解析器,也就是就Struts2不會自己去處理multipart/form-data的請求,它需要呼叫其他請求解析器,將HTTP請求中的表單域解析出來。但Struts2在原有的上傳解析器基礎

上做了進一步封裝,更進一步簡化了檔案上傳。
Struts2預設使用的是Jakarta的Common-FileUpload框架來上傳檔案,因此,要在web應用中增加兩個Jar檔案:commons-fileupload-1.2.jar和commons-io-1.3.1.jar。它在原上傳框架上做了進一步封裝

,簡化了檔案上傳的程式碼實現,取消了不同上傳框架上的程式設計差異。
如果要改成其它的檔案上傳框架,可以修改struts.multipart.parser常量的值為cos/pell,預設值是jakata。並在classpath中增加相應上傳元件的類庫

例如配置成cos上傳

struts.multipart.parser=cos

struts.multipart.maxSize=1024 指定檔案的最大字結數

二.原理

不管用common-fileUPload框架,還是用cos,都是透過將HTTP的資料儲存到臨時資料夾,然後Struts使用fileUpload攔截器將檔案繫結到Action的例項中。
也就是配置檔案的
我們可以透過原始碼struts2-code-XX.jar的struts-default.xml檔案找到

開啟這個類的原始碼可以看見相關如下:
/**

*

* Interceptor that is based off of {@link MultiPartRequestWrapper}, which is automatically applied for any request that
* includes a file. It adds the following parameters, where [File Name] is the name given to the file uploaded by the
* HTML form:
*

*

      *

*

  • [File Name] : File - the actual File


*

*

  • [File Name]ContentType : String - the content type of the file


*

*

  • [File Name]FileName : String - the actual name of the file uploaded (not the HTML name)


*

*

*

也就是說我們需要三個變數File(表單的name),其他兩個引數透過set個體方法有strtus呼叫

接著下面是一些國際化提示的東西:
* processed for all i18n requests. You can override the text of these messages by providing text for the following
* keys:
*

  • struts.messages.error.uploading - a general error that occurs when the file could not be uploaded


*

*

  • struts.messages.error.file.too.large - occurs when the uploaded file is too large


*

*

  • struts.messages.error.content.type.not.allowed - occurs when the uploaded file does not match the expected
    * content types specified


*

*

  • struts.messages.error.file.extension.not.allowed - occurs when the uploaded file does not match the expected
    * file extensions specified


*

例如struts.messages.error.content.type.not.allowed 表示檔案型別錯誤:也就是說如果我們給攔截器配置了屬性allowedTypes 例如:
image/bmp,image/png,image/gif,image/jpeg,image/jpg 但是上傳的時候沒有上傳規定的型別
struts2就會去我們的資原始檔去找key為struts.messages.error.content.type.not.allowed的國際化資源給與提示這時候我們可以在我們的資源中配置這個key:
例如:struts.messages.error.content.type.not.allowed=您上傳的檔案型別只能為...!請重新選擇!
(當然需要)globalMessages為資源字首,然後透過:來顯示提示

*

*

      *

*

  • maximumSize (optional) - the maximum size (in bytes) that the interceptor will allow a file reference to be set
    * on the action. Note, this is not related to the various properties found in struts.properties.
    * Default to approximately 2MB.


*

*

  • allowedTypes (optional) - a comma separated list of content types (ie: text/html) that the interceptor will allow
    * a file reference to be set on the action. If none is specified allow all types to be uploaded.


*

*

  • allowedExtensions (optional) - a comma separated list of file extensions (ie: .html) that the interceptor will allow
    * a file reference to be set on the action. If none is specified allow all extensions to be uploaded.


*

*

上面則是攔截器的相關引數,一目瞭然:maximumSize 上傳檔案最大多少 預設:2MB。allowedTypes容許的上傳型別。allowedExtensions容許的副檔名
接著是相關action的程式碼說明:

* package com.example;
*

* import java.io.File;
* import com.opensymphony.xwork2.ActionSupport;
*

* public UploadAction extends ActionSupport {
* private File file;
* private String contentType;
* private String filename;
*

* public void setUpload(File file) {
* this.file = file;
* }
*

* public void setUploadContentType(String contentType) {
* this.contentType = contentType;
* }
*

* public void setUploadFileName(String filename) {
* this.filename = filename;
* }
*

* public String execute() {
* //...
* return SUCCESS;
* }
* }
其實最主要的是set方法的確定:我們跟蹤到大約238行:
String contentTypeName = inputName + "ContentType";
String fileNameName = inputName + "FileName";
最終確定我們的private File file;屬性名稱可以隨便,
但是filenam和contenttype的set方法要有規定 例如:
如果private File myFile;

則對應的其他的兩個屬性set方法如下:

public void setMyFileContentType(String contentType) {
this.contentType = contentType;//當然contentType可以隨便起名 最終要的是set+MyFile+ContentType方法
}

public void setMyFileFileName(String filename) {
this.filename = filename;/當然filename可以隨便起名 最終要的是set+MyFile+FileName方法
}
以下是例項:
三.需要的jar包(預設使用commons-fileupload,如果使用cos,要將jar引進來)

commons-logging-1.1.jar
freemarker-2.3.8.jar
ognl-2.6.11.jar
struts2-core-2.0.6.jar
xwork-2.0.1.jar
commons-io-1.3.1.jar
commons-fileupload-1.2.jar

四.例項

1.首先,建立上傳頁面

Html程式碼
1.
2.
3.
4.
5.
6.
19.
20.
21.
22. 
23. 
24. 
25. 
26. 

27.
28.
29.

2.action

1.package com;
2.
3.import java.io.BufferedInputStream;
4.import java.io.BufferedOutputStream;
5.import java.io.File;
6.import java.io.FileInputStream;
7.import java.io.FileOutputStream;
8.import java.io.InputStream;
9.import java.io.OutputStream;
10.import java.util.Date;
11.
12.import org.apache.struts2.ServletActionContext;
13.
14.import com.opensymphony.xwork2.ActionSupport;
15.
16.public class FileUploadAction extends ActionSupport {
17.
18. private static final long serialVersionUID = 6452146812454l;
19.
20. private File upload;
21.
22. private String uploadContentType;
23.
24. private String uploadFileName;
25.
26. private String imageFileName;
27.
28. public String getUploadContentType() {
29. return uploadContentType;
30. }
31.
32. public void setUploadContentType(String uploadContentType) {
33. this.uploadContentType = uploadContentType;
34. }
35.
36. public File getUpload() {
37. return upload;
38. }
39.
40. public void setUpload(File upload) {
41. this.upload = upload;
42. }
43.
44. public String getUploadFileName() {
45. return uploadFileName;
46. }
47.
48. public void setUploadFileName(String uploadFileName) {
49. this.uploadFileName = uploadFileName;
50. }
51.
52. public void setImageFileName(String imageFileName) {
53. this.imageFileName = imageFileName;
54. }
55.
56. public String getImageFileName() {
57. return imageFileName;
58. }
59.
60. private static void copy(File src, File dst) {
61. try {
62. InputStream in = null;
63. OutputStream out = null;
64. try {
65. in = new BufferedInputStream(new FileInputStream(src));
66. out = new BufferedOutputStream(new FileOutputStream(dst));
67. byte[] buffer = new byte[1024*10];
68. while (in.read(buffer) > 0) {
69. out.write(buffer);
70. }
71. } finally {
72. if (null != in) {
73. in.close();
74. }
75. if (null != out) {
76. out.close();
77. }
78. }
79. } catch (Exception e) {
80. e.printStackTrace();
81. }
82. }
83.
84. @Override
85. public String execute() {
86. System.out.println(uploadFileName);
87.
88. imageFileName = System.currentTimeMillis() + uploadFileName.substring(uploadFileName.lastIndexOf("."));
89. File imageFile = new File(ServletActionContext.getServletContext()
90. .getRealPath("/uploadImages")
91. + "/" + imageFileName); //我們自己重新定義的檔名,也可以直接用 uploadFileName
92. copy(upload, imageFile);
93. return SUCCESS;
94. }
95.
96.}
97
表單的enctype ="multipart/form-data,與一般的上傳一樣.

會將upload繫結到action的upload,其次他還會將上傳記檔案的MIME型別繫結到uploadContentType,檔名繫結到uploadFileName中,他們是透過

setUploadContentType和setUploadFileName進行繫結的,下面進行的多檔案上傳也是同個道理,不過要用陣列或者是list來進行繫結,然後多個檔案的MIME型別也會繫結到以陣列

名字加ContentType和FileName的字串陣列中。 比如說上傳的檔案的陣列名為:File[] uploads,則它們的MIME型別繫結的對應的陣列是uploadsFileName和uploadsContentType.

3.struts.xml的配置

Xml程式碼

Xml程式碼
1.
2.
5.
6.
7.
8.
9.
10.
11.
12. image/bmp,image/png,image/gif,image/jpeg,image/jpg
13.
14.
15.
16. /fileUpload.jsp
17. /showUpload.jsp
18.
19.
20.

4.最後是web.xml的配置

Xml程式碼
1.
2.
7.
8.
9. struts-cleanup
10.
11. org.apache.struts2.dispatcher.ActionContextCleanUp
12.
13.
14.
15.
16. struts2
17.
18. org.apache.struts2.dispatcher.FilterDispatcher
19.
20.
21.
22.
23. struts-cleanup
24. /*
25.
26.
27.
28. struts2
29. /*
30.
31.
32.
33. index.jsp
34.
35.

(2) 多檔案上傳

多檔案上傳
與單檔案上傳相似,實現多檔案你可以將多個繫結Action的陣列或列表。如下例所示。






清單14 多檔案上傳JSP程式碼片段
如果你希望繫結到陣列,Action的程式碼應類似:

private File[] uploads;
private String[] uploadSFileName;
private String[] uploadSContentType;

多檔案上傳陣列繫結Action程式碼片段
如果你想繫結到列表,則應類似:

private List uploads ;
private List uploadSFileName ;
private List uploadSContentType ;
多檔案上傳列表繫結Action程式碼片段

另外是一般上傳檔案的原理:當然具體可以看http協議的rfc文件:
關於multipart/form-data 相關資料可以看; 大約在[Page 1]的地方有介紹
表單配置multipart/form-data 說明以二進位制流的方式傳輸表單欄位的資料:
我們透過以下程式碼看到request資料流中的內容:

PrintWriter out = response.getWriter();
InputStream is = request.getInputStream();

BufferedReader br = new BufferedReader(
new InputStreamReader(is));
String buffer = null;
while( (buffer = br.readLine()) != null)
{
//在頁面中顯示讀取到的請求引數
out.println(buffer + "
");
}
out.flush();
out.close();
例如:我上傳一個檔案D:\apache-tomcat-6018\bin\version.sh (tomcat版本檔案)
最終頁面顯示:
-----------------------------7da1052ec05fe
Content-Disposition: form-data; name="ff"; filename="D:\apache-tomcat-6018\bin\version.sh"
Content-Type: text/plain

#!/bin/sh

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
#
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# resolve links - $0 may be a softlink
PRG="$0"

while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`/"$link"
fi
done

PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh

# Check that target executable exists
if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
echo "Cannot find $PRGDIR/$EXECUTABLE"
echo "This file is needed to run this program"
exit 1
fi

exec "$PRGDIR"/"$EXECUTABLE" version "$@"

-----------------------------7da1052ec05fe--
我們發現我們上傳的內容在
-----------------------------7da1052ec05fe
Content-Disposition: form-data; name="ff"; filename="D:\apache-tomcat-6018\bin\version.sh"
Content-Type: text/plain和-----------------------------7da1052ec05fe--中間
因此我們可以透過以下程式碼來獲取上傳內容並儲存:

//取得HttpServletRequest的InputStream輸入流
InputStream is = request.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String buffer = null;
//迴圈讀取請求內容的每一行內容
while( (buffer = br.readLine()) != null)
{
//如果讀到的內容以-----------------------------開始,
//且以--結束,表明已到請求內容尾
if(buffer.endsWith("--") && buffer
.startsWith("-----------------------------"))//length為29
{
//跳出迴圈
break;
}
//如果讀到的內容以-----------------------------開始,表明開始了一個表單域
if(buffer.startsWith("-----------------------------"))
{
//如果下一行內容中有filename字串,表明這是一個檔案域
if (br.readLine().indexOf("filename") > 1)
{
//跳過兩行,開始處理上傳的檔案內容
br.readLine();
br.readLine();
//以系統時間為檔名,建立一個新檔案
File file = new File(request.getRealPath("/")
+ System.currentTimeMillis());
//當然我們可以讀取filenam來儲存這裡簡化
//建立一個檔案輸出流
PrintStream ps = new PrintStream(new FileOutputStream(file));
String content = null;
//接著開始讀取檔案內容
while( (content = br.readLine()) != null)
{
//如果讀取的內容以-----------------------------開始,
//表明開始了下一個表單域內容
if(content.startsWith("-----------------------------"))length為29
{
//跳出處理
break;
}
//將讀到的內容輸出到檔案中
ps.println(content);
}
//關閉輸出
ps.flush();
ps.close();
}
}
}
br.close();

關於strtus2下載:
下載最終是透過contentType和資料流將資料輸出到客戶端來實現,在struts中也是透過InputStream和相關的配置來實現:
同樣最終到strtus的下載相關的原始碼:org.apache.struts2.dispatcher.StreamResult我們看到

public static final String DEFAULT_PARAM = "inputName";

protected String contentType = "text/plain";
protected String contentLength;
protected String contentDisposition = "inline";//線上
protected String contentCharSet ;
protected String inputName = "inputStream";
protected InputStream inputStream;
protected int bufferSize = 1024;
protected boolean allowCaching = true;

當然這些引數都可以在 中配置 例如;
\uploads\document.pdf

Application/pdf

targetFile
attachment;filename="document.pdf"

2048
其中:
contentType:指定被下載檔案的檔案型別。 application/octet-stream 預設值,可以下載所有型別
inputName:指定被下載檔案的入口輸入流, 和DownloadAction中的getInputStream()對應,主要是獲得實際資原始檔
contentDisposition:指定下載的檔名和顯示方式,一般和檔名一致,但是要注意中檔名儲存時亂碼問題,解決辦法就是進行編碼處理

targetFile
是下載的入口 我們不需要在我們的action裡面配置targetFile變數 但需要getTargetFile方法,預設需要getInputStream()方法 也就是:inputName引數的值就是入口方法去掉get字首、首字母小寫的

字串

我們的action裡面的程式碼如下:

private String inputPath;//透過strtus獲取檔案地址 也可以直接寫例如:String inputPath = ServletActionContext.getRequest().getRealPath("\uploads\document.pdf");

public void setInputPath(String value)
{
inputPath = value;
}
public InputStream getTargetFile() throws Exception
{
return ServletActionContext.getServletContext().getResourceAsStream(inputPath);
}
如果報以下錯誤:
Can not find a java.io.InputStream with the name [targetFile] in the invocation stack. Check the tag specified for this action.

實際問題是ServletActionContext.getServletContext().getResourceAsStream(inputPath);找不到資源,請檢查你的path是否正確。
而關於下載實際struts做了什麼呢?我們看一部分原始碼程式碼就很明白了:

HttpServletResponse oResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE);
// Set the content type
...
//Set the content length
...
// Set the content-disposition
...
// Set the cache control headers if neccessary
...
// Get the outputstream
//------------
oOutput = oResponse.getOutputStream();

if (LOG.isDebugEnabled()) {
LOG.debug("Streaming result [" + inputName + "] type=[" + contentType + "] length=[" + contentLength +
"] content-disposition=[" + contentDisposition + "] charset=[" + contentCharSet + "]");
}

// Copy input to output
LOG.debug("Streaming to output buffer +++ START +++");
byte[] oBuff = new byte[bufferSize];
int iSize;
while (-1 != (iSize = inputStream.read(oBuff))) {
oOutput.write(oBuff, 0, iSize);
}
LOG.debug("Streaming to output buffer +++ END +++");

// Flush
oOutput.flush();
}
finally {
if (inputStream != null) inputStream.close();
if (oOutput != null) oOutput.close();
}
//-----------
很簡單,就像以前在servlet中一樣透過getOutputStream 和配置content type ,content-disposition,cache control,content length這些引數的來實現。

這樣就很簡單的實現了下載功能。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29212814/viewspace-1258043/,如需轉載,請註明出處,否則將追究法律責任。

相關文章