【小家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 Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
41 0
|
3月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
65 4
|
23天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
78 14
|
1月前
|
Java 开发者 微服务
Spring Boot 入门:简化 Java Web 开发的强大工具
Spring Boot 是一个开源的 Java 基础框架,用于创建独立、生产级别的基于Spring框架的应用程序。它旨在简化Spring应用的初始搭建以及开发过程。
67 6
Spring Boot 入门:简化 Java Web 开发的强大工具
|
2月前
|
Java 开发者 Spring
Spring AOP 底层原理技术分享
Spring AOP(面向切面编程)是Spring框架中一个强大的功能,它允许开发者在不修改业务逻辑代码的情况下,增加额外的功能,如日志记录、事务管理等。本文将深入探讨Spring AOP的底层原理,包括其核心概念、实现方式以及如何与Spring框架协同工作。
|
1月前
|
监控 NoSQL 时序数据库
《docker高级篇(大厂进阶):7.Docker容器监控之CAdvisor+InfluxDB+Granfana》包括:原生命令、是什么、compose容器编排,一套带走
《docker高级篇(大厂进阶):7.Docker容器监控之CAdvisor+InfluxDB+Granfana》包括:原生命令、是什么、compose容器编排,一套带走
223 77
|
1月前
|
监控 Docker 容器
在Docker容器中运行打包好的应用程序
在Docker容器中运行打包好的应用程序
|
11天前
|
Ubuntu Linux 开发工具
docker 是什么?docker初认识之如何部署docker-优雅草后续将会把产品发布部署至docker容器中-因此会出相关系列文章-优雅草央千澈
Docker 是一个开源的容器化平台,允许开发者将应用程序及其依赖项打包成标准化单元(容器),确保在任何支持 Docker 的操作系统上一致运行。容器共享主机内核,提供轻量级、高效的执行环境。本文介绍如何在 Ubuntu 上安装 Docker,并通过简单步骤验证安装成功。后续文章将探讨使用 Docker 部署开源项目。优雅草央千澈 源、安装 Docker 包、验证安装 - 适用场景:开发、测试、生产环境 通过以上步骤,您可以在 Ubuntu 系统上成功安装并运行 Docker,为后续的应用部署打下基础。
docker 是什么?docker初认识之如何部署docker-优雅草后续将会把产品发布部署至docker容器中-因此会出相关系列文章-优雅草央千澈
|
17天前
|
存储 Kubernetes 开发者
容器化时代的领航者:Docker 和 Kubernetes 云原生时代的黄金搭档
Docker 是一种开源的应用容器引擎,允许开发者将应用程序及其依赖打包成可移植的镜像,并在任何支持 Docker 的平台上运行。其核心概念包括镜像、容器和仓库。镜像是只读的文件系统,容器是镜像的运行实例,仓库用于存储和分发镜像。Kubernetes(k8s)则是容器集群管理系统,提供自动化部署、扩展和维护等功能,支持服务发现、负载均衡、自动伸缩等特性。两者结合使用,可以实现高效的容器化应用管理和运维。Docker 主要用于单主机上的容器管理,而 Kubernetes 则专注于跨多主机的容器编排与调度。尽管 k8s 逐渐减少了对 Docker 作为容器运行时的支持,但 Doc
90 5
容器化时代的领航者:Docker 和 Kubernetes 云原生时代的黄金搭档
|
1天前
|
Kubernetes Linux 虚拟化
入门级容器技术解析:Docker和K8s的区别与关系
本文介绍了容器技术的发展历程及其重要组成部分Docker和Kubernetes。从传统物理机到虚拟机,再到容器化,每一步都旨在更高效地利用服务器资源并简化应用部署。容器技术通过隔离环境、减少依赖冲突和提高可移植性,解决了传统部署方式中的诸多问题。Docker作为容器化平台,专注于创建和管理容器;而Kubernetes则是一个强大的容器编排系统,用于自动化部署、扩展和管理容器化应用。两者相辅相成,共同推动了现代云原生应用的快速发展。
24 10
下一篇
开通oss服务