学而实习之 不亦乐乎

Java:Apache Commons FileUpload的使用

2020-08-23 06:41:32

Apache Commons FileUpload 1.4 (requires Java 1.6 or later)

一、简介

Commons File Upload包可以方便地向servlet和web应用程序添加健壮的、高性能的文件上传功能。
FileUpload 解析符合RFC1867、 "Form-based File Upload in HTML" 的HTTP请求。 也就是说,如果使用POST方法提交HTTP请求,并且内容类型为“multipart/form-data”,则File Upload可以解析该请求,并以调用方容易使用的方式提供结果。
从版本1.3开始,文件上传处理RFC2047编码的头值。

向服务器发送 multipart/form-data 请求的最简单方法是通过Web表单,如:
 
<form method="POST" enctype="multipart/form-data" action="fup.cgi">
  File to upload: <input type="file" name="upfile"><br/>
  Notes about the file: <input type="text" name="note"><br/>
  <br/>
  <input type="submit" value="Press"> to upload the file!
</form>

二、使用FileUpload(解析请求)

1.判断请求的类型

在处理上传的项目之前,当然需要解析请求本身。File Upload通过提供一个静态方法来实现。

// Check that we have a file upload request
boolean isMultipart = ServletFileUpload.isMultipartContent(request);

2.简单用例

最简单的使用方案如下:

  1. 只要上传的项目很小,就应将其保留在内存中。
  2. 较大的项目应写入磁盘上的临时文件。
  3. 不允许很大的上传请求。
  4. 内置默认值是要保留在内存中的项目的最大大小,上载请求的最大允许大小以及临时文件的位置。

在这种情况下处理请求再简单不过了:

// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();

// Configure a repository (to ensure a secure temp location is used)
ServletContext servletContext = this.getServletConfig().getServletContext();
File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
factory.setRepository(repository);

// Create a new file upload handler
ServletFileUpload upload = new ServletFileUpload(factory);

// Parse the request
List<FileItem> items = upload.parseRequest(request);

行使更多控制权

如果您的使用方案接近上述最简单的情况,但是您需要更多控制,则可以轻松自定义上传处理程序或文件项目工厂或两者的行为。以下示例显示了几个配置选项:

// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();

// Set factory constraints
factory.setSizeThreshold(yourMaxMemorySize);
factory.setRepository(yourTempDirectory);

// Create a new file upload handler
ServletFileUpload upload = new ServletFileUpload(factory);

// Set overall request size constraint
upload.setSizeMax(yourMaxRequestSize);

// Parse the request
List<FileItem> items = upload.parseRequest(request);

当然,每种配置方法都相互独立,但是如果您想一次配置所有工厂,则可以使用替代构造函数来完成,如下所示:

// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory(yourMaxMemorySize, yourTempDirectory);

如果您需要进一步控制请求的解析,例如将项目存储在其他地方(例如,存储在数据库中),则需要考虑自定义 FileUpload。


3.处理上传的项目

解析完成后,需要对获取的列表数据进行处理。通常如下:

// Process the uploaded items
Iterator<FileItem> iter = items.iterator();
while (iter.hasNext()) {
    FileItem item = iter.next();

    if (item.isFormField()) {
        processFormField(item);
    } else {
        processUploadedFile(item);
    }
}

对于常规表单字段,可能只需要项目名称及其字符串值。

// Process a regular form field
if (item.isFormField()) {
    String name = item.getFieldName();
    String value = item.getString();
    ...
}

对于文件上传,在处理内容之前,您可能需要了解几件事。这是您可能感兴趣的一些方法的示例。

// Process a file upload
if (!item.isFormField()) {
    String fieldName = item.getFieldName();
    String fileName = item.getName();
    String contentType = item.getContentType();
    boolean isInMemory = item.isInMemory();
    long sizeInBytes = item.getSize();
    ...
}

对于上载的文件,除非它们很小或没有其他选择,否则通常不希望通过内存访问它们。相反,您将需要将内容作为流处理,或将整个文件写入其最终位置。FileUpload提供了完成这两项的简单方法。

// Process a file upload
if (writeToFile) {
    File uploadedFile = new File(...);
    item.write(uploadedFile);
} else {
    InputStream uploadedStream = item.getInputStream();
    ...
    uploadedStream.close();
}

请注意,在FileUpload的默认实现中 ,如果数据已经在临时文件中,则write()将尝试将文件重命名为指定的目标。实际上,仅在重命名由于某种原因而失败或数据在内存中时,才完成数据复制。

如果确实需要访问内存中的上载数据,则只需调用get()方法即可将数据获取为字节数组。

// Process a file upload in memory
byte[] data = item.get();
...

4.资源清理

