一、前言
关于tomcat源码中实现文件附件表单multipart/form-data数据流后端解析FileUploadBase通用抽象类,并针对不同的场景定义具体实现类DiskFileUpload、FileUpload,详情参见源码说明部分。
二、源码说明
1. FileUploadBase通用抽象实现类
package org.apache.tomcat.util.http.fileupload; @b@@b@import java.io.IOException;@b@import java.io.InputStream;@b@import java.io.OutputStream;@b@import java.util.ArrayList;@b@import java.util.HashMap;@b@import java.util.List;@b@import java.util.Map;@b@import javax.servlet.http.HttpServletRequest; @b@ @b@public abstract class FileUploadBase@b@{@b@@b@ // ---------------------------------------------------------- Class methods@b@@b@@b@ /**@b@ * Utility method that determines whether the request contains multipart@b@ * content.@b@ *@b@ * @param req The servlet request to be evaluated. Must be non-null.@b@ *@b@ * @return <code>true</code> if the request is multipart;@b@ * <code>false</code> otherwise.@b@ */@b@ public static final boolean isMultipartContent(HttpServletRequest req)@b@ {@b@ String contentType = req.getHeader(CONTENT_TYPE);@b@ if (contentType == null)@b@ {@b@ return false;@b@ }@b@ if (contentType.startsWith(MULTIPART))@b@ {@b@ return true;@b@ }@b@ return false;@b@ }@b@@b@@b@ // ----------------------------------------------------- Manifest constants@b@@b@@b@ /**@b@ * HTTP content type header name.@b@ */@b@ public static final String CONTENT_TYPE = "Content-type";@b@@b@@b@ /**@b@ * HTTP content disposition header name.@b@ */@b@ public static final String CONTENT_DISPOSITION = "Content-disposition";@b@@b@@b@ /**@b@ * Content-disposition value for form data.@b@ */@b@ public static final String FORM_DATA = "form-data";@b@@b@@b@ /**@b@ * Content-disposition value for file attachment.@b@ */@b@ public static final String ATTACHMENT = "attachment";@b@@b@@b@ /**@b@ * Part of HTTP content type header.@b@ */@b@ public static final String MULTIPART = "multipart/";@b@@b@@b@ /**@b@ * HTTP content type header for multipart forms.@b@ */@b@ public static final String MULTIPART_FORM_DATA = "multipart/form-data";@b@@b@@b@ /**@b@ * HTTP content type header for multiple uploads.@b@ */@b@ public static final String MULTIPART_MIXED = "multipart/mixed";@b@@b@@b@ /**@b@ * The maximum length of a single header line that will be parsed@b@ * (1024 bytes).@b@ */@b@ public static final int MAX_HEADER_SIZE = 1024;@b@@b@@b@ // ----------------------------------------------------------- Data members@b@@b@@b@ /**@b@ * The maximum size permitted for an uploaded file. A value of -1 indicates@b@ * no maximum.@b@ */@b@ private long sizeMax = -1;@b@@b@@b@ /**@b@ * The content encoding to use when reading part headers.@b@ */@b@ private String headerEncoding;@b@@b@@b@ // ----------------------------------------------------- Property accessors@b@@b@@b@ /**@b@ * Returns the factory class used when creating file items.@b@ *@b@ * @return The factory class for new file items.@b@ */@b@ public abstract FileItemFactory getFileItemFactory();@b@@b@@b@ /**@b@ * Sets the factory class to use when creating file items.@b@ *@b@ * @param factory The factory class for new file items.@b@ */@b@ public abstract void setFileItemFactory(FileItemFactory factory);@b@@b@@b@ /**@b@ * Returns the maximum allowed upload size.@b@ *@b@ * @return The maximum allowed size, in bytes.@b@ *@b@ * @see #setSizeMax(long)@b@ *@b@ */@b@ public long getSizeMax()@b@ {@b@ return sizeMax;@b@ }@b@@b@@b@ /**@b@ * Sets the maximum allowed upload size. If negative, there is no maximum.@b@ *@b@ * @param sizeMax The maximum allowed size, in bytes, or -1 for no maximum.@b@ *@b@ * @see #getSizeMax()@b@ *@b@ */@b@ public void setSizeMax(long sizeMax)@b@ {@b@ this.sizeMax = sizeMax;@b@ }@b@@b@@b@ /**@b@ * Retrieves the character encoding used when reading the headers of an@b@ * individual part. When not specified, or <code>null</code>, the platform@b@ * default encoding is used.@b@ *@b@ * @return The encoding used to read part headers.@b@ */@b@ public String getHeaderEncoding()@b@ {@b@ return headerEncoding;@b@ }@b@@b@@b@ /**@b@ * Specifies the character encoding to be used when reading the headers of@b@ * individual parts. When not specified, or <code>null</code>, the platform@b@ * default encoding is used.@b@ *@b@ * @param encoding The encoding used to read part headers.@b@ */@b@ public void setHeaderEncoding(String encoding)@b@ {@b@ headerEncoding = encoding;@b@ }@b@@b@@b@ // --------------------------------------------------------- Public methods@b@@b@@b@ /**@b@ * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>@b@ * compliant <code>multipart/form-data</code> stream. If files are stored@b@ * on disk, the path is given by <code>getRepository()</code>.@b@ *@b@ * @param req The servlet request to be parsed.@b@ *@b@ * @return A list of <code>FileItem</code> instances parsed from the@b@ * request, in the order that they were transmitted.@b@ *@b@ * @exception FileUploadException if there are problems reading/parsing@b@ * the request or storing files.@b@ */@b@ public List /* FileItem */ parseRequest(HttpServletRequest req)@b@ throws FileUploadException@b@ {@b@ if (null == req)@b@ {@b@ throw new NullPointerException("req parameter");@b@ }@b@@b@ ArrayList items = new ArrayList();@b@ String contentType = req.getHeader(CONTENT_TYPE);@b@@b@ if ((null == contentType) || (!contentType.startsWith(MULTIPART)))@b@ {@b@ throw new InvalidContentTypeException(@b@ "the request doesn't contain a "@b@ + MULTIPART_FORM_DATA@b@ + " or "@b@ + MULTIPART_MIXED@b@ + " stream, content type header is "@b@ + contentType);@b@ }@b@ int requestSize = req.getContentLength();@b@@b@ if (requestSize == -1)@b@ {@b@ throw new UnknownSizeException(@b@ "the request was rejected because it's size is unknown");@b@ }@b@@b@ if (sizeMax >= 0 && requestSize > sizeMax)@b@ {@b@ throw new SizeLimitExceededException(@b@ "the request was rejected because "@b@ + "it's size exceeds allowed range");@b@ }@b@@b@ try@b@ {@b@ int boundaryIndex = contentType.indexOf("boundary=");@b@ if (boundaryIndex < 0)@b@ {@b@ throw new FileUploadException(@b@ "the request was rejected because "@b@ + "no multipart boundary was found");@b@ }@b@ byte[] boundary = contentType.substring(@b@ boundaryIndex + 9).getBytes();@b@@b@ InputStream input = req.getInputStream();@b@@b@ MultipartStream multi = new MultipartStream(input, boundary);@b@ multi.setHeaderEncoding(headerEncoding);@b@@b@ boolean nextPart = multi.skipPreamble();@b@ while (nextPart)@b@ {@b@ Map headers = parseHeaders(multi.readHeaders());@b@ String fieldName = getFieldName(headers);@b@ if (fieldName != null)@b@ {@b@ String subContentType = getHeader(headers, CONTENT_TYPE);@b@ if (subContentType != null && subContentType@b@ .startsWith(MULTIPART_MIXED))@b@ {@b@ // Multiple files.@b@ byte[] subBoundary =@b@ subContentType.substring(@b@ subContentType@b@ .indexOf("boundary=") + 9).getBytes();@b@ multi.setBoundary(subBoundary);@b@ boolean nextSubPart = multi.skipPreamble();@b@ while (nextSubPart)@b@ {@b@ headers = parseHeaders(multi.readHeaders());@b@ if (getFileName(headers) != null)@b@ {@b@ FileItem item =@b@ createItem(headers, false);@b@ OutputStream os = item.getOutputStream();@b@ try@b@ {@b@ multi.readBodyData(os);@b@ }@b@ finally@b@ {@b@ os.close();@b@ }@b@ items.add(item);@b@ }@b@ else@b@ {@b@ // Ignore anything but files inside@b@ // multipart/mixed.@b@ multi.discardBodyData();@b@ }@b@ nextSubPart = multi.readBoundary();@b@ }@b@ multi.setBoundary(boundary);@b@ }@b@ else@b@ {@b@ if (getFileName(headers) != null)@b@ {@b@ // A single file.@b@ FileItem item = createItem(headers, false);@b@ OutputStream os = item.getOutputStream();@b@ try@b@ {@b@ multi.readBodyData(os);@b@ }@b@ finally@b@ {@b@ os.close();@b@ }@b@ items.add(item);@b@ }@b@ else@b@ {@b@ // A form field.@b@ FileItem item = createItem(headers, true);@b@ OutputStream os = item.getOutputStream();@b@ try@b@ {@b@ multi.readBodyData(os);@b@ }@b@ finally@b@ {@b@ os.close();@b@ }@b@ items.add(item);@b@ }@b@ }@b@ }@b@ else@b@ {@b@ // Skip this part.@b@ multi.discardBodyData();@b@ }@b@ nextPart = multi.readBoundary();@b@ }@b@ }@b@ catch (IOException e)@b@ {@b@ throw new FileUploadException(@b@ "Processing of " + MULTIPART_FORM_DATA@b@ + " request failed. " + e.getMessage());@b@ }@b@@b@ return items;@b@ }@b@@b@@b@ // ------------------------------------------------------ Protected methods@b@@b@@b@ /**@b@ * Retrieves the file name from the <code>Content-disposition</code>@b@ * header.@b@ *@b@ * @param headers A <code>Map</code> containing the HTTP request headers.@b@ *@b@ * @return The file name for the current <code>encapsulation</code>.@b@ */@b@ protected String getFileName(Map /* String, String */ headers)@b@ {@b@ String fileName = null;@b@ String cd = getHeader(headers, CONTENT_DISPOSITION);@b@ if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT))@b@ {@b@ int start = cd.indexOf("filename=\"");@b@ int end = cd.indexOf('"', start + 10);@b@ if (start != -1 && end != -1)@b@ {@b@ fileName = cd.substring(start + 10, end).trim();@b@ }@b@ }@b@ return fileName;@b@ }@b@@b@@b@ /**@b@ * Retrieves the field name from the <code>Content-disposition</code>@b@ * header.@b@ *@b@ * @param headers A <code>Map</code> containing the HTTP request headers.@b@ *@b@ * @return The field name for the current <code>encapsulation</code>.@b@ */@b@ protected String getFieldName(Map /* String, String */ headers)@b@ {@b@ String fieldName = null;@b@ String cd = getHeader(headers, CONTENT_DISPOSITION);@b@ if (cd != null && cd.startsWith(FORM_DATA))@b@ {@b@ int start = cd.indexOf("name=\"");@b@ int end = cd.indexOf('"', start + 6);@b@ if (start != -1 && end != -1)@b@ {@b@ fieldName = cd.substring(start + 6, end);@b@ }@b@ }@b@ return fieldName;@b@ }@b@@b@@b@ /**@b@ * Creates a new {@link FileItem} instance.@b@ *@b@ * @param headers A <code>Map</code> containing the HTTP request@b@ * headers.@b@ * @param isFormField Whether or not this item is a form field, as@b@ * opposed to a file.@b@ *@b@ * @return A newly created <code>FileItem</code> instance.@b@ *@b@ * @exception FileUploadException if an error occurs.@b@ */@b@ protected FileItem createItem(Map /* String, String */ headers,@b@ boolean isFormField)@b@ throws FileUploadException@b@ {@b@ return getFileItemFactory().createItem(getFieldName(headers),@b@ getHeader(headers, CONTENT_TYPE),@b@ isFormField,@b@ getFileName(headers));@b@ }@b@@b@@b@ /**@b@ * <p> Parses the <code>header-part</code> and returns as key/value@b@ * pairs.@b@ *@b@ * <p> If there are multiple headers of the same names, the name@b@ * will map to a comma-separated list containing the values.@b@ *@b@ * @param headerPart The <code>header-part</code> of the current@b@ * <code>encapsulation</code>.@b@ *@b@ * @return A <code>Map</code> containing the parsed HTTP request headers.@b@ */@b@ protected Map /* String, String */ parseHeaders(String headerPart)@b@ {@b@ Map headers = new HashMap();@b@ char buffer[] = new char[MAX_HEADER_SIZE];@b@ boolean done = false;@b@ int j = 0;@b@ int i;@b@ String header, headerName, headerValue;@b@ try@b@ {@b@ while (!done)@b@ {@b@ i = 0;@b@ // Copy a single line of characters into the buffer,@b@ // omitting trailing CRLF.@b@ while (i < 2 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n')@b@ {@b@ buffer[i++] = headerPart.charAt(j++);@b@ }@b@ header = new String(buffer, 0, i - 2);@b@ if (header.equals(""))@b@ {@b@ done = true;@b@ }@b@ else@b@ {@b@ if (header.indexOf(':') == -1)@b@ {@b@ // This header line is malformed, skip it.@b@ continue;@b@ }@b@ headerName = header.substring(0, header.indexOf(':'))@b@ .trim().toLowerCase();@b@ headerValue =@b@ header.substring(header.indexOf(':') + 1).trim();@b@ if (getHeader(headers, headerName) != null)@b@ {@b@ // More that one heder of that name exists,@b@ // append to the list.@b@ headers.put(headerName,@b@ getHeader(headers, headerName) + ','@b@ + headerValue);@b@ }@b@ else@b@ {@b@ headers.put(headerName, headerValue);@b@ }@b@ }@b@ }@b@ }@b@ catch (IndexOutOfBoundsException e)@b@ {@b@ // Headers were malformed. continue with all that was@b@ // parsed.@b@ }@b@ return headers;@b@ }@b@@b@@b@ /**@b@ * Returns the header with the specified name from the supplied map. The@b@ * header lookup is case-insensitive.@b@ *@b@ * @param headers A <code>Map</code> containing the HTTP request headers.@b@ * @param name The name of the header to return.@b@ *@b@ * @return The value of specified header, or a comma-separated list if@b@ * there were multiple headers of that name.@b@ */@b@ protected final String getHeader(Map /* String, String */ headers,@b@ String name)@b@ {@b@ return (String) headers.get(name.toLowerCase());@b@ }@b@@b@@b@ /**@b@ * Thrown to indicate that the request is not a multipart request.@b@ */@b@ public static class InvalidContentTypeException@b@ extends FileUploadException@b@ {@b@ /**@b@ * Constructs a <code>InvalidContentTypeException</code> with no@b@ * detail message.@b@ */@b@ public InvalidContentTypeException()@b@ {@b@ super();@b@ }@b@@b@ /**@b@ * Constructs an <code>InvalidContentTypeException</code> with@b@ * the specified detail message.@b@ *@b@ * @param message The detail message.@b@ */@b@ public InvalidContentTypeException(String message)@b@ {@b@ super(message);@b@ }@b@ }@b@@b@@b@ /**@b@ * Thrown to indicate that the request size is not specified.@b@ */@b@ public static class UnknownSizeException@b@ extends FileUploadException@b@ {@b@ /**@b@ * Constructs a <code>UnknownSizeException</code> with no@b@ * detail message.@b@ */@b@ public UnknownSizeException()@b@ {@b@ super();@b@ }@b@@b@ /**@b@ * Constructs an <code>UnknownSizeException</code> with@b@ * the specified detail message.@b@ *@b@ * @param message The detail message.@b@ */@b@ public UnknownSizeException(String message)@b@ {@b@ super(message);@b@ }@b@ }@b@@b@@b@ /**@b@ * Thrown to indicate that the request size exceeds the configured maximum.@b@ */@b@ public static class SizeLimitExceededException@b@ extends FileUploadException@b@ {@b@ /**@b@ * Constructs a <code>SizeExceededException</code> with no@b@ * detail message.@b@ */@b@ public SizeLimitExceededException()@b@ {@b@ super();@b@ }@b@@b@ /**@b@ * Constructs an <code>SizeExceededException</code> with@b@ * the specified detail message.@b@ *@b@ * @param message The detail message.@b@ */@b@ public SizeLimitExceededException(String message)@b@ {@b@ super(message);@b@ }@b@ }@b@@b@}
2. 应用常见具体DiskFileUpload、FileUpload实现类
package org.apache.tomcat.util.http.fileupload; @b@@b@import java.io.File;@b@import java.util.List;@b@import javax.servlet.http.HttpServletRequest; @b@ @b@public class DiskFileUpload@b@ extends FileUploadBase@b@ { @b@@b@ /**@b@ * The factory to use to create new form items.@b@ */@b@ private DefaultFileItemFactory fileItemFactory;@b@@b@@b@ // ----------------------------------------------------------- Constructors@b@@b@@b@ /**@b@ * Constructs an instance of this class which uses the default factory to@b@ * create <code>FileItem</code> instances.@b@ *@b@ * @see #DiskFileUpload(DefaultFileItemFactory fileItemFactory)@b@ */@b@ public DiskFileUpload()@b@ {@b@ super();@b@ this.fileItemFactory = new DefaultFileItemFactory();@b@ }@b@@b@@b@ /**@b@ * Constructs an instance of this class which uses the supplied factory to@b@ * create <code>FileItem</code> instances.@b@ *@b@ * @see #DiskFileUpload()@b@ */@b@ public DiskFileUpload(DefaultFileItemFactory fileItemFactory)@b@ {@b@ super();@b@ this.fileItemFactory = fileItemFactory;@b@ }@b@@b@@b@ // ----------------------------------------------------- Property accessors@b@@b@@b@ /**@b@ * Returns the factory class used when creating file items.@b@ *@b@ * @return The factory class for new file items.@b@ */@b@ public FileItemFactory getFileItemFactory()@b@ {@b@ return fileItemFactory;@b@ }@b@@b@@b@ /**@b@ * Sets the factory class to use when creating file items. The factory must@b@ * be an instance of <code>DefaultFileItemFactory</code> or a subclass@b@ * thereof, or else a <code>ClassCastException</code> will be thrown.@b@ *@b@ * @param factory The factory class for new file items.@b@ */@b@ public void setFileItemFactory(FileItemFactory factory)@b@ {@b@ this.fileItemFactory = (DefaultFileItemFactory) factory;@b@ }@b@@b@@b@ /**@b@ * Returns the size threshold beyond which files are written directly to@b@ * disk.@b@ *@b@ * @return The size threshold, in bytes.@b@ *@b@ * @see #setSizeThreshold(int)@b@ */@b@ public int getSizeThreshold()@b@ {@b@ return fileItemFactory.getSizeThreshold();@b@ }@b@@b@@b@ /**@b@ * Sets the size threshold beyond which files are written directly to disk.@b@ *@b@ * @param sizeThreshold The size threshold, in bytes.@b@ *@b@ * @see #getSizeThreshold()@b@ */@b@ public void setSizeThreshold(int sizeThreshold)@b@ {@b@ fileItemFactory.setSizeThreshold(sizeThreshold);@b@ }@b@@b@@b@ /**@b@ * Returns the location used to temporarily store files that are larger@b@ * than the configured size threshold.@b@ *@b@ * @return The path to the temporary file location.@b@ *@b@ * @see #setRepositoryPath(String)@b@ */@b@ public String getRepositoryPath()@b@ {@b@ return fileItemFactory.getRepository().getPath();@b@ }@b@@b@@b@ /**@b@ * Sets the location used to temporarily store files that are larger@b@ * than the configured size threshold.@b@ *@b@ * @param repositoryPath The path to the temporary file location.@b@ *@b@ * @see #getRepositoryPath()@b@ */@b@ public void setRepositoryPath(String repositoryPath)@b@ {@b@ fileItemFactory.setRepository(new File(repositoryPath));@b@ }@b@@b@@b@ // --------------------------------------------------------- Public methods@b@@b@@b@ /**@b@ * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>@b@ * compliant <code>multipart/form-data</code> stream. If files are stored@b@ * on disk, the path is given by <code>getRepository()</code>.@b@ *@b@ * @param req The servlet request to be parsed. Must be non-null.@b@ * @param sizeThreshold The max size in bytes to be stored in memory.@b@ * @param sizeMax The maximum allowed upload size, in bytes.@b@ * @param path The location where the files should be stored.@b@ *@b@ * @return A list of <code>FileItem</code> instances parsed from the@b@ * request, in the order that they were transmitted.@b@ *@b@ * @exception FileUploadException if there are problems reading/parsing@b@ * the request or storing files.@b@ */@b@ public List /* FileItem */ parseRequest(HttpServletRequest req,@b@ int sizeThreshold,@b@ long sizeMax, String path)@b@ throws FileUploadException@b@ {@b@ setSizeThreshold(sizeThreshold);@b@ setSizeMax(sizeMax);@b@ setRepositoryPath(path);@b@ return parseRequest(req);@b@ }@b@@b@}
package org.apache.tomcat.util.http.fileupload;@b@ @b@public class FileUpload@b@ extends FileUploadBase@b@ {@b@@b@ // ----------------------------------------------------------- Data members@b@@b@@b@ /**@b@ * The factory to use to create new form items.@b@ */@b@ private FileItemFactory fileItemFactory;@b@@b@@b@ // ----------------------------------------------------------- Constructors@b@@b@@b@ /**@b@ * Constructs an instance of this class which uses the default factory to@b@ * create <code>FileItem</code> instances.@b@ *@b@ * @see #FileUpload(FileItemFactory)@b@ */@b@ public FileUpload()@b@ {@b@ super();@b@ }@b@@b@@b@ /**@b@ * Constructs an instance of this class which uses the supplied factory to@b@ * create <code>FileItem</code> instances.@b@ *@b@ * @see #FileUpload()@b@ */@b@ public FileUpload(FileItemFactory fileItemFactory)@b@ {@b@ super();@b@ this.fileItemFactory = fileItemFactory;@b@ }@b@@b@@b@ // ----------------------------------------------------- Property accessors@b@@b@@b@ /**@b@ * Returns the factory class used when creating file items.@b@ *@b@ * @return The factory class for new file items.@b@ */@b@ public FileItemFactory getFileItemFactory()@b@ {@b@ return fileItemFactory;@b@ }@b@@b@@b@ /**@b@ * Sets the factory class to use when creating file items.@b@ *@b@ * @param factory The factory class for new file items.@b@ */@b@ public void setFileItemFactory(FileItemFactory factory)@b@ {@b@ this.fileItemFactory = factory;@b@ }@b@@b@@b@}