关于文件上传的改进

本文介绍了一种基于Smart框架的文件上传方案,通过自定义Multipart类和UploadHelper工具类实现了文件与表单数据的一体化处理。

之前在 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 标签上添加两个属性:

  1. method="post"(默认为 get)

  2. enctype="multipart/form-data"(默认为 application/x-www-form-urlencoded)

此外,必须使用 type="file" 的 input 标签才能上传文件。

想必上面的一切都能与大家达成共识,但对于文件上传的服务端开发或许会五花八门。

有些朋友通过 Servlet API 来获取 Request 中的输入流,从而解析出所提交的数据(包括普通字段与文件字段),但这样做未免太过于繁琐。在 Java 圈子里有个响当当的文件上传类库,它就能帮您一个大忙,它就是 Apache Commons FileUpload

大名鼎鼎的 Spring 也支持文件上传,同时也支持 FileUpload 类库,我个人真心觉得 Spring 已经做得很好了,网上可以搜索到大量的参考资料来讲解如何通过 Spring 来实现文件上传。

如果要总结一下的话,使用 Spring 来实现文件上传,我们大致需要做以下几件事情:

  1. 在 Spring 配置文件中,定义一个 Bean(org.springframework.web.multipart.commons.CommonsMultipartResolver),可指定文件上传的大小限制,比如最大只能上传 10M 的文件。

  2. 在 Controller 方法的参数中,提供一个 MultipartFile 类型的参数,用来接收已上传的文件对象,可通过 MultipartFile 对象获取 InputStream,其它普通字段可通过其它参数进行映射。

  3. 创建一个 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 代码示例吧!

转载于:https://my.oschina.net/huangyong/blog/184619

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值