2698 字
约 8 分钟
14
S06-X16 · 集成库的落地点:PDFBox、JSqlParser、Elastic-Job 与 Ip2region

S06-X16 · 集成库的落地点:PDFBox、JSqlParser、Elastic-Job 与 Ip2region

引入一个第三方库,真正花时间的不是"调通 API",而是判断它能不能被你的边界容纳:线程安全不安全、异常面有多大、升级会不会带进传递依赖、数据文件由谁维护。四个库恰好覆盖四类不同的集成形态——文档解析、SQL 改写、分布式调度、离线数据查询。把它们放在一起看,选型判据比单个库的用法更值钱。


1. 先立判据:一个库要过五道关

判据 观察方式 不合格的信号
维护活跃度 提交频率、issue 响应、发布节奏 两年无 release,issue 只剩机器人
线程安全 文档是否声明;源码里有没有共享可变状态 "文档没写"就等于不安全
依赖体积 mvn dependency:tree -Dincludes=groupId 一个工具库拖进整个日志/序列化栈
异常面 抛哪些受检异常,是否吞异常 大量 catch (Exception e) {}
数据/资源归属 是否需要外部数据文件、字典、模型 文件升级路径不明,靠人工替换
$ mvn dependency:tree -Dincludes=org.apache.pdfbox,com.github.jsqlparser \
    -DoutputFile=/tmp/tree.txt && cat /tmp/tree.txt

看的是传递依赖:文档解析库常带字体与图像库,SQL 解析库很轻,调度框架会带注册中心客户端——对启动时间与类加载的影响差一个量级(典型量级)。


2. PDFBox:文本抽取、坐标与那些"抽不出来"的页

PDFBox 的价值是在不整份解密的前提下按结构读取:页面树、内容流、字体资源、注释。常见用法是抽文本,工程上要处理的是"抽不出来怎么办"。

try (PDDocument doc = PDDocument.load(new File("/data/in/report.pdf"))) {
    PDFTextStripper stripper = new PDFTextStripper();
    stripper.setSortByPosition(true);      // 按坐标排序,避免多栏错行
    stripper.setStartPage(1);
    stripper.setEndPage(3);
    String text = stripper.getText(doc);
    // 版面坐标:拿到每段文字在页面上的位置
    PDFTextStripperByArea area = new PDFTextStripperByArea();
    area.addRegion("header", new Rectangle(0, 0, 595, 90));
    area.extractRegions(doc.getPage(0));
}
现象 成因 处理方向
抽出的文字全是乱码 字体是子集化嵌入,没有 ToUnicode 映射 接受限制,或走 OCR
文字顺序错乱 内容是绝对定位绘制,无阅读顺序 setSortByPosition(true),仍乱则按区域抽
完全抽不到文本 扫描件,页面只有一张图 走 OCR 链路,PDFBox 只负责渲染
页面旋转 90° 存在 /Rotate 属性 按 getRotation() 校正坐标
中文缺字 字体子集不含该字形 渲染图像后 OCR,或换来源

线程安全要写死:PDDocument 不是线程安全的,一个实例只能单线程访问,PDFTextStripper 也不该跨线程复用。并发抽取应每任务一个 PDDocument,用信号量控制同时打开数——内存占用与页数、图片数正相关,典型量级几十 MB 到数百 MB。


3. JSqlParser:在 SQL 进数据库之前改写它

JSqlParser 把 SQL 解析成 AST,能遍历、能改、能还原成文本。落地点三个:分页改写、字段脱敏、强制索引与租户条件注入。

String sql = "select id, name, phone from t_user where status = 1";
Statement stmt = CCJSqlParserUtil.parse(sql);
Select select = (Select) stmt;

// 1) 包一层分页:select * from (原 SQL) t limit ? offset ?
PlainSelect body = (PlainSelect) select.getSelectBody();
PlainSelect page = new PlainSelect();
page.addSelectItems(new AllColumns());
page.setFromItem(new ParenthesedSelect().withSelect(select).withAlias(new Alias("t")));
page.setLimit(new Limit().withRowCount(new LongValue(20)));

// 2) 追加租户条件
Expression tenant = new EqualsTo(
    new Column("tenant_id"), new LongValue(1024));
body.setWhere(new AndExpression(body.getWhere(), tenant));

System.out.println(page);   // 还原为可执行 SQL
改写目标 要动的 AST 节点 容易翻车的点
分页 Limit / Offset、包一层子查询 原 SQL 带 order by 时,外层排序要保留
脱敏 SelectItem 换 Function(如 mask()) select * 难以定位列,必须先展开
强制索引 IndexItem / 厂商扩展语法 各家方言不同,解析器不一定认
租户隔离 Where 上 AndExpression 原 where 为 null 时直接赋值,别拼 and
白名单校验 遍历 Table、Column 子查询与 union 里的表要递归走

工程约束:解析器认的是语法,不是语义。它不会告诉你这条 SQL 在目标库上跑不跑得通,也不会替你处理绑定参数与执行计划。所以改写层只做"结构变换",权限与合法性判断另设一层。另外,同一段 SQL 反复解析是有成本的,典型做法是按 SQL 文本做缓存(注意缓存键要含方言与参数占位符形态)。

// 缓存键必须区分方言与绑定形态
String key = dialect + "|" + sqlText + "|" + (hasPlaceholder ? "?" : "literal");

4. Elastic-Job:分片、弹性与"任务跑在哪台机器上"

