每個 ViewResolver 的重點就是 resolveViewName()。
public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.isInstanceOf(ServletRequestAttributes.class, attrs); List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); if (requestedMediaTypes != null) { List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); View bestView = getBestView(candidateViews, requestedMediaTypes); if (bestView != null) { return bestView; } } if (this.useNotAcceptableStatusCode) { // ... return NOT_ACCEPTABLE_VIEW; } else { logger.debug("No acceptable view found; returning null"); return null; } }得到三個線索,分別為 getMediaTypes()、getCandidateViews() 與 getBestView()。
除此之後,可以知道如果找不到合適的 View 物件,有兩條路可以走,如果將 useNotAcceptableStatusCode 設為 true(預設為 false),那就會丟出 406 Not Acceptable 錯誤,如果設為 false,那就回傳 null,表示交由下一個 View Resolver 處理,這就是 View Resolver Chaining。
getBestView()
getBestView() 是 private 而且沒有加註解,所以在 JavaDoc 裡看不到。
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes) { for (View candidateView : candidateViews) { if (candidateView instanceof SmartView) { SmartView smartView = (SmartView) candidateView; if (smartView.isRedirectView()) { // ... return candidateView; } } } for (MediaType mediaType : requestedMediaTypes) { for (View candidateView : candidateViews) { if (StringUtils.hasText(candidateView.getContentType())) { MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType()); if (mediaType.includes(candidateContentType)) { // ... return candidateView; } } } } return null; }內容分成兩部份,第一部份在判斷是否為 RedirectView(SmartView 唯一的實作),是的話就直接送走,不用往下走了,第二部份才是重點。
ContentNegotiatingViewResolver 從字義直翻是「內容談判」,不懂,那叫做「內容協調」,還是不懂,那叫做「依據 request 決定 response」,嗯,廢話,,哪個 request 不是這樣!
不完全是,看例子。
http://.../hello.html http://.../hello.pdf http://.../hello.xml看似三個 url,需要三個 requestMapping 處理,但其實產出給 View 的內容是一樣的,那可不可以用同一個 requestMapping 處理,然後依據「某些訊息」回傳不同格式的 View 物件呢?這就是 ContentNegotiatingViewResolver 要做的事情。
ContentNegotiatingViewResolver 右手拿著 request 想要的格式,左手握著可能的 View 物件(怎麼來的?稍後講),交叉比對,若有符合就回傳,舉例來說,request 想要 PDF,就去 View 裡找有沒有提供 PDF 格式的 View 物件,若有則回傳,若沒有呢?再看 request 裡有沒有第二志願、第三志願,如果都沒有,那就爆了,這就是 getBestView() 第二部份在做的事情。
從 getBestView() 往前追蹤,傳進來的兩個參數分別為 List<View> candidateViews 與 List<MediaType> requestedMediaTypes,先看第二個參數,也就是 ContentNegotiatingViewResolver 右手拿的東西,也是前面提到的「依據『某些訊息』回傳不同格式的 View 物件」的訊息來源。
這就可以追蹤到 getMediaType() 這個 method。
getMediaType()
protected List<MediaType> getMediaTypes(HttpServletRequest request) { if (this.favorPathExtension) { String requestUri = urlPathHelper.getLookupPathForRequest(request); String filename = WebUtils.extractFullFilenameFromUrlPath(requestUri); MediaType mediaType = getMediaTypeFromFilename(filename); if (mediaType != null) { // ... return Collections.singletonList(mediaType); } } if (this.favorParameter) { if (request.getParameter(this.parameterName) != null) { String parameterValue = request.getParameter(this.parameterName); MediaType mediaType = getMediaTypeFromParameter(parameterValue); if (mediaType != null) { // ... return Collections.singletonList(mediaType); } } } if (!this.ignoreAcceptHeader) { String acceptHeader = request.getHeader(ACCEPT_HEADER); if (StringUtils.hasText(acceptHeader)) { // ... } } if (this.defaultContentType != null) { // ... return Collections.singletonList(this.defaultContentType); } else { return Collections.emptyList(); } }getMediaType() 有四種選項,依序為副檔名、request 參數、Accept Header 與預設值。
副檔名
預設開啟(favorPathExtension),也是建議使用的方式,因為副檔名可以讓瀏覽器知道回傳的檔案類型,而使用正確的應用程式來開啟。
http://.../hello.html http://.../hello.pdf http://.../hello.xml
request 參數
預設關閉(favorParameter),需搭配 parameterName(預設為 format)使用。
http://.../hello?format=html http://.../hello?format=pdf http://.../hello?format=xml
這邊有個奇怪的地方,ContentNegotiatingViewResolver 有個 mediaTypes 屬性,是用來設定 ContentNegotiatingViewResolver 需要處理的副檔名,奇怪的地方在於前一項「副檔名」不需要 mediaTypes,而這一項「request 參數」一定需要。
protected MediaType getMediaTypeFromFilename(String filename) { String extension = StringUtils.getFilenameExtension(filename); if (!StringUtils.hasText(extension)) { return null; } extension = extension.toLowerCase(Locale.ENGLISH); MediaType mediaType = this.mediaTypes.get(extension); if (mediaType == null) { String mimeType = getServletContext().getMimeType(filename); if (StringUtils.hasText(mimeType)) { mediaType = MediaType.parseMediaType(mimeType); } // ... if (mediaType != null) { this.mediaTypes.putIfAbsent(extension, mediaType); } } return mediaType; }從副檔名取 MediaType 時,如果不存在 mediaTypes 屬性定義範圍內,則會去 ServletContext 裡找,這邊一般的檔案都應該找得到,找到之後,甚至回存到 mediaTypes 裡。
protected MediaType getMediaTypeFromParameter(String parameterValue) { return this.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH)); }但是從 request 參數裡找 MediaType 時,則完全不管找不到的狀況。
換句話說,也就是說,使用 request 參數指定副檔名時,一定要將這個副檔名也定義在 mediaTypes 屬性裡。
唯一我能想到的合理解釋就是,使用 request 參數不一定要用「真實的副檔名」,也許可以用暗號,然後在 mediaTypes 裡再加上暗號與真實 MediaType 的對照。
Accept Header
預設開啟(ignoreAcceptHeader),但無法從網址中指定,所以常用在由程式送出 request 這類的 Web service。
預設值
由 defaultContentType 設定。
getCandidateViews()
看完 ContentNegotiatingViewResolver 右手的 MediaType,來看看左手的 View 物件,View 物件有兩個來源,分別為其他的 ViewResolver 與預設值。
其他的 ViewResolver?神奇吧,ContentNegotiatingViewResolver 除了預設的 View 物件,也就是由 defaultViews 定義的 View 物件以外,本身不事生產,而是向其他的 ViewResolver 要 View 物件。
ContentNegotiatingViewResolver 預設是由 Spring 找出所有其他的 ViewResolver 來建立 View 物件,但是也可以透過 viewResolvers 屬性明確指定使用哪些 ViewResolver。
也因為這個特性,ContentNegotiatingViewResolver 的 order 屬性必須有相當高的優先權,至少得在被用到的 ViewResolver 之前,不然就發揮不了作用了,預設值為 Ordered.HIGHEST_PRECEDENCE,也就是最小值 Integer.MIN_VALUE。
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception { List<View> candidateViews = new ArrayList<View>(); for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { candidateViews.add(view); } for (MediaType requestedMediaType : requestedMediaTypes) { List<String> extensions = getExtensionsForMediaType(requestedMediaType); for (String extension : extensions) { String viewNameWithExtension = viewName + "." + extension; view = viewResolver.resolveViewName(viewNameWithExtension, locale); if (view != null) { candidateViews.add(view); } } } } if (!CollectionUtils.isEmpty(this.defaultViews)) { candidateViews.addAll(this.defaultViews); } return candidateViews; }先要到所有的 ViewResolver,當然本身除外,然後用 viewName 去找,找到後再用 MediaType 去過濾,都符合的就取得 View 物件候選資格,全部的 ViewResolver 看過之後,再加入由 defaultViews 屬性定義的 View 物件(這屬性好用嗎?),就是左手的 View 物件。
---
---
---
沒有留言:
張貼留言