jFinal基于COS组件实现文件上传进度条的一种解决方案
近端时间拿jfinal在移动WEB上鼓捣,发现手机在非WIFI模式下上传图片有点慢,拿iphone为例,用户实时拍照上传,照片的尺寸保守在3M以上(手机WEB浏览器要是都支持在前台降质量就好了),在2G\3G\4G网络下页面出现假死现象,其实图片正在上传,因而必须要给上传页面加上进度条才可。由于jFinal使用了cos组件来处理上传,而cos组件本身是没有进度数据接口的,搜索查到有一篇文章,是修改COS源码的(地址)。当然,作为一个coder,肯定不是很喜欢这种方式,继续搜索cos组件代理,这回搜索到了一点有用的东西(原文地址),在此基础上,对jfinal进行了相关类进行了扩展,相关如下:
首先,添加servlet底层输入流的代理ServletInputStreamProxy,代码如下:
package com.nq.jfinal.upload; import java.io.IOException; import javax.servlet.ServletInputStream; /** * 代理底层的输入流,参考http://bbs.csdn.net/topics/330245424 * @author liu_jingcheng@ctg.com.cn * */ public class ServletInputStreamProxy extends ServletInputStream{ ServletInputStream in; ProgressBarObserver observer; public ServletInputStreamProxy(ServletInputStream in, ProgressBarObserver observer) { this.in = in; this.observer = observer; } @Override public int read(byte[] b, int off, int len) throws IOException { int r = in.read(b, off, len); if (r != -1) { observer.incomingContent(r); } return r; } @Override public int read() throws IOException { return 0; } }
按参考思路继续添加HttpServletRequest的代理
package com.nq.jfinal.upload; import java.io.IOException; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; /** * 代理HttpServletRequest,参考http://bbs.csdn.net/topics/330245424 * @author liu_jingcheng@ctg.com.cn * */ public class HttpServletRequestProxy extends HttpServletRequestWrapper { private ProgressBarObserver observer; public HttpServletRequestProxy(HttpServletRequest request, ProgressBarObserver observer) { super(request); this.observer = observer; } /* (non-Javadoc) * @see javax.servlet.ServletRequestWrapper#getInputStream() */ public ServletInputStream getInputStream() throws IOException { ServletInputStream in = super.getInputStream(); return new ServletInputStreamProxy(in, observer); } }
我们添加一个被观察者ProgressBarObserver
package com.nq.jfinal.upload; import java.util.Observable; /** * 进度条组件被观察者 * @author liu_jingcheng@ctg.com.cn * */ public class ProgressBarObserver extends Observable { private int uploadedSize = 0; private ProgressBarEntity bar; public ProgressBarObserver(long totalSize, int uploadedSize) { super(); this.uploadedSize = uploadedSize; bar = new ProgressBarEntity(totalSize, uploadedSize); } public void incomingContent(int readSize) { uploadedSize += readSize; bar.setUploadedSize(uploadedSize); setChanged(); notifyObservers(bar); } public int getUploadedSize() { return uploadedSize; } public void setUploadedSize(int uploadedSize) { this.uploadedSize = uploadedSize; } }
代理相关的部分基本结束了,现在进入对jfinal原有类的扩展,主要是Controller类,我们定义一个扩展类ProgressBarController继承自Controller,话说jFinal2.1以前的原有类对继承一点都不友好。首先,我们先观察一下jFinal原有类Controller和MultipartRequest,需要在后者里面剥离部分代码出来直接使用(在这里偷个懒,再说原装的总是感觉踏实些),主要是3个方法handleSaveDirectory,wrapMultipartRequest【这个方法处理上传】,isSafeFile。再进入原装的Controller类中,获取上传文件相关的是getFile和getFiles的几个重载方法,这里我们需要把我们的上面创建的被观察者作为参数重载这几个方法,直接将原有类中这几个方法拷贝到ProgressBarController,在每一个方法中加入ProgressBarObserver参数,清空所有的方法体。
现在,我们开始重载第一个方法,以public UploadFile getFile(String parameterName, ProgressBarObserver observer) {}这个方法为例,创建代理类HttpServletRequestProxy对象,组装上传方法private List<UploadFile> wrapMultipartRequest(HttpServletRequest request, String saveDirectory, int maxPostSize, String encoding) {},没有的参数我们利用Constants取默认值,主要是将wrapMultipartRequest中HttpServletRequest参数传为我们创建的代理类对象,对wrapMultipartRequest的改动仅添加返回值List<UploadFile>。
重点在底层代理类ServletInputStreamProxy的read方法中触发被观察者对象变动,以唤起我们自定义的观察者,ProgressBarController代码如下,由于字数限制已删除部分重载方法:
package com.nq.jfinal.upload; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import javax.servlet.http.HttpServletRequest; import com.jfinal.config.Constants; import com.jfinal.core.Controller; import com.jfinal.core.JFinal; import com.jfinal.kit.PathKit; import com.jfinal.upload.UploadFile; import com.oreilly.servlet.multipart.DefaultFileRenamePolicy; import com.oreilly.servlet.multipart.FileRenamePolicy; /** * 进度条控制器,继承自原Controller * @author liu_jingcheng@ctg.com.cn * */ public class ProgressBarController extends Controller { static FileRenamePolicy fileRenamePolicy = new DefaultFileRenamePolicy(); public UploadFile getFile(String parameterName, String saveDirectory, int maxPostSize, ProgressBarObserver observer) { HttpServletRequestProxy uploadRequest = new HttpServletRequestProxy(getRequest(), observer); List<UploadFile> uploadFiles = wrapMultipartRequest(uploadRequest, saveDirectory, maxPostSize, JFinal.me().getConstants().getEncoding()); for (UploadFile uploadFile : uploadFiles) { if (uploadFile.getParameterName().equals(parameterName)) { return uploadFile; } } return null; } public UploadFile getFile(ProgressBarObserver observer) { HttpServletRequestProxy uploadRequest = new HttpServletRequestProxy(getRequest(), observer); Constants constants = JFinal.me().getConstants(); List<UploadFile> uploadFiles = wrapMultipartRequest(uploadRequest, constants.getUploadedFileSaveDirectory(), constants.getMaxPostSize(), constants.getEncoding()); return uploadFiles.size() > 0 ? uploadFiles.get(0) : null; } public UploadFile getFile(String parameterName, ProgressBarObserver observer) { HttpServletRequestProxy uploadRequest = new HttpServletRequestProxy(getRequest(), observer); Constants constants = JFinal.me().getConstants(); System.out.println(constants.getUploadedFileSaveDirectory()); List<UploadFile> uploadFiles = wrapMultipartRequest(uploadRequest, constants.getUploadedFileSaveDirectory(), constants.getMaxPostSize(), constants.getEncoding()); for (UploadFile uploadFile : uploadFiles) { if (uploadFile.getParameterName().equals(parameterName)) { return uploadFile; } } return null; } /**以下部分代码剥离自原装的MultipartRequest**/ /** * 添加对相对路径的支持 * 1: 以 "/" 开头或者以 "x:开头的目录被认为是绝对路径 * 2: 其它路径被认为是相对路径, 需要 JFinalConfig.uploadedFileSaveDirectory 结合 */ private String handleSaveDirectory(String saveDirectory) { if (saveDirectory.startsWith("/") || saveDirectory.indexOf(":") == 1) return saveDirectory; else{ //这个地方有修改 return PathKit.getWebRootPath() + "/" + saveDirectory; } } @SuppressWarnings("rawtypes") private List<UploadFile> wrapMultipartRequest(HttpServletRequest request, String saveDirectory, int maxPostSize, String encoding) { saveDirectory = handleSaveDirectory(saveDirectory); File dir = new File(saveDirectory); if ( !dir.exists()) { if (!dir.mkdirs()) { throw new RuntimeException("Directory " + saveDirectory + " not exists and can not create directory."); } } // String content_type = request.getContentType(); // if (content_type == null || content_type.indexOf("multipart/form-data") == -1) { // throw new RuntimeException("Not multipart request, enctype=\"multipart/form-data\" is not found of form."); // } List<UploadFile> uploadFiles = new ArrayList<UploadFile>(); try { com.oreilly.servlet.MultipartRequest multipartRequest = new com.oreilly.servlet.MultipartRequest(request, saveDirectory, maxPostSize, encoding, fileRenamePolicy); Enumeration files = multipartRequest.getFileNames(); while (files.hasMoreElements()) { String name = (String)files.nextElement(); String filesystemName = multipartRequest.getFilesystemName(name); // 文件没有上传则不生成 UploadFile, 这与 cos的解决方案不一样 if (filesystemName != null) { String originalFileName = multipartRequest.getOriginalFileName(name); String contentType = multipartRequest.getContentType(name); UploadFile uploadFile = new UploadFile(name, saveDirectory, filesystemName, originalFileName, contentType); if (isSafeFile(uploadFile)) uploadFiles.add(uploadFile); } } } catch (IOException e) { throw new RuntimeException(e); } return uploadFiles; } private boolean isSafeFile(UploadFile uploadFile) { String fileName = uploadFile.getFileName().trim().toLowerCase(); if (fileName.endsWith(".jsp") || fileName.endsWith(".jspx")) { uploadFile.getFile().delete(); return false; } return true; } }
主体代码部分基本完工,我们看看调用的情况,新建一个Controller继承自ProgressBarController代码如下:
package com.nq.jfinal.controller.mobile; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import com.jfinal.aop.Before; import com.jfinal.aop.Clear; import com.jfinal.kit.PathKit; import com.jfinal.kit.PropKit; import com.jfinal.render.JsonRender; import com.jfinal.upload.UploadFile; import com.nq.jfinal.interceptors.MobileAuthInterceptor; import com.nq.jfinal.interceptors.TokenInterceptor; import com.nq.jfinal.upload.ProgressBarController; import com.nq.jfinal.upload.ProgressBarEntity; import com.nq.jfinal.upload.ProgressBarObserver; import com.nq.jfinal.validators.NySdValidator; @Before(MobileAuthInterceptor.class) public class MobSdController extends ProgressBarController { @Clear public void _get_progressbar() { Object prc = getSessionAttr("progressbar"); if (prc == null) { prc = 0; } renderJson(prc); } @SuppressWarnings("unchecked") public void upload() { removeSessionAttr("progressbar"); ProgressBarObserver observer = new ProgressBarObserver(getRequest().getContentLength(), 0); observer.addObserver(new Observer() { @Override public void update(Observable o, Object arg) { //这里处理进度变化的事情 if (arg instanceof ProgressBarEntity) { ProgressBarEntity bar = (ProgressBarEntity) arg; setSessionAttr("progressbar", bar.getProgress()); System.out.println(bar.getTotalSize() + "\t" + bar.getUploadedSize() + "\t" + bar.getProgress()); } } }); UploadFile file = getFile("uploadFile", observer); //UploadFile file = getFile("uploadFile","",PropKit.getInt("fileMaxPostSize", 5242880)); //TODO } }
字数限制,ProgressBarEntity就不放出来了,这个类就是一个包含了totalSize、uploadedSize的实体类。
第一次发博文,希望能帮到需要的人,同时也请众位指正错误,这个方案正确的话,希望jfinal下个版本可以加入对进度条的支持!