Spring——SpringMVC(三)

本文主要依據《Spring實戰》第七章內容進行總結

前兩節中,我們主要介紹了Spring MVC的控制器和視圖解析器,通過上面兩節內容,我們對Spring MVC已經有了一個初步的瞭解,也能夠使用Spring MVC進行基本的Web層編程了,但是Spring MVC的知識還遠未結束,在本節,我們將繼續介紹Spring MVC的一些高級技術。

1、Spring MVC配置的替代方案

1.1、自定義DispatcherServlet配置

Spring MVC(一) 小節中,我們介紹瞭如何使用Java代碼配置DispatcherServlet,我們只需要擴展AbstractAnnotationConfigDispatcherServletInitializer類即可,我們重載了三個abstract方法,實際上還有更多的方法可以進行重載,從而實現額外的配置,其中一個方法就是customizeRegistration()。

在AbstractAnnotationConfigDispatcherServletInitializer將DispatcherServlet註冊到Servlet容器中後,就會調用customizeRegistration()方法,並將Servlet註冊後得到的ServletRegistration.Dynamic傳遞進來。通過重載customizeRegistration()方法,我們就可以對DispatcherServlet進行額外的配置,例如,通過調用Dynamic的setLoadOnStartup()設置load-on-startup優先級,通過調用setInitParameter()設置初始化參數,還有可以通過調用setMultipartConfig()來設置對multipart的支持,我們可以看如下的代碼:

@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("f://tmp"));     
}

上面這段代碼就是通過調用Dynamic的setMultipartConfig()來設置對multipart的支持,並設置上傳文件的臨時目錄爲f://tmp,具體的multipart功能我們在後面的內容會進行介紹。

1.2、添加其他的Servlet和Filter

按照AbstractAnnotationConfigDispatcherServletInitializer的定義,它會創建DispatcherServlet和ContextLoaderListener,但是如果我們想要註冊其他的Servlet、Filter和Listener,那該怎麼辦呢?

基於Java的初始化器(initializer)的一個好處就在於我們可以定義任意數量的初始化器類。因此,如果我們想往Web容器中註冊其他組件的話,只需要創建一個新的初始化器就可以了。最簡單的方式就是實現Spring的WebApplicationInitializer接口。例如:

public class MyServletInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext)
            throws ServletException {
        //1、註冊Servlet
        Dynamic d = servletContext.addServlet("myServlet", MyServlet.class);
        //2、添加映射
        d.addMapping("/myservlet/**");
    }

}

上面這段代碼就是新註冊一個名爲myServlet的Servlet,然後將其映射到一個路徑上。我們也可以通過這種方式手動註冊DispatcherServlet,但是這並沒有必要,因爲AbstractAnnotationConfigDispatcherServletInitializer能夠很好地完成這項任務。

同樣的,我們也可以使用這種方式來註冊Filter、Listener:

public class MyFilterInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext)
            throws ServletException {
        //1、註冊Filter
        Dynamic d = servletContext.addFilter("myFilter", MyFilter.class);
        //2、添加映射路徑
        d.addMappingForUrlPatterns(null, false, "/");

    }

}

需要注意的是,上面通過實現WebApplicationInitializer來註冊Servlet、Filter、Listener只在Servlet 3.0以後的版本中才適用,如果容器不支持Servlet 3.0及以上的版本,我們不能通過以上的方式註冊組件。在上面的例子中,我們註冊了一過濾器MyFilter,如果這個Filter只會映射到DispatcherServlet上的話,AbstractAnnotationConfigDispatcherServletInitializer提供了一個更爲快捷的方式,就是重載getServletFilters()方法:

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

這個方法返回一個Filter的數組,這個數組中的所有Filter都會映射到DispatcherServlet上。

1.3、在web.xml中聲明DispatcherServlet

在典型的Spring MVC應用中,我們會需要DispatcherServlet和ContextLoaderListener,AbstractAnnotationConfigDispatcherServletInitializer會自動註冊它們,但如果我們使用web.xml來進行配置的話,我們就需要手動註冊它們了。下面的實例是一個簡單的web.xml文件,它按照傳統的方式搭建了DispatcherServelt和ContextLoaderListener。

<?xml version="1.0" encoding="UTF-8"?>  
<web-app version="3.0" 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_3_0.xsd">      
    <!-- 設置根上下文配置文件位置 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:root.xml</param-value>
    </context-param>

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

    <!-- 註冊DispathcherServlet -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

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

在這個配置文件中,上下文參數contextConfigLocation指定了一個XML文件的地址,這個文件定義了根應用上下文,它會被ContextLoaderListener加載。DispatcherServlet會根據Servlet的名字找到一個文件,並基於該文件加載應用上下文,在上面的例子中,Servlet的名字是appServlet,所以DispatcherServlet會從“/WEB-INF/appServlet-servlet.xml”文件中加載其應用上下文。

如果需要指定DIspatcherServlet配置文件的位置的話,那麼可以在Servlet上指定一個contextConfigLocation初始化參數,例如:

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:appServlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

在上面的例子中,DispatherServlet將會去類路徑下的appServlet.xml中加載應用上下文的bean。

在上面的兩個例子裏,我們都是通過xml的形式配置Spring應用上下文的bean的,如果我們使用Java配置的形式配置bean,然後再在web.xml配置DispathcerServlet和ContextLoaderListener,那麼它們該如何加載Java配置中的bean呢?

要在Spring MVC中使用基於Java的配置,我們需要告訴DipathcerServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext,這是一個WebApplicationContext的實現類,它會加載Java配置類,而不是使用XML,我們可以設置contextClass上下文參數以及DispatcherServlet的初始化參數。

下面的web.xml配置就是基於Java配置的Spring MVC:

<?xml version="1.0" encoding="UTF-8"?>  
<web-app version="3.0" 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_3_0.xsd">      

    <!-- 使用Java配置 -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>

    <!-- 指定根配置類 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>config.WebConfig</param-value>
    </context-param>

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

    <!-- 註冊DispathcherServlet -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <!-- 使用Java配置 -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <!-- 指定DispatcherServlet配置類 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>config.WebConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

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

2、處理multipart形式的數據

在Web應用中,允許用戶上傳文件的功能是很常見的,一般表單提交所形成的請求結果是很簡單的,就是以”&”符分割的多個name-value對,而對於要上傳文件的表單,表單的類型爲multipart,它會將一個表單分割爲多個部分(part),每個部分對應一個輸入域,在一般的表單輸入域中,它所對應的部分會放置文本型數據,如果是上傳文件的話,它所對應的部分可以是二進制。

在Spring MVC中處理multipart請求很容易,在編寫控制器方法處理文件上傳之前,我們必須配置一個multipart解析器,通過它來告訴DispatcherServlet該如何讀取multipart請求。

2.1、配置multipart解析器

DispatcherServlet並沒有實現任何解析multipart請求數據的功能,它將該任務委託給Spring中MultipartResolver接口的實現,通過這個實現類來解析multipart請求中的內容。從Spring 3.1開始,Spring內置了兩個MultipartResolver的實現供我們選擇:

  • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart請求;
  • StandardServletMultipartResolver:依賴於Servlet 3.0對multipart請求的支持,始於Spring 3.1。

其中StandardServletMultipartResolver使用Servlet提供的功能支持,不需要依賴於任何其他項目,但是如果我們需要將應用部署到Servlet 3.0之前的容器中,或者還沒有使用Spring 3.1以及更高的版本,我們就需要使用CommonsMultipartResolver。下面,我們分別來看一下這兩個multipart解析器。

2.1.1、StandardServletMultipartResolver

兼容Servlet 3.0的StandardServletMultipartResolver沒有構造器參數,也沒有要設置的屬性:

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

這個類不需要設置任何的構造器參數和屬性,我們可能會擔心這個multipart解析器的功能是否會受到限制?比如我們該如何設置文件上傳的路徑?該如何限制用戶上傳的文件大小?

