当前位置: 首页 > news >正文

Google Mug库——一个现代的通用工具库

Google Mug库是我维护的一款开源Java工具库。包含了一些近几年在Google内部的labs代码库中被广泛使用的工具,集成了一些经实践验证很成功也比较成熟了的新工具。

今天我先介绍Mug的StringFormat库。

这个库的初衷是为了解决很多很常见的从字符串中抽取信息的问题。比如,某个文件名会是这样一个人格式 /usrs/{user}/logs/{year}/{month}/{day}/{name}.log。那么,给定一个这个格式的文件名,怎么从中抽取这些占位符对应的值呢?

为什么不用正则表达式?

传统上,大家会用正则来处理这种信息抽取。

java

private static final Pattern LOG_FILE_PATTERN = Pattern.compile(

"/usrs/(?<user>[^/]+)/logs/(?<year>\d{4})/(?<month>\d{2})/(?<day>\d{2})/(?<name>.+)\.log");

Matcher matcher = LOG_FILE_PATTERN.matcher(filePath);

if (matcher.matches()) {

String user = matcher.group("user");

String year = matcher.group("year");

String month = matcher.group("month");

String day = matcher.group("day");

String name = matcher.group("name");

...

}

这样做的好处是:正则嘛,大家都会。坏处呢,正则表达式往往可读性较差,在Java里写有时候是两个反斜线还是四个反斜线也容易搞混了。对复杂的匹配规则,这么做是值得的,但是对上面这种常见的格式固定的抽取,就显得杀鸡用牛刀,代码维护起来就会难一些。

另外,为了效率,正则的Pattern 对象往往要定义成static final来一次性编译regex。但是带来的问题是pattern和具体parse的代码可能会分开得比较远(比如隔上几个翻页)。这样写 group("name")这种的时候, 你可能要上滚去找具体的组名字,如果写错了组名字,或者有时候图省事都不用命名capture group,直接用魔法数索引,编译也不会报错;读代码的时候,尤其是调试的时候,也可能要上下滚动对照pattern和底下的抽取代码的一致性。

还有一个问题一般人可能不会在意,但是如果你的代码要跑在高可用性,高吞吐量的服务器上的话,regex其实是有稳定性的缺陷的。Java的regex实现用的是NFA+回溯,这种实现的特点是它可能对大多数输入都很快,但是对某些特殊输入,或者恶意的regex-dos攻击,可能会造成指数级的“灾难回溯”。真实的例子:

  • Stack Overflow 2016: a regex used to extract comment anchors caused a global outage due to backtracking explosion (postmortem).
  • Cloudflare 2019: 一个有问题的regex造成cpu超负载,大量服务器宕机 (incident report).

用StringFormat抽取格式化信息

这大概算一个80-20问题。对80%的简单但普遍的情况,Google Mug的StringFormat 是一个更方便更安全高效的工具。这个抽取可以用以下代码直观和简单地做到:

java

private static final StringFormat LOG_FILE_FORMAT =

new StringFormat("/usrs/{user}/logs/{year}/{month}/{day}/{name}.log");

LOG_FILE_FORMAT.parse(filePath, (user, year, month, day, name) -> ...);

它直接用我们上面最直观的日常用到的带占位符的格式串,然后直接抽取。返回的是一个Optional<T>,这样就如果格式不匹配就显式返回空,帮助使用者不会忘记处理失败情况。

或者如果你知道这个格式肯定匹配,那么就用 parseOrThrow()。

这么做的好处有:

  1. 格式串直观可读。
  2. 抽取部分代码简洁,不需要依赖魔法数,没有组名字写错的风险。
  3. 库自带ErrorProne的编译期插件,如果你在lambda里,把参数顺序搞错了,比如 (year, month, day, user, name),编译器会报错。 这就让你可以放心地把StringFormat定义成static final,然后在别的地方重用而不需担心一致性问题。
  4. 在运行时,它用的是简单的String.indexOf(), 一般比regex要更高效,也没有回溯问题。

禁用NFA

多说一句。因为对服务器可靠性的考量(还记得前几天的Google全球宕机吗?虽然那个是C++ UB的锅,但是可靠性是大型互联网公司都无法忽视的普遍问题),Google内部已经原则上禁用JDK的regex,因为NFA虽然对平均情况的性能不错,但是遇到某些特殊的输入甚至恶意攻击可能会指数级回溯。

目前谷歌的替代品是用JNI包裹了一个C++的RE2的实现。但是benchmark跑下来,在JNI的边界传递输入输出的代价高昂,所以比如你的输入字符串很大,或者你要用regex来做抽取,效率都不高。

我现在在写一个静态分析,帮助把一些本来没必要用regex的用例迁移到StringFormat或者是Substring上(后者是一个比Apache StringUtils更灵活更强大可读性更好的字符串工具类,支持链式调用的)。比如,"^projects/(?<project>[^/]+)/locations/(?<location>[^/]+)/jobs/(?<job>[^/]+)$" 这种蛋疼的regex完全可以写成:

java

new StringFormat("`projects/{project}/locations/{location}/jobs/{job}`")

.parseOrThrow(input, (project, location, job) -> ...);

