Spring MVC 视图解析

SpringMVC解析视图概述

  • 不论控制器返回一个StringModelAndViewView都会转换为ModelAndView对象,由视图解析器解析视图,然后,进行页面的跳转在这里插入图片描述
  • 视图解析两个重要的接口(ViewViewResolver
    • view:视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户
    public interface View {
    	String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
    	String PATH_VARIABLES = View.class.getName() + ".pathVariables";
    	String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
    	// 如果预先确定,则返回视图的内容类型。可用于预先检查视图的内容类型,即在实际渲染尝试之前。
    	default String getContentType() {
    		return null;
    	}
    	// 根据指定的模型渲染视图
    	void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
    			throws Exception;
    
    }
    
    • ViewResolver:Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是 JSP ,也可能是 Excel、JFreeChart等各种表现形式的视图
    public interface ViewResolver {
    	// 按名称解析给定视图
    	@Nullable
    	View resolveViewName(String viewName, Locale locale) throws Exception;	
    }
    

视图常用实现类

在这里插入图片描述

视图解析器

在这里插入图片描述

  • SpringMVC 为逻辑视图名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。
  • 每个视图解析器都实现了 Ordered 接口并开放出一个 order 属性,可以通过 order 属性指定解析器的优先顺序,order 越小优先级越高。

JstlView

  • 若项目中使用了JSTL,则SpringMVC 会自动把视图由InternalResourceView转为 JstlView

代码如下

大致过程与Java Web原生的国际化过程相同i18n 国际化

  • 导包
    <!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>
    
  • 在resources文件夹中,放入i18n的配置文件
  • 在spring-mvc的配置文件中,注册messageSource
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n"/>
        <!-- 支持UTF-8的中文 -->
        <property name="defaultEncoding" value="UTF-8"/>
    </bean>
    
  • 编写一个控制器
      @RequestMapping("/viewAndViewResolverTest")
      public String viewAndViewResolverTest() {
        System.out.println("viewAndViewResolverTest");
        return "success";
      }
    
  • 与Java Web不同的是,在jsp中无需声明地区以及基础名称basename,可以直接通过message调用,获取配置文件的内容。
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    <html>
    <head>
        <title>success</title>
    </head>
    <body>
    <h1>
        <fmt:message key="username"/>
    </h1>
    <h1>
        <fmt:message key="password"/>
    </h1>
    </body>
    </html>
    

mvc:view-controller标签

  • 若希望直接响应通过 SpringMVC 渲染的页面,可以使用 mvc:view-controller 标签实现(即省去写Controller的代码,直接调用Spring MVC内部的Controller代码)
  • 在spring-mvc的配置文件中,加入以下代码(页面为success.jsp,url为/success
    <!-- 直接配置响应的页面:无需经过控制器来执行结果 -->
    <mvc:view-controller path="/success" view-name="success"/>
    <mvc:annotation-driven/>
    

    注意,必须添加<mvc:annotation-driven/>这句话,否则除了该页面以外,所有页面都会失效。

自定义视图

  • 自定义视图(需要加入SpringMVC,那么,一定需要实现框架的接口)

简单案例

  • 自己写一个view,发送请求之后返回该view
  • 首先,定义view
    @Component
    public class HelloView implements View {
    // 返回内容类型
      @Override
      public String getContentType() {
        return "text/html;charset=utf-8";
      }
    
      @Override
      public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setContentType("text/html;charset=utf-8");  // 必须设置,否则会出现中文乱码现象
        response.getWriter().write("自定义view测试");  // 自定义页面内容
      }
    }
    

    即使存在过滤器CharacterEncodingFilter,仍要在响应中设置该返回类型。因为在过滤器中,只设置了编码格式,没有设置响应类型。详见Spring MVC解决中文乱码问题

  • 在sring-mvc配置文件中,设置BeanNameViewResolver视图解析器。并将优先级设为100(其实多少数字都可以,只要比internalResourceViewResolver的oder小就行)。
    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
        <property name="order" value="100"/>
    </bean>
    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    
    InternalResourceViewResolver的父类中,将order设置为private int order = Ordered.LOWEST_PRECEDENCE;int LOWEST_PRECEDENCE = 2147483647;
  • 控制器
    @RequestMapping("/testView")
    public String testView(){
    	System.out.println("testView...");
    	return "helloView"; //与视图Bean对象的id一致
    }
    

重定向

  • 之前都是返回一个字符串,然后视图解析器通过拼接,获取真实资源的位置,并转发到此处。
  • 我们也可以让视图解析器解析为重定向的方式。
    @RequestMapping("redirectTest")
      public String redirectTest() {
        System.out.println("redirectTest");
        return "redirect:/index.jsp";
      }
    

    使用此方法之后,不会视图解析器不会使用拼接的,而是直接根据url寻找资源。

转发

  • 同理,也可以自己写一个转发的地址,不需要视图解析器帮忙拼接。
    @RequestMapping("forwardTest")
      public String forwardTest() {
        System.out.println("forwardTest");
        return "forward:/index.jsp";
      }
    

源码分析

  • DispatcherServlet中,doDispatch()方法,在接近末尾的地方,有一个processDispatchResult()方法;
  • processDispatchResult()方法调用render()方法渲染;
  • render()方法中,调用resolveViewName()通过View的名字,解析出View对象;并在末尾,调用Viewrend()方法,开启重定向或转发
    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 != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
    	response.setLocale(locale);
    
    	View view;
    	String viewName = mv.getViewName();
    	if (viewName != null) {
    		// 通过viewname解析出view
    		view = resolveViewName(viewName, 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.isTraceEnabled()) {
    		logger.trace("Rendering view [" + view + "] ");
    	}
    	try {
    		if (mv.getStatus() != null) {
    			response.setStatus(mv.getStatus().value());
    		}
    		// 执行view
    		view.render(mv.getModelInternal(), request, response);
    	}
    	catch (Exception ex) {
    		if (logger.isDebugEnabled()) {
    			logger.debug("Error rendering view [" + view + "]", ex);
    		}
    		throw ex;
    	}
    }
    
  • resolveViewName()方法中,遍历所有视图解析器,尝试通过view的名字,解析出结果。一旦解析出来,就返回view
    @Nullable
    protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
    		Locale locale, HttpServletRequest request) throws Exception {
    
    	if (this.viewResolvers != null) {
    		// 遍历所有视图解析器
    		for (ViewResolver viewResolver : this.viewResolvers) {
    			View view = viewResolver.resolveViewName(viewName, locale);
    			if (view != null) {
    				return view;
    			}
    		}
    	}
    	return null;
    }
    

