跳到主要內容

JavaEE 檔案上傳範例-使用Cos套件

Java EE規格雖然是針對網路應用開發的,但直到 Servlet 3.0 前都沒有提供檔案上傳的功能
Servlet 3.0的上傳機制可以參考這篇
因為還沒打算轉換到Servlet 3.0 的打算下,先研究使用套件的解決方案
時下較為流行的上傳套件共有 Cos、FileUpload、SmartUpload
Cos套件是 O'reilly 公司提供,可至 http://www.servlets.com/cos 下載套件(cos-26Dec2008.zip)
FileUpload 的下載位置: http://commons.apache.org/fileupload/
FileUpload 使用須具備的套件:http://commons.apache.org/io/
SmartUpload 的網站已關閉,大概未來也不會繼續開發了

FileUpload 具有較多的功能,也有提供方便用來做 Ajax 應用的 listener
不過純以上傳效率來說Cos是最優秀的,贏過其他套件不少
這次實作選擇使用Cos

先提到上傳的介紹
撰寫上傳的功能必須先撰寫好一個頁面
並由表單以POST的形式將資料傳送到上傳處理的頁面
表單的編碼方式也與一般有所不同,enctype屬性共有三種值
1.application/x-www-form-urlencoded 是預設的編碼方式,它只處理表單域裡的value值
    採用這種編碼方式的表單會將表單域的值處理成URL編碼方式
2.multipart/form-data 編碼會以二進制流的方式來處理表單數據
   它把文件域指定文件的內容也封裝到請求參數里
   一旦設置了這種方式,就無法透過HttpServletRequest.getParameter()請求獲取請求參數
3.text/plain 編碼方式當表單的action屬性為mailto:URL的形式時比較方便
    這種方式主要適用於直接通過表單發送郵件的方式

這次實作的內容為撰寫一個提供圖片及附件上傳的功能
先寫好提供表單的網頁
designUploader.jsp======

.....
      <form action="UploadHandler" method="POST" enctype="multipart/form-data">
          <label>正面:</label><input type="file" id="f_pic" name="f_pic" value="" width="20" /><br />
          <label>背面:</label><input type="file" id="b_pic" name="b_pic" value="" width="20" /><br />
          <label>附件:</label><input type="file" id="append" name="append" value="" width="20" /><br />
          <input type="submit" value="Upload" /><br />
          <span>${uploadInfo.message}</span>
      </form>
....

解壓縮下載好的 Cos套件,並將 lib 裡的 cos.jar 複製到 WEB_INF 目錄底下的 lib 裡
接著撰寫處理上傳的頁面

使用Cos時,可以使用兩個類別來進行上傳工作:
1.  MultipartRequest     2.  MultipartParser

一般情況下使用MultipartRequest即可,不需要複雜的設定就可以輕鬆上傳物件
實際上MultipartRequest封裝了MultipartParser
在構造MultipartRequest實例時,建構了MultipartParser實例
建構 parser 的過程取得了上傳的 InputStream,但並不會真正讀取
然後透過 MultipartParser的readNextPart() method,從request流中讀取數據
區別出 InputStream 中的參數域和文件域
如果是參數的話用ParamPart類封裝;如果是文件的話用FilePart封裝
此時如果設置了重新命名策略,則將在Server端新建一個新命名的空白物件
接著用FilePart的writeTo(saveDir)方法將流數據寫到硬碟中,文件上傳完成

MultipartRequest 範例:
UploadHandler.java=========

package tw.vencs;

import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.http.*;
import com.oreilly.servlet.MultipartRequest;

public class UploadHandler extends HttpServlet{

      public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
       
           String saveDirectory = .....;           //uploaded files' store dictionary 
           int maxPostSize=5*1024*1024;     //limit of file capacity
           String FileName=null;      
           String ContentType=null;             //declare file type
       
           int count = 0;                              //count numbers of file uploaded

           MultipartRequest multi = new MultipartRequest(request,saveDirectory,maxPostSize, "UTF-8");
           //get all types and descriptions from uploaded files
           Enumeration filename = multi.getFileNames();
           Enumeration filesdc = multi.getParameterNames();
         
