码农戏码

新生代农民工的自我修养

0%

SaaS部署标准化与定制化

一个SaaS,但凡涉及客户现场部署场景,打包必然就会面临标准化与定制化两种路径

总有各种各样的原因来强迫架构的演化,不以架构师的意志转移

标准化

首先不管是架构师还是公司,都希望只有一个标准化。

标准化能把高昂的研发成本摊薄到海量用户上,就可以降低边际成本,实现规模化盈利。

开发团队会预埋大量的if-else或策略模式,把所有可能的变化都抽象成配置文件(如YAML、JSON)。用户通过修改配置来“定制”,但这些配置不改变程序本质,只是给同一套代码换了不同的“皮肤”或“流程顺序”

定制化

定制化的核心技术是 “模块化”与“插件化”
开发者会设计出清晰的“内核”和“外挂”机制。基础功能标准化,但关键的业务逻辑做成可替换的“插件”或“微服务”。定制时,只需重写或新增一个插件模块,而不用动整个底层引擎。

其中插件化,类似DDD中供应商模式。

model层有接口,而具体实现在各个“供应商”

比如发消息,有微信、钉钉、飞书。

总体原则:构建时用 Maven 控制依赖是否打进包;运行时用 Spring 条件装配隔离缺失的类

Maven 层面控制依赖打包

1、把功能依赖声明为 optional

common/pom.xml 中,把仅部分客户使用的依赖声明为 optional:

1
2
3
4
5
<dependency>  
<groupId>com.larksuite.oapi</groupId>
<artifactId>oapi-sdk</artifactId>
<optional>true</optional>
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<profiles>
<profile>
<id>customer-a</id>
<dependencies>

<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
</dependency>
</dependencies>
</profile>

<profile>
<id>customer-b</id>

</profile>
</profiles>

<optional><scope>provided</scope> 都能“不传递给下游”,但它们对打包的影响完全不同。关键在于:

  • <optional>true</optional> 只是一个传递性开关,不改变依赖本身的 scope(默认仍是 compile)。它影响的是“下游模块会不会自动继承这个依赖”,不影响“声明模块自己运行时/打包时带不带它”。
  • <scope>provided</scope> 改变的是依赖的生命周期范围:编译/测试可见,运行时不可见,普通 jar/war 打包时不带它。但 Spring Boot 的 spring-boot-maven-plugin 默认会把 provided 也打进 fat jar(它只默认排除 system scope),所以在本项目的 Spring Boot fat jar 里,provided 并不能自动把依赖排除掉。

Spring 官方文档明确说明:

By default, both the repackage and run goals will include any provided dependencies that are defined in the project. A Spring Boot project should consider provided dependencies as container dependencies that are required to run the application.

spring-boot-maven-pluginrepackagerun 目标默认就会包含所有 provided 依赖,官方将其视为“运行所需的容器依赖”。

正确排除方式

若需排除特定 provided 依赖,需在插件中显式配置:

1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>com.aliyun.odps</groupId>
<artifactId>odps-sdk-core</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>

Spring 层面隔离缺失依赖

1
2
3
4
5
6
7
8
9
@Service
@ConditionalOnClass(FFmpegFrameRecorder.class)
public class JavaCvVideoEncoderService implements VideoEncoderService {

@Override
public Map<String, Integer> encodeToMp4(String imageDir, String outputPath, double fps, int bitrate) {

}
}

关键点:

  • @ConditionalOnClass(FFmpegFrameRecorder.class):classpath 中没有 javacv 类时,该 Bean 不会被创建,也不会触发类加载。

调用方注入 Optional

1
2
3
4
5
6
7
8
@RestController
public class ReviseDataController {

private final Optional<VideoEncoderService> videoEncoder;

public ReviseDataController(Optional<VideoEncoderService> videoEncoder) {
this.videoEncoder = videoEncoder;
}