第17天 笔记 文件上传下载
-
使用过滤器的步骤
-
filter执行顺序如何确定,如果有两个过滤器都拦截同一个请求。
3)Filter接口中的doFilter方法和FilterChain接口中的doFilter的区别
今日任务:
-
文件上传
-
文件下载
课堂笔记
1、文件上传
1.1、文件上传介绍
生活中遇到的文件上传技术实现?
网盘,头像,邮件的附件,朋友圈(日报)
文件上传(B/S)架构和基本原理?
B/S架构是浏览器和服务器架构,浏览器发出请求,服务器给出响应。
C/S架构是客户端和服务器架构,客户端发出请求,服务器给出相应。
历史:以前都是喜欢客户端和服务器,第一,获取用户的计算机数据方便,对用户的控制力强,方便培养用户习惯,自定义网络传输的协议。
客户端,占用计算机内存和硬盘,客户端,使用协议太多,安全性不高,所以诞生浏览器
浏览器设计宗旨:丢弃客户端,占用内存和硬盘,统一协议,安全性高,使用浏览器,只需要更新浏览器即可。
使用B/S架构来做文件上传的思路?
-
给用户一个功能,让他自己可以去选择要上传的文件
-
需要让用户可以发送当前被上传文件的数据,而且,还要告诉服务器,这是文件上传的请求
-
接收请求,使用IO技术读取请求中被上传文件的数据
-
将数据保存到服务器的硬盘上。
1.2、文件上传页面实现和注意细节(重点:必须掌握)
第一个问题:如何告诉浏览器,让用户选择要上传的文件?
<!-- 演示在页面中显示选择文件上传的按钮 -->
请选择要被上传的文件:<input type="file" name="upload">
第二问题:如何提交数据?
GET请求:GET请求数据长度有限制(1KB),所以不能用。
POST请求:就用这个,这个好,数据长度没有限制。
使用POST请求,一般是用form表单,设置请求方式为POST
第三个问题:如何通知服务器,这是一个文件上传的请求?
设置enctype(这是form表单中属性)的值为:multipart/form-data
思考:
文件上传的时候,我们数据,都是二进制的数据,不应该被编码
普通的请求,数据到服务器之后,我们需要的都是文本内容,需要被存入数据库,所以需要编码
MIME类型:文件的媒体类型,.Jpg .avi .rmvb .mp4 .3gp
现在很多视频软件,它的后缀名都是当前软件名称,爱奇艺,.aqy .itcast
我的视频,提供下载功能,也只能用自己的播放器看。
浏览器代码实现:
<!-- 设置一个form表单,设置请求方式为post,保证发送数据没有长度限制 -->
<!-- 设置enctype="multipart/form-data" 用来提示当前是文件上传请求 -->
<form action="${root }/upload1" method="post" enctype="multipart/form-data">
<!-- 设置一个按钮,让用户可以选择要被上传的文件 -->
请选择要上传的文件:<input type="file" name="upload">
<input type="submit" value="确认上传">
</form>
小结(文件上传浏览器端实现):
-
设置一个<input type="file" name="upload">,让用户去选择要被上传的文件
-
设置一个form表单,method="POST"(让文件上传数据长度没有限制) ,enctype=" multipart/form-data"(告诉服务器这是一个文件上传的请求,数据不要编码)
-
设置一个提交按钮,提交请求。
文件上传浏览器效果:
1.3、文件上传的服务器简单实现
上传的数据在哪里?
浏览器的请求发送过来之后,服务器创建一个请求对象,将所有的请求数据,都封装在request对象中,
所以文件上传的数据,肯定在request对象中。
文件上传代码示例:
package cn.itcast.upload;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UploadServelt1 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//文件上传请求的数据,肯定在request
//第一步:获取上传的请求流
ServletInputStream in = request.getInputStream();
//第二步:将数据打印在控制台上
int temp = 0;
while((temp = in.read()) != -1){
System.out.write(temp);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
基本的IO操作:
File : 文件和文件夹的操作
Inputstream : 读取文件数据
Outputstream:输出数据
文件上传原理分析:
-----------------------------15767295007281,一个数据的分隔符,
这个分隔符,将不同input输入框中的数据,进行分隔。
text/plain enctype(MIME类型),都是指文件的媒体类型
.avi .mp3 .txt .jpg 以后缀名的方式显示的媒体格式;
后缀名对应的文件媒体格式,text/plain这样的方式描述,如果需要找到后缀名相关的文件媒体格式
注意:文件名的后缀名和文件的媒体格式的对应关系
可以通过tomcat服务器目录下的conf文件夹中的web.xml文件来查询,查询搜索mime-mapping就可以了
重点:被上传文件的真实数据,其实都在text/plain这一行后,隔一行开始,一直到分隔符结束。
2、Apache的commons-fileupload工具jar包介绍
2.1、commons-fileupload介绍
下载:http://commons.apache.org/proper/commons-fileupload/
需要使用fileupload组件完成文件上传,这时还需要下载commons-io 的jar包。
解压,把jar包复制到项目中。
flieUpload工具的目录结构:
2.2、fileupload的文件上传解析流程图
因为,文件上传,必须,完成所有步骤才能看到效果,所以,先完成文件上传,然后,分许其中的对象和步骤。
普通字段: <input type="text" name="desc">——普通文本框
上传文件1:<input type="file" name="upload">——上传文件的组件
上传文件2:<input type="file" name="upload">——上传文件的组件
ServletFIleUpload: 它是解析文件上传数据的核心对象
DiskFileItemFactory: 它是帮助ServletFIleUpload一起解析request的对象,作用:将所有input输入框中的数据,解析,成一个一个的FileItem对象
FileItem:封装了所有input输入框中数据的对象
以上三个对象,是我们核心对象。
过程:
-
创建一个DiskFileItemFactory,将这个对象绑定到ServletFIleUpload
-
使用ServletFIleUpload对象,解析request
-
解析完request,获取的是一个一个的fileItem对象(input输入框中内容)
2.3、fileupload的实现演示(重点:必须掌握=必须自己代码写出来)
package cn.itcast.web;
import java.io.File;
import java.io.IOException;
import java.util.List;
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.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
public class UploadServlet2 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//需求:完成文件上传
//1) 创建一个DiskFileItemFactory,将这个对象绑定到ServletFIleUpload
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload fileUpload = new ServletFileUpload(factory);
//2) 使用ServletFIleUpload对象,解析request
try {
//3) 解析完request,获取的是一个一个的fileItem对象(input输入框中内容)
//解析动作已经完成。
List<FileItem> list = fileUpload.parseRequest(request);
//4)遍历循环list集合,根据不同fileItem对象,不同处理
for (FileItem item : list) {
//item.isFormField() 判断当前是否是一个普通字段:true,是 false 不是
if(item.isFormField()){
//表示普通字段
System.out.println(item.getFieldName());
System.out.println(item.getString());
}else{
//表示文件上传
//5)将被上传的文件数据,保存到服务器的硬盘上
//将被上传的文件统一保存在upload文件夹中
//C:/apache-tomcat-7.0.52/webapps/day17/upload
String realPath = this.getServletContext().getRealPath("/upload");
//6)获取文件名
String fileName = item.getName();
File file = new File(realPath,fileName);
try {
item.write(file);
} catch (Exception e) {
e.printStackTrace();
}
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
3、commons-fileupload 详解(对核心类的API介绍)
3.1、DiskFileItemFactory类(磁盘的文件项工厂类)
3.1.1、设置缓存大小
方法截图:
为什么要设置缓存大小?
缓存越大,效率越高。
缓存:内存中开辟的一个存储数据的空间
内存读取效率高于硬盘,设置缓存,提高读取效率,缓存越大,效率越高。
代码操作:
-
设置临时文件存储位置
方法截图:
为什么要设置临时文件存储位置?
被上传文件的大小,一定会出现超出缓存大小的情况,这些超出的数据,也需要找一个地方存储(临时)起来。
相当于:卡车拉了一堆砖头,需要一个临时的场地,存放这些砖头,为了后期,在建筑房子(形成文件),使用这些砖头。
总结:1)数据必然会超出缓存
2)数据必须在形成文件之前临时存储
代码操作:
如何获取当前操作系统的临时文件存储位置?
System类
代码演示:
package cn.itcast.utils;
public class TestUtils {
public static void main(String[] args) {
//C:UserswjnAppDataLocalTemp
System.out.println(System.getProperty("java.io.tmpdir"));
}
}
3.2、ServletFileUpload类
3.2.1、parseRequest方法
方法截图:
定义:解析request对象,获取文件上传的数据的方法
代码操作:
3.2.2、isMultipartContent方法
方法截图:
定义:判断当前请求,是否,是一个文件上传的请求
代码操作:
//如果当前请求,不是文件上传
boolean b = ServletFileUpload.isMultipartContent(request);
//b true:表示是文件上传,false:不是文件上传
if(!b){
//不是文件上传
response.sendRedirect(request.getContextPath());
return;
}
3.2.3、setHeaderEncoding方法
方法截图:
定义:解决中文文件名乱码问题
代码操作:
3.3、FileItem类
3.3.1、isFormField方法
方法截图:
定义:判断当前input输入框是否是一个普通字段,true,是,false,不是
代码操作:
3.3.2、getFieldName方法
方法截图:
定义:获取input输入框中name属性值
代码操作:
3.3.3、getString方法
方法截图:
定义:获取input输入框中,用户输入的值(value值)的方法
代码操作:
3.3.4、getName方法
方法截图:
定义:获取被上传文件的文件名
代码操作:
3.3.5、getInputStream方法(不推荐使用)
方法截图:
定义:
为什么不推荐使用getInputStream?
如果使用这个方法,先获取输入流,在获取输出流,继续写IO代码,代码太过繁琐,推荐使用原有的方法
item.write(new File(realPath,fileName));
代码操作:
//获取输入流
// InputStream in = item.getInputStream();
// //获取输出流
// FileOutputStream out = new FileOutputStream(new File(realPath,fileName));
// //设置缓存区
// byte[] buf = new byte[1024];
// int len = 0;
// while( (len = in.read(buf)) != -1 ){
// out.write(buf,0,len);
// }
3.3.6、delete方法
方法截图:
定义:删除临时文件的方法
临时文件会自动删除(item.write()),但是,如果发生异常,临时文件有可能没有删出,调用删除方法
代码操作:
4、多文件上传
4.1、浏览器端js实现
需求:让用户自己决定上传文件的input输入框的个数
思路:
-
提供一个功能给用户,让用户可以自己去添加input输入框
-
提供一个功能给用户,让用户可以自己去删除input输入框
-
提交数据按钮
实现:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="root" value="${pageContext.request.contextPath }"/>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>My JSP 'index.jsp' starting page</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<script type="text/javascript">
//用户自己添加input,用户点击一下,创建一个input
function _addInput(){
//创建一个input输入框
var _input = document.createElement("input");
//设置文件上传相关属性
_input.setAttribute("type", "file");
_input.setAttribute("name", "upload");
//将input标签存入div中(id="_un")
var _div = document.getElementById("_un");
_div.appendChild(_input);
//再提供一个删除的按钮:点击一个删除按钮,删除一个input输入框
var _delete = document.createElement("input");
//设置删除按钮的相关属性
_delete.setAttribute("type", "button");
_delete.setAttribute("value", "删除");
//设置删除按钮的onclick事件,事件(启动一个js函数,执行删除操作)
_delete.onclick = function(){
//执行删除的操作
//删除的内容有:文件上传输入框,删除按钮本身,换行符
//删除操作由父标签执行
//this:代表了当前这个标签对象_delete
var _f = this.parentNode;
//删除文件上传输入框,先获取当前元素的前一个元素
var _p = this.previousSibling;
_f.removeChild(_p);
//删除换行符,先获取当前元素的后一个元素
var _n = this.nextSibling;
_f.removeChild(_n);
//自杀
_f.removeChild(this);
};
_div.appendChild(_delete);
//在每一个input输入框之后,加上一个换行符
var _br = document.createElement("br");
_div.appendChild(_br);
}
</script>
</head>
<body>
<!-- 设置一个按钮,用来启动js函数,创建文件上传的input输入框 -->
<input type="button" value="添加文件上传" onclick="_addInput();"><br/>
<form action="${root }/upload3" method="post" enctype="multipart/form-data">
文件描述:<input type="text" name="desc"><br/>
<!-- 演示在页面中显示选择文件上传的按钮 -->
请选择要被上传的文件:<input type="file" name="upload"><br/>
<!-- 当前这个div的位置,就是input标签要存放的位置,那么input标签,只要存放在div中,就是放对了位置 -->
<div id="_un">
</div>
<!-- 不要忘了添加表单的提交按钮 -->
<input type="submit" value="文件上传">
</form>
</body>
</html>
在js变量命名时,加上下划线,是为了防止和js中本身全局变量或者函数起冲突。
4.2多文件上传问题
4.2.1、文件重名
重名的原因:
每次上传文件的时候,因为文件名相同,所以后上传的文件覆盖前一个文件。
解决方案(学生讨论15分钟,每个小组尽可能多的提供解决方案):
方案一:用户ip-文件名-(1).zip
文件名命名方式:使用用户id 加上 文件名 加上数字(文件出现多少次,数据就是几)
用户名如果保证用户名,每个都不同
IP 不是每个用户都有一个IP的,在局域网范围内,对外的IP是同一个。
如果有重复的文件名,在原来的文件名上,加上数字,每出现一次,加一。
不能擅自压缩用户的数据,那么用户重新从网盘下载数据的时候,发现文件后缀名不同。
方案二:添加当前文件的最后修改时间,文件的大小,文件前几KB的数据
方案三:UUID,可以解决
方案四:文件绝对路径+用户名 进行拼接 作为新的文件名
不可取,绝对路径会出现重复,而且文件上传只有IE Edge浏览器上传文件,才会带上绝对路径。
上传文件的时候,不适合将文件的绝对路径显示出来,不太安全。
方案五:判断文件名是否相同 如果相同添加数字区别
场景:当前upload文件夹中,有10W文件,那么每做一次文件上传,都要进行10W次循环和判断,这样非常消耗系统性能,这个方案理论成立,实际执行,有问题
方案六:mac加时间
Mac:所有电子设备的物理地址,理论上,每个设备都是唯一的,但是,会出现修改地址的情况
注意:修改文件名的权限,应该,保存在服务器中,不应该将权限放给用户。
用户的唯一性识别:保证使用当前手机的是一个真实的人。保证mac地址是可以获取的(保证非法用户无法通过购买大两手机号进行注册),也不行,安卓或者ios的模拟器
方案七:毫秒值
双十一:一秒钟处理订单量,上百万的,同理,如果出现极端情况,在同一毫秒也会出现同名文件上传,那么也会出现问题。
注意:在考虑制作系统功能的时候,有时,需要考虑极端情况。
方案八:hashcode
同名的文件,他们的文件名计算出来的hashcode,应该是同样的,方案不可取。
方案九:随机数
比如:十个随机数,执行11次,就可能出现重复,所以不可取。
方案十:上传检查,如果重复,则返回,让用户重命名
用户操作复杂,不可取。
文件上传,先检查文件名,如果重复,让用户重写书写文件(注意:这个时候,让用户重命名,一定要给出明确的提示)。
例如:注册游戏,张三已经被注册,提示用户如果你要注册一个张三,(你可以用户名变成张三2333),
方案十一:不作判断,所有文件都加
每个文件上传后,获取文件名,然后直接添加一串数字(每次文件上传完成之后,数字加一)
嘿嘿嘿1.txt
嘿嘿嘿2.txt
嘿嘿嘿3.txt
BigInteger
需要数据的字段长度,可变(动态改变)
注意:在数据存储的时候,要注意当前数据不能超过,数据库字段的长度限制
注意:每次文件上传完成之后,数字加一,需要服务器之间数据同步,不然,肯定会出现重复
重名问题解决方案
UUID.randomUUID()这个方法就会生成一个唯一的字符串标识,不会重复。可以作为我们文件名使用。
代码演示:
4.3、目录分离
目录分离需求:
因为用户上传的文件,日积月累,文件量太大,导致打开一个文件夹的时候,需要时间太长
所以,我们需要将文件存放在不同的文件夹中。
场景:比如一个文件夹中,有1个文件,这时打开这个文件夹,需要一秒钟
比如一个文件夹中,有10W个文件,这时打开这个文件夹,需要10W秒钟
将10w文件存入1000个文件夹中,打开文件夹的速度,变快了
4.4、目录分离方案解决
1)每一个用户有一个专属的文件夹
2)每一个用户一个专属文件夹的基础上,每天生成一个文件夹,用来存当天上传的文件
先是有一个张三专属的文件夹,然后,每天创建一个当前的文件夹,专门用来存储,当天上传的文件。
方案二:由于现在网速的提升,特别是百兆千兆光纤进入普通用户,现在一天上传的文件量,非常大。
3)使用文件名的哈希值,来计算当前文件应该存储的位置
生成目录的工具类:
package cn.itcast.utils;
public class DirUtils {
//根据文件名的哈希值,计算文件该存储的位置
public static String getDir(String fileName){
//获取哈希值
int hashCode = fileName.hashCode();
//开始计算
//int类型数据,4个字节,32位,每四位获取一个数字,可以获取8个数字
//这8个数字,都,作为当前文件要存贮的目录
//获取当前哈希值最低4位,作为一级目录
int dir1 = hashCode & 15;
//将哈希值,向右移动4位,更新最低四位
hashCode = hashCode >> 4;
//获取第二级目录
int dir2 = hashCode & 15;
return "/"+dir1+"/"+dir2;
}
}
文件上传servlet修改:
//修改被上传文件应该储存的位置
//c:tomcat服务器webappsitcast-upload\upload12
realPath = realPath + DirUtils.getDir(fileName);
//在文件上传之前,先判断当前目录是否存在
File file = new File(realPath);
if(file.exists()){
//什么事情都不用干
}else{
//不存在,创建目录
file.mkdirs();
}
5、下载介绍
5.1、超链接下载方式
文件的下载有2种方式可以完成:
1、超链接形式:
<a href="资源的路径" >资源名称</a>
问题:
图片和文本资源会直接被解析到浏览器中,不是文件下载。
中文的文件名点击之后,是404,找不到,中文的文件名被url编码过了导致访问资源路径出错。
2、使用Servlet通知浏览器下载:
1)浏览器发送请求给服务器(用户要下载的文件名)
2)服务器加载用户要下载的文件数据。
3)通知浏览器以下载的方式请求资源
4)使用IO技术,将数据发送(使用response对象发送数据)
小结:
-
先修改页面——发送要下载的文件的文件名
-
定位要被下载的文件的位置(获取文件名的时候,处理中文乱码问题)
-
设置文件的媒体格式
-
解决中文文件名问题(先要判断浏览器,根据不同的浏览器,不同编码处理)
-
设置要被下载的文件名
-
使用IO技术将数据发送给浏览器(response获取的out输出流,不用手动关闭,自己使用的输入流,需要手动关闭)
5.2、服务器端写程序下载(重点:必须掌握)
package cn.itcast.servlet;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sun.misc.BASE64Encoder;
public class DownloadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//服务器加载用户要下载的文件数据。
String realPath = this.getServletContext().getRealPath("/upload");
//获取当前用户需要下载的文件的文件名
String parameter = request.getParameter("fileName");
//处理文件名的中文问题
String fileName = new String(parameter.getBytes("iso-8859-1"),"utf-8");
//加载数据
FileInputStream in = new FileInputStream(new File(realPath,fileName));
//设置文件的媒体格式
response.setContentType(getServletContext().getMimeType(fileName));
//中文的文件名在发送给浏览器之前,需要编码
//解决编码之前,需要判断浏览器
String header = request.getHeader("User-Agent");
if(header.contains("Firefox")){
//当前浏览器是火狐浏览器,使用base64编码
BASE64Encoder base64Encoder = new BASE64Encoder();
fileName = "=?utf-8?B?" + base64Encoder.encode(fileName.getBytes("utf-8")) + "?=";
}else{
//其他浏览器,使用URLEncoder编码
fileName = URLEncoder.encode(fileName,"utf-8");
}
//设置要被下载的文件名
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
//使用IO技术将数据发送给浏览器
//使用response向浏览器发送响应
ServletOutputStream out = response.getOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = in.read(buf)) != -1){
out.write(buf, 0, len);
}
in.close();
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
响应消息头设置:
Content-Type 设置文件媒体格式
response.setContentType(getServletContext().getMimeType(filename));
Content-Disposition 设置要被下载的文件名
response.setHeader("Content-Disposition", "attachment;filename=" + filename);
谷歌浏览器(和主流其他浏览器)中文乱码处理:
fileName = URLEncoder.encode(fileName,"utf-8");
火狐下载文件名乱码处理:(先判断是否是火狐浏览器)
BASE64Encoder base64Encoder = new BASE64Encoder();
filename = "=?utf-8?B?" + base64Encoder.encode(filename.getBytes("utf-8")) + "?=";
注意:如果无法使用BASE64Encoder这个类(无法导入这个包),按一下方式解决。
解决方法:按照如下方法设置Eclipse导入%JAVA_HOME%jrelib目录下的rt.jar包即可,Project->Properties,选择Java Build Path设置项,再选择Libraries标签,Add External Jars添加%JAVA_HOME%jrelib t.jar就可以使用
如果还是不行,可以再修改项目的JDK,将JDK版本改成本地安装的JDK版本
如果没有自己的JDK:
6.作业:
1)使用fileupload完成文件上传功能(40点积分)
2)使用js实现多文件上传功能(20点积分)
3)解决文件重名问题(10点积分)
4)解决文件目录分离问题(10点积分)
5)完成文件下载功能(10点积分)
6)解决文件下载中文乱码问题(10点积分)