SpringMVC-视图解析器ViewResolver详解

本文基于spring 5.5.2.release

前几篇文章介绍了springmvc调用Controller的处理流程,现在已经知道调用Controller之后可以得到ModelAndView对象,那么如何根据ModelAndView对象得到View对象,这就需要借助视图解析器ViewResolver了。

一、ViewResolver

ViewResolver是一个接口,下面是它的定义:

public interface ViewResolver {
	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;
}

该接口只有一个方法resolveViewName,入参是视图名和语言环境对象,该方法根据视图名和语言环境找到View对象。
springmvc提供了很多实现类:
在这里插入图片描述
默认情况下,DispatcherServlet使用了其中四个:InternalResourceViewResolver、BeanNameViewResolver、ContentNegotiatingViewResolver、ViewResolverComposite。

1、BeanNameViewResolver

该视图解析器认为resolveViewName()方法的入参viewName是容器中某个bean的名字,且该bean是View对象,因此BeanNameViewResolver直接从容器中获取该bean然后返回。

	public View resolveViewName(String viewName, Locale locale) throws BeansException {
		ApplicationContext context = obtainApplicationContext();
		if (!context.containsBean(viewName)) {
			// 如果容器中没有该bean,返回null
			return null;
		}
		//检查名字和类型是否匹配
		if (!context.isTypeMatch(viewName, View.class)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Found bean named '" + viewName + "' but it does not implement View");
			}
			return null;
		}
		//根据类型和名字从容器中获取View对象
		return context.getBean(viewName, View.class);
	}

2、ViewResolverComposite

从名字上能看出来,该类是ViewResolver的组合类,它有一个List属性持有了其他视图解析器:

private final List<ViewResolver> viewResolvers = new ArrayList<>();

该类的resolveViewName方法也是遍历viewResolvers中的各个解析器,从这些解析器中找到View对象。

	public View resolveViewName(String viewName, Locale locale) throws Exception {
		//遍历解析器
		for (ViewResolver viewResolver : this.viewResolvers) {
			View view = viewResolver.resolveViewName(viewName, locale);
			if (view != null) {
				return view;
			}
		}
		return null;
	}

通过查看创建ViewResolverComposite对象的代码,viewResolvers中的元素可以为下面三种情况:

  1. 可以为空;
  2. 持有InternalResourceViewResolver对象;
  3. 持有ContentNegotiatingResolver对象。

3、InternalResourceViewResolver

InternalResourceViewResolver是一个与内部资源有关的视图解析器,这里的内部资源指的是在/WEB-INF目录下的JSP、servlet或者HTML文件。InternalResourceViewResolver将viewName作为文件名,添加上前缀路径和文件名后缀得到完整的文件路径,之后视图渲染的时候就根据该路径查找资源文件。前缀路径和文件名后缀可以可以使用spring.mvc.view.prefix和spring.mvc.view.suffix两个配置指定。
该类内部有一个缓存,已经解析过的视图会被缓存起来,下次访问相同的视图名时,可以直接从缓存中取,默认最多缓存1024个视图。如果缓存中不存在,那么需要调用父类UrlBasedViewResolver.buildView()方法创建一个视图。下面来看一下如何创建视图对象。

	protected AbstractUrlBasedView buildView(String viewName) throws Exception {
		//viewClass是视图类的Class对象,
		//可以是InternalResourceView.class或者JstlView.class。
		Class<?> viewClass = getViewClass();
		Assert.state(viewClass != null, "No view class");
		//根据viewClass创建视图对象
		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;
	}

创建视图对象的过程并不复杂,不过有一点需要注意,创建视图对象的时候,没有检查该资源文件是否存在。springmvc是在渲染View的时候才去检查文件是否存在。
我们还可以设置父类UrlBasedViewResolver中的属性viewNames,指定该视图解析器只处理固定的一些视图名,不过该属性值不能在配置文件中设置,只能通过编程方式。

4、ContentNegotiatingViewResolver

该视图解析器和MediaType有关,相对于前面三个来说要复杂一些。
该视图解析器查找视图对象主要分为两步:

  1. 将请求报文的accept字段值转换为对应的MediaType对象集合,然后找到每个MediaType对应的资源文件名后缀,比如application/json对应.json后缀,之后遍历除ContentNegotiatingViewResolver之外的容器中的其他视图解析器,对每个视图解析器首先根据视图名查找视图对象,之后将视图名与上面找到的每个文件后缀组合成新的视图名,再根据新视图名查找视图对象,将找到的每个视图对象都放到一个集合中;
  2. 从上一步的视图对象集合中找到一个最优的视图对象,这个过程很简单,如果请求报文的MediaType可以兼容响应报文的content-type,那么找到的第一个视图对象就是最优的。

