本文是《轻量级 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() { } }