美文网首页
Spring--视图内容协商(一)

Spring--视图内容协商(一)

作者: NealLemon | 来源:发表于2018-12-10 12:19 被阅读0次

本文是学习了小马哥在慕课网的课程的《Spring Boot 2.0深度实践之核心技术篇》的内容结合自己的需要和理解做的笔记。

所谓的视图内容协商,就是让Web客户端根据不同的请求策略,实现服务端响应对应视图的内容输出。 接下来让我们深入的了解一下到底Spring是如何视图内容协商的。

核心组件

  • 视图解析器:ContentNegotiatingViewResolver
  • 内容协商管理器:ContentNegotiationManager
  • 内容协商策略:ContentNegotiationStrategy

源码解读前置工作

在我们要理解Spring的视图内容协调流程图之前,我们需要新建一个spring-boot项目,然后进行必要的配置来启动视图内容协商。我们新建一个模块名为 springboot-restful 之所以起这个名字,是因为视图内容协商不仅是对客户端视图渲染的协商操作,也是针对restful形式的内容的请求和响应的协商操作。

1.新建Model-- springboot-restful

目录.PNG

2.pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
    </dependency>

    <!-- Provided -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

3.其他代码

/**
 * Spring 拦截器 配置
 */
@Configuration   //配置
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 配置视图内容协商
     * @param configurer
     */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorParameter(true).favorPathExtension(true);

    }


    /**
     * 解决在IDEA下maven多模块使用spring-boot跳转JSP 404问题
     * @return
     */
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> customizer() {
        return (factory -> {
            factory.addContextCustomizers(context -> {
                //当前webapp路径
                String relativePath = "springboot-restful/src/main/webapp";
                File docBaseFile = new File(relativePath);
                if(docBaseFile.exists()) {
                    context.setDocBase(new File(relativePath).getAbsolutePath());
                }
            });
        });
    }

}
/**
 * Spring-boot 启动引导类
 */
@SpringBootApplication
public class SpringBootRestfulBootStrap {


    public static void main(String[] args) {
        SpringApplication.run(SpringBootRestfulBootStrap.class,args);

    }
}

流程图

流程图.PNG

源码解读

我们可以根据上面的流程图来一起阅读源码,这样能让我们有个初步的理解。

步骤一

首先我们来看一下Spring-boot是什么时候开始初始化声明ContentNegotiationConfigurer

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcContentNegotiationManager

在这个方法中,我们可以看到在容器初始化时,Spring-boot的自动装配就把ContentNegotiationManager装在到容器了。

@Bean
@Override
public ContentNegotiationManager mvcContentNegotiationManager() {
   ContentNegotiationManager manager = super.mvcContentNegotiationManager();
   List<ContentNegotiationStrategy> strategies = manager.getStrategies();
   ListIterator<ContentNegotiationStrategy> iterator = strategies.listIterator();
   while (iterator.hasNext()) {
      ContentNegotiationStrategy strategy = iterator.next();
      if (strategy instanceof PathExtensionContentNegotiationStrategy) {
         iterator.set(new OptionalPathExtensionContentNegotiationStrategy(
               strategy));
      }
   }
   return manager;
}

super.mvcContentNegotiationManager()源码:

/**
 * Return a {@link ContentNegotiationManager} instance to use to determine
 * requested {@linkplain MediaType media types} in a given request.
 */
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
   if (this.contentNegotiationManager == null) {
      ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
      configurer.mediaTypes(getDefaultMediaTypes());
      configureContentNegotiation(configurer);
      this.contentNegotiationManager = configurer.buildContentNegotiationManager();
   }
   return this.contentNegotiationManager;
}

在这里通过ContentNegotiationConfigurer来创建ContentNegotiationManager 对象,我们先来看一下ContentNegotiationConfigurer中都有哪些关键的方法。

public class ContentNegotiationConfigurer {

   private final ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
    .......
            /**
     * Build a {@link ContentNegotiationManager} based on this configurer's settings.
     * @since 4.3.12
     * @see ContentNegotiationManagerFactoryBean#getObject()
     */
    protected ContentNegotiationManager buildContentNegotiationManager() {
        this.factory.addMediaTypes(this.mediaTypes);
        return this.factory.build();
    }

}

在这里 我们可以看到ContentNegotiationConfigurer 类中,声明了一个ContentNegotiationManagerFactoryBean这也如流程图中的 步骤1 ---- 关联。

步骤二

配置策略 则是使用ContentNegotiationConfigurer 的几个方法来配置,这里我们在自定义的com.web.configuration.WebMvcConfig中只使用了两种配置。代码如下

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.favorParameter(true).favorPathExtension(true);

}

