JQuick-Connector
JQuick-Connector
Section titled “JQuick-Connector”JQuick-Connector 解决的是一个很具体的问题:同一个业务方法里,数据可能来自 MySQL 的一行记录、运营发来的一份 CSV、上游接口的一段 JSON,以及 Redis 里的一个缓存键。它们的访问方式本来毫不相干,取出来之后却要被拼成同一张表。
JQuick-Connector 把这些数据源统一收敛成一条查询语句,执行结果是一个内存数据集 JQuickDataSet——列有名字和类型,行可以按名字取值,还能继续过滤、排序、分组、关联、转 Bean。也就是说,数据源之间的差异被挡在查询语句里,进入业务代码的只有一张表。
SELECT field(id) -> userId:Long, path('$.profile.city') -> city:StringFROM MYSQL( url: 'jdbc:mysql://localhost:3306/app', username: 'root', password: 'pwd', driver: 'com.mysql.cj.jdbc.Driver', sql: 'select * from users')整套能力由纯 Java 实现,核心是一条类 SQL 语法、一组字段处理器、一套数据源处理器注册表,以及一个可选的 XML 声明式入口。不依赖 Spring,普通 Java 工程与 Spring Boot 工程使用同一套 API。
- 一条语法打通异构数据源:
SELECT ... FROM 类型(参数),换数据源只改FROM这一段。 - 22 种内置数据源:14 种关系型数据库、6 种文件格式、Redis 与 HTTP 接口,全部通过 SPI 自动注册。
- 两种字段处理器:
field()取扁平结构的列,path()按 JSON 路径取嵌套字段,两者可在同一条语句里混用。 - 强类型映射:
Integer、Long、Float、Double、String、Boolean、Date、Object八种类型,取数时按声明类型转换。 - 上下文变量:
{name}占位符配合JContext,把账号、库名、租户等运行期参数从语句里挪出去。 - 内存数据集:
JQuickDataSet自带过滤、排序、分页、分组聚合、并集与内连接,也支持toMapList()/toBeanList()。 - XML 声明式配置:把查询写进
jquick-connector.xml,Service 接口用方法名绑定,支持if/foreach/choose动态拼接。 - 可扩展:自定义数据源实现
JQuickConnectorHandler,自定义取值逻辑实现JQuickConnectorProcessor,注册后即可在语句里使用。 - Apache-2.0 开源协议:可自由用于商业与个人项目。
适用业务场景
Section titled “适用业务场景”- 报表取数:同一张报表需要合并数据库、Excel 补录数据与外部接口数据,先各自查成数据集,再
concat/innerJoin。 - 数据迁移与核对:从 CSV 或 Excel 读入,与库中记录比对,差异行直接打印。
- 运营后台导入:用户上传的表格先落成数据集,做字段校验与转换后再入库。
- 多租户配置读取:库名、账号由租户上下文注入,查询语句保持不变。
- 接口聚合:用
CURL连接器拉取上游 JSON,按路径取字段,与本地数据合并。
Maven依赖
Section titled “Maven依赖”环境要求:
| 项目 | 要求 |
|---|---|
| JDK | Java 8 或更高版本 |
| 构建工具 | Maven 3.6+ 或 Gradle 7+ |
| 数据源 | JDBC 数据源需自备对应驱动;文件类无需额外依赖 |
| 底层依赖 | Javelin Core、JQuick Path、JQuick JSON、JQuick XML 代理、Jackson,均为纯 Java |
<dependency> <groupId>io.github.paohaijiao</groupId> <artifactId>jquick-connector</artifactId> <version>2.1.0</version></dependency>版本号请以 Maven Central 上的最新发布版为准。
Gradle依赖
Section titled “Gradle依赖”implementation 'io.github.paohaijiao:jquick-connector:2.1.0'一次调用只需要两行:工厂执行语句,数据集接结果。
import com.github.paohaijiao.factory.JQuickConnectorFactory;import com.github.paohaijiao.statement.JQuickDataSet;
JQuickConnectorFactory factory = new JQuickConnectorFactory();JQuickDataSet dataSet = factory.executeQuery( "SELECT field(id) -> uid:Long, field(name) -> name:String " + "FROM CSV(filepath: 'D:/data/users.csv', split: ',', header: 'true')");
for (int i = 0; i < dataSet.size(); i++) { System.out.println(dataSet.get(i).getLong("uid") + " / " + dataSet.get(i).getString("name"));}executeQuery(String) 的返回类型固定是 JQuickDataSet,取数方式见 内存数据集 JQuickDataSet。
一条连接器查询由 SELECT 与 FROM 两段组成,整体形状贴近 SQL,但 SELECT 里写的是字段映射而不是列名,FROM 里写的是数据源类型和它的连接参数。
SELECT field(源字段) -> 目标字段:数据类型, path('$.json.路径') -> 目标字段:数据类型FROM 数据源类型( 参数名: 参数值, 参数名: 参数值)SELECT:字段怎么取
Section titled “SELECT:字段怎么取”SELECT 的每一项都是 处理器(来源) -> 目标字段:类型 三段式:
| 部分 | 说明 |
|---|---|
| 处理器 | field(...) 取扁平字段,path('...') 取嵌套字段 |
| 来源 | field 里写源列名或源字段名;path 里写 JSON 路径表达式 |
| 目标字段 | 数据集里的列名,取数时用这个名字 |
| 类型 | 目标列的 Java 类型,见 数据类型 |
目标字段可以与来源同名,也可以重命名——这正是把不同数据源对齐到同一套字段的手段:
SELECT field(id) -> id:Long, field(user_name) -> userName:StringFROM MYSQL(...)FROM:数据源怎么连
Section titled “FROM:数据源怎么连”FROM 后面是数据源类型加一对括号,括号内是 参数名: 参数值 列表,逗号分隔。参数值的写法决定了它如何被解析:
| 写法 | 含义 | 示例 |
|---|---|---|
| 单引号字符串 | 普通字符串参数 | url: 'jdbc:mysql://...' |
| 数字 / 布尔 | 直接量 | port: 3306 |
| 花括号变量 | 从 JContext 取值 |
password: {dbPassword} |
| 反引号 | 原样文本,用于 curl 命令 | curl: `curl -X GET '...'` |
把账号密码写死在语句里显然不合适。JContext 是一个 Map<String, Object>,构造工厂时传入,语句里用 {name} 引用:
import com.github.paohaijiao.factory.JQuickConnectorFactory;import com.github.paohaijiao.param.JContext;
JContext context = new JContext();context.put("dbUser", "report_reader");context.put("dbPassword", "******");
JQuickConnectorFactory factory = new JQuickConnectorFactory(context);JQuickDataSet dataSet = factory.executeQuery( "SELECT field(id) -> id:Long FROM MYSQL(" + "url: 'jdbc:mysql://localhost:3306/app', " + "username: {dbUser}, password: {dbPassword}, " + "driver: 'com.mysql.cj.jdbc.Driver', " + "sql: 'select * from users')");同一个 JContext 也可以直接用于只解析不执行的场景——JQuickConnectorQueryParser 把语句解析成 JQuickConnectorParsedQuery,便于在真正连接数据源之前做校验或审计。
连接器把「取值」抽象成处理器,当前内置两个:field 与 path。
field:扁平字段
Section titled “field:扁平字段”按列名或字段名直接取值,适用于数据库结果集、CSV、Excel 这类扁平结构。
field(id) -> userId:Long,field(name) -> userName:Stringpath:嵌套字段
Section titled “path:嵌套字段”按 JSON 路径从嵌套结构中取值,适用于 JSON、XML、YAML、Properties 这类有层级的数据。
path('$.user.name') -> name:String,path('$.department.employees[0].title') -> firstTitle:Stringpath 的路径表达式由 JQuick-Path 解析,因此根节点 $、属性访问 .、数组下标 [0]、切片、通配符等写法都可直接使用。一个 JSON 文件里既有顶层字段又有嵌套字段时,两种处理器可以混着写:
SELECT field(id) -> id:String, field(name) -> name:String, path('$.manager.title') -> managerTitle:StringFROM JSON(filepath: 'D:/data/dept.json', searchPath: '$.departments')SELECT 中声明的类型决定了数据集列的 Java 类型,也决定了取数时用哪个方法:
| 类型 | Java 类型 | 说明 |
|---|---|---|
Integer |
Integer |
整数 |
Long |
Long |
长整数 |
Float |
Float |
单精度浮点 |
Double |
Double |
双精度浮点 |
String |
String |
文本 |
Boolean |
Boolean |
布尔值 |
Date |
java.util.Date |
日期时间 |
Object |
Object |
复杂结构,通常用于保留 JSON 子树 |
类型名大小写不敏感;声明了未知类型会直接抛出异常,不会静默降级成字符串。
数据源处理器在类加载时通过 SPI 注册,FROM 里的类型名就是注册名。
| 类型名 | 数据库 |
|---|---|
MYSQL |
MySQL |
POSTGRESQL |
PostgreSQL |
ORACLE |
Oracle |
SQLSERVER |
当前版本未注册,见下方说明 |
DB2 |
DB2 |
SYBASE |
Sybase |
SQLITE |
SQLite |
H2 |
H2 |
DERBY |
Derby |
INFORMIX |
Informix |
KINGBASE |
人大金仓 KingbaseES |
DAMENG |
达梦 DM |
HIVE |
Hive |
IMPALA |
Impala |
CLICKHOUSE |
ClickHouse |
GREENPLUM |
当前版本未注册,见下方说明 |
数据库类连接器共用同一套 JDBC 实现,参数也完全一致:
| 参数 | 必填 | 说明 |
|---|---|---|
url |
是 | JDBC 连接串 |
username |
是 | 用户名 |
password |
是 | 密码 |
sql |
是 | 要执行的查询语句 |
driver |
是 | JDBC 驱动类名,如 com.mysql.cj.jdbc.Driver |
| 类型名 | 格式 | 必填参数 | 可选参数 |
|---|---|---|---|
CSV |
CSV | filepath |
split、header、encoding |
EXCEL |
xls / xlsx | filepath |
sheet、header |
JSON |
JSON | filepath |
searchPath、encoding |
XML |
XML | filepath |
searchPath、encoding |
YAML |
YAML | filepath |
searchPath、encoding |
PROPERTIES |
Properties | filepath |
searchPath、encoding |
searchPath 用来指定从哪个节点开始取数据:文件顶层就是数组时可不填,数据挂在 $.data 之类节点下时用它定位。
缓存与 REST
Section titled “缓存与 REST”| 类型名 | 目标 | 必填参数 | 可选参数 |
|---|---|---|---|
REDIS |
Redis | host、port |
password、database、redisKey |
CURL |
HTTP 接口 | curl |
searchPath |
CURL 直接复用 jquick-curl 的命令语法,因此请求方法、请求头、请求体都按 curl 的写法表达。
各数据源示例
Section titled “各数据源示例”SELECT field(id) -> id:Long, field(title) -> title:String, field(created_time) -> createdTime:Date, field(is_deleted) -> deleted:BooleanFROM MYSQL( url: 'jdbc:mysql://localhost:3306/app', username: 'root', password: 'password', driver: 'com.mysql.cj.jdbc.Driver', sql: 'select * from article')SELECT field(id) -> id:Long, field(name) -> name:String, field(salary) -> salary:Double, field(birth_date) -> birthDate:DateFROM CSV( filepath: 'D:/data/staff.csv', split: ',', header: 'true')header 为 true 时首行作为列名;为 false 时自动生成 column1、column2 这样的列名。
SELECT field(id) -> id:Long, field(name) -> name:String, field(amount) -> amount:DoubleFROM EXCEL( filepath: 'D:/data/orders.xlsx', sheet: 'Sheet1', header: 'true')SELECT field(id) -> id:String, field(name) -> name:String, path('$.manager.title') -> managerTitle:String, field(manager) -> manager:ObjectFROM JSON( filepath: 'D:/data/dept.json', searchPath: '$.departments')Object 类型的字段会保留原始子结构,需要时可以在 Java 侧继续解析。
SELECT path('$.attributes.id[0]') -> id:Long, path('$.user.name[0]') -> name:String, path('$.user.email[0]') -> email:StringFROM XML( filepath: 'D:/data/user.xml', searchPath: '$.users')YAML 与 Properties
Section titled “YAML 与 Properties”SELECT path('$.app.name') -> name:String, path('$.app.version') -> version:StringFROM YAML(filepath: 'D:/data/config.yml', searchPath: '$')SELECT path('$.database.username') -> username:String, path('$.server.port') -> port:IntegerFROM PROPERTIES(filepath: 'D:/data/config.properties', searchPath: '$')SELECT field(name) -> name:String, field(age) -> age:Long, field(email) -> email:StringFROM REDIS( host: '127.0.0.1', port: 6379, password: '', database: 0, redisKey: 'user:1')REST 接口
Section titled “REST 接口”SELECT field(projectId) -> projectId:String, field(projectName) -> projectName:StringFROM CURL( curl: `curl -X GET 'http://api.example.com/projects' -H 'Accept: application/json'`, searchPath: '$.data')内存数据集 JQuickDataSet
Section titled “内存数据集 JQuickDataSet”查询结束之后,所有数据源的区别就消失了:摆在面前的是一张有列定义、有类型的表。JQuickDataSet 提供的方法大致分四类——读、筛、算、转。
| 方法 | 返回 | 说明 |
|---|---|---|
getColumnNames() |
List<String> |
全部列名 |
getColumns() |
List<JQuickColumnMeta> |
列元信息,含名称、类型、来源字段 |
getRows() |
List<JQuickRow> |
全部行 |
size() / isEmpty() |
int / boolean |
行数、是否为空 |
get(int) |
JQuickRow |
按下标取行 |
first() / last() |
JQuickRow |
首行、末行 |
getColumnValues(name) |
Stream<Object> |
某列的全部值 |
JQuickRow 本身实现了 Map<String, Object>,因此 row.get("name") 拿到的是原始对象;需要类型化取值时用它的转换方法:
JQuickRow row = dataSet.first();String name = row.getString("name");Long id = row.getLong("id");Date created = row.getDate("created_time");boolean has = row.hasColumn("email");Object raw = row.getObject("extra", null); // 带默认值常用的还有 getInt、getDouble、getBoolean、getDate(name, format)、getAsConverted(name, Class),以及判断空值的 isNull(name)。
过滤、排序、分页
Section titled “过滤、排序、分页”这些方法都返回新的数据集,原数据集不变,可以链式串起来:
JQuickDataSet result = dataSet .filter(row -> row.getDouble("amount") > 1000) .orderBy("amount", false) .limit(20);| 方法 | 说明 |
|---|---|
filter(Predicate<JQuickRow>) |
按行过滤 |
map(Function<JQuickRow, JQuickRow>) |
逐行改写 |
orderBy(column) / orderBy(column, ascending) / orderBy(String...) |
单列或多列排序 |
limit(n) / skip(n) |
分页 |
subDataset(start, end) |
按下标区间截取 |
distinct() / distinctBy(String...) |
整行去重 / 按列去重 |
select(String...) / drop(String...) |
只保留或去掉指定列 |
addColumn(name, type, source, extractor) |
派生新列 |
Map<Object, List<JQuickRow>> byCity = dataSet.groupBy("city");double total = dataSet.sum("amount");double avg = dataSet.avg("amount");Double max = dataSet.max("amount");Double min = dataSet.min("amount");long big = dataSet.count(row -> row.getDouble("amount") > 1000);需要自定义聚合时用 groupByAndAggregate(column, Collector),直接传 JDK 的收集器。单个列的统计摘要可用 getStatistics(column),返回包含计数、求和、均值、最值的 Map。
JQuickDataSet all = january.concat(february);JQuickDataSet joined = orders.innerJoin(users, "user_id", "id");concat 纵向拼接,innerJoin 按指定列做内连接。
转成 Map 或 Bean
Section titled “转成 Map 或 Bean”List<Map<String, Object>> maps = dataSet.toMapList();List<UserVO> users = dataSet.toBeanList(UserVO.class);toBeanList 按列名与属性名匹配填充,适合直接把查询结果交给上层。
dataSet.printTable(); // 控制台表格dataSet.printTable(20); // 只打印前 20 行dataSet.printSummary(); // 行列数与列类型摘要排查字段映射问题时,printTable() 通常比打断点更快——列名不对、类型没转过来,一眼就能看见。
XML 规则与接口绑定
Section titled “XML 规则与接口绑定”把查询语句写进 XML、用接口方法名绑定,适合查询较多、需要集中管理的项目。写法与 MyBatis 的 Mapper 类似。
在 src/main/resources 下创建 jquick-connector.xml:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE connectors PUBLIC "-//PAOHAIJIAO//DTD JQUICK CONNECTOR 1.0//EN" "classpath:paohaijiao/dtd/Jquick-connector.dtd">
<connectors namespace="com.example.service.UserConnectorService">
<connector name="findById" returnClass="com.github.paohaijiao.statement.JQuickDataSet"> SELECT field(id) -> id:Long, field(name) -> name:String, field(email) -> email:String FROM MYSQL( url: 'jdbc:mysql://localhost:3306/user_db', username: 'root', password: 'password', driver: 'com.mysql.cj.jdbc.Driver', sql: 'select * from user where id = {userId}' ) </connector>
<connector name="readConfig" returnClass="com.github.paohaijiao.statement.JQuickDataSet"> SELECT path('$.database.username') -> username:String, path('$.server.port') -> port:Integer FROM JSON(filepath: 'D:/data/config.json', searchPath: '$.settings') </connector>
</connectors><connectors> 的 namespace 是 Service 接口的全限定名;每个 <connector> 的 name 对应接口方法名,returnClass 为必填属性。
Service 接口与代理
Section titled “Service 接口与代理”接口方法名与 name 一一对应,参数用 @Param 绑定语句里的变量占位符:
package com.example.service;
import com.github.paohaijiao.statement.JQuickDataSet;import com.github.paohaijiao.xml.param.Param;
public interface UserConnectorService {
JQuickDataSet findById(@Param("userId") Long userId);
JQuickDataSet readConfig();}调用时用工厂生成代理:
import com.github.paohaijiao.xml.JQuickConnectorXmlParseFactory;import com.github.paohaijiao.xml.factory.JQuickFactory;import com.github.paohaijiao.xml.factory.JQuickXmlFactory;import com.github.paohaijiao.xml.handler.JQuickParseHandler;
JQuickParseHandler parser = new JQuickConnectorXmlParseFactory();JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-connector.xml");UserConnectorService service = factory.createApi(UserConnectorService.class);
JQuickDataSet result = service.findById(1L);result.printTable();XML 里支持三种动态元素,用来拼装可变的查询片段。
<if test="..."> 按布尔条件决定是否拼接:
<connector name="search" returnClass="com.github.paohaijiao.statement.JQuickDataSet"> SELECT field(id) -> id:Long, field(name) -> name:String FROM MYSQL( url: 'jdbc:mysql://localhost:3306/db', username: 'root', password: 'pwd', driver: 'com.mysql.cj.jdbc.Driver', sql: 'select * from users where 1 = 1' ) <if test="name != null"> and name = {name} </if></connector><foreach> 遍历集合,常用于 in 条件:
<foreach collection="ids" item="id" open="(" close=")" separator=","> {id}</foreach><choose> / <when> / <otherwise> 处理多分支:
<choose> <when test="type == 1">and type = 1</when> <when test="type == 2">and type = 2</when> <otherwise>and type = 0</otherwise></choose>| 元素 | 属性 | 说明 |
|---|---|---|
<if> |
test(必填) |
条件成立时拼接内部内容 |
<foreach> |
collection、item(必填);open、close、separator(可选) |
遍历集合生成片段 |
<when> |
test(必填) |
分支条件 |
<otherwise> |
无 | 所有 when 都不成立时生效 |
扩展自定义连接器
Section titled “扩展自定义连接器”数据源不在内置清单里,或者取值逻辑需要特殊处理时,可以自己实现并注册。
自定义数据源:实现 JQuickConnectorHandler,在 buildRow(JQuickConnectorParsedQuery) 里把数据读成 List<JQuickRow>,再注册类型名。
import com.github.paohaijiao.factory.JQuickConnectorFactory;
JQuickConnectorFactory.registerConnector("MYSERVICE", new MyServiceHandler());自定义字段处理器:实现 JQuickConnectorProcessor,在 SELECT 里用新的处理器名取数。
JQuickConnectorFactory.registerProcessor("header", new MyHeaderProcessor());注册是全局的,注册一次后所有 JQuickConnectorFactory 实例都能识别。实现类也可以按 SPI 规范登记到 META-INF/services,随 jar 一起分发。
- GitHub 仓库:https://github.com/paohaijiao/jquick-connector
- Maven Central:io.github.paohaijiao:jquick-connector
- 当前版本:
2.1.0(要求 JDK 8 或更高版本) - 问题反馈:GitHub Issues
JQuick-Connector 基于 Apache License, Version 2.0 发布。
Copyright (c) [2025-2099] Martin ([email protected])
Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.