JavaWeb实现简单的文件上传和下载

发布时间 2023-11-27 13:52:58作者: Xproer-松鼠

一、文件上传
1.1 文件上传的简单实现
前端的标签介绍
① 需要一个<form>表单标签,请求方式为post请求

PS:因为get请求时url有长度限制,而带有文件上传的url一般会超出get请求的长度限制,所以只能用post

② <form>标签中需添加enctype属性,属性值必须为multipart/form-data

enctype属性:encodetype的缩写,就是编码类型的意思
multipart/form-data属性值:multipart是多元的意思,表示数据由多段的形式拼接而成,既有文本又有文件,每一个表单项表示一段数据,拼接后以二进制流的方式提交到服务器
PS:enctype属性值只有设置为multipart/form-data时,才能实现文件的上传

③ <form>标签里面添加<input type=file>标签,在此处添加需要上传的文件
④ 后台服务器中使用Servlet 接受和处理上传的数据

实现代码

前端页面

<%-- 添加表单项 --%>
<form action="" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username"> <br>
头像:<input type="file" name="profile"> <br>
<input type="submit" value="上传">
</form>

后台Servlet

@WebServlet("/fileUpload")
public class FileUploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("文件上传成功!");
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
}

1.2 http协议内容分析
分析图

 

分析请求头
Content-Type参数:对应表单中的enctype属性,表示提交数据的编码类型
Content-Type的两个参数值
multipart/form-data:对应enctype属性值,就是表示提交的数据以多段形式拼接而成,并以二进制流形式发给服务器
boundary=分隔符:表示每段数据的分隔符,由浏览器每次随机生成,这里演示的分隔符为----WebKitFormBoundarynn2YwuHk30PBRDRM(前面一般带有4个减号)
分析请求体
----WebKitFormBoundarynn2YwuHk30PBRDRM:分隔符,表示一段数据(一个表单项)的开始
----WebKitFormBoundarynn2YwuHk30PBRDRM--:结束分隔符,在原来的分隔符后再添加2个减号,表示整段数据的结束,即在最后一个表单项后面出现
表单项内容分析
Content-Disposition:对当前表单项的描述,参数值有form-data、name="表单名"
Content-Type:表示上传的文件MIME类型,值为image/jpeg
空行:在表单项描述和表单项值之间会有空行隔开,类似于请求头与请求体之间的请求空行
表单项的值:如果是文本类型的表单项,就直接显示表单项输入框中的值,如果是文件类型,就是一些文件数据,一般文件的数据都会很多,所以浏览器会隐藏起来,但可以在后台打印查看
1.3 解析文件上传时的数据
使用第三方(apache)jar包:commons-fileupload-…jar、commons-io-…jar

jar包中重要类及其方法介绍

ServeltFileUpload类:用于解析文件上传时的数据
boolean isMultipartContent(req):ServletFileUpload类中的静态方法,判断上传的表单数据是否是多段的格式
List<FileItem> parseRequest(req):解析上传的数据(多段数据格式的前提下)
FileItem类:对应一个表单项
boolean isFormField():判断当前的表单项是否是普通类型的表单项,true表示普通表单项,如文本,false表示非普通表单项,如文件
String getFieldName():获取表单项的名称,即获取<form>标签的name属性值
String getString("utf-8"):获取普通表单项的值,加上参数"utf-8",设置编码,可以防止中文乱码
String getName():获取上传的文件名,表单项是文件类型才能使用
write(文件路径):将上传的文件写到对应的硬盘地址中
jar包方法在Servlet中的使用
① 判断当前表单数据是否多段格式,如果不是就不能进行文件数据解析,下列操作都是在多段格式数据下进行
② 先创建一个表单项工厂类对象FileItemFactory
③ 根据创建的表单项工厂类对象创建出ServletFileUpload对象,用于解析数据
④ 调用servletFileUpload的解析方法,对表单数据进行解析,获取数据段集合,即表单项集合List<FileItem>(会有异常,需要捕获)
⑤ 遍历获取的表达项集合,判断每一个表单项类型,如果是普通类型表单项就只打印表单项名和表单项值,如果是文件类型表单项,就打印表单项名和文件名,同时将上传的文件写到指定位置

@WebServlet("/fileUpload")
public class FileUploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("文件上传成功!");

// 1. 判断当前表单是否是多段格式
if (ServletFileUpload.isMultipartContent(req)) {
// 2. 创建表单项工厂对象
FileItemFactory fileItemFactory = new DiskFileItemFactory();

// 3. 根据工厂对象创建出ServletFileUpload对象
ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory);