實際上,我們無須在Spring中配置StandardServletMultipartResolver,而是在Servlet中指定multipart的配置。因爲StandardServletMultipartResolver使用的是Servlet所提供的功能支持,所以不用在StandardServletMultipartResolver中配置,而是在Servlet中配置。我們必須在web.xml中或者Servlet初始化類中,將multipart的具體細節作爲DispatcherServlet配置的一部分。

在1.1節中,我們已經看到了如何使用Servlet初始化類來配置multipart,只需要重載customizeRegistration()方法,調用ServletRegistration.Dynamic的setMultipartConfig()方法,傳入一個MultipartConfigElement實例即可:

@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("f://tmp"));     
}

到目前爲止,我們使用的是隻有一個參數的MultipartElement構造器,這個參數指定的是文件系統中一個絕對目錄,上傳文件將會臨時寫入該目錄。除了臨時路徑的位置,其他的構造器所能接受的參數如下:

  • 上傳文件的最大容量(以字節爲單位),默認是沒有限制的;
  • 整個multipart請求的最大容量(以字節爲單位),不會關心有多少個part以及每個part的大小,默認是沒有限制的;
  • 在上傳的過程中,如果文件大小達到了一個指定的最大容量(以字節爲單位),將會寫入到臨時文件路徑中,默認值爲0,也就是所有上傳的文件都會寫入到磁盤。

例如,我們想限制文件大小不超過2MB,整個請求不超過4MB,而且所有的文件都要寫到磁盤中,而且所有的文件都要寫入到磁盤中,下面的代碼設置了這些臨界值:

@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("f://tmp", 2097152, 4194304, 0));
}

如果我們使用web.xml配置MultipartElement的話,那麼可以使用<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>f://tmp</location>
        <max-file-size>2097152</max-file-size>
        <max-request-size>4194304</max-request-size>
        <file-size-threshold>0</file-size-threshold>
    </multipart-config>
</servlet>

2.1.2、CommonsMultipartResolver

我們在使用CommonsMultipartResolver作爲multipart解析器時,需要引入commons-fileupload-1.3.1.jar和commons-io-2.2.jar,如果我們的項目使用了Maven,只需要在pom.xml文件中加入以下依賴即可:

<dependency>  
    <groupId>commons-fileupload</groupId>  
    <artifactId>commons-fileupload</artifactId>  
    <version>1.3.1</version>  
</dependency> 

將CommonsMultipartResolver聲明爲Spring中的bean最簡單的方式如下:

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

乍一看,這樣的聲明和StandardServletMultipartResolver聲明是一樣的,都沒有指定構造器參數和屬性,實際上,CommonsMultipartResolver不會強制要求設置臨時文件路徑,默認情況下,這個路徑就是Servlet容器的臨時目錄,不過通過設置uploadTempDir屬性,我們可以將其指定爲一個不同的位置。實際上,我們還可以通過CommonsMultipartResolver的setMaxUploadSize()方法設置上傳文件的大小限制,setMaxInMemorySize()方法設置文件是否全部寫入磁盤:

@Bean
public MultipartResolver multipartResolver() throws Exception {
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setUploadTempDir(new FileSystemResource("f://tmp"));
    multipartResolver.setMaxUploadSize(2097152);
    multipartResolver.setMaxInMemorySize(0);

    return multipartResolver;
}

與MultipartConfigElement不同的是,我們無法設定multipart請求整體的最大容量。

2.2、處理multipart請求

在上面一節中,我們已經配置好了對multipart請求的處理,現在我們可以在控制器方法中接收上傳的文件了,最常見的方式就是在某個控制器方法參數上添加@RequestPart註解,下面我們來看幾種接收文件的方式。

2.2.1、使用byte[]接收上傳的文件

假設我們需要一個頁面來上傳每個學生的照片,我們可以編寫如下的JSP:

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>上傳照片</title>
</head>
<body>
    <h1>請上傳學生照片</h1>
    <form action="/uploadStudentPicture" method="POST" enctype="multipart/form-data">
        <input type="file" name="picture"/><br/>
        <input type="submit" value="提交" />
    </form>
