欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JSP 实用程序之简易文件上传组件

程序员文章站 2022-03-24 23:46:28
文件上传,包括但不限于图片上传,是 web 开发中司空见惯的场景,相信各位或多或少都曾写过这方面相关的代码。java 界若说文件上传,则言必称 apache commons fileupload,论...

文件上传,包括但不限于图片上传,是 web 开发中司空见惯的场景,相信各位或多或少都曾写过这方面相关的代码。java 界若说文件上传,则言必称 apache commons fileupload,论必及 smartupload。更甚者,servlet 3.0 将文件上传列为 jsr 标准,使得通过几个注解就可以在 servlet 中配置上传,无须依赖任何。使用第三方组件或 servlet 自带组件固然强大,但只靠 jsp 亦能完成任务,且短小而精悍,岂不美哉?本文实现的方法纯粹基于 jsp 代码,没有弄成 servlet 和专门的 class(.java),实现方法纯粹是基于 jsp,没有太高的技术难度。实际使用过程中直接部署即可。

操作组件的代码行数不超过 10 行,只需几个步骤:

生成组件实例设置实例属性调用上传/下载方法处理调用结果

首先是上传页面,本例是一张静态的 html。

JSP 实用程序之简易文件上传组件

上传成功如下图所示。

JSP 实用程序之简易文件上传组件

使用 post 的表单,设置 contenttype 为 multipart/form-data 多段数据,还要记得 input 的 name 属性。

<html>
<body>
	<form action="action.jsp" enctype="multipart/form-data" method="post">
		selectimage: <input type="file" name="myfile" /><br> <input
			type="submit" value="upload" />
	</form>
</body>
</html>

action 中接受客户端请求的服务端代码在 action.jsp 中。action.jsp 通过<%@include file="upload.jsp"%>包含了核心 java 代码,而 upload.jsp 里面又包含了另外一个 uploadrequest.jsp 文件。总之,我们这个小小的 java 程序,一共包含了 uploadrequest 请求信息类、uploadexception 自定义异常类和最重要的 upload 类这三个类。

<%@page pageencoding="utf-8"%>
<%@include file="upload.jsp"%>
<%
	uploadrequest ur = new uploadrequest();// 创建请求信息,所有参数都在这儿设置
	ur.setrequest(request);	//一定要传入 request
	ur.setfileoverwrite(true);// 相同文件名是否覆盖?true=允许覆盖

	upload upload = new upload();// 上传器

	try {
		upload.upload(ur);
	} catch (uploadexception e) {
		response.getwriter().println(e.tostring());
	}

	if (ur.isok()) // 上传成功
		response.getwriter().println("上传成功:" + ur.getuploaded_save_filename());
	else
		response.getwriter().println("上传失败!");
%>

这里创建了 uploadrequest 实例。文件上传操作通常会附加一些限制,如:文件类型、上传文件总大小、每个文件的最大大小等。除此以外,作为一个通用组件还需要考虑更多的问题, 如:支持自定义文件保存目录、支持相对路径和绝对路径、支持自定义保存的文件的文件名称等。这些配置通通在 uploadrequest 里设置。

至于 jsp 里面的类,我愿意多说说。 jsp 里面允许我们定义 java 的类,类本是可以是 static,但不能有 static 成员。实际上 jsp 类都是内部类,定义 static 与否关系不大。如果不能定义 static 方法,就把 static 方法移出类体外,书写成,

 <%!

    /**
     * 获取开头数据头占用的长度
     *
     * @param datebytes
     *            文件二进制数据
     * @return
     */
    private static int getstartpos(byte[] datebytes) {

      ....

    }

%>

<%! ... %> 和 <% ... %> 不同,前者是定义类成员的。

好~我们在看看 uploadrequest.jsp,就知道具体配置些什么。

