視圖和視圖解析器
-
請求處理方法執行完成后,最終返回一個 ModelAndView 對象。對於那些返回 String,View 或 ModeMap 等類型的處理方法,Spring MVC 也會在內部將它們裝配成一個 ModelAndView 對象,它包含了邏輯名和模型對象的視圖
-
Spring MVC 借助視圖解析器(ViewResolver)得到最終的視圖對象(View),最終的視圖可以是 JSP ,也可能是 Excel、JFreeChart等各種表現形式的視圖
-
對於最終究竟采取何種視圖對象對模型數據進行渲染,處理器並不關心,處理器工作重點聚焦在生產模型數據的工作上,從而實現 MVC 的充分解耦
視圖
視圖的作用是渲染模型數據,將模型里的數據以某種形式呈現給客戶。為了實現視圖模型和具體實現技術的解耦,Spring 在 org.springframework.web.servlet 包中定義了一個高度抽象的 View 接口:
視圖對象由視圖解析器負責實例化。由於視圖是無狀態的,所以他們不會有線程安全的問題
常用的視圖實現類

視圖解析器
SpringMVC 為邏輯視圖名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一種或多種解析策略,並指定他們之間的先后順序。每一種映射策略對應一個具體的視圖解析器實現類。
視圖解析器的作用比較單一:將邏輯視圖解析為一個具體的視圖對象。
所有的視圖解析器都必須實現 ViewResolver 接口:
程序員可以選擇一種視圖解析器或混用多種視圖解析器。每個視圖解析器都實現了 Ordered 接口並開放出一個 order 屬性,可以通過 order 屬性指定解析器的優先順序,order 越小優先級越高。 SpringMVC 會按視圖解析器順序的優先順序對邏輯視圖名進行解析,直到解析成功並返回視圖對象,否則將拋出 ServletException 異常。
JSP 是最常見的視圖技術,可以使用 InternalResourceViewResolve作為視圖解析器:
實驗代碼
請求轉發
@RequestMapping("/hello1")
public String hello1(){
return "../../hello";
}
@RequestMapping("/hello2")
public String hello2(){
return "forward:/hello.jsp";
}
@RequestMapping("/hello3")
public String hello3(){
return "forward:/hello2";
}
@RequestMapping("/hello4")
public String hello4(){
return "redirect:/index.jsp";
}
都可以跳到index.jsp
源碼斷點
1.任何的方法都會進入到doDispatch(request, response);中
2.在它的方法中mv = ha.handle(processedRequest, response, mappedHandler.getHandler());,有處理器適配器調用生成ModelAndView
3.調用handle方法,是由子類AnnotationMethodHandlerAdapter實現的,里面會調用invokeHandlerMethod(request, response, handler)方法。
最終會返回ModelAndView對象,view就是要跳轉的頁面,model里面就是數據。
4.方法繼續放下走到processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
//要進行渲染
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
5.render(mv, request, response);
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale = this.localeResolver.resolveLocale(request);
response.setLocale(locale);
View view;
if (mv.isReference()) {
// We need to resolve the view name.
view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException(
"Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" +
getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
try {
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '"
+ getServletName() + "'", ex);
}
throw ex;
}
}
其中有一個重要的方法resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);,解析試圖名,點進去看看。
試圖解析器根據方法的返回值,得到一個view對象。要是不為空,說明能解析。
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
// Ask the subclass to create the View object.
//根據方法的返回值創建view對象
view = createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
if (logger.isTraceEnabled()) {
logger.trace("Cached view [" + cacheKey + "]");
}
}
}
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
view = createView(viewName, locale);這個方法,是真正的創建試圖。
創建完成,會有一個view
視圖解析器得到View對象的流程就是,所有配置的視圖解析器都來嘗試根據視圖名(返回值)得到View(試圖)對象;如果能得到就返回,得不到就換下一個試圖解析器。
6.view進行渲染。
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine which request handle to expose to the RequestDispatcher.
HttpServletRequest requestToExpose = getRequestToExpose(request);
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, requestToExpose);
// Expose helpers as request attributes, if any.
exposeHelpers(requestToExpose);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(requestToExpose, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(requestToExpose, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(requestToExpose, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.forward(requestToExpose, response);
}
}
視圖解析器只是為了得到試圖對象;視圖對象才能真正的轉發(將模型數據全部放在請求域中)或者重定向到頁面,視圖對象才能渲染試圖。
拓展
<!-- 直接配置響應的頁面:無需經過控制器來執行結果 -->
<mvc:view-controller path="/success" view-name="success"/>
但是請求別的接口的時候,都報錯404。
配置mvc:view-controller會導致其他請求路徑失效
解決辦法:
<mvc:annotation-driven/>
自定義視圖和視圖解析器
/**
* @author WGR
* @create 2021/4/8 -- 0:06
*/
@Component
public class HelloView implements View {
@Override
public String getContentType() {
return "text/html";
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
System.out.println(model);
response.setContentType("text/html");
response.getWriter().println("HelloView - time = " + new Date());
response.getWriter().println(model.get("name"));
}
}
/**
* @author WGR
* @create 2021/4/8 -- 0:04
*/
@Component
public class MyViewResolver implements ViewResolver, Ordered {
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
if(viewName.startsWith("wgr")){
return new HelloView();
}else {
return null;
}
}
@Override
public int getOrder() {
return 1;
}
}
@RequestMapping("/testView")
public String testView(Model model){
System.out.println("testView...");
model.addAttribute("name","dalianpai");
return "wgr:/helloView"; //與視圖Bean 對象的id一致
}