Spring MVC 配置的替代方案
自定義 DispatcherServlet 配置
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
public class MyServletInitializer implements WebApplicationInitializer{
@Override
public void onStartup(ServletContext servletContext) throws ServletException{
Dynamic myServlet = servletContext.addServlet("myServlet",MyServlet.class);
myServlet.addMapping("/custom/**");
}
}
雖然從代碼外觀上不一定能夠看得出來(lái)蛹尝,但是 Abstract-AnnotationConfigDispatcherServletInitializer 所完成的事情其實(shí)比看上去要多戈擒。在 SpittrWebAppInitializer 中我們所編寫的三個(gè)方法僅僅是必須要重載的abstract方法切心。但實(shí)際上還有更多的方法可以進(jìn)行重載,從而實(shí)現(xiàn)額外的配置。
此類的方法之一就是 customizeRegistration()。在 AbstractAnnotation-
ConfigDispatcherServletInitializer 將 DispatcherServlet 注冊(cè)到 Servlet 容器中之后,就會(huì)調(diào)用 customizeRegistration()温兼,并將 Servlet 注冊(cè)后得到的 Registration.Dynamic 傳遞進(jìn)來(lái)。通過(guò)重載 customizeRegistration() 方法武契,我們可以對(duì) DispatcherServlet 進(jìn)行額外的配置募判。
在稍后的內(nèi)容中,我們將會(huì)看到如何在 SpringMVC 中處理 multipart 請(qǐng)求和文件上傳咒唆。如果計(jì)劃使用 Servlet 3.0 對(duì) multipart 配置的支持届垫,那么需要使用 DispatcherServlet 的 registration 來(lái)啟用 multipart 請(qǐng)求。我們可以重載 customizeRegistration() 方法來(lái)設(shè)置 MultipartConfigElement全释,如下所示:
@Override
protected void customizeRegistration(Dynamic registration){
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}
借助 customizeRegistration() 方法中的 ServletRegistration.Dynamic装处,我們能夠完成多項(xiàng)任務(wù),包括通過(guò)調(diào)用 setLoadOnStartup() 設(shè)置 load-on-startup 優(yōu)先級(jí)浸船,通過(guò) setInitParameter() 設(shè)置初始化參數(shù)妄迁,通過(guò)調(diào)用setMultipartConfig() 配置 Servlet 3.0 對(duì) multipart 的支持。在前面的樣例中李命,我們?cè)O(shè)置了對(duì) multipart 的支持登淘,將上傳文件的臨時(shí)存儲(chǔ)目錄設(shè)置在 “/tmp/spittr/uploads” 中。
添加其他的Servlet和Filter
基于Java的初始化器(initializer)的一個(gè)好處就在于我們可以定義任意數(shù)量的初始化器類封字。因此形帮,如果我們想往 Web 容器中注冊(cè)其他組件的話,只需創(chuàng)建一個(gè)新的初始化器就可以了周叮。最簡(jiǎn)單的方式就是實(shí)現(xiàn) Spring 的WebApplicationInitializer 接口。
開頭的程序清單展現(xiàn)了如何創(chuàng)建 WebApplicationInitializer 實(shí)現(xiàn)并注冊(cè)一個(gè) Servlet界斜。
類似地仿耽,我們還可以創(chuàng)建新的 WebApplicationInitializer 實(shí)現(xiàn)來(lái)注冊(cè) Listener 和 Filter。例如各薇,如下的程序清單展現(xiàn)了如何注冊(cè) Filter项贺。
@Override
public void onStartup(ServletContext servletContext) throws ServletException{
javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter",MyFilter.class);
filter.addMappingForUrlPatterns(null,false,"/custom/**");
}
如果要將應(yīng)用部署到支持 Servlet 3.0 的容器中,那么 WebApplicationInitializer 提供了一種通用的方式峭判,實(shí)現(xiàn)在Java中注冊(cè) Servlet开缎、Filter 和 Listener。不過(guò)林螃,如果你只是注冊(cè) Filter奕删,并且該 Filter 只會(huì)映射到 DispatcherServlet 上的話,那么在 AbstractAnnotationConfigDispatcherServletInitializer 中還有一種快捷方式疗认。
為了注冊(cè) Filter 并將其映射到 DispatcherServlet完残,所需要做的僅僅是重載 AbstractAnnotationConfigDispatcherServletInitializer 的 getServlet-Filters() 方法伏钠。例如,在如下的代碼中谨设,重載了 AbstractAnnotationConfig-DispatcherServletInitializer 的 getServletFilters() 方法以注冊(cè) Filter:
@Override
protected Filter[] getServletFilters(){
return new Filter[] {new MyFilter()};
}
我們可以看到熟掂,這個(gè)方法返回的是一個(gè) javax.servlet.Filter 的數(shù)組。在這里它只返回了一個(gè) Filter扎拣,但它實(shí)際上可以返回任意數(shù)量的 Filter赴肚。在這里沒(méi)有必要聲明它的映射路徑,getServletFilters() 方法返回的所有 Filter 都會(huì)映射到 DispatcherServlet 上二蓝。
處理 multipart 形式的數(shù)據(jù)
在 Web 應(yīng)用中誉券,允許用戶上傳內(nèi)容是很常見的需求。Spittr 應(yīng)用在兩個(gè)地方需要文件上傳侣夷。當(dāng)新用戶注冊(cè)應(yīng)用的時(shí)候横朋,我們希望他們能夠上傳一張圖片,從而與他們的個(gè)人信息相關(guān)聯(lián)百拓。當(dāng)用戶提交新的 Spittle 時(shí)琴锭,除了文本消息以外,他們可能還會(huì)上傳一張照片衙传。
一般表單提交所形成的請(qǐng)求結(jié)果是很簡(jiǎn)單的决帖,就是以“&”符分割的多
個(gè)name-value對(duì)。例如蓖捶,當(dāng)在Spittr應(yīng)用中提交注冊(cè)表單時(shí)地回,請(qǐng)求會(huì)如
下所示:
firstName=Charles&lastName=Xavier&email=porfessorx%40xmen.org
&username=professorx&password=letmein01
盡管這種編碼形式很簡(jiǎn)單,并且對(duì)于典型的基于文本的表單提交也足夠滿足要求俊鱼,但是對(duì)于傳送二進(jìn)制數(shù)據(jù)刻像,如上傳圖片,就顯得力不從心了并闲。與之不同的是细睡,multipart 格式的數(shù)據(jù)會(huì)將一個(gè)表單拆分為多個(gè)部分(part),每個(gè)部分對(duì)應(yīng)一個(gè)輸入域帝火。在一般的表單輸入域中溜徙,它所對(duì)應(yīng)的部分中會(huì)放置文本型數(shù)據(jù),但是如果上傳文件的話犀填,它所對(duì)應(yīng)的部分可以是二進(jìn)制蠢壹,下面展現(xiàn)了 multipart 的請(qǐng)求體:
配置 multipart 解析器
DispatcherServlet 并沒(méi)有實(shí)現(xiàn)任何解析 multipart 請(qǐng)求數(shù)據(jù)的功能。它將該任務(wù)委托給了 Spring 中 MultipartResolver 策略接口的實(shí)現(xiàn)九巡,通過(guò)這個(gè)實(shí)現(xiàn)類來(lái)解析 multipart 請(qǐng)求中的內(nèi)容图贸。從 Spring 3.1 開始,Spring 內(nèi)置了兩個(gè) MultipartResolver 的實(shí)現(xiàn)供我們選擇:
-CommonsMultipartResolver:使用 Jakarta CommonsFileUpload 解析 multipart 請(qǐng)求;
-StandardServletMultipartResolver:依賴于 Servlet 3.0 對(duì) multipart 請(qǐng)求的支持(始于 Spring 3.1)求妹。
StandardServletMultipartResolver 沒(méi)有構(gòu)造器參數(shù)乏盐,也沒(méi)有要設(shè)置的屬性。這樣制恍,在 Spring 應(yīng)用上下文中父能,將其聲明為 bean 就會(huì)非常簡(jiǎn)單,如下所示:
@Bean
public MultipartResolver multipartResolver() throws IOException{
return new StandardServletMultipartResolver();
}
既然這個(gè) @Bean 方法如此簡(jiǎn)單净神,你可能就會(huì)懷疑我們到底該如何限制
StandardServletMultipartResolver 的工作方式呢何吝。如果我們想要限制用戶上傳文件的大小,該怎么實(shí)現(xiàn)鹃唯?如果我們想要指定文件在上傳時(shí)爱榕,臨時(shí)寫入目錄在什么位置的話,該如何實(shí)現(xiàn)坡慌?因?yàn)闆](méi)有屬性和構(gòu)造器參數(shù)黔酥,StandardServletMultipartResolver 的功能看起來(lái)似乎有些受限。
其實(shí)并不是這樣洪橘,我們是有辦法配置 StandardServletMultipartResolver 的限制條件的跪者。只不過(guò)不是在 Spring 中配置 StandardServletMultipartResolver,而是要在 Servlet 中指定 multipart 的配置熄求。至少渣玲,我們必須要指定在文件上傳的過(guò)程中,所寫入的臨時(shí)文件路徑弟晚。如果不設(shè)定這個(gè)最基本配置的話忘衍,StandardServlet-MultipartResolver 就無(wú)法正常工作。具體來(lái)講,我們必須要在 web.xml 或 Servlet 初始化類中,將 multipart 的具體細(xì)節(jié)作為 DispatcherServlet 配置的一部分鹅髓。
如果我們采用 Servlet 初始化類的方式來(lái)配置 DispatcherServlet 的話,這個(gè)初始化類應(yīng)該已經(jīng)實(shí)現(xiàn)了 WebApplicationInitializer搀捷,那我們可以在 Servlet registration 上調(diào)用 setMultipartConfig() 方法,傳入一個(gè) MultipartConfig-Element 實(shí)例勉耀。如下是最基本的 DispatcherServlet multipart 配置,它將臨時(shí)路徑設(shè)置為 “/tmp/spittr/uploads”:
DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet",ds);
registration.addMapping("/");
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
如果我們配置 DispatcherServlet 的 Servlet 初始化類繼承了Abstract AnnotationConfigDispatcherServletInitializer 或 AbstractDispatcher-ServletInitializer 的話蹋偏,那么我們不會(huì)直接創(chuàng)建 DispatcherServlet 實(shí)例并將其注冊(cè)到 Servlet 上下文中便斥。這樣的話,將不會(huì)有對(duì) Dynamic Servlet registration 的引用供我們使用了威始。但是枢纠,我們可以通過(guò)重載 customizeRegistration() 方法(它會(huì)得到一個(gè) Dynamic 作為參數(shù))來(lái)配置 multipart 的具體細(xì)節(jié):
@Override
protected void customizeRegistration(Dynamic registration){
registration.seMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}
到目前為止,我們所使用是只有一個(gè)參數(shù)的 MultipartConfigElement 構(gòu)造器黎棠,這個(gè)參數(shù)指定的是文件系統(tǒng)中的一個(gè)絕對(duì)目錄晋渺,上傳文件將會(huì)臨時(shí)寫入該目錄中镰绎。但是,我們還可以通過(guò)其他的構(gòu)造器來(lái)限制上傳文件的大小木西。除了臨時(shí)路徑的位置畴栖,其他的構(gòu)造器3所能接受的參數(shù)如下:
-上傳文件的最大容量(以字節(jié)為單位)。默認(rèn)是沒(méi)有限制的八千。
-整個(gè) multipart 請(qǐng)求的最大容量(以字節(jié)為單位)吗讶,不會(huì)關(guān)心有多少個(gè) part 以及每個(gè) part 的大小。默認(rèn)是沒(méi)有限制的恋捆。
-在上傳的過(guò)程中照皆,如果文件大小達(dá)到了一個(gè)指定最大容量(以字節(jié)為單位),將會(huì)寫入到臨時(shí)文件路徑中沸停。默認(rèn)值為 0膜毁,也就是所有上傳的文件都會(huì)寫入到磁盤上。
例如愤钾,假設(shè)我們想限制文件的大小不超過(guò) 2MB瘟滨,整個(gè)請(qǐng)求不超過(guò) 4MB,而且所有的文件都要寫到磁盤中绰垂。下面的代碼使用 MultipartConfigElement 設(shè)置了這些臨界值:
@Override
protected void customizeRegistration(Dynamic registration){
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads",2097152,4194304,0));
}
配置 Jakarta Commons FileUpload multipart 解析器
通常來(lái)講室奏,StandardServletMultipartResolver 會(huì)是最佳的選擇,但是如果我們需要將應(yīng)用部署到非 Servlet 3.0 的容器中劲装,那么就得需要替代的方案胧沫。如果喜歡的話,我們可以編寫自己的 MultipartResolver 實(shí)現(xiàn)占业。不過(guò)绒怨,除非想要在處理 multipart 請(qǐng)求的時(shí)候執(zhí)行特定的邏輯,否則的話谦疾,沒(méi)有必要這樣做南蹂。Spring 內(nèi)置了 CommonsMultipartResolver,可以作為 StandardServletMultipartResolver 的替代方案念恍。
將 CommonsMultipartResolver 聲明為 Spring bean 的最簡(jiǎn)單方式如下:
@Bean
public MultipartResolver multipartResolver(){
return new CommonsMultipartResolver();
}
與 StandardServletMultipartResolver 有所不同六剥,CommonsMultipart-Resolver 不會(huì)強(qiáng)制要求設(shè)置臨時(shí)文件路徑。默認(rèn)情況下峰伙,這個(gè)路徑就是 Servlet 容器的臨時(shí)目錄疗疟。不過(guò),通過(guò)設(shè)置 uploadTempDir 屬性瞳氓,我們可以將其指定為一個(gè)不同的位置:
@Bean
public MultipartResolver multipartResolver() throws IOException{
CommonMultipartResolver multipartResolver = new CommonMultipartResolver();
multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
return multipartResolver;
}
實(shí)際上策彤,我們可以按照相同的方式指定其他的 multipart 上傳細(xì)節(jié),也就是設(shè)置 CommonsMultipartResolver 的屬性。例如店诗,如下的配置就等價(jià)于我們?cè)谇拔耐ㄟ^(guò) MultipartConfigElement 所配置的 StandardServletMultipartResolver:
@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)存大小設(shè)置為0字節(jié)庞瘸。這兩個(gè)屬性直接對(duì)應(yīng)于 MultipartConfigElement 的第二個(gè)和第四個(gè)構(gòu)造器參數(shù)捧弃,表明不能上傳超過(guò) 2MB 的文件,并且不管文件的大小如何恕洲,所有的文件都會(huì)寫到磁盤中塔橡。但是與 MultipartConfigElement 有所不同,我們無(wú)法設(shè)定 multipart 請(qǐng)求整體的最大容量霜第。
處理multipart請(qǐng)求
現(xiàn)在已經(jīng)在 Spring 中(或 Servlet 容器中)配置好了對(duì) mutipart 請(qǐng)求的處理葛家,那么接下來(lái)我們就可以編寫控制器方法來(lái)接收上傳的文件。要實(shí)現(xiàn)這一點(diǎn)泌类,最常見的方式就是在某個(gè)控制器方法參數(shù)上添加 @RequestPart 注解癞谒。
假設(shè)我們?cè)试S用戶在注冊(cè) Spittr 應(yīng)用的時(shí)候上傳一張圖片,那么我們需要修改表單刃榨,以允許用戶選擇要上傳的圖片弹砚,同時(shí)還需要修改 SpitterController 中的 processRegistration() 方法來(lái)接收上傳的圖片。如下的代碼片段來(lái)源于 Thymeleaf 注冊(cè)表單視圖(registrationForm.html)枢希,著重強(qiáng)調(diào)了表單所需的修改:
<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/>
...
</form>
<form> 標(biāo)簽現(xiàn)在將 enctype 屬性設(shè)置為 multipart/formdata桌吃,這會(huì)告訴瀏覽器以 multipart 數(shù)據(jù)的形式提交表單,而不是以表單數(shù)據(jù)的形式進(jìn)行提交苞轿。在 multipart 中茅诱,每個(gè)輸入域都會(huì)對(duì)應(yīng)一個(gè) part。
除了注冊(cè)表單中已有的輸入域搬卒,我們還添加了一個(gè)新的 <input> 域瑟俭,其 type 為 file。這能夠讓用戶選擇要上傳的圖片文件契邀。accept 屬性用來(lái)將文件類型限制為 JPEG摆寄、PNG 以及 GIF 圖片。根據(jù)其 name 屬性坯门,圖片數(shù)據(jù)將會(huì)發(fā)送到 multipart 請(qǐng)求中的 profilePicture part 之中微饥。
現(xiàn)在,我們需要修改 processRegistration() 方法古戴,使其能夠接受上傳的圖片欠橘。其中一種方式是添加byte數(shù)組參數(shù),并為其添加 @RequestPart 注解允瞧。如下為示例:
@RequestMapping(value="/register",method=POST)
public String processRegistration(
@RequestPart("profilePicture") byte[] profilePicture,
@Valid Spitter spitter, Errors errors){
...
}
當(dāng)注冊(cè)表單提交的時(shí)候简软,profilePicture 屬性將會(huì)給定一個(gè) byte 數(shù)組,這個(gè)數(shù)組中包含了請(qǐng)求中對(duì)應(yīng)part的數(shù)據(jù)(通過(guò) @RequestPart 指定)述暂。如果用戶提交表單的時(shí)候沒(méi)有選擇文件痹升,那么這個(gè)數(shù)組會(huì)是空(而不是null)。獲取到圖片數(shù)據(jù)后畦韭,processRegistration() 方法剩下的任務(wù)就是將文件保存到某個(gè)位置疼蛾。
接受MultipartFile
使用上傳文件的原始 byte 比較簡(jiǎn)單但是功能有限。因此艺配,Spring 還提供了 MultipartFile 接口察郁,它為處理 multipart 數(shù)據(jù)提供了內(nèi)容更為豐富的對(duì)象。如下的程序清單展現(xiàn)了 MultipartFile 接口的概況转唉。
程序清單7.5 Spring 所提供的 MultipartFile 接口皮钠,用來(lái)處理上傳的文件
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public interface MultipartFile {
String geName();
String getOriginalFilename();
String getContentType();
boolean isEmpty();
long getSize();
byte[] getByte() throws IOException;
InputStream getInputStream() throws IOException;
void transferTo(File dest) throws IOException;
}
我們可以看到,MultipartFile 提供了獲取上傳文件 byte 的方式赠法,但是它所提供的功能并不僅限于此麦轰,還能獲得原始的文件名、大小以及內(nèi)容類型砖织。它還提供了一個(gè) InputStream款侵,用來(lái)將文件數(shù)據(jù)以流的方式進(jìn)行讀取。
除此之外侧纯,MultipartFile 還提供了一個(gè)便利的 transferTo() 方法新锈,它能夠幫助我們將上傳的文件寫入到文件系統(tǒng)中。作為樣例眶熬,我們可以在 process-Registration() 方法中添加如下的幾行代碼妹笆,從而將上傳的圖片文件寫入到文件系統(tǒng)中:
profilePicture.transferTo(new File("/data/spittr/" + profilePicture.getOriginalFilename()));
以 Part 的形式接受上傳的文件
如果你需要將應(yīng)用部署到 Servlet 3.0 的容器中,那么會(huì)有 MultipartFile 的一個(gè)替代方案聋涨。Spring MVC 也能接受 javax.servlet.http.Part 作為控制器方法的參數(shù)晾浴。如果使用 Part 來(lái)替換 MultipartFile 的話,那么 processRegistration() 的方法簽名將會(huì)變成如下的形式:
@RequestMapping(value="/register",method=POST)
public String processRegistration(
@RequestPart("profilePicture") Part profilePicture,
@Valid Spitter spitter,
Errors errors){
}
Part接口:Spring MultipartFile的替代方案:
package javax.servlet.http;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
public interface Part {
InputStream getInputStream() throws IOException;
String getContentType();
String getName();
long getSize();
void write(String var1) throws IOException;
void delete() throws IOException;
String getHeader(String var1);
Collection<String> getHeaders(String var1);
Collection<String> getHeaderNames();
}