2012-10-31

近看 Spring MVC 3.1.2.RELEASE 的 ContentNegotiatingViewResolver

誕生自 3.0 的 ContentNegotiatingViewResolver 從字義不太容易知道在做什麼或怎麼用,那就從 Source code 好好來研究一下。

每個 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 物件。

---
---
---

沒有留言:

張貼留言