try {
// 4. 调用方法解析数据,获取表单项集合
List<FileItem> fileItems = fileUpload.parseRequest(req);

// 5. 遍历表单项集合,对不同类型的表单项做不同处理
for (FileItem item : fileItems) {
// 5.1 判断每一个表单项的类型,是否是普通类型
if (item.isFormField()) {
// 普通类型表单项
System.out.println("表单项名:" + item.getFieldName());
System.out.println("表单项值:" + item.getString("utf-8"));
} else {
// 不是普通类型表单项
System.out.println("文件表单项名:" + item.getFieldName());
System.out.println("文件名:" + item.getName());

// 将上传的文件写到指定位置,文件名不变
item.write(new File(
"D:\\ideaProject\\workplace_java\\Pro_FileUpAndDown\\src\\main\\webapp\\" +
item.getName()));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
}


二、文件下载
参考博文

java web文件下载功能实现
web项目中各种路径的获取
2.1 文件下载的简单实现
2.1.1 通过超链接下载
创建download_href.jsp(或html),创建两个超链接,分别对应压缩包资源和图片资源

PS:两个文件都在工程的webapp/file目录下,这里的href的值是绝对路径,客户端的绝对路径都是相对于服务器根目录(http://ip地址:端口号),而不是工程中的webapp目录,所以需要加上工程路径(虚拟目录),推荐使用<%= request.getContextPath()%>/动态获取工程路径

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>通过超链接下载文件</title>
</head>
<body>
<%-- 创建两个超链接 --%>
<a href="<%= request.getContextPath()%>/file/like.rar">压缩包下载</a> <br/>
<a href="<%= request.getContextPath()%>/file/doge.png">图片下载</a>
</body>
</html>

在浏览器中直接点击对应链接,下载对应文件

PS:一般这种方法实现文件下载是不可行的,因为很多浏览器可以识别该文件格式,识别后就会直接打开对应的超链接(打开后一般是错误页面)。只有浏览器不能识别该文件格式的时候,才能实现下载

2.1.2 跳转到后台Servlet实现下载
创建download.jsp页面,通过点击对应超链接,跳转到FileDownloadServlet(资源路径为/fileDownload)中,并带上请求参数,将文件名传给后台

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>文件下载</title>
</head>
<body>
<%-- 在后台通过Servlet下载文件 --%>
<a href="<%= request.getContextPath()%>/fileDownload?filename=like.rar">压缩包下载</a> <br/>
<a href="<%= request.getContextPath()%>/fileDownload?filename=doge.png">图片下载</a>
</body>
</html>

在FileDownloadServlet中,先获取请求参数中的文件名,再通过ServletContext对象获取文件名对应的MIME类型,然后将对应的MIME类型设置为响应时的数据类型,再然后通过设置响应头Content-Disposition为attachment,告知浏览器客户端对数据的进行下载的操作,最后获取文件的位置,并通过输入流和输入流将文件写到浏览器端(使用jar包中的工具类实现更方便)

PS1:如果不设置响应头,在浏览器端就不能实现下载的功能,只会将图片展示到页面

PS2:服务器端的绝对路径指的是相对于工程的webapp目录,即http://ip地址:端口号/工程路径,所以在获取文件的位置时只需写成/file/文件名;另一种解释为相对路径/file/文件名中的第一个/被服务器解析成http://ip地址:端口号/工程路径,并映射到工程的webapp目录下

PS3:在获取文件名时,有两种方法

通过servletContext对象直接调用InputStream getResourceAsStream(文件位置),可以直接将文件位置转成输入流
先通过servletContext对象调用String getRealPath(文件位置),获取文件的真实路径,再通过FileInputStream(文件真实路径),获取输入流
@WebServlet("/fileDownload")
public class FileDownloadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
// 1. 获取请求参数中的文件名
String filename = req.getParameter("filename");
System.out.println(filename);

// 2. 获取ServletContext对象,用于获取文件对应的MIME类型
ServletContext servletContext = getServletContext();

// 3. 获取文件名对应的MIME类型,这里直接写filename也可
String mimeType = servletContext.getMimeType("/file/" + filename);
System.out.println(mimeType);

// 4. 将对应的MIME类型设置为响应时的数据类型
resp.setContentType(mimeType);

// 5. 设置响应头,告知浏览器要对文件做下载的操作,这里文件名可以自定义,不一定与原文件一样
resp.setHeader("Content-Disposition", "attachment;filename=" + filename);

// 6. 获取文件的路径,并转成输入流
InputStream inOfFile = servletContext.getResourceAsStream("/file/" + filename);

// 7. 获取响应的输出流
ServletOutputStream outOfFile = resp.getOutputStream();

// 8. 通过IO工具类,直接将输入流复制到输出流,然后写到浏览器客服端(简单方便)
IOUtils.copy(inOfFile, outOfFile);
}

@Override
protected void doGet(HttpServletRequest req,
HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
}


遇到的小问题:第一次运行,在浏览器点击超链接后,显示“无法访问此网站”,而且服务器收到多次请求。通过debug模式检查后,发现每次获取的文件输入流inOfFile值都为null,而且target文件夹中也没有图片和压缩包的资源,原因可能是webapp目录下的资源没有加载成功

解决方法:执行一次maven的clean命令,再重启服务器,就能解决问题

2.2 文件名的中文乱码问题
引入问题:在设置Content-Disposition响应头时,可以设置与原文件不一样的文件名,如果设置为中文名,会出现中文乱码的问题,导致下载到本地的文件名是乱码

2.2.1 使用URL编码
适用浏览器:谷歌、IE

使用:将resp.setHeader("Content-Disposition", "attachment;filename=" + filename),中的filename换成URLEncoder.encode("带中文的文件名", "utf-8");

resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("狗头.png", "utf-8"));

2.2.2 使用BASE64编码
适用浏览器:火狐

BASE64的简单测试

PS:BASE64编码类BASE64Encoder中的编码方法encode()不是静态方法,所以需要先创建出编码对象,才能调用;BASE64解码类BASE64Decoder也是一样

测试代码
@Test
public void testBASE64() throws IOException {
// 1. 定义一个带中文的字符串
String content = "我爱中国!";

// 2. 创建Base64编码对象
BASE64Encoder encoder = new BASE64Encoder();

// 3. 先将内容字符串转成字节
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);

// 4. 调用编码方法对字节进行编码,获取编码后的字符串
String encodeString = encoder.encode(bytes);
System.out.println("编码结果:" + encodeString);

// 5. 创建Base64解码对象
BASE64Decoder decoder = new BASE64Decoder();

// 6. 将编码的字符再转成字节数组
byte[] decodeBuffer = decoder.decodeBuffer(encodeString);

// 7. 获取解码后的字符串
String decodeString = new String(decodeBuffer, StandardCharsets.UTF_8);
System.out.println("解码结果:" + decodeString);
}