</body>
</html>

在這個JSP頁面中,我們將<form> 表單的enctype屬性設置爲multipart/form-data,這樣瀏覽器就能以multipart數據的形式提交表單,而不是以表單數據的形式提交,在multipart中,每個輸入域都會對應一個part。另外,在這個頁面中,我們使用<input> 標籤,並將其type屬性設置爲file,這樣我們就能夠在頁面上傳文件了,最終顯示的頁面形式如下:

這裏寫圖片描述

頁面完成之後,我們需要在控制器中添加處理文件的方法:

@RequestMapping(value="/uploadStudentPicture",method=RequestMethod.POST)
public String uploadStudentPicture(@RequestPart("picture") byte[] picture) {
    System.out.println(picture.length);

    return null;
}

在這個案例中,處理文件的方法只是簡單地將文件的字節大小打印出來。我們在處理方法中傳入一個byte數組,並使用@RequestPart註解進行標註,當註冊表單提交的時候,picture屬性將會給定一個byte數組,這個數組中包含了請求中對應part的數據(通過@RequestPart指定),如果用戶提交表單的時候沒有選擇文件,那麼這個數組會是空(不是null)。獲取到圖片數據後,uploadStudentPicture()方法就是將文件保存到某個位置了。

我們來測試一下上面的案例,假設我們有一個圖片,圖片大小爲143785個字節,具體信息如下:

這裏寫圖片描述

我們現將其上傳,點擊提交按鈕,查看後臺日誌,可以看到,日誌正確打印出了文件的大小:

……

五月 24, 2018 3:01:01 下午 org.apache.catalina.startup.Catalina start
信息: Server startup in 7068 ms
143785

另外,因爲我們在multipart解析器中配置的臨時文件路徑爲f://tmp,所以,我們在該路徑下能夠看到已經上傳的文件:

臨時目錄

在這裏我們看到,上傳的文件文件類型被改成了tmp,而且文件名稱也被改成了我們認不出的信息,儘管我們獲取到了byte數組形式的圖片數據,並且我們能夠獲取到它的大小,但是關於它的其他信息,我們就一無所知了,我們不知道文件名是什麼,甚至文件的類型是什麼也不知道,同時上傳的文件信息也被修改了,我們無法得知正確的文件信息,我們需要使用其他的方式來獲取文件的更多信息。

2.2.2、使用MultipartFile接收上傳的文件

使用上傳文件的原始byte比較簡單但功能有限,所以Spring還提供了MultipartFile接口,它爲處理multipart數據提供了內容更爲豐富的對象,如下是MultipartFile接口的定義:

public interface MultipartFile extends InputStreamSource {

    /**
     * Return the name of the parameter in the multipart form.
     * @return the name of the parameter (never {@code null} or empty)
     */
    String getName();

    /**
     * Return the original filename in the client's filesystem.
     * <p>This may contain path information depending on the browser used,
     * but it typically will not with any other than Opera.
     * @return the original filename, or the empty String if no file has been chosen
     * in the multipart form, or {@code null} if not defined or not available
     */
    String getOriginalFilename();

    /**
     * Return the content type of the file.
     * @return the content type, or {@code null} if not defined
     * (or no file has been chosen in the multipart form)
     */
    String getContentType();

    /**
     * Return whether the uploaded file is empty, that is, either no file has
     * been chosen in the multipart form or the chosen file has no content.
     */
    boolean isEmpty();

    /**
     * Return the size of the file in bytes.
     * @return the size of the file, or 0 if empty
     */
    long getSize();

    /**
     * Return the contents of the file as an array of bytes.
     * @return the contents of the file as bytes, or an empty byte array if empty
     * @throws IOException in case of access errors (if the temporary store fails)
     */
    byte[] getBytes() throws IOException;

    /**
     * Return an InputStream to read the contents of the file from.
     * The user is responsible for closing the stream.
     * @return the contents of the file as stream, or an empty stream if empty
     * @throws IOException in case of access errors (if the temporary store fails)
     */
    @Override
    InputStream getInputStream() throws IOException;

