《Spring實(shí)戰(zhàn)》學(xué)習(xí)筆記-第七章:Spring MVC進(jìn)階

本章主要內(nèi)容:

  • 備用的Spring MVC配置項(xiàng)
  • 處理文件上傳
  • 控制器中的異常處理
  • 使用flash屬性

“等等,客官!不止這些”

也許大家在看電視廣告時(shí)對(duì)上面這句話(huà)比較熟悉,廣告里通常在已經(jīng)對(duì)商品做了完整的介紹,這時(shí),電視里就會(huì)冒出這句:等等,客官,還不止這些。。。接著,就會(huì)繼續(xù)吹噓他們的商品還有更多讓你意想不到的功能。

其實(shí),Spring MVC(或者說(shuō)Spring的每一個(gè)模塊)就給人一種“不止這些”的感覺(jué),就在你以為已經(jīng)對(duì)Spring MVC的功能有了完備的了解時(shí),又會(huì)發(fā)現(xiàn)可以利用它做的更多。

在第五章中,我們使用Spring MVC的基本功能以及如何編寫(xiě)控制器來(lái)處理各種各樣的請(qǐng)求。接著在第六章中創(chuàng)建了JSP和Thymeleaf視圖來(lái)將model數(shù)據(jù)對(duì)用戶(hù)進(jìn)行了展示。也許你會(huì)覺(jué)得Spring MVC不過(guò)如此。但是等等,還不止這些!

本章中會(huì)繼續(xù)討論Spring MVC,比如編寫(xiě)控制器來(lái)處理文件上傳,如何處理控制器中的異常,以及如何在model上傳遞數(shù)據(jù)從而可以在重定向時(shí)使用。

首先,在第五章中使用了AbstractAnnotationConfigDispatcherServletInitializer來(lái)設(shè)置Spring MVC,并且說(shuō)了可以使用其他備用設(shè)置選擇。因此在文件上傳和異常處理之前,先來(lái)探索一下如何使用其他方式來(lái)設(shè)置DispatcherServletContextLoaderListener。

Spring MVC備用配置

第五章中,通過(guò)繼承AbstractAnnotationConfigDispatcherServletInitializer來(lái)快速地對(duì)Spring MVC進(jìn)行了設(shè)置。該類(lèi)假設(shè)你想要一個(gè)基礎(chǔ)的DispatcherServletContextLoaderListener設(shè)置,并且通過(guò)Java而不是XML文件來(lái)配置Spring。

盡管這樣配置對(duì)大多數(shù)Spring應(yīng)用都是適用的,但是總有意外,比如你想要除了DispatcherServlet之外的servlet和filter,或者你想對(duì)DispatcherServlet做一些進(jìn)一步的配置,再或者,你想在Servlet3.0之前的版本上部署應(yīng)用,那么你就要使用傳統(tǒng)的web.xml文件對(duì)DispatcherServlet進(jìn)行配置了。

幸運(yùn)的是,在(garden-variety)普通的AbstractAnnotationConfigDispatcherServletInitializer不適用于你的需求時(shí),還有其他的一些方式供你使用。下面,我們就開(kāi)始如何定制化的配置DispatcherServlet吧。

DispatcherServlet個(gè)性化配置

SpittrWebAppInitializer中所包含的三個(gè)方法僅僅是必須重寫(xiě)的三個(gè)抽象方法,同時(shí)還有許多其他方法可以重寫(xiě)從而可以實(shí)現(xiàn)更多的配置。

其中一個(gè)就是customizeRegistration(),在AbstractAnnotationConfigDispatcherServletInitializer注冊(cè)了DispatcherServlet之后,就會(huì)調(diào)用customizeRegistration()方法,并根據(jù)servlet的注冊(cè)返回值傳送ServletRegistration.Dynamic,通過(guò)對(duì)customizeRegistration()的重寫(xiě),就可以對(duì)DispatcherServlet進(jìn)行額外的配置。

比如,在稍后的章節(jié)中(7.2),你會(huì)看到Spring MVC如何處理多個(gè)請(qǐng)求和文件上傳。如果打算使用Servlet3.0來(lái)實(shí)現(xiàn)多部分配置,那么就需要激活DispatcherServlet配置來(lái)實(shí)現(xiàn)多路請(qǐng)求??梢允褂孟旅娴姆绞街貙?xiě)customizeRegistration()方法:

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

其中ServletRegistration.Dynamic作為入?yún)?,你可以做很多事情,比如調(diào)用setLoadOnStartup()來(lái)設(shè)置加載時(shí)優(yōu)先級(jí),調(diào)用setInitParameter()來(lái)設(shè)置初始化參數(shù),調(diào)用setMultipartConfig()來(lái)設(shè)置Servlet3.0的多路支持。在上述示例中,設(shè)置了多路支持的上傳文件臨時(shí)存儲(chǔ)路徑為:/tmp/spittr/uploads。

添加額外的servlet和filter

根據(jù)之前的配置,可以生成DispatcherServlet和ContextLoaderListener,但是你需要注冊(cè)額外的servlet、filter或者listener時(shí)怎么辦呢?

使用基于Java配置的一個(gè)好處就是你可以盡量多的定義初始化類(lèi)。因此,如果需要定義額外的組件,只需新建相應(yīng)的初始化類(lèi)即可。最簡(jiǎn)單的方法就是實(shí)現(xiàn)Spring的WebApplicationInitializer接口。