测试结果

 

BASE64在文件下载中的使用:将resp.setHeader("Content-Disposition", "attachment;filename=" + filename),中的filename换成=?charset?B?XXXX?=

=?charset?B?XXXX?=的解释:

=?:表示编码内容的开始
charset:字符集,如utf-8
B:表示BASE64编码,不用修改
XXXX:表示通过BASE64编码之后的内容
?=:表示编码内容的结束
// 先获取BASE64编码后的内容
String encodeString = new BASE64Encoder.encode("狗头.png".getBytes(StandardCharsets.UTF_8));

// 再设置响应头
resp.setHeader("Content-Disposition", "attachment;filename==?utf-8?B?" + encodeString + "?=");

2.2.3 通过User-Agent请求头选择使用对应编码方式
PS:因为不同浏览器适用不用的编码方式,为统一解决所有浏览器的文件名中文乱码问题,可以通过User-Agent请求头来判断浏览器的版本,从而使用对应的编码方式

// 5. 设置响应头,告知浏览器要对文件做下载的操作
// 获取User-Agent请求头,判断浏览器种类
String reqHeader = req.getHeader("User-Agent");
// 判断请求头中是否包含火狐的字样
if (reqHeader.contains("Firefox")) {
// 如果是火狐浏览器,就使用BASE64编码
// 先使用BASE64对内容进行编码,再插入
String encodeString = new BASE64Encoder().encode("狗头.png".getBytes(StandardCharsets.UTF_8));
resp.setHeader("Content-Disposition", "attachment;filename==?utf-8?B?" + encodeString + "?=");
} else {
// 如果是非火狐浏览器,如IE、谷歌,就使用URL编码即可
resp.setHeader("Content-Disposition", "attachment;filename=" +
URLEncoder.encode("狗头.png", "utf-8"));
}

 

参考文章:http://blog.ncmem.com/wordpress/2023/11/27/javaweb%e5%ae%9e%e7%8e%b0%e7%ae%80%e5%8d%95%e7%9a%84%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e5%92%8c%e4%b8%8b%e8%bd%bd/

欢迎入群一起讨论