Spring實戰(zhàn)(七)-Spring MVC的高級技術(shù)

本文基于《Spring實戰(zhàn)(第4版)》所寫呢燥。

Spring MVC配置的替代方案

自定義DispatcherServlet配置

除了我們之前在SpitterWebAppInitializer中所編寫的三個方法僅僅是必須要重載的abstract方法,AbstractAnnotationConfigDispatcherServletInitializer所完成的事情其實比看上去的要多。

此類的方法之一就是customizeRegistration()甜紫。
在AbstractAnnotationConfigDispatcherServletInitializer將DispatcherServlet注冊到Servlet容器中之后薪贫,就會調(diào)用customizeRegistration(),并將Servlet注冊后得到的Registration.Dynamic傳遞進來已卸。通過重載customizeRegistration()方法佛玄,我們可以對DispatcherServlet進行額外的配置。

例如累澡,可以在Spring MVC中處理請求和文件上傳梦抢。如果計劃使用Servlet 3.0對multipart配置的支持,那么需要使用DispatcherServlet的registration來啟動multipart請求永乌。我們可以重載customizeRegistration()方法來設(shè)置MultipartConfigElement惑申,如下所示:

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        // 存入文件路徑;單個文件大小不超過2MB翅雏;整個請求不超過4MB圈驼;所有的文件都要寫入磁盤中
        registration.setMultipartConfig(new MultipartConfigElement("/Users/Shangnan/Documents/service_upload/springinaction/spittr"
                                        ,2097152,4194304, 0));
    }

借助customizeRegistration()方法中的ServletRegistration.Dynamic,我們能夠完成多項任務(wù)望几,包括通過調(diào)用setLoadOnStartup()設(shè)置load-on-startup優(yōu)先級绩脆,通過setInitParameter()設(shè)置初始化參數(shù),通過調(diào)用setMultipartConfig()配置Servlet 3.0對multipart的支持橄抹。

添加其他的Servlet和Filter

基于Java的初始化器(initializer)的一個好處就在于我們可以任意數(shù)量的初始化器類靴迫。因此,如果我們想往Web容器中注冊其他組件的話楼誓,只需要創(chuàng)建一個新的初始化器就可以了玉锌。最簡單的方式就是實現(xiàn)Spring的WebApplicationInitializer接口。

例如疟羹,如下的程序展示了如何創(chuàng)建WebApplicationInitializer實現(xiàn)并注冊一個Servlet主守。

package com.myapp.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import com.myapp.MyServlet;

public class MyServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
         Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);    // 注冊 Servlet
         myServlet.addMapping("/custom/**");   // 映射 Servlet
    }
}

這個初始化器注冊了一個Servlet并將其映射到一個路徑上禀倔。
類似地,我們還可以創(chuàng)建新的WebApplicationInitializer實現(xiàn)來注冊Listener和Filter参淫。例如救湖,如下程序展示了注冊Filter的WebApplicationInitializer

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
         Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);    // 注冊 Filter
         filter.addMappingForUrlPatterns(null, false, "/custom/**");   // 添加Filter的映射路徑
    }

如果只是注冊Filter,并且該Filter只會映射到DispatcherServlet上的話涎才,那么在還有一種快捷方式鞋既,所需要做的僅僅是重載AbstractAnnotationConfigDispatcherServletInitializer的getServetFilters()方法。例如:

@Override
protected Filter[] getServletFilters(){
     return new Filter[] { new MyFilter() };
}

這個方法返回的是一個javax.servlet.Filter的數(shù)組耍铜,說明可以返回任意數(shù)量的Filter邑闺。

在web.xml中聲明DispatcherServlet

在典型的Spring MVC應(yīng)用中,我們會需要DispatcherServlet和ContextLoaderListener棕兼。如下是一個基本的web.xml文件检吆,它按照傳統(tǒng)的方式搭建了DispatcherServlet和ContextLoaderListener。

<?xml version="1.0" encoding="UTF-8" ?>
<web-app version="2.5"
     xmlns="http://java.sun.com/xml/ns/javaee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
           http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
     <context-param>
         <param-name>contextConfigLocation</param-name>
         <!--設(shè)置根上下文配置文件位置-->
         <param-value>/WEB-INF/spring/root-context.xml</param-value>
     </context-param>

     <!--注冊ContextLoaderListener-->
     <listener>
        <listener-class>
           org.springframework.web.context.ContextLoaderListener
        </listener-class>
     </listener>

     <!--注冊DispatcherServlet-->
     <servlet>
         <servlet-name>appServlet</servlet-name>
         <servlet-class>
             org.springframework.web.servlet.DispatcherServlet
         </servlet-class>
     </servlet>

     <!--將DispatcherServlet映射到“/”-->
     <servlet-mapping>
         <servlet-name>appServlet</servlet-name>
         <url-pattern>/</url-pattern>
     </servlet-mapping>