例如,下面的代碼展示了如何通過(guò)實(shí)現(xiàn)WebApplicationInitializer接口的方式來(lái)注冊(cè)一個(gè)servlet:

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 {
        // 定義servlet
        Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
        // 映射servlet
        myServlet.addMapping("/custom/**");
    }
}

上述代碼僅僅是一個(gè)基本的servlet注冊(cè)初始化類(lèi),實(shí)現(xiàn)了對(duì)servlet的注冊(cè)并映射到一個(gè)路徑。你也可以使用這種方式來(lái)手動(dòng)地注冊(cè)DispatcherServlet(不過(guò)這好像沒(méi)有必要,因?yàn)锳bstractAnnotationConfigDispatcherServletInitializer在這方面已經(jīng)做得很不錯(cuò)了)。

同樣的,你也可以通過(guò)上述方式來(lái)注冊(cè)listener和filter。例如:

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    // 注冊(cè)一個(gè)filter
    javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
    // 添加映射
    filter.addMappingForUrlPatterns(null, false, "/custom/*");
}

WebApplicationInitializer是一個(gè)在注冊(cè)servlet、filter、listener時(shí)比較推薦的方式,當(dāng)然你是使用基于Java的配置方式并將應(yīng)用部署在Servlet3.0容器上的。如果你僅僅需要注冊(cè)一個(gè)filter并將其映射到DispatcherServlet,那么使用AbstractAnnotationConfigDispatcherServletInitializer將是一個(gè)捷徑。

要注冊(cè)多個(gè)filter并將它們映射到DispatcherServlet,你所要做的僅僅是重寫(xiě)getServletFilters()方法。比如:

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

如你所見(jiàn),該方法返回了一個(gè)javax.servlet.Filter的數(shù)組,這里僅僅返回了一個(gè)filter,但是它可以返回很多個(gè)。同時(shí)這里不再需要為這些filter去聲明映射,因?yàn)橥ㄟ^(guò)getServletFilters()返回的filter會(huì)自動(dòng)地映射到DispatcherServlet。

當(dāng)部署到Servlet3.0的容器時(shí),Spring提供了很多方法來(lái)注冊(cè)servlet、filter和listener,而不再需要web.xml。如果你使用的不是Servlet3.0版本的容器,或者你就喜歡使用基于web.xml的配置方式,那么該如何對(duì)Spring MVC進(jìn)行配置呢?

使用web.xml聲明DispatcherServlet

下面是一個(gè)典型的web.xml文件,其中對(duì)DispatcherServlet和ContextLoaderListener進(jìn)行了聲明:

<?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>
        <param-value>/WEB-INF/spring/root-context.xml</param-value>
    </context-param>
    <listener>
        <!-- 注冊(cè)ContextLoaderListener -->
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <!-- 注冊(cè)DispatcherServlet -->
        <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>

正如在第五章中所說(shuō)的,DispatcherServlet和ContextLoaderListener可以加載Spring應(yīng)用上下文。contextConfigLocation上下文參數(shù)指定了用來(lái)定義由ContextLoaderListener加載的根應(yīng)用上下文的XML文件的位置。DispatcherServlet用來(lái)通過(guò)文件中定義的bean(名稱(chēng)基于指定的servlet名稱(chēng):appServlet)來(lái)加載應(yīng)用上下文。因此,DispatcherServlet會(huì)從/WEB-INF/appServlet-context.xml文件中加載應(yīng)用上下文。

如果你想指定DispatcherServlet配置文件的位置,那么可以通過(guò)設(shè)置contextConfigLocation初始化參數(shù)的方式實(shí)現(xiàn)。例如,下面的DispatcherServlet配置就會(huì)從/WEB-INF/spring/appServlet/servlet-context.xml文件中加載:

<servlet>
    <servlet-name>appServlet</servlet-name>
    <!-- 注冊(cè)DispatcherServlet -->
    <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>

本書(shū)中采用的都是基于Java的配置方式,所以你需要對(duì)Spring MVC進(jìn)行設(shè)置從而可以從@Configuration注解的類(lèi)中加載配置。為了使用基于Java的配置,需要通知DispatcherServlet和ContextLoaderListener去使用AnnotationConfigWebApplicationContext,該類(lèi)是WebApplicationContext接口的實(shí)現(xiàn)類(lèi),它可以對(duì)Java配置類(lèi)進(jìn)行加載??梢酝ㄟ^(guò)設(shè)置DispatcherServlet的contextClass參數(shù)和初始化參數(shù)來(lái)實(shí)現(xiàn)。下面對(duì)web.xml進(jìn)行配置從而可以使用Java配置:

<?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">
    <!-- 使用Java配置 -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>
    
    <!-- 指定所使用的Java配置類(lèi) -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>spittr.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>
        <!-- 使用Java配置 -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!-- 指定DispatcherServlet的配置類(lèi) -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                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>

以上就是配置Spring MVC的一些方法,下面來(lái)看如何使用Spring MVC處理文件上傳。

處理multipart表單數(shù)據(jù)

