Struts的FormFile與Commons-FileUpload控制元件使用心得
前一段時間剛來公司,看到一個專案中以前有人寫的struts程式碼。是使用了FormFile來處理關於檔案上傳的模組。但是用力一段時間後,發現出問題了。寫完的這個模組,上傳檔案是沒有問題的,但是當伺服器的空間較小的時候,穿一個比較大的檔案就出問題了,檔案還沒有上傳完,就丟擲一個錯誤的頁面,報告上傳模組出了問題,而且是Tomcat預設的出錯頁面。
於是想辦法,修改,檢視原始碼,發現原來寫這段程式碼的人是預設等檔案上傳完以後進入Action了才判斷檔案大小是否超出了限制。
但是,預設配置下使用struts的FormFile比較特殊,FormFile是struts包對外的一個介面,而且org.apache.struts.upload包是使用的commons-fileupload-1.0進行的封裝。如果使用了它來實現檔案上傳的功能,則必須是FormFile物件在被初始化以後才能使用
答案是:在進入Action之前就已經初始化好了!
因此,原先的設計:在Action中判斷檔案大小是根本不能在上傳過程中起到提示作用的,因為這時候檔案已經上傳完了。而且這個設計還有一個確定就是不能捕獲上傳過程中出現的任何問題。也就是說:在Action裡我們得到的FormFile物件是上傳的一個結果,而不是一個未上傳好就可以使用的物件!
那如何控制FormFile上傳的過程呢?顯然,在Action裡處理已經不能奏效了,想想別的辦法,讓我們翻看一下Struts的原始碼找找靈感吧。
這是struts1.1的org.apache.struts.upload包的描述:
(見附件)
從上圖我們可以看出有有CommonsMultipartRequestHandler和DiskMultipartRequestHandler兩個類實現了MultipartRequestHandler介面。
大家都知道,Commons-fileupload控制元件在上傳的時候,使用的enctype為:enctype="multipart/form-data"
但是這裡有兩個類,CommonsMultipartRequestHandler和DiskMultipartRequestHandler。到底哪個是處理FormFile的上傳的呢?這個問題應該從org.apache.struts.config包裡來找。
org.apache.struts.config包是用來處理struts配置檔案的資料的包。找到org.apache.struts.config. ControllerConfig。
看這幾行:
- /**
- * The fully qualified Java class name of the MultipartRequestHandler
- * class to be used.
- */
- protected String multipartClass =
- "org.apache.struts.upload.CommonsMultipartRequestHandler";
- public String getMultipartClass()
- {
- return (this.multipartClass);
- }
- publicvoid setMultipartClass(String multipartClass)
- {
- if (configured)
- {
- thrownew IllegalStateException("Configuration is frozen");
- }
- this.multipartClass = multipartClass;
- }
/**
* The fully qualified Java class name of the MultipartRequestHandler
* class to be used.
*/
protected String multipartClass =
"org.apache.struts.upload.CommonsMultipartRequestHandler";
public String getMultipartClass()
{
return (this.multipartClass);
}
public void setMultipartClass(String multipartClass)
{
if (configured)
{
throw new IllegalStateException("Configuration is frozen");
}
this.multipartClass = multipartClass;
}
這幾行的意思很明白,如果沒有在配置檔案中配置MultipartRequestHandler實現類的絕對路徑,那就使用org.apache.struts.upload.CommonsMultipartRequestHandler類預設處理。
^_^,這就是關鍵了:struts是預設使用org.apache.struts.upload.CommonsMultipartRequestHandler類來處理FormFile指定的上傳檔案的。
馬上轉到org.apache.struts.upload.CommonsMultipartRequestHandler來看看:
- /**
- *預設檔案上傳的大小是250M
- */
- publicstaticfinallong DEFAULT_SIZE_MAX = 250 * 1024 * 1024;
- /**
- *上傳檔案在記憶體中使用的緩衝區大小,超過次數值的資料寫入硬碟。
- */
- publicstaticfinalint DEFAULT_SIZE_THRESHOLD = 256 * 1024;
/**
*預設檔案上傳的大小是250M
*/
public static final long DEFAULT_SIZE_MAX = 250 * 1024 * 1024;
/**
*上傳檔案在記憶體中使用的緩衝區大小,超過次數值的資料寫入硬碟。
*/
public static final int DEFAULT_SIZE_THRESHOLD = 256 * 1024;
還有,最最重要的實現方法:
- /**
- * Parses the input stream and partitions the parsed items into a set of
- * form fields and a set of file items. In the process, the parsed items
- * are translated from Commons FileUpload <code>FileItem</code> instances
- * to Struts <code>FormFile</code> instances.
- * @param request The multipart request to be processed.
- * @throws ServletException if an unrecoverable error occurs.
- 就是這個函式處理上傳檔案的request,把request交給Commons FileUpload 控制元件處理,並解析FileItem轉換成Struts的FormFile。
- */
- publicvoid handleRequest(HttpServletRequest request) throws ServletException
/**
* Parses the input stream and partitions the parsed items into a set of
* form fields and a set of file items. In the process, the parsed items
* are translated from Commons FileUpload <code>FileItem</code> instances
* to Struts <code>FormFile</code> instances.
* @param request The multipart request to be processed.
* @throws ServletException if an unrecoverable error occurs.
就是這個函式處理上傳檔案的request,把request交給Commons FileUpload 控制元件處理,並解析FileItem轉換成Struts的FormFile。
*/
public void handleRequest(HttpServletRequest request) throws ServletException
再看看這個函式內部是怎麼實現的吧?
- // 使用了DiskFileUpload。
- // (Commons-FileUpload很老版本的一個上傳實現類了,還在用?我的顯示是Deprecated)
- DiskFileUpload upload = new DiskFileUpload();
- // 上傳最大值
- upload.setSizeMax((int) getSizeMax(ac));
- // 上傳檔案在記憶體中使用的緩衝區大小
- upload.setSizeThreshold((int) getSizeThreshold(ac));
- // 存在硬碟的什麼地方,一般是預設
- pload.setRepositoryPath(getRepositoryPath(ac));
// 使用了DiskFileUpload。
// (Commons-FileUpload很老版本的一個上傳實現類了,還在用?我的顯示是Deprecated)
DiskFileUpload upload = new DiskFileUpload();
// 上傳最大值
upload.setSizeMax((int) getSizeMax(ac));
// 上傳檔案在記憶體中使用的緩衝區大小
upload.setSizeThreshold((int) getSizeThreshold(ac));
// 存在硬碟的什麼地方,一般是預設
pload.setRepositoryPath(getRepositoryPath(ac));
接著看handleRequest如何處理request的:
- // Parse the request into file items.
- List items = null;
- try
- {
- items = upload.parseRequest(request);
- }
- //這裡是關鍵:上傳過程中出了超出最大值的異常了,如何處理?
- catch (DiskFileUpload.SizeLimitExceededException e)
- {
- // Special handling for uploads that are too big
- request.setAttribute(
- MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED,
- Boolean.TRUE);
- return;
- }
- //出了其他異常,如enctype不對,磁碟空間不足怎麼辦?
- catch (FileUploadException e)
- {
- log.error("Failed to parse multipart request", e);
- thrownew ServletException(e);
- }
// Parse the request into file items.
List items = null;
try
{
items = upload.parseRequest(request);
}
//這裡是關鍵:上傳過程中出了超出最大值的異常了,如何處理?
catch (DiskFileUpload.SizeLimitExceededException e)
{
// Special handling for uploads that are too big
request.setAttribute(
MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED,
Boolean.TRUE);
return;
}
//出了其他異常,如enctype不對,磁碟空間不足怎麼辦?
catch (FileUploadException e)
{
log.error("Failed to parse multipart request", e);
throw new ServletException(e);
}
這次一目瞭然了:
Struts根本沒有把上傳過程中出的超出最大值的異常帶到Action,而是把它放到了rquest的Attribute裡。
而出了其他異常如enctype不對,磁碟空間不足怎麼辦?很遺憾,Struts沒有去處理它,而是log了一下,拋給了上一層了。
那我一定要獲得這些全部異常咋辦呢?沒辦法,自己定製一個MultipartRequestHandler吧,那樣就能徹底解決上傳過程中的控制問題了!
在此之前,我們得先去最新版的commons-fileupload控制元件看看上傳過程中可能丟擲多少異常?
- //所有上傳異常的父類
- org.apache.commons.fileupload.FileUploadException
- //注意:這個類的類名是FileUploadBase.SizeLimitExceededException是個public內
- //部類。上傳的formdata總的資料超出了規定大小
- org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException
- //注意:也是個內部類。這個才是上傳的檔案超出了規定大小
- org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException
- //其它的,也看看吧:
- org.apache.commons.fileupload.FileUploadBase.FileUploadIOException
- org.apache.commons.fileupload.FileUploadBase.InvalidContentTypeException
- org.apache.commons.fileupload.FileUploadBase.IOFileUploadException
- org.apache.commons.fileupload.FileUploadBase.UnknownSizeException
//所有上傳異常的父類
org.apache.commons.fileupload.FileUploadException
//注意:這個類的類名是FileUploadBase.SizeLimitExceededException是個public內
//部類。上傳的formdata總的資料超出了規定大小
org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException
//注意:也是個內部類。這個才是上傳的檔案超出了規定大小
org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException
//其它的,也看看吧:
org.apache.commons.fileupload.FileUploadBase.FileUploadIOException
org.apache.commons.fileupload.FileUploadBase.InvalidContentTypeException
org.apache.commons.fileupload.FileUploadBase.IOFileUploadException
org.apache.commons.fileupload.FileUploadBase.UnknownSizeException
要想獲得儘可能仔細的資料就在處理的try/catch塊裡把上面的異常都catch一下,放到request的attribute裡去就OK了。
另外還有要說的是,最好用commons-fileupload控制元件的最新版本,因為DiskFileUpload這個類,commons-fileupload已經棄用了,取而代之的是ServletFileUpload類了,所以一定要注意!切記,切記…..
這是我寫的CommonsMultipartRequestHandler替代類的public void handleRequest(HttpServletRequest request) throws ServletException函式:
- publicvoid handleRequest(HttpServletRequest request) throws ServletException
- {
- // Get the app config for the current request.
- ModuleConfig ac = (ModuleConfig) request.getAttribute(Globals.MODULE_KEY);
- // DiskFileItem工廠,主要用來設定上傳檔案的引數
- DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
- // 上傳檔案所用到的緩衝區大小,超過此緩衝區的部分將被寫入到磁碟
- fileItemFactory.setSizeThreshold((int) this.getSizeThreshold(ac));
- // 上傳檔案用到的臨時檔案存放位置
- fileItemFactory.setRepository(this.getRepository(ac));
- // 使用fileItemFactory為引數例項化一個ServletFileUpload物件
- // 注意:該物件為commons-fileupload-1.2新增的類.
- // 對於1.2以下的commons-fileupload版本並不存在此類.
- ServletFileUpload upload = new ServletFileUpload(fileItemFactory);
- // 從session中讀取對本次上傳檔案的最大值的限制
- String maxUploadSize = (String)request.getSession().
- getAttribute(BasicConstants.maxUploadSize);
- // 獲取struts-config檔案中controller標籤的maxFileSize屬性來確定預設上傳的限制
- // 如果struts-config檔案中controller標籤的maxFileSize屬性沒設定則使用預設的上傳限制250M.
- long defaultOrConfigedMaxUploadSize = this.getSizeMax(ac);
- if (maxUploadSize != null && maxUploadSize != "")
- {
- // 如果maxUploadSize設定不正確則上傳限制為defaultOrConfigedMaxUploadSize的值
- // 正確則為maxUploadSize轉換成的位元組數
- upload.setSizeMax((long) this.convertSizeToBytes(
- maxUploadSize, defaultOrConfigedMaxUploadSize));
- }
- else
- {
- // 如果maxUploadSize沒設定則使用預設的上傳限制
- upload.setSizeMax(defaultOrConfigedMaxUploadSize);
- }
- // 從session中清空maxUploadSize
- request.getSession().removeAttribute("maxUploadSize");
- // Create the hash tables to be populated.
- elementsText = new Hashtable();
- elementsFile = new Hashtable();
- elementsAll = new Hashtable();
- // Parse the request into file items.
- List items = null;
- // ServletFileUpload類來處理表單請求
- // 丟擲的異常為FileUploadException的子異常
- // 如果捕獲這些異常就將捕獲的異常放到session中返回.
- try
- {
- items = upload.parseRequest(request);
- }
- catch (FileUploadBase.SizeLimitExceededException e)
- {
- // 請求資料的size超出了規定的大小.
- request.getSession().setAttribute(
- BasicConstants.baseSizeLimitExceededException, e);
- return;
- }
- catch (FileUploadBase.FileSizeLimitExceededException e)
- {
- // 請求檔案的size超出了規定的大小.
- request.getSession().setAttribute(
- BasicConstants.baseFileSizeLimitExceededException, e);
- return;
- }
- catch (FileUploadBase.IOFileUploadException e)
- {
- // 檔案傳輸出現錯誤,例如磁碟空間不足等.
- request.getSession().setAttribute(
- BasicConstants.baseIOFileUploadException, e);
- return;
- }
- catch (FileUploadBase.InvalidContentTypeException e)
- {
- // 無效的請求型別,即請求型別enctype != "multipart/form-data"
- request.getSession().setAttribute(
- BasicConstants.baseInvalidContentTypeException, e);
- return;
- }
- catch (FileUploadException e)
- {
- // 如果都不是以上子異常,則丟擲此總的異常,出現此異常原因無法說明.
- request.getSession().setAttribute(
- BasicConstants.FileUploadException, e);
- return;
- }
- // Partition the items into form fields and files.
- Iterator iter = items.iterator();
- while (iter.hasNext())
- {
- FileItem item = (FileItem) iter.next();
- if (item.isFormField())
- {
- addTextParameter(request, item);
- }
- else
- {
- addFileParameter(item);
- }
- }
- }
public void handleRequest(HttpServletRequest request) throws ServletException
{
// Get the app config for the current request.
ModuleConfig ac = (ModuleConfig) request.getAttribute(Globals.MODULE_KEY);
// DiskFileItem工廠,主要用來設定上傳檔案的引數
DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
// 上傳檔案所用到的緩衝區大小,超過此緩衝區的部分將被寫入到磁碟
fileItemFactory.setSizeThreshold((int) this.getSizeThreshold(ac));
// 上傳檔案用到的臨時檔案存放位置
fileItemFactory.setRepository(this.getRepository(ac));
// 使用fileItemFactory為引數例項化一個ServletFileUpload物件
// 注意:該物件為commons-fileupload-1.2新增的類.
// 對於1.2以下的commons-fileupload版本並不存在此類.
ServletFileUpload upload = new ServletFileUpload(fileItemFactory);
// 從session中讀取對本次上傳檔案的最大值的限制
String maxUploadSize = (String)request.getSession().
getAttribute(BasicConstants.maxUploadSize);
// 獲取struts-config檔案中controller標籤的maxFileSize屬性來確定預設上傳的限制
// 如果struts-config檔案中controller標籤的maxFileSize屬性沒設定則使用預設的上傳限制250M.
long defaultOrConfigedMaxUploadSize = this.getSizeMax(ac);
if (maxUploadSize != null && maxUploadSize != "")
{
// 如果maxUploadSize設定不正確則上傳限制為defaultOrConfigedMaxUploadSize的值
// 正確則為maxUploadSize轉換成的位元組數
upload.setSizeMax((long) this.convertSizeToBytes(
maxUploadSize, defaultOrConfigedMaxUploadSize));
}
else
{
// 如果maxUploadSize沒設定則使用預設的上傳限制
upload.setSizeMax(defaultOrConfigedMaxUploadSize);
}
// 從session中清空maxUploadSize
request.getSession().removeAttribute("maxUploadSize");
// Create the hash tables to be populated.
elementsText = new Hashtable();
elementsFile = new Hashtable();
elementsAll = new Hashtable();
// Parse the request into file items.
List items = null;
// ServletFileUpload類來處理表單請求
// 丟擲的異常為FileUploadException的子異常
// 如果捕獲這些異常就將捕獲的異常放到session中返回.
try
{
items = upload.parseRequest(request);
}
catch (FileUploadBase.SizeLimitExceededException e)
{
// 請求資料的size超出了規定的大小.
request.getSession().setAttribute(
BasicConstants.baseSizeLimitExceededException, e);
return;
}
catch (FileUploadBase.FileSizeLimitExceededException e)
{
// 請求檔案的size超出了規定的大小.
request.getSession().setAttribute(
BasicConstants.baseFileSizeLimitExceededException, e);
return;
}
catch (FileUploadBase.IOFileUploadException e)
{
// 檔案傳輸出現錯誤,例如磁碟空間不足等.
request.getSession().setAttribute(
BasicConstants.baseIOFileUploadException, e);
return;
}
catch (FileUploadBase.InvalidContentTypeException e)
{
// 無效的請求型別,即請求型別enctype != "multipart/form-data"
request.getSession().setAttribute(
BasicConstants.baseInvalidContentTypeException, e);
return;
}
catch (FileUploadException e)
{
// 如果都不是以上子異常,則丟擲此總的異常,出現此異常原因無法說明.
request.getSession().setAttribute(
BasicConstants.FileUploadException, e);
return;
}
// Partition the items into form fields and files.
Iterator iter = items.iterator();
while (iter.hasNext())
{
FileItem item = (FileItem) iter.next();
if (item.isFormField())
{
addTextParameter(request, item);
}
else
{
addFileParameter(item);
}
}
}
其它部分均未做什麼大改變。
好了,替代類寫好了,我們怎麼去用呢?
這樣:在struts-config檔案中寫配置:
- <!-- ========== Controller Configuration ================================ -->
- <controller>
- <!-- The "input" parameter on "action" elements is the name of a
- local or global "forward" rather than a module-relative path -->
- <set-propertyvalue="true"property="inputForward"/>
- <set-propertyvalue="text/html; charset=UTF-8"
- property="contentType"/>
- <!-- 通過寫類的全名來替代struts預設的MultipartRequestHandler -->
- <set-propertyproperty="multipartClass"
- value="com.amplesky.commonmodule.struts.AmpleskyMultipartRequestHandler"/>
- <!-- 規定的上傳檔案的最大值 -->
- <set-propertyproperty="maxFileSize"value="15M"/>
- <!-- 緩衝區大小 -->
- <set-propertyproperty="memFileSize"value="5M"/>
- </controller>
好了!現在我們再用FormFile上傳檔案,可以在上傳之前動態設定或者從配置檔案設定上傳檔案的大小,一旦上傳過程中出現了異常,就會被寫入request的attributs裡。當進入action的時候,通過在Action裡獲取異常就可以判斷上傳過程中出了什麼問題了,而且在上傳過程中檔案一旦超出了規定大小,或者磁碟大小不足的情況會立即中斷上傳的。這樣我們的功能就實現了。
最後,感慨一下,
1.感覺commons-fileupload還是挺好用的,但是FormFile的使用不大好,基本上誤導能力很強,網上關於FormFile的資料說明很少,以上這些都是我自己摸索出來的。
2.Struts 1都到了1.3版本了,但是對於FormFile的實現依然使用commons-fileupload-1.0版本的DiskFileUpload類,可見更新也不怎麼樣。其實我覺得這是apache大部分開源工具的一個通病:更新確實不怎麼快,commons框架裡邊的原始碼和jar包不一致,還有很多是2004或者2003年的…要命的是居然不支援javase5的新特性????
3. 認為把異常放到request的attributs裡不好,曾經嘗試過丟擲異常然後通過配置exception-controller來抓去異常處理,但是抓不到,怎麼都抓不到,後來翻了大半天原始碼才發現:struts在處理異常請求的時候將出現的ServletException和IOExcepton都交給了上層去處理了,根本不會丟擲來。所以這兩種異常是抓不到的。
評論
to iamnotachild:引用
如果我使用的是common-fileupload對檔案進行上傳。
那麼,能區分我上傳的檔案是空檔案還是對應的路徑下的檔案不存在麼?
怎麼區分?
老實說,我自己也沒有測試過使用Commons-fileupload控制元件能不能正確處理,但是它的異常裡邊是有這個的,org.apache.commons.fileupload.FileUploadBase.IOFileUploadException能夠處理IO的異常,應該就能抓到.但我自己沒有測試過.
我問一個問題。
如果我使用的是common-fileupload對檔案進行上傳。
那麼,能區分我上傳的檔案是空檔案還是對應的路徑下的檔案不存在麼?
怎麼區分?
struts的formfile是不能區分的。 引用
這是不是意味著, 我不用struts的formfile,而改成直接在action裡面用common-fileupload進行上傳,就能在action中自由控制對各種可能的錯誤的處理。
是的,不用formfile,直接在action裡面用common-fileupload進行上傳,網上的例子大多是這樣用的。 分析得挺徹底的, 沒注意過這種問題, 因為直接就用的是tomcat的出錯頁面了;
引用
如果使用了它來實現檔案上傳的功能,則必須是FormFile物件在被初始化以後才能使用,那什麼時候它才是被初始化的呢?
答案是:在進入Action之前就已經初始化好了!
引用
3. 認為把異常放到request的attributs裡不好,曾經嘗試過丟擲異常然後通過配置exception-controller來抓去異常處理,但是抓不到,怎麼都抓不到,後來翻了大半天原始碼才發現:struts在處理異常請求的時候將出現的ServletException和IOExcepton 都交給了上層去處理了,根本不會丟擲來。所以這兩種異常是抓不到的。
這是不是意味著, 我不用struts的formfile,而改成直接在action裡面用common-fileupload進行上傳,就能在action中自由控制對各種可能的錯誤的處理。