记一次ViewResolver引起的问题 javax.servlet.ServletException: Could not resolve view with name

记一次ViewResolver引起的问题

文章目录

  • 记一次ViewResolver引起的问题
    • 问题背景
    • 解决过程
    • 查找原因
      • 其他的解决方案
    • 源码分析
      • Spring初始化过程
      • ViewResolver的初始化过程
      • InternalResourceViewResolver的视图解析过程
    • 知识盘点
    • 遗留问题

问题背景

公司的项目基于SpringBoot开发,基本上所有接口都是Restful风格的,接收json参数,返回json数据。一般控制类上直接用@RestController或者@Controller+@ResponseBody来将对象序列化后返回给前端。
项目中还有少数几个接口是通过redirect:xxx的方式进行重定向的,其中包括微信登录授权。这些接口需要用到Spring的ViewResolver实现类进行视图解析,得到viewName对应的View对象,然后由View对象进行视图的渲染(render)。

解决过程

  1. 早上10:30左右,测试人员反映微信公众号的登录页面加载错误,出现空白错误页面,页面显示Whitelabel Error Page
    记一次ViewResolver引起的问题 javax.servlet.ServletException: Could not resolve view with name_第1张图片

  2. 查看服务器日志,显示javax.servlet.ServletException: Could not resolve view with name 'redirect:https:

  3. 搜索该错误信息,查询到一堆博客等资料,都未能解决;

  4. 开始排查代码提交记录,发现pom.xml中删除了springcloud相关的5个依赖,添加依赖后,服务恢复正常。

查找原因

  1. 服务恢复后,开始查找springcloud导致该问题的原因,逐一排除springcloud的5个依赖,发现是spring-cloud-starter-hystrix-dashboard包中的spring-boot-stater-freemarkerjar包影响;
  2. 原服务中有该依赖和freemarker的jar包,服务可以正常进行redirect,排除了spring-cloud-starter-hystrix-dashboard后,freemarker的jar包不在classpath中,服务异常,不能进行redirect重定向;
  3. 开车查找FreeMarker的jar包在服务中的作用,单步调试到DispatcherServletrender(ModelAndView mv, HttpServletRequest request, HttpServletResponse response)方法,发现:如果通过viewName无法解析到相应的View对象,则会抛出javax.servlet.ServletException: Could not resolve view with name异常,详见源码:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
       //省略
       View view;
   	//如果使用 viewName 来关联一个View,则使用ViewResolver进行解析,生成一个View对象
   	if (mv.isReference()) {
   		// We need to resolve the view name.
   		view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
   		//如果视图解析器解析失败,没有得到View对象,则抛出异常
   		if (view == null) {
   			throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
   					"' in servlet with name '" + getServletName() + "'");
   		}
   	}//否则说明ModelAndView对象保护了实际的View对象
   	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() + "'");
   		}
   	}
   	//省略
   }
  1. 查看DispatcherServlet.resolveViewName(String viewName, Map model, Locale locale,HttpServletRequest request)方法,发现该方法是遍历该类的List viewResolvers成员属性,然后调用ViewResolver的各个实现类的View resolveViewName(String viewName, Locale locale)方法;
  2. 重点来了:单步时查看遍历,发现viewResolvers中有三个对象:
    记一次ViewResolver引起的问题 javax.servlet.ServletException: Could not resolve view with name_第2张图片
    其中两个ViewResolver都不能解析出View对象,当轮到FreeMarkerViewResolver时,解析出来一个RedirectView对象。
  3. 至此,查找到Freemarker的jar包影响重定向的原因:项目中默认的ViewResolver实现类BeanNameViewResolverViewResolverComposite,不能解析带有redirect:的重定向view。

其他的解决方案

既然原因如上所述,那么我们可以不依赖Freemarker的视图解析器,而去使用Spring自带的内部资源解析器InternalResourceViewResolver或者基于url的解析器UrlBasedViewResolver,因为Freemarker的解析器也是UrlBasedViewResolver的子类,使用该基类的createView(String viewName, Locale locale)方法。
所以我们可以在项目中手动加载:


<bean id="defaultViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" >
   <property name="order" value="99"/>
bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"/>

源码分析

Spring初始化过程

参看之前的博客

ViewResolver的初始化过程

DispatcherServlet.initViewResolvers(ApplicationContext context)

private void initViewResolvers(ApplicationContext context) {
   	this.viewResolvers = null;

   	if (this.detectAllViewResolvers) {
   		// 查找在 应用上下文 中注册的ViewResolver接口的所有实现类的bean,然后进行排序。
   		Map matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
   		if (!matchingBeans.isEmpty()) {
   			this.viewResolvers = new ArrayList(matchingBeans.values());
   			// We keep ViewResolvers in sorted order.
   			AnnotationAwareOrderComparator.sort(this.viewResolvers);
   		}
   	}
   	else {
   		try {
   		   //否则则加载默认的命名为viewResolver的InternalResourceViewResolver对象
   			ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
   			this.viewResolvers = Collections.singletonList(vr);
   		}
   		catch (NoSuchBeanDefinitionException ex) {
   			// Ignore, we'll add a default ViewResolver later.
   		}
   	}
   	//省略
   }

InternalResourceViewResolver的视图解析过程

可参看该博客

  1. 首先看看该类的继承图,它继承于UrlBasedViewResolverAbstractCachingViewResolver,后者实现了ViewResolver接口的resolveViewName(String viewName, Locale locale)方法,第一步先查找成员变量private final Map viewCreationCache是否缓存了该view,如果没有则生成,并同步地缓存到成员变量中;
  2. 生成方法如下所示:
@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.
   	if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
   		String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
   		RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
   		return applyLifecycleMethods(viewName, view);
   	}
   	// Check for special "forward:" prefix.
   	if (viewName.startsWith(FORWARD_URL_PREFIX)) {
   		String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
   		return new InternalResourceView(forwardUrl);
   	}
   	// Else fall back to superclass implementation: calling loadView.
   	return super.createView(viewName, locale);
   }

知识盘点

  1. SpringBoot和SpringMVC在配置上有所不同,前者

遗留问题

  1. 正常来说,对于SpringBoot的WebMVC项目,InternalResourceViewResolver是自动加载并实例化到BeanFactory中的,然后在Spring的初始化过程中,initViewResolvers(ApplicationContext context)方法会将该视图解析器对象加载到List viewResolvers中。
  2. 没找到本项目未自动加载InternalResourceViewResolver的原因。

你可能感兴趣的:(踩坑记录,工作总结,JAVA相关笔记)