一個(gè)web應(yīng)用通常都會(huì)允許用戶(hù)上傳內(nèi)容,比如像Facebook、Flickr這樣的站點(diǎn),都會(huì)允許用戶(hù)上傳小片的。我們的Spittr應(yīng)用中在兩處會(huì)用到文件上傳:一是新用戶(hù)注冊(cè)的時(shí)候,這時(shí)需要選擇一個(gè)頭像之類(lèi)的;還有就是當(dāng)用戶(hù)新建一個(gè)Spittle(推文?)時(shí),也許需要在文中插入一張圖片。

來(lái)自傳統(tǒng)的表單提交的請(qǐng)求結(jié)果一般比較簡(jiǎn)單并且采用多個(gè)鍵值對(duì)的方式。例如,當(dāng)提交一個(gè)注冊(cè)信息的表單時(shí),請(qǐng)求會(huì)是這樣的:
firstName=Charles&lastName=Xavier&email=professorx%40xmen.org &username=professorx&password=letmein01

雖然這種編碼方式對(duì)于傳統(tǒng)的基于文本的提交是最夠的,但是它卻沒(méi)有強(qiáng)大到可以攜帶二進(jìn)制數(shù)據(jù),比如上傳一個(gè)圖像。相反的,Multipart/form-data將表單分割成獨(dú)立的部分,每個(gè)部分都有各自的類(lèi)型。傳統(tǒng)的表單域都有文本數(shù)據(jù),但是當(dāng)要上傳一些東西時(shí),該部分可以是二進(jìn)制的,如下面的multipart請(qǐng)求體:

------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="firstName"
Charles
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="lastName"
Xavier
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="email"
charles@xmen.com
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="username"
professorx
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="password"
letmein01
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"
Content-Type: image/jpeg
[[ Binary image data goes here ]]
------WebKitFormBoundaryqgkaBn8IHJCuNmiW--

在這個(gè)multipart請(qǐng)求中,值得注意的,profilePicture部分是與其他部分不同的,它有一個(gè)Content-Type頭部用來(lái)表示這是一個(gè)JPEG圖像。雖然不是很明顯,profilePicture的內(nèi)容是一個(gè)二進(jìn)制數(shù)據(jù)而不是簡(jiǎn)單文本。

雖然multipart請(qǐng)求看起來(lái)比較復(fù)雜,但是在Spring MVC中處理起來(lái)還是比較簡(jiǎn)單的。在編寫(xiě)控制器方法來(lái)處理文件上傳之前,還需要配置一個(gè)multipart解析器來(lái)告知DispatcherServlet如何讀取multipart請(qǐng)求。

配置multipart解析器

DispatcherServlet并沒(méi)有實(shí)現(xiàn)任何邏輯用來(lái)將數(shù)據(jù)轉(zhuǎn)換成multipart請(qǐng)求。它使用了Spring的MultipartResolver接口的實(shí)現(xiàn)類(lèi)來(lái)解析multipart請(qǐng)求中的內(nèi)容。從Spring3.1開(kāi)始,Spring提供了兩種MultipartResolver實(shí)現(xiàn)類(lèi)供選擇:

  • CommonsMultipartResolver:使用Jakarta Commons FileUpload來(lái)解析multipart請(qǐng)求;
  • StandardServletMultipartResolver:依靠Servlet 3.0支持來(lái)解析(Spring 3.1及以上);

一般來(lái)講,StandardServletMultipartResolver應(yīng)該是第一選擇。它使用servlet容器中現(xiàn)有的支持,并且不需要其他附加的項(xiàng)目依賴(lài)。但是,如果你將應(yīng)用部署在Servlet 3.0之前的版本,或者你沒(méi)有使用Spring3.1及以上版本,那么就要使用CommonsMultipartResolver。

使用Servlet 3.0解析multipart請(qǐng)求

StandardServletMultipartResolver沒(méi)有構(gòu)造器參數(shù)和屬性需要設(shè)置,這樣它的設(shè)置就比較簡(jiǎn)單,就像在Spring配置文件中聲明一個(gè)bean:

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

也許你想這么簡(jiǎn)單的方法,我該如何加一下限制呢?比如,如何限制一個(gè)用戶(hù)可以上傳的文件大小,或者如何設(shè)置上傳過(guò)程中文件的臨時(shí)存放位置。因?yàn)闆](méi)有構(gòu)造器和屬性可以設(shè)置,StandardServletMultipartResolver好像是有限制的。

其實(shí)是有辦法來(lái)設(shè)置StandardServletMultipartResolver的,但是它的設(shè)置不是在Spring配置中進(jìn)行的,而是在Servlet配置中。起碼要配置一下存放臨時(shí)文件的位置,進(jìn)一步來(lái)講,還要將multipart配置為DispatcherServlet的一部分。

如果你是在繼承自WebMvcConfigurerAdapter的servlet初始化類(lèi)中配置的DispatcherServlet,那么就可以在servlet注冊(cè)時(shí)通過(guò)調(diào)用setMultipartConfig()方法來(lái)配置multipart詳情。比如:

DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));

如果你是在繼承自AbstractAnnotationConfigDispatcherServletInitializer或者AbstractDispatcherServletInitializer的servlet初始化類(lèi)進(jìn)行的配置,沒(méi)有創(chuàng)建DispatcherServlet的實(shí)例或者使用servlet上下文對(duì)其進(jìn)行注冊(cè)。因此就沒(méi)有直接的引用供Dynamicservlet注冊(cè)來(lái)使用。但是你可以重寫(xiě)customizeRegistration()方法來(lái)進(jìn)行配置:

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

MultipartConfigElement的唯一參數(shù)設(shè)置了上傳文件時(shí)臨時(shí)文件的存放位置。也可以進(jìn)行其他一些設(shè)置:

  • 文件上傳的最大值(byte),默認(rèn)沒(méi)有限制;
  • 所有multipart請(qǐng)求的文件最大值(byte),不管有多少個(gè)請(qǐng)求,默認(rèn)無(wú)限制;
  • 直接上傳文件(不需存儲(chǔ)到臨時(shí)目錄)的最大值(byte),默認(rèn)是0,也就是所有的文件都要寫(xiě)入硬盤(pán);

例如,你想設(shè)置文件大小不超過(guò)2MB,所有請(qǐng)求的總和不超過(guò)4MB,并且所有文件都要寫(xiě)入硬盤(pán),那么就可以這樣設(shè)置:

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

如果你是使用的傳統(tǒng)的web.xml的方式來(lái)設(shè)置的DispatcherServlet,那么就需要使用多個(gè)<multipart-config>元素,其默認(rèn)值和MultipartConfigElement相同,并且<location>是必填項(xiàng):

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

配置Jakarta Commons FileUpload解析器

最簡(jiǎn)單的CommonsMultipartResolver聲明方式是這樣的:

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

與StandardServletMultipartResolver不同的是,它不需要配置一個(gè)臨時(shí)目錄。默認(rèn)情況下會(huì)使用servlet容器的臨時(shí)目錄。但是,你也可以通過(guò)uploadTempDir屬性進(jìn)行設(shè)置,同時(shí)還可以對(duì)其他參數(shù)進(jìn)行設(shè)置:

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

這里設(shè)置了文件的最大大小為2MB,最大的內(nèi)存中大小為0,即每個(gè)上傳文件都會(huì)直接寫(xiě)入磁盤(pán)的。但是它是無(wú)法設(shè)置multipart請(qǐng)求總的文件大小的。

處理multipart請(qǐng)求

通過(guò)上面的配置,Spring已經(jīng)支持multipart請(qǐng)求,那么就可以開(kāi)始編寫(xiě)控制器來(lái)處理文件上傳了。最普遍的做法就是使用@RequestPart注解一個(gè)控制器參數(shù)。

假設(shè)你想讓用戶(hù)可以在注冊(cè)時(shí)上傳圖像,那么就需要對(duì)注冊(cè)表單進(jìn)行更改從而用戶(hù)可以選擇一個(gè)圖片,同時(shí)還需要更改SpitterController中的processRegistration()方法以獲取上傳的文件。下面的代碼是使用Thymeleaf的注冊(cè)頁(yè)面:

  <form method="POST" th:object="${spitter}" enctype="multipart/form-data">
  ...
  <label>Profile Picture</label>:
    <input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif" /><br/>
    <input type="submit" value="Register" />
  ...   

可以發(fā)現(xiàn)<form>標(biāo)簽多了enctype="multipart/form-data"屬性,該屬性會(huì)告知瀏覽器要將當(dāng)前form作為multipart數(shù)據(jù)處理。

除此之外,還添加了一個(gè)新的file類(lèi)型的<input>標(biāo)簽,該標(biāo)簽允許用戶(hù)選擇一個(gè)圖片進(jìn)行上傳。accept屬性設(shè)置了允許選擇的圖片類(lèi)型。根據(jù)它的name屬性,圖片數(shù)據(jù)會(huì)放在profilePicture部分進(jìn)行發(fā)送。

現(xiàn)在所需做的就是更新processRegistration()方法,來(lái)獲取上傳的圖片,其中一種方法就是添加一個(gè)用@RequestPart注解的byte數(shù)組:

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

當(dāng)注冊(cè)表單提交時(shí),請(qǐng)求部分的數(shù)據(jù)就會(huì)賦予到profilePicture屬性中,如果用戶(hù)沒(méi)有選中一個(gè)文件,那么該數(shù)組就會(huì)是一個(gè)空值(不是null)。既然已經(jīng)獲取到上傳的文件,下面所需要的就是將文件保存。

接收multipart文件

處理上傳文件的原始數(shù)據(jù)比較簡(jiǎn)單但是是有局限的,因此,Spring提供了MultipartFile,使用它可以獲取到富對(duì)象從而更好地處理multipart數(shù)據(jù),下面就是MultipartFile接口:

package org.springframework.web.multipart;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public interface MultipartFile {
    String getName();
    String getOriginalFilename();
    String getContentType();
    boolean isEmpty();
    long getSize();
    byte[] getBytes() throws IOException;
    InputStream getInputStream() throws IOException;
    void transferTo(File dest) throws IOException;
}

MultipartFile提供獲取上傳文件的方法,同時(shí)提供了很多其他方法,比如原始文件名稱(chēng)、大小和內(nèi)容類(lèi)型等。另外還提供了一個(gè)InputStream可以將文件數(shù)據(jù)作為數(shù)據(jù)流讀取。

另外,MultipartFile還提供了一個(gè)方便的transferTo()方法幫助你將上傳文件寫(xiě)入到文件系統(tǒng)。例如,你可以將如下代碼加入到processRegistration()中:

profilePicture.transferTo(new File("/data/spittr/" + profilePicture.getOriginalFilename()));

像這樣將文件保存到本地文件系統(tǒng)非常簡(jiǎn)單,但是將文件管理的工作留給了你。你需要保證有足夠的空間,保證對(duì)文件進(jìn)行了備份以防硬件問(wèn)題。同事還需要進(jìn)行多服務(wù)器之間的文件同步。

將文件保存到Amazon S3

另外的辦法就是將上面這些都托管給其他人,可以存放在云端,下面的代碼可以將上傳的圖像保存到Amazon S3:

private void saveImage(MultipartFile image) throws ImageUploadException {
    try {
        AWSCredentials awsCredentials = new AWSCredentials(s3AccessKey, s2SecretKey);
        // 配置S3服務(wù)
        S3Service s3 = new RestS3Service(awsCredentials);
        // 創(chuàng)建S3 bucket對(duì)象
        S3Bucket bucket = s3.getBucket("spittrImages");
        S3Object imageObject = new S3Object(image.getOriginalFilename());
        // 設(shè)置圖像數(shù)據(jù)
        imageObject.setDataInputStream(image.getInputStream());
        imageObject.setContentLength(image.getSize());
        imageObject.setContentType(image.getContentType());
        AccessControlList acl = new AccessControlList();
        // 設(shè)置權(quán)限
        acl.setOwner(bucket.getOwner());
        acl.grantPermission(GroupGrantee.ALL_USERS, Permission.PERMISSION_READ);
        imageObject.setAcl(acl);
        // 保存圖片
        s3.putObject(bucket, imageObject);
    } catch (Exception e) {
        throw new ImageUploadException("Unable to save image", e);
    }

saveImage()的第一步就是設(shè)置Amazon Web Service (AWS)認(rèn)證,你需要提供S3的密鑰和私鑰,這些在注冊(cè)S3服務(wù)時(shí)Amazon都會(huì)給你的。

認(rèn)證過(guò)AWS之后,saveImage()創(chuàng)建了一個(gè)JetS3t的RestS3Service實(shí)例,可以通過(guò)它操作S3文件系統(tǒng)。它會(huì)獲取一個(gè)spittrImages的bucket引用,并創(chuàng)建用于包含圖標(biāo)的S3Object對(duì)象,然后將突破數(shù)據(jù)填充到S3Object中。

在調(diào)用putObject()方法將圖片數(shù)據(jù)寫(xiě)入S3之前,saveImage()方法設(shè)置了S3Object的權(quán)限,允許有所有用戶(hù)查看。這很重要,因?yàn)槿绻麤](méi)有設(shè)置的話(huà),那么這些圖片對(duì)于應(yīng)用程序的用戶(hù)來(lái)說(shuō)都是不可見(jiàn)得了。如果出現(xiàn)什么問(wèn)題的話(huà),會(huì)拋出ImageUploadException異常。

接收上傳文件為Part

如果你將應(yīng)用部署在Servlet 3.0的容器上,那么你可以選擇不使用MultipartFile,Spring MVC也可以將javax.servlet.http.Part作為控制器的入?yún)?,使?code>Part后processRegistration()方法就是這樣的了:

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

大多數(shù)情況下Part接口和MultipartFile沒(méi)什么區(qū)別,如下面的代碼所示:

package javax.servlet.http;
import java.io.*;
import java.util.*;

public interface Part {
    public InputStream getInputStream() throws IOException;
    public String getContentType();
    public String getName();
    public String getSubmittedFileName();
    public long getSize();
    public void write(String fileName) throws IOException;
    public void delete() throws IOException;
    public String getHeader(String name);
    public Collection<String> getHeaders(String name);
    public Collection<String> getHeaderNames();
}

一些方法就是名稱(chēng)上的不同,比如getSubmittedFileName()getOriginalFilename()是對(duì)應(yīng)的。write()transferTo()是對(duì)應(yīng)的,可以這樣使用:
profilePicture.write("/data/spittr/" + profilePicture.getOriginalFilename());

值得注意的是,如果你使用Part作為參數(shù),那么就不再需要配置StandardServletMultipartResolverbean,它只需在使用MultipartFile時(shí)進(jìn)行配置。

異常處理

一直以來(lái)我們都是假設(shè)Spittr應(yīng)用中的一切都是正常運(yùn)行的,但是如果哪里出現(xiàn)錯(cuò)誤了呢?或者在處理請(qǐng)求時(shí)出現(xiàn)了異常?這時(shí)該向客戶(hù)端發(fā)送什么響應(yīng)呢?

不論發(fā)生什么,好的或者壞的,一個(gè)servlet請(qǐng)求的輸出只能是一個(gè)servlet響應(yīng)。如果在處理請(qǐng)求的過(guò)程中出現(xiàn)異常,輸出結(jié)果仍然是一個(gè)servlet響應(yīng),需要將異常轉(zhuǎn)換為一個(gè)響應(yīng)。

Spring提供了一些將異常轉(zhuǎn)化為響應(yīng)的方法:

  • 某些Spring異常會(huì)自動(dòng)的映射為特定的HTTP狀態(tài)碼;
  • 使用@ResponseStatus注解將一個(gè)異常映射為HTTP狀態(tài)碼;
  • 使用ExceptionHandler注解的方法可以用來(lái)處理異常

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

Spring可以自動(dòng)地將其異常映射為狀態(tài)碼,如下表:

Spring異常 HTTP狀態(tài)碼
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

表格里的異常通常是在DispatcherServlet中出錯(cuò)由Spring自身拋出的。例如,如果DispatcherServlet無(wú)法找到合適的控制器來(lái)處理請(qǐng)求,那么就會(huì)拋出NoSuchRequestHandlingMethodException,對(duì)應(yīng)的狀態(tài)碼就是404。

雖然這些內(nèi)置的映射有點(diǎn)用,但是不一定適用于其他的應(yīng)用異常。還好,Spring提供了@ResponseStatus注解將一個(gè)異常映射為HTTP狀態(tài)碼。

比如下面SpittleController中的請(qǐng)求處理方法就可以返回HTTP 404狀態(tài):

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

如果findOne()方法返回了一個(gè)null,那么就會(huì)拋出SpittleNotFoundException。這里,SpittleNotFoundException就是一個(gè)未經(jīng)檢查的異常:

package spittr.web;

public class SpittleNotFoundException extends Exception {

}

如果在處理請(qǐng)求時(shí)調(diào)用了spittle()方法,并且傳入的ID是空的,那么SpittleNotFoundException就會(huì)默認(rèn)產(chǎn)生500的響應(yīng)。實(shí)際上,如果沒(méi)有找到對(duì)應(yīng)的映射都會(huì)返回500的錯(cuò)誤。但是你也可以通過(guò)對(duì)SpittleNotFoundException進(jìn)行映射改變這種情況。

當(dāng)拋出SpittleNotFoundException時(shí)就表示一個(gè)請(qǐng)求的資源不存在,404恰好符合這種情況。那么,我們就使用@ResponseStatus來(lá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 Exception {

}

編寫(xiě)異常處理方法

將異常映射為狀態(tài)碼大多數(shù)情況下是比較簡(jiǎn)單有效的,但是如果想讓響應(yīng)不僅僅只有一個(gè)狀態(tài)碼呢?也許你想對(duì)異常進(jìn)行一些處理,就行處理請(qǐng)求一樣。

例如,SpittleRepository的save()方法在用戶(hù)重復(fù)創(chuàng)建Spittle時(shí)拋出了一個(gè)DuplicateSpittleException,那么SpittleController的saveSpittle()方法就需要處理該異常。如下面的代碼所示,saveSpittle()方法可以直接處理該異常:

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

上面的代碼并沒(méi)有什么特別的,這就是一個(gè)簡(jiǎn)單的Java異常處理。

這樣做還可以,但是這個(gè)方法有點(diǎn)復(fù)雜。如果saveSpittle()方法專(zhuān)注于業(yè)務(wù)處理,讓其他方法來(lái)處理異常該多好。下面就為SpittleController添加一個(gè)新的方法來(lái)處理DuplicateSpittleException異常:

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

@ExceptionHandler注解應(yīng)用在handleDuplicateSpittle()方法上,用來(lái)指定在有DuplicateSpittleException異常拋出時(shí)執(zhí)行。

有意思的是,@ExceptionHandler注解的方法在同一個(gè)控制器里是通用的額,即無(wú)論SpittleController的哪一個(gè)方法拋出DuplicateSpittleException異常,handleDuplicateSpittle()方法都可以對(duì)其進(jìn)行處理,而不再需要在每一個(gè)出現(xiàn)異常的地方進(jìn)行捕獲。

也許你在想,@ExceptionHandler注解的方法能不能捕獲其他controller里的異常?。吭赟pring3.2里是可以的,但僅僅局限于定義在控制器增強(qiáng)類(lèi)(controller advice class)里的方法。

那么什么是控制器增強(qiáng)類(lèi)呢?下面我們就來(lái)看看這個(gè)控制器增強(qiáng)類(lèi)。

控制器增強(qiáng)類(lèi)(controller advice class)

如果controller類(lèi)的特定切面可以跨越應(yīng)用的所有controller進(jìn)行使用,那么這將會(huì)帶來(lái)極大的便捷。例如,@ExceptionHandler方法就可以處理多個(gè)controller拋出的異常了。如果多個(gè)controller類(lèi)都拋出同一個(gè)異常,也許你會(huì)在這些controller進(jìn)行重復(fù)的@ExceptionHandler方法編寫(xiě)?;蛘?,你也可以編寫(xiě)一個(gè)異常處理的基類(lèi),供其他@ExceptionHandler方法進(jìn)行繼承。

Spring3.2帶來(lái)了另外一種處理方法:控制器增強(qiáng)類(lèi),即使用@ControllerAdvice進(jìn)行注解的類(lèi),它們會(huì)有下面幾個(gè)方法構(gòu)成:

  • @ExceptionHandler注解的
  • @InitBinder注解的
  • @ModelAttribute注解的

@ControllerAdvice注解的類(lèi)中的這些方法會(huì)在整個(gè)應(yīng)用中的所有controller的所有@RequestMapping注解的方法上應(yīng)用。

@ControllerAdvice注解本身是使用了@Component注解的,因此,使用@ControllerAdvice注解的類(lèi)會(huì)在組件掃描時(shí)進(jìn)行提取,就行使用@Controller注解的類(lèi)一樣。

@ControllerAdvice的最實(shí)用的一個(gè)功能就是將所有的@ExceptionHandler方法集成在一個(gè)類(lèi)中,從而可以在一個(gè)地方處理所有controller中的異常。例如,假設(shè)你想處理應(yīng)用中所有的DuplicateSpittleException異常,可以采用下面的方法:

package spittr.web;

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

// 聲明控制器增強(qiáng)
@ControllerAdvice
public class AppWideExceptionHandler {

    // 定義異常處理方法
    @ExceptionHandler(DuplicateSpittleException.class)
    public String handleDuplicateSpittle() {
        return "error/duplicate";
    }

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

}

現(xiàn)在,不論哪一個(gè)controller拋出DuplicateSpittleException,都會(huì)調(diào)用handleDuplicateSpittle()方法來(lái)處理。

在redirect請(qǐng)求中攜帶數(shù)據(jù)

正如前文提到的,在處理完一個(gè)POST請(qǐng)求后進(jìn)行重定向是一個(gè)不錯(cuò)的選擇,起碼這樣可以避免用戶(hù)點(diǎn)擊刷新造成的POST請(qǐng)求重發(fā)的問(wèn)題。

在第五章中,已經(jīng)在控制器方法返回的視圖名稱(chēng)中使用了redirect:前綴,這時(shí)返回的String不是用來(lái)尋找視圖,而是瀏覽器進(jìn)行跳轉(zhuǎn)的路徑:
return "redirect:/spitter/" + spitter.getUsername();

也許你認(rèn)為Spring處理重定向只能這樣了,但是等等:Spring還可以做得更多。

特別是一個(gè)重定向方法如何向處理重定向的方法發(fā)送數(shù)據(jù)呢?一般的,當(dāng)一個(gè)處理函數(shù)結(jié)束后,方法中的model數(shù)據(jù)都會(huì)作為request屬性復(fù)制到request中,并且request會(huì)傳遞到視圖中進(jìn)行解析。因?yàn)榭刂破骱鸵晥D面對(duì)的是同一個(gè)request,因此request屬性在forward時(shí)保留了下來(lái)。

