Spring MVC -- 下载文件
目录
像图片或者HTML文件这样的静态资源,在浏览器中打开正确的URL即可下载,只要该资源是放在应用程序的目录下,或者放在应用程序目录的子目录下,而不是放在WEB-INF下,tomcat服务器就会将该资源发送到浏览器。然而,有时静态资源是保存在应用程序目录之外,或者是保存在某一个数据库中,或者有时需要控制它的访问权限,防止其他网站交叉引用它。如果出现以上任意一种情况,都必要通过编程来发送资源。
简言之,通过编程进行的文件下载,使你可以有选择地将文件发送到浏览器。本篇博客将介绍如果通过编程把资源发送到浏览器,并通过两个案例进行示范。
一 文件下载概览
为了将像文件这样的资源发送到浏览器,需要在控制器中完成以下工作:
- 对请求处理方法添加HttpServletResponse、HttpServletRequest参数;
- 将相应的内容类型设为文件的内容类型。content-Type标题在某个实体的body中定义数据的类型,并包含媒体类型和子类型标识符。如果不清楚内容类型,并且希望浏览器式中显示Save As(另存为)对话框,则将它设为application/octet-stream。这个值是不区分大小写的(HTTP Content-type 对照表)。
- 添加一个属性为content-Disposition的HTTP的响应标题,并赋予attachement;filename=fileName,这里的fileName是默认文件名,应该出现在File Download(文件下载)对话框中,它通常与文件同名,但是也并非一定如此。
文件下载的流程具体如下:
- 通过浏览器,输入URL请求控制器的请求处理函数;
- 请求处理方法根据文件路径,将文件转换为输入流;
- 通过输出流将刚才已经转为输入流的文件,输出给用户(浏览器);
例如,以下代码将一个文件发送到浏览器:
//下载文件:需要设置消息头 response.setCharacterEncoding("UTF-8"); response.addHeader("content-Type", "application/octet-stream"); //指定文件类型 MIME类型:二进制文件(任意文件) String encodeFileName = null; if(userAgent.contains("MSIE") || userAgent.contains("Trident") || (userAgent.contains("GECKO") && userAgent.contains("RV:11"))) { //处理IE浏览器下载中文文件名乱码问题 encodeFileName = URLEncoder.encode( filename,"UTF-8"); }else { encodeFileName = "=?UTF-8?B?" + new String(Base64.encodeBase64(filename.getBytes("UTF-8"))) + "?="; //encodeFileName = new String(filename.getBytes("UTF-8"),"ISO-8859-1"); } System.out.println(filename + ":" + encodeFileName); //如果有换行,对于文本文件没有什么问题,但是对于其他格式:比如AutoCAD,Word,Excel等文件下载下来的文件中就会多出来一些换行符//0x0d和0x0a,这样可能导致某些格式的文件无法打开 response.reset(); response.addHeader("content-Disposition", "attachement;filename="+encodeFileName); //告诉浏览器该文件以附件方式处理,而不是去解析 //通过文件地址,将文件转换为输入流 InputStream in = request.getServletContext().getResourceAsStream(filename); //通过输出流将刚才已经转为输入流的文件,输出给用户 ServletOutputStream out= response.getOutputStream(); byte[] bs = new byte[1000]; int len = -1; while((len=in.read(bs)) != -1) { out.write(bs,0,len); } out.close(); in.close();
为了编程将一个文件发送到浏览器,首先要读取该文件作为InputStream ,随后,获取HttpServletResponse的OutputStream;循环从in中读取1000个字节,写入out中,直至文件读取完毕。
注意:这里将文件转换为输入流使用的是:
request.getServletContext().getResourceAsStream(filename);
filename指的是相对当前应用根路径下的文件。如果给定的路径是绝对路径,可以采用如下函数将文件转换为输入流:
FileInputStream in = new FileInputStream(filename) ;
将文件发送到HTTP客户端的更好方法是使用Java NIO的Files.copy()方法:
//将文件的虚拟路径转为在文件系统中的真实路径 String realpath = request.getServletContext().getRealPath(filename); System.out.print(realpath); Path file = Paths.get(realpath); Files.copy(file,response.getOutputStream());
代码更短,运行速度更快。
二 范例1:隐藏资源
我们创建一个download应用程序,用于展示如何向浏览器发送文件。
1、目录结构
2、 Login类
Login类有两个属性,登录名和登录密码:
package domain; import java.io.Serializable; //登录实体类 public class Login implements Serializable { private static final long serialVersionUID = 1L; //用户名 private String userName; //用户密码 private String password; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
3、ResourceController类
在这个应用程序中,由ResourceController类处理用户登录,并将一个secret.pdf文件发送给浏览器。secret.pdf文件放在/WEB-INF/data目录下,因此不能直接方法。只能得到授权的用户,才能看到它,如果用户没有登录,应用程序就会跳转到登录页面。
package controller; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import domain.Login; @Controller public class ResourceController { private static final Log logger = LogFactory.getLog(ResourceController.class); //请求URL:/login @RequestMapping(value="/login") public String login(@ModelAttribute Login login, HttpSession session, Model model) { model.addAttribute("login", new Login()); //校验用户名和密码 if ("paul".equals(login.getUserName()) && "secret".equals(login.getPassword())) { //设置sessopm属性"loggedIn" session.setAttribute("loggedIn", Boolean.TRUE); //校验通过 请求转发到Main.jsp页面 return "Main"; } else { //校验失败 请求转发到LoginForm.jsp页面 return "LoginForm"; } } //请求URL:/download-resource @RequestMapping(value="/download-resource") public String downloadResource(HttpSession session, HttpServletRequest request, HttpServletResponse response, Model model) { //如果用户没有登录 if (session == null || session.getAttribute("loggedIn") == null) { model.addAttribute("login", new Login()); //请求转发到LoginForm.jsp页面 等待用户登录 return "LoginForm"; } //用户已经登录 获取待下载文件夹/WEB-INF/data在文件系统的真实路径 String dataDirectory = request. getServletContext().getRealPath("/WEB-INF/data"); //创建Path对象 文件为/WEB-INF/data/secret.pdf Path file = Paths.get(dataDirectory, "secret.pdf"); //如果文件存在 下载文件 if (Files.exists(file)) { //指定文件类型 pdf类型 response.setContentType("application/pdf"); //告诉浏览器该文件以附件方式处理,而不是去解析 response.addHeader("Content-Disposition", "attachment; filename=secret.pdf"); try { Files.copy(file, response.getOutputStream()); } catch (IOException ex) { } } return null; } }
4、视图
控制器的第一个请求处理方法是login(),将用户请求转发到登录表单LoginForm.jsp:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <title>Login</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <form:form modelAttribute="login" action="login" method="post"> <fieldset> <legend>Login</legend> <p> <label for="userName">User Name: </label> <form:input id="userName" path="userName" cssErrorClass="error"/> </p> <p> <label for="password">Password: </label> <form:password id="password" path="password" cssErrorClass="error"/> </p> <p id="buttons"> <input id="reset" type="reset" tabindex="4"> <input id="submit" type="submit" tabindex="5" value="Login"> </p> </fieldset> </form:form> </div> </body> </html>
当我们输入用户名"paul",密码"secret",将会成功登录,然后请求转发到Main.jsp页面,该页面包含一个链接,点击它可以将secret.pdf文件下载下来:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE html> <html> <head> <title>Download Page</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <h4>Please click the link below.</h4> <p> <a href="download-resource">Download</a> </p> </div> </body> </html>
控制器的第二个方法downloadResource(),它通过验证session属性loggedIn是否存在,来核实用户是否已经成功登录。如果找到该属性,就会将文件发送到浏览器。否则,用户就会跳转到登录页面。
main.css:

#global { text-align: left; border: 1px solid #dedede; background: #efefef; width: 560px; padding: 20px; margin: 100px auto; } form { font:100% verdana; min-width: 500px; max-width: 600px; width: 560px; } form fieldset { border-color: #bdbebf; border-width: 3px; margin: 0; } legend { font-size: 1.3em; } form label { width: 250px; display: block; float: left; text-align: right; padding: 2px; } #buttons { text-align: right; } #errors, li { color: red; } .error { color: red; font-size: 9pt; }
5、配置文件
下面给出springmvc-config.xml文件的所有内容:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="controller" /> <mvc:annotation-driven/> <mvc:resources mapping="/css/**" location="/css/" /> <mvc:resources mapping="/*.html" location="/" /> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> </beans>
部署描述符(web.xml文件):

<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/config/springmvc-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
6、测试
将应用程序部署到tomcat服务器,并在网页输入以下URL:
http://localhost:8008/download/login
将会看到一个表单:
输入用户名"paul",密码"secret",将会跳转到文件下载页面:
点击超链接,下载secret.pdf文件。
三 范例2:防止交叉引用
心怀叵测的竞争对手有可能通过交叉引用“窃取”你的网站资产,比如,将你的资料公然放在他的资源上,好像那些东西原本就属于他的一样。如果通过编程控制,使其只有当referer中包含你的域名时才发出资源,就可以防止那种情况发生。当然,那些心意坚决的窃贼仍有办法下载到你的东西,只不过不像之前那么简单罢了。
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了