</web-app>

ContextLoaderListener和DispatcherServlet各自都會加載一個Spring應(yīng)用上下文程储。上下文參數(shù)contextConfigLocation指定了一個XML文件的地址蹭沛,這個文件定義了根應(yīng)用上下文,它會被ContextLoaderListener加載章鲤。本例中摊灭,根上下文會從“/WEB-INF/spring/root-context.xml”中加載bean定義。

DispatcherServlet會根據(jù)Servlet的名字找到一個文件败徊,并基于該文件加載應(yīng)用上下文帚呼。本例中,Servlet的名字是appServlet皱蹦,因此DispatcherServlet會從“/WEB-INF/appServlet-context.xml”文件中加載其應(yīng)用上下文煤杀。

也可以指定DispatcherServlet配置文件的位置,需要在Servlet上指定一個contextConfigLocation初始化參數(shù)沪哺。例如沈自,如下的配置,DispatcherServlet會從“/WEB-INF/spring/appServlet/servlet-context.xml”加載它的bean:

    <servlet>
         <servlet-name>appServlet</servlet-name>
         <servlet-class>
             org.springframework.web.servlet.DispatcherServlet
         </servlet-class>
         <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
               /WEB-INF/spring/appServlet/servlet-context.xml
            </param-value>
         </init-param>
         <load-on-startup>1</load-on-startup>
     </servlet>

要在Spring MVC中使用基于Java的配置辜妓,我們需要告訴DispatcherServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext枯途,這是一個WebApplicationContext的實現(xiàn)類,它會加載Java配置類籍滴,而不是使用XML酪夷,要實現(xiàn)這種配置,我們可以設(shè)置contextClass上下文參數(shù)以及DispatcherServlet的初始化參數(shù)孽惰。如下的程序清單展現(xiàn)了一個新的web.xml晚岭,在這個文件中,它所搭建的Spring MVC使用基于Java的Spring配置:

<?xml version="1.0" encoding="UTF-8" ?>
<web-app version="2.5"
     xmlns="http://java.sun.com/xml/ns/javaee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
           http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
     <context-param>
         <param-name>contextClass</param-name>
         <!--使用Java配置-->
         <param-value> 
             org.springframework.web.context.support.
                AnnotationConfigWebApplicationContext
         </param-value>
     </context-param>
     <context-param>
         <param-name>contextConfigLocation</param-name>
         <!--指定根配置類-->
         <param-value>com.habuma.spitter.config.RootConfig</param-value>
     </context-param>
     
     <listener>
        <listener-class>
           org.springframework.web.context.ContextLoaderListener
        </listener-class>
     </listener>
    <servlet>
         <servlet-name>appServlet</servlet-name>
         <servlet-class>
             org.springframework.web.servlet.DispatcherServlet
         </servlet-class>
         <init-param>
            <param-name>contextClass</param-name>
            <param-value>
              org.springframework.web.context.support.
                AnnotationConfigWebApplicationContext
            </param-value>
         </init-param>
         <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
               com.habuma.spittr.config.WebConfigConfig
            </param-value>
         </init-param>
         <load-on-startup>1</load-on-startup>
     </servlet>
     <servlet-mapping>
         <servlet-name>appServlet</servlet-name>
         <url-pattern>/</url-pattern>
     </servlet-mapping>

</web-app>

處理multipart形式的數(shù)據(jù)

Spittr應(yīng)用在兩個地方需要文件上傳勋功。當新用戶注冊應(yīng)用的時候坦报,我們希望他們能夠上傳一張圖片辅甥,從而與他們的個人信息相關(guān)聯(lián)。當用戶提交新的Spittle時燎竖,除了文本消息以外,他們可能還會上傳一張照片要销。

對于傳送二進制數(shù)據(jù)构回,如上傳圖片,與典型的基于文本的表單提交有所不同疏咐,multipart格式的數(shù)據(jù)會將一個表單拆分為多個部分(part)纤掸,每個部分對應(yīng)一個輸入域。在一般的表單輸入域中浑塞,它所對應(yīng)的部分中會放置文本型數(shù)據(jù)借跪,但是如果上傳文件的話,它所對應(yīng)的部分是二進制酌壕,下面展示了multipart的請求體:

multipart的請求體