InternalResourceViewResolver

  • 继承关系
    在这里插入图片描述
  • 所有ViewResolver都需要实现resolveViewName()方法
AbstractCachingViewResolver
  • AbstractCachingViewResolver首先实现了这个方法,在此其中,最重要的是createView()方法,用于创造View;而该方法又调用一个loadView()的方法(该方法是一个抽象方法)。
    @Override
    @Nullable
    public View resolveViewName(String viewName, Locale locale) throws Exception {
    	if (!isCache()) {
    		// 创造View
    		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 = createView(viewName, locale);
    					if (view == null && this.cacheUnresolved) {
    						view = UNRESOLVED_VIEW;
    					}
    					if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
    						this.viewAccessCache.put(cacheKey, view);
    						this.viewCreationCache.put(cacheKey, view);
    					}
    				}
    			}
    		}
    		else {
    			if (logger.isTraceEnabled()) {
    				logger.trace(formatKey(cacheKey) + "served from cache");
    			}
    		}
    		return (view != UNRESOLVED_VIEW ? view : null);
    	}
    }
    
    @Nullable
    protected View createView(String viewName, Locale locale) throws Exception {
    	return loadView(viewName, locale);
    }
    
UrlBasedResolver
  • 子类UrlBasedResolver实现了loadView()方法,该方法又调用一个buildView()方法实现。
    @Override
    protected View loadView(String viewName, Locale locale) throws Exception {
    	AbstractUrlBasedView view = buildView(viewName);
    	View result = applyLifecycleMethods(viewName, view);
    	return (view.checkResource(locale) ? result : null);
    }
    
  • buildView()是最重要的实现方法。通过BeanUtils.instantiateClass()方法实例化一个view对象,通过各类getset方法,给view注入多个参数(基本上响应有什么,就注入什么)。
    protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    	Class<?> viewClass = getViewClass();
    	Assert.state(viewClass != null, "No view class");
    	// 实例化一个view
    	AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
    	view.setUrl(getPrefix() + viewName + getSuffix());
    	view.setAttributesMap(getAttributesMap());
    
    	String contentType = getContentType();
    	if (contentType != null) {
    		view.setContentType(contentType);
    	}
    
    	String requestContextAttribute = getRequestContextAttribute();
    	if (requestContextAttribute != null) {
    		view.setRequestContextAttribute(requestContextAttribute);
    	}
    
    	Boolean exposePathVariables = getExposePathVariables();
    	if (exposePathVariables != null) {
    		view.setExposePathVariables(exposePathVariables);
    	}
    	Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
    	if (exposeContextBeansAsAttributes != null) {
    		view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
    	}
    	String[] exposedContextBeanNames = getExposedContextBeanNames();
    	if (exposedContextBeanNames != null) {
    		view.setExposedContextBeanNames(exposedContextBeanNames);
    	}
    
    	return view;
    }
    
  • 值得一提的是,该类还重写了父类的createView()方法,加入了viewName的判断
    @Override
    protected View createView(String viewName, Locale locale) throws Exception {
    	// If this resolver is not supposed to handle the given view,
    	// return null to pass on to the next resolver in the chain.
    	if (!canHandle(viewName, locale)) {
    		return null;
    	}
    
    	// Check for special "redirect:" prefix. 判断是否以redirect开头
    	if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
    		String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
    		RedirectView view = new RedirectView(redirectUrl,
    				isRedirectContextRelative(), isRedirectHttp10Compatible());
    		String[] hosts = getRedirectHosts();
    		if (hosts != null) {
    			view.setHosts(hosts);
    		}
    		return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
    	}
    
    	// Check for special "forward:" prefix.判断是否以forward开头
    	if (viewName.startsWith(FORWARD_URL_PREFIX)) {
    		String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
    		InternalResourceView view = new InternalResourceView(forwardUrl);
    		return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
    	}
    
    	// Else fall back to superclass implementation: calling loadView.
    	return super.createView(viewName, locale);
    }
    
