关于文件上传问题,就不过多的阐述了,网上的框架很多,基本随取随用了,这里主要就文件上传过程中的一些细节进行一个盘点,对各种问题的解决也做一个总结,也希望能帮到每个走在IT路上的童鞋们。
直接用IO流进行文件的上传和下载可以实现没问题,但是有点太耗时了,用别人封装好的比较成熟稳定的框架是一个不错的选择,今天的样例以阿帕奇的fileUpload框架为基础进行
先上一段我自己写的利用fileUpload框架上传文件的原始核心代码:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/*
* 实现思路:
* 1、判断表单enctype属性是否为multipart/form-data类型
* 2、创建一个DiskFileItemFactory类
* 3、通过DiskFileItemFactory类创建一个ServletFileUpload对象
* 4、解析request,得到一个表单Item的集合List<FlieItem>
* 5、遍历list,判断是Items是否为文件,如果是,则创建流读取
* 5.1不是文件类型,则控制台输出
* 5.2是文件,则创建流读取
*
* */
//因为要输出提示内容,所以需要先设置response字符集
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
String basePath = this.getServletContext().getRealPath("/files");
System.out.println(basePath);
//利用ServletFileUpload的静态方法判断表单enctype属性是否为multipart/form-data
boolean ismultipart = ServletFileUpload.isMultipartContent(request);
if (ismultipart) {
// 创建一个DiskFileItemFactory类
DiskFileItemFactory factory = new DiskFileItemFactory();
// 利用factory类构建一个ServletFileUpload 对象
ServletFileUpload sfu = new ServletFileUpload(factory);
// 解析request,获取List集合
List<FileItem> Items;
try {
Items = sfu.parseRequest(request);
// 遍历集合
for (FileItem fileItem : Items) {
if (fileItem.isFormField()) {
// 如果是表单字段则打印出来
String name = fileItem.getName();
String value = fileItem.getString();
System.out.println(name+","+value);
} else {
// 上传字段,则保存到服务器
// 获取文件名
String filePath = fileItem.getName();
// 截取文件名
String fileName = filePath.substring(filePath.lastIndexOf(File.separator)+1);
// 获取文件输入流
InputStream is = fileItem.getInputStream();
// 使用绝对路径构建一个文件
File file = new File(basePath);
if (!file.exists()) {
file.mkdirs();
}
byte[] by = new byte[1024];
int len = -1;
OutputStream os = new FileOutputStream(basePath+File.separator+fileName);
while ((len = is.read(by))!=-1) {
os.write(by, 0, len);
}
is.close();
os.close();
}
}
} catch (FileUploadException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
writer.println("请检查你的表单是否支持文件上传");
}
}
上面这段代码是实现文件上传功能最简单最核心的实现过程,但是这段代码中也存在诸多安全隐患及各种问题需要处理,接下来,我们就一个一个的盘点,然后找出解决之道,使程序的健壮性更强。
问题一、文件的安全问题。
涉及到文件的上传,一般是用户行为,我们对用户上传的文件以及意图一般是无法把控的,如果不进行防范,就存在用户这就涉及到安全问题。
一般的安全问题都是用户通过上传问题文件导致的,比如一个.jsp文件,然后通过浏览器访问,以执行文件中的代码,如果该文件中有恶意代码,就会影响服务端的正常工作,所以,这个安全问题是必须考虑的。
解决此问题的办法就是将上传的文件放到 WEB-INF之下,使客户端无法通过客户端访问上传的文件,无法执行恶意代码
如下:
String basePath = this.getServletContext().getRealPath("/WEB-INF/files");
如此一来,客户无法通过浏览器访问问题文件,就可大大提高程序的安全性。
问题二、中文乱码问题。
中文乱码一般有两个地方涉及,其一是普通字段的中文乱码问题,其二是中文文件名的乱码问题。
第一种的解决方案是:利用FileItem对象的getString方法设置字符集
item.getString("UTF-8");
第二种的解决方案是:设置请求对象字符集解决中文文件名乱码问题
request.setCharacterEncoding("UTF-8");
问题三、文件同名问题。
同名文件问题也是一个问题,如果同一目录下有上传了两个同名同类型的文件,后面的文件会将前面的文件覆盖掉,所以,这个问题也是需要解决的,解决方法很简单,原则就是文件命名时不使重名就可以了
两种解决方案:
1、利用时间戳命名文件
long time = System.currentTimeMillis();
String currentFileName = String.valueOf(time)+"_"+fileName;
2、利用UUID随机数命名文件
String UUIDFileName = UUID.randomUUID().toString()+"_"+fileName;
问题四、文件的目录问题
一般来说,每个文件夹中的文件数量是有一定限制的,根据系统和存储格式不同有不同的限制标准,再退一步说,即使文件夹中可以存放无限个文件,这样以来对于文件的查找和使用,也是非常不方便的,所以,我们不可能将文件统一放到同一个文件夹中,这时就需要进行分目录存放。
方案一、使用固定日期格式存储:
使用日期来创建文件目录:
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
String datePath = format.format(new Date());
但是如果遇到的是类似与记录日志的功能,按照日期存储仍然可能出现一个目录下文件过多的情况,这样就可能无法解决我们的问题,此时,我们可以采用下面这个方法
方案二、使用使用UUID文件的hashCode计算两级目录
int code = UUIDFileName.hashCode();
int dir1 = 0xf&code;
int dir2 = (0xf0&code)>>4;
问题五、文件大小问题
对于用户上传文件,我们一般都会限制上传文件的大小,以减少资源的消耗和服务器的压力,对于upload框架来说,提供了两个非常好用的方法可以解决这个问题
1、限制单个文件的大小
upload.setFileSizeMax(10*1024*1024);
2、限制文件总大小
upload.setSizeMax(20*1024*1024);
问题六、文件类型问题
我们很多时候做上传文件需求时,往往需要限制用户上传的文件类型,比如上传头像或者其他文件类型,等等,这个就需要我们对用户上传的文件类型做限制,可是,这个限制往往也跟浏览器的功能有关,我们无法做到绝对的限制住用户上传的文件类型(如果有童鞋知道其他解决方案,也欢迎交流分享啊),只能最大限度的规避。
// 判断文件的后缀是否为要求的类型
String extention = fileName.substring(fileName.lastIndexOf(".")+1);
// 定义允许上传文件的类型
String extentions = "txt";
int e =extentions.indexOf(extention);
if(e<0){
writer.write("请确认您上传的文件类型是否符合要求");
return;
}
// 通过FileItem的一个方法来获取文件内容的MIME类型
if (!fileItem.getContentType().startsWith("text")) {
writer.write("请确认您上传的文件类型是否符合要求");
return;
}
问题七、用户没传全的问题
有时候,我们需要用户上传多个文件,而用户并未选择全部上传,而是选择部分上传,我们就需要对这种情况进行处理
String fileName = fileItem.getName();
if ("".equals(fileName)) {
// 如果上传多个文件时有文件为空,则继续循环
continue;
}
问题八、 临时文件问题
FileUpload在上传文件过程中,如果文件大小在10k以内,一般使用内存作为缓存,但针对较大的文件,如视频音频等,便会用到临时文件,所以我们有时候需要创建一个临时文件目录,以供程序使用
File file = new File("D:/temp");
//判断文件目录是否存在
if(!file.exists()){
//创建一个临时文件目录
file.mkdirs();
}
//设置临时文件的存放目录
factory.setRepository(file);
//删除临时文件,注意要放到关闭流之后
fileItem.delete();
至此,总结了八个关于上传文件过程中需要注意的问题以及解决方案,以下是优化后的代码,仅供参考!
package com.icbc.fileuploaddemo.servlet;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
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 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;
/**
* 上传文件中的问题:
* 1、安全问题
* 将文件放到WEB-INF文件夹下面
* 2、文件乱码问题
* 2.1、非上传字段乱码问题:获取非上传字段文件名时传入字符集
* 2.2、上传文件中文名称问题,需要设置request的字符集
* 3、文件大小问题
* 4、文件类型问题
* 5、文件重名问题
* 6、分目录存储问题
* 7、用户没传全
* 8、临时文件和缓存问题
*/
@WebServlet("/FileUploadServlet3")
public class FileUploadServlet3 extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public FileUploadServlet3() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/*
* 实现思路:
* 1、判断表单enctype属性是否为multipart/form-data类型
* 2、创建一个DiskFileItemFactory类
* 3、通过DiskFileItemFactory类创建一个ServletFileUpload对象
* 4、解析request,得到一个表单Item的集合List<FlieItem>
* 5、遍历list,判断是Items是否为文件,如果是,则创建流读取
* 5.1不是文件,则控制台输出
* 5.2是文件,则创建流读取
*
* */
//因为要输出提示内容,所以需要先设置response字符集
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
// 设置请求字符集
request.setCharacterEncoding("UTF-8");
String basePath = this.getServletContext().getRealPath("/WEB-INF/files");
System.out.println(basePath);
//利用ServletFileUpload的静态方法判断表单enctype属性是否为multipart/form-data
boolean ismultipart = ServletFileUpload.isMultipartContent(request);
if (ismultipart) {
// 创建一个DiskFileItemFactory类
DiskFileItemFactory factory = new DiskFileItemFactory();
File temp = new File("D:/temp");
//判断文件目录是否存在
if (!temp.exists()) {
//创建一个临时文件目录
temp.mkdirs();
}
//设置临时文件的存放目录
factory.setRepository(temp);
// 利用factory类构建一个ServletFileUpload 对象
ServletFileUpload sfu = new ServletFileUpload(factory);
// 限制上传单个文件大小
// sfu.setFileSizeMax(10*1024*1024);
// 限制总文件大小
// sfu.setSizeMax(20*1024*1024);
// 解析request,获取List集合
List<FileItem> Items;
try {
Items = sfu.parseRequest(request);
// 遍历集合
for (FileItem fileItem : Items) {
if (fileItem.isFormField()) {
// 如果是表单字段则打印出来
String name = fileItem.getFieldName();
String value = fileItem.getString("UTF-8");
System.out.println(name+","+value);
} else {
// 上传字段,则保存到服务器
// 获取文件名
String fileName = fileItem.getName();
if ("".equals(fileName)) {
// 如果上传多个文件时有文件为空,则继续循环
continue;
}
// 判断文件的后缀是否为要求的类型
// String extention = fileName.substring(fileName.lastIndexOf(".")+1);
定义允许上传文件的类型
// String extentions = "txt";
// int e =extentions.indexOf(extention);
// if(e<0){
// writer.write("请确认您上传的文件类型是否符合要求");
// return;
// }
// 通过FileItem的一个方法来获取文件内容的MIME类型
// if (!fileItem.getContentType().startsWith("text")) {
// writer.write("请确认您上传的文件类型是否符合要求");
// return;
// }
// 截取文件名
fileName = fileName.substring(fileName.lastIndexOf(File.separator)+1);
String fullPath = dealFilePath(basePath, fileName);
// 获取文件输入流
InputStream is = fileItem.getInputStream();
byte[] by = new byte[1024];
int len = -1;
OutputStream os = new FileOutputStream(fullPath);
while ((len = is.read(by))!=-1) {
os.write(by, 0, len);
}
is.close();
os.close();
//删除临时文件,注意要放到关闭流之后
fileItem.delete();
}
}
} catch (FileUploadBase.FileSizeLimitExceededException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (FileUploadBase.SizeLimitExceededException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (FileUploadException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
writer.println("请检查你的表单是否支持上传");
}
}
/**
* 处理同名文件问题和文件目录问题
* @param path
* @param fileName
* @return
*/
protected String dealFilePath(String basePath,String fileName) {
// 1、利用时间戳命名文件
// long time = System.currentTimeMillis();
// String currentFileName = String.valueOf(time)+"_"+fileName;
// 2、使用UUID命名文件
String currentFileName = UUID.randomUUID().toString()+"_"+fileName;
// 使用日期命名文件夹
// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// String datePatten = sdf.format(new Date());
// 使用UUID的hashcode命名文件夹
int code = UUID.randomUUID().hashCode();
int dir1 = 0xf&code;
int dir2 = (0xf0&code)>>4;
// String fullPath = basePath+File.separator+datePatten+File.separator+currentFileName;
String fullPath = basePath+File.separator+dir1+File.separator+dir2+File.separator+currentFileName;
// 使用绝对路径构建一个文件
File file = new File(fullPath);
if (!file.exists()) {
file.getParentFile().mkdirs();
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// file.mkdirs();
}
return fullPath;
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}