在這個multipart的請求中掏愁,我們可以看到profilePicture部分與其他部分明顯不同。除了其他內(nèi)容以外卵牍,它還有自己的Content-type頭果港,表明它是一個JPEG圖片。盡管不一定那么明顯糊昙,但profilePicture部分的請求體是二進制數(shù)據(jù)辛掠,而不是簡單的文本。

在編寫控制器方法之前释牺,我們必須要配置一個multipart解析器萝衩,通過它來告訴DispatcherServlet該如何讀取multipart請求。

配置multipart解析器

DispatcherServlet并沒有實現(xiàn)任何解析multipart請求數(shù)據(jù)的功能没咙。它將該任務(wù)委托給了Spring中MultipartResolver策略接口的實現(xiàn)猩谊,通過這個實現(xiàn)類解析multipart請求中的內(nèi)容。從Spring 3.1開始祭刚,Spring內(nèi)置了兩個MultipartResolver的實現(xiàn)供我們選擇:

  • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart請求预柒;
  • StandardServletMultipartResolver:依賴于Servlet 3.0對multipart請求的支持(始于Spring 3.1)

一般來講,StandardServletMultipartResolver會是優(yōu)選袁梗。它使用Servlet所提供的功能支持宜鸯,并不需要依賴任何其他的項目。如果我們需要將應(yīng)用部署到Servlet 3.0之前的容器中遮怜,或者還沒有使用Spring 3.1或者更高版本淋袖,那就需要使用CommonsMultipartResolver了。

StandardServletMultipartResolver沒有構(gòu)造器參數(shù)锯梁,也沒有要設(shè)置的屬性即碗,這樣焰情,在Spring應(yīng)用上下文中,將其聲明為bean就會非常簡單剥懒,如下所示:

@Bean
public MultipartResolver multipartResolver() throws IOException {
    return new StrandardServletMultipartResolver();
}

如果想配置StrandardServletMultipartResolver的限制條件内舟,不在Spring中配置,而是要在Servlet中指定multipart的配置初橘。至少验游,我們必須要指定在文件上傳的過程中,所寫入的臨時文件路徑保檐。如果不設(shè)定這個最基本配置的話耕蝉,就無法正常工作了。具體來講夜只,我們必須要在web.xml或Servlet初始化類中垒在,將multipart的具體細節(jié)作為DispatcherServlet配置的一部分。

如果我們采用Servlet初始化類的方式來配置DispatcherServlet的話扔亥,這個初始化類應(yīng)該已經(jīng)實現(xiàn)了WebApplicationInitializer场躯,那我們可以在Servlet registration上調(diào)用setMultipartConfig()方法,傳入一個MultipartConfigElement實例旅挤。我們可以通過重載customizeRegistration()方法來配置multipart的具體細節(jié)推盛。

MultipartConfigElement的構(gòu)造器所能接受的參數(shù)如下:

  • 上傳文件的臨時路徑。
  • 上傳文件的最大容量(以字節(jié)為單位)谦铃。默認是沒有限制的耘成。
  • 整個multipart請求的最大容量(以字節(jié)為單位),不會關(guān)心有多少個part以及每個part的大小驹闰。默認是沒有限制的瘪菌。
  • 在上傳的過程中,如果文件大小達到了一個指定最大容量(以字節(jié)單位)嘹朗,將會寫入到臨時文件路徑中师妙。默認值為0,也就是所上傳的文件都會寫入到磁盤上屹培。

例如默穴,假設(shè)我們想限制文件的大小不超過2MB,整個請求不超過4MB褪秀,而且所有的文件都要寫到磁盤中蓄诽。下面的代碼使用MultipartConfigElement設(shè)置了這些臨界值:

@Override
protected void customizeRegistration(Dynamic registration) {
   registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194394, 0));
}

如果我們使用更為傳統(tǒng)的web.xml來配置MultipartConfigElement的話,那么可以使用<servlet>中<multipart-config>元素媒吗,如下所示:

<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>4194394</max-request-size>
    </multipart-config>
</servlet>

除了StrandardServletMultipartResolver仑氛,我們還可以使用Spring內(nèi)置的CommonsMultipartResolver。聲明如下:

@Bean
public MultipartResolver multipartResolver() {
    return new CommonsMultipartResulver();
}

CommonsMultipartResulver不會強制要求設(shè)置臨時文件路徑。默認情況下锯岖,這個路徑就是Servlet容器的臨時目錄介袜。不過,通過設(shè)置uploadTempDir屬性出吹,我們可以將其指定為一個不同的位置遇伞。實際上,我們還可以按照這種方式指定其他的multipart上傳細節(jié)捶牢,也就是設(shè)置CommonsMultipartResolver的屬性鸠珠。例如,如下的配置就等價于我們通過MultipartConfigElement所配置的StrardardServletMultipartResolver:

