前言

ShardingSphere-JDBC 5.3版本开始有一个大变动,不再像以往一样提供ShardingSphere Spring Boot Starter 的起步依赖,而是推荐使用者通过ShardingSphereDriver来完成接入。

这使得 ShardingSphere-JDBC 更加的轻盈化,不再需要考虑兼容 Spring 或是 SpringBoot ,让开发者更关注功能的核心,而不是对生态的支持。

但这也有带来一些不好的点,例如以往可以使用${}作为占位符引用其它变量,在新版本中由于不依赖 Spring 环境,这类变量不受 Spring 管理,就不生效了。

看了下官方文档,新版本的变量仅支持系统变量或者是环境变量,而我们的配置大部分都维护在 SpringBoot 的配置文件中(或者是依赖的配置中心上)

大致翻了翻 ShardingSphere-JDBC 的源码,发现还是提供了不少的 SPI,可以做这块功能的扩展。于是乎,自己动手实现一个。

实现思路

翻了翻ShardingSphereDriver的实现,核心逻辑org.apache.shardingsphere.driver.ShardingSphereDriver#connect中调用了org.apache.shardingsphere.driver.jdbc.core.driver.DriverDataSourceCache#get方法,这个方法里的核心逻辑调用了org.apache.shardingsphere.driver.jdbc.core.driver.DriverDataSourceCache#createDataSource的逻辑。对配置的读取以及数据库的初始化,就是从这儿开始的

其中ShardingSphereURLLoadEngine用于解析 url 地址并读取配置,而 YamlShardingSphereDataSourceFactory.createDataSource用于根据配置生成数据库对象。我们需要处理配置,所以关心的是ShardingSphereURLLoadEngine的实例化以及其loadContent的方法。

  • loadContent

    这个方法很简单,其实只有两行,一个是通过`ShardingSphereURLLoader`的实现读取配置文件数据,一个是调用`org.apache.shardingsphere.infra.url.core.arg.URLArgumentLineRender#render`对配置文件数据做变量替换,其中`org.apache.shardingsphere.infra.url.core.arg.URLArgumentPlaceholderType`是一个枚举类,里面有三个类型,分别代表无,环境变量,系统变量

    看起来后一个比较满足我们的需要,但可惜org.apache.shardingsphere.infra.url.core.arg.URLArgumentLineRender#render的方法里,调用的替换逻辑org.apache.shardingsphere.infra.url.core.arg.URLArgumentLine#replaceArgument写死了两种类型,也没有提供可供扩展的 SPI。

    看来这条路行不通,除非是改源码自己实现。

  • ShardingSphereURLLoadEngine的实例化.

    ShardingSphereURLLoadEngine的实例化使用了 SPI 机制,根据 url的sourceType来决定实例化的ShardingSphereURLLoader对象。结合刚才loadContent的分析,我们可以自实现一个ShardingSphereURLLoader的逻辑,在其 load方法中,提前将 Spring 管理的变量进行替换。

因此,实现思路上,我们可以通过ShardingSphere-JDBC 的 SPI 机制,自定义一个ShardingSphereURLLoader的实现类,在里面将 Spring 管理的变量进行替换,来达到这个效果。

那对于自定义的ShardingSphereURLLoader的实现类,我们怎么能读取到Spring的上下文,来实现替换呢?

我们先看看 Spring 自己是怎么实现的。

在Spring Context中,有一个org.springframework.boot.context.properties.bind.PlaceholdersResolver的接口,用于定义如何将带有占位符的值转换为实际业务值,其中有一个实现是org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver,就是用于解析占位符并填充值的核心逻辑。

因此,我们需要在自定义的ShardingSphereURLLoader的实现类中,组合一个PropertySourcesPlaceholdersResolver对象,来实现我们的需求。

怎么得到PropertySourcesPlaceholdersResolver对象呢,看了看实例化方法,最简单的方式是能传入 Spring Context 的 Environment。

接下来,需要找找怎么得到org.springframework.core.env.Environment

在 SpringBoot 启动的过程中,会有org.springframework.core.env.Environment的实例化过程,在实例化且配置后,SpringBoot 提供了一个 listener 的回调,用于通知关心 SpringBoot 启动阶段的实例,具体逻辑在org.springframework.boot.SpringApplication#prepareEnvironment

所以我们可以基于这个回调,拿到Environment

那怎么样可以自定义这个回调呢?在org.springframework.boot.SpringApplication#getRunListeners中可以看到,listener是通过Spring的SPI得到的。

因此,我们需要通过Spring的SPI,实例化一个org.springframework.boot.SpringApplicationRunListener的对象,从而接收SpringBoot启动时的回调,得到Environment,继而一步步完成ShardingSphereURLLoader的实例化。

