本文基于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中的元素可以为下面三种情况:
- 可以为空;
- 持有InternalResourceViewResolver对象;
- 持有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有关,相对于前面三个来说要复杂一些。
该视图解析器查找视图对象主要分为两步:
- 将请求报文的accept字段值转换为对应的MediaType对象集合,然后找到每个MediaType对应的资源文件名后缀,比如application/json对应.json后缀,之后遍历除ContentNegotiatingViewResolver之外的容器中的其他视图解析器,对每个视图解析器首先根据视图名查找视图对象,之后将视图名与上面找到的每个文件后缀组合成新的视图名,再根据新视图名查找视图对象,将找到的每个视图对象都放到一个集合中;
- 从上一步的视图对象集合中找到一个最优的视图对象,这个过程很简单,如果请求报文的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对象。