2.2 处理 multipart 请求
配置好了对multipart
请求的处理后需要在控制器编写相关方法接受上传文件,要实现这一点,最简单的就是在控制器方法参数上添加@RequestPart
注解。
首先需要修改一下表单:
<form method="POST" th:object="${spitter}" enctype="multipart/form-data">
...
<label>Profile Picture</label>
<input type="file" name="profilePicture" accept="image/jpeg, image/png,image/gif" /><br/>
...
</form>
说明:<form>
标签将enctype
属性设置为multipart/form-data
,这会浏览器以multipart
数据的形式提交表单,而不是以表单数据的形式进行提交。在multipart
中,每个输入域都会对应一个part
。<input>
标签将type
设置为file
,同时设定接受多种图片格式。
现在需要修改控制器中的processRegistration()
方法,使其能够接受上传的图片。其中一种方式添加byte
数组参数,并为其添加@RequestPart
注解,如下:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegisteration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter, Errors errors){
...
}
说明:当注册表单提交的时候,profilePicture
属性将会给定一个byte
数组,这个数组中包含了请求中对应part
的数据(通过@RequestPart
指定)。如果用户提交表单的时候没有选择文件,那么这个数组会是空(而不是null
),接下来的任务就是保存文件了。
2.2.1 接受 MultipartFile
使用上传文件的原始的byte
比较简单但是功能有限。因此,Spring
提供了MultipartFile
接口,它为处理multipart
数据提供了内容更为丰富的对象。MultipartFile
接口如下:
public interface MultipartFile extends InputStreamSource {
String getName();
String getOriginalFilename();
String getContentType();
boolean isEmpty();
long getSize();
byte[] getBytes() throws IOException;
InputStream getInputStream() throws IOException;
void transferTo(File dest) throws IOException, IllegalStateException;
}
说明:MultipartFile
提供了获取上传文件byte
的方式,除此之外,还能获得原始的文件名、大小以及内容类型。还提供了一个InputStream
,用来将文件数据以流的方式进行读取。其中transferTo()
方法能够将上传文件写入到文件系统中。这里可以在控制器方法processRegisteration()
添加:
profilePicture.transferTo(new File("data/spittr/" + profilePicture.getOriginalFilename()));
说明:这里我们将文件保存到本地系统中,但是有时候可能出现故障,这里我们可以将文件上传到云端,让别人帮我们保存。
2.2.2 将文件保存到 Amazon S3中
相关内容可以在亚马逊官网(https://aws.amazon.com/cn/getting-started/tutorials/backup-files-to-amazon-s3/
),具体细节这里不做过多说明,下面给出相关代码,我们可以在控制器方法中调用:
private void saveImage(MultipartFile image) throws ImageUploadException{
try{
AWSCredentials awsCredentials = new AWSCredentials(s3AccessKey, s3SecretKey);
S3Service s3 = new RestS3Service(awsCredentials);
S3Bucket bucket = s3.getBucket("spittrImages");
S3Object imageObject = new S3Object(image.getOriginalFilename());
imageObject.setDataInputStream(image.getInputStream());
imageObject.setContentLength(image.getSize());
imageObject.setContentType(image.getContentType());
//设置权限
AccessControlList acl = new AccessControlList();
acl.setOwner(bucket.getOwner());
acl.grantPermission(GroupGrantee.ALL_USERS, Permission.PERMISSION_READ);
imageObject.setAcl(acl);
s3.putObject(bucket, imageObject);//保存图片
}catch (Exception e){
}
}
2.2.3 以Part的形式接受上传文件
Spring MVC
中也能接受javax.servlet.http.Part
作为控制器方法的参数。如果使用Part
来替换MultipartFile
的话,那么控制器方法processRegisteration()
的方法签名将会变成如下形式:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegisteration(@RequestPart("profilePicture") Part profilePicture,
@Valid Spitter spitter, Errors errors){
...
}
说明:就主体而言,Part
接口与MultipartFile
并没有太大区别,接口如下:
package javax.servlet.http;
public interface Part {
public InputStream getInputStream() throws java.io.IOException;
public String getContentType();
public String getName();
public String getSubmittedFileName();
public long getSize();
public void write(java.lang.String s) throws java.io.IOException;
public void delete() throws java.io.IOException;
public String getHeader(java.lang.String s);
public Collection<java.lang.String> getHeaders(java.lang.String s);
public Collection<java.lang.String> getHeaderNames();
}
说明:两个接口基本一致,但是也有些许差异,如getSubmittedFileName()
方法对应之前的getOriginalFilename()
方法。write()
方法对应之前的transforTo()
方法。将文件保存到本地系统如下:
profilePicture.write(new File("data/spittr/" + profilePicture.getOriginalFilename()));
三、处理异常
不管发生什么事情,Servlet
请求的输出都是一个Servlet
响应。如果在请求处理的时候,出现了异常,那它的输出依然会是Servlet
响应。异常必须要以某种方式转换为响应。Spring
提供了多种方式将异常转换为响应:
- 特定的
Spring
异常将会自动映射为指定的HTTP
状态码 - 异常上可以添加
@ResponseStatus
注解,从而将其映射为某一个HTTP
状态码。 - 在方法上可以添加
@ExceptionHandler
注解,使其用来出来异常。
3.1 将异常映射为HTTP状态码
Spring
的一些异常会默认映射为HTTP
状态码
Spring异常 | HTTP状态码 |
---|---|
BindException |
400 无效请求 |
ConversionNotSupportedException |
500 服务器内部错误 |
HttpMediaTypeNotAcceptableException |
406 不接受 |
HttpMediaTypeNotSupportedException |
415 不支持的媒体类型 |
HttpMessageNotReadableException |
400 无效请求 |
HttpMessageNotWritableException |
500 服务器内部错误 |
HttpRequestMethodNotSupportedException |
405 不支持的方法 |
MethodArgumentNotValidException |
400 无效请求 |
MissingServletRequestParameterException |
400 无效请求 |
MissingServletRequestPartException |
400 无效请求 |
NoSuchRequestHandlingMethodException |
404 请求未找到 |
TypeMismatchException |
400 无效请求 |
NoHandlerFoundException |
404 请求未找到 |
MissingPathVariableException |
500 服务器内部错误 |
上表中的异常一般会由Spring
自身抛出,作为DispatcherServlet
处理过程中或执行校验时出现问题的结果。如当DispatcherServlet
无法找到相关控制器方法则会抛出NoSuchRequestHandlingMethodException
异常,最终结果就是产生404
状态码的响应。尽管这些内置的映射很有用,但是对于应用所抛出的异常就不够用了。Spring
提供了一种机制,能够通过@ResponseStatus
注解将异常映射为HTTP
状态码。
在控制器SpittleController
中定义了如下方法,可能会产生404
状态码:
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(
@PathVariable("spittleId") long spittleId, Model model) {
Spittle spittle = spittleRepository.findOne(spittleId);
if(spittle == null){
throw new SpittleNotFoundException();
}
model.addAttribute(spittle);
return "spittles";
}
说明:该方法中,是通过给定ID
查找Spittle
对象,如果没有找到,则抛出异常SpittleNotFoundException
:
public class SpittleNotFoundException extends RuntimeException{
}
说明:在实际请求中,如果没有找到Spittle
对象,则SpittleNotFoundException
(默认)将会产生500
状态码(服务器内部错误),当然这显然提示信息不够明确,我们可以对这种默认行为进行变更,让其抛出404
(请求未找到)这个更为精准的响应状态码。
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException{
}
3.2 编写异常处理的方法
有时候可能将异常映射成简单的HTTP
状态码并不够用,我们想在响应中不仅要包括状态码,还要包含所产生的错误,此时就要按照处理请求的方式来处理异常了。
假设用户视图创建的Spittle
已经在数据库中存在,那么SpittleRepository
的save()
方法将会抛出DuplicateSpittleException
异常,这意味着SpittleController
的saveSpittle()
方法可能需要处理这个异常。
@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
try{
spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),
form.getLongitude(), form.getLatitude()));
return "redirect:/spittles";
}catch(DuplicateSpittleException e){
return "error/duplicate";
}
}
说明:方法本身没有什么特别,但是可以有两个路径,每个路径会有不同的输出。如果能让此方法只关注正确的路径,而让其他方法处理异常的话,将是不错的选。此时方法就变成如下形式:
@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),
form.getLongitude(), form.getLatitude()));
return "redirect:/spittles";
}
说明:可以看到方法内部没有处理异常,也没有抛出异常,此时如果要处理异常可以给SpittleController
控制器添加一个新的方法,它会处理此异常情况:
@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle(){
return "error/duplicate";
}
说明:此控制器方法使用@ExceptionHandler
注解进行标识,它可以处理本控制器中所有其他方法发生的DuplicateSpittleException
异常,而且不必在其他方法中声明或抛出异常。
四、为控制器添加通知
如上所述,如果控制类的特定切面能够运用到整个应用程序中的所有控制器,那么这将会便利很多。也就是说,如果如果每个控制器都需要抛出DuplicateSpittleException
异常,那统一为所有控制器添加一个此异常通知将会更加简便。
这里其实我们可以创建一个基础的控制类,让所有控制器类都扩展于此类,从而继承通用的@ExceptionHandler
方法。而在Spring3.2
为这类问题引入了一个新的解决方案:控制器通知。控制器通知(controller advice
)是任意带有@ControllerAdvice
注解的类,这个类包含一个或多个如下类型的方法:
-
@ExceptionHandler
注解标注的方法; -
@InitBinder
注解标注的方法; -
@ModelAttribute
注解标注的方法
在带有@ControllerAdvice
注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping
注解的方法上。同时,@ControllerAdvice
注解本身已经使用了@Component
,因此,它所标注的类将会自动被组件扫描获取到。此注解的一个最为使用的场景就是将所有的@ExceptionHandler
方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。
@ControllerAdvice
public class AppWideExceptionHandler{
@ExceptionHandler(DuplicateSpittleException.class)
public String duplicateSpittleHandler(){
return "error/duplicate";
}
}
五、跨重定向请求传递数据
之前有说过,Spring
提供了重定向、请求转发等方法,但是它还为重定向功能提供了一些其他的辅助功能。具体来讲,正在发起重定向功能的方法该如何发送数据给重定向的目标方法呢?我们知道,如果是请求转发,则上一次转发之前处理的数据结果会保留到转发之后,但是如果是重定向,则是发起了一个新的GET
请求,上一次请求的数据不会跟随下一次请求,此时请求必须要自己计算数据。
显然,对于重定向来说,模型并不能来传递数据。但是也有一些其他方案,能够从发起重定向的方法传递数据给处理重定向的方法中:
- 使用
URL
模版以路径变量和/或查询参数的形式传递数据 - 通过
flash
属性发送数据
5.1 通过URL模版进行重定向
在之前表单校验时在方法processRegistration()
中重定向设这样做的:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(@Valid Spitter spitter, Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
说明:上述方法中进行重定向是能够正常运行的,但是却不一定没有问题,当构建一些复杂的URL
或SQL
查询语句的时候,使用这种字符串方式很容易出错,而且会有安全问题。,可以使用URL
模版进行修改:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(@Valid Spitter spitter, Model model, Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
return "redirect:/spitter/{username}";
}
说明:首先是将username
存入到模型中,然后通过{username}
将相关值取出。这里username
是作为占位符填充到URL
模版中,而不是直接连接到重定向字符串中,所以username
中所有的不安全的字符都会进行转义,这样更加安全。
除此之外,模型中所有其他的原始类型都可以添加到URL
中作为查询参数。
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(@Valid Spitter spitter, Model model, Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
model.addAttribute("spitterId", spitter.getId());
return "redirect:/spitter/{username}";
}
说明:可以看到,这里我们向模型中还添加了spitterId
属性,但是在重定向中并没有对应的占位符,此时它会自动以查询参数的形式附加到重定向的URL
上,也就是重定向的路径为"/spitter/Tom?spitterId=26"
。
5.2 使用 flash 属性
在上面的方式中我们只是发送一些简单的数据,但加入我们要发送实际的Spitter
对象呢?当然你可以传递相关对象ID
,然后到数据库中查询,但是在重定向之前我们已经有对象了,重定向之后要再次查询显得多此一举了。
如果要传递对象,我们不能像路径变量或查询参数那么容易地发送对象,它只能设置为模型中的属性。但是模型在重定向之后是会消失的,因此,我们要将对象放到一个位置,使其能够在重定向中存活下来。
其中一种方案就是将对象放在会话中。会话能长期存在,但是我们需要负责在重定向之后在会话中将此对象清理掉。Spring
认为这是一个不错的选择,但是却不认为我们需要对这些对象数据进行管理,于是提供了将数据发送为flash
属性的功能。flash
属性会一直携带这些数据直到下一次请求,然后才会消失。
Spring
提供了通过RedirectAttributes
设置flash
属性的方法,这是Spring3.1
中引入的Model
的子接口。提供了一组addFlashAttribute()
方法来添加flash
属性。
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(@Valid Spitter spitter, RedirectAttributes model, Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
model.addFlashAttribute("spitter", spitter);
return "redirect:/spitter/{username}";
}
说明:这里我们将Spitter
对象以key
为spitter
的方式存入到了flash
中,当然如果不传递key
,也可以自行推断出,但不推荐。其实这种方式原理很简单,就是在重定向之前将对象存入到会话中,重定向之后将对象从会话中取出,并将对象从会话中清理掉。
此时我们就需要更新showSpitterProfile()
方法了,从数据库中查找之前,首先需要从模型中检查Spitter
对象,如果模型中存在,就什么都不做,否则,才会从数据库中查找:
@RequestMapping(value="/{username}", method=RequestMethod.GET)
public String showSpitterProfile(@PathVariable String username, Model model) {
if(!model.containsAttribute("spitter")){
model.addAttribute(spittleRepository.findByUsername(username));
}
return "profile";
}