<%@page pageencoding="utf-8"%>
<%!/**
	 * 上传请求的 bean,包含所有有关请求的信息
	 * @author frank
	 *
	 */
	public static class uploadrequest {
		/**
		 * 上传最大文件大小,默认 1 mb
		 */
		private int maxfilesize = 1024 * 1000;

		/**
		 * 保存文件的目录
		 */
		private string upload_save_folder = "e:\\temp\\";

		/**
		 * 上传是否成功
		 */
		private boolean isok;

		/**
		 * 是否更名
		 */
		private boolean isnewname;

		/**
		 * 成功上传之后的文件名。如果 isnewname = false,则是原上传的名字
		 */
		private string uploaded_save_filename;

		/**
		 * 相同文件名是否覆盖?true=允许覆盖
		 */
		private boolean isfileoverwrite = true;

		private httpservletrequest request;

		/**
		 * @return the maxfilesize
		 */
		public int getmaxfilesize() {
			return maxfilesize;
		}

		/**
		 * @param maxfilesize the maxfilesize to set
		 */
		public void setmaxfilesize(int maxfilesize) {
			maxfilesize = maxfilesize;
		}

		/**
		 * @return the upload_save_folder
		 */
		public string getupload_save_folder() {
			return upload_save_folder;
		}

		/**
		 * @param upload_save_folder the upload_save_folder to set
		 */
		public void setupload_save_folder(string upload_save_folder) {
			this.upload_save_folder = upload_save_folder;
		}

		/**
		 * @return the isok
		 */
		public boolean isok() {
			return isok;
		}

		/**
		 * @param isok the isok to set
		 */
		public void setok(boolean isok) {
			this.isok = isok;
		}

		/**
		 * @return the isnewname
		 */
		public boolean isnewname() {
			return isnewname;
		}

		/**
		 * @param isnewname the isnewname to set
		 */
		public void setnewname(boolean isnewname) {
			this.isnewname = isnewname;
		}

		/**
		 * @return the uploaded_save_filename
		 */
		public string getuploaded_save_filename() {
			return uploaded_save_filename;
		}

		/**
		 * @param uploaded_save_filename the uploaded_save_filename to set
		 */
		public void setuploaded_save_filename(string uploaded_save_filename) {
			this.uploaded_save_filename = uploaded_save_filename;
		}

		/**
		 * @return the isfileoverwrite
		 */
		public boolean isfileoverwrite() {
			return isfileoverwrite;
		}

		/**
		 * 相同文件名是否覆盖?true=允许覆盖
		 * @param isfileoverwrite the isfileoverwrite to set
		 */
		public void setfileoverwrite(boolean isfileoverwrite) {
			this.isfileoverwrite = isfileoverwrite;
		}

		/**
		 * @return the request
		 */
		public httpservletrequest getrequest() {
			return request;
		}

		/**
		 * @param request the request to set
		 */
		public void setrequest(httpservletrequest request) {
			this.request = request;
		}

	}
	
%>

这是一个普通的 java bean。完成上传逻辑的是 upload 类。 其原理是,1、由客户端把要上传的文件生成 request 数据流,与服务器端建立连接;2、在服务器端接收 request 流,将流缓存到内存中;3、由服务器端的内存把文件输出到指定的目录。upload.jsp 完整代码如下所示。

<%@page pageencoding="utf-8" import="java.io.*"%>
<%@include file="uploadrequest.jsp"%>
<%!

public static class uploadexception extends exception {
	
	private static final long serialversionuid = 579958777177500819l;

	public uploadexception(string msg) {
		super(msg);
	}

}

