#微服务开发下的 build 困扰
前提:Java 开发,Maven 做依赖管理,JetBrains IntelliJ IDEA 做 IDE
在 Java 微服务开发下,项目开发结构(repo)可能有两种:
- 所有模块(maven module),包括服务(Spring Application)和自己开发的依赖模块,都塞在一个项目里。
- 方便管理各模块间的关系,方便写代码
- git log 很乱很多,但也方便集中检查
- 服务和自己开发的依赖模块独立在不同项目里
- 不方便管理模块关系
- git log 少又清晰,只有该组件相关的提交
(以我的工作经历来看,两种结构怎么选好像和开发组的人员结构有关:如果负责不同服务的小组紧密围绕在开发组长周围,选集中式的面儿大;如果小组间相互不太紧密,比如这个项目是不同部门联合开发之类,选分散式的面儿大。)
现在假设:服务A -依赖-> 依赖B
。现在我想改依赖B的代码,启动服务A检查修改效果。(假如B不方便写单测,比如写了个自定义某框架相关的配置类,只能启动看看效果)正确的操作步骤为先 build B,再 debug run A,虽然 JetBrains 会监控服务A的代码变化并在 run 时 rebuild A,但它不会(至少默认不会)监控A依赖的B的变化。这个算常识,对吧,但人总有大意的时候,我已经不止一次忘记先 build B,然后 debug A时B的行为就死活不变,直到花很长时间想起来这个原因。
怎样实现启动A时让B自动 build,或者简单点,让B的 artifact 能反映最新的代码修改?
对分散式结构,我至今没什么好招,都跨文件目录了,感觉只有用外部工具才能实现,至今没找到此类工具或 Maven plugin/ IDEA plugin。如果下个我参与的项目是分散式,我打算自己写一个工具。还好现在我所在的组采用集中式结构。
首先想到的是 Maven 的reactor内部机制,如果在顶级(maven root)项目上 build,reactor 就能根据拓扑排序依次 build 所有模块。现在出现了另一个问题:浪费时间在 build 不需要的模块上。比如我组的项目里有个胖服务,整个 build 它和它的依赖链要十几分钟,这样会让开发体验变得不流畅(此处插一个程序员笑话xkcd - Compiling)。
Maven 提供了一些命令行选项实现这种需求。我没能找到一个讲得非常明白的博客,所以自己写一篇。注意这里只挑一般会用到的选项写,全部选项请看链接(没写的都是几乎不会用的选项),或者直接mvn --help
参数比网站上的还全。
#Maven reactor cmd options
这里有一个示例项目,它的依赖结构和 reactor 给出的 root build 顺序是:
|
|
#projects
简写-pl
,特别指定一个模块的子集去 build,不 build 其他的。
如:mvn --projects "dep-util, dep-util2" clean install
[INFO] dep-util ........................................... SUCCESS [ 0.995 s]
[INFO] dep-util2 .......................................... SUCCESS [ 0.084 s]
这个选项比较万能,不会沿依赖链传递 build,指定什么 build 什么。
比如我改了dep-util2
,但没改接口,选这个命令是最适合的。回到开头的问题,如果我频繁改模块B,B直接被服务A依赖,那每次运行只需手动 build 模块B,再跑 Run/Debug 自动 build 服务A。
我在 SOF 上找到了一个自动化程度高一些的命令,根据 git 修改记录自动取需要 build 的模块名:
|
|
#resume-from
简写-rf
(一阵恶寒),这个选项本意是指定一个 build fail 的 callback,“如果主 build 命令失败,从 root build 顺序的中间指定模块开始重新尝试 build”。
但是它可以单独用,效果是直接从中间开始 build。单独用到的机会很少,因为从指定的中间到底部可能包含我们不想 build 的模块。
比如我们改了dep-util
和app-1
,mvn --resume-from dep-util clean install
,等于从顶 build 到尾,把dep-util2
和app-2
也 build 了。
#also-make
简写-am
,这个选项用于想同时 build 依赖和服务,意为“也 build 主命令目标的依赖”,目标选服务,会向下递归传递。比如:改了dep-util
和app-1
,
mvn --projects app-1 --also-make clean install
[INFO] dep-util ........................................... SUCCESS [ 0.824 s]
[INFO] dep-service ........................................ SUCCESS [ 0.106 s]
[INFO] app-1 .............................................. SUCCESS [ 0.109 s]
#also-make-dependents
简写-amd
,这个是--also-make
的相反选项,“也 build 使用主命令目标的模块”,目标选依赖,会向上传递。
如:mvn --projects dep-util --also-make-dependents clean install
[INFO] dep-util ........................................... SUCCESS [ 1.010 s]
[INFO] dep-service ........................................ SUCCESS [ 0.105 s]
[INFO] app-1 .............................................. SUCCESS [ 0.109 s]
[INFO] app-2 .............................................. SUCCESS [ 0.097 s]
用在接下来要在本地环境同时开多个依赖该模块的服务做测试的情况(微服务一个请求串多个服务常有的事)。
#理解 Maven 的 artifact 控制逻辑
理解 Maven 的控制逻辑,能让你更清楚的知道什么时候该 build 什么模块。总结下来就一点:改了哪个 build 哪个,其他不用动,从底向上 build。但是如果你 pull 了代码,想跑一整个服务,不知道什么模块被同事改了,那就只能图省事用上面的--also-make
build 服务主模块,以更新整个依赖链了。
(我的实践是每参加一个 Java 项目组,为其项目分配一个独立的 Maven 配置和仓库目录,防止依赖混淆。其实对 Maven 这种通过版本管理依赖的工具+Java 语言的目标文件(库分发文件)即 jar 包中的字节码不会被轻松修改,所有项目用一个库也是可行的。你看 Python、JS 的工具,大部分不允许同一依赖的多个版本共存在一个环境中。)
看这个 SOF 上的提问:target folder vs local repository 我们得知:跑mvn install
时,Maven 会生成模块的 artifact(jar 包)放进本地仓库。使用的打包方式不同,服务“依赖”模块 artifact 的时机也不同,有的是打包有的是运行时,但都是从本地仓库取的 artifact。总之我们通过更新模块的 artifact,把模块的修改“带进了服务里”。
#常见错误操作
-
我们有时会用鼠标在 IDEA 的 Maven 插件的界面中(在界面右侧边栏里),运行 Maven 命令弹窗->输入栏右边点击选择目标模块,这个操作不等于加
--projects
选项,即比如mvn --also-make clean install
+选中 app-1
是无效的,只 buildapp-1
。 -
IDEA 有一个Delegate build and run actions to Maven设置,勾选后效果大概是 Run/Debug 时跑
--also-make
,即 build 整个依赖链。因为我们并不是每次都要 build 整个依赖链,这选项还是不打开比较好,浪费的时间更多。 -
有时我们改了两个模块,先 build 上层的模块,会报“找不到(下层模块的)符号”,但是我们在编辑器里能看到代码没报红色错误。这就是因为我们忘了先 build 下层模块,而不该判断为 IDEA 的索引缓存又坏了,进而花大量时间重新缓存索引。