记一次ViewResolver引起的问题
文章目录
问题背景
公司的项目基于SpringBoot开发,基本上所有接口都是Restful风格的,接收json参数,返回json数据。一般控制类上直接用
@RestController
或者@Controller
+@ResponseBody
来将对象序列化后返回给前端。
项目中还有少数几个接口是通过redirect:xxx
的方式进行重定向的,其中包括微信登录授权。这些接口需要用到Spring的ViewResolver
实现类进行视图解析,得到viewName对应的View
对象,然后由View
对象进行视图的渲染(render)。
解决过程
-
早上10:30左右,测试人员反映微信公众号的登录页面加载错误,出现空白错误页面,页面显示
Whitelabel Error Page
;
-
查看服务器日志,显示
javax.servlet.ServletException: Could not resolve view with name 'redirect:https:
-
搜索该错误信息,查询到一堆博客等资料,都未能解决;
-
开始排查代码提交记录,发现
pom.xml
中删除了springcloud相关的5个依赖,添加依赖后,服务恢复正常。
查找原因
- 服务恢复后,开始查找springcloud导致该问题的原因,逐一排除springcloud的5个依赖,发现是
spring-cloud-starter-hystrix-dashboard
包中的spring-boot-stater-freemarker
jar包影响; - 原服务中有该依赖和freemarker的jar包,服务可以正常进行
redirect
,排除了spring-cloud-starter-hystrix-dashboard
后,freemarker的jar包不在classpath
中,服务异常,不能进行redirect
重定向; - 开车查找FreeMarker的jar包在服务中的作用,单步调试到
DispatcherServlet
的render(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() + "'");
}
}
//省略
}
- 查看
DispatcherServlet.resolveViewName(String viewName, Map<String, Object> model, Locale locale,HttpServletRequest request)
方法,发现该方法是遍历该类的List<ViewResolver> viewResolvers
成员属性,然后调用ViewResolver
的各个实现类的View resolveViewName(String viewName, Locale locale)
方法; - 重点来了:单步时查看遍历,发现
viewResolvers
中有三个对象:
其中两个ViewResolver
都不能解析出View对象,当轮到FreeMarkerViewResolver
时,解析出来一个RedirectView
对象。 - 至此,查找到Freemarker的jar包影响重定向的原因:项目中默认的
ViewResolver
实现类BeanNameViewResolver
和ViewResolverComposite
,不能解析带有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的视图解析过程
可参看该博客
- 首先看看该类的继承图,它继承于
UrlBasedViewResolver
和AbstractCachingViewResolver
,后者实现了ViewResolver
接口的resolveViewName(String viewName, Locale locale)
方法,第一步先查找成员变量private final Map<Object, View> viewCreationCache
是否缓存了该view,如果没有则生成,并同步地缓存到成员变量中;
- 生成方法如下所示:
@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);
}
知识盘点
- SpringBoot和SpringMVC在配置上有所不同,前者
遗留问题
- 正常来说,对于SpringBoot的WebMVC项目,
InternalResourceViewResolver
是自动加载并实例化到BeanFactory
中的,然后在Spring的初始化过程中,initViewResolvers(ApplicationContext context)
方法会将该视图解析器对象加载到List<ViewResolver> viewResolvers
中。 - 没找到本项目未自动加载
InternalResourceViewResolver
的原因。