但是,當(dāng)一個(gè)控制器返回的是一個(gè)redirect時(shí),原來(lái)的request會(huì)終止,并且會(huì)開(kāi)啟一個(gè)新的HTTP請(qǐng)求。原來(lái)request中所有的model數(shù)據(jù)都會(huì)清空。新的request不會(huì)有任何的model數(shù)據(jù)。

Model屬性會(huì)作為request的屬性但是不能再redirect中傳遞
Model屬性會(huì)作為request的屬性但是不能再redirect中傳遞

明顯的,現(xiàn)在不能再redirect時(shí)使用model來(lái)傳遞數(shù)據(jù)了。但是還有其他方法用來(lái)從重定向的方法中獲取數(shù)據(jù):

  • 將數(shù)據(jù)轉(zhuǎn)換為路徑參數(shù)或者查詢(xún)參數(shù)
  • 在flash屬性中發(fā)送數(shù)據(jù)
    首先來(lái)看一下Spring如何在路徑參數(shù)或者查詢(xún)參數(shù)中傳遞數(shù)據(jù)。

使用URL模版重定向

將數(shù)據(jù)轉(zhuǎn)化為路徑參數(shù)和查詢(xún)參數(shù)看起來(lái)比較簡(jiǎn)單。在之前的代碼里,新建的Spitter的username就是作為路徑參數(shù)進(jìn)行傳遞的。但是這里的username是轉(zhuǎn)換為String進(jìn)行傳遞的。使用String傳遞URL和SQL時(shí)是比較危險(xiǎn)的事情。

除了使用重定向鏈接,Spring提供了使用模版來(lái)定義重定向鏈接。例如下面的代碼:
return "redirect:/spitter/{username}";

你所需做的就是設(shè)置model中的相關(guān)值。因此,processRegistration()方法需要接收model作為入?yún)?,并將username設(shè)置其中。

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

由于這里使用了占位符而不是直接使用重定向String進(jìn)行連接,就可以將username中的不安全字符隱藏起來(lái)。這樣就比讓用戶(hù)直接輸入username并將其添加到路徑后面要更加安全。

另外,model中其他的原始值也會(huì)作為查詢(xún)參數(shù)添加到重定向URL中。例如,除了username,model同時(shí)也包括新建的Spitter對(duì)象的id屬性:

@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并沒(méi)有什么變化,但是由于model中的spitterId屬性并沒(méi)有映射到URL中的占位符,它會(huì)自動(dòng)作為查詢(xún)參數(shù)。

如果username是habuma,spitterId是42,那么返回的重定向路徑將是/spitter/habuma?spitterId=42。

使用路徑參數(shù)和查詢(xún)參數(shù)傳遞數(shù)據(jù)比較簡(jiǎn)單,但是它也有局限性。它只適用于傳遞簡(jiǎn)單值,比如String和數(shù)字,不能傳遞比較復(fù)雜的東西,那么我們就需要flash屬性來(lái)幫忙。

使用flash屬性

比如說(shuō)你不再是想在重定向中傳送一個(gè)username或者ID,而是傳送一個(gè)真正的Spitter對(duì)象。如果只傳送了一個(gè)ID,那么處理重定向的方法不得不去數(shù)據(jù)庫(kù)中查找該對(duì)象。但是在重定向之前你已經(jīng)有有一個(gè)Spitter對(duì)象了,為什么不將它傳送給重定向處理方法呢?

