java

位置:IT落伍者 >> java >> 浏览文章

构建用于正则表达式的抽象Java API


发布日期:2019年10月20日
 
构建用于正则表达式的抽象Java API

在我的经验中大多数 Java 开发人员都需要解析某种文本通常这意味着他们最初要花一些时间使用象 indexOf 或 substring 那样的与 Java 字符串相关的函数或方法并且希望输入格式永远不变但是如果输入格式改变那么用于读取新格式的代码维护起来就会变得更复杂更困难最后代码可能需要支持自动换行(word wrapping)区分大小写等

由于逻辑变得更加复杂所以维护也变得很困难因为任何更改都可能产生副作用并使文本解析器的其它部分停止工作所以开发人员需要时间修正这些小错误

有一定 Perl 经验的开发人员可能也有过使用正则表达式的经验如果够幸运(或优秀)的话这位开发人员能够说服团队其余的人(或至少是团队领导)使用这项技术新的方法将取消编写用来调用 String 方法的多行代码它意味着将解析器逻辑的核心委托出去并替换为 regexp 库

接受了有 Perl 经验的开发人员的建议后团队必须选择哪个 regex 实现最适合他们的项目然后他们需要学习如何使用它

在简要地研究了从因特网上找到的众多可选方案后假设团队决定从人们更熟悉的库中选择一个使用如属于 Jakarta 项目的 Oro接下来对解析器进行较大程度地重构或几乎重新编写并且解析器最终使用了 Oro 的类如 PerlCompilerPerlMatcher 等

这一决定的后果很明显代码与 Jakarta Oro 的类紧密地耦合在一起

团队承担了风险因为不知道非功能性需求(如性能或线程模型)是否将得到满足

团队已花费时间和财力来学习并重新编写代码以使它使用 regexp 库如果他们的决定是错误的并且选择了新的库则这一工作在成本上将不会有很大区别因为将需要再次重新编写代码

即使库工作正常如果他们决定应该迁移到全新的库(例如包括在 JDK 中的库)怎么办?

去耦的好处

有没有办法使团队知道哪个实现最适合他们的需要呢(不仅现在能将来也能)?让我们试着寻找答案

避免依赖任何特定的实现

前面的情形在软件工程中十分常见在有些情况中这样的情形会导致较大的投资和较长的延期当不了解所有后果就作出决定而且决策制定人不太走运或缺乏必需的经验时就常常会发生这种情况

可将该情形概括如下

您需要某种提供者

您没有选择最佳提供者的客观标准

您希望能用最低的成本来评估所有的待选项

所作的决定不应将您束缚在所选的提供者上

这一问题的解决方法是使代码更加独立于提供者这引入了新的层 ? 同时去除客户机和提供者的耦合的层

在服务器端开发中很容易找到使用该方法的模式或体系结构下面引用一些示例

对于 JEE您主要关注如何构建应用程序而不是应用程序服务器的细节

数据访问对象(Data Access ObjectDAO)模式隐藏了如何访问数据库(或 LDAP 服务器XML 文件等)的细节和复杂性因为它提供了访问抽象持久存储层的方法而您则不需要在客户机代码中处理数据库问题(数据实际存储在哪里)这不是四人组(Gang of FourGoF)模式而是 Sun 的 JEE 最佳实践的一部分

在假想的开发团队示例中他们正在寻找这样的层

抽象所有正则表达式实现背后的概念团队就可以着重学习和理解这些概念他们所学的可以应用到任何实现或版本

支持新的库且没有副作用基于插件体系结构动态选择执行 regexp 模式的实际库并且适配器不会被耦合新库仅会引入对新适配器的需要

提供比较不同可选方案的方法一个简单的基准实用程序就可以显示有趣的性能测量结果如果对每个实现都执行这样的实用程序团队就会获得有价值的信息并能选择最好的可选方案

听起来不错但……

任何去耦方法都至少有一个缺点如果客户机代码仅需要一个实现所提供的特定功能怎么办?您不能使用任何其它实现因此您最终将代码与该实现耦合也许将来会在这方面有所改善但您现在却束手无策

这样的示例并不象您想的那样少在 regexp 领域中一些编译器选项仅被某些实现支持如果您的客户机代码需要这种特定的功能那么这个一般层是不够的 ? 至少从迄今对它描述来看是不够的

附加层是否应支持每个实现的所有非公共功能并且如果选择了不支持该实现的附加层则抛出异常?那可以是一种解决方案但它并不支持仅定义公共抽象概念这一最初目标