           while(filename.hasMoreElements()){
                  String name = (String)filename.nextElement();
                  String dc = (String)filesdc.nextElement();
                  FileName = multi.getFilesystemName(name);
                  ContentType = multi.getContentType(name);

                  if(FileName != null){
                      count++;
                  }
           }
           ............
      }
}

當 MultipartRequest 建立好的時候就是檔案上傳完畢的時候
不設定檔案上傳大小時,最大上傳限制預設是 1mb
如果上傳檔案大小超過設定的大小時會丟出例外
雖然看API的說明是會丟出 ExceededSizeException,不過結果似乎是裝在IOException

命名策略是指上傳的檔案遇到了檔名重複時的處理方式
有自訂處理方式可以自行撰寫,如下:
RandomFileRenamePolicy.java===========

package tw.vencs;

import java.io.File;
import java.util.Date;
import com.oreilly.servlet.multipart.FileRenamePolicy;

public class RandomFileRenamePolicy implements FileRenamePolicy {

    public File rename(File file) {
      String body="";
      String ext="";
      Date date = new Date();
      int pot=file.getName().lastIndexOf(".");
      if(pot!=-1){
          body= date.getTime() +"";
          ext=file.getName().substring(pot);
      }else{
          body=(new Date()).getTime()+"";
          ext="";
      }
      String newName=body+ext;
      file=new File(file.getParent(),newName);
      return file;

    }
}

這個命名規則會取得時間來做為隨機命名的依據,接著將MultipartRequest改寫成:
RandomFileRenamePolicy rfrp=new RandomFileRenamePolicy();
MultipartRequest multi = new MultipartRequest(request,saveDirectory,maxPostSize, "UTF-8",rfrp);

因為我實作的需求是希望能限制上傳檔案的大小
不管是怎麼樣的上傳套件都沒辦法再接收資料前得知檔案大小
畢竟那等同於Server端直接從Client讀取資料,權限上不會被允許
JavaScript可以做到檢查檔案大小的功能
<script language="JavaScript">
    var checkImageSize = true;  //是否檢查圖片檔案大小 
    var ImageSizeLimit = 100000;  //上傳上限,單位:byte 
   
    function checkFile() { 
        var f = document.forms[0]; 
        var re = /\.(jpg|gif)$/i;  //允許的圖片副檔名 
        if (checkImageType && !re.test(f.file1.value)) { 
            alert("只允許上傳JPG或GIF影像檔"); 
        } else { 
            var img = new Image(); 
            img.onload = checkImage; 
            img.src = f.file1.value; 
        } 
        ...
        document.forms[0].submit();
    } 
    function checkImage() { 
        ...
        if (checkImageSize && this.fileSize > ImageSizeLimit) {
            showMessage('檔案大小','kb',this.fileSize/1000,ImageSizeLimit/1000);         
        }
    } 
    function showMessage(kind,unit,real,limit) { 
        var msg = "選擇的圖片kind為 real unit\n超過了上傳上限 limit unit\n不允許上傳!" 
        alert(msg.replace(/kind/,kind).replace(/unit/g,unit).replace(/real/,real).replace(/limit/,limit)); 
    } 
</script>


不過預防Client端沒開啟JavaScript的情形,我再查了些資料
java.io套件裡的 FileInputStream 有available() method可以檢查request送來的資料流大小
而不必等到資料流全傳輸完畢
可惜的是看過 FileInputStream 的API後知道available()的限制
它處理的是沒遭遇到network blocking時所送來的一串資料流
只是現實上常有blocking發生...
MultipartRequest最多是在全部的物件上傳完後再來檢查
既然傳輸資料的過程已經免不了了,那我希望至少是當一個檔案上傳時就先檢查
而省掉全傳輸完所耗費的資源及時間
所以將 UploadHandler 改寫成以 MultipartParser 處理
UploadHandler.java=========

package tw.vencs;