Spitter對(duì)象不像String或者int那么簡(jiǎn)單,因此不能作為路徑參數(shù)或者查詢(xún)參數(shù)進(jìn)行傳送。但是,它可以作為model的一個(gè)屬性。

但是在上面的討論中,model屬性最終都會(huì)拷貝到request中,并隨著redirect的觸發(fā)而消失。因此,你需要將Spitter對(duì)象放在一個(gè)會(huì)隨著redirect存活的地方。

其中一個(gè)方法是將其放在session中,session是可以長(zhǎng)期存活的,可以跨越多個(gè)request。因此,你可以將Spitter對(duì)象在redirect之前放在session中,并在redirect之后取出。當(dāng)然你還要在取出之后將其從session中清理。

事實(shí)證明,Spring允許將數(shù)據(jù)存放在session中,從而在redirect時(shí)傳遞數(shù)據(jù)。但是Spring認(rèn)為你不應(yīng)該負(fù)責(zé)管理這些數(shù)據(jù)。相反,Spring提供了將數(shù)據(jù)作為flash屬性進(jìn)行傳送的功能。Flash屬性,即在到下一個(gè)request之前一直攜帶數(shù)據(jù),然后它們就走了。

Spring提供了通過(guò)RedirectAttributes來(lái)設(shè)置flash屬性,RedirectAttributes作為Model的子接口,新增了一些方法用來(lái)設(shè)置flash屬性。

特別的,RedirectAttributes提供了addFlashAttribute()方法用來(lái)添加flash屬性。那么就可以利用它來(lái)重寫(xiě)processRegistration()方法:

@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對(duì)象作為一個(gè)值添加到flash屬性中。另外,你也可以不填對(duì)應(yīng)的key值:
model.addFlashAttribute(spitter);
由于你傳遞了一個(gè)Spitter對(duì)象,因此key會(huì)自動(dòng)生成為spitter

在重定向之前,所有的flash屬性都會(huì)拷貝到session中,在重定向之后,存儲(chǔ)在session中的flash屬性會(huì)從session中移出到model中。然后處理重定向請(qǐng)求的方法就可以使用Spitter對(duì)象了,如下圖所示:

flash屬性都會(huì)拷貝到session中,然后轉(zhuǎn)存到model中
flash屬性都會(huì)拷貝到session中,然后轉(zhuǎn)存到model中

下面對(duì)showSpitterProfile()進(jìn)行一點(diǎn)點(diǎn)更,在從數(shù)據(jù)庫(kù)查找之前對(duì)Spitter進(jìn)行檢查:

@RequestMapping(value = "/{username}", method = RequestMethod.GET)
public String showSpitterProfile(@PathVariable("username") String username, Model model) {
    if (!model.containsAttribute("spitter")) {
        Spitter spitter = spitterRepository.findByUsername(username);
        model.addAttribute(spitter);
    }
    return "profile";
}

正如你所見(jiàn),該方法的第一件事是檢查model中是否含有spitter的屬性,如果有就啥也不做了。Spitter對(duì)象會(huì)被直接傳送到視圖中進(jìn)行解析。如果沒(méi)有再去數(shù)據(jù)庫(kù)里查。

總結(jié)

每當(dāng)使用Spring時(shí),好像總有更多:更多的特性、更多的選擇以及更多的途徑可以達(dá)到目標(biāo),Spring MVC有很多花樣繁多的功能。

Spring MVC的配置就是一個(gè)你需要進(jìn)行選擇的地方。本章中,我們從如何配置Spring MVC的DispatcherServletContextLoaderListener說(shuō)起。你可以看到如何進(jìn)行DispatcherServlet注冊(cè)以及其他的servlet和filter的注冊(cè)。另外,如果將應(yīng)用部署在比較舊的容器上,我們還可以使用web.xml進(jìn)行配置。

接著,我們看了如何處理Spring MVC控制器拋出的異常。盡管@RequestMapping方法可以處理異常,如果你將異常處理部分抽取出來(lái)那么你的代碼就會(huì)比較清爽。

為了完成通用的任務(wù),比如異常處理,會(huì)在整個(gè)應(yīng)用中使用,Spring3.2開(kāi)始提供了@ControllerAdvice來(lái)創(chuàng)建增強(qiáng)型控制器,從而可以在一個(gè)地方完成通用的異常處理。

最后,我們研究了如何在重定向時(shí)傳遞數(shù)據(jù),那就是使用Spring的flash屬性。

至此,也許你會(huì)覺(jué)得,不過(guò)如此嘛!但是我們討論的僅僅是Spring MVC功能的一小部分。在16章中我們還會(huì)討論其他功能,比如如何利用它來(lái)創(chuàng)建REST API。

下面的章節(jié),我們先放一放Spring MVC,來(lái)看一下Spring Web Flow,這是一個(gè)流框架,是Spring MVC的擴(kuò)展,它能夠在Spring中實(shí)現(xiàn)面向會(huì)話(huà)的Web開(kāi)發(fā)。


如果覺(jué)得有用,歡迎關(guān)注我的微信,有問(wèn)題可以直接交流:

你的關(guān)注是對(duì)我最大的鼓勵(lì)!
你的關(guān)注是對(duì)我最大的鼓勵(lì)!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容