时间:2016-12-17 18:07
——文件上传概述
上传不能使用BaseServlet
1、文件上传的作用
例如网络硬盘,就是用来上传和下载文件的。
2、文件上传对表单的限制
1)必须使用表单,而不能是超链接
2)表单的method必须是POST,而不能是GET
3)表单的enctype必须是multipart/form-data
4)在表单中添加type="file",即<input type="file" name="" />
<form action="xxx" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username" /><br/>
照 片:<input type="file" name="photo" /><br/>
<input type="submit" value="上传" />
</form>
3、文件上传对Servlet的限制
1)当表单为enctype="multipart/form-data"时,不能使用request.getParameter()方法,该方法永远返回null。
2)使用ServletInputStream request.getInputStream()来获取完整的请求体。
4、多部件表单结构
1)分割出多个部件,即一个表单项生成一个部件。
2)一个部件中会自己包含请求头和空行以及请求体。
3)普通表单项:
> 一个头:
* Content-Disposition,包含name="xxx",即表单项名称。
> 体就是表单项的值。
4)文件表单项:
> 两个头:
* Content-Disposition,包含name="xxx",即表单项名称,还有一个filename="xxx",表示上传文件的名称。
* Content-Type:它是上传文件的MIME类型,例如:image/jpeg,表示上传的是图片。
> 体就是上传文件的内容。
——commons-fileupload
* commins-fileupload.jar
* 依赖包:commons-io.jar
这个组件会解析request中的上传数据,解析后的结果是一个表单项数据封装到一个FileItem对象中,只需要调用FIleItem对象的方法即可。(多个表单项会封装多个FileItem,即List<FileItem>)
1、上传三个步骤
相关类:
* 工厂类:DiskFileItemFactory
* 解析器:ServletFileUpload
* 表单项:FileItem(该类对应一个表单项)
1)创建工厂:
DiskFileItemFactory factory = new DiskFileItemFactory();
2)创建解析器:
ServletFileUpload sfu = new ServletFileUpload(factory);
3)使用解析器来解析request,得到FileItem集合:
List<FileItem> fileItemList = sfu.parseRequest(request);
2、FileItem类
普通表单项相关方法:
* boolean isFormField()
是否为普通表单项。
true为普通表单项。
false为文件表单项。
* String getFieldName()
返回当前表单项的名称。
* String getString(String charset)
通过指定编码获取表单项的值。
该方法不适用于文件表单项。
必须指定编码。
文件表单项相关方法:
* String getName()
返回上传的文件名称。
* long getSize()
返回上传文件的字节数。
* String getContentType()
获取MIME类型。
* InputStream getInputStream()
返回上传文件对应的输入流。
* void write(File file)
将上传的文件写入指定文件中。
如果file已存在,则会替换目标文件,如果file不存在,则会创建该文件。
——文件上传简单示例
===============================================================================
com.wyc.servlet.UploadServlet
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
public class Upload2Servlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
/*
* 上传三步
* 1、得到工厂
* 2、通过工厂创建解析器
* 3、解析request,得到FileItem集合
* 4、遍历FileItem集合,遍历其API完成文件的保存
*/
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload sfu = new ServletFileUpload(factory);
try {
List<FileItem> fileItemList = sfu.parseRequest(request);
for(FileItem f : fileItemList){
if(f.isFormField()){
System.out.println("普通表单项:" + f.getFieldName() + "=" + f.getString("utf-8"));
} else {
System.out.println("文件表单项:");
// 获取文件MIME类型
String mime = f.getContentType();
System.out.println("Content-Type:" + mime);
// 获取文件大小(字节数)
long size = f.getSize();
System.out.println("文件大小:" + size);
// 获取文件名
String fileName = f.getName();
System.out.println("文件名称:" + fileName);
// 保存文件
String filePath = request.getServletContext().getRealPath("") + "/file/" + fileName;
File destFile = new File(filePath);
f.write(destFile);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
------------------------------------------------------------------------------------------------------------------------------
form.jsp
<body>
<h1>上传</h1>
<form action="<c:url value='/Upload2Servlet'/>" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username" value="张三" /><br/>
照 片:<input type="file" name="photo" /><br/>
<input type="submit" value="上传" />
</form>
</body>
===============================================================================
——文件上传细节
1、把上传的文件放到WEB-INF目录下
如果没有把用户上传的文件放到WEB-INF目录下,那么用户就可以通过浏览器直接访问上传的文件,这是非常危险的。
假如说用户上传了一个JSP文件,然后用户再通过浏览器去访问这个JSP文件,那么服务器就会执行JSP文件中的内容,如果在JSP文件中有如下语句:Runtime.getRuntime().exec("shutdown -s -t 1");.........
通常我们会在WEB-INF目录下创建一个upload目录来存放上传的文件,而在Servlet中找到这个目录需要使用ServletContext()的getRealPath(String)方法,例如在项目中有如下语句:
ServletContext servletContext = this.getServletContext();
String uploadPath = servletContext.getRealPath("/WEB-INF/uploads");
其中uploadPath为:G:\tomcat\webapps\FileDemo\WEB-INF\uploads
2、文件名称(完整路径)
上传的文件名称可能是完整路径。
IE6获取的上传文件名称是完整路径,而在其他浏览器获取的上传文件名称只是文件名称而已,浏览器差异的问题还需要处理一下。
例如:C:\file\哈哈.jpg
可以通过分割字符串来获取文件名。
int index = fileName.lastIndexOf("\\");
if(index != -1)
fileName = fileName.substring(index+1);
3、处理文件名称乱码和普通表单项乱码
当上传的文件名称中包含中文时,需要设置编码,commons-fileupload组件为我们提供了两种设置编码的方式:
request.setCharacterEncoding("utf-8");
fileUpload.setHeaderEncoding(String charset); // 这种方式的优先级高于前一种。
commons-fileupload内部会调用request.getCharacterEncoding();来指定编码。
上传文件的文件内容中包含中文:
通常我们不需要关心上传文件的内容,因为我们会把上传文件保存到硬盘上,也就是说,原文件是什么样子,保存到服务器之后还是什么样子。
如果想要在控制台显示上传文件的内容,那么可以使用fileItem.getString(String encoding)来使用指定编码处理字符串。
文本内容和普通表单项内容都是用FileItem类的getString(String encoding)来处理编码。
4、文件重名问题
应该为每个文件名添加前缀名称,这个前缀要保证不能重复,可以使用UUID。
fileName = CommonUtils.getUuid() + "_" + fileName;
===============================================================================
5、一个目录不能存放过多文件(存放目录打散)
一个目录下不应该存放过多的文件,一般一个目录存放1000个文件就是上限了,如果再多,那么打开目录时就会很“卡”。
也就是说,我们需要把上传的文件存放到不同的目录中,但是也不能为每个上传的文件都新建一个目录,这样会导致目录过多,所以我们应该采用某种算法来“打散”存放文件的目录。
方法有很多,例如使用日期来打散,每天生成一个目录,也可以使用文件名的首字母来生成目录,相同首字母的文件放到同一目录下。
日期打散算法:如果某一天上传的文件过多,那么也会出现一个目录文件过多的情况。
首字母打散算法:
例如abc.txt,那么可以把文件保存到a目录下,如果a目录不存在,则创建a目录。
如果文件名是中文的,因为中文过多,所以会导致目录过多的情况。
哈希打散算法:
* 通过文件名称得到一个int值,即调用hashCode()获取哈希值。
* 将int值转换成16进制。
* 获取16进制的前两位用来生成目录,目录为两层:例如1B2C3D4E5F,使用/1/B/ 来保存文件。
该存储方式非常不直观,一般会将文件存放的目录保存起来,以后访问下载时可以直接获取。
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import com.wyc.bean.CommonUtils;
public class Upload3Servlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
/*
* 上传三步
*/
// 得到工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
// 得到解析器
ServletFileUpload fileUpload = new ServletFileUpload(factory);
// 解析request
try {
List<FileItem> fileItemList = fileUpload.parseRequest(request);
// 判断表单项
for(FileItem f : fileItemList){
if(f.isFormField()){
System.out.println(f.getFieldName() + "=" + f.getSize());
} else {
/*
* 1、得到文件保存的根路径,在此路径下创建多级目录
*/
String realPath = this.getServletContext().getRealPath("/WEB-INF/files");
/*
* 2、生成二级目录
* 1)得到文件名称
* 2)得到hashCode
* 3)转成16进制
* 4)获取前两个字符用来生成目录
*/
/*
* 获取文件名称,并处理文件名的绝对路径问题
*/
String fileName = f.getName();
int index = fileName.lastIndexOf("\\");
if(index != -1){
fileName = fileName.substring(index + 1);
}
/*
* 处理文件重名问题,给文件名称添加UUID前缀
*/
String saveName = CommonUtils.getUuid() + "_" + fileName;
/*
* 得到hashCode,并转换成16进制
*/
int hashCode = fileName.hashCode();
String hexCode = Integer.toHexString(hashCode);
/*
* 获取hexCode的前两个字符,与根路径连接在一起,生成一个完整的路径
* 将此抽象路径传递给File类的构造方法
*/
File dirFile = new File(realPath, hexCode.charAt(0) + "/" + hexCode.charAt(1));
/*
* 创建目录
* mkdirs():创建此抽象路径名指定的目录,包括所有必需但不存在的父目录。
*/
dirFile.mkdirs();
/*
* 创建目标文件
* 根据parent抽象路径名和child路径名字符串创建一个新 File 实例。
*/
File destFile = new File(dirFile, saveName);
/*
* 写入硬盘(保存)
*/
f.write(destFile);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
===============================================================================
6、上传的单个文件、整个表单的大小限制
限制上传文件的大小很简单,ServletFileUpload类的setFileSizeMax(long)就可以实现了,参数就是上传文件的上限字节数,例如:servletFileUpload.setFileSizeMax(1024 * 10),表示上限为10kb。
该方法一定要在parseRequest()方法之前调用。
一旦上传的文件大小超出了上限,那么就会在执行parseRequest()方法时抛出FileUploadBase.FileSizeLimitExceededException异常。
我们可以在Servlet中获取这个异常,然后想页面输出“上传的文件大小超出限制”。
限制整个表单的大小:
fileUpload.setSizeMax(long size);
该方法必须在parseRequest()方法之前调用。
如果大小超出限制,会抛出FileUploadBase.SizeLimitExceededException异常。
public class Upload4Servlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload fileUpload = new ServletFileUpload(factory);
// 限制单个文件大小为10M
fileUpload.setFileSizeMax(1024 * 10000);
// 限制整个表单大小不能超过100M
fileUpload.setSizeMax(1024 * 100000);
List<FileItem> fileItemList;
try {
fileItemList = fileUpload.parseRequest(request);
FileItem f = fileItemList.get(1);
} catch (FileUploadException e) {
if (e instanceof FileUploadBase.FileSizeLimitExceededException)
throw new RuntimeException("上传单个文件大小超出限制");
else if (e instanceof FileUploadBase.SizeLimitExceededException)
throw new RuntimeException("表单大小超出限制");
}
}
}
===============================================================================
7、设置缓存大小与临时目录
* 缓存大小:
默认为10kb,当上传文件超出缓存大小时,先将文件保存到硬盘,当文件上传完毕后,再将文件保存。
* 临时目录:
缓存的保存目录。
* 可以通过DiskFileItemFactory的构造方法来指定缓存大小和临时目录
DiskFileItemFactory(int sizeThreshold, File repository)
> 默认的临时目录为:System.getProperty("java.io.tmpdir");,打印后:G:\apache-tomcat-7.0.72\temp
> 例如:new DiskFileItemFactory(1024 * 10, new File("F:/temp"));
public class Upload5Servlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
// 打印默认临时目录
System.out.println(System.getProperty("java.io.tmpdir"));
// 设置缓存大小与临时目录
DiskFileItemFactory factory = new DiskFileItemFactory(1024 * 10, new File("F:/temp"));
ServletFileUpload fileUpload = new ServletFileUpload(factory);
try {
List<FileItem> fileItemList = fileUpload.parseRequest(request);
FileItem f = fileItemList.get(1);
File file = new File("F:/temp/" + f.getName());
f.write(file);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
===============================================================================
——文件下载
被下载的资源必须放到WEB-INF目录下,然后通过Servlet完成下载。
在JSP页面中给出超链接,链接到DownloadServlet,并提供要下载的文件名称,然后在DownloadServlet获取文件的真实路径,然后把文件写入到response.getOutputStream()流中。
1、概述
下载就是向客户端响应字节数据。
之前响应的都是HTML的字符数据。
其实就是把一个文件编程字节数组,使用response.getOutputStream()来将文件响应给浏览器。
2、下载的要求
两个头、一个流
* Content-Type:表示传递给客户端文件的MIME类型。
> 通过文件名称调用ServletContext的getMimeType()方法得到MIME类型。
* Content-Disposition:
> 它的默认值为inline,表示在浏览器窗口中打开(当打不开时会弹框)。
> attachment;filename=xxx:表示类型为附件,并在弹框的界面中显示下载的文件名。
* 流:要下载的文件数据。
> 自己new 一个输入流即可,然后通过IOUtils完成流的数据写入。
——下载示例
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
public class DownloadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
/*
* 准备两个头,一个流
* 1、Content-Type
* 2、Content-Disposition
* 3、流:下载文件的数据
*/
/*
* 如何获取文件的MIME类型
*/
String fileName = "F:/Acacia - Valder Fields.mp3";
// 通过文件名获取文件MIME类型
String contentType = this.getServletContext().getMimeType(fileName);
String contentDisposition = "attachment;filename=Acacia - Valder Fields.mp3";
// 设置响应头
response.setHeader("Content-Type", contentType);
response.setHeader("Content-Disposition", contentDisposition);
// 获取绑定了客户端的响应流
ServletOutputStream output = response.getOutputStream();
// 获取要下载的文件
FileInputStream input = new FileInputStream(new File("F:/Acacia - Valder Fields.mp3"));
// 将输入流中的数据复制到输出流中
IOUtils.copy(input, output);
input.close();
}
}
——DownloadUtils(下载文件名乱码问题)
显示在下载框中包含中文名称时,会出现乱码。
* FireFox,使用Base64编码。
* 其他大部分浏览器,使用URL编码。
通用方法:filename = new String(filename.getBytes("gbk"), "iso-8859-1");
浏览器能读懂ISO-8859-1编码
public class DownloadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
/*
* 准备两个头,一个流
* 1、Content-Type
* 2、Content-Disposition
* 3、流:下载文件的数据
*/
/*
* 如何获取文件的MIME类型
*/
String fileName = "F:/哈哈.mp3";
// 解决乱码问题
// String frameName = new String(fileName.substring(fileName.lastIndexOf("/") + 1).getBytes("GBK"), "ISO-8859-1");
// 使用DownloadUtils解决乱码问题
String frameName;
try {
frameName = fileNameEncoding("哈哈.mp3", request);
} catch (Exception e) {
throw new RuntimeException(e);
}
// 通过文件名获取文件MIME类型
String contentType = this.getServletContext().getMimeType(fileName);
String contentDisposition = "attachment;filename=" + frameName;
// 设置响应头
response.setHeader("Content-Type", contentType);
response.setHeader("Content-Disposition", contentDisposition);
// 获取绑定了客户端的响应流
ServletOutputStream output = response.getOutputStream();
// 获取要下载的文件
FileInputStream input = new FileInputStream(new File("F:/哈哈.mp3"));
// 将输入流中的数据复制到输出流中
IOUtils.copy(input, output);
input.close();
}
public static String fileNameEncoding(String fileName, HttpServletRequest request) throws Exception{
String agent = request.getHeader("User-Agent");
if(agent.contains("Firefox")){
BASE64Encoder base64Encoder = new BASE64Encoder();
fileName = "=?utf-8?B?" + base64Encoder.encode(fileName.getBytes("utf-8")) + "?=";
} else if (agent.contains("<SIE")) {
fileName = URLEncoder.encode(fileName, "utf-8");
} else {
fileName = URLEncoder.encode(fileName, "utf-8");
}
return fileName;
}
}
注意:
Base64Encoder并不属于JDK标准库范畴,但是又包含在了JDK中,解决方法:
1、按照如下方法设置Eclipse导入%JAVA_HOME%\jre\lib目录下的rt.jar包即可,Project->Properties,选择Java Build Path设置项,再选择Libraries标签,Add External Jars添加%JAVA_HOME%\jre\lib\rt.jar就可以使用了。
2、MyEclipse —— Preferences —— Java —— Compiler —— Errors/Warnings —— Deprecated and restricted API —— Forbidden reference(access rules): Ignore
——BASE64Encoder、BASE64Decoder
public void fun() throws Exception {
// BASE64编码
String s = "haha";
BASE64Encoder encoder = new BASE64Encoder();
s = encoder.encode(s.getBytes("utf-8"));
System.out.println(s);
// BASE64解码
BASE64Decoder decoder = new BASE64Decoder();
byte[] b = decoder.decodeBuffer(s);
System.out.println(new String(b, "utf-8"));
}