public static class upload {
	/**
	 * 接受上传
	 * 
	 * @param urequest
	 *            上传 pojo
	 * @return
	 * @throws uploadexception
	 */
	public uploadrequest upload(uploadrequest urequest) throws uploadexception {
		httpservletrequest req = urequest.getrequest();
		
		// 取得客户端上传的数据类型
		string contenttype = req.getcontenttype();

		if(!req.getmethod().equals("post")){
			throw new uploadexception("必须 post 请求");
		}
		
		if (contenttype.indexof("multipart/form-data") == -1) {
			throw new uploadexception("未设置表单  multipart/form-data");
		}
		
		int formdatalength = req.getcontentlength();
		
		if (formdatalength > urequest.getmaxfilesize()) { // 是否超大
			throw new uploadexception("文件大小超过系统限制!");
		}
		
		// 保存上传的文件数据
		byte datebytes[] = new byte[formdatalength];
		int byteread = 0, totalread = 0;

		try(datainputstream in = new datainputstream(req.getinputstream());){
			while (totalread < formdatalength) {
				byteread = in.read(datebytes, totalread, formdatalength);
				totalread += byteread;
			}
		} catch (ioexception e) {
			e.printstacktrace();
			throw new uploadexception(e.tostring());
		}				
				
		// 取得数据分割字符串
		int lastindex = contenttype.lastindexof("="); // 数据分割线开始位置boundary=---------------------------
		string boundary = contenttype.substring(lastindex + 1, contenttype.length());// ---------------------------257261863525035

		// 计算开头数据头占用的长度
		int startpos = getstartpos(datebytes);
		// 边界位置
		int endpos = byteindexof(datebytes, boundary.getbytes(), (datebytes.length - startpos)) - 4;

		// 创建文件
		string filename = urequest.getupload_save_folder() + getfilename(datebytes, urequest.isnewname());
		urequest.setuploaded_save_filename(filename);
		file checkedfile = initfile(urequest);

		// 写入文件
		try(fileoutputstream fileout = new fileoutputstream(checkedfile);){
			fileout.write(datebytes, startpos, endpos - startpos);
			fileout.flush();
			
			urequest.setok(true);
		} catch (filenotfoundexception e) {
			e.printstacktrace();
			throw new uploadexception(e.tostring());
		} catch (ioexception e) {
			e.printstacktrace();
			throw new uploadexception(e.tostring());
		} 
		
		return urequest;
	}
}

	/**
	 * 获取开头数据头占用的长度
	 * 
	 * @param datebytes
	 *            文件二进制数据
	 * @return
	 */
	private static int getstartpos(byte[] datebytes) {
		int startpos;
		startpos = byteindexof(datebytes, "filename=\"".getbytes(), 0);
		startpos = byteindexof(datebytes, "\n".getbytes(), startpos) + 1; // 遍历掉3个换行符到数据块
		startpos = byteindexof(datebytes, "\n".getbytes(), startpos) + 1;
		startpos = byteindexof(datebytes, "\n".getbytes(), startpos) + 1;
		
		return startpos;
	}
	
	/**
	 * 在字节数组里查找某个字节数组,找到返回>=0,未找到返回-1
	 * @param data
	 * @param search
	 * @param start
	 * @return
	 */
	private static int byteindexof(byte[] data, byte[] search, int start) {
		int index = -1;
		int len = search.length;
		for (int i = start, j = 0; i < data.length; i++) {
			int temp = i;
			j = 0;
			while (data[temp] == search[j]) {
				// system.out.println((j+1)+",值:"+data[temp]+","+search[j]);
				// 计数
				j++;
				temp++;
				if (j == len) {
					index = i;
					return index;
				}
			}
		}
		return index;
	}
	
	/**
	 * 如果没有指定目录则创建;检测是否可以覆盖文件
	 * 
	 * @param urequest
	 *            上传 pojo
	 * @return
	 * @throws uploadexception
	 */
	private static file initfile(uploadrequest urequest) throws uploadexception {
		file dir = new file(urequest.getupload_save_folder());
		if (!dir.exists())
			dir.mkdirs();
		
		file checkfile = new file(urequest.getuploaded_save_filename());
		
		if (!urequest.isfileoverwrite() && checkfile.exists()) {
			throw new uploadexception("文件已经存在,禁止覆盖!");
		}
		
		return checkfile;
	}
	
	/**
	 * 获取 post body 中的文件名
	 * 
	 * @param datebytes
	 *            文件二进制数据
	 * @param isautoname
	 *            是否自定命名,true = 时间戳文件名
	 * @return
	 */
	private static string getfilename(byte[] datebytes, boolean isautoname) {
		string savefile = null;
		
		if(isautoname){
			savefile = "2016" + system.currenttimemillis();
		} else {
			string data = null;
			try {
				data = new string(datebytes, "utf-8");
			} catch (unsupportedencodingexception e) {
				e.printstacktrace();
				data = "errfilename";
			}
			
			// 取得上传的文件名
			savefile = data.substring(data.indexof("filename=\"") + 10);
			savefile = savefile.substring(0, savefile.indexof("\n"));
			savefile = savefile.substring(savefile.lastindexof("\\") + 1, savefile.indexof("\""));
		}
		
		return savefile;
	}
%>

通过 datainputstream 读取流数据到 databytes 中然后写入 fileoutputstream。另外还有些围绕配置的逻辑。

值得一提的是,tomcat 7 下 jsp 默认的 java 语法仍旧是 1.6 的。在 jsp 里面嵌入 java 1.7 特性的代码会抛出“resource specification not allowed here for source level below 1.7”的异常。于是需要修改 tomcat/conf/web.xml 里面的配置文件,找到 节点,加入下面粗体部分才可以。注意是 jsp 节点,不是 default 节点(很相似)。

 
        jsp
        org.apache.jasper.servlet.jspservlet
        
            forkfalsexpoweredbyfalsecompilersourcevm1.7compilertargetvm1.7
        
        3
    

至此,一个简单的文件上传器就完成了。但是本组件的缺点还是很明显的,试列举两项:一、上传流占用内存而非磁盘,所以上传大文件时内存会吃紧;二、尚不支持多段文件上传,也就是一次只能上传一个文件。