【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter的匹配规则(选择原理)(下)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter的匹配规则(选择原理)(下)

使用Spring MVC实现优雅的文件下载


传统的,我们要进行文件下载,可以直接操作HttpServletRequestHttpServletResponse来处理下载。那基本上就与Spring MVC的关系不大了。 我们能看到形如下面的代码:


    //设置响应头和客户端保存文件名
    response.setCharacterEncoding("utf-8");
    response.setContentType("multipart/form-data");
    response.setHeader("Content-Disposition", "attachment;fileName=" + fileName);
  ...
        //打开本地文件流
        InputStream inputStream = new FileInputStream(filePath);
        //激活下载操作
        OutputStream os = response.getOutputStream();
        //循环写入输出流
        byte[] b = new byte[2048];
        int length;
        while ((length = inputStream.read(b)) > 0) {
            os.write(b, 0, length);
            downloadedLength += b.length;
        }
        // 这里主要关闭。
        os.close();
        inputStream.close();
  ...


显然这一大段处理起来还是比较麻烦的。本文另外一种方案:在Spring MVC环境下能让你优雅的处理文件下载:使用ResponseEntity方式


Demo如下:


    // 处理下载 get/post/put请求等等都是可以的  但一般都用get请求
    @RequestMapping(value = "/download", method = RequestMethod.GET)
    public ResponseEntity<Resource> downloadFile(@RequestParam String fileName) {
        // 构造下载对象  读取出一个Resource出来  此处以类路径下的logback.xml
        DownloadFileInfoDto downloadFile = new DownloadFileInfoDto(fileName, new ClassPathResource("logback.xml"));
        return downloadResponse(downloadFile);
    }
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    private static class DownloadFileInfoDto {
        private String fileName; // 下载的文件
        private Resource resource; // 下载的具体文件资源
    }
    // 共用方法  兼容到了IE浏览器~~~
    private static ResponseEntity<Resource> downloadResponse(
            DownloadFileInfoDto fileInfo) {
        String fileName = fileInfo.getFileName();
        Resource body = fileInfo.getResource();
        // ========通过User-Agent来判断浏览器类型 做一定的兼容~========
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String header = request.getHeader("User-Agent").toUpperCase();
        HttpStatus status = HttpStatus.CREATED;
        try {
            if (header.contains("MSIE") || header.contains("TRIDENT") || header.contains("EDGE")) {
                fileName = URLEncoder.encode(fileName, "UTF-8");
                fileName = fileName.replace("+", "%20");    // IE下载文件名空格变+号问题
                status = HttpStatus.OK;
            } else { // 其它浏览器 比如谷歌浏览器等等~~~~
                fileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1");
            }
        } catch (UnsupportedEncodingException e) {
        }
        // =====响应头需设置为MediaType.APPLICATION_OCTET_STREAM=====
        HttpHeaders headers = new HttpHeaders();
    // 注意:若这个响应头不是必须的,但是如果你确定要下载,建议写上这个响应头~
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        // 此处,如果你自己没有设置这个请求头,浏览器肯定就不会弹窗对话框了。它就会以body的形式直接显示在浏览器上
        headers.setContentDispositionFormData("attachment", fileName);
        return new ResponseEntity<>(body, headers, status);
    }


备注:使用此种方式最终处理返回值的处理器为:HttpEntityMethodProcessor,使用的消息转换器为:ResourceHttpMessageConverter


这样请求就能弹出下载框了。响应头如下:


image.png


可以看到这里不仅设置了Content-Disposition请求头,还是设置了Content-type为application/octet- stream那就意味着你不想让浏览器直接显示内容,而是弹出一个”文件下载”的对话框。


关于application/octet-stream等响应头的解释,请看如下例子形象解释:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="picture.png"


它表示对浏览器说:“我不清楚代码内容,请把其保存为一个文件,最好命名为picture.png”。

Content-Type: image/png
Content-Disposition: attachment; filename="picture.png"

它表示对浏览器说:表示“这是一个PNG图像,请将其保存为一个文件,最好命名为picture.png”。

Content-Type: image/png
Content-Disposition: inline; filename="picture.png"



它表示对浏览器说:“这是一个PNG图像,除非你不知道如何显示PNG图像,否则请显示它,如果用户选择保存它,我们建议文件名保存为picture.png”。(inline方式)


在能够识别内联的浏览器中,可议使用这个方法(现在绝大多数浏览器都能识别这种方式),少数浏览器会对它进行保存~~~~