    /**
     * Transfer the received file to the given destination file.
     * <p>This may either move the file in the filesystem, copy the file in the
     * filesystem, or save memory-held contents to the destination file.
     * If the destination file already exists, it will be deleted first.
     * <p>If the file has been moved in the filesystem, this operation cannot
     * be invoked again. Therefore, call this method just once to be able to
     * work with any storage mechanism.
     * <p><strong>Note:</strong> when using Servlet 3.0 multipart support you
     * need to configure the location relative to which files will be copied
     * as explained in {@link javax.servlet.http.Part#write}.
     * @param dest the destination file
     * @throws IOException in case of reading or writing errors
     * @throws IllegalStateException if the file has already been moved
     * in the filesystem and is not available anymore for another transfer
     */
    void transferTo(File dest) throws IOException, IllegalStateException;

}

我們可以看到,MultipartFile提供了獲取上傳文件byte的方式,還能夠獲取原始的文件名、大小以及內容類型,它還提供了一個InputStream,用來將文件數據以流的方式進行讀取。另外,MultipartFile提供了一個transferTo()方法它能夠幫我們將上傳的文件寫入到文件系統中,這樣我們可以修改一下上面的方法,將上傳的圖片寫入到文件系統中:

@RequestMapping(value="/uploadStudentPicture",method=RequestMethod.POST)
public String uploadStudentPicture(@RequestPart("picture") MultipartFile picture) throws Exception {
    picture.transferTo(new File("f:/tmp/" + picture.getOriginalFilename()));

    return null;
}

在這裏,我們將文件上傳到f://tmp目錄下,這樣再測試應用,點擊提交按鈕之後,我們在f://tmp目錄下看到如下內容:

亂碼文件名

這一次,文件類型是正確的,爲jpg格式,但是文件名被轉換成了亂碼,我們需要在web.xml中加一個Spring的轉碼攔截器,轉成項目的默認編碼格式:

<filter>   
    <filter-name>SpringCharacterEncodingFilter</filter-name>   
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>   
    <init-param>   
        <param-name>encoding</param-name>   
        <param-value>UTF-8</param-value>   
    </init-param>   
</filter>   
<filter-mapping>   
    <filter-name>SpringCharacterEncodingFilter</filter-name>   
    <url-pattern>/*</url-pattern>   
</filter-mapping> 

這樣我們就能夠獲取到正確的文件名了:

正確編碼的文件名

2.2.3、使用Part接收上傳的文件

如果要將應用部署到Servlet 3.0的容器中,那麼會有一個MultipartFile的替代方案,Spring MVC也能接受javax.servlet.http.Part作爲控制器方法參數:

@RequestMapping(value="/uploadStudentPicture",method=RequestMethod.POST)
public String uploadStudentPicture(@RequestPart("picture") Part picture) throws Exception {
    picture.write("f:/tmp/" + picture.getSubmittedFileName());

    return null;
}

我們可以看到,Part接口的方法調用好像和MultipartFile是類似的,實際上,Part接口和Multipart並沒有太大的差別:

public interface Part {

    /**
     * Gets the content of this part as an <tt>InputStream</tt>
     * 
     * @return The content of this part as an <tt>InputStream</tt>
     * @throws IOException If an error occurs in retrieving the contet
     * as an <tt>InputStream</tt>
     */
    public InputStream getInputStream() throws IOException;

    /**
     * Gets the content type of this part.
     *
     * @return The content type of this part.
     */
    public String getContentType();

    /**
     * Gets the name of this part
     *
     * @return The name of this part as a <tt>String</tt>
     */
    public String getName();

    /**
     * Gets the file name specified by the client
     *
     * @return the submitted file name
     *
     * @since Servlet 3.1
     */
    public String getSubmittedFileName();

    /**
     * Returns the size of this fille.
     *
     * @return a <code>long</code> specifying the size of this part, in bytes.
     */
    public long getSize();

    /**
     * A convenience method to write this uploaded item to disk.
     * 
     * <p>This method is not guaranteed to succeed if called more than once for
     * the same part. This allows a particular implementation to use, for
     * example, file renaming, where possible, rather than copying all of the
     * underlying data, thus gaining a significant performance benefit.
     *
     * @param fileName the name of the file to which the stream will be
     * written. The file is created relative to the location as
     * specified in the MultipartConfig
     *
     * @throws IOException if an error occurs.
     */
    public void write(String fileName) throws IOException;

    /**
     * Deletes the underlying storage for a file item, including deleting any
     * associated temporary disk file.
     *
     * @throws IOException if an error occurs.
     */
    public void delete() throws IOException;

    /**
     *
     * Returns the value of the specified mime header
     * as a <code>String</code>. If the Part did not include a header
     * of the specified name, this method returns <code>null</code>.
     * If there are multiple headers with the same name, this method
     * returns the first header in the part.
     * The header name is case insensitive. You can use
     * this method with any request header.
     *
     * @param name      a <code>String</code> specifying the
     *              header name
     *
     * @return          a <code>String</code> containing the
     *              value of the requested
     *              header, or <code>null</code>
     *              if the part does not
     *              have a header of that name
     */
    public String getHeader(String name);

    /**
     * Gets the values of the Part header with the given name.
     *
     * <p>Any changes to the returned <code>Collection</code> must not 
     * affect this <code>Part</code>.
     *
     * <p>Part header names are case insensitive.
     *
     * @param name the header name whose values to return
     *
     * @return a (possibly empty) <code>Collection</code> of the values of
     * the header with the given name
     */
    public Collection<String> getHeaders(String name);

    /**
     * Gets the header names of this Part.
     *
     * <p>Some servlet containers do not allow
     * servlets to access headers using this method, in
     * which case this method returns <code>null</code>
     *
     * <p>Any changes to the returned <code>Collection</code> must not 
     * affect this <code>Part</code>.
     *
     * @return a (possibly empty) <code>Collection</code> of the header
     * names of this Part
     */
    public Collection<String> getHeaderNames();

}

可以看到,Part方法的名稱和MultipartFile方法的名稱是差不多的,有些比較類似,例如getSubmittedFileName()對應getOriginalName(),write()類似於transferTo()等等。

需要注意的是,如果在編寫控制器方法的時候,通過Part參數的形式接受文件上傳,那麼就沒有必要配置MultipartResolver了,如果配置了CommonsMultipartResolver反而運行會出錯,只有使用MultipartFile的時候我們才需要MultipartResolver。

3、處理異常

Spring提供了多種方式將異常轉換爲響應:

  • 特定的Spring異常將自動映射爲指定的HTTP狀態碼;
  • 異常上可以添加@ResponseStatus註解,從而將其映射爲某一個HTTP狀態碼;
  • 在方法上添加@ExceptionHandler註解,使其用來處理異常

3.1、將異常映射爲HTTP狀態碼

在默認情況下,Spring會將自身的一些異常自動轉換爲合適的狀態碼,下表中列出了這些映射關係:

Spring異常 HTTP狀態碼
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotReadableException 400 - Bad Request
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處理過程中或執行校驗時出現問題的結果。例如,如果DispatcherServlet無法找到適合處理請求的控制器方法,那麼將會拋出NoSuchRequestHandlingMethodException異常,最終的結果就是產生404狀態碼的響應。儘管這些內置映射很有用,但是對於應用拋出的異常它們就無法處理了,Spring提供了一種機制,能夠通過@ResponseStatus註解將異常映射爲HTTP狀態碼。

3.2、將異常映射爲HTTP狀態碼

我們通過一個案例來講解如何使用@ResponseStatus註解將異常映射爲HTTP狀態碼,假設我們使用路徑參數查詢學生信息,如果查詢不到相關的學生信息,則拋出一個無此學生的異常StudentNotExistsException:

