题目这么长我也不想的,主要是想手动做一下 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
2
3
4
5
class ServiceA {
private InterfaceB b;
private InterfaceC c;
private InterfaceD d;
}

上面这个类的一个实例,为了实现它的逻辑,需要使用到其他三个对象,而这个三个对象的类型都是接口。我们希望让容器自动帮我们构建 ServiceA 的实例,并自动配置它依赖的三个对象。

这个概念很容易理解。GuiceSpring 都提供了依赖注入的功能,但是它们的思路有一些不同。Guice通过类型和 annotation 来确定上面例子中的 b 应该绑定到哪一个对象,并且强制要求使用者定义 Module来显式绑定。而在 Spring 中既可以通过上述两者,还可以通过名字,并且也不需要通过 @Configuration来显式绑定。

我个人觉得依赖注入虽然可以去耦合,但是也确实会降低代码可读性,之前在 Google 就有很多同事和我抱怨。而在 Spring Boot中,引入了各种 auto configuration, 让可读性进一步降低了。很多时候,开发者都不知道框架到底干了些什么。比如,我们会在没有使用任何 MongoDB的时候,看到 Spring Boot 报连接不上 MongoDB 的警告。

最终解决

虽然我上面的猜测确实是对的,但是很难非常容易的找到原因,所以我就简单的单步调试了一下。最后发现在 Spring 启动的时候,会自动判断 webApplicationTypeSERVLET 还是 REACTIVE。而这个判断方法(代码见此),就是检查 classpath 上是否有或者没有特定的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SpringApplication {
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// ...
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// ...
}
}

public enum WebApplicationType {
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
}

从上面的 deduceFromClasspath 可以看到,如果有 WEBFLUX_INDICATOR_CLASS 并且没有 WEBMVC_INDICATOR_CLASSJERSEY_INDICATOR_CLASS 的时候,才会使用 REACTIVE 的方式。而最开始出问题的项目,用于引入了 spark 的依赖,导致引入了 JERSEY_INDICATOR_CLASS 这个类。解决办法也很简单,exclude 掉对应的依赖就可以了。

总结

这次遇到的这个问题,虽然比较容易解决,但还是反映了两个我觉得可以想一想的问题

  1. Spring Boot 使用的 Convention over Configuration 这种思路到底好不好?
  2. Maven 的依赖管理方式会引入大量不需要的类?是否能够解决?

第一个问题,我觉得每个人都有不同的想法。虽然我希望大家可以和我分享自己的想法,但是由于我这个博客实在人迹罕至,就算了。。。

至于第二个问题,我其实觉得之前在 Google 的时候用的内部构建工具 Blaze 可能可以一定程度上解决这个问题,而它有个开源版本 bazel。它的思路大概就是

  1. 让开发者自己选择构建的粒度。你可以以一个文件,多个文件,一个目录,多个目录为单位构建。
  2. 让开发者自行选择构建目标的可见度。拿 Spring Boot举个例子,比如你可以将你的接口实现构建成一个私有目标,将你的 Configuration 类和你的接口构建成一个公共目标并依赖你的私有目标。这样使用方只能依赖你的公共目标。

这样的设计可以减小不必要的类的引入(Maven 粗暴的 exclude 可能会 break 你的依赖,但是 bazel 的构建目标都是人为定义保证自包含),同时也能引导你写代码的时候使用更优良的设计。