用 Servlet 进行文件上传的原理和实现Word下载.docx
《用 Servlet 进行文件上传的原理和实现Word下载.docx》由会员分享,可在线阅读,更多相关《用 Servlet 进行文件上传的原理和实现Word下载.docx(12页珍藏版)》请在冰豆网上搜索。
multipart/form-data"
的编码方式,采用这种方式,浏览器可以很容易的表单内的数据和文件一起。
这种编码方式先定义好一个不可能在数据中出现的字符串作为分界符,然后用它将各个数据段分开,而对于每个数据段都对应着HTML页面表单中的一个Input区,包括一个content-disposition属性,说明了这个数据段的一些信息,如果这个数据段的内容是一个文件,还会有Content-Type属性,然后就是数据本身。
这里,我们可以编写一个简单的Servlet来看到浏览器到底是怎样编码的。
实现流程:
∙重载HttpServlet中的doPost方法
∙调用request.getContentLength()得到Content-Length,并定义一个与Content-Length大小相等的字节数组buffer。
∙从HttpServletRequest的实例request中得到一个InputStream,并把它读入buffer中。
∙使用FileOutputStream将buffer写入指定文件。
代码清单
//ReceiveServlet.java
importjava.io.*;
importjavax.servlet.*;
importjavax.servlet.http.*;
//示例程序:
记录下Form提交上来的数据,并存储到Log文件中
publicclassReceiveServletextendsHttpServlet
{
publicvoiddoPost(HttpServletRequestrequest,HttpServletResponseresponse)
throwsIOException,ServletException
{
//1
intlen=request.getContentLength();
bytebuffer[]=newbyte[len];
//2
InputStreamin=request.getInputStream();
inttotal=0;
intonce=0;
while((total<
len)&
&
(once>
=0)){
once=in.read(buffer,total,len);
total+=once;
}
//3
OutputStreamout=newBufferedOutputStream(newFileOutputStream("
Receive.log"
true));
byte[]breaker="
\r\nNewLog:
-------------------->
\r\n"
.getBytes();
System.out.println(request.getContentType());
out.write(breaker,0,breaker.length);
out.write(buffer);
out.close();
in.close();
}
∙在使用Opera作为浏览器测试时,从指定的文件(Receive.log)中可以看到如下的内容
--_OPERAB__-T/DQLi2fn47+D52OOrpdrz
Content-Disposition:
form-data;
id"
id00
file3"
;
filename="
Autoexec.bat"
Content-Type:
application/octet-stream
@echooff
prompt$d$t[$p]$_$$
--_OPERAB__-T/DQLi2fn47+D52OOrpdrz--
这里_OPERAB__-T/DQLi2fn47+D52OOrpdrz就是浏览器指定的分界符,不同的浏览器有不同的确定分界符的方法,但都需要保证分界符不会在文件内容中出现。
下面是用IE进行测试的结果
-----------------------------7d137a26e18
name"
123
introduce"
Iam...
Iam..
C:
\Autoexec.bat"
SETPATH=d:
\pf\IBMVJava2\eab\bin;
%PATH%;
D:
\PF\ROSE98I\COMMON
-----------------------------7d137a26e18--
这里---------------------------7d137a26e18作为分界符。
关于分界符的规则可以概况为两条:
∙除了最后一个分界符,每个分界符后面都加一个CRLF即'
\u000D'
和'
\u000A'
最后一个分界符后面是两个分隔符"
--"
∙每个分界符的开头也要加一个CRLF和两个分隔符("
-"
)。
浏览器采用默认的编码方式是application/x-www-form-urlencoded,可以通过指定form标签中的enctype属性使浏览器知道此表单是用multipart/form-data方式编码如:
formaction="
/servlet/ReceiveServlet"
ENCTYPE="
method=post>
C)提交请求
提交请求的过程由浏览器完成的,并且遵循HTTP协议,每一个从浏览器端到服务器端的一个请求,都包含了大量与该请求有关的信息,在Servlet中,HttpServletRequest类将这些信息封装起来,便于我们提取使用。
在文件上载和表单提交的过程中,有两个关心的问题,一是上载的数据是是采用的那种方式的编码,这个问题的可以从Content-Type中得到答案,另一个是问题是上载的数据量有多少即Content-Length,知道了它,就知道了HttpServletRequest的实例中有多少数据可以读取出来。
这两个属性,我们都可以直接从HttpServletRequest的一个实例中获得,具体调用的方法是getContentType()和getContentLength()。
Content-Type是一个字符串,在上面的例子中,增加
System.out.println(request.getContentType());
可以得到这样的一个输出字符串:
multipart/form-data;
boundary=---------------------------7d137a26e18
前半段正是编码方式,而后半段正是分界符;
任务分解上述的字符串,取出分界符。
通过String类中的方法,我们可以把这个字符串分解,提取出分界符。
StringcontentType=request.getContentType();
intstart=contentType.indexOf("
boundary="
);
intboundaryLen=newString("
).length();
Stringboundary=contentType.substring(start+boundaryLen);
boundary="
+boundary;
判断编码方式可以直接用String类中的startsWith方法判断。
if(contentType==null||!
contentType.startsWith("
))
这样,我们在解码前可以知道:
编码的方式是否是multipart/form-data
数据内容的分界符
数据的长度
我们可以用类似于ReceiveServlet中的方式将这个请求的输入流读入一个长度为Content-Length的字节数组,接下来就是将这个字节数组里的内容全部提取出来了。
D)解码
解码对我们来说是整个上载过程最繁琐的一个步骤,经过以上的流程,我们可以得到一个包含有所有上载数据的一个字节数组和一个分界符,通过对Receive.log分析,还可以得到每个数据段中的分界符。
而我们要得到以下内容:
∙提交的表单中的各个字段以及对应的值
∙如果表单中有file控件,并且用户选择了上载文件,则需要分析出字段的名称、文件在浏览器端的名字、文件的Content-Type和文件的内容。
字节数组的内容可以分解如下:
具体解码过程也可以分为两个步骤:
∙将上载的数据分解成数据段,每个数据段对应着表单中的一个Input区。
∙对每个数据段,再进行分解,提出上述要求得到的内容。
这两个步骤主要的操作有两个,一个是从一个数组中找出另一个数组的位置,类似于String类中的indexOf的功能,另一个是从一个数组中提取出另一个数组,类似于String类中的substring的功能,为此我们可以专门写两个方法,实现这种功能。
intbyteIndexOf(byte[]source,byte[]search,intstart)
byte[]subBytes(byte[]source,intfrom,intend)
为了便于使用,可以从这两个方法中衍生出下列方法
intbyteIndexOf(byte[]source,Stringsearch,intstart)以一个String作为搜索对象参数
StringsubBytesString(byte[]source,intfrom,intend)直接返回一个String
intbytesLen(Strings)返回字符串转化为字节数组后,字节数组的长度
这样,从一个字节数组中,根据标记提取出另一个字节数组可以表示如下:
假设我们已经将数据存入字节数组buffer中,分界符存入Stringboundary中
intpos1=0;
//pos1记录在buffer中下一个boundary的位置
//pos0,pos1用于subBytes的两个参数
intpos0=byteIndexOf(buffer,boundary,0);
//pos0记录boundary的第一个字节在buffer中的位置
do
pos0+=boundaryLen;
//记录boundary后面第一个字节的下标
pos1=byteIndexOf(buffer,boundary,pos0);
if(pos1==-1)
break;
pos0+=2;
//考虑到boundary后面的\r\n
PARSE[(subBytes(buffer,pos0,pos1-2));
]
pos0=pos1;
}while(true);
其中PARSE部分是对每一个数据段进行解码的方法,考虑到Content-Disposition等属性,首先定义一个String数组
String[]tokens={"
name=\"
"
"
\"
filename=\"
"
\r\n\r\n"
};
对于一个不是文件的数据段,只可能有tokens中的第一个元素和最后一个元素,如果是一个文件数据段,则包含所有的元素。
第一步先得到tokens中每个元素在这个数据段中的位置
int[]position=newint[tokens.length];
for(inti=0;
i<
tokens.length;
i++)
position[i]=byteIndexOf(buffer,tokens[i],0);
第二步判断是否是一个文件数据段,如果是一个文件数据段则position[1]应该大于0,并且postion[1]应该小于postion[2]即position[1]>
0&
position[1]<
position[2]如果为真,则为一个文件数据段,
1.得到字段名
Stringname=subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[1]);
2.得到文件名
Stringfile=subBytesString(buffer,position[1]+bytesLen(tokens[1]),position[2]);
3.得到Content-Type
StringcontentType=subBytesString(buffer,position[3]+bytesLen(tokens[3]),position[4]);
4.得到文件内容
byte[]b=subBytes(buffer,position[4]+bytesLen(tokens[4]),buffer.length);
否则,说明数据段是一个name/value型的数据段,
且name在tokens[0]和tokens[2]之间,value在tokens[4]之后
//1.得到name
Stringname=subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[2]);
//2.得到value
Stringvalue=subBytesString(buffer,position[4]+bytesLen(tokens[4]),buffer.length);
回页首
三、具体实现
为便于使用,定义upload包,包括以下类:
ContentFactory
对从client中传来的数据进行解码,并提供一系列get方法,从中得到上传的各种信息。
具体接口如下
staticContentFactory
getContentFactory(javax.servlet.http.HttpServletRequestrequest)
返回根据当前请求生成的一个ContentFactory实例
getContentFactory(javax.servlet.http.HttpServletRequestrequest,intmaxLength)
FileHolder
getFileParameter(java.lang.Stringname)
返回一个FileHolder实例,该实例包含了通过字段名为name的file控件上载的文件信息,如果不存在这个字段或者提交页面时,没有选择上载的文件,则返回null。
java.util.Enumeration
getFileParameterNames()
返回一个由String对象构成的Enumeration,包含了Html页面窗体中所有file控件的name属性。
FileHolder[]
getFileParameterValues(java.lang.Stringname)
返回一个FileHolder数组,该数组包含了所有通过字段名为name的file控件上载的文件信息,如果不存在这个字段或者提交页面时,没有选择任何上载的文件,则返回一个零元素的数组(不是null)。
java.lang.String
getParameter(java.lang.Stringname)
以String类型返回请求的参数的值,如果该参数不存在,则返回为null。
参数存于提交的表单数据中。
getParameterNames()
返回一个String类型的Enumeration对象,该对象包含了所有提交请求的参数名称。
java.lang.String[]
getParameterValues(java.lang.Stringname)
返回String类型的数组,该数组包含了指定名称的参数对应的所有的值,如果参数不存在,则返回为null。
封装一个文件数据段,可以从中提取文件名,Content-Type和文件内容等属性。
接口如下:
byte[]
getBytes()
返回一个文件内容的字节数组
getContentType()返回该文件的Content-Type
getFileName()
返回该文件在文件上载前在客户端的名称
getParameterName()
返回上载该文件时,Html页面窗体中file控件的name属性
void
saveTo(java.io.Filefile)
把文件的内容存到指定的文件中
saveTo(java.lang.Stringname)
ContentFactoryException
在ContentFactory.getContentFactory方法中可能抛出。
各类的源文件详解代码清单。
四、使用示例
附录中包含了一个Servlet示例,该示例重载了HttpServlet的两个方法(doGet,doPost),在浏览器发送GET请求时,产生一个表单,在用户提交表单时,将文件和数据上载,并在浏览器端显示出上载文件存盘后的URL,以及页面中的各字段的name和value。
该示例及各类在Windows98、jdk1.3和tomcat3.1,浏览器为IE5和Opera3.6的环境下调试通过。
五、附录
∙代码清单
o1、ContentFactory.java
o2、FileHolder.java
o3、ContentFactoryException.java
o4、FormUpload.java
∙示例及整个upload包,以及javadoc生成的API文档(source.zip)
参考资料
∙RFC1867Form-basedFileUploadinHTML
∙RFC2045/2046MIME(MultipurposeInternetMailExtensions)
∙RFC1806TheContent-DispositionHeader
∙RFC2388ReturningValuesfromForms:
multipart/form-data