InternalResourceViewResolver
  • 在真正的ViewResolver中,就简单地调用父类的buildView()方法,即可实现功能。
    @Override
    protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    	InternalResourceView view = (InternalResourceView) super.buildView(viewName);  //调用父类的方法实现
    	if (this.alwaysInclude != null) {
    		view.setAlwaysInclude(this.alwaysInclude);
    	}
    	view.setPreventDispatchLoop(true);
    	return view;
    }
    

BeanNameViewResolver

  • 继承关系
    在这里插入图片描述
  • 该类相较于上面庞大的类来说,简单得多。基本上就是将整个view实例化一遍,然后还回去。
    public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
    
    	private int order = Ordered.LOWEST_PRECEDENCE;  // default: same as non-Ordered
    	/**
    	 * Specify the order value for this ViewResolver bean.
    	 * <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered.
    	 * @see org.springframework.core.Ordered#getOrder()
    	 */
    	public void setOrder(int order) {
    		this.order = order;
    	}
    
    	@Override
    	public int getOrder() {
    		return this.order;
    	}
    
    
    	@Override
    	@Nullable
    	public View resolveViewName(String viewName, Locale locale) throws BeansException {
    		ApplicationContext context = obtainApplicationContext();
    		if (!context.containsBean(viewName)) {
    			// Allow for ViewResolver chaining...
    			return null;
    		}
    		if (!context.isTypeMatch(viewName, View.class)) {
    			if (logger.isDebugEnabled()) {
    				logger.debug("Found bean named '" + viewName + "' but it does not implement View");
    			}
    			// Since we're looking into the general ApplicationContext here,
    			// let's accept this as a non-match and allow for chaining as well...
    			return null;
    		}
    		return context.getBean(viewName, View.class);
    	}
    
    }
    

InternalResourceView

  • 继承关系
    在这里插入图片描述
AbstractView
  • 该类首先实现了rend()方法,并调用了抽象方法renderMergedOutputModel()
    @Override
    public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
    		HttpServletResponse response) throws Exception {
    
    	if (logger.isDebugEnabled()) {
    		logger.debug("View " + formatViewName() +
    				", model " + (model != null ? model : Collections.emptyMap()) +
    				(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
    	}
    
    	Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
    	prepareResponse(request, response);
    	renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
    }
    
InternalResourceView
  • 该类就直接实现了renderMergedOutputModel()方法,并将大部分model的信息,都保存在request中。
  • 最后获取Dispatcher,并forward到指定的url中。
    @Override
    protected void renderMergedOutputModel(
    	Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    	// Expose the model object as request attributes.
    	exposeModelAsRequestAttributes(model, request);
    
    	// Expose helpers as request attributes, if any.
    	exposeHelpers(request);
    
    	// Determine the path for the request dispatcher.
    	String dispatcherPath = prepareForRendering(request, response);
    
    	// Obtain a RequestDispatcher for the target resource (typically a JSP).
    	RequestDispatcher rd = getRequestDispatcher(request, 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(request, response)) {
    		response.setContentType(getContentType());
    		if (logger.isDebugEnabled()) {
    			logger.debug("Including [" + getUrl() + "]");
    		}
    		rd.include(request, response);
    	}
    
    	else {
    		// Note: The forwarded resource is supposed to determine the content type itself.
    		if (logger.isDebugEnabled()) {
    			logger.debug("Forwarding to [" + getUrl() + "]");
    		}
    		rd.forward(request, response);
    	}
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值