侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130562 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

关于文件上传的改进

2022-06-29 星期三 / 0 评论 / 0 点赞 / 72 阅读 / 26924 字

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

  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 这样做是否可行呢?

@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 代码示例吧!

广告 广告

评论区