有一个 GoF 模式非常适合这种情形职责链(Chain of Responsibility)它在设计中引入了另一种间接方法用这种方法客户机代码向能处理其所发消息的实体列表发送消息或命令列表项被组织成链因此消息可按顺序被处理并且在到达链尾之前被用掉

在这种情况中可以通过特殊类型的消息对仅被某些实现支持的特定功能建模由链中的每一项根据其是否了解这些功能来决定是否将该消息传给下一项

定义一个公共 API

这里讲述的 API 名为 RegexpPlugin已将它设计成遵循刚刚讨论的方法并且它在 regexp 库和使用该库的代码之间支持去耦

RegexpPlugin

在以下示例中我将总结一下使用具体实现(Jakarta Oro)和使用 RegexpPlugin API 之间的差别

我从一个非常简单的 regexp 开始假定您必须要解析的文本只是人名您接收的格式是象 John A Smith 这样的内容而您只想获取名字(John)但您不知道单词由什么分隔是空格换行符制表符还是这些字符的组合能处理这样的输入格式的 regexp 只是 *s*(*?)s+*我将一步一步地说明如何使用该 regexp 来抽取信息

第一部分是点号和星号字符 *它们在这里表示任意数量的空格和 (*?) 组之前的任何字符第二部分比较引人注意(因为它被圆括号括起来)问号表示取第一个符合条件的项

接下来的符号表示任意数量的空格换行或制表符(s)但至少要有一个(+)最后的点号和星号 * 仅代表文本的余下部分(对它没有兴趣)

因此该 regexp 相当于取空格前的第一段文本让我们来编写 Java 代码

上机实践

要在 Java 代码中使用正则表达式通常需要完成以下七个步骤

创建编译器实例如果使用 Jakarta Oro则必须实例化 PerlCompiler

orgapacheorotextregexPerlCompiler compiler =new orgapacheorotextregexPerlCompiler();

使用 RegexpPlugin 时的等同代码是相似的

orgacmslregexppluginCompiler compiler =orgacmslregexppluginRegexpManagercreateCompiler();

但存在差异正如前面提到的该 API 对实际使用哪个具体实现加以隐藏您可以选择一个具体实现或保留缺省的 Jakarta Oro如果所选的库在运行时不可用则 RegexpPlugin API 会尝试用它的类名创建一个编译器如果该操作失败它会将异常发回 API 的客户机

假定您一直在使用 JDK 的内置 regexp 类那样的话包含始终不会使用的额外 jar 文件毫无意义那就是为什么仅仅调用 createCompiler() 方法还不够的原因您需要管理这样的异常每当所选的库不存在时就会抛出该异常因而必须更新示例

try

{

orgacmslregexppluginCompiler compiler =orgacmslregexppluginRegexpManagercreateCompiler();

}

catch (orgacmslregexppluginRegexpEngineNorFoundException exception)

{

[]

}

编译 regexp 模式将正则表达式本身编译到 Pattern 对象中

orgapacheorotextregexPattern pattern =pile(*\s*(*?)\s+* PerlCompilerMULTILINE_MASK);

您必须转义反斜槓()字符

该模式对象代表以文本格式定义的正则表达式请尽可能多地重用模式实例然后如果 regexp 是固定的(缺少任何可变部分(*?)Tom*则模式应是类中的静态成员

compile 方法适合用标志(如 EXTENDED_MASK)来配置(请参阅参考资料以获得更详细的 regexp 教程)但是RegexpPlugin 并不允许随意的标志受支持的标志只有 case sensitivity 和 multiline因为所有受支持的库都可以处理它们

编译器实例有特定的特性来定义这些标志

compilersetMultiline(true);

orgacmslregexppluginPattern pattern =pile(*\s*(*?)\s+*);

创建 Matcher 对象在 Jakarta Oro 中这一步非常简单

orgapacheorotextregexPerlMatcher matcher =new orgapacheorotextregexPerlMatcher();

它之所以如此简单是因为它不需要构造任何信息在后来的 regexp 中它将变得具体基本上RegexpPlugin 中的步骤差不多相似您不必亲自创建 matcher而是可以将其代理给 RegexpManager 类

orgacmslregexppluginMatcher matcher =orgacmslregexppluginRegexpManagercreateMatcher();

区别和前面一样您需要处理 RegexpEngineNotFoundException实际上RegexpManager 需要为您所选的库或缺省库创建 matcher 适配器如果这样的类在运行时不可用它会抛出该异常

评估正则表达式matcher 对象需要解释正则表达式并抽取所需的信息这在一行代码中完成

if (ntains(John A Smith pattern))

如果输入文本与正则表达式匹配则该方法返回 true隐含的副作用是执行该行代码之后matcher 对象包含在输入文本中找到的第一个匹配项接下来的一步演示如何实际获取感兴趣的信息

通过使用 RegexpPlugin API在此时根本没有任何不同

检索找到的第一个匹配项这一简单的步骤仅用一行完成

orgapacheorotextregexMatchResult matchResult = matchergetMatch();

您可以声明一个局部变量来存储这样的对象该对象含有与 regexp 匹配的一段文本在这两种情况下该步骤是相同的除了变量声明(因为一个是另一个的适配器)

orgacmslregexppluginMatchResult matchResult =matchergetMatch();

获取感兴趣的 group您可以使用两种方法

具体库

RegexpPlugin API

因为您的 regexp 是 *s*(*?)s+*所以您只有一个组(*?)

MatchResult 对象包含已排序列表中的所有组您只需要知道要获取的组的位置因为该示例只有一个组所以毫无疑问

String name = matchResultgroup();

[]

}