所以当你给客户端传递的不知道是文本、图片、还是其它的格式时,使用application/octet-stream最佳。(是否弹出下载框不是由它决定的,主要是Content-Disposition这个请求头来决定的)


ResponseEntity方式对比传统Java方式


单单从代码上看ResponseEntity方式秒杀传统的Java方式,但是否我们想都不想就采用优雅方式呢?但其实非也,有些东西还是要注意的。


  • **基于ResponseEntity的实现的局限性还是很大:**这种下载方式是一种一次性读取的下载方式,在文件较大的时候会直接抛出内存溢出(所以适合小文件下载,不超过1G吧)。还有就是这种下载方式因为是一次性全部输出,所以无法统计已下载量、未下载量等扩展功能,所以也就不能实现断点续传
  • **传统Java通用实现在功能上能够更加的丰富:**对下载文件的大小无限制((循环读取一定量的字节写入到输出流中,因此不会造成内存溢出)。 因为是这种实现方式是基于循环写入的方式进行下载,在每次将字节块写入到输出流中的时都会进行输出流的合法性检测,在因为用户取消或者网络原因造成socket断开的时候,系统会抛出SocketWriteException。 当然我们可以捕获到这个异常,记录下当前已经下载的数据量、下载状态等。这样我们就可以实现断点续传的功能了


ResponseEntity方式的优点就是简洁,所以在比较小的文件下载时,它绝对是首选。若是有大量的下载需求,其实一般都建议使用ftp服务器而不是http了。


当然还有一种使用ResponseEntity<byte[]>的方式,也挺优雅的,这里就只提供代码参考了:


@RequestMapping("/testResponseEntity")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    byte[] body = null;
    ServletContext servletContext = session.getServletContext();
    InputStream in = servletContext.getResourceAsStream("/files/abc.txt");
    body = new byte[in.available()];
    in.read(body);
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Disposition","attachment;filename=abc.txt");
    HttpStatus statusCode = HttpStatus.OK;
    ResponseEntity<byte[]> response = new ResponseEntity<byte[]>(body,headers,statusCode);
    return  response;
}


此处使用的返回值处理同上,消息转换器是:ByteArrayHttpMessageConverter


总结


自己写代码也要注意啊:代码中有顺序遍历匹配这种逻辑,或者叫责任链模式时,功能越具体的节点越是应该放在前面,功能最广最泛的节点应该放在最后面;


写程序就是这样,不断追求更好的解决方法,永远不满足于“能够运行”!


附:关于Spring MVC应用中自动下载f.txt问题


不知道小伙伴有没有遇见过这样的情况:你用浏览器访问一个rest请求,但是浏览器却总是自动弹出了一个下载框,然后给你下载了一个名字为f.txt的文件,里面内容为你的异常信息(或者body内容信息),简直一脸懵逼有木有


其实这个现象上面已经提到过了原因,但是一笔带过没有详细解释。下面就通过模拟这种现象,然后给大伙说说具体原因~~~


现象模拟

其实模拟出这种效果来,也是很大的一个学问。反而我觉得你得先知道原理、根本原因才好模拟,否则也是无头苍蝇,不知从哪儿下手。但是此处,我们还是先模拟一下吧:


基于上面的下载的例子(使用ResponseEntity方式):


        // =====响应头需设置为MediaType.APPLICATION_OCTET_STREAM=====
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        //headers.setContentDispositionFormData("attachment", fileName);


相当于我只设置application/octet-stream,但是我并不设置Content-Disposition这个请求头。

我们这么访问:http://localhost:8080/demo_war_war/download?fileName=aaa.xml,会弹出下载框下载文件


请重点关注download的后缀名,第一次访问是无后缀名的形式,后面会有改变

image.png

并且文件里面的内容也是没有问题的。


但我们这么访问:xxx/download.json,弹出下载框,下载的文件如下名为:download.json

当我们这么访问:xxx/download.aaa,弹出下载框,下载的文件如下名为:f.txt

终于,这就是我们想演示的自动下载f.txt的case~~~~~


ContentType和ContentDisposition都不设置的case


        // =====响应头需设置为MediaType.APPLICATION_OCTET_STREAM=====
        HttpHeaders headers = new HttpHeaders();
        //headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        //headers.setContentDispositionFormData("attachment", fileName);


相当于什么都不设置,有如下效果:

当我们这么访问:xxx/download和xxx/download.aaa等,浏览器看到的结果是:

image.png


