跳转到内容

JQuick-Connector

JQuick-Connector 解决的是一个很具体的问题:同一个业务方法里,数据可能来自 MySQL 的一行记录、运营发来的一份 CSV、上游接口的一段 JSON,以及 Redis 里的一个缓存键。它们的访问方式本来毫不相干,取出来之后却要被拼成同一张表。

JQuick-Connector 把这些数据源统一收敛成一条查询语句,执行结果是一个内存数据集 JQuickDataSet——列有名字和类型,行可以按名字取值,还能继续过滤、排序、分组、关联、转 Bean。也就是说,数据源之间的差异被挡在查询语句里,进入业务代码的只有一张表。

SELECT
field(id) -> userId:Long,
path('$.profile.city') -> city:String
FROM 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 开源协议:可自由用于商业与个人项目。
  • 报表取数:同一张报表需要合并数据库、Excel 补录数据与外部接口数据,先各自查成数据集,再 concat / innerJoin。
  • 数据迁移与核对:从 CSV 或 Excel 读入,与库中记录比对,差异行直接打印。
  • 运营后台导入:用户上传的表格先落成数据集,做字段校验与转换后再入库。
  • 多租户配置读取:库名、账号由租户上下文注入,查询语句保持不变。
  • 接口聚合:用 CURL 连接器拉取上游 JSON,按路径取字段,与本地数据合并。

环境要求:

项目 要求
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 上的最新发布版为准。

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 的每一项都是 处理器(来源) -> 目标字段:类型 三段式:

部分 说明
处理器 field(...) 取扁平字段,path('...') 取嵌套字段
来源 field 里写源列名或源字段名;path 里写 JSON 路径表达式
目标字段 数据集里的列名,取数时用这个名字
类型 目标列的 Java 类型,见 数据类型

目标字段可以与来源同名,也可以重命名——这正是把不同数据源对齐到同一套字段的手段:

SELECT
field(id) -> id:Long,
field(user_name) -> userName:String
FROM MYSQL(...)

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。

按列名或字段名直接取值,适用于数据库结果集、CSV、Excel 这类扁平结构。

field(id) -> userId:Long,
field(name) -> userName:String

按 JSON 路径从嵌套结构中取值,适用于 JSON、XML、YAML、Properties 这类有层级的数据。

path('$.user.name') -> name:String,
path('$.department.employees[0].title') -> firstTitle:String

path 的路径表达式由 JQuick-Path 解析,因此根节点 $、属性访问 .、数组下标 [0]、切片、通配符等写法都可直接使用。一个 JSON 文件里既有顶层字段又有嵌套字段时,两种处理器可以混着写:

SELECT
field(id) -> id:String,
field(name) -> name:String,
path('$.manager.title') -> managerTitle:String
FROM 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 之类节点下时用它定位。

类型名 目标 必填参数 可选参数
REDIS Redis host、port password、database、redisKey
CURL HTTP 接口 curl searchPath

CURL 直接复用 jquick-curl 的命令语法,因此请求方法、请求头、请求体都按 curl 的写法表达。

SELECT
field(id) -> id:Long,
field(title) -> title:String,
field(created_time) -> createdTime:Date,
field(is_deleted) -> deleted:Boolean
FROM 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:Date
FROM 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:Double
FROM 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:Object
FROM 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:String
FROM XML(
filepath: 'D:/data/user.xml',
searchPath: '$.users'
)
SELECT
path('$.app.name') -> name:String,
path('$.app.version') -> version:String
FROM YAML(filepath: 'D:/data/config.yml', searchPath: '$')
SELECT
path('$.database.username') -> username:String,
path('$.server.port') -> port:Integer
FROM PROPERTIES(filepath: 'D:/data/config.properties', searchPath: '$')
SELECT
field(name) -> name:String,
field(age) -> age:Long,
field(email) -> email:String
FROM REDIS(
host: '127.0.0.1',
port: 6379,
password: '',
database: 0,
redisKey: 'user:1'
)
SELECT
field(projectId) -> projectId:String,
field(projectName) -> projectName:String
FROM CURL(
curl: `curl -X GET 'http://api.example.com/projects' -H 'Accept: application/json'`,
searchPath: '$.data'
)

查询结束之后,所有数据源的区别就消失了:摆在面前的是一张有列定义、有类型的表。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)。

这些方法都返回新的数据集,原数据集不变,可以链式串起来:

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 按指定列做内连接。

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、用接口方法名绑定,适合查询较多、需要集中管理的项目。写法与 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 为必填属性。

接口方法名与 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 都不成立时生效

数据源不在内置清单里,或者取值逻辑需要特殊处理时,可以自己实现并注册。

自定义数据源:实现 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 一起分发。

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, software
distributed 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 and
limitations under the License.