@RequestMapping(value="/queryStudentInfo/{id}",method=RequestMethod.GET)
public String queryStudentInfo(@PathVariable("id") int id, Model model) {
    Student s = new Student().getStudentById(id);

    if(null != s) {
        model.addAttribute("student", s);
    } else {
        throw new StudentNotExistsException();
    }

    return "student";
}

我們再定義一下StudentNotExistsException:

public class StudentNotExistsException extends RuntimeException {
}

假設我們現在有如下四個學生:

Student s1 = new Student(1, "張三", "男");
Student s2 = new Student(2, "李四", "女");
Student s3 = new Student(3, "王五", "男");
Student s4 = new Student(4, "測試", "女");

當我們查詢id爲5的學生時,頁面會拋出StudentNotExistsException異常並返回500的HTTP的狀態碼:

500狀態碼

這是因爲在Spring中,如果出現任何沒有映射的異常,響應都會帶有500狀態碼。但是在這個功能中,如果查不到相應的學生信息,我們想返回404的狀態碼,用於表示無法找到相應的資源,這樣地描述更爲準確,所以我們需要在StudentNotExistsException定義上添加@ResponseStatus註解:

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Student Not Exists")
public class StudentNotExistsException extends RuntimeException {
}

這樣,如果控制器方法拋出StudentNotExistsException異常的話,它會返回404狀態碼,並且失敗原因爲:Student Not Exists。

404狀態碼

3.3、編寫異常處理的方法

如果我們想在響應中不僅包括狀態碼,還想包含所產生的錯誤,這樣,我們就不能將異常視爲HTTP錯誤了,而是要按照處理請求的方式來處理異常,我們可以修改一下上述代碼:

@RequestMapping(value="/queryStudentInfo/{id}",method=RequestMethod.GET)
public String queryStudentInfo(@PathVariable("id") int id, Model model) {
    try {
        Student s = new Student().getStudentById(id);

        if(null != s) {
            model.addAttribute("student", s);
        } else {
            throw new StudentNotExistsException();
        }

        return "student";
    } catch (StudentNotExistsException e) {
        return "studentNotExists";
    }
}

我們在代碼中添加了異常處理機制,如果拋出StudentNotExistsException異常,那麼系統會跳轉到學生信息不存在的頁面。我們也可以不修改原有的代碼來實現與上述代碼相同的效果,我們只需要在控制器中添加一個新的方法,並在方法上添加@ExceptionHandler註解即可:

@ExceptionHandler(value=StudentNotExistsException.class)
public String handleStudentNotExists() {
    return "studentNotExists";
}

這樣如果系統拋出StudentNotExistsException異常,將會委託該方法來處理,它返回的是一個String,指定了要渲染的視圖名,這與處理請求的方法是一致的。還有一點需要我們注意的是,儘管我們是從queryStudentInfo()方法中抽出這個異常處理方法的,但是,這個方法能夠處理同一個控制器中所有處理器方法所拋出的StudentNotExistsException異常,我們不用在每一個可能拋出StudentNotExistsException異常的方法中添加異常處理代碼。現在,我們就需要考慮有沒有一種方式能夠處理所有控制器中處理器方法所拋出的異常呢?從Spring 3.2開始,我們只需要將其定義到控制器通知類中即可。

4、控制器通知

控制器通知是任意帶有@ControllerAdvice註解的類,這個類會包含一個或多個如下類型的方法:

  • @ExceptionHandler註解標註的方法;
  • @InitBinder註解標註的方法;
  • @ModelAttribute註解標註的方法。

在帶有@ControllerAdvice註解的類中,以上所述的這些方法會運用到整個應用程序所有控制器中帶有@RequestMapping註解的方法上。例如,我們上節介紹的,如果我們想爲應用中所有的控制器的處理器方法都添加StudentNotExistsException異常處理方法,我們可以這樣寫:

@ControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(value=StudentNotExistsException.class)
    public String handleStudentNotExists() {
        return "studentNotExists";
    }
}

這樣的話,如果任意的控制器拋出StudentNotExistsException異常,不管這個方法位於哪個控制器中,都會調用handleStudentNotExists()方法來處理異常。

