1. 程式人生 > >Spring——SpringMVC(三)

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為:

相關推薦

Spring——SpringMVC

本文主要依據《Spring實戰》第七章內容進行總結 前兩節中,我們主要介紹了Spring MVC的控制器和檢視解析器,通過上面兩節內容,我們對Spring MVC已經有了一個初步的瞭解,也能夠使用Spring MVC進行基本的Web層程式設計了,但是Spring MV

spring boot : 熱部署

pom.xml文件 添加 gin 字節 loader 信息 dev spring tool 介紹了Spring boot實現熱部署的兩種方式,這兩種方法分別是使用 Spring Loaded和使用spring-boot-devtools進行熱部署。 熱部署是什麽

Spring入門之IoC

使用 bsp martin 需要 容器 nbsp 依賴註入 tin 這就是 一、IoC定義   IoC,即控制反轉。開發者在使用類的實例之前,需要先創建對象的實例。但是IoC將創建實例的任務交給IoC容器,這樣開發應用代碼時只需要直接使用類的實例,這就是IoC。在討論控制反

spring框架

方法 業務邏輯層 方式 spa 調用 aso ctrl oot ring 基於aspectj的註解aop(會用)   使用註解方式實現aop操作     第一步 創建對象     第二步 在spring核心配置文件,開啟aop操作         aop

Spring入門— AOP註解、jdbc模板、事務

list() 規範 行數 get attribute 樂觀鎖 過濾 callback 賬號 一、AOP註解開發 導入jar包 aop聯盟包、 aspectJ實現包 、 spring-aop-xxx.jar 、 spring-aspect-xxx.jar 導入約束 a

Spring Cloud:服務提供與調用 Eureka【Finchley 版】

fan default fun cer 觀察 微服務 divide 動態 erl Spring Cloud(三):服務提供與調用 Eureka【Finchley 版】 發表於 2018-04-15 | 更新於 2018-05-07 | 上一篇文章我們介紹了 Eure

Spring十九Spring AOP:切面的優先級

表達 對象 技術分享 方法 多個 getname exp 訪問 ioc容器 背景: 1)指定切面優先級示例:有的時候需要對一個方法指定多個切面,而這多個切面有時又需要按照不同順序執行,因此,切面執行優先級別指定功能就變得很實用。 2)重復使用切入點表達式:上一篇文章中,

spring boot數據訪問

document -s red pri 統一 ear color localhost link spring boot的數據訪問 spring data是spring用來解決數據訪問問題的的一攬子解決方案,spring data是一個傘形項目,包含了大量的關系型和非關系型數

Spring Boot——Spring Boot資料訪問

一、簡介 對於資料訪問層,無論是SQL還是NOSQL,Spring Boot預設採用整合Spring Data的方式進行統一處理,新增大量自動配置,遮蔽了很多設定。引入各種xxxTemplate,xxxRepository來簡化我們對資料訪問層的操作。對我們來說只需要進行簡單的設定即

Spring學習| Spring AOP

文章目錄 1. 什麼是 AOP ? 2. 為什麼需要使用 AOP ? 3. AOP 術語 4. 在 Spring 中使用 Aspect 註解方式進行切面程式設計 4.1 在 Spring 中啟用 AspectJ 註解支援 4.2

JDK的Proxy技術實現AOP,InvocationHandler和Proxy詳解——Spring AOP

上一章已經講到了想要完成AOP的設計原理以及要實現AOP功能,得需要使用代理模式: 本章就介紹一個實現動態代理的兩種方式之一——JDK中的Proxy技術 AOP實現原理(使用JDK中的Proxy技術實現AOP功能,InvocationHandler和Proxy(Class)詳解

Spring Boot整合MyBatis,MyBatis Generator

簡介 MyBatis 是一款優秀的持久層框架,它支援定製化 SQL、儲存過程以及高階對映。MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。MyBatis 可以使用簡單的 XML 或註解來配置和對映原生資訊,將介面和 Java 的 POJOs(Plain Old J

Spring MVC控制器獲取頁面請求引數以及將控制器資料傳遞給頁面和實現重定向的方式

首先做好環境配置 在mvc.xml裡進行配置   1.開啟元件掃描   2.開啟基於mvc的標註   3.配置試圖處理器 1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www

spring-cloud服務消費者Feign(Finchley版本)

Feign是一個宣告式的偽Http客戶端,它使得寫Http客戶端變得更簡單。使用Feign,只需要建立一個介面並註解。它具有可插拔的註解特性,可使用Feign 註解和JAX-RS註解。Feign支援可插拔的編碼器和解碼器。Feign預設集成了Ribbon,並和Eureka結合,預設實現了負載均衡的效

Spring框架AOP

 AOP為了解決動態代理的繁瑣,而產生的一種方便實現動態代理的簡單框架 動態代理簡單的來說就是將經常要用,重複的程式碼放到一個代理類裡,在其他類裡面呼叫就行,不用再每次重複寫。 1.將要下載的包放到pom.xml裡面 <?xml version="1.0"?> &l

Spring Cloud zookeeper實現服務治理

1.安裝ZooKeeper 我這裡直接使用docker安裝zookeeper,docker是個好東西,推薦大家使用,安裝docker和常用命令的使用可以去找找資料學習下 2. 服務註冊和發現 1.1 Maven依賴 <dependencies>

SpringMVC獲取控制器引數

SpringMVC獲取控制器引數 在無註解下獲取引數 在沒有註解的情況下,SpringMVC也可以獲取引數,且引數允許為空,唯一的要求是引數名稱和HTTP請求的引數名稱保持一致。 package com.lay.mvc.controller; import org.spri

spring-shiro-- spring boot + mybatis +shrio+spring mvc

前面對shiro記性了一些邏輯的操作,和具體的程式碼片段的操作,現在將我寫的一個小的案例,總結一下。總結完明天就上班了。 本案例使用的是spring boot + spring mvc +mybatis 專案的結構 首先對pom檔案進行操作 <?xml version="

spring整理Spring AOP

Spring AOP       AOP作為Spring中一個核心模組,通過代理模式在程式中架起了一層可自定義的攔截器 代理模式原理 靜態代理 一個靜態代理只能代理一個介面下的所有實現類 程式碼

Spring Cloud服務註冊與發現

Spring Cloud(三)服務註冊與發現 案例中有三個角色:服務註冊中心、服務提供者、服務消費者,其中服務註冊中心就是eureka單機版啟動既可,流程是首先啟動註冊中心,服務提供者生產服務並註冊到服務中心中,消費者從服務中心中獲取服務並執行。 這裡新建兩個spring boo