变量 name 现在包含文本 John那正是您需要的

如果需要则重复该过程如果您需要的信息可多次出现而您想分析所有出现的信息而不只是第一个那么您只需循环执行第 步到第 直到不满足第 步中描述的条件为止

while (ntains(John A Smith pattern)){}

映射

除了编写公共抽象 API主要的工作实际上是实现 Java 环境中某些已存在的 regexp 引擎的适配器

以下各表提供了对如何从一个库迁移至另一个库的详细描述有些情况中概念明显不同也有些情况中却不是那么明显

Regexp 概念 GNU Regexp

编译器 gnuregexpRE

模式 gnuregexpRE

匹配程序 gnuregexpREMatchEnumeration

gnuregexpRE

匹配结果 gnuregexpREMatch

畸形模式异常 gnuregexpREException

Regexp 概念 Jakarta Oro

编译器 orgapacheorotextregexPerlCompiler

模式 orgapacheorotextregexPattern

匹配程序 orgapacheorotextregexPerlMatcher

匹配结果 orgapacheorotextregexMatchResult

畸形模式异常 org[]regexMalformedPatternException

Regexp 概念 Jakarta Regexp

编译器 orgapacheregexpRE

orgapacheregexpRECompiler

orgapacheregexpREProgram

模式 orgapacheregexpREProgram

orgapacheregexpRE

匹配程序 orgapacheregexpRE

orgapacheregexpREProgram

匹配结果 orgapacheregexpRE

畸形模式异常 orgapacheregexpRESyntaxException

Regexp 概念 JDK regex 包

编译器 javautilregexPattern

模式 javautilregexPattern

匹配程序 javautilregexMatcher

匹配结果 javautilregexMatcher

畸形模式异常 javautilregexPatternSyntaxException

基准

该 API 较显着的用法之一是用来比较实现测量性能对 Perl 语法的兼容性或其它标准之间的差异

为这些测试开发的基准实用程序使用 HTML 解析器来处理 Web 内容更新有关链接表单和表等元素的信息但是重要的是解析逻辑用正则表达式来表示因此会通过 RegexpPlugin API 实现

基准测试包括对非常简单的 HTML 页面解析 结果在下表中显示

Regexp 库 Benchmark 结果(秒)

Jakarta Oro

Jakarta Regexp

GNU Regexp

JDK

您可以用多种方法在实际应用程序中改进性能最重要的是当您使用 regexp 库时不需要每次都编译模式而是编译它们并重用各自的实例但是如果 regexp 本身不固定则不能忽略编译过程

因为基准需要在实现之间切换以比较性能所以必须始终废弃已编译模式以避免库之间的交互但是正如您所见大多数已评估的库有相似的响应时间尽管更详细的基准能让我们更好的理解每个库在不同环境下的行为

结束语

正则表达式解析器有强大的功能一旦团队适应了它解析逻辑就会改进这有助于降低维护但是开发人员需要了解 regexp 语法以理解这些代码是如何工作的本文已经用一个非常简单的示例说明了如何使用这些库中的一个除此之外本文还描述了使用附加层去除客户机代码与 regexp 引擎本身之间的耦合的好处

               

上一篇:在Java程序中调用Matlab函数

下一篇:Java媒体架构基础:Framework Basics