@Bean
public MultipartResolver multipartResolver() {
   CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
    multipartResolver.setMaxUploadSize(2097152);
    multipartResolver.setMaxInMemorySize(0);
    return multipartResolver;
}

在這里叫确,我們將最大的文件容量設(shè)置為2MB,最大的內(nèi)存大小設(shè)置為0字節(jié)芍锦,表明不能上傳超過2MB的文件竹勉,并不管文件的大小如何,所有的文件都會寫到磁盤中娄琉。但是與MultipartConfigElement有所不同次乓,我們無法設(shè)定multipart請求整體的最大容量。

處理multipart請求

要實現(xiàn)控制器方法來接收上傳的文件孽水,最常見的方式就是在某個控制器方法參數(shù)上添加@RequestPart注解票腰。

假設(shè)我們允許用戶在注冊Spittr應(yīng)用的時候上傳一張圖片,那么我們需要修改表單女气,以允許用戶選擇要上傳的圖片杏慰,同時還需要修改SpitterController中的processRegistration() 方法來接收上傳的圖片。如下的代碼片段來源于JSP注冊表單視圖:

<sf:form method="POST" commandName="spitter" enctype="multipart/form-data">
...
  <label>Profile Picture</label>:
      <input type="file"
             name="profilePicture"
             accept="image/jpeg,image/png,image/gif" /><br/>
...
</sf:form>

<form>標簽現(xiàn)在將enctype屬性設(shè)置為multipart/form-data炼鞠,這會告訴瀏覽器以multipart數(shù)據(jù)的形式提交表單缘滥,而不是以表單數(shù)據(jù)的形式進行提交。在multipart中谒主,每個輸入域都會對應(yīng)一個part朝扼。

除了注冊表單中已有的輸入域,我們還要添加了一個新的<input>域霎肯,其type為file擎颖。這能夠讓用戶選擇要上傳的圖片文件。accept屬性用來將文件類型限制為JPEG观游、PNG以及GIF圖片搂捧。根據(jù)其name屬性,圖片數(shù)據(jù)將會發(fā)送到multipart請求中的profilePicture part之中懂缕。

現(xiàn)在我們需要修改processRegistration()方法异旧,使其能夠接受上傳的圖片。其中一種方式是添加byte數(shù)組參數(shù)提佣,并為其添加@RequestPart注解吮蛹。如下為示例:

@RequestMappting(value="/register", method=POST)
public String processRegistration(
      @RequestPart("profilePicture") byte[] profilePicture,
      @Valid Spitter spitter,
       Errors errors) {
    ...
}

當注冊表單提交的時候荤崇,profilePicture屬性將會給定一個byte數(shù)組,這個數(shù)組中包含了請求中對應(yīng)part的數(shù)據(jù)(通過@RequestPart指定)潮针。如果用戶提交表單的時候沒有選擇文件术荤,那么這個數(shù)組會是空(而不是null)。獲取到圖片數(shù)據(jù)后每篷,processRegistration() 方法剩下的任務(wù)就是將文件保存到某個位置瓣戚。

使用上傳文件的原始byte比較簡單但是功能有限。因此焦读,Spring還提供了MultipartFile接口子库,它為處理multipart數(shù)據(jù)提供了內(nèi)容更為豐富的對象。如下的程序清單展示了MultipartFile接口的概況矗晃。

package org.springframework.web.multipart;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.InputStreamSource;

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 var1) throws IOException, IllegalStateException;
}

它提供了獲取上傳文件byte的方式仑嗅,還能獲得原始的文件名、大小以及內(nèi)容類型张症。它還提供了一個InputStream仓技,用來將文件數(shù)據(jù)以流的方式進行讀取。

除此之外俗他,MultipartFile還提供了一個便利的transferTo()方法脖捻,它能夠幫助我們將上傳的文件寫入到文件系統(tǒng)中。作為樣例兆衅,我們可以在processRegistration() 方法中添加如下的幾行代碼地沮,從而將上傳的圖片文件寫入到文件系統(tǒng)中:

    @RequestMapping(value="/register", method=POST)
    public String processRegistration(
            @Valid SpitterForm spitterForm,   // 校驗 Spitter輸入
            Errors errors) throws IllegalStateException, IOException {
        if (errors.hasErrors()) {
            return "registerForm";   // 如果校驗出現(xiàn)錯誤,則重新返回表單
        }

        Spitter spitter = spitterForm.toSpitter();
        spitterRepository.save(spitter);
        MultipartFile profilePicture = spitterForm.getProfilePicture();
        profilePicture.transferTo(new File("/tmp/spittr/" + profilePicture.getOriginalFilename()));
        return "redirect:/spitter/" + spitter.getUsername();
    }