但是如果这样访问xxx/download.json/xxx/download.txt/xxx/download.png的话得到的效果如下:

xxx/download.json/xxx/download.txt:


image.png


image.png


/xxx/download.png:浏览器会以图片的形式进行展示image.png


image.png

**我们发现后缀名不同,Spring MVC就自动给了一个合适的content-type,**原因下面再会解释


可以看到这两个请求头全都不设置的话,肯定是不会触发弹出下载的


原因分析


其实上面文件下载的Demo已经解释得差不多了,但是还没有搞明白为啥有的时候弹出的的文件名不是f.txt呢?

AbstractMessageConverterMethodProcessor#addContentDispositionHeader这个方法上,它会给响应只能的设置一个content-type和Content-Disposition:


  private void addContentDispositionHeader(ServletServerHttpRequest request, ServletServerHttpResponse response) {
    ...
    // 默认它采用inline的模式
    if (!safeExtension(servletRequest, ext) || !safeExtension(servletRequest, extInPathParams)) {
      headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=f.txt");
    }
    ...
  }


然后为啥后缀名不同,展示也不一样呢?比如xxx/download.json/xxx/download.txt/xxx/download.png等后缀名不一样,浏览器的展示效果也不一样~其实原理在这:


public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {
  /* Extensions associated with the built-in message converters */
  private static final Set<String> WHITELISTED_EXTENSIONS = new HashSet<>(Arrays.asList(
      "txt", "text", "yml", "properties", "csv",
      "json", "xml", "atom", "rss",
      "png", "jpe", "jpeg", "jpg", "gif", "wbmp", "bmp"));
  private static final Set<String> WHITELISTED_MEDIA_BASE_TYPES = new HashSet<>(
      Arrays.asList("audio", "image", "video"));
  ...
}


简单的说就是请求的后缀名是上面例句出来的额话,*浏览器会采用内联、直接展示的方式~~~~*

备注:以上方案都是基于Chrome浏览器模拟的操作

相关文章
|
2月前
|
XML Java 数据格式
Spring容器Bean之XML配置方式
通过对以上内容的掌握,开发人员可以灵活地使用Spring的XML配置方式来管理应用程序的Bean,提高代码的模块化和可维护性。
73 6
|
2月前
|
安全 Java 开发者
Spring容器中的bean是线程安全的吗?
Spring容器中的bean默认为单例模式,多线程环境下若操作共享成员变量,易引发线程安全问题。Spring未对单例bean做线程安全处理,需开发者自行解决。通常,Spring bean(如Controller、Service、Dao)无状态变化,故多为线程安全。若涉及线程安全问题,可通过编码或设置bean作用域为prototype解决。
50 1
|
3月前
|
前端开发 Java Docker
使用Docker容器化部署Spring Boot应用程序
使用Docker容器化部署Spring Boot应用程序
|
3月前
|
Java Docker 微服务
利用Docker容器化部署Spring Boot应用
利用Docker容器化部署Spring Boot应用
70 0
|
4月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
257 3
|
3月前
|
开发框架 搜索推荐 数据可视化
Django框架适合开发哪种类型的Web应用程序?
Django 框架凭借其强大的功能、稳定性和可扩展性,几乎可以适应各种类型的 Web 应用程序开发需求。无论是简单的网站还是复杂的企业级系统,Django 都能提供可靠的支持,帮助开发者快速构建高质量的应用。同时,其活跃的社区和丰富的资源也为开发者在项目实施过程中提供了有力的保障。
155 62
|
2月前
|
前端开发 安全 JavaScript
2025年,Web3开发学习路线全指南
本文提供了一条针对Dapp应用开发的学习路线,涵盖了Web3领域的重要技术栈,如区块链基础、以太坊技术、Solidity编程、智能合约开发及安全、web3.js和ethers.js库的使用、Truffle框架等。文章首先分析了国内区块链企业的技术需求,随后详细介绍了每个技术点的学习资源和方法,旨在帮助初学者系统地掌握Dapp开发所需的知识和技能。
2025年,Web3开发学习路线全指南
|
3月前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
262 45
|
3月前
|
存储 前端开发 JavaScript
如何在项目中高效地进行 Web 组件化开发
高效地进行 Web 组件化开发需要从多个方面入手,通过明确目标、合理规划、规范开发、加强测试等一系列措施,实现组件的高效管理和利用,从而提高项目的整体开发效率和质量,为用户提供更好的体验。
52 7
|
3月前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
63 2