对应的方法代码如下:

    /**
     * Whether a request parameter ("format" by default) should be used to
     * determine the requested media type. For this option to work you must
     * register {@link #mediaType(String, MediaType) media type mappings}.
     * <p>By default this is set to {@code false}.
     * @see #parameterName(String)
     */
//请求参数
public ContentNegotiationConfigurer favorParameter(boolean favorParameter) {
   this.factory.setFavorParameter(favorParameter);
   return this;
}
    /**
     * Whether the path extension in the URL path should be used to determine
     * the requested media type.
     * <p>By default this is set to {@code true} in which case a request
     * for {@code /hotels.pdf} will be interpreted as a request for
     * {@code "application/pdf"} regardless of the 'Accept' header.
     */
//URL后缀
public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) {
    this.factory.setFavorPathExtension(favorPathExtension);
    return this;
}

这里就不多做解释了,感兴趣的小伙伴可以看一下英文注释,也就轻轻松松明白了。

步骤三/步骤四

添加策略和创建ContentNegotiationManager 放在一起讲。

我们可以从步骤一的源码中看到 最后是调用ContentNegotiationConfigurer中的buildContentNegotiationManager()方法来创建ContentNegotiationManager的。

那么我们来进一步看一下代码。

org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer#buildContentNegotiationManager

protected ContentNegotiationManager buildContentNegotiationManager() {
   this.factory.addMediaTypes(this.mediaTypes);
   return this.factory.build();
}

这个方法在步骤一中我就有贴出,因为非常重要,在这我们可以看到,其实是使用ContentNegotiationManagerFactoryBean来创建ContentNegotiationManager的。我们来看一下build()方法。

/**
 * Actually build the {@link ContentNegotiationManager}.
 * @since 5.0
 */
public ContentNegotiationManager build() {
   List<ContentNegotiationStrategy> strategies = new ArrayList<>();

   if (this.strategies != null) {
      strategies.addAll(this.strategies);
   }
   else {
       //是否配置 URL后缀策略
      if (this.favorPathExtension) {
          //声明并配置 PathExtensionContentNegotiationStrategy 策略
         PathExtensionContentNegotiationStrategy strategy;
         if (this.servletContext != null && !useRegisteredExtensionsOnly()) {
            strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
         }
         else {
            strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
         }
         strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
         if (this.useRegisteredExtensionsOnly != null) {
            strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
         }
         strategies.add(strategy);
      }

       //是否配置了参数策略
      if (this.favorParameter) {
          //声明并配置ParameterContentNegotiationStrategy策略
         ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
         strategy.setParameterName(this.parameterName);
         if (this.useRegisteredExtensionsOnly != null) {
            strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
         }
         else {
            strategy.setUseRegisteredExtensionsOnly(true);  // backwards compatibility
         }
         strategies.add(strategy);
      }

      if (!this.ignoreAcceptHeader) {
         strategies.add(new HeaderContentNegotiationStrategy());
      }

      if (this.defaultNegotiationStrategy != null) {
         strategies.add(this.defaultNegotiationStrategy);
      }
   }

   this.contentNegotiationManager = new ContentNegotiationManager(strategies);
   return this.contentNegotiationManager;
}

一下贴出这么多代码可能有些懵,但是我们对照着流程图一步一步的看,由于我们已经配置了 <u>参数策略</u>以及<u>URL后缀策略</u>。所以 上面的if else 就很好懂了。看注释就可以明白了。

步骤五

关联ContentNegotiatingViewResolver通过org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#viewResolver方法,来初始化Bean ContentNegotiatingViewResolver 。可以看到方法第二行就是关联ContentNegotiationManager的地方。

@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
   ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    //关联 ContentNegotiationManager
   resolver.setContentNegotiationManager(
         beanFactory.getBean(ContentNegotiationManager.class));
   // ContentNegotiatingViewResolver uses all the other view resolvers to locate
   // a view so it should have a high precedence
   resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
   return resolver;
}

步骤六

ViewResolver Bean 关联

这一步骤非常的绕,看了小马哥的视频后,debugger了很久,不明白是在何时ContentNegotiatingViewResolver调用方法关联其他ViewResolver的。

org.springframework.web.servlet.view.ContentNegotiatingViewResolver#initServletContext

我们先来看源码:

