14.FileUpload
14.1.文件上传中遇到的问题
14.1.1.尝试文件上传
- 使用form表单,设置属性input type=file,method=post,Content-Length: 11,表示的是请求体的长度。
- 此时仅会上传文件名,没有长传文件的data。还需要设置一个属性
- 设置enctype=multipart/form-data
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletInputStream inputStream = request.getInputStream();
//FileInputStream fileInputStream = new FileInputStream();
//比如保存在当前应用下 upload目录下面
String realPath = getServletContext().getRealPath("upload/1.jpg");
File file = new File(realPath);
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
FileOutputStream fileOutputStream = new FileOutputStream(file);
int length = 0;
byte[] bytes = new byte[1024];
while ((length = inputStream.read(bytes)) != -1){
fileOutputStream.write(bytes, 0, length);
}
fileOutputStream.flush();
fileOutputStream.close();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
}catch (Exception e){
response.setStatus(500);
}
}
}
- 在out目录下,打开upload,发现文件无法打开。
14.1.2.上传文件之后会引入很多字符数据
- 用一个txt文件来测试,为什么文件会多了很多数据,而对于一个二进制文件,引入了这些字符之后,直接导致文件的损坏,文件就会无法打开。
------WebKitFormBoundarytdzmIhaGTkvQcl7f
content-Disposition: form-data; name="image" ; filename="1.txt"content-Type: text/plain
hello
------WebKitFormBoundarytdzmIhaGTkvQcl7f--
- 字符因为我们引入了enctype=multipart/form-data造成的
- 如果不加入这个enctype属性---------只会上传文件的名字, 不会上传文件的数据
- 但是引入了这个enctype属性---------------会上传文件的数据,但是还会引入一些字符
- 当我们在进行文件上传的同时,很大概率也会上传普通的文本表单数据,那么这些数据混合在一起,这些字符就是用来分割不同的部分的,分割每个input。
14.1.3.在引入文件上传的同时,无法获取普通文本表单数据
- 发现之前可以获取到请求参数的,但是在上传文件以后,就无法获取了。
- 因为表单数据默认的数据提交方式是Key=value型提交的,如果以这种方式提交数据,那么肯定是无法上传文件的,导致了表单数据无法获取到。
- 如果希望提交文件,那么肯定要将数据的提交方式改变,也就是数据提交的格式不能够再使用key=value型了
- 而我们的获取请求参数的API,只能获取到key=value型数据。
14.2.使用第三方jar包实现文件上传
- 对于文件上传问题,如果想完全自己实现,实现的难度还是很大的。所以我们还是需要第三方jar包来实现。
-
https://mvnrepository.com/
进去验证以后搜索fileupload,下载fileupload和io一共两个jar包。
- 使用手册:
http://commons.apache.org/proper/commons-fileupload/using.html
- API:
- DiskFileItemFactory 是创建 FileItem 对象的工厂,这个工厂类常用方法:
- 构造函数:public DiskFileItemFactory(int sizeThreshold, java.io.File repository)
- public void setSizeThreshold(int sizeThreshold):设置内存缓冲区的大小,默认值为10K。当上传文件大于缓冲区大小时, fileupload组件将使用临时文件缓存上传文件。
- public void setRepository(java.io.File repository):指定临时文件目录,默认值为System.getProperty(“java.io.tmpdir”).
- ServletFileUpload 负责处理上传的文件数据,并将表单中每个输入项封装成一个 FileItem 对象中。常用方法有:
- boolean isMultipartContent(HttpServletRequest request) :判断上传表单是否为multipart/form-data类型
- List parseRequest(HttpServletRequest request):解析request对象,并把表单中的每一个输入项包装成一个fileItem 对象,并返回一个保存了所有FileItem的list集合。
- setFileSizeMax(long fileSizeMax):设置单个上传文件的最大值bytes
- setSizeMax(long sizeMax) :设置上传文件总量的最大值(多个文件同时上传时文件最大值的总和)
- setHeaderEncoding(java.lang.String encoding):设置编码格式,解决上传文件名乱码问题
- FileItem 用来表示文件上传表单中的一个上传文件对象或者普通表单对象
- boolean isFormField() 判断FileItem是一个文件上传对象还是普通表单对象
- 如果判断是一个普通表单对象
- String getFieldName() 获得普通表单对象的name属性
- String getString(String encoding) 获得普通表单对象的value属性
- 如果判断是一个文件上传对象
- String getName() 获得上传文件的文件名(有些浏览器会携带客户端路径)
- InputStream getInputStream() 获得上传文件的输入流
- delete() 在关闭FileItem输入流后,删除临时文件
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.List;
@WebServlet("/upload")
public class Upload extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//注意中文乱码问题,提交上来的表单设置这个没有用
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
//判断是否含有上传的文件
if(!ServletFileUpload.isMultipartContent(request)){
response.getWriter().println("没有包含上传的文件");
return;
}
DiskFileItemFactory factory = new DiskFileItemFactory();
//设置一个仓库、缓冲区,目的是为了传输大型文件时,会利用缓冲区边中转边传输
ServletContext servletContext = getServletContext();
File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
factory.setRepository(repository);
//拿到真正去处理请求的处理器
ServletFileUpload handler = new ServletFileUpload(factory);
//解决上传文件中文名乱码问题
handler.setHeaderEncoding("utf-8");
//设置文件大小为1024Bytes*1024*10=10MB
handler.setFileSizeMax(1024*1024*10);
try {
//这一步是最为关键的,它会对请求进行解析,会将每个input封装成为一个FileItem
List<FileItem> items = handler.parseRequest(request);
//遍历出每一个input,这里每一个FileItem就是每一个input
Iterator<FileItem> iterator = items.iterator();
while (iterator.hasNext()){
//item有两种可能性,一种是普通的form表单,一种是上传的文件
FileItem item = iterator.next();
//执行这一分支说明他是普通的form表单
if(item.isFormField()){
processFormField(item);
}else {
//这一分支说明他是文件
processUploadedFile(item);
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
}
private void processUploadedFile(FileItem item) {
//表单的name属性
String fieldName = item.getFieldName();
//上传的文件名
String fileName = item.getName();
//MIME类型
String contentType = item.getContentType();
//是否在内存里面
boolean isInMemory = item.isInMemory();
//大小,单位是字节
long sizeInBytes = item.getSize();
System.out.println("file:" + fieldName + ":" + fileName + ":" + contentType + ":" + isInMemory + ":" + sizeInBytes);
//保存在本地硬盘上,real是获取到的硬盘的绝对路径
String realPath = getServletContext().getRealPath("upload/" + fileName);
File file = new File(realPath);
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
try {
//执行该行代码时,它会将文件的数据写入到file
//这一步也是对磁盘IO的封装
item.write(file);
} catch (Exception e) {
e.printStackTrace();
}
}
private void processFormField(FileItem item) {
//表单的name属性名
String name = item.getFieldName();
//表单的value值
String value = null;
try {
//解决普通表单数据中有中文乱码问题
value = item.getString("utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println(name + ":" + value);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
14.3.JavaBean
- 本质上,就是使用API将其封装成一个Map,以后添加需求的时候就可以使用BeanUtils.poulate(Object, parameterMap)方法直接封装
import com.cskaoyan.request.User;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class FileUploadUtils {
public static Map<String, String> parseRequest(HttpServletRequest request) {
DiskFileItemFactory factory = new DiskFileItemFactory();
//设置一个仓库、缓冲区,目的是为了传输大型文件时,会利用缓冲区边中转边传输
ServletContext servletContext = request.getServletContext();
File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
factory.setRepository(repository);
// 拿到真正去处理请求的处理器
ServletFileUpload handler = new ServletFileUpload(factory);
//可以解决上传文件中文名乱码问题
handler.setHeaderEncoding("utf-8");
// bytes
//handler.setFileSizeMax(1024);
Map<String, String> params = new HashMap<>();
try {
//这一步是最为关键的,它会对请求进行解析,会将每个input封装成为一个FileItem
List<FileItem> items = handler.parseRequest(request);
//遍历出每一个input,
Iterator<FileItem> iterator = items.iterator();
while (iterator.hasNext()){
//item有两种可能性,一种是普通的form表单,一种是上传的文件
FileItem item = iterator.next();
if(item.isFormField()){
processFormField(item, params);
}else {
processUploadedFile(item, params,request);
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
return params;
}
private static void processUploadedFile(FileItem item, Map<String, String> params, HttpServletRequest request) {
String fieldName = item.getFieldName();
String fileName = item.getName();
String contentType = item.getContentType();
boolean isInMemory = item.isInMemory();
long sizeInBytes = item.getSize();
System.out.println("file:" + fieldName + ":" + fileName + ":" + contentType + ":" + isInMemory + ":" + sizeInBytes);
//保存在本地硬盘上 硬盘的绝对路径
String relativePath = "upload/" + fileName;
String realPath = request.getServletContext().getRealPath(relativePath);
File file = new File(realPath);
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
try {
//执行该行代码时,它会将文件的数据写入到file
//这一步也是对磁盘IO的封装,相当于一个新的线程去写入
item.write(file);
//应该把文件的相对路径保存下来,为什么不保存绝对路径呢
//因为我们这边有一个需求, 需要将该图片再次显示给客户端 img src /应用名 /资源路径
// /app /upload/1.jpg
params.put(fieldName, relativePath);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void processFormField(FileItem item, Map<String, String> params) {
String name = item.getFieldName();
String value = null;
try {
value = item.getString("utf-8");
//BeanUtils 没有map参数
params.put(name, value);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println(name + ":" + value);
}
}
14.4.重名文件
- 如果用户上传的文件有重复名称,解决方法之一如下:
- 对于每个FileItem的filename,都是一个String值,可以将其filename赋值为任意生成的一个随机UUID,因此其32位hashcode一般不同。
- 采取这样一种思想,将32位二进制数据转换成8位16进制数据,每一位都是一个文件目录,这样就可以创建16^8次方个文件目录,重复的概率几乎为0
import com.cskaoyan.request.User;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.*;
public class FileUploadUtils {
public static Map<String, String> parseRequest(HttpServletRequest request) {
DiskFileItemFactory factory = new DiskFileItemFactory();
//设置一个仓库、缓冲区,目的是为了传输大型文件时,会利用缓冲区边中转边传输
ServletContext servletContext = request.getServletContext();
File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
factory.setRepository(repository);
// 拿到真正去处理请求的处理器
ServletFileUpload handler = new ServletFileUpload(factory);
//可以解决上传文件中文名乱码问题
handler.setHeaderEncoding("utf-8");
// bytes
//handler.setFileSizeMax(1024);
Map<String, String> params = new HashMap<>();
try {
//这一步是最为关键的,它会对请求进行解析,会将每个input封装成为一个FileItem
List<FileItem> items = handler.parseRequest(request);
//遍历出每一个input,
Iterator<FileItem> iterator = items.iterator();
while (iterator.hasNext()){
//item有两种可能性,一种是普通的form表单,一种是上传的文件
FileItem item = iterator.next();
if(item.isFormField()){
processFormField(item, params);
}else {
processUploadedFile(item, params,request);
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
return params;
}
private static void processUploadedFile(FileItem item, Map<String, String> params, HttpServletRequest request) {
//属性
String fieldName = item.getFieldName();
String fileName = item.getName();
//随机生成UUID
String uuid = UUID.randomUUID().toString();
//赋值给filename,将filename拼接在后面不要隐藏文件后缀名,生成新的filename
fileName = uuid + "-" + fileName;
String contentType = item.getContentType();
boolean isInMemory = item.isInMemory();
long sizeInBytes = item.getSize();
System.out.println("file:" + fieldName + ":" + fileName + ":" + contentType + ":" + isInMemory + ":" + sizeInBytes);
//保存再本地硬盘上 硬盘的绝对路径
String basePath = "upload";
int hashCode = fileName.hashCode();
String hexString = Integer.toHexString(hashCode);
char[] chars = hexString.toCharArray();
//生成了新的文件名
for (char aChar : chars) {
basePath = basePath + "/" + aChar;
}
String relativePath = basePath + "/" + fileName;
String realPath = request.getServletContext().getRealPath(relativePath);
File file = new File(realPath);
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
try {
//执行该行代码时,它会将文件的数据写入到file
//这一步也是对磁盘IO的封装
item.write(file);
//应该把文件的相对路径保存下来,为什么不保存绝对路径呢
//因为我们这边有一个需求, 需要将该图片再次显示给客户端 img src /应用名 /资源路径
// /app /upload/1.jpg
params.put(fieldName, relativePath);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void processFormField(FileItem item, Map<String, String> params) {
String name = item.getFieldName();
String value = null;
try {
value = item.getString("utf-8");
//BeanUtils 没有map参数
params.put(name, value);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println(name + ":" + value);
}
}
14.5.应用案例
- 表单页面,输入数据表单数据,图片等,写逻辑处理,最终还需要将刚刚录入的数据进行回显(图片需要回显出来)
import request.utils.FileUploadUtils;//导入刚刚封装的JavaBean工具
import org.apache.commons.beanutils.BeanUtils;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
@WebServlet("/uploadrefresh")
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//FileUpload
response.setContentType("text/html;charset=utf-8");
Map<String, String> map = FileUploadUtils.parseRequest(request);
User user = new User();
try {
BeanUtils.populate(user, map);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//将user数据和另外一个servlet进行共享
//没有在当前页面里面进行页面显示的代码原因是因为一个组件内处理的逻辑越多
//那么它的整个功能耦合性就越强;如果部分代码发生变化,其他代码很容易也会收到影响 MVC
//context、request域都可以,但是,context域有一个bug
//request.setAttribute("user", user);
//注意:转发前后请求方法不变
//如果你适用refresh、重定向,浏览器默认适用get
//context域有一个bug,在FileUplopUtils中,item.write(file)涉及到磁盘IO的写入,IO比正常运行的内存代码慢一些,
//在out文件中,文件是存在的,而在浏览器中,资源还没准备好就显示了,所以无法显示出来,因此用refresh等待一些时间就可以正常显示了
//request.getRequestDispatcher("/view").forward(request, response);
getServletContext().setAttribute("user", user);
response.getWriter().println("上传成功,即将回显数据");
response.setHeader("refresh", "5;url=" + request.getContextPath() + "/view");
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/view")
public class ViewServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
User user = (User) getServletContext().getAttribute("user");
response.getWriter().println("<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>Title</title>\n" +
"</head>\n" +
"<body>");
response.getWriter().println("<div>" + user.getUsername() + "</div>");
response.getWriter().println("<div>" + user.getPassword() + "</div>");
response.getWriter().println("<div><img src='" + request.getContextPath() + "/" + user.getImage() + "' /></div>");
response.getWriter().println("</body>\n" +
"</html>");
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/app/uploadrefresh" enctype="multipart/form-data" method="post">
<input type="file" name="image"><br>
<input type="text" name="username"><br>
<input type="password" name="password"><br>
<input type="submit">
</form>
</body>
</html>