@月黑风高食肉虎 噗噗虎的技术博客

Spring boot 的 viewResolver 黑科技总结


Spring framework的viewResolver

当一个Controller返回一串String类型的view name之后,Spring是如何找到对应的view的?

答案就在DispatcherServletresolveViewName方法中,它循环一个viewResolvers列表, 然后调用ViewResolver.resolveViewName(...)方法,当列表中任意一个viewResolver返回不为null时, DispatcherServlet就认为这个view解析完毕,并使用解析得到的View对象继续处理。

那么问题就来了,这个viewResolvers列表是如何来的呢?

答案在DispatcherServletinitViewResolver方法中。在该方法中,首先根据detectAllViewResolvers标识来确定viewResolver的获取方式:

  • 如果detectAllViewResolverstrue(默认为true),则会从当前的ApplicationContext中获取所有实现ViewResolver包括其子类的对象,然后还会根据对象的Order进行排序。
  • 如果detectAllViewResolversfalse,则会从当前的ApplicationContext中获取Bean name为viewResolver的,类型为ViewResolver的单个对象,作为viewResolvers列表中的唯一对象。

然后,如果上述两种情况还没有获取到任何viewResolversDispatcherServlet会启用默认策略, 根据DispatcherServlet.properties的配置,获取并初始化键值为 org.springframework.web.servlet.ViewResolver的class作为viewResolvers列表成员。

Spring Boot的viewResolver自动配置

WebMvcAutoConfiguration中的WebMvcAutoConfigurationAdapter中可以看到,它根据条件自动注入了以下几个viewResolver

  • 如果当前环境里没有InternalResourceViewResolver同类型的Bean,则自动注入类型为InternalResourceViewResolver、 名称为defaultViewResolver的Bean。InternalResourceViewResolverUrlBasedViewResolver的扩展,Order默认为Integer.MAX_VALUE
  • 如果当前环境里有类型为View的Bean,并且没有注入过BeanNameViewResolver同类型的Bean, 则自动注入类型为BeanNameViewResolver、名称为beanNameViewResolver的Bean。 并且设置其OrderOrder.LOWEST_PRECEDENCE - 10,其中Order.LOWEST_PRECEDENCE的值为Integer.MAX_VALUE
  • 如果当前环境中已经存在类型为ViewResolver的Bean,并且没有类型为ContentNegotiatingViewResolver、 名称为viewResolver的Bean,则自动注入同类型同名的Bean。并且设置其OrderOrder.HIGHEST_PRECEDENCE, 其中Order.HIGHEST_PRECEDENCE的值为Integer.MIN_VALUE

这3个ViewResolverWebMvcAutoConfigurationAdapter为我们默认配置的3个Bean, 但是你以为这样就完事儿了么?其实不然,Spring Boot还会配置一个ViewResolverCompositeViewResolver, 这个ViewResolver包含了所有使用WebMvcConfigurerAdapter#configureViewResolvers(ViewResolverRegistry)方法配置的viewResolver。 并且该ViewResolverCompositeOrderOrder.LOWEST_PRECEDENCE

这个ViewResolverCompositeEnableWebMvcConfiguration类提供。EnableWebMvcConfiguration继承了DelegatingWebMvcConfiguration, 而DelegatingWebMvcConfiguration则继承了WebMvcConfigurationSupport

WebMvcConfigurationSupport中,提供了一个注册ViewResolver类型Bean的mvcViewResolver方法。 该方法先初始化了一个ViewResolverRegistry实例,然后将其传入并调用configureViewResolvers方法。 最后它根据在configureViewResolvers方法中配置的ViewResolverRegistry实例初始化并返回了一个ViewResolverComposite的实例。

而在WebMvnConfigurationSupport的子类,也是EnableWebMvcConfiguration的父类DelegatingWebMvcConfiguration中, 它首先注入了当前环境中所有的WebMvcConfigurerWebMvcConfigurationAdapter实现了该接口)类型的Bean的列表, 并将该列表包装给WebMvcConfigurerComposite,最后将所有configure*add*等方法代理给WebMvcConfigurerComposite。 这样就实现了将所有可配置的方法代理给外部WebMvcConfigurer的实现类的这样一个功能。

所以总结一下,在Spring boot自动配置的情况下,会有这样几个viewResolver(按优先度从高到低):

  1. ContentNegotiatingViewResolver,该ViewResolver会根据前端传来的请求内容类型 (譬如说由accept头指定、由format参数指定或者由url的后缀名指定等,根据ContentNegotiatingManager中配置的strategies来确定, 可通过WebMvcConfigurer#configureContentNegotiation方法来配置)、并且轮询其他各个ViewResolver来返回相应的View
  2. BeanNameViewResolver。如果没有配置其他的ViewResolver则该viewResolver仅次于ContentNegotiatingViewResolver。 自动注入这个viewResolver的前提是当前环境中已经有类型为View的Bean,否则该viewResolver不会被注入到当前环境中。 这个ViewResolver的职责是将Controller返回的String url当成bean name去环境中找相应的实现了View接口的Bean。
    1. ViewResolverComposite。所有通过WebMvConfigurer#configureViewResolvers方法配置的viewResolver都会集中在这个composite里。
    2. InternalResourceViewResolver。该viewResolver负责将Controller返回的String解析成为InternalResourceViewservletJSP等)。 需要特别注意的是,该ViewResolver会尝试去解析所有view name,无论对应的资源文件(如JSP)是否存在,所以该viewResolver理论上要放在最后。