@Override
protected void initServletContext(ServletContext servletContext) {
    //获取到所有 ViewResolvers
   Collection<ViewResolver> matchingBeans =
         BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
    //关联他们 
   if (this.viewResolvers == null) {
      this.viewResolvers = new ArrayList<>(matchingBeans.size());
      for (ViewResolver viewResolver : matchingBeans) {
         if (this != viewResolver) {
            this.viewResolvers.add(viewResolver);
         }
      }
   }
   else {
      for (int i = 0; i < this.viewResolvers.size(); i++) {
         ViewResolver vr = this.viewResolvers.get(i);
         if (matchingBeans.contains(vr)) {
            continue;
         }
         String name = vr.getClass().getName() + i;
         obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
      }

   }
   if (this.viewResolvers.isEmpty()) {
      logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
            "'viewResolvers' property on the ContentNegotiatingViewResolver");
   }
    //排序
   AnnotationAwareOrderComparator.sort(this.viewResolvers);
   this.cnmFactoryBean.setServletContext(servletContext);
}

从代码中我们可以很清晰的看到 首先 先获取到所有的ViewResolver 然后遍历关联。

但是,到底是什么时候关联的呢。

经过debugger是在

org.springframework.boot.SpringApplication#run(java.lang.String...)启动方法中的 refreshContext(context);这个调用后就可以关联上了。那么为什么会这样,我们可以看到

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
      implements ViewResolver, Ordered, InitializingBean {
      ...
      }
    

ContentNegotiatingViewResolver继承了 WebApplicationObjectSupport

这个类中有这么一个方法

org.springframework.web.context.support.WebApplicationObjectSupport#initApplicationContext

@Override
protected void initApplicationContext(ApplicationContext context) {
   super.initApplicationContext(context);
   if (this.servletContext == null && context instanceof WebApplicationContext) {
      this.servletContext = ((WebApplicationContext) context).getServletContext();
      if (this.servletContext != null) {
         initServletContext(this.servletContext);
      }
   }
}

那么是在什么时候触发的这个方法呢。

由于过程是在太复杂,只把最后几步贴出来。

1.org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean
2.org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization
3.org.springframework.context.support.ApplicationContextAwareProcessor#postProcessBeforeInitialization
4.org.springframework.context.support.ApplicationContextAwareProcessor#invokeAwareInterfaces

最后在invokeAwareInterfaces这个方法里调用setApplicationContext
如果感兴趣的可以自己打好断点去Debugger跟一下,对于整体spring的装载机制都会有一个比较基础的理解。

if (bean instanceof ApplicationContextAware) {
   ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}

其实这么复杂的过程,用一句简单的话总结就是Spring-boot的一个关于在访问 ServletContext的一个回调接口,来自定义初始化。

总结

以上的内容其实有点难懂,感兴趣的小伙伴可以仔细研究一下,毕竟理解Spring比使用API接口要难得多。共勉加油。

相关文章

  • Spring--视图内容协商(一)

    本文是学习了小马哥在慕课网的课程的《Spring Boot 2.0深度实践之核心技术篇》的内容结合自己的需要和理解...

  • Spring--视图内容协商(二)

    本文是学习了小马哥在慕课网的课程的《Spring Boot 2.0深度实践之核心技术篇》的内容结合自己的需要和理解...

  • Spring--视图内容协商(三)

    Spring--视图内容协商(三) 本文是学习了小马哥在慕课网的课程的《Spring Boot 2.0深度实践之核...

  • Spring--视图内容协商(四)

    本文是学习了小马哥在慕课网的课程的《Spring Boot 2.0深度实践之核心技术篇》的内容结合自己的需要和理解...

  • Content negotiation - Django RES

    内容协商-Django REST框架 negotiation.py 内容协商 http有关于“内容协商”的几种机制...

  • 3.6 内容协商

    当浏览器的默认语言为英语或中文,访问相同的URI的web页面时,则会显示对应的英文版或者中文版的web页面。这样的...

  • 内容协商与编码

    内容协商技术 共有3种不同的方法可以决定服务器上哪个页面最适合客户端:让客户端来选择,服务器自动判定,或中间代理来...

  • SpringBoot之内容协商器

    背景 使用了restful的小伙伴对于导出这些需求本能就是拒绝的~破坏了restful的url的一致性【严格矫正 ...

  • http内容协商机制

    指客户端和服务器之间就响应资源内容进行交涉,然后提供给客户端最为合适的资源,内容协商会以响应资源的语言、字符集、编...

  • SpringMVC-ContentNegotiatingView

    内容协商器(ContentNegotiatingViewResolver与contentNegotiationMa...

网友评论

      本文标题:Spring--视图内容协商(一)

      本文链接:https://www.haomeiwen.com/subject/dhzlhqtx.html