【0】README
1)本文部分文字描述转自:“Spring In Action(中/英文版)”,旨在review “spring(7)spring mvc 的高级技术” 的相关知识;
2)本文将会看到如何编写控制器来处理文件上传,如何处理控制器所抛出的异常,以及如何在模型中传递数据,使其能够在重定向之后仍然存活;
【1】spring mvc 配置的替代方案
【1.1】 自定义 DispatcherServlet配置
【1.2】添加其他的Servlet 和 Filter
【1.3】在web.xml 中声明 DispatcherServlet
【2】处理multipart 形式的数据
1)应用需求:Spittr 在新用户注册的时候需要上传头像,在发布Spittle的时候需要插入图片(同微博一样);
2)intro:图片是二进制数据,multipart格式的数据 会将一个表单拆分为多个部分(part),每个部分对应一个输入域;
3)在一般表单输入域中,它所对应的部分会防止文本型数据,但如果上传文件的话,它所对应的部分可以是二进制,下面展现了 multipart的请求体;
【2.1】配置multipart解析器
1)intro:DispatcherServlet并没有实现任何解析 multipart 请求数据的功能,它将该任务委托给了 spring 中 MultipartResolver 接口的实现,通过这个实现类来解析multipart 请求中的内容;
2)spring3.1 开始,内置了两个 MultipartResolver 的实现;
2.1)CommonsMultipartResolver:使用 Jakarta Commons FileUpload 解析 multipart请求;
2.2)StandardServletMultipartResolver:依赖于Servlet3.0 对 multipart 请求 的支持;(干货——优选方案,因为它不依赖于第三方库)
【2.1.1】使用 Servlet3.0解析 multipart请求
1)在spring 应用上下文中,将StandardServletMultipartResolver 声明为bean :
@Bean
public MultipartResolver multipartResolver() throws IOException {
return new StandardServletMultipartResolver();
}
2)如何限制 StandardServletMultipartResolver 的工作方式呢?(如限制用户上传文件的大小和文件类型)(干货——我们就不会直接创建 DispatcherServlet实例并将其注册到 Servlet上下文中)
2.1)看个荔枝:最基本的 DispatcherServlet multipart配置,它将临时路径设置为 "/tmp/spittr/uploads"
DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/spittr/uploads"));
2.2)如果配置DispatcherServlet 的 Servlet初始化类继承了 AbstractAnnotationConfigDispatcherServletInitializer 或 AbstractDispatcherServletInitializer,那么我们就不会直接创建 DispatcherServlet实例并将其注册到 Servlet上下文中;这样的话,将不会有对 Dynamic Servlet registration 的引用供我们使用了。但我们可以通过重载 customizeRegistration() 方法 来配置 multipart 的具体细节;
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/spittr/uploads"));
}
对以上代码的分析(Analysis):上述代码所使用的 只有一个参数的 MultipartConfigElement 构造器,指定的是文件系统中的一个绝对目录,上传文件将会临时写入到该目录中;
3)处理设置临时路径,还可以设置其他参数(parameters):
parameter1)上传文件的最大容量(以字节为单位),默认是没有限制的;
parameter2)整个mulitpart 请求的最大容量(以字节为单位),不会关心有多少个part以及每个part的大小,默认是没有限制的;
parameter3)在上传的过程中,如果文件大小得到了一个指定最大容量(以字节为单位),将会写入到临时文件路径中。默认值为0.也就是所上传的文件都会写入到磁盘上;
3.1)看个荔枝:限制文件大小不超过2M,整个请求不超过4M,而且所有的文件都写到磁盘上,则设置为:
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/spittr/uploads",
2097152, 4194304, 0));
}
Supplement)使用xml 配置来设置的话,如下:
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
<multipart-config>
<location>/tmp/spittr/uploads</location>
<max-file-size>2097152</max-file-size>
<max-request-size>4194304</max-request-size>
</multipart-config>
</servlet>
【2.1.2】 配置 JAKARTA COMMONS FILEUPLOAD MULTIPART RESOLVER
1)intro:如果我们需要将应用部署到非 Servlet3.0 容器中,就使用该配置;spring 内置了CommonsMultipartResolver,可以作为 StandardServletMultipartResolver 配置的替代方案;
2)如何配置CommonsMultipartResolver
2.1)将CommonsMultipartResolver 声明为 spring bean的 简单方式如下:
@Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
2.2)CommonsMultipartResolver:不会强制要求设置临时文件路径,默认case下,这个路径就是 Servlet容器的临时目录;不过通过 updateTempDir属性类设置不同 位置;
@Bean
public MultipartResolver multipartResolver() throws IOException {
CommonsMultipartResolver multipartResolver =
new CommonsMultipartResolver();
multipartResolver.setUploadTempDir(
new FileSystemResource("/tmp/spittr/uploads"));
return multipartResolver;
}
2.3)看个荔枝: 设置最大的文件容量为2M,最大的内存大小为0字节(所有的文件都会写到磁盘中),与MultipartConfigElement 不同的是,我们无法设置 multipart 请求整体的最大容量;
@Bean
public MultipartResolver multipartResolver() throws IOException {
CommonsMultipartResolver multipartResolver =
new CommonsMultipartResolver();
multipartResolver.setUploadTempDir(
new FileSystemResource("/tmp/spittr/uploads"));
multipartResolver.setMaxUploadSize(2097152);
multipartResolver.setMaxInMemorySize(0);
return multipartResolver;
}
【2.2】处理multipart请求
1)intro:保存图片到文件系统有两种方式(method)
method1)multipartFile;
method2)part形式
2)编写spring 控制器来接收上传的文件: 最常见的方式就是在某个控制器方法上添加 @RequestPart注解;
2.1)修改前台模板添加上传图片插件;(省略)
2.2)修改控制器方法
@RequestMapping(value="/register", method=POST)
public String processRegistration(
@RequestPart("profilePicture") byte[] profilePicture, // highlight line.
@Valid Spitter spitter,
Errors errors) {
//...
}
对以上代码的分析(Analysis):
A1)profilePicture属性:将会给定一个 byte数组,这个数组中包含了请求中对应的part数据(通过@RequestPart类指定);如果用户提交表单的时候没有选择文件,那么这个数组是空的(而不是null);
A2)获取到图片数据后:processRegistration方法接下来就是将文件保存到某个位置了;(下面讲如何处理文件的存储)
【2.2.1】接收 MultipartFile
1)intro:spring提供了MultipartFile接口,它为处理multipart数据提供了内容丰富的对象;
对以上代码的分析(Analysis):
A1)Multipart提供了获取上传文件byte的方式,但是它所提供的功能并不仅限于此,还能获得原始的文件名,大小以及内容类型;
A2)它还提供了一个 InputStream,用来将文件数据以流的方式进行读取;
A3)MultipartFile 还提供了一个便利的 transferTo()方法,能够帮助我们将上传的文件写入到文件系统中;
profilePicture.transferTo(
new File("/data/spittr/" + profilePicture.getOriginalFilename()));
【2.2.2】将文件保存到 Amazon S3中(省略)
【2.2.3】以Part的形式接收上传的文件
1)intro:如果你需要将应用程序部署到 Servlet3.0的容器中,那么会有Multipart的一个替代方案——spring mvc 接收 javax.servlet.http.Part 作为控制器方法的参数;
2)若使用 Part来替换 MultipartFile的话,那么 processRegistration() 方法签名会变成如下形式:
@RequestMapping(value="/register", method=POST)
public String processRegistration(
@RequestPart("profilePicture") Part profilePicture,
@Valid Spitter spitter,
Errors errors) {
//...
}
3)Part接口方法一览:
package javax.servlet.http;
import java.io.*;
import java.util.*;
public interface Part {
public InputStream getInputStream() throws IOException;
public String getContentType();
public String getName();
public String getSubmittedFileName();
public long getSize();
public void write(String fileName) throws IOException;
public void delete() throws IOException;
public String getHeader(String name);
public Collection<String> getHeaders(String name);
public Collection<String> getHeaderNames();
}
对以上代码的分析(Analysis):
A1)Part方法与 MultipartFile 方法有些类似:如getSubmittedFileName() 方法 同 getOriginalFilename()方法类似,write()方法 与 transferTo()方法类似;
A2)借助于该方法(write方法),可以将上传的文件写入文件系统中:
profilePicture.write("/data/spittr/" +
profilePicture.getOriginalFilename());
Attention)只有使用 MultipartFile 的时候才需要 MultipartResolver;
【3】处理异常
1)intro:spring提供了多种方式将异常转换为响应:
way1)特定的spring 异常将会自动映射为指定 的 HTTP 状态码;
way2)异常上可以添加 @ResponseStatus注解,从而将其映射为某一个HTTP 状态码;
way3)在方法上添加 @ExceptionHandler 注解,使其用来处理异常;
2)处理异常的最简单的方法:就是将其映射到 HTTP 状态码上,进而放到响应中;
【3.1】将异常映射为 HTTP 状态码
1)intro:spring 的一些异常会默认映射为 HTTP状态码;
对上表的分析(Analysis): 以上异常一般会由 spring 自身抛出,作为 DispatcherServlet处理过程中或执行校验时出现问题的结果;
2)@ResponseStatus注解:spring 提供了一种机制,通过@ResponseStatus注解 将异常也会为 HTTP 状态码;
2.1)不加 @ResponseStatus注解的case:SpittleNotFoundException 将会产生500状态码的响应,实际上,如果出现任何没有映射的异常,响应都会带有500状态码;
2.2)加上 @ResponseStatus注解的case:使用该注解将 SpittleNotFoundException 映射为 HTTP 状态码 404;
@ResponseStatus(value=HttpStatus.NOT_FOUND,
reason="spittle not found")
public class SpittleNotFoundException extends RuntimeException{
}
【3.2】编写异常处理的方法
1)problem+solution:
1.1)problem:如果我们想在响应中不仅要包括状态码,还要包含所产生的错误,怎么来处理?
1.2)solution:我们不能将异常视为 HTTP 错误了,而是要按照处理请求的方式来处理异常;
2)看个荔枝:
对以上代码的分析(Analysis):
若视图创建的Spittle 已存在数据库中,则抛出DuplicateSpittleException,这样一来,该方法就有两条路径,每个路径有不同的输出;
3)如何让saveSpittle方法只关注正确的路径,而让其他方法处理异常?
3.1)step1:首先将saveSpittle方法中的异常处理剥离掉;
3.2)step2:为SpittleController 添加新方法,处理抛出的异常;
对@ExceptionHandler注解的分析:该注解能处理同一个控制器中所有处理器方法所抛出的异常;所以我们不用在每个可能抛出 DuplicateSpittleException 方法中添加异常处理代码,这一个方法就涵盖了所有功能;
【4】为控制器添加通知
1)intro:有没有一种方法能够处理所有控制器中处理器方法所抛出的异常呢?
2)spring3.2 提供了solution:控制器通知,它是任意带有 @ControllerAdvice注解 的类,这个类会包含一个或多个如下类型的方法;
func1)@ExceptionHandler 注解标注的方法;
func2)@InitBinder注解标注的方法;
func3)@ModelAttribute注解标注的方法;
Attention)在带有@ControllerAdvice注解的类中,以上这些方法会运用到整个应用程序所有控制器中带有 @RequestMapping注解的方法上;
3)@ControllerAdvice注解最为实用的一个case是:将所有 @ExceptionHandler 方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理了;(干货——@ControllerAdvice注解最为实用的一个case)
4)看个荔枝:如我们想将DuplicateSpittleException 的处理方法用到整个应用程序的所有控制器上;
【5】跨重定向请求传递数据
1)传递数据;
1.1)传递简单数据(如String,int类型):使用 URL 模板进行重定向;
1.2)传递复杂数据(如对象):使用 Flash 属性;
2.1)problem:正在发起重定向功能的方法该如何发送数据给重定向的目标方法呢?一般来讲,当一个处理器方法完成后,该方法所指定的模型数据将会copy 到 请求中,并作为请求中的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发过程中,请求属性能够得以保存;但是重定向(redirect) 的case就不同了;
2.2)solution:
如下图所示,当控制器的结果是重新向的话,原始的请求就结束了,并且会发出一个新的 GET 请求;原始请求中所带有的模型数据也就消亡了;
Attention)显然,对于重定向来说,模型并不能用来传递数据;(干货——对于重定向来说,模型并不能用来传递数据)
3)其他数据传递方案:能够从发起重定向的方法传递数据给处理重定向方法中:
way1)使用 URL 模板以路径变量 或/和 查询参数的形式传递数据;
【5.1】 通过URL 模板进行重定向
1)通过路径变量和查询参数传递数据;(以下使用String连接的写法很危险)
return "redirect:/spitter/{username}";
2)spring还提供了使用模板的方式来定义重定向URL:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(
Spitter spitter, Model model) {
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
return "redirect:/spitter/{username}";
}
对以上代码的分析:username 作为占位符填充到了URL 模板中,所以username中所有不安全字符都会进行转义;
3)除此之外,模型中所有其他的原始类型值都可以添加到 URL中作为查询参数;
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration2(Spitter spitter, Model model) {
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername()); // highlight line.
model.addAttribute("spitterId", spitter.getId()); // highlight line.
return "redirect:/spitter/{username}";
}
对以上代码的分析: 因为模型中的spitterId 属性没有匹配重定向URL 中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上;
Attention)通过路径变量和查询参数传递数据 有一个限制: 它只能用来发送简单的数据(如String类型 和 数字的值);
【5.2】使用flash属性(发送复杂数据)
1)problem+solution:
1.1)problem:发送实际的 Spitter对象,而不是简单的int类型数据;
1.2.1)schema1:将Spitter对象 放入到 会话中,然后重定向后再将其从会话中取出;
1.2.2)schema2:spring 提供了提供了将数据发送为 flash 属性的功能。flash 属性会一直携带这些数据直到下一次请求才会消失;(干货——flash属性的作用)
2)RedirectAttributes 提供了一组 addFlashAttribute() 方法来添加flash属性,如下:
3)通过flash属性传递数据的原理:在重定向前,所有的flash属性都会复制到会话中,重定向后,存在会话中的flash 属性会被取出,并从会话转移到模型中,如下图所示:
4)看个荔枝: