文件上传
DiskFileUpload
DiskFileUpload已过时,使用1.2版本中的ServletFileUpload 与 DiskFileItemFactory 两个类来替代它。虽然过时,但原理是一样的。
setSizeMax(long sizeMax)
设置请求体内容的最大允许大小,如果超过则抛出FileUploadIOException。默认值为-1,表示没有限制。
setSizeThreshold(int sizeThreshold)
上传组件在解析和处理上传消息中的每个字段内容时,需要临时保存解析出的数据。因为Java虚拟机默认可以使用的内存空间是有限的(笔者测试不大100MB),超出限制时将会发生“java.lang.OutOfMemoryError”错误,如果上传的文件很大,例如上传800MB的文件,在内存中将无法保存该文件内容,组件将用临时文件来保存这些数据;但如果上传的文件很小,例如上传600个字节的文件,显然将其直接保存在内存中更加有效。此方法就是用来设置这个阀值的,超过了则会保存在临时文件里。
setRepositoryPath(String repositoryPath)
用于设置setSizeThreshold方法中提到的临时文件的存放目录,这里要使用绝对路径。如果不设置存放路径,那么临时文件将被存储在“java.io.tmpdir”这个JVM环境属性所指定的目录中。Tomcat设置成了 tomcat安装目录下的tmp目录。
parseRequest
parseRequest(javax.servlet.http.HttpServletRequest req)
parseRequest(javax.servlet.http.HttpServletRequest req, int sizeThreshold, long sizeMax, java.lang.String path)
它是对HTTP请求消息进行解析的入口方法,如果请求消息中的实休内容的类型不是“multipart/form-data”,该方法将抛出FileUploadException异常。parseRequest方法解析出FORM表单中每个字段的数据,并将它们分别包装成独立的FileItem对象,然后将这些FileItem对象加入到一个List类型的集合对象中返回。
第二个方法的后三个参数等同于前面三个方法设置的。
isMultipartContent(HttpServletRequest req)
Content-Type: multipart/form-data; boundary=---------------------------7d9165750396
判断请求中的内容是否是multipart/form-data 类型。
setHeaderEncoding(String encoding)
用于指定将各个“表单字段的描述头内容”转换成字符串时所使用的字符集编码。如果没有指定,则使用reuqest. setCharacterEncoding中的编码,如果request也没有设置,则使用系统本地编码。
“表单字段的描述头内容”是上传请求消息中如下部分内容:
-----------------------------7d9165750396
Content-Disposition: form-data; name="textParam1"
中a ~!@#$%^&*()_+{}|:\" <>?`-=[]\\;',./
-----------------------------7d9165750396
Content-Disposition: form-data; name="fileParam1"; filename="file1.txt"
Content-Type: text/plain
这是第一个测试文件的内容:
1111111111111
aaaaaaaaaaaaa
因为文件名有可能是中文,所以需要设置读取这些内容所使用的编码。
FileItem
该类用来封装单个表单字段元素的数据,一个表单字段元素对应一个FileItem对象,而不管该表单对象是普通的表单元素还是文件类型元素。通过调用FileItem对象的方法可以获得相关表单字段元素的数据。
以“multipart/form-data”类型提交表单时,表单里的元素所对应的消息形式如下:
-----------------------------7d9165750396
Content-Disposition: form-data; name="textParam1"
中a ~!@#$%^&*()_+{}|:\" <>?`-=[]\\;',./
-----------------------------7d9165750396
Content-Disposition: form-data; name="fileParam1"; filename="file1.txt"
Content-Type: text/plain
这是第一个测试文件的内容:
1111111111111
aaaaaaaaaaaaa
-----------------------------7d9165750396
可以看出每个元素都使用分隔线分开,每部分都是由两部分组成:一个是“表单字段的描述头”,一个是“字段所对应的正文本”。每个分区就封装成一个FileItem对象。
FileItem类中有一个指向表单元素正文部分的流对象,当主体内容大小小于sizeThreshold值时,这个流对象关联到一片内存,主体内容将会被保存在内存中。当主体内容超过sizeThreshold值时,这个流对象关联到硬盘上的一个临时文件,主体内容将被保存到该临时文件中,临时文件名形如“upload_00000001.tmp”,组件会维护这个临时文件,比如读取后它会删除这个临时文件。
isFormField
判断FileItem类对象封装的数据是属于一个普通表单字段还是属于一个文件表单字段,如果是普通表单字段则返回true。
getName
获得文件上传字段中的原始文件名。如果是普通表单字段,则返回null。
如果是IE,则为上传文件的完整路径,如果是其他浏览器,大部分只有文件名。
getFieldName
返回表单字段元素的name属性值,也就是消息头中各个描述头部分中的name属性的值,包括普通字段与文件类型字段元素。
write(File file)
将FileItem对象中保存的主体内容保存到某个指定的文件中。如果主体内容是保存在某个临时文件中,该方法顺利完成后,临时文件有可能会被清除。该方法也可将普通表单字段内容写入到一个文件中,但它的主要用途是将上传的文件内容保存在本地文件系统中。
getOutputStream
InputStream getInputStream()
返回一个用于读取FileItem主体内容的输入字节流。
getString
String getString()
String getString(String charset)
用于将FileItem对象中保存的主体内容作为一个字符串返回,
getContentType
返回上传文件的类型:
-----------------------------7d9165750396
Content-Disposition: form-data; name="fileParam1"; filename="file1.txt"
Content-Type: text/plain
这是第一个测试文件的内容:
1111111111111
aaaaaaaaaaaaa
-----------------------------7d9165750396
即“Content-Type”头的值。如果是普通字段,则返回null。
isInMemory
判断FileItem类对象封装的主体内容是存储在内存中,还是存储在临时文件中,如果存储在内存中则返回true,否则返回false。
delete
delete方法用来清空FileItem类对象中存放的主体内容,如果主体内容被保存在临时文件中,delete方法将删除该临时文件。尽管Apache组件使用了多种方式来尽量及时清理临时文件,但系统出现异常时,仍有可能造成有的临时文件被永久保存在了硬盘中。在有些情况下,可以调用这个方法来及时删除临时文件。
FileUploadException
除了这个异常外,还定义了4个异常类,它是它们的父类。在使用时我们只需对FileUploadException进行捕获即可。
1.2版上传示例
DiskFileItemFactory dif = new DiskFileItemFactory();
// 超过1M的字段数据采用临时文件缓存
dif.setSizeThreshold(1024 * 1024);
// 采用默认的临时文件存储位置
dif.setRepository(new File(getServletContext().getRealPath("/upload")));
// 使用ServletFileUpload 与 DiskFileItemFactory替换DiskFileUpload
ServletFileUpload sfu = new ServletFileUpload(dif);
// 最多上传200M数据
sfu.setSizeMax(1024 * 1024 * 200);
// 设置上传的普通字段的名称和文件字段的文件名所采用的字符集编码
sfu.setHeaderEncoding("gb2312");
System.out.println("sssssssss");
// 得到所有表单字段对象的集合
List fileItems = null;
try {
fileItems = sfu.parseRequest(request);
} catch (FileUploadException e) {
e.printStackTrace();
return;
}
// 处理每个表单字段
Iterator i = fileItems.iterator();
Map paramMap = new HashMap();
while (i.hasNext()) {
FileItem fi = (FileItem) i.next();
if (fi.isFormField()) {//如果是普通字段
String content = fi.getString("GB2312");
String fieldName = fi.getFieldName();
paramMap.put(fieldName, content);
} else {//否则为文件
try {
String pathSrc = fi.getName();
/*
* 如果用户没有在FORM表单的文件字段中选择任何文
* 件,那么忽略对该字段项的处理
*/
if (pathSrc.trim().equals("")) {
continue;
}
int start = pathSrc.lastIndexOf('\\');
String fileName = pathSrc.substring(start + 1);
File pathDest = new File(uploadDir, fileName);
fi.write(pathDest);
String fieldName = fi.getFieldName();
request.setAttribute(fieldName, fileName);
} catch (Exception e) {
e.printStackTrace();
return;
} finally// 总是立即删除保存表单字段内容的临时文件
{
fi.delete();
}
}
}
Filter过滤器
在Servlet API中定义了三个接口类来供开发人员缩写Filter程序:javax.servlet.Filter、javax.servlet.FilterChain和javaservlet.FilterConfig。
Filter链中的各个Filter的拦截顺序与它们在应用程序的web.xml文件中的映射先后顺序一致,上一个Filter的doFilter方法调用FilterChain.doFilter方法将激活下一个Filter的doFilter方法,最后一个Filter的doFilter方法中调用的FilterChain.doFilter方法将激活Servlet的service方法:
Filter会在容器启动的过程中创建好。
Filter接口
init(FilterConfig filterConfig)
在Web应用程序启动时,服务器将根据其web.xnl文件的配置信息来创建每个注册的Filter实例对象,并将其保存在内存中。容器创建Filter的实例对象后,将立即调用该Filter对象的init方法。init方法在Filter的生命期中仅执行一次,Web容器在调用init方法时,会传递一个包含Filter的配置和运行环境信息的FilterConfig对
象。
可以在init方法中完成与构造方法类似的初始化功能,如果初始化代码中要使用到FilterConfig对象,那么,这些初始化代码就只能在Filter的init方法中编写,而不能在构造方法中编写(因为构造函数在这个方法前执行,那时还没有初始化FilterConfig对象)。Filter的init方法与Servlet的init的作用非常相似。
如果init方法抛出了ServletException或者init方法中的初始化过程超过了Web容器规定的时间,Web容器将销毁Filter对象。
doFilter
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
参数request 和response为Web容器或Filter链中的上一个Filter传递过来的请求和响应对象,参数chain代表当前Filter链的对象,在当前Filter对象的doFilter方法内部需要调用FiltetChain对象的doFilter方法,才能把请求交付给Filter链中的下一个Filter或者目标Servlet程序去处理。只有Filter对象的init方法执行成功后,Filter对象才会被加入到Filter链中,该Filter对象的doFilter方法才会被调用。
destroy
与init一样,只执行一次。一般用于释放资源。
FilterChain接口
doFilter(ServletRequest request, ServletResponse response)
用于通知Web容器把请求交给Filter链中的下一个Filter去处理,如果当前调用此方法的Filter对象是Filter链中的最后一个Filter,那么将把请求交给目标Servlet去处理。
FilterConfig接口
<filter>
<filter-name>FirstFilter</filter-name>
<filter-class>FirstFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>GB2312</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>FirstFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
publicclass FirstFilter implements Filter {
private FilterConfig filterConfig = null;
publicvoid init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
publicvoid doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
publicvoid destroy() {
this.filterConfig = null;
}
}
与普通的Serviet程序一样,Filter程序也很可能需要访问Servlet容器,Servlet规范将代表ServletContext对象和Filter的配置参数信息都封装到一个称为FilterConfig的对象中,FilterConfig接口则用于定义FilterConfig对象应该对外提供的方法,以便在Filter程序中可以调用这些方法来获取ServletContext对象,以及获取在web.xml文件中为Filter设置的友好名称和初始化参数。
Web容器装载并创建一个Filter的实例对象后,接着调用该实例对象的init(FilterConfig config)方法,将FilterConfig对象通过这个方法传递给该Filter对象。在Filter实现类的init方法中可以将作为参数传递进来的FilterConfig对象赋值给一个成员变量,以后在Filter实现类的doFilter和destroy方法中就可以通过这个成员变量来调用FilterConfig对象的各个方法。
getFilterName
getFilterName方法用于返回在web.xml文件中为Filter所设置的友好名称,也就是返回<filter-name>元素的设置值。
getServletContext
getServletContext方法用于返回内Filterconfig对象中所包装的ServletContext对象的引用。
getInitParameter
getInitParameter方法用于返回在web.xml文件中为Filter所设置的某个名称的初始化参数值,如果指定名称的初始化参数不存在,则返回值为null。
getInitParameterNames
getInitParameterNames方法用于返回一个Enumeration集合对象,该集合对象中包含在web.xml文件中为当前Filter设置的所有初始化参效的名称。
Filter映射
<filter-mapping>有四个子元素<filter-name>、<url-pattern>、<servlet-name>、<dispatcher>。其中<dispatcher>表示Filter所拦截的资源被Servlet容器调用的方式。Servlet容器调用一个资源的方式有以下4种:
l 通过正常的访问请求调用
l 通过RequestDispatcher.include方法调用
l 通过RequestDispatcher.forward方法调用
l 作为错误响应资源调用
它们对应<dispatcher>元素的4个取值:REQUEST、INCLUDE、FORWARD、ERROR
一个Filter拦截的资源可以通过两种方式来指定:
第一种:
<filter-mapping>
<filter-name>FirstFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
第二种:
<filter-mapping>
<filter-name>FirstFilter</filter-name>
<servlet-name>default</servlet-name>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<servlet-name>元素与<url-pattern>元素是二选一的关系,<servlet-name>元素是某个Servlet在web.xml文件中的注册名称,上面是缺省Servlet的名字。
如果<filter-mapping>元素中没有设置任何<dispatcher>子元素,那么,Filter只对通过正常的访问请求方式调用的资源进行拦截(比如上面的第一种方式就是默认的)。
<filter-mapping>可以设置多个<dispatcher>,用来指定Filter对资源的多种调用方式都进行拦截。
在同一个web.xml文件中可以为同一个Filter设置多个映射,这与配置Servlet是一样的。
如果在web.xml文件中设置的多个<filter-mapping>元素都可以对某一资源调用过程进行拦截,那么这些<filter-mapping>元素中的Filter将形成一个针对该资源的调用过程的Filter链。
经过实验,容器按照<filter-mapping>配置的顺序而不是<filter>声明的顺序来构造Filter链。
如果为同一个Filter设置多个<filter-mapping>元素都可以对某一资源的调用过程进行拦截,那么所形成的Filter链中就会多次出现这个Filter,这就意味着这个Filter的doFilter方法要被执行多次。
在调用FilterChain.doFilter方法的语句前后向out对象输出的数据会在被拦截资源的内容前后显示出来:
publicvoid doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
out.println("会在被拦截资源前面显示");
chain.doFilter(request, response);
out.println("会在被拦截资源后面追加显示");
}
如果注释掉上面的chain.doFilter方法调用,则不会再显示被拦截资源的内容,而是直接显示向out对象里输出的两条语句。
在Filter程序中修改请求
例如,浏览器访问它已经缓存了的HTML页面时,它将发送If-Modified-Since请求头询问服务器是否可以使用缓存,可以使用Filter程序来屏蔽掉这个If-Modified-Since请求头字段,让request对象的getHeader等方法不要返回这个头字段的信息,从而让默认Servlet程序永远都不要给浏览器回送304(Not Modified)状态码。但是,有什么办法可以修改request对象中的信息呢?查看HttpServletRequest接口的帮助文档,看到的尽是getHeader和getParamater之类的getXxx方法,其中并没有定义对应的setHeader和setParamater之类的setXxx方法(SetCharacterEncoding是一个特殊情况),显然无法直接修改request对象中封装的请求信息。
实现自定义的请求对象.假设要对目标Servlet屏蔽掉If-Modified-Since请求头字段,那么需要在Filter程序中创建一个HttpServletRequestWrapper的子类的实例对象,并在HttpServletRequestWrapper的子类中对getHeader、getHeaders和getHeaderNames等方法进行重写。另外,由于构造方法不具有继承性,HttpServletRequestWrapper的子类还需要定义有接受原始request对象作为参数的构造方法。HttpServletRequestWrapper的子类的编写细节如下:
.在HttpServletRequestWrapper的子类中定义一个接受HttpServletRequest类型的参数的构造方法,在该构造方法内部调用HttpServletRequestWrapper类的构造方法。
.让getHeader和getHeaders方法对“If-Modified-Since”请求头字段返回null,而对其他头字段则按HttpServletRequestWrapper类的原有方式进行处理(即分别调用super.getHeader和super.getHeaders方法)。
.让getHeaderNames方法返回的Enumeration对象中不要包含“If-Modified-Since”这个字段名。
用Filter实现文件上传请求的透明处理
POST请求方式提交的“multipart/form-data”类型的HTTP请求消息,在Servlet程序中无法调用HttpServletRequest实例对象的getParameter等方法来读取表单字段元素的信息。对于这种情况,可以通过一Filter程序进行预处理,在Filter程序中创建一个自定义的请求对象传递给目标Servlet,让自定义的请求对象的getParameter等方法可以返回“multipart/form-data”编码类型的HTTP请求消息中的表单字段元素信息,从而让目标Servlet能够透明地使用request对象的getParameter方法。
要实现这样一个包装“multipart/form-data”类型的请求消息的 Filter程序,主要是需要编写好request包装类,这个request包装类的具体要求如下:在其构造方法中除了调用父类的构造方法对原始的请求对象进行包装外,它还要调用Apache文件上传组件来读取“multipart/form-data”类型的FORM表单数据,并将读取到的所有普通表单字段元素的名称和值存储到一个HashMap对象中,将所有上传了文件的文件字段的名称和对应的
FileItem对象存储进另外一个HashMap对象中,以便目标Servlet程序调用request。getParameter等方法来读取普通表单字段的数据并调用该Request包装类专有的方法来读取上传的文件信息。为了与HttpServietRequest接口的规范保持一致,该Request包装类中不仅需要重写getParameter方法,还需要重写与参数读取相关的其他三个方法:getParameterNames, getParameterValues和getParameterMap。由于HttpServletRequest接口中没有定义读取上传文件信息的方法,该Request包装类中还需要定义两个额外的方法:getFileItemNames和getFileItem,分别用来返回所有上传的文件的文件字段的名称和某个文件字段上传的文件信息。
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.fileupload.DiskFileUpload;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
publicclass MultipartRequest extends HttpServletRequestWrapper {
HashMap parameters = new HashMap();
HashMap files = new HashMap();
public MultipartRequest(HttpServletRequest request)
throws FileUploadException {
super(request);
DiskFileUpload fu = new DiskFileUpload();
// 最多上传200M数据
fu.setSizeMax(1024 * 1024 * 200);
// 超过1M的字段数据采用临时文件缓存
fu.setSizeThreshold(1024 * 1024);
// 采用默认的临时文件存储位置
// fu.setRepositoryPath(...);
// 设置上传的普通字段内容和文件字段的文件名所采用的字符集编码
fu.setHeaderEncoding("gb2312");
// 得到所有表单字段对象的集合
List fileItems = null;
// 如果解析数据时出现问题,直接将FileUploadException外抛
fileItems = fu.parseRequest(request);
// 处理每个表单字段
Iterator i = fileItems.iterator();
while (i.hasNext()) {
FileItem fi = (FileItem) i.next();
if (fi.isFormField()) {
String fieldName = fi.getFieldName();
String content = null;
try {
content = fi.getString("GB2312");
} catch (Exception e) {
}
setParameter(fieldName, content);
} else {
String pathSrc = fi.getName();
/*
* 如果用户没有在FORM表单的文件字段中选择任何文件,那么忽略对该字段项的处理
*/
if (pathSrc.trim().equals("")) {
continue;
}
String fieldName = fi.getFieldName();
files.put(fieldName, fi);
}
}
}
/**
* 向集合中添加一个参数,主要是考虑多个同名字段的情况。
*/
privatevoid setParameter(String name, String value) {
String[] mValue = (String[]) parameters.get(name);
if (mValue == null) {
mValue = new String[0];
}
String[] newValue = new String[mValue.length + 1];
System.arraycopy(mValue, 0, newValue, 0, mValue.length);
newValue[mValue.length] = value;
parameters.put(name, newValue);
}
/**
* 返回某个名称的参数值,如果一个名称对应有多个值,那么只返回其中的第一个。
*/
public String getParameter(String name) {
String[] mValue = (String[]) parameters.get(name);
if ((mValue != null) && (mValue.length > 0)) {
return mValue[0];
}
returnnull;
}
/**
* 返回所有参数名称的集合
*/
public Enumeration /* String */getParameterNames() {
Collection c = parameters.keySet();
return Collections.enumeration(c);
}
/**
* 返回某个名称所对应的所有参数值
*/
public String[] getParameterValues(String name) {
String[] mValue = (String[]) parameters.get(name);
return mValue;
}
/**
* 返回包含所有参数名和对应的参数值的Map集合
*/
public Map getParameterMap() {
returnparameters;
}
/**
* 返回某个表单字段名所对应的FileItem对象
*/
public FileItem getFileItem(String name) {
FileItem fItem = (FileItem) files.get(name);
return fItem;
}
/**
* 返回所有上传了文件的文件字段的名称的集合
*/
public Enumeration /* String */getFileItemNames() {
Collection c = files.keySet();
return Collections.enumeration(c);
}
}
Filter:
publicvoid doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String type = req.getHeader("Content-Type");
// 对非"multipart/form-data"类型的请求消息不进行过滤处理
if (type == null || !type.startsWith("multipart/form-data")) {
chain.doFilter(request, response);
} else {
MultipartRequest mreq = null;
try {
mreq = new MultipartRequest(req);
} catch (Exception e) {
thrownew ServletException(e);
}
chain.doFilter(mreq, response);
}
}