Elastic-Job 的模型是三件套:注册中心(ZooKeeper 等)负责协调,作业配置描述分片,执行器按分片索引跑业务。它和"定时触发一次全量"的思路差别在于:任务被切成 N 片,每片只处理数据的一个子集。

elasticjob:
  regCenter:
    serverLists: zk-1:2181,zk-2:2181
    namespace: job-prod
  jobs:
    orderArchive:
      elasticJobClass: com.example.job.OrderArchiveJob
      cron: "0 0 3 * * ?"
      shardingTotalCount: 8
      shardingItemParameters: "0=A,1=B,2=C,3=D,4=E,5=F,6=G,7=H"
      failover: true
      misfire: true
配置 作用 常见误配
shardingTotalCount 分片总数,也是最大并行度上限 设成 100 但只有 4 个实例,分片空转
shardingItemParameters 把分片号映射成业务参数 分片数与参数个数不一致,部分分片无参数
failover 实例宕机后由其他实例接管分片 任务非幂等时,接管会重复处理
misfire 错过调度点是否补跑 补跑堆积,形成雪崩
cron 触发时间 用 * * * * * ? 每小时误写成每分钟

分片的核心是分片键要与数据分布对齐。按 id % shardingTotalCount 分片,改分片数会全量重分布;按时间窗口分片(每片处理一段日期),扩容只是缩短每片的时间段,更安全。

public class OrderArchiveJob implements SimpleJob {
    @Override
    public void execute(ShardingContext ctx) {
        int item = ctx.getShardingItem();
        String param = ctx.getShardingParameter();   // 来自 shardingItemParameters
        archiveOneSlice(item, ctx.getShardingTotalCount());
    }
}

5. Ip2region:离线 IP 归属的取舍

Ip2region 把 IP 段索引与地域数据做成二进制文件(xdb),查询走 btree/binary 算法,本地内存映射,不依赖外部服务。

try (Searcher searcher = Searcher.newWithFileOnly("/data/ip2region.xdb")) {
    String region = searcher.search("203.0.113.7");
    // 返回"国家|区域|省份|城市|ISP"的拼接串
}
模式 内存占用 查询延迟 适用
文件模式 几乎为零 每次查都读文件 低频查询、内存紧张的边缘节点
向量索引缓存 只缓存索引(MB 级) 中 通用场景,性价比最高
全量缓存 整个 xdb 进内存 最低 高频查询,但要评估容器内存配额

两条边界要先说清:一是归属数据会过期,运营商调整 IP 段后需要换 xdb 文件,版本要进发布流程;二是它不是风控手段。机房 IP、代理池、移动网络出口都会让"归属地"失去意义,用它做地域拦截只能当弱信号。IPv6 支持情况按所使用版本的文档核对,别默认可用。


6. 上游生态:版本、活跃度与 GSoC 这类旁证

选库时要回答"这东西还能用几年"。除了看 release,还可以看上游是否参与 GSoC(Google Summer of Code)这类学生项目——有导师、有提案,说明维护者有持续投入的意愿。这只是弱旁证,替代不了发布节奏与 issue 响应。

检查项 命令/入口 用途
是否有新版本 仓库 releases / Maven Central 元数据 判断是否已停更
传递依赖 dependency:tree 判断升级代价
许可证 仓库 LICENSE / pom.xml 影响分发方式
安全公告 依赖扫描(如 OWASP Dependency-Check 类工具) 判断是否需紧急升级
上游参与度 贡献者数、GSoC 项目列表 弱旁证,仅作参考
$ mvn versions:display-dependency-updates -DoutputFile=/tmp/updates.txt
$ grep -E "pdfbox|jsqlparser|elasticjob|ip2region" /tmp/updates.txt

7. 排错:五个"调通了但线上不对"的场景

现象 原因 处理
抽取偶尔 OOM 并发打开大量 PDDocument 加信号量限并发,及时 close()
解析某些 SQL 抛异常 方言语法超出解析器支持范围 降级为字符串拼接兜底,并打点记录样本
分片任务部分不执行 shardingItemParameters 与分片数不一致 补齐参数或改分片数
调度错过时间点不补 misfire 关闭 按业务容忍度打开,并限制补跑窗口
IP 归属明显错误 xdb 文件版本旧 换数据文件,纳入发布流程

8. 边界:这四个库不该承担的事

场景 为什么不该用它 该用什么
复杂版面的扫描件 PDFBox 只做结构与文本,不做识别 OCR 链路 + 版面分析
跨库 SQL 改写 方言差异太大,解析器覆盖不全 各库原生能力,或 ORM 层拦截
秒级触发的高频调度 调度框架的协调开销高于任务本身 进程内调度器 + 队列
精准风控与反欺诈 IP 归属只是弱信号 设备指纹、行为序列、规则引擎
需要强一致的任务分片 分片只是并行手段,不保证一致性 幂等 + 状态表 + 补偿

一句话:库负责把结构读出来,业务负责决定结构怎么用。


10. 自测

  1. 选库时 dependency:tree 回答的是哪一个问题?
  2. PDFBox 抽不出文本但页面明显有内容,最可能是哪种 PDF?
  3. 用 JSqlParser 改写 select * 做脱敏,为什么必须先展开列?
  4. Elastic-Job 的 shardingTotalCount 远大于实例数会发生什么?
  5. Ip2region 的全量缓存模式换来的是什么、付出的是什么?
  6. IP 归属地做地域拦截,为什么只能算弱信号?
S06-X16 · 集成库的落地点:PDFBox、JSqlParser、Elastic-Job 与 Ip2region
https://www.clxhxhhr.top/posts/6020/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。