其中用到的SpitterForm類羡亩,如下所示:

package spittr.web;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.springframework.web.multipart.MultipartFile;
import spittr.model.Spitter;


public class SpitterForm {

  @NotNull
  @Size(min=5, max=16, message="{username.size}")
  private String username;

  @NotNull
  @Size(min=5, max=25, message="{password.size}")
  private String password;
  
  @NotNull
  @Size(min=2, max=30, message="{firstName.size}")
  private String firstName;

  @NotNull
  @Size(min=2, max=30, message="{lastName.size}")
  private String lastName;
  
  @NotNull
  @Email
  private String email;
  
  private MultipartFile profilePicture;
  
  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public MultipartFile getProfilePicture() {
    return profilePicture;
  }

  public void setProfilePicture(MultipartFile profilePicture) {
    this.profilePicture = profilePicture;
  }

  public Spitter toSpitter() {
    return new Spitter(username, password, firstName, lastName, email);
  }
}

如果需要將應(yīng)用部署到Servlet 3.0的容器中诉濒,那么會有MultipartFile的一個替代方案。Spring MVC也能接受javax.servlet.http.Part作為控制器方法的參數(shù)夕春。如果使用Part來替換MultipartFile的話未荒,那么processRegistration()的方法簽名將會變成如下的形式:

@RequestMappting(value="/register", method=POST)
public String processRegistration(
      @RequestPart("profilePicture") Part profilePicture,
      @Valid Spitter spitter,
       Errors errors) {
    ...
}

那么將上傳的文件寫入文件系統(tǒng)中的代碼為:

profilePicture.write(new File("/tmp/spittr/" + profilePicture.getSubmittedFileName()));

值得一提的是,如果在編寫控制器方法的時候及志,通過Part參數(shù)的形式接受文件上傳片排,那么就沒有必要配置MultipartResolver了。只有使用MultipartFile的時候速侈,我們才需要MultipartResolver率寡。

處理異常

不管發(fā)生什么事情,不管是好的還是壞的倚搬,Servlet請求的輸出都是一個Servlet響應(yīng)冶共。如果在請求處理的時候,出現(xiàn)了異常,那它的輸出依然會是Servlet響應(yīng)捅僵。異常必須要以某種方式轉(zhuǎn)換為響應(yīng)家卖。

Spring提供了多種方式將異常轉(zhuǎn)換為響應(yīng):

  • 特定的Spring異常將會自動映射為指定的HTTP狀態(tài)碼;
  • 異常上可以添加@ResponseStatus注解庙楚,從而將其映射為某一個HTTP狀態(tài)碼上荡;
  • 在方法上可以添加@ExceptionHandler注解,使其用來處理異常馒闷。

將異常映射為HTTP狀態(tài)碼

在默認情況下酪捡,Spring會將自身的一些異常自動轉(zhuǎn)換為合適的狀態(tài)碼。下表列出了這些映射關(guān)系纳账。

Spring異常 HTTP狀態(tài)碼
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotWritableException 500 - Internal Server Error
HttpRequestMethodNotSupportedException 405 - Method Not Allowed
MethodArgumentNotValidException 400 - Bad Request
MissingServletRequestParameterException 400 - Bad Request
MissingServletRequestPartException 400 - Bad Request
NoSuchRequestHandlingMethodException 404 - Not Found
TypeMismatchException 400 - Bad Request

以上的異常一般會由Spring自身拋出逛薇,作為DispatcherServlet處理過程中或執(zhí)行校驗時出現(xiàn)問題的結(jié)果。例如疏虫,如果DispatcherServlet無法找到適合處理請求的控制器方法永罚,那么將會拋出NoSuchRequestHandlingMethodException異常,最終的結(jié)果就是產(chǎn)生404狀態(tài)碼的響應(yīng)(Not Found)议薪。

對于應(yīng)用所拋出的異常尤蛮,這些內(nèi)置的映射就無能為力了媳友。幸好斯议,Spring提供了一種機制,能夠通過@RequestStatus注解將異常映射為HTTP狀態(tài)碼醇锚。

為了闡述這項功能哼御,請參考SpittleController中如下的請求處理方法,它可能會產(chǎn)生HTTP 404狀態(tài)(但目前還沒有實現(xiàn)):

