十七:Spring Boot (2)-- spring-boot-starter-web 依赖详解
目录
1. spring-boot-starter-web 简介
1.1 作用与功能:
1.2 引入方式:
1.3 包含的核心依赖:
2. 自动配置原理
3. 内嵌 Servlet 容器
3.1 默认 Tomcat 配置:
3.2 替换容器(Jetty 或 Undertow):
4. 构建 RESTful Web 服务:
4.1 什么是 RESTful Web 服务
4.2 创建 REST 控制器
5. 自动处理 JSON:
6. 静态资源支持
7. Web 配置定制(通过 WebMvcConfigurer)
7.1 注册拦截器(Interceptor)
7.2 配置静态资源处理
7.2.1 addResourceHandlers(ResourceHandlerRegistry registry) 方法
7.2.2 registry.addResourceHandler("/assets/**")
7.2.3 addResourceLocations("classpath:/static/assets/")
7.3 配置视图解析器(ViewResolver)
7.3.1 pom.xml 加两个引用 支持jsp 的
7.3.2 配置视图解析器
7.3.3 创建jsp页面
7.3.4 写controller 一定用@Controller
7.3.4 浏览器访问 乱码 无所谓 只要能请求到 就没大问题
7.3.5 工作原理
7.4 CORS 配置(跨域资源共享)
7.4.1 什么是跨域?
7.4.2 为什么会有跨域问题?
7.4.3 同源策略(Same-Origin Policy)
7.4.4 跨域的场景
7.4.5 浏览器的跨域限制
7.4.6 跨域的解决方案
7.4.6.1 CORS(跨域资源共享)
7.4.6.2 JSONP(仅限 GET 请求)
7.4.6.3 服务器端代理(推荐)
7.5 消息转换器(Message Converters)
7.8 定制异常处理(@ExceptionHandler)(不推荐)
8. 支持文件上传与下载
8.1 文件上传
8.1.1 配置文件上传的基本设置
8.1.2 实现文件上传接口
8.1.3 上传目录配置
8.1.4 上传多个文件
8.2 文件下载
8.2.1 实现文件下载接口
8.2.2 设置响应头部以下载文件
-
1. spring-boot-starter-web
简介-
1.1 作用与功能:
-
spring-boot-starter-web
是 Spring Boot 的一个启动器(starter),用于构建 Web 应用,它自动配置了多种常见的 Web 组件,尤其适合构建 RESTful Web 服务。
-
-
1.2 引入方式:
- Maven:在
pom.xml
中添加:-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
- Gradle:在
build.gradle
中添加:-
implementation 'org.springframework.boot:spring-boot-starter-web'
-
- Maven:在
-
1.3 包含的核心依赖:
-
Spring MVC:构建 Web 应用的基础框架,提供了控制器、视图解析等功能。
-
内嵌 Servlet 容器(Tomcat):提供一个内嵌的默认 Servlet 容器,简化部署。
-
Jackson:用于 JSON 数据的序列化和反序列化。
-
Spring Boot 自动配置:自动配置
DispatcherServlet
,自动配置 Spring MVC 相关的功能。
-
-
-
2. 自动配置原理
- DispatcherServlet 自动配置:
-
Spring Boot 会自动配置
DispatcherServlet
,它是 Spring MVC 的核心,用于路由请求到适当的控制器方法。
-
-
Spring MVC 相关的自动配置:
spring-boot-starter-web
自动配置了 Spring MVC 所需的MessageConverter
、视图解析器等,简化了手动配置。 -
Tomcat 自动配置:
默认情况下,spring-boot-starter-web
使用 Tomcat 作为嵌入式容器,你可以通过配置改变容器(如使用 Jetty 或 Undertow)。
- DispatcherServlet 自动配置:
-
3. 内嵌 Servlet 容器
-
3.1 默认 Tomcat 配置:
-
默认内嵌容器是 Tomcat,并且默认端口是
8080
。 -
可以通过
application.properties
或application.yml
修改端口:-
server.port=8081
-
-
-
3.2 替换容器(Jetty 或 Undertow):
-
如果你不想使用 Tomcat,可以排除它并使用 Jetty 或 Undertow:
-
排除 Tomcat:
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>provided</scope> </dependency>
-
-
添加 Jetty 或 Undertow:
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency>
-
-
-
-
4. 构建 RESTful Web 服务:
-
4.1 什么是 RESTful Web 服务
- REST(Representational State Transfer)是一种通过 HTTP 协议与 Web 服务交互的架构风格。RESTful Web 服务遵循一系列约定,通常使用 HTTP 方法(如 GET、POST、PUT、DELETE)来进行资源的创建、查询、更新和删除操作。每个资源通常由一个 URL 唯一标识,且资源的数据通常以 JSON 返回。
-
4.2 创建 REST 控制器
-
在 Spring Boot 中,构建 RESTful 服务的核心是
@RestController
注解。@RestController
是一个结合了@Controller
和@ResponseBody
注解的注解,表示该类是一个控制器,且返回的内容会自动以 JSON 或 XML 格式返回(根据客户端请求的Accept
头)。 -
package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, World!"; } }
-
@RestController
:表示该类是一个 REST 控制器,返回数据会被自动序列化为 JSON 格式。 -
@RequestMapping("/api")
:为所有请求添加一个基础路径/api
。 -
@GetMapping("/hello")
:处理 GET 请求,当客户端访问/api/hello
时,返回"Hello, World!"
。 -
@PostMapping
:处理 HTTP POST 请求,用于创建新用户。 -
@PutMapping
:处理 HTTP PUT 请求,用于更新用户信息。 -
@DeleteMapping
:处理 HTTP DELETE 请求,用于删除用户。
-
-
-
5. 自动处理 JSON:
-
spring-boot-starter-web
默认集成了 Jackson 序列化和反序列化,自动将 Java 对象与 JSON 数据进行转换。 -
你可以使用
@RequestBody
注解接收请求体中的 JSON 数据,使用@ResponseBody
返回 JSON 数据。
-
-
6. 静态资源支持
-
默认情况下,Spring Boot 会从
/static
、/public
、/resources
和/META-INF/resources
目录提供静态资源。 -
可以将静态文件(如 HTML、CSS、JavaScript、图片等)放入这些目录中,Spring Boot 会自动提供访问。
-
自定义静态资源路径:
-
可以通过配置文件修改静态资源的根目录
-
spring.resources.static-locations=classpath:/custom-static/
-
-
-
7. Web 配置定制(通过
WebMvcConfigurer
)-
在开发 Spring Web 应用时,
WebMvcConfigurer
是一个非常常用的接口,它提供了一种灵活的方式来定制 Spring MVC 的行为。 -
7.1 注册拦截器(Interceptor)
-
package com.lirui.springbootmoduledemo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册一个拦截器 registry.addInterceptor(new WebInterceptor()) // 设置拦截路径 .addPathPatterns("/api/**") // 排除不需要拦截的路径 .excludePathPatterns("/api/login", "/api/register"); } }
package com.lirui.springbootmoduledemo.config; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class WebInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 这里可以做一些拦截前的校验或操作 System.out.println("preHandle: 请求即将到达Controller"); // 返回true表示继续处理请求,false表示请求被拦截,不会继续执行 return true; // 如果返回false,请求会被拦截,后续不会执行 } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 请求处理之后,视图渲染之前调用 System.out.println("postHandle: 请求已处理,视图渲染之前"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求处理完后,视图渲染完毕后调用(通常用于清理资源) System.out.println("afterCompletion: 请求完成后,视图渲染之后"); } }
-
-
7.2 配置静态资源处理
-
package com.lirui.springbootmoduledemo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册一个拦截器 registry.addInterceptor(new WebInterceptor()) // 设置拦截路径 .addPathPatterns("/api/**") // 排除不需要拦截的路径 .excludePathPatterns("/api/login", "/api/register"); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 配置 /assets/** 请求,映射到 classpath:/static/assets/ 文件夹 registry.addResourceHandler("/assets/**") .addResourceLocations("classpath:/static/assets/"); // 配置 /images/** 请求,映射到 classpath:/static/images/ 文件夹 registry.addResourceHandler("/images/**") .addResourceLocations("classpath:/static/images/"); } }
-
7.2.1 addResourceHandlers(ResourceHandlerRegistry registry)
方法-
addResourceHandlers
是 Spring MVC 提供的一个方法,用于配置静态资源的处理方式。ResourceHandlerRegistry
用来注册静态资源的访问路径和实际的资源位置。 -
addResourceHandlers
方法允许你定义一组 资源处理器(Resource Handlers),这些处理器负责处理静态资源请求并返回相应的资源。通过配置,Spring MVC 能够自动映射 URL 路径到实际的文件存放位置,从而提供静态文件的访问功能。
-
-
7.2.2 registry.addResourceHandler("/assets/**")
-
registry.addResourceHandler("/assets/**")
-
addResourceHandler("/assets/**")
指定了一个 资源请求的映射路径,即用户访问/assets/
路径下的任何 URL(匹配/assets/**
)时,都会交给 Spring MVC 来处理。-
**
是一种通配符,表示匹配/assets/
后面所有的路径(包括子目录)。 -
例如,访问
http://localhost:8080/assets/img/logo.png
或者http://localhost:8080/assets/css/style.css
都会被映射到对应的静态资源文件。
-
-
-
7.2.3 addResourceLocations("classpath:/static/assets/")
-
addResourceLocations("classpath:/static/assets/")
指定了 静态资源文件的实际存储路径。classpath:/static/assets/
是资源文件所在的路径。 -
classpath:
表示资源位置在类路径中。Spring Boot 默认会将静态资源放在src/main/resources/static
目录下。所以如果你的静态文件在src/main/resources/static/assets/
目录下,路径应该设置为classpath:/static/assets/
。 -
这样,当用户请求
/assets/**
路径时,Spring 会在classpath:/static/assets/
目录中查找对应的文件。
-
-
-
7.3 配置视图解析器(ViewResolver)
- 感觉很少会用到来
-
7.3.1 pom.xml 加两个引用 支持jsp 的
-
<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency>
-
-
7.3.2 配置视图解析器
-
package com.lirui.springbootmoduledemo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册一个拦截器 registry.addInterceptor(new WebInterceptor()) // 设置拦截路径 .addPathPatterns("/api/**") // 排除不需要拦截的路径 .excludePathPatterns("/api/login", "/api/register"); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 配置 /assets/** 请求,映射到 classpath:/static/assets/ 文件夹 registry.addResourceHandler("/assets/**") .addResourceLocations("classpath:/static/assets/"); // 配置 /images/** 请求,映射到 classpath:/static/images/ 文件夹 registry.addResourceHandler("/images/**") .addResourceLocations("classpath:/static/images/"); } @Override public void configureViewResolvers(ViewResolverRegistry registry) { // 配置 JSP 视图解析器 registry.jsp().prefix("/WEB-INF/views1/").suffix(".jsp"); } }
-
-
7.3.3 创建jsp页面
-
-
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>hello</title> </head> <body> <h1>欢迎</h1> <h1>欢迎</h1> <h1>欢迎</h1> <h1>欢迎</h1> </body> </html>
-
-
-
7.3.4 写controller 一定用@Controller
-
package com.lirui.springbootmoduledemo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Controller @RequestMapping("/api") public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, World!"; } @GetMapping("/helloJSP") public String home() { // 返回 index.jsp 视图 return "hello"; } }
-
-
7.3.4 浏览器访问 乱码 无所谓 只要能请求到 就没大问题
-
7.3.5 工作原理
-
当 Spring MVC 中的控制器(Controller)返回一个视图名称时(比如
"
hello"
),视图解析器会根据配置的prefix
和suffix
来构造最终的 JSP 文件路径。- 控制器返回视图名称:hello
- 视图解析器会去
WEB-INF/views/
目录下查找名为hello.jsp的文件。 - 如果文件存在,Spring 会将该 JSP 文件渲染到响应中,返回给客户端。
-
-
7.4 CORS 配置(跨域资源共享)
-
7.4.1 什么是跨域?
-
**跨域(Cross-Origin)**是指浏览器在不同的域、协议、端口之间进行资源请求的行为。简单来说,当一个网页试图从不同的域名、端口号或协议(如
http://example.com
和https://example.com
)加载资源时,就涉及到“跨域”问题。
-
-
7.4.2 为什么会有跨域问题?
-
浏览器出于安全考虑,采用了 同源策略(Same-Origin Policy),即一个网页只能访问同一来源(同协议、同域名、同端口)的资源。这是为了防止恶意网站通过脚本获取用户的敏感数据或做其他恶意操作。
-
-
7.4.3 同源策略(Same-Origin Policy)
- 协议:
http
、https
。 - 域名:如
example.com
。 - 端口:如
8080
。 - 同源策略要求这三者必须完全相同才能进行资源的访问和交互。例如,
https://example.com
和http://example.com
是不同的源,因为协议不同;http://example.com:8080
和http://example.com:9090
是不同的源,因为端口不同。
- 协议:
-
7.4.4 跨域的场景
-
不同的域:
- 网站 A(
https://site-a.com
)想要访问网站 B(https://site-b.com
)的资源。
- 网站 A(
-
不同的协议:
- 网站 A(
http://site.com
)想要访问网站 B(https://site.com
)的资源,协议不同。
- 网站 A(
-
不同的端口:
- 网站 A(
http://site.com:8080
)想要访问网站 B(http://site.com:9090
)的资源,端口不同。
- 网站 A(
-
-
7.4.5 浏览器的跨域限制
-
浏览器出于安全原因,限制了网页脚本对跨域资源的访问。比如,如果你的网页在
http://localhost:3000
上,试图去请求http://api.example.com
上的资源,浏览器会默认阻止这种请求,称为 跨域请求(Cross-Origin Request)。跨域请求的类型
跨域请求可以分为两种类型:
-
简单请求(Simple Request):
-
请求方法是
GET
、POST
或HEAD
。 -
请求头只包括浏览器内置的标准头部(如
Content-Type
设置为application/x-www-form-urlencoded
、multipart/form-data
或text/plain
)。
-
-
复杂请求(Preflighted Request):
-
请求方法是
PUT
、DELETE
或自定义的方法,或者请求头包含了非标准的自定义头部(例如Authorization
,X-Custom-Header
等)。 -
在这种情况下,浏览器会先发送一个 预检请求(Preflight Request),以确认服务器是否允许实际的请求。
-
-
-
-
7.4.6 跨域的解决方案
-
7.4.6.1 CORS(跨域资源共享)
-
CORS(Cross-Origin Resource Sharing) 是一种浏览器和服务器之间的协议,它允许服务器声明哪些源(域、协议、端口)可以访问其资源。
-
当浏览器发起跨域请求时,浏览器会自动添加一些 CORS 相关的头部信息:
-
Origin
:表示请求发起的源(域、协议、端口)。 -
Access-Control-Allow-Origin
:服务器响应的头部,表示允许哪些源可以访问资源。
-
-
CORS 预检请求: 当浏览器发起一个复杂的跨域请求时(例如,方法是
PUT
、DELETE
或请求头包含自定义头),浏览器会先发起一个 OPTIONS 请求,即 预检请求(Preflight Request),询问服务器是否允许跨域请求。如果服务器返回适当的响应头,则允许实际请求的发送。 -
CORS 响应头:
-
Access-Control-Allow-Origin
:允许访问的源,*
表示所有域都可以访问,或者可以指定特定的域(如http://example.com
)。 -
Access-Control-Allow-Methods
:允许的 HTTP 方法(如GET
,POST
,PUT
,DELETE
)。 -
Access-Control-Allow-Headers
:允许的请求头。 -
Access-Control-Allow-Credentials
:是否允许带上身份凭证(如 Cookies)。 -
Access-Control-Max-Age
:预检请求的有效时间,表示浏览器在多长时间内不需要再次发送预检请求。
-
-
@Override public void addCorsMappings(CorsRegistry registry) { // 配置全局跨域 registry.addMapping("/**") .allowedOrigins("http://example.com") // 允许来自 example.com 的跨域请求 .allowedMethods("GET", "POST") // 允许的请求方法 .allowedHeaders("*"); // 允许所有请求头 }
-
-
7.4.6.2 JSONP(仅限 GET 请求)
-
在早期,浏览器没有支持 CORS 机制时,开发者常用 JSONP 来绕过跨域限制。JSONP 是通过
<script>
标签的跨域特性来发送请求的,但它只支持GET
请求,不支持发送其他类型的请求,如POST
。
-
-
7.4.6.3 服务器端代理(推荐)
-
一种常见的解决跨域问题的方式是通过设置 代理服务器。前端应用发送请求到同源的代理服务器,由代理服务器转发请求到实际的跨域资源服务器。代理服务器和跨域服务器之间的请求不受浏览器的同源策略限制。
-
-
-
-
7.5 消息转换器(Message Converters)
-
Spring MVC 使用
HttpMessageConverter
来将请求和响应的主体内容转换成 Java 对象或从 Java 对象转换为响应的格式(如 JSON、XML)。你可以定制消息转换器来支持自定义的序列化方式。 -
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // 创建 Jackson 转换器 MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(); // 创建 ObjectMapper 并进行配置 ObjectMapper objectMapper = new ObjectMapper(); // 排除 null 字段 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 设置日期格式 objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // 可以通过 Jackson 的 MixIn 来定制序列化规则 objectMapper.addMixIn(SomeClass.class, SomeClassMixIn.class); // 设置 ObjectMapper 到 Jackson 转换器 jacksonConverter.setObjectMapper(objectMapper); // 添加到转换器列表 converters.add(jacksonConverter); }
-
-
7.8 定制异常处理(@ExceptionHandler)(不推荐)
-
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { // 定制异常处理 resolvers.add(new MyCustomExceptionResolver()); } }
-
-
-
8. 支持文件上传与下载
-
8.1 文件上传
-
8.1.1 配置文件上传的基本设置
- 在 Spring Boot 中,文件上传的功能默认已经启用。但是,你可以在
application.properties
或application.yml
中配置一些上传限制(如文件大小)。 -
# 最大上传文件大小 spring.servlet.multipart.max-file-size=10MB # 最大请求数据大小 spring.servlet.multipart.max-request-size=10MB
- 在 Spring Boot 中,文件上传的功能默认已经启用。但是,你可以在
-
8.1.2 实现文件上传接口
-
@RestController @RequestMapping("/api/files") public class FileUploadController { @Value("${file.upload-dir}") private String uploadDir; // 用于存储文件的目录 // 文件上传接口 @PostMapping("/upload") public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) { try { // 获取文件名 String fileName = file.getOriginalFilename(); // 将文件存储到指定目录 Path path = Paths.get(uploadDir, fileName); Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); return ResponseEntity.ok("文件上传成功:" + fileName); } catch (IOException e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("文件上传失败:" + e.getMessage()); } } }
-
-
8.1.3 上传目录配置
- 你可以在
application.properties
文件中指定文件上传的目录。例如: -
# 文件存储路径 file.upload-dir=./uploads
- 你可以在
-
8.1.4 上传多个文件
-
@PostMapping("/uploadMultiple") public ResponseEntity<String> uploadMultipleFiles(@RequestParam("files") List<MultipartFile> files) { for (MultipartFile file : files) { // 保存每个文件 try { String fileName = file.getOriginalFilename(); Path path = Paths.get(uploadDir, fileName); Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("部分文件上传失败:" + e.getMessage()); } } return ResponseEntity.ok("所有文件上传成功"); }
-
-
-
8.2 文件下载
-
8.2.1 实现文件下载接口
-
下载文件的实现比较简单,通常使用
HttpServletResponse
来将文件内容写入响应流中,浏览器会自动处理并触发文件下载。 -
@RestController @RequestMapping("/api/files") public class FileDownloadController { @Value("${file.upload-dir}") private String uploadDir; // 文件存储目录 // 文件下载接口 @GetMapping("/download/{fileName}") public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) { try { // 获取文件路径 Path filePath = Paths.get(uploadDir).resolve(fileName).normalize(); // 如果文件不存在,抛出异常 Resource resource = new FileSystemResource(filePath); if (!resource.exists()) { throw new FileNotFoundException("文件未找到:" + fileName); } // 返回文件资源并设置下载的 Content-Disposition return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"") .body(resource); } catch (Exception e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } } }
-
-
8.2.2 设置响应头部以下载文件
-
通过
Content-Disposition
响应头可以让浏览器以下载的形式处理文件而不是直接显示内容。上述代码中已经设置了这个头部: -
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
-
-
-