当然,我们也可以通过其他方式获得Environment,例如Spring Context提供的org.springframework.context.EnvironmentAware接口,但这么做需要保证加载的顺序性。因为数据库连接池的创建需要依赖ShardingSphereDriver的实例化,ShardingSphereDriver的实例化需要依赖ShardingSphereURLLoadEngine的实例化。而我们此次的改动,会让ShardingSphereURLLoadEngine的实例化采用我们自定义ShardingSphereURLLoader的实例,但这个实例需要依赖Environment实例化。因此,需要保证我们自定义的类,其实例化时可以拿到Environment。也就是需要在数据库(Datesource)实例化时,ShardingSphereURLLoader已经准备完成。

实现方式

总结一下刚才的思路分析,总体实现方式为:

  • 通过 Spring 的 SPI,实例化SpringApplicationRunListener的对象得到Environment
  • 通过Environment,实例化PropertySourcesPlaceholdersResolver,提供给自定义的ShardingSphereURLLoader使用
  • 通过 ShardingSphere 的 SPI,实例化我们自定义的ShardingSphereURLLoader,在逻辑中通过PropertySourcesPlaceholdersResolver进行变量替换,从而实现该功能。

可以把两部分逻辑做合并,得到以下类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 自定义SpringPathUrLLoader实现类,用于在ShardingSphere中加载Spring配置文件中的数据源信息
*
* @author xYohn
* @date 2024/9/25
*/
public class SpringPathUrLLoader implements ShardingSphereURLLoader, SpringApplicationRunListener {

private static PlaceholdersResolver propertySourcesPlaceholdersResolver;

public SpringPathUrLLoader() {
}

public SpringPathUrLLoader(SpringApplication application, String[] args) {
}

@Override
@SneakyThrows(IOException.class)
public String load(String configurationSubject, Properties queryProps) {
try (InputStream inputStream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(configurationSubject)) {
Objects.requireNonNull(inputStream);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
return reader.lines().map(e -> propertySourcesPlaceholdersResolver.resolvePlaceholders(e))
.map(Object::toString).collect(Collectors.joining(System.lineSeparator()));
}
}
}

@Override
public String getType() {
return "springPath:";
}


@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
SpringPathUrLLoader.setEnvironment(environment);
}

private static void setEnvironment(Environment environment) {
propertySourcesPlaceholdersResolver = new PropertySourcesPlaceholdersResolver(environment);
}
}

这里有一个注意点,ShardingSphereURLLoader 的实例化是无参的,而SpringApplicationRunListener需要org.springframework.boot.SpringApplicationargs,所以需要准备两个构造方法给 SPI 使用。

接着我们在实现 SPI 的登记

  • 关于 Spring 的,可以使用META-INF/spring.factories 的形式,或是新版本中使用 Java SPI 标准的方式

    1
    org.springframework.boot.SpringApplicationRunListener=io.github.xyohn.shardingsphereloader.SpringPathUrLLoader
  • 关于 ShardingSphere 的,定义在META-INF/services/org.apache.shardingsphere.infra.url.spi.ShardingSphereURLLoader

    1
    io.github.xyohn.shardingsphereloader.SpringPathUrLLoader

然后在配置文件中,对于数据库地址的定义,使用jdbc:shardingsphere:springPath:的前缀做定义。

完事儿。

碎碎念

发现 ShardingSphere 对自己的介绍现在是:

Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。

看到这个任意数据库转换为分布式数据库,感慨良多。

曾经认识 ShardingSphere 是因为分库分表的需求,后来随着分布式数据库的逐步流行,也让我越发的觉得,这一类从应用层面产生的业务,确实应该下沉到数据库层,又或是中间多一层。

因为他的原理,他的理念,是大差不差的,更大的区别主要还是应用层使用的时候,配置上的不同。

把他作为一个中间件,又或是数据库的一部分,对应用透明,减少应用的复杂度,对应用的后期维护也更友好。

但分布式数据库,好像还并没有能完全的走入每个项目。但项目中许多许多的问题,在我看来是分布式数据库都能解决的。

因为他贵?又或者是,技术的发展和转型,需要时间一步步沉淀积累和转变。

换回话说,像 ShardingSphereJDBC 这类应用层改造的组件,或是 ShardingSphereProxy/Mycat 这类的中间层服务,随着分布式数据库的逐步普及,未来他们会逐步消失吗?

不知道,又或者,到时候会有其他的业务场景产生。

汽车行业这几年,技术的革新,政策的支持,新能源车的比例越来越高,人们开始逐步了解和接触新能源车,享受着新技术带来的红利。而传统燃油车,也越发开始出现颓态。

在这个行业的变动里,许多企业算是踩准了时机,纷纷发力,在逐步实现弯道超车。

传统燃油车的时代,海外的品牌更加驰名,技术底蕴也更加雄厚;而新能源车时代,国内的品牌逐个发力,试图颠覆行业。

这又让我想到了现在的传统数据库和分布式数据库。传统数据库行业里,大家用来用去无不都是那些头部品牌的产品。但分布式数据库的赛道,还算做是新赛道,也有很多国内的品牌开始展露头角,甚至也打出了自己的一片天地。

随着业务的发展,随着自主可控的要求,不知道在这个新赛道上,有没有能抓住这次变革的产品,成为冲出来的黑马。

当然,骗补的那些,就还是算了吧。我还是坚信,良币会驱逐劣币的。