本节仅在使用 DiskFileItem时适用。换句话说,如果您在处理之前将上传的文件写入临时文件,则适用。

如果不再使用这些临时文件(更确切地说,如果DiskFileItem的相应实例 被垃圾回收),则会自动删除这些文件。这是由org.apache.commons.io.FileCleanerTracker 类以静默方式完成的,该类启动了收割线程。

如果不再需要该收割线程,则应停止该线程。在Servlet环境中,这是通过使用称为FileCleanerCleanup的特殊Servlet上下文侦听器来 完成的。为此,请将以下内容添加到web.xml中:

<web-app> 
  ... 
  <listener> 
    <listener-class> 
      org.apache.commons.fileupload.servlet.FileCleanerCleanup 
    </ listener-class> 
  </ listener> 
  ... 
</ web-app>

创建一个DiskFileItemFactory
FileCleanerCleanup提供org.apache.commons.io.FileCleaningTracker的实例 。创建org.apache.commons.fileupload.disk.DiskFileItemFactory时必须使用此实例 。这可以通过调用如下方法来完成:

public static DiskFileItemFactory newDiskFileItemFactory(ServletContext context,
                                                         File repository) {
    FileCleaningTracker fileCleaningTracker
        = FileCleanerCleanup.getFileCleaningTracker(context);
    DiskFileItemFactory factory
        = new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD,
                                  repository);
    factory.setFileCleaningTracker(fileCleaningTracker);
    return factory;
}

禁用清除临时文件
要禁用对临时文件的跟踪,可以将FileCleaningTracker设置 为null。因此,将不再跟踪创建的文件。特别是,它们将不再被自动删除。

5.与病毒扫描程序的交互

与Web容器在同一系统上运行的病毒扫描程序会导致使用FileUpload的应用程序出现某些意外行为。本节描述了您可能会遇到的某些行为,并提供了一些有关如何处理它们的想法。

FileUpload的默认实现将导致将超过特定大小阈值的上载项目写入磁盘。一旦关闭了此类文件,系统上的任何病毒扫描程序都将唤醒并检查该文件,并有可能隔离该文件-即,将其移至不会造成问题的特殊位置。当然,这将使应用程序开发人员感到惊讶,因为上载的文件项将不再可用于处理。另一方面,低于相同阈值的上载项目将保存在内存中,因此不会被病毒扫描程序看到。这样就可以保留某种形式的病毒(即使将其写入磁盘,病毒扫描程序也会定位并检查它)。

一种常用的解决方案是在系统上预留一个目录,所有上载文件都将放置在该目录中,并将病毒扫描程序配置为忽略该目录。这样可以确保文件不会从应用程序下面被盗走,而是将病毒扫描的责任留给了应用程序开发人员。然后,可以通过外部过程来扫描上传的文件中是否存在病毒,该过程可能会将干净的或已清理的文件移动到“批准的”位置,或者将病毒扫描程序集成到应用程序本身中。配置外部进程或将病毒扫描集成到应用程序中的详细信息不在本文档的范围之内。

6.观看进度

如果您希望上传非常大的文件,那么最好向用户报告已收到多少文件。甚至HTML页面也允许通过返回multipart/replace响应或类似的东西来实现进度条。

观看上传进度可以通过提供进度监听器来完成:

//Create a progress listener
ProgressListener progressListener = new ProgressListener(){
   public void update(long pBytesRead, long pContentLength, int pItems) {
       System.out.println("We are currently reading item " + pItems);
       if (pContentLength == -1) {
           System.out.println("So far, " + pBytesRead + " bytes have been read.");
       } else {
           System.out.println("So far, " + pBytesRead + " of " + pContentLength
                              + " bytes have been read.");
       }
   }
};
upload.setProgressListener(progressListener);

像上面一样,帮自己一个忙,实现第一个进度监听器,因为它向您展示了一个陷阱:进度监听器被频繁调用。根据servlet引擎和其他环境工厂的不同,可能会为任何网络数据包调用它!换句话说,您的进度侦听器可能会成为性能问题!一个典型的解决方案可能是减少进度侦听器活动。例如,如果兆字节数已更改,则可能仅发出一条消息:

//Create a progress listener
ProgressListener progressListener = new ProgressListener(){
   private long megaBytes = -1;
   public void update(long pBytesRead, long pContentLength, int pItems) {
       long mBytes = pBytesRead / 1000000;
       if (megaBytes == mBytes) {
           return;
       }
       megaBytes = mBytes;
       System.out.println("We are currently reading item " + pItems);
       if (pContentLength == -1) {
           System.out.println("So far, " + pBytesRead + " bytes have been read.");
       } else {
           System.out.println("So far, " + pBytesRead + " of " + pContentLength
                              + " bytes have been read.");
       }
   }
};