11-Upload&Download
文件上传#
开发步骤#
1. 提供表单,允许用户通过表单选择文件进行上传
- 表单必须是 POST 提交(表单默认为 GET 提交,请求参数不能超过 1KB)
- 表单输入项必须有 name 属性(虽然文件表单输入项的name到后台没啥用),一个表单输入项如果没有 name 属性,Browser 是不会把它当作请求参数提交的。示例:
<input type="file" name="file1"/>
<form>
加上enctype="multipart/form-data"
属性
2. 在 Servlet 中将上传的文件保存在 sever 的硬盘中
- 由于没有提供原生 API,需要自己手动实现:先用
request.getInputStream()
调用该方法获取包含请求正文的 ServletInputStream 对象,然后一大堆步骤,比如要拿到分隔符,然后分割请求正文 .... 很麻烦。所以,功能实现需要基于 apache 提供的 jar:commons-fileupload-1.2.1.jar 和 commons-io-1.4.jar。 - 工作流程图示:
- 对照图大致理解下代码:
// 创建工厂 DiskFileItemFactory factory = new DiskFileItemFactory(); // 生产文件上传核心类 ServletFileUpload fileUpload = new ServletFileUpload(factory); // 利用文件上传核心类解析request List<FileItem> list = fileUpload.parseRequest(request); // 遍历所有的FileItem for(FileItem item : list) { if(item.isFormField()) { // 当前是一个普通的字段项 String name = item.getFieldName(); String value = item.getString(); System.out.println(name+" = "+value); } else { // 当前是一个文件上传项 String fileName = item.getName(); InputStream in = item.getInputStream(); OutputStream out = new FileOutputStream( getServletContext().getRealPath("upload/"+fileName)); IOUtils.transfer(in, out); IOUtils.close(in,out); } }
相关 API#
- DiskFileItemFactory
DiskFileItemFactory(int sizeThreshold, File repository) DiskFileItemFactory() void setSizeThreshold(int sizeThreshold) 设定内存缓冲区大小 [默认10KB] void setRepository(File repository) 设定临时文件夹大小 [默认System.getProperty("java.io.tmpdir")]
- ServletFileUpload
static boolean isMultipartContent(HttpServletRequest request) 判断上传表单是否为 multipart/form-data 类型 List parseRequest(HttpServletRequest request) 解析 request 对象,并把表单中的每一个输入项包装成一个FileItem 对象 并返回一个保存了所有 FileItem 的 List setFileSizeMax(long fileSizeMax) 设置单个上传文件的最大值;超过阈值抛 FileSizeLimitExceededException setSizeMax(long sizeMax) 设置上传文件总量的最大值 setHeaderEncoding(String encoding) 设置编码格式,解决上传文件名乱码问题 setProgressListener(ProgressListener pListener) 实时监听文件上传状态 (注册监听要放在解析request之前进行!)
- FileItem
boolean isFormField() 判断FileItem是一个文件上传对象还是普通表单对象 > 如果判断是一个普通表单对象 String getFieldName() 获得普通表单对象的name属性 String getString(String encoding) 获得普通表单对象的value属性,可用形参来解决乱码问题 > 如果判断是一个文件上传对象 String getName() 获得上传文件的文件名 InputStream getInputStream() 获得上传文件的输入流 void delete() 在关闭 FileItem 输入流后,删除临时文件
JS 实现多文件上传#
每次动态增加一个文件上传输入框,都把它和删除按纽放置在一个单独的 <div>
中,并对删除按纽的 onclick 事件进行响应,使之删除删除按纽所在的 <div>
。
moreUpload.jsp
<html>
<head>
<title>多文件上传</title>
<script>
function addOne() {
var fdiv = document.getElementById("fdiv");
fdiv.innerHTML += "<div><input type='file' name='file' />"
+ "<input type='button' id='delBtn' onclick='delOne(this)'"
+ "value='删除'/><br></div>";
}
function delOne(btn) {
btn.parentNode.parentNode.removeChild(btn.parentNode);
}
</script>
</head>
<body>
<h1>文件上传</h1>
<input type="button" id="addBtn" onclick="addOne()" value="加一个"/>
<form action="${pageContext.request.contextPath }/servlet/UploadServlet2"
method="POST" enctype="multipart/form-data">
描述信息1: <input type="text" name="desc1" />
描述信息2: <input type="text" name="desc2" />
<div id="fdiv"></div>
<input type="submit" value="提交" />
</form>
</body>
</html>
上传文件保存#
- 文件名
为防止多用户上传相同文件名的文件,而导致文件覆盖的情况发生,文件上传程序应保证上传文件具有唯一文件名。可以使用一个表示通用唯一标识符 (UUID) 的类。 UUID 表示一个 128 位的值。static UUID randomUUID() 生成一个不重复的 128 位的二进制 public String toString() 128 位二进制 -> 32 位十六进制
- 文件的存储结构
为防止单个目录下文件过多,影响文件读写速度,处理上传文件的程序应根据可能的文件上传总量,选择合适的目录结构生成算法,将上传文件分散存储(比如,根据 hash 值来分目录存储)。 - 文件在web应用中的存放路径
为保证服务器安全,上传文件应保存在应用程序的 WEB-INF 目录下,或者不受 web 服务器管理的目录。防止用户上传 JSP恶意入侵或访问其他用户上传的资源
一个较完整的文件上传代码:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
try {
// 检查表单格式是否正确
if(!ServletFileUpload.isMultipartContent(request))
throw new RuntimeException("请用正确的表单格式上传");
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(100*1024);
factory.setRepository(new File(getServletContext().getRealPath("WEB-INF/temp")));
ServletFileUpload fileUpload = new ServletFileUpload(factory);
// 设置单个文件的最大值
fileUpload.setFileSizeMax(100*1024);
// 设置上传文件总量的最大值
fileUpload.setSizeMax(200*1024);
// 设置编码集(解决上传文件名乱码问题)
fileUpload.setHeaderEncoding("utf-8");
// 设置文件上传监听
// fileUpload.setProgressListener(new ProgressListener() {...});
List<FileItem> list = fileUpload.parseRequest(request);
for(FileItem item : list)
if(item.isFormField()) {
String name = item.getFieldName();
String value = item.getString("utf-8");// 解决请求参数乱码
System.out.println(name+" = "+value);
} else {
// ==== 存储 [上传文件] 前的准备工作 ====
String fileName = item.getName();
String uuidName = UUID.randomUUID().toString()+"_"+fileName;
// 根据 hash 值实现分目录存储
int hash = uuidName.hashCode();
String hashStr = Integer.toHexString(hash);
// 每一位代表一级目录, 一共可以有: 16^8 = 4294967296
char[] dirArr = hashStr.toCharArray();
String path = getServletContext().getRealPath("/WEB-INF/upload");
for(char c : dirArr) path += "/"+c;
// 系统若找不到指定目录,就应该先创建出这个层级目录
new File(path).mkdirs();
// ==== 存储 [上传文件] ====
InputStream in = item.getInputStream();
OutputStream out = new FileOutputStream(new File(path, uuidName));
IOUtils.transfer(in, out);
IOUtils.close(in, out);
item.delete();
}
} catch (FileSizeLimitExceededException e) {
response.getWriter().write("单个文件不超过10M, 总大小不超过100M");
} catch (FileUploadException e) {
e.printStackTrace();
}
}
文件上传监视#
fileUpload.setProgressListener(new ProgressListener() {
Long beginTime = System.currentTimeMillis();
@Override
public void update(long pBytesRead, long pContentLength, int pItems) {
BigDecimal br = new BigDecimal(pBytesRead).
divide(new BigDecimal(1024), 2,BigDecimal.ROUND_HALF_UP);
BigDecimal cl = new BigDecimal(pContentLength)
.divide(new BigDecimal(1024), 2,BigDecimal.ROUND_HALF_UP);
System.out.print("当前读取的是第"+pItems+"个field,总大小是"+cl+"KB,正在读取"+br+"KB,");
// 剩余字节数
BigDecimal rb = cl.subtract(br);
System.out.print("剩余"+rb+"KB,");
// 上传百分比
// BigDecimal per = br.divide(cl,4,BigDecimal.ROUND_HALF_UP)
.multiply(new BigDecimal(100));
// 结果成了:85.7600 应该先乘以100
BigDecimal per = br.multiply(new BigDecimal(100)
.divide(cl,4,BigDecimal.ROUND_HALF_UP));
System.out.print("已经完成"+per+"%,");
// 上传用时
Long nowTime = System.currentTimeMillis();
Long useTime = (nowTime - beginTime)/1000;
System.out.print("已经用时"+useTime+"秒,");
// 上传速度
BigDecimal speed = new BigDecimal(0);
if(useTime != 0)
speed = br.divide(new BigDecimal(useTime),2,BigDecimal.ROUND_HALF_UP);
System.out.print("上传速度为"+speed+"KB/s,");
// 大致剩余时间
BigDecimal rt = new BigDecimal(0);
if(!speed.equals(new BigDecimal(0)))
rt = rb.divide(speed,0,BigDecimal.ROUND_HALF_UP);
System.out.println("剩余时间"+rt+"秒");
}
});
文件下载#
public class DownloadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String fileName = request.getParameter("file");
// 通知 Browser 以附件形式打开
response.setHeader("Content-Disposition"
, "attachment;filename=" + URLEncoder.encode(fileName,"utf-8"));
// 通知 Browser 发送的是什么格式的数据
response.setContentType(getServletContext().getMimeType(fileName)); // MIME类型
InputStream in = new FileInputStream(getServletContext().getRealPath(fileName));
OutputStream out = response.getOutputStream();
IOUtils.transfer(in, out);
IOUtils.close(in, out);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
- 发送响应头 Content-Disposition
- 文件名 URL 编码
- 设置 MIME 类型
exer:网盘#
功能分析#
- index.jsp:提供 [上传]、[下载列表]
- upload.jsp:提供上传表单,允许用户选择文件进行上传
- UploadServlet:① 保存上传的文件到服务器;② 在 DB 中保存文件相关信息
- DownloadListServlet:查询数据库,列出所有可供下载的资源信息,存入 request 域后转发到 downloadList.jsp 做显示
- downloadList.jsp:遍历 request 域中所有资源信息,提供下载链接
- DownloadServlet:下载指定 id 的资源
代码实现#
Resource
public class Resource implements Serializable {
private int id;
private String uuidName;
private String realName;
private String savePath;
private String uploadTime;
private String description;
private String ip;
...
}
UploadServlet
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!ServletFileUpload.isMultipartContent(request))
throw new RuntimeException("请使用正确的表单格式上传文件");
String upload = getServletContext().getRealPath("WEB-INF/upload");
String temp = getServletContext().getRealPath("WEB-INF/temp");
Map<String,String> paramMap = new HashMap<String,String>();
paramMap.put("ip", request.getRemoteAddr());
// 封装上传文件
try {
DiskFileItemFactory factory = new DiskFileItemFactory(1024,new File(temp));
ServletFileUpload fileUpload = new ServletFileUpload(factory);
fileUpload.setHeaderEncoding("utf-8");
fileUpload.setFileSizeMax(100*1024*1024);
List<FileItem> list = fileUpload.parseRequest(request);
for(FileItem item : list) {
if(item.isFormField()) {
String name = item.getFieldName();
String value = item.getString("utf-8");
paramMap.put(name, value);
} else {
String realName = item.getName();
paramMap.put("realName", realName);
InputStream in = item.getInputStream();
String uuidName = UUID.randomUUID().toString()+"_"+realName;
paramMap.put("uuidName", uuidName);
int hash = uuidName.hashCode();
char[] dirArr = Integer.toHexString(hash).toCharArray();
String savePath = "/WEB-INF/upload";
for(char c : dirArr) {
upload += "/"+c;
savePath += "/"+c;
}
paramMap.put("savePath", savePath);
new File(upload).mkdirs();
OutputStream out = new FileOutputStream(new File(upload, uuidName));
IOUtils.transfer(in, out);
IOUtils.close(in, out);
item.delete();
}
}
// 向数据库插入数据
Resource r = new Resource();
BeanUtils.populate(r, paramMap);
String sql = "insert into netdisk values(null, ?, ?, ?, null, ?, ?)";
QueryRunner runner = new QueryRunner(DaoUtils.getSource());
runner.update(sql, r.getUuidName(), r.getRealName()
, r.getSavePath(), r.getDescription(), r.getIp());
// 重定向回主页
response.sendRedirect(request.getContextPath());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
DownloadListServlet
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 找出数据库中所有可供下载的资源信息
String sql = "select * from netdisk";
QueryRunner runner = new QueryRunner(DaoUtils.getSource());
List<Resource> list = null;
try {
list = runner.query(sql, new BeanListHandler<Resource>(Resource.class));
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// 存入 request 域中,带到 downloadList.jsp 做展示
request.setAttribute("downloadList", list);
request.getRequestDispatcher("/downloadList.jsp").forward(request, response);
}
downloadList.jsp
<body>
<div align="center">
<h1>资源下载列表</h1>
<table border="1">
<tr>
<th>文件名称</th>
<th>上传时间</th>
<th>上传者IP</th>
<th>描述信息</th>
<th>可选操作</th>
</tr>
<c:forEach items="${requestScope.downloadList }" var="resource">
<tr>
<td><c:out value="${resource.realName }" /></td>
<td><c:out value="${resource.uploadTime }" /></td>
<td><c:out value="${resource.ip }" /></td>
<td><c:out value="${resource.description }" /></td>
<td><a href="${pageContext.request.contextPath }
/servlet/DownloadServlet?id=${resource.id }">下载</a></td>
</tr>
</c:forEach>
</table>
</div>
</body>
DownloadServlet
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String id = request.getParameter("id");
String sql = "select * from netdisk where id = ?";
QueryRunner runner = new QueryRunner(DaoUtils.getSource());
Resource resource = null;
try {
resource = runner.query(sql, new BeanHandler<Resource>(Resource.class),id);
} catch (SQLException e) {
e.printStackTrace();
throws new RuntimeException(e);
}
if(resource!=null) {
response.setHeader("content-disposition"
, "attachment;filename=" + URLEncoder.encode(resource.getRealName(), "utf-8"));
response.setContentType(getServletContext().getMimeType(resource.getRealName()));
InputStream in = new FileInputStream(new File(getServletContext()
.getRealPath(resource.getSavePath()),resource.getUuidName()));
OutputStream out = response.getOutputStream();
IOUtils.transfer(in,out);
IOUtils.close(in,null);
} else response.getWriter().write("资源不存在");
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?