import java.io.File;
import java.io.IOException;
import javax.servlet.http.*;
import com.oreilly.servlet.multipart.FilePart;
import com.oreilly.servlet.multipart.MultipartParser;
import com.oreilly.servlet.multipart.Part;

public class UploadHandler extends HttpServlet{

      public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
          HttpSession session = request.getSession();
       
          String saveDirectory = null;     //uploaded files' store dictionary   
          File dir = new File(saveDirectory);
         
          int pictureSize = Integer.parseInt(getServletConfig().getInitParameter("pictureSize"));
          int appendSize = Integer.parseInt(getServletConfig().getInitParameter("appendSize"));
          int maxPostSize = pictureSize * 4 + appendSize;         //limit of file capacity
         
          String Message = "";
          
              MultipartParser fileMap = null;
              String fileName = null;              
            
              try{
                  fileMap = new MultipartParser(request, maxPostSize);
                  fileMap.setEncoding("UTF-8");

                  Part part = null;
             
                   while ((part = fileMap.readNextPart()) != null){
                       if(part.isFile()){         //if this inputstream is from HTML file upload element
                           FilePart filePart = (FilePart) part;
                           filePart.setRenamePolicy(new DefaultFileRenamePolicy());
                           String name = part.getName();        //get HTML elements' name of form
                           fileName = filePart.getFileName();   //get name of uploaded object
                         
                           if(fileName != null && !fileName.equals("")){                          
                             
                               String fileType = fileName.substring( fileName.lastIndexOf('.')+1).toLowerCase();
                               ....

                               if(name.substring(2).equals("pic")){
                                   File dir = new File(saveDirectory);
                                   dir.mkdirs();
                                   long size = filePart.writeTo(dir);     //write data into designated file directory
                                 
                                   if(fileType.equals("jpg") || fileType.equals("jpeg") ||
                                     fileType.equals("png") || fileType.equals("bmp") ||
                                     fileType.equals("gif") || fileType.equals("ai")){
                                     
                                      if(size > (long)pictureSize){
                                          tempMessage = fileName + "大小超過限制!!<br />";
                                          Message += (tempMessage);
                                          File disposedFile = new File(
                                                                             saveDirectory + "/" + filePart.getFileName());
                                          disposedFile.delete();
                                      }else{
                                          ...  //success process
                                      }
                                        
                                   }else{
                                       tempMessage = fileName + "不是圖檔格式!!<br />";
                                       Message += tempMessage;
                                       File disposedFile = new File(saveDirectory + "/" + fileName);
                                       disposedFile.delete();
                                   }
                               }else{                                        //appendix check
                                  File appendDir = new File(saveDirectory + "/append");
                                  appendDir.mkdirs();
                                  int size = (int)filePart.writeTo(appendDir);
                                  int uAppendSize = 0;                    //store uploaded data size info
                                
                                  if(session.getAttribute("append") != null &&
                                    !session.getAttribute("append").equals("")){
                                    
                                      File[] fList = appendDir.listFiles();
                                         for (int j = 0; j < fList.length; j++){
                                             FileInputStream in = new FileInputStream(fList[j]);
                                             uAppendSize += in.available();
                                             in.close();         //must close FileInputStream after use it
                                         }
                                  }
                                 
                                    if(size > (appendSize - uAppendSize)){
                                        tempMessage = fileName + "大小超過限制!!<br />";
                                        Message += tempMessage;
                                        File disposedFile = new File(
                                               saveDirectory + "/appendix/" + filePart.getFileName());
                                        disposedFile.delete();  
                                    }else{
                                        ....    //success process
                                    }
                               }
                           }
                      }
                   }
              }catch(IOException e){
                  .....
              }
              ...
          }
      }
}

預設的命名規則是遇到覆蓋已存在的同名檔案
因為我想處理的只有上傳域的物件
假使表單裡還有其他文件域的資訊,如下處理:
 if (part.isParam()){              //if this inputstream is from normal HTML input element   
       ParamPart paramPart = (ParamPart) part;
       String value = paramPart.getStringValue();  //get param's value

留言