下面来看一下源代码实现。因为篇幅,下面的代码有删减。

	public View resolveViewName(String viewName, Locale locale) throws Exception {
		RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
		//根据请求对象找到客户端可以接受的所有的MediaType类型,
		//这些MediaType是根据请求报文头的accept字段得到的
		List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
		if (requestedMediaTypes != null) {
			//根据视图名和MediaType查找视图对象,下面介绍该方法内容
			List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
			//对视图对象排序,找到最优的,下面介绍该方法内容
			View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
			if (bestView != null) {
				return bestView;
			}
		}
		if (this.useNotAcceptableStatusCode) {
			return NOT_ACCEPTABLE_VIEW;
		}
		else {
			return null;
		}
	}
	private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
			throws Exception {
		List<View> candidateViews = new ArrayList<>();
		//遍历容器中除了ContentNegotiatingViewResolver之外的其他视图解析器
		if (this.viewResolvers != null) {
			for (ViewResolver viewResolver : this.viewResolvers) {
				//首先根据视图名使用视图解析器查找
				View view = viewResolver.resolveViewName(viewName, locale);
				if (view != null) {
					candidateViews.add(view);
				}
				//遍历MediaType
				for (MediaType requestedMediaType : requestedMediaTypes) {
					//因为每个MediaType对应了一些文件后缀,比如application/json对应.json后缀,
					//下面这个方法便是根据MediaType查找文件后缀的,
					//springmvc提供了接口MediaTypeFileExtensionResolver,用来查找上述对应关系
					List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(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;
	}
	//如果找到多个视图,使用下面的方法找到最优的视图对象
	private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
		//优先选择重定向的视图
		for (View candidateView : candidateViews) {
			if (candidateView instanceof SmartView) {
				SmartView smartView = (SmartView) candidateView;
				if (smartView.isRedirectView()) {
					return candidateView;
				}
			}
		}
		//每个视图对象都有一个属性contentType,这个属性对应了响应报文的content-type字段
		//如果请求报文中的MediaType兼容视图对象的contentType,那么第一个遍历到的视图对象就是最优的
		for (MediaType mediaType : requestedMediaTypes) {
			for (View candidateView : candidateViews) {
				if (StringUtils.hasText(candidateView.getContentType())) {
					MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
					if (mediaType.isCompatibleWith(candidateContentType)) {
						attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
						return candidateView;
					}
				}
			}
		}
		return null;
	}

二、DispatcherServlet如何使用ViewResolver

下面来看一下DispatcherServlet是如何使用ViewResolver的。

1、生成默认视图名

调用Controller之后会得到一个ModelAndView对象,但是该ModelAndView对象有可能没有设置视图名字或者View对象,比如Controller方法的返回类型是void,此时ModelAndView对象的view字段为null,如果不设置该值,视图解析器是无法正常工作的。因此在调用视图解析器之前,如果发现ModelAndView的view字段为null,DispatcherServlet会调用applyDefaultViewName()方法生成一个默认的视图名:

	private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
		if (mv != null && !mv.hasView()) {
			String defaultViewName = getDefaultViewName(request);
			if (defaultViewName != null) {
				mv.setViewName(defaultViewName);
			}
		}
	}
	protected String getDefaultViewName(HttpServletRequest request) throws Exception {
		//调用viewNameTranslator得到默认视图名,
		//属性viewNameTranslator是RequestToViewNameTranslator的对象
		return (this.viewNameTranslator != null ? this.viewNameTranslator.getViewName(request) : null);
	}

RequestToViewNameTranslator.getViewName()根据请求对象创建视图名。RequestToViewNameTranslator是一个接口,springmvc提供了一个默认实现类DefaultRequestToViewNameTranslator,该类生成默认视图名的方式很简单:

	public String getViewName(HttpServletRequest request) {
		//lookupPath是请求路径,比如http://localhost:8111/test,lookupPath=/test
		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH);
		//前缀+lookupPath+后缀作为默认视图名,默认prefix和suffix都是空字符串
		return (this.prefix + transformPath(lookupPath) + this.suffix);
	}

如果不想使用默认的RequestToViewNameTranslator实现类,我们可以创建自定义的实现类,然后指定bean对象名字为“viewNameTranslator”,这样springmvc就会使用自定义的bean对象替换默认的。
调用完applyDefaultViewName()方法之后,接下来会调用拦截器,拦截器执行完毕后,调用DispatcherServlet.render()方法开始处理视图。

2、调用视图解析器

DispatcherServlet在render()方法里面调用视图解析器查找View对象。它会遍历每个视图解析器,不过如果ModelAndView对象里面已经指定了View对象,那么DispatcherServlet会直接使用该View对象,不会再通过解析器查找了。
下面的代码有删减。

	protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		//获取请求对象中的语言环境对象
		Locale locale =
				(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
		response.setLocale(locale);

		View view;
		//得到视图名
		String viewName = mv.getViewName();
		//如果viewName为null,DispatcherServlet认为ModelAndView对象中已经设置了View对象,
		//如果不为null,那么使用视图解析器查找viewName对应的View对象
		if (viewName != null) {
			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 {
			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() + "'");
			}
		}
		try {
			if (mv.getStatus() != null) {
				response.setStatus(mv.getStatus().value());
			}
			//渲染视图,渲染是指将页面中的一些变量设置上值,或者说是将一些数据按照视图要求进行组织,
			//这些数据或者值来自于模型上下文,
			//视图渲染完后,就会写入response对象返回给客户端
			view.render(mv.getModelInternal(), request, response);
		}
		catch (Exception ex) {
			throw ex;
		}
	}
	//遍历每个视图解析器,根据视图名和语言环境对象查找视图对象
	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;
	}

DispatcherServlet渲染完视图后,就会将视图返回给客户端,这一步是在view.render()方法中处理的,这样便完成了一次http请求的处理。

三、总结

本文首先介绍了ViewResolver接口,之后介绍了四个默认使用的视图解析器,如果程序中还需要其他的视图解析器,那么我们可以将自定义的视图解析器对象注册到spring容器中,这样DispatcherServlet初始化的时候就可以找到该自定义的解析器。
第二小节介绍了DispatcherServlet遍历ViewResolver查找View对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值