@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 "spittle";
}

現(xiàn)在SpittleNotFoundException就是一個簡單的非檢查型異常焊唬,如下所示:

package spittr.web;
public class SpittleNotFoundException extends RuntimeException {
}

SpittleNotFoundException默認會產(chǎn)生500狀態(tài)碼(Internal Server Error)的響應(yīng)恋昼。實際上,如果出現(xiàn)任何沒有映射的異常赶促,響應(yīng)都會帶有500狀態(tài)碼液肌,但是,我們可以通過映射SpittleNotFoundException對這種默認行為進行變更鸥滨。

當拋出SpittleNotFoundException異常時嗦哆,這是一種請求資源沒有找到的場景。我們要使用@ResponseStatus注解將SpittleNotFoundException映射為HTTP狀態(tài)碼404婿滓。

package spittr.web;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND,
                reason = "Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {
}

編寫異常處理的方法

作為樣例老速,假設(shè)用戶試圖創(chuàng)建的Spittle與已創(chuàng)建的Spittle文本完全相同,那么SpittleRepository的save()方法將會拋出DuplicateSpittleException異常凸主。這意味著SpittleController的saveSpittle()方法可能需要處理這個異常橘券。如下面的程序所示,saveSpittle()方法可以只關(guān)注成功保存Spittle的情況。

    @RequestMapping(method=RequestMethod.POST)
    public String saveSpittle(SpittleForm form, Model model) throws Exception {
        spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),
                form.getLongitude(), form.getLatitude()));
        return "redirect:/spittles";
    }

然后旁舰,我們?yōu)镾pittleController添加一個新的方法锋华,它會處理拋出DuplicateSpittleException的情況:

@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle(){
    return "error/duplicate";
}

對于@ExceptionHandler注解標注的方法來說,比較有意思的一點在于它能處理同一個控制器中所有處理器方法所拋出的異常鬓梅。所有供置,我們不用在每一個可能拋出DuplicateSpittleException的方法中添加異常處理代碼,這一個方法就涵蓋了所有的功能绽快。

為控制器添加通知

如果控制器類的特定切面能夠運用到整個應(yīng)用程序的所有控制器中芥丧,那么這將會便利很多。舉例說明坊罢,如果要在多個控制器中處理異常续担,那@ExceptionHandler注解所標注的方法是很有用的。不過活孩,如果多個控制器類中都會拋出某個特定的異常物遇,那么你可能會發(fā)現(xiàn)要在所有的控制器方法中重復(fù)相同的@ExceptionHandler方法『度澹或者询兴,為了避免重復(fù),我們會創(chuàng)建一個基礎(chǔ)的控制器類起趾,所有控制器類要擴展這個類诗舰,從而繼承通用的@ExceptionHandler方法。

Spring 3.2為這類問題引入了一個新的解決方案:控制器通知训裆。
控制器通知(controller advice)是任意帶有@ControllerAdvice注解的類眶根,這個類會包含一個或多個如下類型的方法:

  • @ExceptionHandler注解標注的方法;
  • @InitBinder注解標注的方法边琉;
  • @ModelAttribute注解標注的方法属百;

在帶有@ControllerAdvice注解的類中,以上所述的這些方法會運用到整個應(yīng)用程序所有控制器中帶有@RequestMapping注解的方法上变姨。

@ControllerAdvice注解本身已經(jīng)使用@Component族扰,因此@ControllerAdvice注解所標注的類將會自動被組件掃描獲取到,就像帶有@Component注解的類一樣定欧。

@ControllerAdvice最為實用的一個場景就是將所有的@ExceptionHandler方法收集到一個類中渔呵,這樣所有控制器的異常就能在一個地方進行一致的處理。例如忧额,我們想將DuplicateSpittleException的處理方法用到整個應(yīng)用程序的所有控制器上厘肮。

package spittr.web;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class AppWideExceptionHandler {

    @ExceptionHandler(DuplicateSpittleException.class)
    public String duplicateSpittleHandler(){
        return "error/duplicate";
    }
}

現(xiàn)在,如果任意的控制器方法拋出了DuplicateSpittleException睦番,不管這個方法位于哪個控制器中类茂,都會調(diào)用這個duplicateSpittleHandler()方法來處理異常耍属。

跨重定向請求傳遞數(shù)據(jù)

在處理完P(guān)OST請求后,通常來講一個最佳實踐就是執(zhí)行一下重定向巩检。除了其他的一些因素外厚骗,這樣做能夠防止用戶點擊瀏覽器的刷新按鈕或后退箭頭時,客戶端重新執(zhí)行危險的POST請求兢哭。

“redirect:”前綴能夠讓重定向功能變得非常簡單领舰。Spring為重定向功能還提供了一些其他的輔助功能。

一般來講迟螺,當一個處理器方法完成之后冲秽,該方法所指定的模型數(shù)據(jù)將會復(fù)制到請求中,并作為請求中的屬性矩父,請求會轉(zhuǎn)發(fā)(forward)到視圖上進行渲染锉桑。因為控制器方法和視圖所處理的是同一個請求,所以在轉(zhuǎn)發(fā)的過程中窍株,請求屬性能夠得以保存民轴。

但是,當控制器的結(jié)果是重定向的話球订,原始的請求就結(jié)束了后裸,并且會發(fā)起一個新的GET請求。原始請求中所帶有的模型數(shù)據(jù)也就隨著請求一起消亡了冒滩。在新的請求屬性中微驶,沒有任何的模型數(shù)據(jù),這個請求必須要自己計算數(shù)據(jù)旦部。

顯然祈搜,對于重定向來說较店,模型并不能用來傳遞數(shù)據(jù)士八。但是我們也有一些其他的方案,能夠從發(fā)起重定向的方法傳遞數(shù)據(jù)給處理重定向方法中:

  • 使用URL模板以路徑變量和/或查詢參數(shù)的形式傳遞數(shù)據(jù)梁呈;
  • 通過flash屬性發(fā)送數(shù)據(jù)婚度。

通過URL模板進行重定向

通過路徑變量和查詢參數(shù)傳遞數(shù)據(jù)看起來非常簡單。例如

return "redirect:/spitter/" + spitter.getUsername();

這能夠正常運行官卡,但是還遠遠不能說沒有問題蝗茁。當構(gòu)建URL或SQL查詢語句的時候,使用String連接是很危險的寻咒。

除了連接String的方式來構(gòu)建重定向URL哮翘,Spring還提供了使用模板的方式來定義重定向URL。例如

    @RequestMapping(value="/register", method=POST)
    public String processRegistration( 
        Spitter spitter,   Model model) {
        spitterRepository.save(spitter);
        model.addAttribute("username", spitter.getUsername());
        return "redirect:/spitter/{username}";
    }

現(xiàn)在毛秘,username作為占位符填充到了URL模板中饭寺,而不是直接連接到重定向String中阻课,所以username中所有的不安全字符都會進行轉(zhuǎn)義。這樣會更加安全艰匙,這里允許用戶輸入任何想要的內(nèi)容作為username限煞,并會將其附加在路徑上。

除此之外员凝,模型中所有其他的原始類型值都可以添加到URL中作為查詢參數(shù)署驻。作為樣例,假設(shè)除了username以外健霹,模型中還要包含新創(chuàng)建Spitter對象的id屬性旺上,那processRegistration()方法可以改為如下寫法:

    @RequestMapping(value="/register", method=POST)
    public String processRegistration( 
        Spitter spitter,   Model model) {
        spitterRepository.save(spitter);
        model.addAttribute("username", spitter.getUsername());
        model.addAttribute("spitterId", spitter.getId());
        return "redirect:/spitter/{username}";
    }

所返回的重定向String并沒有太大的變化。但是糖埋,因為模型中的spitterId屬性沒有匹配重定向URL中的任何占位符抚官,所以它會自動以查詢參數(shù)的形式附加到重定向URL上。

如果username屬性的值是habuma并且spitterId屬性的值是42阶捆,那么結(jié)果得到的重定向URL路徑將會是“/spitter/habuma?spitterId=42”凌节。

使用flash屬性

如果在重定向的時候,需要實際發(fā)送對象洒试。例如前面的例子中倍奢,我們需要重定向的時候傳遞Spitter對象。Spitter對象要比String和int更為復(fù)雜垒棋。因此卒煞,我們不能想路徑變量或查詢參數(shù)那么容易地發(fā)送Spitter對象。

正如我們前面討論的那樣叼架,模型數(shù)據(jù)最終是以請求參數(shù)的形式復(fù)制到請求中的畔裕,當重定向發(fā)生的時候,這些數(shù)據(jù)就會丟失乖订。因此扮饶,我們需要將Spitter對象放到一個位置座享,使其能夠在重定向的過程中存活下來蛋济。

有個方案是將Spitter放到會話中,Spring也認為將跨重定向存活的數(shù)據(jù)放到會話中是一個很不錯的方式喳张。但是哥遮,Spring認為我們并不需要管理這些數(shù)據(jù)岂丘,相反,Spring提供了將數(shù)據(jù)發(fā)送為flash屬性(flash attribute)的功能眠饮。
按照定義奥帘,flash屬性會一直攜帶這些數(shù)據(jù)直到下一次請求,然后才會消失仪召。

