之前在 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 这样做是否可行呢?
@Bean
public 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 代码示例吧!
本文介绍了一种基于Smart框架的文件上传方案,通过自定义Multipart类和UploadHelper工具类实现了文件与表单数据的一体化处理。

2472

被折叠的 条评论
为什么被折叠?



