目录
一次 Spring Boot 服务无法找到 ServletWebServerFactory 的问题
题目这么长我也不想的,主要是想手动做一下 SEO🤐
这周五下午,我正在认真践行 TDD 的时候,同组的一个同事来问我一个问题。他说他创建了一个新的微服务,原样照抄了之前服务的 maven
依赖和代码,结果启动的时候 spring
报找不到 ServletWebServerFactory
这个 bean
。我当时大概看了一下代码,觉得好像没什么问题。之后花了几分钟调了一下,找到了原因。虽然比较容易解决,但是我觉得还是蛮有意思的,在这里和大家分享一下。
初感觉 - Servlet?
我第一眼看到这个报错就觉得挺奇怪的,因为他拷贝的微服务使用了 Spring Webflux
,不应该会用到 Servlet
。所以我当时就先看了一下他的 pom
文件,结果发现确实只添加了 spring-boot-starter-webflux
这个依赖。那就奇怪了,这是为什么呢?
Classpath 有什么奇怪的类?
在确认依赖没有问题的时候,我的第二个猜测就是,在 classpath 上有什么奇怪的类。熟悉 spring boot
的朋友可能都知道,spring boot
使用了 convention over configuration
的设计思路。是不是它在扫描 classpath 的时候发现了什么类,然后就觉得应该需要 ServletWebServerFactory
这个 bean
呢?
Convention over Configuration
我之前在 Google 的时候,公司内部使用的是 Guice 来做依赖注入。所谓依赖注入,通俗来讲,就是让程序的容器,来帮一个类自动的添加它需要的依赖,来做到去耦合。比如下面这个例子:
1 | class ServiceA { |
上面这个类的一个实例,为了实现它的逻辑,需要使用到其他三个对象,而这个三个对象的类型都是接口。我们希望让容器自动帮我们构建 ServiceA
的实例,并自动配置它依赖的三个对象。
这个概念很容易理解。Guice
和 Spring
都提供了依赖注入的功能,但是它们的思路有一些不同。Guice
通过类型和 annotation
来确定上面例子中的 b 应该绑定到哪一个对象,并且强制要求使用者定义 Module
来显式绑定。而在 Spring
中既可以通过上述两者,还可以通过名字,并且也不需要通过 @Configuration
来显式绑定。
我个人觉得依赖注入虽然可以去耦合,但是也确实会降低代码可读性,之前在 Google 就有很多同事和我抱怨。而在 Spring Boot
中,引入了各种 auto configuration
, 让可读性进一步降低了。很多时候,开发者都不知道框架到底干了些什么。比如,我们会在没有使用任何 MongoDB
的时候,看到 Spring Boot
报连接不上 MongoDB
的警告。
最终解决
虽然我上面的猜测确实是对的,但是很难非常容易的找到原因,所以我就简单的单步调试了一下。最后发现在 Spring
启动的时候,会自动判断 webApplicationType
是 SERVLET
还是 REACTIVE
。而这个判断方法(代码见此),就是检查 classpath 上是否有或者没有特定的类。
1 | class SpringApplication { |
从上面的 deduceFromClasspath
可以看到,如果有 WEBFLUX_INDICATOR_CLASS
并且没有 WEBMVC_INDICATOR_CLASS
和 JERSEY_INDICATOR_CLASS
的时候,才会使用 REACTIVE
的方式。而最开始出问题的项目,用于引入了 spark
的依赖,导致引入了 JERSEY_INDICATOR_CLASS
这个类。解决办法也很简单,exclude 掉对应的依赖就可以了。
总结
这次遇到的这个问题,虽然比较容易解决,但还是反映了两个我觉得可以想一想的问题
Spring Boot
使用的 Convention over Configuration 这种思路到底好不好?- Maven 的依赖管理方式会引入大量不需要的类?是否能够解决?
第一个问题,我觉得每个人都有不同的想法。虽然我希望大家可以和我分享自己的想法,但是由于我这个博客实在人迹罕至,就算了。。。
至于第二个问题,我其实觉得之前在 Google 的时候用的内部构建工具 Blaze 可能可以一定程度上解决这个问题,而它有个开源版本 bazel。它的思路大概就是
- 让开发者自己选择构建的粒度。你可以以一个文件,多个文件,一个目录,多个目录为单位构建。
- 让开发者自行选择构建目标的可见度。拿
Spring Boot
举个例子,比如你可以将你的接口实现构建成一个私有目标,将你的Configuration
类和你的接口构建成一个公共目标并依赖你的私有目标。这样使用方只能依赖你的公共目标。
这样的设计可以减小不必要的类的引入(Maven 粗暴的 exclude 可能会 break 你的依赖,但是 bazel 的构建目标都是人为定义保证自包含),同时也能引导你写代码的时候使用更优良的设计。