文件上传
文件上传操作通常会附加一些限制,如:文件类型、上传文件总大小、每个文件的最大大小等。除此以外,作为一个通用组件还需要考虑更多的问题,如:支持自定义文件保存目录、支持相对路径和绝对路径、支持自定义保存的文件的文件名称、支持上传进度反馈和上传失败清理等。另外,本座也不想重新造车轮,本组件是基于 Commons File Upload 实现,省却了本座大量的工作 ^_^ 下面先从一个具体的使用例子讲起:
- 上传请求界面及代码
- <form action="checkupload.action" method="post" enctype="multipart/form-data">
- First Name: <input type="text" name="firstName" value="丑">
- <br>
- Last Name: <input type="text" name="lastName" value="怪兽">
- <br>
- Birthday: <input type="text" name="birthday" value="1978-11-03">
- <br>
- Gender: 男 <input type="radio"" name="gender" value="false">
- 女 <input type="radio"" name="gender" value="true" checked="checked">
- <br>
- Working age: <select name="workingAge">
- <option value="-1">-请选择-</option>
- <option value="3">三年</option>
- <option value="5" selected="selected">五年</option>
- <option value="10">十年</option>
- <option value="20">二十年</option>
- </select>
- <br>
- Interest: 游泳 <input type="checkbox" name="interest" value="1" checked="checked">
- 打球 <input type="checkbox" name="interest" value="2" checked="checked">
- 下棋 <input type="checkbox" name="interest" value="3">
- 打麻将 <input type="checkbox" name="interest" value="4">
- 看书 <input type="checkbox" name="interest" value="5" checked="checked">
- <br>
- Photo 1.1: <input type="file"" name="photo-1">
- <br>
- Photo 1.2: <input type="file"" name="photo-1">
- <br>
- Photo 2.1: <input type="file"" name="photo-2">
- <br>
- <br>
- <input type="submit" value="确 定"> <input type="reset" value="重 置">
- </form>
从上面的 HTML 代码可以看出,表单有 6 个普通域和 3 个文件域,其中前两个文件域的 name 属性相同。
- 上传处理代码
- import com.bruce.util.BeanHelper;
- import com.bruce.util.Logger;
- import com.bruce.util.http.FileUploader;
- import com.bruce.util.http.FileUploader.FileInfo;
- import static com.bruce.util.http.FileUploader.*;
- @SuppressWarnings("unused")
- public class CheckUpload extends ActionSupport
- {
- // 上传路径
- private static final String UPLOAD_PATH = "upload";
- // 可接受的文件类型
- private static final String[] ACCEPT_TYPES = {"txt", "pdf", "doc", ".Jpg", "*.zip", "*.RAR"};
- // 总上传文件大小限制
- private static final long MAX_SIZE = 1024 * 1024 * 100;
- // 单个传文件大小限制
- private static final long MAX_FILE_SIZE = 1024 * 1024 * 10;
- @Override
- public String execute()
- {
- // 创建 FileUploader 对象
- FileUploader fu = new FileUploader(UPLOAD_PATH, ACCEPT_TYPES, MAX_SIZE, MAX_FILE_SIZE);
- // 根据实际情况设置对象属性(可选)
- /*
- fu.setFileNameGenerator(new FileNameGenerator()
- {
- @Override
- public String generate(FileItem item, String suffix)
- {
- return String.format("%d_%d", item.hashCode(), item.get().hashCode());
- }
- });
- fu.setServletProgressListener(new ProgressListener()
- {
- @Override
- public void update(long pBytesRead, long pContentLength, int pItems)
- {
- System.out.printf("%d: length -> %d, read -> %d.\n", pItems, pContentLength, pBytesRead);
- }
- });
- */
- // 执行上传并获取操作结果
- Result result = fu.upload(getRequest(), getResponse());
- // 检查操作结果
- if(result != FileUploader.Result.SUCCESS)
- {
- // 设置 request attribute
- setRequestAttribute("error", fu.getCause());
- // 记录日志
- Logger.exception(fu.getCause(), "upload file fail", Level.ERROR, false);
- return ERROR;
- }
- // 通过非文件表单域创建 Form Bean
- Persion persion = BeanHelper.createBean(Persion.class, fu.getParamFields());
- // 图片保存路径的列表
- List<String> photos = new ArrayList<String>();
- /* 轮询文件表单域,填充 photos */
- Set<String> keys = fu.getFileFields().keySet();
- for(String key : keys)
- {
- FileInfo[] ffs = fu.getFileFields().get(key);
- for(FileInfo ff : ffs)
- {
- photos.add(String.format("(%s) %s%s%s", key, fu.getSavePath(), File.separator, ff.getSaveFile().getName()));
- }
- }
- // 设置 Form Bean 的 photos 属性
- persion.setPhotos(photos);
- // 设置 request attribute
- setRequestAttribute("persion", persion);
- return SUCCESS;
- }
- }
- public class Persion
- {
- private String firstName;
- private String lastName;
- private Date birthday;
- private boolean gender;
- private int workingAge;
- private int[] interest;
- private List<String> photos;
- }
- public static class FileInfo
- {
- private String uploadFileName;
- private File saveFile;
- }
分析下上面的 Java 代码,本例先根据保存目录、文件大小限制和文件类型限制创建一个 FileUploader 对象,然后调用该对象的 upload() 方法执行上传并返回操作结果,如果上传成功则 通过 getParamFields() 方法获取所有非文件表单域内容,并交由 BeanHelper 进行解析(若想了解更多关于 BeanHelper 的内容请猛击这里 ^_^)创建 Form Bean,再调用 getFileFields() 方法获取所有文件表单域的 FileInfo(FileInfo 包含上传文件的原始名称和被保存文件的 File 对象),最后完成 Form Bean 所有字段的填充并把 Form Bean 设置为 request 属性。
- 上传结果界面及代码
- <table border="1">
- <caption>Persion Attributs</caption>
- <tr><td>Name</td><td><c:out value="${persion.firstName} ${persion.lastName}"/> </td></tr>
- <tr><td>Brithday</td><td><c:out value="${persion.birthday}"/> </td></tr>
- <tr><td>Gender</td><td><c:out value="${persion.gender}"/> </td></tr>
- <tr><td>Working Age</td><td><c:out value="${persion.workingAge}"/> </td></tr>
- <tr><td>Interest</td><td><c:forEach var="its" items="${persion.interest}">
- <c:out value="${its}" />
- </c:forEach> </td></tr>
- <tr><td>Photos</td><td><c:forEach var="p" items="${persion.photos}">
- <c:out value="${p}" /><br>
- </c:forEach> </td></tr>
- </table>
从上面的处理结果可以看出,文件上传组件 FileUploader 正确地处理了表单的所有文件域和非文件域名,并且,整个文件上传操作过程非常简单,无需用户过多参与。下面我们来详细看看组件的主要实现代码:
- /** 文件上传器 */
- public class FileUploader
- {
- /** 不限制文件上传总大小的 Size Max 常量 */
- public static final long NO_LIMIT_SIZE_MAX = -1;
- /** 不限制文件上传单个文件大小的 File Size Max 常量 */
- public static final long NO_LIMIT_FILE_SIZE_MAX = -1;
- /** 默认的写文件阀值 */
- public static final int DEFAULT_SIZE_THRESHOLD = DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD;
- /** 默认的文件名生成器 */
- public static final FileNameGenerator DEFAULT_FILE_NAME_GENERATOR = new CommonFileNameGenerator();
- /** 设置上传文件的保存路径(不包含文件名)
- *
- * 文件路径,可能是绝对路径或相对路径<br>
- * 1) 绝对路径:以根目录符开始(如:'/'、'D:\'),是服务器文件系统的路径<br>
- * 2) 相对路径:不以根目录符开始,是相对于 WEB 应用程序 Context 的路径,(如:mydir 是指
- * '${WEB-APP-DIR}/mydir')<br>
- * 3) 规则:上传文件前会检查该路径是否存在,如果不存在则会尝试生成该路径,如果生成失败则
- * 上传失败并返回 {@link Result#INVALID_SAVE_PATH}
- *
- */
- private String savePath;
- /** 文件上传的总文件大小限制 */
- private long sizeMax = NO_LIMIT_SIZE_MAX;
- /** 文件上传的单个文件大小限制 */
- private long fileSizeMax = NO_LIMIT_FILE_SIZE_MAX;
- /** 可接受的上传文件类型集合,默认:不限制 */
- private Set<String> acceptTypes = new LStrSet();
- /** 非文件表单域的映射 */
- private Map<String, String[]> paramFields = new HashMap<String, String[]>();
- /** 文件表单域的映射 */
- private Map<String, FileInfo[]> fileFields = new HashMap<String, FileInfo[]>();
- /** 文件名生成器 */
- private FileNameGenerator fileNameGenerator = DEFAULT_FILE_NAME_GENERATOR;
- // commons file upload 相关属性
- private int factorySizeThreshold = DEFAULT_SIZE_THRESHOLD;
- private String factoryRepository;
- private FileCleaningTracker factoryCleaningTracker;
- private String servletHeaderencoding;
- private ProgressListener servletProgressListener;
- /** 文件上传失败的原因(文件上传失败时使用) */
- private Throwable cause;
- /** 执行上传
- *
- * @param request : {@link HttpServletRequest} 对象
- * @param response : {@link HttpServletResponse} 对象
- *
- * @return : 成功:返回 {@link Result#SUCCESS} ,失败:返回其他结果,
- * 失败原因通过 {@link FileUploader#getCause()} 获取
- *
- */
- @SuppressWarnings("unchecked")
- public Result upload(HttpServletRequest request, HttpServletResponse response)
- {
- reset();
- // 获取上传目录绝对路径
- String absolutePath = getAbsoluteSavePath(request);
- if(absolutePath == null)
- {
- cause = new FileNotFoundException(String.format("path '%s' not found or is not directory", savePath));
- return Result.INVALID_SAVE_PATH;
- }
- ServletFileUpload sfu = getFileUploadComponent();
- List<FileItemInfo> fiis = new ArrayList<FileItemInfo>();
- List<FileItem> items = null;
- Result result = Result.SUCCESS;
- // 获取文件名生成器
- String encoding = servletHeaderencoding != null ? servletHeaderencoding : request.getCharacterEncoding();
- FileNameGenerator fnGenerator = fileNameGenerator != null ? fileNameGenerator : DEFAULT_FILE_NAME_GENERATOR;
- try
- {
- // 执行上传操作
- items = (List<FileItem>)sfu.parseRequest(request);
- }
- catch (FileUploadException e)
- {
- cause = e;
- if(e instanceof FileSizeLimitExceededException) result = Result.FILE_SIZE_EXCEEDED;
- else if(e instanceof SizeLimitExceededException) result = Result.SIZE_EXCEEDED;
- else if(e instanceof InvalidContentTypeException) result = Result.INVALID_CONTENT_TYPE;
- else if(e instanceof IOFileUploadException) result = Result.FILE_UPLOAD_IO_EXCEPTION;
- else result = Result.OTHER_PARSE_REQUEST_EXCEPTION;
- }
- if(result == Result.SUCCESS)
- {
- // 解析所有表单域
- result = parseFileItems(items, fnGenerator, absolutePath, encoding, fiis);
- if(result == Result.SUCCESS)
- // 保存文件
- result = writeFiles(fiis);
- }
- return result;
- }
- // 解析所有表单域
- private Result parseFileItems(List<FileItem> items, FileNameGenerator fnGenerator, String absolutePath, String encoding, List<FileItemInfo> fiis)
- {
- for(FileItem item : items)
- {
- if(item.isFormField())
- // 解析非文件表单域
- parseFormField(item, encoding);
- else
- {
- if(item.getSize() == 0)
- continue;
- // 解析文件表单域
- Result result = parseFileField(item, absolutePath, fnGenerator, fiis);
- if(result != Result.SUCCESS)
- {
- reset();
- cause = new InvalidParameterException(String.format("file '%s' not accepted", item.getName()));
- return result;
- }
- }
- }
- return Result.SUCCESS;
- }
- // 解析文件表单域
- private Result parseFileField(FileItem item, String absolutePath, FileNameGenerator fnGenerator, List<FileItemInfo> fiis)
- {
- String suffix = null;
- String uploadFileName = item.getName();
- boolean isAcceptType = acceptTypes.isEmpty();
- if(!isAcceptType)
- {
- suffix = null;
- int stuffPos = uploadFileName.lastIndexOf(".");
- if(stuffPos != -1)
- {
- suffix = uploadFileName.substring(stuffPos, uploadFileName.length()).toLowerCase();
- isAcceptType = acceptTypes.contains(suffix);
- }
- }
- if(!isAcceptType)
- return Result.INVALID_FILE_TYPE;
- // 通过文件名生成器获取文件名
- String saveFileName = fnGenerator.generate(item, suffix);
- if(!saveFileName.endsWith(suffix))
- saveFileName += suffix;
- String fullFileName = absolutePath + File.separator + saveFileName;
- File saveFile = new File(fullFileName);
- FileInfo info = new FileInfo(uploadFileName, saveFile);
- // 添加表单域文件信息
- fiis.add(new FileItemInfo(item, saveFile));
- addFileField(item.getFieldName(), info);
- return Result.SUCCESS;
- }
- private void parseFormField(FileItem item, String encoding)
- {
- String name = item.getFieldName();
- String value = item.getString();
- // 字符串编码转换
- if(!GeneralHelper.isStrEmpty(value) && encoding != null)
- {
- try
- {
- value = new String(value.getBytes("ISO-8859-1"), encoding);
- }
- catch(UnsupportedEncodingException e)
- {
- }
- }
- // 添加表单域名/值映射
- addParamField(name, value);
- }
- /** 文件名生成器接口 </SP< span>