一、如何向服务器上传一个本地图片?
首先,Fileupload的使用场景:比如说提交作业,更换头像。
需要一个form表单,表单有如下三个要求
- input type = file
- method = post
- enctype=multipart/form-data
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//response给我们封装现存的响应输出流
//response.getOutputStream
//request给我们封装好现存的输入流
ServletInputStream inputStream = request.getInputStream();
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.close();
//inputStream可以关也可以不关,不关的话tomcat会帮助我们关闭
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
二、为什么文件会损坏?
用上述代码上传一个图片,去部署根目录下找到文件,发现文件已损坏
上传一个txt文件发现文件平白无故多出了一些WebKitFormBoundary分隔符
对于一个二进制文件,图片、音频、视频、exe文件,多出来这一部分会直接导致文件损坏。
并且普通form表单请求参数也无法获取,因为请求体中的数据结构发生了变更,之前的数据key=value&key=value拼接的,getParameter通过key可以获取到数据value,那么如果数据结构发生了变更,getParameter理应不能再用了。
所有一切的原因都是因为添加了enctype=multipart/form-data。但是如果不添加,就不会上传文件。
三、使用组件上传文件
实现步骤:
- 创建DiskFileItemFactory对象,设置缓冲区大小和临时文件目录
- 使用DiskFileItemFactory 对象创建ServletFileUpload对象,并设置上传文件的大小限制。
- (关键)调用ServletFileUpload.parseRequest方法解析request对象,得到一个保存了所有上传内容的List对象。list中的每一个元素,就是每个input的封装。
- 对list进行迭代,每迭代一个FileItem对象,调用其isFormField方法判断是否是上传文件。
- true表示为普通表单字段,则调用getFieldName、getString方法得到字段名和字段值。
- false 为上传文件,则调用getInputStream方法得到数据输入流,从而读取上传数据。
@WebServlet("/upload2")
public class UploadServlet2 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//判断是否含有上传的文件
response.setContentType("text/html;charset=utf-8");
boolean result = ServletFileUpload.isMultipartContent(request);
if(!result){
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);
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);
}else {
processUploadedFile(item);
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
}
private void processUploadedFile(FileItem item) {
//属性
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 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) {
String name = item.getFieldName();
String value = item.getString();
System.out.println(name + ":" + value);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
四、中文乱码问题
表单里面还是有乱码问题的,且之前可以解决乱码的API,此时也是无法解决的。
那是因为:请求体数据已经发生变化,所以之前设置编码格式的API此时也不再适用了。
把刚刚代码中的processFormField方法改成如下形式:
private void processFormField(FileItem item) {
String name = item.getFieldName();
String value = null;
try {
value = item.getString("utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println(name + ":" + value);
}
如果上传文件的名字中有中文,则添加代码:
ServletFileUpload handler = new ServletFileUpload(factory);
//可以解决上传文件中文名乱码问题
handler.setHeaderEncoding("utf-8");
五、将数据封装到对象中
使用刚刚的代码,我们只是将图片上传到了服务器,将表单数据输出到了控制台。
接下来需要将这些数据封装到一个bean中(对于文件,封装它的路径),然后再保存到数据库。
我们可以写一个FileUploadUtils类,把前端页面输入的所有数据整理放进一个map里。
public class FileUploadUtils {
public static Map parseRequest(HttpServletRequest request){
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletContext servletContext = request.getServletContext();
File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
factory.setRepository(repository);
// Create a new file upload handler
ServletFileUpload upload = new ServletFileUpload(factory);
//如果上传的文件名中文乱码,可以这么解决
//upload.setHeaderEncoding("utf-8");
// bytes 1024 bytes
//upload.setFileSizeMax(1024);
Map map = new HashMap();
try {
List<FileItem> items = upload.parseRequest(request);
//对items进行迭代
Iterator<FileItem> iterator = items.iterator();
while (iterator.hasNext()){
//每一个item其实就是一个input框的封装
FileItem item = iterator.next();
if(item.isFormField()){
//当前是普通的form表单数据
processFormField(item, map);
}else{
//当前是上传的文件
processUploadedFile(item, map, request);
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
return map;
}
/**
* 处理上传的文件逻辑
* @param item
* @param map
* @param request
*/
private static void processUploadedFile(FileItem item, Map map, HttpServletRequest request) {
String fieldName = item.getFieldName();
String fileName = item.getName();
String UPLOAD_BASE = "upload";
//解决文件重名问题
fileName = UUID.randomUUID().toString() + "-" + fileName;
//解决目录内文件数过多的问题
int hashCode = fileName.hashCode();
String hexString = Integer.toHexString(hashCode);
char[] chars = hexString.toCharArray();
for (char aChar : chars) {
UPLOAD_BASE = UPLOAD_BASE + "/" + aChar;
}
System.out.println(fieldName + " === " + fileName);
UPLOAD_BASE = UPLOAD_BASE + "/" + fileName;
String realPath = request.getServletContext().getRealPath(UPLOAD_BASE);
File file = new File(realPath);
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
try {
//保存文件到硬盘中
item.write(file);
//保存什么路径呢?
//img src='/应用名/资源名'
map.put(fieldName, UPLOAD_BASE);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 处理普通的form表单数据
* 只需要获取name和value值即可
* 上传文件之后,不要再用getParameter去获取请求参数了
* 有没有中文乱码问题呢?
* @param item
* @param map
*/
private static void processFormField(FileItem item, Map map) {
String name = item.getFieldName();
String value = null;
try {
value = item.getString("utf-8");
//如果传入进来的是checkbox,那么下面需要变更一下
map.put(name, value);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println(name + " = " + value);
}
}
这样,在主类中我们只需要调用BeanUtils类的populate()方法就可以将map中的数据全部放进一个对象中。
@WebServlet("/upload3")
public class UploadServlet3 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//因为提交的是multipart/form-data,所以响应正文的数据结构发生改变,无法通过以下代码解决乱码问题。
//request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
//post里面写代码
//返回的是true表示有Multipart
boolean result = ServletFileUpload.isMultipartContent(request);
if (!result) {
response.getWriter().println("不包含上传的文件");
return;
}
//通过FileUploadUtils获取map,map中存取的是name-value键值对
Map map = FileUploadUtils.parseRequest(request);
User user = new User();
//将map中的数据封装成一个user对象
try {
BeanUtils.populate(user, map);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
System.out.println(user);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
另外,在刚刚的FileUploadUtils类中,我们解决了两个问题:
- 文件重名问题:
//解决文件重名问题
fileName = UUID.randomUUID().toString() + "-" + fileName;
- 目录内文件数目过多问题:
//解决目录内文件数过多的问题
int hashCode = fileName.hashCode();
String hexString = Integer.toHexString(hashCode);
char[] chars = hexString.toCharArray();
for (char aChar : chars) {
UPLOAD_BASE = UPLOAD_BASE + "/" + aChar;
}
System.out.println(fieldName + " === " + fileName);
UPLOAD_BASE = UPLOAD_BASE + "/" + fileName;
六、回显操作
指到数据库里面去查询这条数据,然后封装到bean中,接下来,将它在页面显示出来。
在这里,我们注册成功之后,封装到bean,我们不去保存到数据库,而是通过转发交给另外一个servlet,该servlet在本页面中将之前选择的数据进行回显。
首先,在UploadServlet3中加如下代码:
//转发给另一个Servlet,将其内容显示出来
request.setAttribute("user", user);
request.getRequestDispatcher("/view").forward(request, response);
编写刚刚在代码中提到的ViewServlet类
@WebServlet("/view")
public class ViewServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
User user = (User) request.getAttribute("user");
response.setContentType("text/html;charset=utf-8");
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>");
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
这样写后发现打断点后能显示头像,直接运行无法显示头像。这是因为FileUploadUtils中的代码:item.write(file);该方法是非阻塞方法。文件可能还在传输的过程中,而此时viewServlet就调用。
这个问题是可以忽略的,如果从数据库中取数据然后再显示是不会出现这种问题的。