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

本文记录了一次由于ViewResolver引发的问题,问题出现在基于SpringBoot的项目中,表现为微信登录页面加载错误。解决过程中发现删除的springcloud依赖导致了问题,具体原因是freemarker的jar包缺失,影响了视图解析。通过分析源码,了解到当InternalResourceViewResolver无法解析特定视图时,会抛出异常。解决方案包括改用Spring内置的视图解析器。文章还涵盖了Spring初始化过程和ViewResolver的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

记一次ViewResolver引起的问题

问题背景

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

解决过程

  1. 早上10:30左右,测试人员反映微信公众号的登录页面加载错误,出现空白错误页面,页面显示Whitelabel Error Page

  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<String, Object> model, Locale locale,HttpServletRequest request)方法,发现该方法是遍历该类的List<ViewResolver> viewResolvers成员属性,然后调用ViewResolver的各个实现类的View resolveViewName(String viewName, Locale locale)方法;
  2. 重点来了:单步时查看遍历,发现viewResolvers中有三个对象:

    其中两个ViewResolver都不能解析出View对象,当轮到FreeMarkerViewResolver时,解析出来一个RedirectView对象。
  3. 至此,查找到Freemarker的jar包影响重定向的原因:项目中默认的ViewResolver实现类BeanNameViewResolverViewResolverComposite,不能解析带有redirect:的重定向view。

其他的解决方案

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

<!--任选其一即可.因为InternalResourceViewResolver可以解析所有的视图,所以当应用中有多种视图时,需要将该视图解析器的排序设置为最低-->
<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<String, ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
   		if (!matchingBeans.isEmpty()) {
   			this.viewResolvers = new ArrayList<ViewResolver>(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<Object, View> 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<ViewResolver> viewResolvers中。
  2. 没找到本项目未自动加载InternalResourceViewResolver的原因。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值