18、Spring MVC 的高级技术(2)(spring笔记)

2.2 处理 multipart 请求

配置好了对multipart请求的处理后需要在控制器编写相关方法接受上传文件,要实现这一点,最简单的就是在控制器方法参数上添加@RequestPart注解。

首先需要修改一下表单:

...
...

说明:

标签将enctype属性设置为multipart/form-data,这会浏览器以multipart数据的形式提交表单,而不是以表单数据的形式进行提交。在multipart中,每个输入域都会对应一个part标签将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 getHeaders(java.lang.String s);
    public Collection 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已经在数据库中存在,那么SpittleRepositorysave()方法将会抛出DuplicateSpittleException异常,这意味着SpittleControllersaveSpittle()方法可能需要处理这个异常。

@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();
}

说明:上述方法中进行重定向是能够正常运行的,但是却不一定没有问题,当构建一些复杂的URLSQL查询语句的时候,使用这种字符串方式很容易出错,而且会有安全问题。,可以使用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对象以keyspitter的方式存入到了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";
}

你可能感兴趣的:(18、Spring MVC 的高级技术(2)(spring笔记))