之前在 Smart 中实现过文件上传功能,它是基于 Servlet 3 的,我们可以通过这篇博文来了解:http://my.oschina.net/huangyong/blog/161989 单纯地上
之前在 Smart 中实现过文件上传功能,它是基于 Servlet 3 的,我们可以通过这篇博文来了解:http://my.oschina.net/huangyong/blog/161989
单纯地上传文件到服务器上,或许这样已经足够了,但是在实际项目中往往不会这么理想。在实际项目中,我们往往是通过一个表单来上传文件,该表单中除了包括一个文件域以外,或许还有一些文本域或下拉框等,这些普通字段需要与文件字段一同传输到服务器端。如果还是使用以前提供的 UploadServlet,未免也就太过于天真了。
Smart 框架是一个解决实际问题的开发框架,不需要鸡肋,所以我们果断地干掉了以前的 UploadServlet,因为它看起来过于玩具了。
我们一般会这样写一个 HTML 上传表单:
<form action="/product/create" method="post" enctype="multipart/form-data" class="css-form"> <div class="css-form-header"> <h3>Create Product</h3> </div> <div class="css-form-row"> <label for="name">Product Name:</label> <input type="text" id="name" name="name"> </div> <div class="css-form-row"> <label for="code">Product Code:</label> <input type="text" id="code" name="code"> </div> <div class="css-form-row"> <label for="price">Price:</label> <input type="text" id="price" name="price"> </div> <div class="css-form-row"> <label for="description">Description:</label> <textarea id="description" name="description" rows="5"></textarea> </div> <div class="css-form-row"> <label for="picture">Picture:</label> <input type="file" id="picture" name="picture"> </div> <div class="css-form-footer"> <button type="submit">Save</button> </div></form
上面只是一个简单的表单,有一些普通字段(name、code、price、description),此外还包括一个文件字段(picture)。
需要说明的是,要想实现文件上传功能,需要在 form 标签上添加两个属性:
method="post"(默认为 get)
enctype="multipart/form-data"(默认为 application/x-www-form-urlencoded)
此外,必须使用 type="file" 的 input 标签才能上传文件。
想必上面的一切都能与大家达成共识,但对于文件上传的服务端开发或许会五花八门。
有些朋友通过 Servlet API 来获取 Request 中的输入流,从而解析出所提交的数据(包括普通字段与文件字段),但这样做未免太过于繁琐。在 Java 圈子里有个响当当的文件上传类库,它就能帮您一个大忙,它就是 Apache Commons FileUpload。
大名鼎鼎的 Spring 也支持文件上传,同时也支持 FileUpload 类库,我个人真心觉得 Spring 已经做得很好了,网上可以搜索到大量的参考资料来讲解如何通过 Spring 来实现文件上传。
如果要总结一下的话,使用 Spring 来实现文件上传,我们大致需要做以下几件事情:
在 Spring 配置文件中,定义一个 Bean(org.springframework.web.multipart.commons.CommonsMultipartResolver),可指定文件上传的大小限制,比如最大只能上传 10M 的文件。
在 Controller 方法的参数中,提供一个 MultipartFile 类型的参数,用来接收已上传的文件对象,可通过 MultipartFile 对象获取 InputStream,其它普通字段可通过其它参数进行映射。
创建一个 FileOutputStream,通过流复制的方式实现文件上传,当然也可直接读取并处理 InputStream 中的数据。
这一切都已经非常简单而强大了,但 Smart 确实不甘心,我们也要搞一个类似的文件上传功能,尽可能地让文件上传更加简单。
在 Smart 的 Action 这样做是否可行呢?
@Beanpublic class ProductAction extends BaseAction { @Inject private ProductService productService; @Request("POST:/product/create") public Result create(Map<String, Object> fieldMap, Multipart multipart) { boolean success = productService.createProduct(fieldMap); if (success) { UploadHelper.uploadFile(Tool.getBasePath(), multipart); } return new Result(success); }...
我们通过一个 Map<String, Object> fieldMap 参数来接收普通字段,通过一个 Multipart multipart 参数来接收文件字段。fieldMap 实际上封装了需要保存到数据库中的数据,而 multipart 才是封装了需要上传到服务器上的文件。我们仅需提供一个 UploadHelper.uploadFile() 方法即可实现文件上传,我们可以自行提供一个 Tool 类(当然您也可以叫其它名字),用它来获取需要上传文件的根目录,Tool 类看起来是这样的:
public class Tool { public static String getBasePath() { return DataContext.getServletContext().getRealPath("") + Constant.UPLOAD_PATH; }}
我们可以通过 DataContext 获取 ServletContext,从而进一步获取 RealPath,我们要知道,这是当前 Web 应用中 Context Path 的绝对路径,再拼接上后面的 Constant.UPLOAD_PATH,它是上传目录的相对路径,Constant 类如下:
public interface Constant { String UPLOAD_PATH = ConfigHelper.getStringProperty("sample.upload_path");}
Constant 只是一个常量接口而已,实际上是从 config.properties 文件中获取的上传路径的,在 config.properties 中我们可以这样配置:
sample.upload_path=/www/upload/
如果需要变更上传目录,直接修改 config.properties 文件的 sample.upload_path 即可,其它代码无需修改。
想必这些内容还不能让您过瘾,或许您想知道是,Smart 是如何实现文件上传的?
一切从 Multipart 开始吧!
第一步:定义一个 Multipart 类来封装所上传的文件
public class Multipart extends BaseBean { private String fileName; private long fileSize; private String contentType; private InputStream inputStream; public Multipart(String fileName, String contentType, long fileSize, InputStream inputStream) { this.fileName = fileName; this.contentType = contentType; this.fileSize = fileSize; this.inputStream = inputStream; } public String getFileName() { return fileName; } public String getContentType() { return contentType; } public long getFileSize() { return fileSize; } public InputStream getInputStream() { return inputStream; }}
Multipart 看来就是一个简单的 JavaBean 而已,但需要说明的是,这里的 fileName 是不带任何路径的,只是单纯的文件名称而已(但包括后缀名),通过 IE 上传文件时会带上路径的,但对于非 IE 的浏览器会自动去掉路径,对于这个问题,我们可以通过一定的手段来处理(见下文)。
可见,inputStream 字段才是所上传文件的核心所在,我们的 UploadHelper 就封装了所谓的流复制操作,代码如下:
public class UploadHelper { public static void uploadFile(String basePath, Multipart multipart) { try { // 创建文件路径(绝对路径) String filePath = basePath + multipart.getFileName(); FileUtil.createFile(filePath); // 执行流复制操作 InputStream inputStream = new BufferedInputStream(multipart.getInputStream()); OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(filePath)); StreamUtil.copyStream(inputStream, outputStream); } catch (Exception e) { logger.error("上传文件出错!", e); throw new RuntimeException(e); } }}
首先创建文件路径,然后进行流复制操作。其中的 FileUtil 与 StreamUtil 请参见 Smart 源码。
下面的才是重头戏,如何接受 multipart/form-data 类型的表单数据?
第二步:处理 multipart/form-data 表单数据
这个可不是普通的表单数据,它是 multipart 的,解析起来非常复杂,不过我们有了 FileUpload 类库,一切就变得简单了。
此时应该是对 DispatcherServlet 做一定改进的时候了,让它同时兼容 multipart 类型!
在 DispatcherServlet 类的 service 方法中,需要获取 Action 的方法参数(指的是参数的值),可调用这个方法获取的:
List<Object> actionMethodParamList = createActionMethodParamList(request, requestPathMatcher, actionBean);
在 createActionMethodParamList 方法中,我们需要做一些改进:
... private List<Object> createActionMethodParamList(HttpServletRequest request, Matcher requestPathMatcher, ActionBean actionBean) throws Exception { // 定义参数列表 List<Object> paramList = new ArrayList<Object>(); // 获取 Action 方法参数类型 Class<?>[] actionParamTypes = actionBean.getActionMethod().getParameterTypes(); // 添加路径参数列表(请求路径中的带占位符参数) paramList.addAll(createPathParamList(requestPathMatcher, actionParamTypes)); // 分两种情况进行处理 if (UploadHelper.isMultipart(request)) { // 添加 Multipart 请求参数列表 paramList.addAll(UploadHelper.createMultipartParamList(request)); } else { // 添加普通请求参数列表(包括 Query String 与 Form Data) Map<String, String> requestParamMap = WebUtil.getRequestParamMap(request); if (MapUtil.isNotEmpty(requestParamMap)) { paramList.add(requestParamMap); } } // 返回参数列表 return paramList; }...
当我们拿到 HttpServletRequest 对象(简称 Request 对象)后,需要判断是否为 multipart 类型。这里仍然可以见到 UploadHelper 的身影,其实我们是想让它来封装 FileUpload 的相关 API,它还提供了这些功能:
public class UploadHelper { private static final Logger logger = Logger.getLogger(UploadHelper.class); // 获取上传限制 private static final int uploadLimit = ConfigHelper.getNumberProperty(FrameworkConstant.APP_UPLOAD_LIMIT); // 定义一个 FileUpload 对象(用于解析所上传的文件) private static ServletFileUpload fileUpload; public static void init(ServletContext servletContext) { // 获取一个临时目录(使用 Tomcat 的 work 目录) File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir"); // 创建 FileUpload 对象 fileUpload = new ServletFileUpload(new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD, repository)); // 设置上传限制 if (uploadLimit != 0) { fileUpload.setFileSizeMax(uploadLimit * 1024 * 1024); // 单位为 M if (logger.isDebugEnabled()) { logger.debug("[Smart] limit of uploading: " + uploadLimit + "M"); } } } public static boolean isMultipart(HttpServletRequest request) { // 判断上传文件的内容是否为 multipart 类型 return ServletFileUpload.isMultipartContent(request); } public static List<Object> createMultipartParamList(HttpServletRequest request) throws Exception { // 定义参数列表 List<Object> paramList = new ArrayList<Object>(); // 创建两个对象,分别对应 普通字段 与 文件字段 Map<String, String> fieldMap = new HashMap<String, String>(); List<Multipart> multipartList = new ArrayList<Multipart>(); // 获取并遍历表单项 List<FileItem> fileItemList; try { fileItemList = fileUpload.parseRequest(request); } catch (FileUploadBase.FileSizeLimitExceededException e) { // 异常转换(抛出自定义异常) throw new UploadException(e); } for (FileItem fileItem : fileItemList) { // 分两种情况处理表单项 String fieldName = fileItem.getFieldName(); if (fileItem.isFormField()) { // 处理普通字段 String fieldValue = fileItem.getString(FrameworkConstant.DEFAULT_CHARSET); fieldMap.put(fieldName, fieldValue); } else { // 处理文件字段 String originalFileName = FileUtil.getRealFileName(fileItem.getName()); String uploadedFileName = FileUtil.getEncodedFileName(originalFileName); String contentType = fileItem.getContentType(); long fileSize = fileItem.getSize(); InputStream inputSteam = fileItem.getInputStream(); // 创建 Multipart 对象,并将其添加到 multipartList 中 Multipart multipart = new Multipart(uploadedFileName, contentType, fileSize, inputSteam); multipartList.add(multipart); // 将所上传文件的文件名存入 fieldMap 中 fieldMap.put(fieldName, uploadedFileName); } } // 初始化参数列表 paramList.add(fieldMap); if (multipartList.size() > 1) { paramList.add(multipartList); } else if (multipartList.size() == 1) { paramList.add(multipartList.get(0)); } else { paramList.add(null); } // 返回参数列表 return paramList; }...
我们可在 config.properties 配置文件中,添加一个用于描述文件上传限制的配置项:
app.upload_limit=10
以上配置说明上传限制为 10M,若超过这个限制,则会抛出异常。
随后,我们定义了一个 ServletFileUpload 对象(简称 FileUpload 对象),它是 Apache Commons FileUpload 给我们提供的一个很强大的武器,用它可以解析所上传的文件。我们在 init 方法中对 FileUpload 对象进行了初始化,指定了上传文件的临时目录,定义了内存缓冲区大小,设置了上传限制,这一切仿佛都那么自然。需要说明的是,FileUpload 对象可定义为一个 static 的,无需反复创建,这样也为了提高一些运行时的性能。
随后,我们提供了一个判断 Request 对象是否为 multipart 类型,其实 ServletFileUpload 已经为我们提供了一个工具方法。
最后,有一个 createMultipartParamList 方法,用它我们可以创建 multipart 类型的参数列表,需要再次强调的是,multipart 类型这并非是普通表单类型。
需要说明的是,在 UploadHelper 中调用了 FileUtil 的几个新增的方法:
public class FileUtil {... // 获取真实文件名(去掉文件路径) public static String getRealFileName(String fileName) { return FilenameUtils.getName(fileName); } // 获取编码后的文件名(将文件名进行 Base64 编码) public static String getEncodedFileName(String fileName) { String prefix = FilenameUtils.getBaseName(fileName); String suffix = FilenameUtils.getExtension(fileName); return CodecUtil.encodeBase64(prefix) + "." + suffix; } // 获取解码后的文件名(将文件名进行 Base64 解码) public static String getDecodedFileName(String fileName) { String prefix = FilenameUtils.getBaseName(fileName); String suffix = FilenameUtils.getExtension(fileName); return CodecUtil.decodeBase64(prefix) + "." + suffix; }}
我们封装了 org.apache.commons.io.FilenameUtils,使用这个工具类可以获取文件的真实名称(考虑到使用 IE 上传文件会带有文件路径),此外还提供了文件名 Base64 的编码与解码功能(为了防止中文乱码问题)。
值得一提的是,在 UploadHelper 中还做了一个异常转换,也就是将 Apache Commons FileUpload 的内部异常 FileUploadBase.FileSizeLimitExceededException 转换为我们的自定义异常 UploadException 。这样做是为了在 UploadHelper 的使用者 DispatcherServlet 中捕获最准确的异常信息。
或许通过阅读 Smart 源码,会让您看清更多的细节。
通过这两个步骤,文件上传功能已基本实现,文件字段可以与普通字段一起提交到服务器端了。
您还等什么?赶紧去看看最新的 Smart Sample 代码示例吧!