5、跨重定向請求傳遞數據

在JSP中我們就知道,重定向是由客戶端向服務端重新發起一個新的請求,這就意味着原來存儲在舊請求中的屬性隨着舊請求一起消亡了,新的請求中將不包含舊請求中的數據。爲了能夠從發起重定向的方法傳遞數據給處理重定向方法中,Spring提供了兩種方式:

  • 使用URL模板以及路徑變量和/或查詢參數的形式傳遞數據;
  • 通過flash屬性發送數據

5.1、通過URL模板進行重定向

通過路徑參數或者查詢參數的方式在重定向中傳遞參數是很簡單的,例如,我們想要重定向到查詢學生信息頁面並查詢id爲1的學生信息,因爲我們已經有了通過學生id查詢學生信息的處理器方法了,所以我們只需要在重定向的時候加上路徑參數即可:return “redirect:/queryStudentInfo/1”,我們想查詢哪個學生信息只需要修改URL的路徑參數即可,但是這樣直接拼接路徑參數是不太合適的,如果要構建SQL查詢語句的話,這樣拼接String是很危險的,所以Spring還提供了使用模板方法的方式,例如:

@RequestMapping(value="/studentOne",method=RequestMethod.GET)
public String studentOne(Model model) {

    model.addAttribute("id", 1);
    return "redirect:/queryStudentInfo/{id}";
}

這樣id作爲佔位符填充到URL中,而不是直接拼接到重定向String中,id中的不安全字符會進行轉義,這裏id可以允許用戶輸入任意的值,並將其附加到路徑上,這裏我們是爲了示意才設置id爲1。

除此之外,模型中所有的其他原始類型都可以添加到URL中作爲查詢參數,例如:

@RequestMapping(value="/studentOne",method=RequestMethod.GET)
public String studentOne(Model model) {

    model.addAttribute("id", 1);
    model.addAttribute("name", "Tom");
    return "redirect:/queryStudentInfo/{id}";
}

在這裏,因爲模型中的name屬性沒有匹配重定向URL中的任何佔位符,所以它會自動以查詢參數的形式附加到重定向URL中,所以最終得到的URL爲:http://localhost:8080/spring_mvc1/queryStudentInfo/1?name=Tom

通過路徑變量和查詢參數的形式跨重定向傳遞數據是很簡單直接的方式,但它也有一定的限制,它只能用來發送簡單的值,如String和數字的值,在URL中,並沒有辦法發送更復雜的值,這時候我們需要就使用flash屬性了。

5.2、使用flash屬性

假設我們想將一個學生對象通過重定向傳遞給重定向的處理方法,我們可以通過session來實現,在重定向之前,將學生對象放入session中,在重定向之後,將學生對象從session中取出,然後將其從session中清除。其實,Spring提供了一種類似的方式,我們無需管理這些對象,只需傳遞、獲取數據即可,這就是flash屬性。

Spring提供了通過RedirectAttributes設置flash屬性的方法,這是Spring 3.1引入的Model的一個子接口,RedirectAttributes提供了Model的所有功能,除此之外,還有幾個方法是用來設置flash屬性的,flash屬性會一直攜帶這些數據直到下一次請求,然後纔會消失。下面這個方法我們就是通過flash屬性將Student對象添加到模型中:

@RequestMapping(value="/studentOne",method=RequestMethod.GET)
public String flashAttr(RedirectAttributes model) { 
    model.addFlashAttribute("student", new Student().getStudentById(1));

    return "redirect:/queryStudentOne";
}

在執行重定向之前,所有的flash屬性都會複製到session中,在重定向後,存在session中的flash屬性會被取出,並從session轉移到模型中,處理重定向的方法就能從模型中訪問Student對象了,就像獲取其他的模型對象一樣,所以下面這個方法無需任何設置可以直接渲染學生信息頁面:

@RequestMapping(value="/queryStudentOne",method=RequestMethod.GET)
public String queryStudentOne(Model model) {

    return "student";
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章