一个SaaS,但凡涉及客户现场部署场景,打包必然就会面临标准化与定制化两种路径
总有各种各样的原因来强迫架构的演化,不以架构师的意志转移
标准化
首先不管是架构师还是公司,都希望只有一个标准化。
标准化能把高昂的研发成本摊薄到海量用户上,就可以降低边际成本,实现规模化盈利。
开发团队会预埋大量的if-else或策略模式,把所有可能的变化都抽象成配置文件(如YAML、JSON)。用户通过修改配置来“定制”,但这些配置不改变程序本质,只是给同一套代码换了不同的“皮肤”或“流程顺序”
定制化
定制化的核心技术是 “模块化”与“插件化”。
开发者会设计出清晰的“内核”和“外挂”机制。基础功能标准化,但关键的业务逻辑做成可替换的“插件”或“微服务”。定制时,只需重写或新增一个插件模块,而不用动整个底层引擎。
其中插件化,类似DDD中供应商模式。
model层有接口,而具体实现在各个“供应商”
比如发消息,有微信、钉钉、飞书。
总体原则:构建时用 Maven 控制依赖是否打进包;运行时用 Spring 条件装配隔离缺失的类
Maven 层面控制依赖打包
1、把功能依赖声明为 optional
在 common/pom.xml 中,把仅部分客户使用的依赖声明为 optional:
1 | <dependency> |
作用:common 模块本身可以编译,但依赖不会传递到 main 模块
<optional>true</optional> 控制的是依赖传递性,不是“当前模块自己不打这个 jar”
含义:common 模块自己可以使用这个依赖来编译和运行,但不会把这个依赖传递给依赖 common 的其他模块
假设模块结构:
1 | main --> depends on --> common --> javacv-platform (optional=true) |
| 情况 | 不加 <optional> |
加了 <optional>true</optional> |
|---|---|---|
common 编译时 |
能使用 javacv-platform |
能使用 javacv-platform |
main 是否会自动继承 javacv-platform |
会 | 不会 |
如果 start 想要这个依赖 |
什么都不用做 | 必须在 main/pom.xml 里显式声明 |
main 作为 Spring Boot 模块:默认会把所有非 optional 的传递依赖打进 fat jar。如果 common 里的 javacv-platform 是 <optional>true</optional>,main 不会自动继承它,所以最终 fat jar 里不会包含 javacv-platform,除非 start 显式引入。
在 main/pom.xml 按 profile 引入依赖
1 | <profiles> |
<optional> 和 <scope>provided</scope> 都能“不传递给下游”,但它们对打包的影响完全不同。关键在于:
<optional>true</optional>只是一个传递性开关,不改变依赖本身的 scope(默认仍是compile)。它影响的是“下游模块会不会自动继承这个依赖”,不影响“声明模块自己运行时/打包时带不带它”。<scope>provided</scope>改变的是依赖的生命周期范围:编译/测试可见,运行时不可见,普通 jar/war 打包时不带它。但 Spring Boot 的spring-boot-maven-plugin默认会把provided也打进 fat jar(它只默认排除systemscope),所以在本项目的 Spring Boot fat jar 里,provided并不能自动把依赖排除掉。
Spring 官方文档明确说明:
By default, both the
repackageandrungoals will include anyprovideddependencies that are defined in the project. A Spring Boot project should considerprovideddependencies as container dependencies that are required to run the application.
即 spring-boot-maven-plugin 的 repackage 和 run 目标默认就会包含所有 provided 依赖,官方将其视为“运行所需的容器依赖”。
正确排除方式
若需排除特定 provided 依赖,需在插件中显式配置:
1 | <plugin> |
Spring 层面隔离缺失依赖
1 |
|
关键点:
@ConditionalOnClass(FFmpegFrameRecorder.class):classpath 中没有 javacv 类时,该 Bean 不会被创建,也不会触发类加载。
调用方注入 Optional
1 |
|