高效,易读,没有灾难性回溯。

多次抽取

前面的示例是完整匹配后抽取。你也可以用 scan()方法来实现在字符串里寻找符合这个格式的子串。比如以下代码扫描markdown文件,找到所有的链接:

java

体验AI代码助手

代码解读

复制代码

List<MarkdownLink> links = new StringFormat("[{title}]({url})")

.scan(markdown, (title, url) -> new MarkdownLink(title, url))

.toList();

scan()返回的是一个懒加载的Stream<T>,所以你也可以比如用findFirst(), limit(n),anyMatch()来中途退出而不用付全字符串扫描的代价。

用StringFormat代替String.format()

StringFormat是个双向的API。除了抽取,还支持格式化字符串,支持 format(Object...)方法。

上面提到的编译期插件也用在了format()。比如:

java

体验AI代码助手

代码解读

复制代码

String logFile = LOG_FILE_FORMAT.format(user, year, month, day, name);

跟抽取类似,如果你把参数的个数或者顺序写错了,编译器会报错

对比JDK的String.format(), 如果你有一个格式串要多次使用,那么你可能想要把它定义为 static final 。但是这样一来,在调用String.format() 的时候,就有风险把参数顺序和个数搞错,造成逻辑错误。

而用StringFormat就没有这个问题了。你可以放心地复用private static final的StringFormat常量。从谷歌内部代码情况来看,用StringFormat来做格式化比做抽取还要常见。

你也可以做所谓的rewrite。比如,如果要把user的部分改名字,就可以做:

java

Map<String, String> renamings = ...;

String newFile = LOG_FILE_FORMAT.parseOrThrow(

filePath,

(user, year, month, day, name) ->

LOG_FILE_FORMAT.format(

renamings.get(user), year, month, day, name)));

最后,运行效率上,Java 17以前的String.format()内部用的是正则表达式去parse这个格式串,效率相当低。换用StringFormat.format()后据benchmark大约有几十倍的提升。即使是Java 17之后,StringFormat(预分配成static final的话)也比JDK的快5倍左右。

原文链接:https://juejin.cn/post/7554322871418814499

http://www.jsqmd.com/news/712337/

相关文章:

  • 适配您选型调研智能教育工具,部署可对接专属顾问
  • 如何高效管理ComfyUI扩展:ComfyUI Manager完整指南
  • AI与人类协作在数据科学中的效能评估与实践
  • FPGA在100GbE网络中的关键技术实现与优化
  • Code-A1对抗演化框架:提升代码生成与测试效率
  • Claude Code无缝切换ChatGPT后端:本地代理实现与MCP工具集成
  • Arm AArch64处理器特性寄存器解析与应用实践
  • 别再手动写审批逻辑了!用SpringBoot+Activiti工作流引擎,5步搞定业务流程自动化
  • 低轨卫星C代码功耗优化实战手册(NASA/JAXA/中国空间技术研究院联合验证的5类高危能耗模式)
  • HuggingFace自定义架构开发指南与实战
  • Vibe Coding与LLM:直觉式编程的新范式
  • 告别混乱报表:用SAP会计报表版本(FSV)统一管理资产负债表与利润表,附中国本地化报表配置要点
  • LingBot-Depth在AR场景中的应用:解决玻璃、镜面识别难题
  • 3分钟突破性解决QtScrcpy鼠标点击失效:从权限迷宫到精准控制
  • 别再手动整理了!用Python一键抓取高德地图城市编码与经纬度,生成Excel表格
  • Python操作DXF文件的终极指南:用ezdxf轻松处理CAD图纸
  • 如何高效解决MZmine3命令行认证问题:专业级解决方案指南
  • 2026音乐喷泉施工技术拆解:3D激光水幕电影/主题乐园激光水幕/大型音乐喷泉工程/广场音乐喷泉/户外大型激光水幕/选择指南 - 优质品牌商家
  • ZeusHammer:融合三大开源项目的超级AI智能体,实现80%任务本地化
  • AI编程助手工具链2026:Devin、SWE-agent与Aider的工程师实战对比
  • 量子计算模拟自动化:El Agente Cuántico系统架构解析
  • 保姆级教程:在浪潮F37X加速卡上从源码编译安装Xilinx QDMA驱动(含libaio依赖处理)
  • 2026高性价比网架厂商TOP5:网架推荐/网架结构/网架钢结构/四川空心球/四川网架/山西空心球/汾阳空心球/选择指南 - 优质品牌商家
  • GodotPckTool:如何高效管理你的Godot游戏资源包?
  • Real Anime Z效果实测:对比Z-Image底座,真实系风格细节提升全解析
  • 告别图形界面:在麒麟LiveCD环境下用命令行高效备份整个家目录到移动硬盘
  • 告别刻板机器味!英文论文降AI率全指南:5款工具实测与3招手动修改
  • 别再傻傻分不清了!ArcGIS里点密度和核密度到底怎么选?附实战案例对比
  • 为AI智能体构建可治理的语义执行层:安全、合规与可控实践
  • VMware Unlocker深度解析:macOS虚拟机限制解除技术原理与架构设计