为什么不要将spring-boot相关依赖打入二方包

 

  本文内容来源于博主一次问题排查的过程,最终说明为什么不要将spring-boot相关依赖打入二方包。

  先介绍一下背景:我们应用是一个标准的spring+webx工程,博主在一次项目发布前为了再次测试一下自己的代码,将分支部署到日常环境中,但是项目启动的时候报错:

  

  第一眼看到这个堆栈后有点懵逼

  第一是上一次部署分支还没问题,距离上次部署自己新增的代码也很简单,不可能写出如此诡异的代码去改变spring的行为。况且从tomcat启动日志来看,报错的时候还根本没有到应用的代码。

  第二是这个错误本身,Could not open ServletContext resource很常见,但是这个错误后面通常都是无法打开一个具体的文件,一般就是工程里(例如autoconfig)配置了一个文件路径,而文件路径不存在。但是,这里的文件路径竟然是NONE。

  •  问题排查及解决过程

  首先是怀疑自己的分支出了什么问题,部署了一下主干,还有这个报错。因为错误堆栈来看是从tomcat过来的,因此猜测有以下解释:

  1. 环境因素(PE有没有对tomcat或pandora做什么事情)
  2. 文件不存在或文件权限有问题(导致path为NONE)
  3. Jar包冲突(原二方包版本变化)或Jar包干扰(新增二方包产生干扰)

  排查过程:

  1. 首先我们怀疑的环境因素导致的,对比了一下日常/预发和线上的环境差异,jdk、tomcat、pandora版本都一样。同时在日常也启动了一遍,也报这个错。如果环境因素导致的不会日常和线上都存在问题吧。看了下邮件也没发现PE有什么动作,这时候还不放心申请了个项目环境跑了一遍还是这个错,如果日常和预发是PE搞了什么鬼那么项目环境完全是一个干净的环境,应该不会产生干扰。因此环境因素基本排除掉了。
  2. 日常和预发的WEB-INF文件都是存在的,同时对比了下线上的WEB-INF文件夹的权限,发现也是完全一样的,因此文件的因素也被排除掉了。
  3. 接下来我们重启了一台线上机器的war包,没有任何报错,这个时候又有点怀疑环境因素,我们将线上机器war包scp到预发机器上,启动没有报错!那么,环境因素可以彻底排除掉了。

  同为主干代码线上war包没问题而日常/预发部署就有问题,问题基本清晰起来了,应该是某个SNAPSHOT二方包引起的问题。

  分别将线上和预发war包里的二方包文件列表输出到两个不同的文件中,然后diff两个文本后发现了始作俑者:

  

  发现framework-sqlanalyse-1.0.0-SNAPSHOT包的大小有所变化,并且新增了pandora-boot及spring-boot等一些新的二方包。

  mvn tree查看了下,pandora-boot和spring-boot果然就是由framework-sqlanalyse引入进来的。在pom中把pandora-boot和spring-boot排掉之后再次部署终于成功了。至此这个问题算是解决了,但是到底是怎么产生的呢?

  • 问题定位

  回到刚刚diff的文件结果,我们发现这并不是一个包的冲突(因为只有framework-sqlanalyse这个包变化了),而是新包产生的干扰。也就是说非spring-boot应用引入了spring-boot或pandora-boot的二方包之后就会产生上述问题。那么这个问题到底是如何产生的?

  首先需要定位到底是哪个包导致的这个问题,经过分类排包后定位到是org.springframework.boot:spring-boot-autoconfigure这个包引起的,但是我们的报错堆栈中并没有org.springframework.boot相关的类。

  spring-boot-autoconfigure这个包用于spring boot自动配置机制,如果在应用中添加了@EnableAutoConfiguration就会触发自动配置,它会根据定义在classpath下的类,自动生成一些Bean,并加载到Spring的Context中。spring boot应用启动类上的@SpringBootApplication便继承自@EnableAutoConfiguration。

  一开始也怀疑是自动配置导致的,但是我们的应用只是一个spring+webx的普通web应用而已,并没有@EnableAutoConfiguration,因此不会触发自动配置,也不会加载embed tomcat。

  后来发现这是来自于spring boot的一个官方issue:https://github.com/spring-projects/spring-boot/issues/5740

  始作俑者是spring-boot-autoconfigure中一个配置类JerseyAutoConfiguration中的内嵌类JerseyWebApplicationInitializer

  @Order(Ordered.HIGHEST_PRECEDENCE)
  public static final class JerseyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
      // We need to switch *off* the Jersey WebApplicationInitializer because it
      // will try and register a ContextLoaderListener which we don't need
      servletContext.setInitParameter("contextConfigLocation", "<NONE>");
    }
  }

  我们知道,继承了WebApplicationInitializer的类都会被应用加载,原因就在于SpringServletContainerInitializer,他会实例化classpath下所有继承了WebApplicationInitializer的类,并且会触发每个WebApplicationInitializer的onStartup方法。这样,servletContext就被篡改了。

  在启动日志中看见有这样的内容:INFO: Spring WebApplicationInitializers detected on classpath: [org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration$JerseyWebApplicationInitializer@155b6f9d]  这也印证了JerseyWebApplicationInitializer确实被加载了。

  当ServletContext初始化完成之后web容器就开始启动了,我们的应用是基于webx的,配置在web.xml中的webx的监听器便开始起作用了。

  WebxContextLoaderListener实现了spring的ContextLoaderListener。它会调用ContextLoader的initWebApplicationContext()方法,而在webx中初始化的是WebxComponentContext(继承自XmlWebApplicationContext)。ContextLoaderListener是使用servletContext来做初始化的,这时已经被修改过了,那个NONE就是这样被传过来的。

  •  总结

  至此这个问题已经搞清楚了,最后总结一下上面这个case有以下几点:

  1. 除非万不得已,今后线上部署时应该杜绝SNAPSHOT二方包,一方面减少下次部署隐患,另一方面排查问题时也可以排除不必要的干扰。
  2. 不要将spring boot相关依赖打入二方包中,如果webx应用使用了该二方包会必现上述问题,目前spring boot与webx依然是不兼容的。
  3. 在项目工程开发时,spring boot应用的正确依赖姿势应该是这样的:根pom中应该将spring-boot及pandora-boot相关依赖放在dependencyManagement标签中,让子模块去显示依赖,而不要放在dependencies标签中污染client包。
posted @ 2017-02-12 13:28  此生重演  阅读(3115)  评论(0编辑  收藏  举报