实现文件上传

本文是《轻量级 Java Web 框架架构设计》的系列博文

在 Web 应用中,文件上传功能是非常基础的功能。过去,我们一般使用第三方类库,例如:Apache Comons Fileupload 实现该功能。Spring 也没有内置文件上传功能,只是提供了对第三方类库的集成。

今天我打算使用 Servlet 3.0 来实现文件上传功能,您可以从中体会一下,这种方法是否优良?

好,开始了!

创建一个 Servlet,名为 UploadServlet:

@MultipartConfig
@WebServlet(name = "upload", urlPatterns = "/upload.do")
public class UploadServlet extends HttpServlet {

    private static final String UPLOAD_BASE_PATH = "www/upload/"; // 文件上传基础路径
    private static final String UPLOAD_RELATIVE_PATH = "path";    // 文件上传相对路径
    private static final String UPLOAD_FILE_NAME = "file";        // 文件标签的 file 名称

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取文件路径
        String relativePath = request.getParameter(UPLOAD_RELATIVE_PATH);
        String filePath = WebUtil.getFilePath(request, UPLOAD_BASE_PATH + relativePath);

        // 获取文件名
        Part part = request.getPart(UPLOAD_FILE_NAME);
        String fileName = WebUtil.getFileName(request, part);

        // 写入文件
        part.write(filePath + "/" + fileName);

        // 返回结果
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("fileName", fileName);
        data.put("fileType", part.getContentType());
        data.put("fileSize", part.getSize());
        WebUtil.writeJSON(response, new Result(true).data(data));
    }
}

在 UploadServlet 的头上标注了 @MultipartConfig 注解与 @WebServlet 注解。我们知道,@WebServlet 注解表示将该类声明为 Servlet。而 @MultipartConfig 注解就是告诉 Web 服务器(如:Tomcat),该 Servlet 支持 multipart,也就是文件上传功能。

使用 Servlet 3.0 API,可以调用 request 的 getPart() 方法,获取一个 Part 对象,这个对象封装了所上传的文件及其基本信息。当然,还可获取一批 Part 对象,此时可实现批量文件上传。该示例中只实现了单个文件上传。

此外,我提供了一个 WebUtil.getFileName() 方法,用于获取上传文件的文件名,代码如下:

public class WebUtil {

    ...

    // 获取文件名
    public static String getFileName(HttpServletRequest request, Part part) {
        // 防止中文乱码(可放在 EncodingFilter 中处理)
//        request.setCharacterEncoding("UTF-8");

        // 从 Request 头中获取文件名
        String cd = part.getHeader("Content-Disposition");
        String fileName = cd.substring(cd.lastIndexOf("=") + 2, cd.length() - 1);

        // 解决 IE 浏览器文件名问题
        if (fileName.contains("\\")) {
            fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
        }

        return fileName;
    }

    ...
}

需要注意的是,可通过 Part 对象中获取请求头信息,并从中获取 Content-Disposition 属性,这个属性里就包括了文件名。但 IE 浏览器有些变态,需要做特殊处理才能获取正确的文件名,其他浏览器没有任何问题。

需要说明的是,为防止路径中带有中文字符,需要将请求进行 UTF-8 编码。如果您使用了字符编码过滤器,那么可以去掉这行代码。以下是我实现的字符编码过滤器:

@WebFilter(
    urlPatterns = "/*",
    initParams = {
        @WebInitParam(name = "encoding", value = "UTF-8")
    }
)
public class EncodingFilter implements Filter {

    private String encoding;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        encoding = filterConfig.getInitParameter("encoding");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        request.setCharacterEncoding(encoding);
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

这里也使用了 Servlet 3.0 提供的 @WebFilter 注解,表明该类是一个 Filter。可在注解中设置初始化参数(init-param),当然也可以定义一个 static 变量,写死在代码中,或者从配置文件中读取。

好了,来一个文件上传页面吧:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Upload Product Picture</title>
    <link rel="stylesheet" href="../style/global.css"/>
    <script type="text/javascript" src="../lib/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="../lib/jquery-form/jquery.form.min.js"></script>
    <script type="text/javascript" src="../script/product/product_upload.js"></script>
</head>
<body>

<form id="upload_form" action="/upload.do" method="post" enctype="multipart/form-data">
    <input type="hidden" name="path" value="product"/>
    <p>
        <label for="file">Picture:</label>
        <input type="file" id="file" name="file"/>
        <button type="submit">Upload</button>
    </p>
</form>
<br/>
<div id="console"></div>

</body>
</html>

表单中传递了两个参数:file 与 path。其中, file 是需要上传的文件(本地路径),path 是文件上传到服务器中的相对路径(在 UploadServlet 中已有定义 ),可由程序员定制需要上传文件的相对路径

最后再看 JS 代码吧:

$(function() {
    $('#upload_form').ajaxForm({
        beforeSubmit: function() {
            var file = $('#file').val();
            if (file == '') {
                alert('Please choose a JPG file!');
                return false;
            }
            if (file.substring(file.lastIndexOf('.') + 1).toLowerCase() != 'jpg') {
                alert('This file is not JPG format!');
                return false;
            }
        },
        success: function(result) {
            if (result.success) {
                var data = result.data;
                if (data) {
                    var html = '';
                    html += '<div>File Name: ' + data.fileName + '</div>';
                    html += '<div>File Type: ' + data.fileType + '</div>';
                    html += '<div>File Size: ' + data.fileSize + '</div>';
                    html += '<img src="../upload/product/' + data.fileName + '" style="width: 400px;"/>';
                    $('#console').html(html);
                }
            }
        }
    });
});

JS 中使用了 jQuery 与 jQuery Form,实现 Ajax 文件上传功能,上传成功后,解析返回的 JSON 数据,并渲染到界面上。

运行一下!

实现文件上传

打完收工!

在 Smart Framework 中,将文件上传功能单独拿出来,就像上面所提到的那样。只要用户发送 upload.do 请求,就会将该请求交给 UploadServlet。

那么如何才能将这类特殊请求,转发到指定的 Servlet 中呢?需要修改 DispatcherServlet:

@WebServlet(urlPatterns = "/*", loadOnStartup = 0)
public class DispatcherServlet extends HttpServlet {

    @Override
    public void init(ServletConfig config) throws ServletException {
        // 初始化 Helper 类
        initHelper();

        // 添加 Servlet 映射
        addServletMapping(config.getServletContext());
    }

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ...
    }

    private void addServletMapping(ServletContext context) {
        // 用 DefaultServlet 映射所有静态资源
        ServletRegistration defaultServletRegistration = context.getServletRegistration("default");
        defaultServletRegistration.addMapping("/favicon.ico", "/www/*");

        // 用 UploadServlet 映射 /upload.do 请求
        ServletRegistration uploadServletRegistration = context.getServletRegistration("upload");
        uploadServletRegistration.addMapping("/upload.do");
    }
}

在 init() 方法中调用了自定义的 addServletMapping() 方法,在该方法中,配置了所有特殊请求的映射关系。

至此,文件上传功能已基本实现,欢迎您的评论!


补充(2013-09-30)

个人认为在 @Filter 中通过 @InitParams 来定义 encoding 为 UTF-8,这种方式不是特别好。因为还需要在 init() 方法中通过 filterConfig.getInitParameter("encoding") 来获取所配置的编码方式,有些脱裤子放屁的嫌疑。还不如直接将 UTF-8 写死在程序中,再说一般情况下编码方式是不需要调整的。

@WebFilter("/*")
public class EncodingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        request.setCharacterEncoding("UTF-8");
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

你可能感兴趣的:(实现文件上传)