請求處理流程六階段:
DispatcherServlet 攔截請求。ModelAndView(模型數據 + 視圖名)。ViewResolver 解析視圖名為 View 對象。

注意:InternalResourceViewResolver 即使找不到文件也會返回 View(非 null),導致後續解析器不被調用,**必須設置最低優先級(order 最大)**。
需求:統一入口,根據副檔名分派不同解析器,避免 Controller 耦合視圖技術。
繼承 AbstractUrlBasedView,直接將文件內容寫入 Response,設置 Content-Type。
public class GenericFileView extends AbstractUrlBasedView {
private static final String DEFAULT_CONTENT_TYPE = "text/plain";
private String responseContent;
public GenericFileView() { setContentType(DEFAULT_CONTENT_TYPE); }
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
response.getWriter().write(responseContent);
response.getWriter().close();
}
public void setResponseContent(String content) { this.responseContent = content; }
}
適用於瀏覽器可直接渲染的靜態資源(文本、SWF、圖片等),**不支持 JSP 等需編譯的動態視圖**。
繼承 AbstractCachingViewResolver,實現 Ordered 接口控制優先級。
public class GenericFileViewResolver extends AbstractCachingViewResolver implements Ordered {
private String location; // 文件根目錄,如 /WEB-INF/swf/
private String viewName; // Spring 容器中 GenericFileView Bean 名稱
private int order = 0;
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
if (location == null) throw new Exception("No location specified");
String requestedPath = location + viewName;
Resource resource = getApplicationContext().getResource(requestedPath);
if (!resource.exists()) return null; // 關鍵:找不到返回 null,讓後續解析器處理
GenericFileView view = getApplicationContext().getBean(this.viewName, GenericFileView.class);
view.setUrl(requestedPath);
view.setResponseContent(inputStreamToString(resource.getInputStream(), "ISO-8859-1"));
return view;
}
// getter/setter/order 實現...
}
根據副檔名路由到對應解析器,類似 ContentNegotiatingViewResolver 但更輕量:命中即返回,不遍歷所有解析器。
<bean id="viewResolver" class="com.sample.web.viewresolver.MultipleViewResolver" p:order="0">
<property name="resolvers">
<map>
<entry key="config">
<bean class="com.sample.web.viewresolver.GenericFileViewResolver"
p:location="/WEB-INF/config/" p:cache="false">
<property name="viewName" value="configFileView"/>
</bean>
</entry>
<entry key="swf">
<bean class="com.sample.web.viewresolver.GenericFileViewResolver"
p:location="/WEB-INF/swf/" p:cache="false">
<property name="viewName" value="swfFileView"/>
</bean>
</entry>
</map>
</property>
</bean>
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
String ext = StringUtils.getFilenameExtension(viewName);
if (ext == null) return null; // 無副檔名交給下一個
ViewResolver resolver = resolvers.get(ext);
return (resolver == null) ? null : resolver.resolveViewName(viewName, locale);
}
@Controller
public class SampleController {
@RequestMapping("/jsp/sample") public String getSampleJsp() { return "SampleJsp"; }
@RequestMapping("/config/sample.config") public String getSampleConfig() { return "SampleConfig.config"; }
@RequestMapping("/swf/sample.swf") public String getSampleSwf() { return "SampleSwf.swf"; }
}
<!-- JSP 解析器,order=1 低於復合解析器 -->
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:order="1">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- 視圖 Bean 原型作用域,每次請求新實例 -->
<bean id="configFileView" class="com.sample.web.view.GenericFileView"
p:contentType="text/plain" p:url="" scope="prototype"/>
<bean id="swfFileView" class="com.sample.web.view.GenericFileView"
p:contentType="application/x-shockwave-flash" p:url="" scope="prototype"/>
| 請求 URL | 處理鏈路 | 響應效果 |
|---|---|---|
| /swf/sample.swf | MultipleViewResolver → swf 解析器 → swfFileView | 瀏覽器播放 Flash ![]() |
| /config/sample.config | MultipleViewResolver → config 解析器 → configFileView | 瀏覽器顯示純文本 ![]() |
| /jsp/sample | MultipleViewResolver 返回 null → jspViewResolver → InternalResourceView | 渲染 JSP 頁面 ![]() |
@RequestMapping("/path/{fileName}") 支持任意文件名。ContentNegotiatingViewResolver 支持 Accept Header 協商。通過 通用視圖類 + 按副檔名分派的復合解析器,實現了: