一、 文件上传的原理
1. 表单元素的 enctype 属性
表单的 enctype 属性指定的是表单数据的编码方式 ,该属性有 3 个值:
1. application/x-www-form-urlencoded : 这是默认的编码方式,它只处理表单域里的 value 属性值,采用这种编码方式的表单会将表单域的值处理成 URL 编码方式。
2. multipart/form-data : 这种编码方式会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数里。
3. text/plain : 这种编码方式当表单的 action 属性为 mailto:URL 的形式时比较方便,这种方式主要适用于直接通过表单发送邮件的方式
下面来介绍 application/x-www-form-urlencoded 和 multipart/form-data 的区别。如下:
application.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title> enctype属性测试 </title>
<meta name="author" content="Yeeku.H.Lee" />
<meta name="website" content="http://www.crazyit.org" />
<meta http-equiv="Content-Type" content="text/html; charset=GBK" />
</head>
<body>
<form id="form1" name="form1"
enctype="application/x-www-form-urlencoded"
method="post" action="pro.jsp">
上传文件:<input type="file" name="file" /><br />
请求参数:<input type="text" name="wawa" /><br />
<input name="dd" type="submit" value="提交" />
</form>
</body>
</html>
下面是处理的 jsp
pro.jsp
<%@ page contentType="text/html;charset=GBK" %>
<%@ page import="java.io.*"%>
<%
//获取HTTP请求的输入流
InputStream is = request.getInputStream();
//以HTTP请求输入流建立一个BufferedReader对象
BufferedReader br = new BufferedReader(
new InputStreamReader(is));
//读取HTTP请求内容
String buffer = null;
while( (buffer = br.readLine()) != null)
{
//在页面中显示读取到的请求参数
out.println(buffer + "<br />");
}
%>
上面的处理页面直接通过二进制流来处理该 HTTP 请求,这是一种更底层的处理方式 ,当通过 HttpServletRequest 的 getParameter() 方法来获取请求参数时,实际上是 Web 服务器替我们处理了这种底层的二进制流 ,并将二进制流转换成对应的请求参数值。
如果上面 html 的 上传文件 是 logo.jpg , 请求参数 是 “crazyit标志” ,提交后 :
pro.jsp 页面将显示:
file=logo.jpg&wawa=crazyit%EA%D6%BE&dd=%CC%E1%BD%BB
如上,即使通过底层的二进制输入流,一样可以读到该请求的内容 :
一个普通字符串,包含了 3 个请求参数,即 file, wawa 和 dd
提示: 即使是“提交”按钮,表单一样将其当成一个表单域,一样转换成一个请求参数。浏览器会将表单里所有具有 name 属性的表单控件转换成请求参数,因此“提交”按钮也有 name 属性,因此也被转化请求参数
Java 提供了 2 个类: URLDecoder 和 URLEncoder ,
来处理 " %CC%E1%BD%BB " 字符串。如下:
TestURLEncoder.java
import java.net.*;
public class TestURLEncoder
{
public static void main(String[] args) throws Exception
{
//定义一个字符串,该字符串的值为上面的dd请求参数的值
String encodeStr = "%CC%E1%BD%BB";
//使用URLDecoder类来处理该dd请求参数值
String decodeStr = URLDecoder.decode(encodeStr ,"GBK");
System.out.println(decodeStr);
//定义一个字符串,该字符串的值为在图7.1中的文本框中输入的内容
String rawStr = "crazyit标志";
//使用URLEncoder类处理该字符串
System.out.println(URLEncoder.encode(rawStr , "GBK"));
}
}
大部分时候,程序中直接通过 HttpServletRequest 的 getParameter 方法即可获得正确的请求参数,而那些底层的二进制流处理,以及使用 URLDecoder 处理请求参数,都由 Web 服务器来替我们完成了
但是如果我们设置表单元素的 enctype 属性为 "multipart/form-data ",该请求将会把文件域里浏览到的文件内容作为请求参数发送。显然此时无法直接通过 request.getParameter() 方法来获取请求参数。
pro.jsp 页面将显示:
-----------------------------7cf87224d2020a
Content-Disposition: form-data; name= "email "
PhCollignon@email.com
-----------------------------7cf87224d2020a
Content-Disposition: form-data; name= "file"; filename= "c:\windows常用健.txt "
Content-Type: text/plain
Ctrl+L 打开“打开”面版(可以在当前页面打开Iternet地址或其他文件...)
Ctrl+N 新建一个空白窗口(可更改,Maxthon选项→标签→新建)
Ctrl+O 打开“打开”面版(可以在当前页面打开Iternet地址或其他文件...)
Ctrl+P 打开“打印”面板(可以打印网页,图片什么的...)
Ctrl+Q 打开“添加到过滤列表”面板(将当前页面地址发送到过滤列表)
Ctrl+R 刷新当前页面
Ctrl+S 打开“保存网页”面板(可以将当前页面所有内容保存下来)
Ctrl+T 垂直平铺所有窗口
Ctrl+V 粘贴当前剪贴板内的内容
Ctrl+W 关闭当前标签(窗口)
Ctrl+X 剪切当前选中内容(一般只用于文本操作)
Ctrl+Y 重做刚才动作(一般只用于文本操作)
Ctrl+Z 撤消刚才动作(一般只用于文本操作)
-----------------------------7cf87224d2020a
Content-Disposition: form-data; name= "Enter "
Submit Query
-----------------------------7cf87224d2020a--
2.手动上传
下面将直接通过底层的二进制流来取得上传文件内容,并将该文件内容放到 Web 应用所在路径。
由上面 “ pro.jsp 页面将显示: ” 可以发现 ,每个表单域的内容总以 “-----------------------------7cf87224d2020a ” 式样的字符串开始,后面的字符串可能变化,但前面的中画线总是不会变的。 对每个文件域而言,在第二行内容中总会包含 filename = "" 的字符串 。
下面是处理上传文件的 JSP :
uploadpro.jsp
<%@ page contentType="text/html;charset=GBK" %>
<%@ page import="java.io.*"%>
<%
//取得HttpServletRequest的InputStream输入流
InputStream is = request.getInputStream();
//以InputStream输入流为基础,建立一个BufferedReader对象
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String buffer = null;
//循环读取请求内容的每一行内容
while( (buffer = br.readLine()) != null)
{
//如果读到的内容以-----------------------------开始,
//且以--结束,表明已到请求内容尾
if(buffer.endsWith("--") && buffer
.startsWith("-----------------------------"))
{
//跳出循环
break;
}
//如果读到的内容以-----------------------------开始,表明开始了一个表单域
if(buffer.startsWith("-----------------------------"))
{
//如果下一行内容中有filename字符串,表明这是一个文件域
if (br.readLine().indexOf("filename") > 1)
{
//跳过两行,开始处理上传的文件内容
br.readLine();
br.readLine();
//以系统时间为文件名,创建一个新文件
File file = new File(request.getRealPath("/")
+ System.currentTimeMillis());
//创建一个文件输出流
PrintStream ps = new PrintStream(new FileOutputStream(file));
String content = null;
//接着开始读取文件内容
while( (content = br.readLine()) != null)
{
//如果读取的内容以-----------------------------开始,
//表明开始了下一个表单域内容
if(content.startsWith("-----------------------------"))
{
//跳出处理
break;
}
//将读到的内容输出到文件中
ps.println(content);
}
//关闭输出
ps.flush();
ps.close();
}
}
}
br.close();
%>
通过上面 JSP ,就可以把一个文件上传到 Web 应用的根路径下。值得注意的是,上面用的是 BufferedReader 字符流( 字符流在处理二进制文件时会出现问题 ),因此上面的上传处理只能处理文本文件的上传。
如果将上面采用字符流 处理文件的上传逻辑,改为以字节流 来处理文件上传,则上传处理将可以处理任何文件的上传
对于一个成熟的文件上传框架而言,它需要完成的逻辑非常简单: 通过分析 HttpServletRequest 的二进制流,解析出二进制流中所包含的全部表单域,分析出每个表单域的类型(是文件还是普通表单域),并允许开发者以简单的方式来取得文件域的内容字节,文件名和文件内容等信息,也可以取得其他表单域的值。
3.使用上传框架完成上传
对于 java 而言,比较常用的上传框架有 2 个 : Common-FileUpload 和 COS ,不管使用哪个,它都负责解析出 HttpServletRequest 请求中的所有域 —— 不管是文件域还是普通表单域。
【1】 Common-FileUpload
这个框架是 Apache 组织下 jakarta-commons 项目组下的一个小项目,该框架可以方便地将 multipart/form-data 类型请求中的各种表单域解析出来。该项目还依赖于另一个项目: Common-IO。
步骤:
① 登录 http://jakarta.apache.org/commons/fileupload/ 站点,下载 Common-FileUpload 项目最新发布版,下载后得到一个压缩文件,将下载的压缩文件打开,看到:
● lib : 该路径下存放的是 Common-FileUpload 项目的二进制类库和源文件等。
● site : 该路径下存放的是 Common-FileUpload 项目的各种文档,包括使用手册和 API 文档等。
● 注意和 LICENSE 等文件
② 将上面文件路径中的 lib\commons-fileupload-1.2.1.jar 文件复制到 Web 应用的 WEB-INF/lib 路径下
③ 登录 http://jakarta.apache.org/commons/io/ 站点,下载 Common-IO 项目的最新发布版本。打开下载文件,看到如下结构:
● docs:该路径下存放了该项目的文档文件,包括使用说明和 API 文档等
● commons-io-1.4.jar:该文件是该项目的二进制类库文件
● commons-io-1.4-javadoc.jar:该项目 API 文档压缩包
● commons-io-1.4-sources.jar:该项目的全部源文件压缩包
● 注意和 LICENSE 等文件
④ 将上面解压出来的 commons-io-1.4.jar 文件复制到 Web 应用的 WEB-INF/lib 路径下
经过上面 4 个步骤,即可在 Web 应用中使用该框架来完成文件上传了。该框架完成文件上传的关键类是 ServletFileUpload ,该类可以对 HttpServletRequest 请求进行分析,分析出该请求中的全部表单域
commonUpload.jsp
<%@ page contentType="text/html;charset=GBK" %>
<%@ page import="java.io.*,java.util.*"%>
<%@ page import="org.apache.commons.fileupload.disk.*" %>
<%@ page import="org.apache.commons.fileupload.*" %>
<%@ page import="org.apache.commons.fileupload.servlet.*" %>
<%
DiskFileItemFactory factory = new DiskFileItemFactory();
//设置上传工厂的限制
factory.setSizeThreshold(1024 * 1024 * 20);
factory.setRepository(new File(request.getRealPath("/")));
//创建一个上传文件的ServletFileUpload对象
ServletFileUpload upload = new ServletFileUpload(factory);
//设置上传文件的最大接受20M
upload.setSizeMax(1024 * 1024 * 20);
//处理HTTP请求,items是所有的表单项
List items = upload.parseRequest(request);
//遍历所有的表单项
for (Iterator it = items.iterator(); it.hasNext() ; )
{
FileItem item = (FileItem)it.next();
if (item.isFormField())
{
String name = item.getFieldName();
String value = item.getString("GBK");
out.println("表单域的name=value对为:"
+ name + "=" + value + "<br />");
}
else
{
//取得文件域的表单域名
String fieldName = item.getFieldName();
//取得文件名
String fileName = item.getName();
//取得文件类型
String contentType = item.getContentType();
//以当前时间来生成上传文件的文件名
FileOutputStream fos = new FileOutputStream(
request.getRealPath("/") + System.currentTimeMillis()
+ fileName.substring(fileName.lastIndexOf(".")
, fileName.length()));
//如果上传文件域对应文件的内容已经在内存中
if (item.isInMemory() )
{
fos.write(item.get());
}
//如果文件内容不完全在内存中
else
{
//获取上传文件内容的输入流
InputStream is = item.getInputStream();
byte[] buffer = new byte[1024];
int len;
//读取上传文件的内容,并将其写入服务器的文件中
while ((len = is.read(buffer)) > 0 )
{
fos.write(buffer , 0 , len);
}
is.close();
fos.close();
}
}
}
%>
不管是文件域还是普通表单域 Common-FileUpload 框架都把它们当成 FileItem 对象处理,如果该对象的 isFormField() 方法返回 true ,表明该表单域是一个普通表单域,否则将是一个文件域
FileItem 类包含了如下几个方法
● getFieldName():取得该表单域的 name 属性值
● getString(String encoding): 取得该表单域的 value 属性值,其中 encoding 参数设置该表单域的编码集
● getName():仅当该表单域是文件域,才有效,该方法返回上传文件的文件名
● getContentType():返回上传文件的文件类型
● get():返回上传文件的文件类型
● getInputStream():返回上传文件对应的输入流
通过上面的几个方法可以很轻易的完成文件上传
【2】 COS
这个框架是 Oreilly 组织下的一个小项目,该项目同样可以将 multipart/form-data 类型请求中的各个表单域解析出来。
为了在 Web 应用中使用 COS 项目,有如下步骤。
① 在 http://www.servlets.com/cos/ ,下载 COS 项目的最新版本。得到一个压缩文件,解压后:
● doc : 该项目的 API 文档
● lib : classes 路径下所有 class 文件打包后的文件
● src : 所有源文件
●注意LICENSE 等文件
② 将上面文件结构中的 lib/cos.jar 复制到 WEB-INF/lib 下 。
COS 实现文件上传更简单,它的核心类是 MultipartParser, 该类用于解析 HttpservletRequest 请求。
COS 用 Part 实例代表了所有的表单域,不管是普通表单域还是文件域。part类有2个子类:ParamPart 和 FilePart ,它们分别代表了 普通表单域和文件域
Part 类包含的常用方法:
● getName() : 获取表单域的 name 属性。
ParamPart 类包含的常用方法:
● getStringValue(String encoding) : 取得该表单域的 value 属性值,encoding 参数是表单域的编码集
FilePart 类包含的常用方法:
● getFileName(): 返回 上传文件的文件名
● getFilePath() : 返回上传文件的文件路径
● getContentType() : 返回上传文件的文件类型
cosUpload.jsp
<%@ page contentType="text/html;charset=GBK" %>
<%@ page import="java.io.*,java.util.*" %>
<%@ page import="com.oreilly.servlet.multipart.*" %>
<%@ page import="com.oreilly.servlet.*" %>
<%
//设置POST请求的内容最大字节为10M,该类用于解析HTTP请求
MultipartParser mp = new MultipartParser(request
, 10*1024*1024);
//所有表单域都是Part实例
Part part;
//遍历请求中的所有表单域
while ((part = mp.readNextPart()) != null)
{
//取得表单域的name属性值
String name = part.getName();
//对于普通表单域
if (part.isParam())
{
//取得普通表单域的值
ParamPart paramPart = (ParamPart) part;
String value = paramPart.getStringValue("GBK");
out.print("普通表单域部分: <br />name="
+ name + ";value=" + value + "<br />");
}
//对于文件域
else if(part.isFile())
{
//取得文件上传域
FilePart filePart = (FilePart) part;
String fileName = filePart.getFileName();
if (fileName != null)
{
//输出文件内容
long size = filePart.writeTo(new File(
request.getRealPath("/")));
out.println("上传文件:<br /> 文件域的名="
+ name + ";文件名=" + fileName
+ "<br />" + "上传文件的内容="
+ filePart.getFilePath() + "<br />"
+ "文件内容类型=" + filePart.getContentType()
+ "<br />" + "文件大小=" + size + "<br />");
}
//文件名为空
else
{
//该文件域没有输入文件名
out.println("名为" + name + "的文件不存在!");
}
out.flush();
}
}
%>
注意: 如果使用 COS 来上传,文件名包含中文字符,会出现异常,可能是 COS 本身的问题。对于中文开放环境,推荐使用 Common-FileUpload 来上传文件 。