【小家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浏览器模拟的操作

相关文章
|
1月前
|
缓存 安全 Java
《深入理解Spring》过滤器(Filter)——Web请求的第一道防线
Servlet过滤器是Java Web核心组件,可在请求进入容器时进行预处理与响应后处理,适用于日志、认证、安全、跨域等全局性功能,具有比Spring拦截器更早的执行时机和更广的覆盖范围。
|
1月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
8月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
483 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
8月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
734 0
|
8月前
|
前端开发 Java 微服务
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@PathVariable
`@PathVariable` 是 Spring Boot 中用于从 URL 中提取参数的注解,支持 RESTful 风格接口开发。例如,通过 `@GetMapping(&quot;/user/{id}&quot;)` 可以将 URL 中的 `{id}` 参数自动映射到方法参数中。若参数名不一致,可通过 `@PathVariable(&quot;自定义名&quot;)` 指定绑定关系。此外,还支持多参数占位符,如 `/user/{id}/{name}`,分别映射到方法中的多个参数。运行项目后,访问指定 URL 即可验证参数是否正确接收。
475 0
|
8月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestMapping
@RequestMapping 是 Spring MVC 中用于请求地址映射的注解,可作用于类或方法上。类级别定义控制器父路径,方法级别进一步指定处理逻辑。常用属性包括 value(请求地址)、method(请求类型,如 GET/POST 等,默认 GET)和 produces(返回内容类型)。例如:`@RequestMapping(value = &quot;/test&quot;, produces = &quot;application/json; charset=UTF-8&quot;)`。此外,针对不同请求方式还有简化注解,如 @GetMapping、@PostMapping 等。
418 0
|
8月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RestController
本文主要介绍 Spring Boot 中 MVC 开发常用的几个注解及其使用方式,包括 `@RestController`、`@RequestMapping`、`@PathVariable`、`@RequestParam` 和 `@RequestBody`。其中重点讲解了 `@RestController` 注解的构成与特点:它是 `@Controller` 和 `@ResponseBody` 的结合体,适用于返回 JSON 数据的场景。文章还指出,在需要模板渲染(如 Thymeleaf)而非前后端分离的情况下,应使用 `@Controller` 而非 `@RestController`
340 0
|
4月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
331 0
|
4月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
156 0
|
4月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
266 0