文件的上传
- 文件上传的条件和原理
文件上传页面书写必须符合以下条件:
- Form表单的请求方式必须为post
- Form表单中提供type="file"类型的上传输入框
- Form表单enctype属性必须是multipart/form-data,enctype默认值为application/x-www-urlencoded
文件上传原理:
使用request.getInputStream();通过流来取得用户上传的数据,关键问题是流的解析
2、借助三方组件实现文件上传
使用Apache提供的commons-fileupload组件进行上传功能开发(内部核心封装的为文件流解析方法)
commons-io为fileupload组件在1.1开始就依赖的jar包,用于辅助文件的io操作
示例代码:
package com.gaojinze.web.upload.servlet;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
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.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FilenameUtils;
/**
* 使用第三方组件进行上传
* apache的
* commons-fileupload.jar:核心内容解析请求正文实现上传
* commons-io.jar:是apache对jdk对java.io.*对扩展和增强,fileupload从1.1就依赖该包
* @author gaopengfei
*
*/
public class UploadServlet3 extends HttpServlet {
private static final long serialVersionUID = 1L;
public UploadServlet3() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//检验表单enctype属性
boolean isMultipartContent = ServletFileUpload.isMultipartContent(request);
if (!isMultipartContent) {
throw new RuntimeException("Please check your form enctype attribute,the value msut be multipart/form-data");
}
//创建核心解析器实例
DiskFileItemFactory factory = new DiskFileItemFactory();//创建工厂实例,该工厂可以创建FileItem对象
ServletFileUpload upload = new ServletFileUpload(factory);//利用工厂创建解析器实例
upload.setFileSizeMax(4 * 1024 * 1024);//设置单个文件的上传大小为4M
upload.setSizeMax(8 * 1024 * 1024);//设置总上传文件的大小为8M
List fileItems = new ArrayList<>();
try {
fileItems = upload.parseRequest(request);
} catch (FileUploadBase.FileSizeLimitExceededException e) {
response.getWriter().write("单个文件上传大小不得超过4M");
}catch (FileUploadBase.SizeLimitExceededException e) {
response.getWriter().write("多文件上传总大小不得超过8M");
}catch (FileUploadException e) {
e.printStackTrace();
}
for (FileItem fileItem : fileItems) {
if (fileItem.isFormField()) {
//非上传字段
processFormField(fileItem);
}else {
//上传字段
processUploadField(fileItem);
}
}
response.getWriter().write("上传成功!");
response.setHeader("refresh", "1;url=" + request.getContextPath() + "/upload.jsp");
// response.sendRedirect(request.getContextPath() + "/upload.jsp");
}
/**
* 处理非上传字段
* @param fileItem
*/
private void processFormField(FileItem fileItem) {
String fieldName = fileItem.getFieldName();
String fieldValue = null;
try {
fieldValue = fileItem.getString("UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(fieldName + ":" + fieldValue);
}
/**
* 处理上传文件
* @param fileItem
*/
private void processUploadField(FileItem fileItem) {
try {
//获取文件名(因为浏览器不同导致在选择完文件后的name也不同,如IE选择后,文件名为:D:\XXX\a.txt)
String fileName = fileItem.getName();
//判断是否有未选择的空项
if ("".equals(fileName) || null == fileName) {
return;
}
//通过扩展名来限制上传文件类型
String extension = FilenameUtils.getExtension(fileName);
//获取上传文件的MIME类型
String contentType = fileItem.getContentType();
if (!extension.equalsIgnoreCase("jpg"))
return;
if (!contentType.startsWith("image")) {
return;
}
fileName = FilenameUtils.getName(fileName);
String uuidFileName = UUID.randomUUID().toString() + "_" + fileName;
//得到存放文件的真实路径
String uploadBasePath = getServletContext().getRealPath("/WEB-INF/files");
// String childUploadDir = getChildUploadDir(uploadBasePath);
String childUploadDir = getChildUploadDir2(uploadBasePath, uuidFileName);
//保存文件,并清理缓存
fileItem.write(new File(uploadBasePath + File.separator +childUploadDir, uuidFileName));
} catch (Exception e) {
throw new RuntimeException("上传失败!");
}
}
/**
* 按照日期进行子目录创建
* @param uploadPath
* @return
*/
private String getChildUploadDir(String uploadBasePath) {
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String childUploadDir = dateFormat.format(date);
File childDir = new File(uploadBasePath, childUploadDir);
if (!childDir.exists()) {
//若目录不存在则创建
childDir.mkdirs();
}
return childUploadDir;
}
/**
* 通过uuid的hascode分散目录
* @param uploadBasePath
* @param uuidFileName
* @return
*/
private String getChildUploadDir2(String uploadBasePath, String uuidFileName){
int hashCode = uuidFileName.hashCode();
//作为一级目录名
int dir1 = hashCode&0xf;
//作为二级目录名
int dir2 = (hashCode&0xf0)>>4;
String childUploadDir = dir1 + File.separator + dir2;
File childDir = new File(uploadBasePath, childUploadDir);
if (!childDir.exists()) {
//若目录不存在则创建
childDir.mkdirs();
}
return childUploadDir;
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
3、三方组件核心类介绍以及实际开发中需要注意的细节
- 如何保证服务器安全?
将文件上传目录放置在WEB-INF目录下即可 - 如何避免文件重名而导致上传后文件覆盖?
创建唯一文件名,使用UUID.randomUUID().toString() + "_" + fileName;即可 - 如何避免同一个目录文件过多而查找难?
解决方法:
a)以日期分类目录
/**
* 按照日期进行子目录创建
* @param uploadPath
* @return
*/
private String getChildUploadDir(String uploadBasePath) {
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String childUploadDir = dateFormat.format(date);
File childDir = new File(uploadBasePath, childUploadDir);
if (!childDir.exists()) {
//若目录不存在则创建
childDir.mkdirs();
}
return childUploadDir;
}
b)利用UUID文件名的hash码进行分散存储
/**
* 通过uuid的hascode分散目录
* @param uploadBasePath
* @param uuidFileName
* @return
*/
private String getChildUploadDir2(String uploadBasePath, String uuidFileName){
int hashCode = uuidFileName.hashCode();
//作为一级目录名
int dir1 = hashCode&0xf;
//作为二级目录名
int dir2 = (hashCode&0xf0)>>4;
String childUploadDir = dir1 + File.separator + dir2;
File childDir = new File(uploadBasePath, childUploadDir);
if (!childDir.exists()) {
//若目录不存在则创建
childDir.mkdirs();
}
return childUploadDir;
}
- 文件的限制
- 限制文件上传大小
Web上传不适合上传太大的文件,一般为2M
/创建核心解析器实例
DiskFileItemFactory factory = new DiskFileItemFactory();//创建工厂实例,该工厂可以创建FileItem对象
ServletFileUpload upload = new ServletFileUpload(factory);//利用工厂创建解析器实例
upload.setFileSizeMax(4 * 1024 * 1024);//设置单个文件的上传大小为4M
upload.setSizeMax(8 * 1024 * 1024);//设置总上传文件的大小为8M
- 限制上传文件的类型
通过MIME类型
操作系统是根据扩展名来区分文件类型的
信息传输的数据通过MIME来区分类型,详细的对应关系,可以查看tomcat目录下的web.xml有记录
//通过扩展名来限制上传文件类型,该方式不靠谱
String extension = FilenameUtils.getExtension(fileName);
if (!extension.equalsIgnoreCase("jpg")) {
return;
}
------------------------------------------------------------
//获取上传文件的MIME类型
String contentType = fileItem.getContentType();
if (!extension.equalsIgnoreCase("jpg"))
return;
if (!contentType.startsWith("image")) {
return;
}
------------------------------------------------------------
//判断是否有未选择的空项
if ("".equals(fileName) || null == fileName) {
return;
}
- 上传时的临时文件处理
DiskFileItemFactory创建FileItem对象时会使用缓存,默认10kb,若上传文件大小超过10kb,则会采用磁盘进行缓存(磁盘缓存即为垃圾文件
//创建核心解析器实例
DiskFileItemFactory factory = new DiskFileItemFactory();//创建工厂实例,该工厂可以创建FileItem对象
//设置缓存文件大小
factory.setSizeThreshold(100 * 1024);
//设置缓存文件的存放目录,默认是系统的临时文件目录
factory.setRepository(new File("f:/"));
若使用原生io流进行文件读写存储,则在关闭流后使用fileItem.delete();方法进行缓存清理
若使用fileItem.write();进行读写存储,则无需处理,其内部已经进行流缓存清理
- 中文乱码问题
- 普通字段中文乱码解决
String fieldValue = fileItem.getString("UTF-8");
- 中文文件名乱码解决
request.setCharacterEncoding("UTF-8");
文件的下载
- FilelistServlet(文件列表获取)
package com.gaojinze.web.upload.servlet.download;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 文件列表Servlet
* @author gaopengfei
*/
public class FilelistServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public FilelistServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//创建存储key为文件在服务器中的名称,value为用户上传文件名
Map fileMap = new HashMap<>();
String fileBasePath = getServletContext().getRealPath("/WEB-INF/files");
File uploadRootDir = new File(fileBasePath);
findAllFiles(uploadRootDir, fileMap);
request.setAttribute("fileMap", fileMap);
request.getRequestDispatcher("/filelist.jsp").forward(request, response);
}
/**
* 遍历服务器所有文件并将文件信息存储到map中
* @param file
* @param fileMap
*/
private void findAllFiles(File file, Map fileMap) {
if (file.isFile()) {
String fileUUIDName = file.getName();
String fileRealName = fileUUIDName.substring(fileUUIDName.indexOf("_") + 1);
fileMap.put(fileUUIDName, fileRealName);
}else {
//如果是目录则递归进行遍历
File[] listFiles = file.listFiles();
if (listFiles == null || listFiles.length == 0) {
return;
}
for (File f : listFiles) {
findAllFiles(f, fileMap);
}
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
- filelist.jsp文件列表展示页
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
文件列表
文件列表
文件名
操作
${ mk.value }
下载
- DownloadServlet(文件下载)
package com.gaoshiyi.web.upload.servlet.download;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 文件下载
* @author gaopengfei
*
*/
public class DownloadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public DownloadServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String uuidName = request.getParameter("filename");
String realName = uuidName.substring(uuidName.indexOf("_") + 1);
// 上传文件跟目录
String uploadRootPath = getServletContext().getRealPath("/WEB-INF/files");
// 获取对应uuidname文件的子目录
String childDirPath = getChildDirPathByUUIDName(uploadRootPath, uuidName);
// 构建文件读取流
InputStream in = new FileInputStream(new File(uploadRootPath + File.separator + childDirPath, uuidName));
//处理下载时的文件名称中文问题
String userAgent = request.getHeader("User-Agent");
if (userAgent.contains("Firefox")) {
//单独处理火狐浏览器
realName = new String(realName.getBytes("UTF-8"), "ISO-8859-1");
}else {
//其他浏览器
realName = URLEncoder.encode(realName, "UTF-8");
}
// 告知浏览器以下载方式下载
response.setHeader("Content-Disposition", "attachment;filename=" + realName);
// 设置头信息告知客户端文件大小
response.setContentLength(in.available());
// 设置头信息告知客户端文件MIME类型
response.setHeader("Content-Type", "application/octet-stream");
// 输出
OutputStream out = response.getOutputStream();
int len = 0;
byte[] buff = new byte[1024];
while ((len = in.read(buff)) != 1) {
out.write(buff, 0, len);
}
in.close();
out.close();
}
/**
* 获取子目录
*
* @param uploadRootPath
* @param uuidName
* @return
*/
private String getChildDirPathByUUIDName(String uploadRootPath, String uuidName) {
int hashCode = uuidName.hashCode();
int dir1 = hashCode&0xf;
int dir2 = (hashCode&0xf0) >> 4;
String cDir = dir1 + File.separator + dir2;
File childDir = new File(uploadRootPath, cDir);
if (!childDir.exists()) {
childDir.mkdirs();
}
return cDir;
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}