Spring提供了通過RedirectAttributes設(shè)置flash屬性的方法寨蹋,這是Spring 3.1引入的Model的一個子接口牲距。RedirectAttributes提供了Model的所有功能,除此之外钥庇,還有幾個方法是用來設(shè)置flash屬性的牍鞠。

具體來講,RedirectAttributes提供了一組addFlashAttribute()方法來添加flash屬性评姨。重現(xiàn)看一下processRegistration()方法难述,我們可以使用addFlashAttribute()將Spitter對象添加到模型中:

    @RequestMapping(value="/register", method=POST)
    public String processRegistration( 
        Spitter spitter,   RedirectAttributes model) {
        spitterRepository.save(spitter);
        model.addAttribute("username", spitter.getUsername());
        model.addFlashAttribute("spitter",spitter);
        return "redirect:/spitter/{username}";
    }

在這里,我們調(diào)用了addFlashAttribute() 方法吐句,并將spitter作為key胁后,Spitter對象作為值。另外嗦枢,我們還可以不設(shè)置key參數(shù)攀芯,讓key根據(jù)值的類型自行推斷得出:

model.addFlashAttribute(spitter);

因為我們傳遞了一個Spitter對象給addFlashAttribute()方法,所以推斷得到的key將會是spitter文虏。

在重定向執(zhí)行之前侣诺,所有的flash屬性都會復(fù)制到會話中。在重定向后氧秘,存在會話中flash屬性會被取出年鸳,并從會話轉(zhuǎn)移到模型之中。處理重定向的方法就能從模型中訪問Spitter對象了丸相,就像獲取其他的模型對象一樣搔确。下圖闡述了它是如何運行的

flash屬性保存在會話中,然后再放到模型中灭忠,因此能夠在重定向的過程中存活

為了完成flash屬性的流程膳算,如下展現(xiàn)了更新版本的showSpitterProfile()方法,在從數(shù)據(jù)庫中查找之前弛作,它會首先從模型中檢查Spitter對象:

    @RequestMapping(value="/{username}", method=GET)
    public String showSpitterProfile(@PathVariable String username, Model model) {

        if (!model.containsAttribute("spitter")){
            Spitter spitter = spitterRepository.findByUsername(username);
            model.addAttribute(spitter);
        }
        return "profile";
    }

可以看到showSpitterProfile() 方法所做的第一件事就是檢查是否存有key為spitter的model屬性涕蜂。如果模型中包含spitter屬性,那就什么都不用做了缆蝉。這里面包含的Spitter對象將會傳到視圖中進行渲染宇葱。但是如果模型中不包含spitter屬性的話瘦真,那么showSpitterProfile()將會從Repository中查找Spitter刊头,并將其存放到模型中。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末诸尽,一起剝皮案震驚了整個濱河市原杂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌您机,老刑警劉巖穿肄,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件年局,死亡現(xiàn)場離奇詭異,居然都是意外死亡咸产,警方通過查閱死者的電腦和手機矢否,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脑溢,“玉大人僵朗,你說我怎么就攤上這事⌒汲梗” “怎么了验庙?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長社牲。 經(jīng)常有香客問我粪薛,道長,這世上最難降的妖魔是什么搏恤? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任违寿,我火速辦了婚禮,結(jié)果婚禮上熟空,老公的妹妹穿的比我還像新娘陨界。我一直安慰自己,他們只是感情好痛阻,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布菌瘪。 她就那樣靜靜地躺著,像睡著了一般阱当。 火紅的嫁衣襯著肌膚如雪俏扩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天弊添,我揣著相機與錄音录淡,去河邊找鬼。 笑死油坝,一個胖子當著我的面吹牛嫉戚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播澈圈,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼彬檀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瞬女?” 一聲冷哼從身側(cè)響起窍帝,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诽偷,沒想到半個月后坤学,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疯坤,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年深浮,在試婚紗的時候發(fā)現(xiàn)自己被綠了压怠。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡飞苇,死狀恐怖刑峡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情玄柠,我是刑警寧澤突梦,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站羽利,受9級特大地震影響宫患,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜这弧,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一娃闲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧匾浪,春花似錦皇帮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冷溶,卻和暖如春渐白,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背逞频。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工纯衍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人苗胀。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓襟诸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親基协。 傳聞我的和親對象是個殘疾皇子歌亲,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容