背景
众所周知,IntelliJ IDEA 是 Java 领域常用的开发工具之一,IDEA Ultimate(旗舰版)或其他例如 DataGrip 等 Intellij 平台的工具都集成了对数据库的访问能力。
但是,对于做了分库分表的项目,直接使用 IDEA 连接数据库只能访问具体的库、表。假如开发测试过程中,需要执行一些不带分片条件的查询语句,直连数据库的话只能手动逐个库表查询。
要解决分库分表数据访问的问题,Apache ShardingSphere 还有 ShardingSphere-Proxy,可以通过 Proxy 实现开发测试过程中的分库分表数据访问。但 Proxy 毕竟是独立进程,部署需要额外的资源与操作。考虑到:
- 部分项目只通过 ShardingSphere-JDBC 访问分库分表,没有引入 ShardingSphere-Proxy 的条件。
- 测试环境没有可供 Proxy 部署的资源,每次开发测试使用 IDEA 时,还要再单独本地启动一个 Proxy,相对麻烦。
所以,我们能否直接把 ShardingSphere-JDBC 集成到 IDEA 的数据库工具中,在 IDEA 内直接用逻辑 SQL 访问数据?
步骤
打包 ShardingSphere-JDBC 并在 IDEA 新建自定义数据库驱动
本文 JAR 的打包可以参考:https://github.com/TeslaCN/shardingsphere-all-in-one
对于使用依赖管理的项目,JAR 数量的多少对于开发者来说相对透明,引入依赖改改 POM 文件即可。但如果需要集成 ShardingSphere 的项目没有 Maven 或 Gradle 依赖管理(甚至没有源码),引入 ShardingSphere 可能是一项麻烦的事情。
Apache ShardingSphere 是一个模块拆分非常细的项目。当你只把基本的 ShardingSphere-JDBC 引入项目,你会发现项目依赖 JAR 包瞬间多了 100+ org.apache.shardingsphere 开头的 JAR,且该数量不包含 ShardingSphere 的第三方依赖。
xml
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>5.5.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-test-util</artifactId>
</exclusion>
</exclusions>
</dependency>
本质就是引入 ShardingSphere-JDBC 的所有依赖,通过 maven-shade-plugin 打包为单个 JAR,以便引入 IDEA。
Apache ShardingSphere 官方提供了 ShardingSphere-JDBC 二进制包 的下载,但该包内 仅包含了 ShardingSphere-JDBC 的 JAR,不包括所有第三方依赖 ,所以这个包的实际应用场景比较受限。
踩坑:报错 SPI 找不到实现类
当然,事情不会那么顺利。当我们把配置文件准备好,填写了正确的 ShardingSphere JDBC URL 后,测试连接发现报错找不到 SPI 实现类。
此处使用的是绝对路径指定配置文件,参考文档:
jdbc:shardingsphere:absolutepath:/home/wuweijie/dev/shardingsphere/config-sharding.yaml
打开 IDEA 日志,发现如下报错。
2024-08-11 21:50:38,554 [22072396] WARN - #c.i.d.d.BaseDatabaseErrorHandler$UnknownErrorInfo - SPI-00001: No implementation class load from SPI 'org.apache.shardingsphere.infra.url.spi.ShardingSphereURLLoader' with type 'absolutepath:'.
org.apache.shardingsphere.infra.spi.exception.ServiceProviderNotFoundException: SPI-00001: No implementation class load from SPI 'org.apache.shardingsphere.infra.url.spi.ShardingSphereURLLoader' with type 'absolutepath:'.
at org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader.lambda$getService$1(TypedSPILoader.java:110)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader.lambda$getService$2(TypedSPILoader.java:110)
at java.base/java.util.Optional.orElseGet(Optional.java:364)
at org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader.getService(TypedSPILoader.java:110)
at org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader.getService(TypedSPILoader.java:97)
at org.apache.shardingsphere.infra.url.core.ShardingSphereURLLoadEngine.<init>(ShardingSphereURLLoadEngine.java:39)
at org.apache.shardingsphere.driver.jdbc.core.driver.DriverDataSourceCache.createDataSource(DriverDataSourceCache.java:56)
at org.apache.shardingsphere.driver.jdbc.core.driver.DriverDataSourceCache.lambda$get$0(DriverDataSourceCache.java:50)
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
at org.apache.shardingsphere.driver.jdbc.core.driver.DriverDataSourceCache.get(DriverDataSourceCache.java:50)
at org.apache.shardingsphere.driver.ShardingSphereDriver.connect(ShardingSphereDriver.java:71)
at com.intellij.database.remote.jdbc.helpers.JdbcHelperImpl.connect(JdbcHelperImpl.java:790)
at com.intellij.database.remote.jdbc.impl.RemoteDriverImpl.connect(RemoteDriverImpl.java:153)
难道是我打的 shade 包出问题了?
检查 JAR 包发现,SPI 的 services 文件和 absolutepath 的 class 文件都在。
于是搜了下 IDEA 加载 SPI 的问题,发现 IDEA 的插件也有无法加载 SPI 实现类的问题:ServiceLoader issues and locating folders inside the plugin
大致意思:SPI 不生效是因为 IDEA 使用了独立的类加载器加载插件导致的。
那这个问题也不是完全无解,数据库插件的 Expert options
中可以去除勾选 Isolate class path
。
去除勾选后 SPI 找不到实现类的问题就解决了。
小结:当项目集成 ShardingSphere 后报错找不到 SPI 时,关注一下与类加载器是否有关系。
再踩坑:报错 StackOverflowError
查 IDEA 日志发现,爆栈的错误信息如下,明显是日志实现发生了问题。
Caused by: java.lang.StackOverflowError
at org.slf4j.jul.JDK14LoggerAdapter.log(JDK14LoggerAdapter.java:167)
at org.slf4j.bridge.SLF4JBridgeHandler.callLocationAwareLogger(SLF4JBridgeHandler.java:221)
at org.slf4j.bridge.SLF4JBridgeHandler.publish(SLF4JBridgeHandler.java:303)
at java.logging/java.util.logging.Logger.log(Logger.java:983)
at org.slf4j.jul.JDK14LoggerAdapter.innerNormalizedLoggingCallHandler(JDK14LoggerAdapter.java:156)
at org.slf4j.jul.JDK14LoggerAdapter.log(JDK14LoggerAdapter.java:172)
at org.slf4j.bridge.SLF4JBridgeHandler.callLocationAwareLogger(SLF4JBridgeHandler.java:221)
at org.slf4j.bridge.SLF4JBridgeHandler.publish(SLF4JBridgeHandler.java:303)
at java.logging/java.util.logging.Logger.log(Logger.java:983)
at org.slf4j.jul.JDK14LoggerAdapter.innerNormalizedLoggingCallHandler(JDK14LoggerAdapter.java:156)
at org.slf4j.jul.JDK14LoggerAdapter.log(JDK14LoggerAdapter.java:172)
at org.slf4j.bridge.SLF4JBridgeHandler.callLocationAwareLogger(SLF4JBridgeHandler.java:221)
at org.slf4j.bridge.SLF4JBridgeHandler.publish(SLF4JBridgeHandler.java:303)
at java.logging/java.util.logging.Logger.log(Logger.java:983)
此时笔者想起,IDEA 自从 2022.1 版本起使用 java.util.logging 代替第三方日志实现:Removing log4j from the IntelliJ Platform
ShardingSphere 内部使用的 Guava EventBus 默认以 java.util.logging
作为日志输出框架,为了将 EventBus 日志与 ShardingSphere-Proxy 的日志输出统一,ShardingSphere 通过 jul-to-slf4j 将日志输出统一到 slf4j 了。由于笔者当时没有考虑到与 ShardingSphere-JDBC 集成的项目可能会把 java.util.logging
作为日志输出框架,jul-to-slf4j 直接应用到了 infra 类上面(对 JDBC 与 Proxy 都生效)。
Bridges error logs which printed by ShardingSphereEventBus to slf4j #17541
由此引发的问题:假如集成 ShardingSphere 的项目本身使用 java.util.logging
作为日志框架,集成就会发生日志实现冲突,可能发生的问题包括但不限于本文中的 StackOverflowError
。
笔者针对该问题提交的 PR 已被合并,将随 ShardingSphere 下一版本发布:
Make jul-to-slf4j work in ShardingSphere-Proxy only #32347
集成初步完成
Test Connection
操作成功后,就可以在 Console 中执行逻辑 SQL 了。
打开 IDEA 的日志,可以在日志中找到逻辑 SQL 与实际执行的物理 SQL。
之所以说集成是"初步"完成的原因,是 ShardingSphere-JDBC 对元数据查询的支持有限,可能没有办法像直连数据库一样在 IDEA 中正确展示已有的数据库和表。
后续笔者会进一步研究 IDEA 加载数据源的库表的实现,尝试让 IDEA 能够加载 ShardingSphere-JDBC 的逻辑库与逻辑表。