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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 【小家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月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
2月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
3月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
101 0
|
3月前
|
设计模式 监控 Java
解析Spring Cloud中的断路器模式原理
解析Spring Cloud中的断路器模式原理
|
20天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
2月前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
|
2月前
|
XML Java 数据格式
Spring5入门到实战------2、IOC容器底层原理
这篇文章深入探讨了Spring5框架中的IOC容器,包括IOC的概念、底层原理、以及BeanFactory接口和ApplicationContext接口的介绍。文章通过图解和实例代码,解释了IOC如何通过工厂模式和反射机制实现对象的创建和管理,以及如何降低代码耦合度,提高开发效率。
Spring5入门到实战------2、IOC容器底层原理
|
2月前
|
Java 程序员 数据库连接
女朋友不懂Spring事务原理,今天给她讲清楚了!
该文章讲述了如何解释Spring事务管理的基本原理,特别是针对女朋友在面试中遇到的问题。文章首先通过一个简单的例子引入了传统事务处理的方式,然后详细讨论了Spring事务管理的实现机制。
女朋友不懂Spring事务原理,今天给她讲清楚了!
|
2月前
|
前端开发 Java Spring
Java 新手入门:Spring Boot 轻松整合 Spring 和 Spring MVC!
Java 新手入门:Spring Boot 轻松整合 Spring 和 Spring MVC!
48 0
|
3月前
|
XML 前端开发 Java
Spring Boot与Spring MVC的区别和联系
Spring Boot与Spring MVC的区别和联系
下一篇
无影云桌面