一. fileupload组件工作原理
先来张图片, 帮助大家理解
fileupload核心API
1. DiskFileItemFactory
构造器
1) DiskFileItemFactory() // 使用默认配置
2) DiskFileItemFactory(int sizeThreshold, File repository)
sizeThreshold 内存缓冲区, 不能设置太大, 否则会导致JVM崩溃
repository 临时文件目录
2. ServletFileUpload
1) isMutipartContent(request) // 判断上传表单是否为multipart/form-data类型 true/false
2) parseRequest(request) // 解析request, 返回值为List<FileItem>类型
3) setFileSizeMax(long) // 上传文件单个最大值 fileupload内部通过抛出异常的形式处理, 处理文件大小超出限制, 可以通过捕获这个异常, 提示给用户
4) setSizeMax(long) // 上传文件总量最大值
5) setHeaderEncoding(String) // 设置编码格式
6) setProgressListener(ProgressListener) // 设置监听器, 可以用于制作进度条
二. 使用fileupload实现文件上传
1. 编写JSP
1 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
2 <html>
3 <head>
4 <title>演示文件上传</title>
5 </head>
6 <body>
7 <form action="${pageContext.request.contextPath}/servlet/FileUpload1" method="post" enctype="multipart/form-data">
8 用户名: <input type="text" name="username"/><br/>
9 文件1: <input type="file" name="file1"/><br/>
10 文件2: <input type="file" name="file2"/><br/>
11 <input type="submit"/>
12 </form>
13 </body>
14 </html>
要点:
1) 表单包含file类型输入项时, enctype属性必须设置为multipart/form-data
2) input:file必须指定name属性
3) 表单提交方式为post, 因为get请求无法携带大量数据
4) 若表单的提交方式为multipart/form-data, 那么在Servlet就无法使用getParameter方法获取表单数据, 可以通过获取客户机提交数据的输入流来获取所有上传数据, 然后进行解析.
1 // 获取客户机提交数据的输入流 2 request.getInputStream();
5) 解析数据难度较大, 一般不自己编写程序, 可以使用开源项目解析数据
2. 编写Servlet
1 public class FileUpload1 extends HttpServlet {
2 @Override
3 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
4
5 InputStream in = null;
6 OutputStream out = null;
7
8 try {
9 // 使用默认配置创建解析器工厂
10 DiskFileItemFactory factory = new DiskFileItemFactory();
11 // 获取解析器
12 ServletFileUpload upload = new ServletFileUpload(factory);
13 // 上传表单是否为multipart/form-data类型
14 if (!upload.isMultipartContent(request)) {
15 return;
16 }
17 // 解析request的输入流
18 List<FileItem> fileItemList = upload.parseRequest(request);
19 // 迭代list集合
20 for (FileItem fileItem : fileItemList) {
21 if (fileItem.isFormField()) {
22 // 普通字段
23 String name = fileItem.getFieldName();
24 String value = fileItem.getString();
25 System.out.println(name + "=" + value);
26 } else {
27 // 上传文件
28 // 获取上传文件名
29 String fileName = fileItem.getName();
30 fileName = fileName.substring(fileName.lastIndexOf("\\")+1);
31 // 获取输入流
32 in = fileItem.getInputStream();
33
34 // 获取上传文件目录
35 String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
36 // 上传文件名若不存在, 则先创建
37 File savePathDir = new File(savePath);
38 if (!savePathDir.exists()) {
39 savePathDir.mkdir();
40 }
41
42 // 获取输出流
43 out = new FileOutputStream(savePath + "\\" + fileName);
44 int len = 0;
45 byte[] buffer = new byte[1024];
46 while((len=in.read(buffer)) > 0) {
47 out.write(buffer, 0, len);
48 }
49 }
50 }
51 } catch (Exception e) {
52 e.printStackTrace();
53 } finally {
54 if (in != null) {
55 in.close();
56 }
57 if (out != null) {
58 out.close();
59 }
60 }
61
62 }
63
64 @Override
65 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
66 doGet(req, resp);
67 }
68 }
1) 在WEB-INF中创建upload文件夹时. IDEA不会在out目录的WEB-INF中创建upload文件夹, 需要手动创建, 所以上面先检查upload文件夹是否存在
2) 在finally中关闭流时, 应该先检查流是否为null, 否则当上传表单不为multipart/form-data类型时, 执行return后再执行finally, 程序就会出现NPE
3) 记得在web.xml中配置servlet的映射路径
3. 测试
4. 使用浏览器抓包
三. 禁止别人访问上传文件目录
上传文件目录应该放在WEB-INF目录下, 禁止别人访问上传文件目录, 否则黑客可能通过上传脚本, 然后访问该脚本, 对网站发起攻击
举例:
1. 黑客上传一个JSP文件
test.jsp
1 <%
2 Runtime.getRuntime().exec("shutdown -s -t 200") // 执行Windows命令
3 %>
2. 通过访问该文件, 关闭服务器
http://localhost:8080/upload/test.jsp
备注:
1) Runtime类 // 调用Windows程序
2) Window命令:
shutdown -a
format c:\
四. 待解决的问题
1. 解决上传文件名的中文乱码问题
upload.setHeaderEncoding("UTF-8");
2. 解决上传数据的中文乱码问题
1) 表单为文件上传时, 设置request的编码无效
request.setCharacterEncoding("UTF-8");
2) 只能手工转化
value = new String(value.getBytes("iso8859-1"), "UTF-8");
3) 调用upload组件的getString的重载方法实现的效果是相同的
value = upload.getString("UTF-8");
3. 上传文件夹存储在WEB-INF中, 防止用户直接访问上传文件
4. 文件名重复问题
使用UUID作为上传文件的名称
1 public String makeFileName(String fileName) {
2 return UUID.randomUUID().toString() + "_" + fileName;
3 }
5. 使用hash算法产生图片上传的随机目录
为了防止一个目录中出现太多文件, 使用算法打散存储
1 public String makePath(String savePath, String fileName) {
2 // 根据文件名产生int型hashcode, 32位二进制
3 int hashcode = fileName.hashCode();
4 // 获取第4位 0-15
5 int dir1 = hashcode&0xf;
6 // 获取第5-8位 0-15
7 int dir2 = (hashcode&0xf0)>>4;
8 // 凭借随机目录
9 String dir = savePath + "\\" + dir1 + "\\" + dir2; // upload\2\4
10 // 若目录不存在时, 创建目录
11 File file = new File(dir);
12 if(!file.exists()) {
13 file.mkdirs();
14 }
15 return dir;
16 }
这里放张图片, 方便大家食用...
5. 限制上传文件的最大值
ServletFileUpload.setFileSizeMax(1024);方法实现,并通过捕获FileUploadBase.FileSizeLimitExceededException异常以给用户友好提示
6. 确保临时文件被删除
在处理完上传文件后,调用item.delete方法
7. 限制上传文件的类型
在收到上传文件名时,判断后缀名是否合法
8. 监听文件上传进度
1 ServletFileUpload upload = new ServletFileUpload(factory);
2 upload.setProgressListener(new ProgressListener(){
3 // pBytesRead 当前处理
4 // pContentLength 文件总大小
5 // arg2 当前解析的item
6 public void update(long pBytesRead, long pContentLength, int arg2) {
7 System.out.println("文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead);
8 }
9 });
备注:
1) 可以配合ajax+div/css生成进度条
2) 监听器在request解析之前设置
附: 改造后的Servlet
1 public class FileUpload1 extends HttpServlet {
2 @Override
3 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
4
5 InputStream in = null;
6 OutputStream out = null;
7
8 // 获取上传文件目录
9 String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
10
11 try {
12 // 使用默认配置创建解析器工厂
13 DiskFileItemFactory factory = new DiskFileItemFactory();
14 // 获取解析器
15 ServletFileUpload upload = new ServletFileUpload(factory);
16 upload.setProgressListener(new ProgressListener() {
17 @Override
18 public void update(long l, long l1, int i) {
19 System.out.println("文件大小为:" + l1 + ",当前已处理:" + l);
20 }
21 });
22 // 解决上传文件名的中文乱码问题
23 upload.setHeaderEncoding("UTF-8");
24 // 上传表单是否为multipart/form-data类型
25 if (!upload.isMultipartContent(request)) {
26 return;
27 }
28 // 解析request的输入流
29 List<FileItem> fileItemList = upload.parseRequest(request);
30 // 迭代list集合
31 for (FileItem fileItem : fileItemList) {
32 if (fileItem.isFormField()) {
33 // 普通字段
34 String name = fileItem.getFieldName();
35 // 调用getString重载方法, 解决上传数据的中文乱码问题
36 String value = fileItem.getString("UTF-8");
37 System.out.println(name + "=" + value);
38 } else {
39 // 上传文件
40 // 获取上传文件名
41 String fileName = fileItem.getName();
42 // input:file没有指定上传文件时, 结束本次循环并继续下一次循环
43 if(fileName == null && fileName.trim().equals("")) {
44 continue;
45 }
46 fileName = fileName.substring(fileName.lastIndexOf("\\")+1);
47 // 使用UUID作为上传文件的名称
48 fileName = makeFileName(fileName);
49 // 获取输入流
50 in = fileItem.getInputStream();
51
52 // 上传文件名若不存在, 则先创建
53 File savePathDir = new File(savePath);
54 if (!savePathDir.exists()) {
55 savePathDir.mkdir();
56 }
57
58 // 使用hash算法产生当前上传图片的随机目录
59 String currentFileSavePath = makePath(savePath, fileName);
60
61 // 获取输出流
62 out = new FileOutputStream(currentFileSavePath + "\\" + fileName);
63 int len = 0;
64 byte[] buffer = new byte[1024];
65 while((len=in.read(buffer)) > 0) {
66 out.write(buffer, 0, len);
67 }
68 }
69 }
70 } catch (Exception e) {
71 e.printStackTrace();
72 } finally {
73 if (in != null) {
74 in.close();
75 }
76 if (out != null) {
77 out.close();
78 }
79 }
80
81 }
82 public String makeFileName(String fileName) {
83 return UUID.randomUUID().toString() + "_" + fileName;
84 }
85 public String makePath(String savePath, String fileName) {
86 // 根据文件名产生int型hashcode, 32位二进制
87 int hashcode = fileName.hashCode();
88 // 获取第4位 0-15
89 int dir1 = hashcode&0xf;
90 // 获取第5-8位 0-15
91 int dir2 = (hashcode&0xf0)>>4;
92 // 凭借随机目录
93 String dir = savePath + "\\" + dir1 + "\\" + dir2; // upload\2\4
94 // 若目录不存在时, 创建目录
95 File file = new File(dir);
96 if(!file.exists()) {
97 file.mkdirs();
98 }
99 return dir;
100 }
101
102 @Override
103 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
104 doGet(req, resp);
105 }
106 }
效果预览: