10.4 Java 8 DSL的实际应用
在10.3节中,我们学习了使用Java开发DSL的三种模式,包括它们的优缺点。表10-1总结了迄今为止我们介绍的所有内容。
表 10-1 DSL模式及其优缺点
| 模式名 | 优点 | 缺点 |
|---|---|---|
| 方法链接 | □ 方法名可以作为关键字参数 □ 与optional参数的兼容性很好 □ 可以强制DSL的用户按照预定义的顺序调用方法 □ 很少使用或者基本不使用静态方法 □ 可能的语法噪声很低 | □ 实现起来代码很冗长 □ 需要使用胶水语言整合多个构建器 □ 领域对象的层级只能通过代码的缩进公约定义 |
| 嵌套函数 | □ 实现代码比较简洁 □ 领域对象的层次与函数嵌套保持一致 | □ 大量使用了静态方法 □ 参数通过位置而非变量名识别 □ 支持可选参数需要实现重载方法 |
| 使用Lambda的函数序列 | □ 对可选参数的支持很好 □ 很少或者基本不使用静态方法 □ 领域对象的层次与Lambda的嵌套保持一致 □ 不需要为支持构建器而使用胶水语言 | □ 实现代码很冗长 □ DSL中的Lambda表达式会带来更多的语法噪声 |
接下来我们会通过分析这些模式在三个著名Java库中的应用来对之前介绍的内容做一个总结。这三个库分别是:一个SQL映射工具、一个行为驱动的开发框架以及一个实现企业级集成模式的工具。
10.4.1 jOOQ
SQL是最通用且应用最广泛的DSL。基于这个事实,如果我跟你说有人为Java编写了一个很不错的DSL,通过它可以编写和执行SQL语句,你应该不会感到意外。jOOQ作为类型安全的嵌入式语言,是直接用Java实现的一种内部DSL。源代码生成器逆向工程了数据库模式,如此一来Java编译器就可以对复杂的SQL语句进行类型检查了。你可以使用这种逆向工程生成的信息对你的数据库进行操作。下面是一个数据库查询的简单示例:
SELECT * FROM BOOKWHERE BOOK.PUBLISHED_IN = 2016ORDER BY BOOK.TITLE
使用jOOQ DSL可以将其重写为下面这种形式:
create.selectFrom(BOOK).where(BOOK.PUBLISHED_IN.eq(2016)).orderBy(BOOK.TITLE)
jOOQ DSL的另一个非常好用的特性是它能够与Stream API无缝联合使用。这一特性让你可以使用流畅语句在内存中对SQL查询的结果进行操作,代码清单如下。
代码清单 10-17 使用JOOQ DSL从数据库中查询图书信息
Class.forName("org.h2.Driver");try (Connection c =getConnection("jdbc:h2:~/sql-goodies-with-mapping", "sa", "")) { ←---- 创建SQL数据库的连接DSL.using(c) ←---- 使用刚刚创建的数据库连接启动jOOQ SQL语句.select(BOOK.AUTHOR, BOOK.TITLE) ←---- 通过jOOQ DL定义SQL语句.where(BOOK.PUBLISHED_IN.eq(2016)).orderBy(BOOK.TITLE).fetch() ←---- 从数据库中返回数据,jOOQ语句终止于此.stream() ←---- 开始使用Stream API处理从数据库中取得的数据.collect(groupingBy( ←---- 按照作者对图书进行分类r -> r.getValue(BOOK.AUTHOR),LinkedHashMap::new,mapping(r -> r.getValue(BOOK.TITLE), toList()))).forEach((author, titles) -> ←---- 把作者名及其所著书名一起打印出来System.out.println(author + " is author of " + titles));}
很明显,jOOQ DSL选择使用的主要DSL模式是方法链接。实际上,这个模式(支持可选参数,某些方法只能按照预先定义的顺序执行调用)的很多特征对模仿格式规范的SQL查询语法都非常重要。这些特性以及它们很小的语法噪声,使得方法链接模式非常适合jOOQ的需求。
10.4.2 Cucumber
行为驱动开发(behavior-driven development,BDD)是测试驱动开发的延伸,它使用由结构化语句构成的简单领域特定脚本语言描述业务场景,这些业务场景可以是多种多样的。与其他的BDD框架一样,Cucumber可以将这种结构化语句转化为可执行的测试用例。因此,采用这种开发技术的脚本既能作为可执行的测试,也能作为该业务特性的接受标准。BDD还专注于帮助大家快速地发布高优先级、可验证的业务价值,同时通过让领域专家和程序员共享业务词汇,减少他们在需求理解上的差异。
这些概念听起来都很抽象,我们接下来会借助一个BDD工具——Cucumber,作为实际的例子进行介绍。Cucumber可以帮助开发者通过纯英文书写业务场景,如下所示:
Feature: Buy stockScenario: Buy 10 IBM stocksGiven the price of a "IBM" stock is 125$When I buy 10 "IBM"Then the order value should be 1250$
Cucumber使用声明将业务需求分成了三部分:需求定义的前提(Given)、测试时对领域对象的实际调用(When),以及检查测试用例结果的断言(Then)。
定义测试场景的脚本使用外部DSL编写,它的关键字数量有限,除此之外你可以随心所欲地书写语句,没有别的规则。测试用例会通过正则表达式匹配这些语句,捕获其中的变量,将其作为参数传递给实现测试的方法自身。以10.3节开始时介绍的股票交易领域模型为例,我们可以开发一个Cucumber测试用例,检查股票交易订单是否计算正确,代码清单如下。
代码清单 10-18 使用Cucumber注解实现一个测试场景
public class BuyStocksSteps {private Map<String, Integer> stockUnitPrices = new HashMap<>();private Order order = new Order();@Given("^the price of a \"(.*?)\" stock is (\\d+)\\$$") ←---- 定义该场景的前置条件和股票的单位价格public void setUnitPrice(String stockName, int unitPrice) {stockUnitValues.put(stockName, unitPrice); ←---- 保存股票的单位价格}@When("^I buy (\\d+) \"(.*?)\"$") ←---- 定义测试领域模型时的动作public void buyStocks(int quantity, String stockName) {Trade trade = new Trade(); ←---- 生成相应的领域模型trade.setType(Trade.Type.BUY);Stock stock = new Stock();stock.setSymbol(stockName);trade.setStock(stock);trade.setPrice(stockUnitPrices.get(stockName));trade.setQuantity(quantity);order.addTrade(trade);}@Then("^the order value should be (\\d+)\\$$") ←---- 定义期望的场景输出public void checkOrderValue(int expectedValue) {assertEquals(expectedValue, order.getValue()); ←---- 检查测试的断言}}
Java 8引入的Lambda表达式赋予了Cucumber新的活力,借助于新语法,你可以使用带两个参数的方法替换掉注释,这两个参数分别是:包含之前注释中期望值的正则表达式以及实现测试方法的Lambda表达式。使用第二种标记法,你可以像下面这样重写测试场景:
public class BuyStocksSteps implements cucumber.api.java8.En {private Map<String, Integer> stockUnitPrices = new HashMap<>();private Order order = new Order();public BuyStocksSteps() {Given("^the price of a \"(.*?)\" stock is (\\d+)\\$$",(String stockName, int unitPrice) -> {stockUnitValues.put(stockName, unitPrice);});// ……为了简洁起见,我们省略了更多的Lambda,譬如什么情况要做什么}}
第二种实现方法明显更加紧凑。尤其是使用匿名Lambda替换了测试方法后,你再也不用绞尽脑汁地替方法构思有意义的名字了(测试场景中,这并不会为可读性带来太多的提升)。
Cucumber的DSL非常简单,但是它展示了如何有效地整合外部DSL与内部DSL,并且(再一次)证明了使用Lambda可以写出更精简、更具可读性的代码。
10.4.3 Spring Integration
为了支持著名的企业集成模式3,Spring Integration扩展了基于Spring编程模型的依赖注入。Spring Integration的首要目标是要提供一个简单的模型用于实现复杂的企业整合方案,并推广异步、消息驱动架构的采用。
3详情请参考由Gregor Hohpe和Bobby Woolf在2004年出版的Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions。
有了Spring Integration之后,在基于Spring的应用中开发轻量级的远程服务(remoting)、消息(messaging),以及计划任务(scheduling)都很方便。这些特性可以借由形式丰富的流畅DSL实现,而这并不只是基于Spring传统XML配置文件构建的语法糖。
Spring Integration实现了创建基于消息的应用所需的所有常用模式,包括管道(channel)、消息处理节点(endpoint)、轮询器(poller)、管道拦截器(channel interceptor)。为了改善可读性,处理节点在该DSL中被表述为动词,集成的过程就是将这些处理节点组合成一个或多个消息流。下面这段代码就是一个展示Spring Integration如何工作的例子,虽然简单,但是“五脏俱全”。
代码清单 10-19 使用Spring Integration DSL配置一个Spring Integration的工作流
@Configuration@EnableIntegrationpublic class MyConfiguration {@Beanpublic MessageSource<?> integerMessageSource() {MethodInvokingMessageSource source =new MethodInvokingMessageSource(); ←---- 创建一个新消息源,每次调用是以原子操作的方式递增一个整型变量source.setObject(new AtomicInteger());source.setMethodName("getAndIncrement");return source;}@Beanpublic DirectChannel inputChannel() {return new DirectChannel(); ←---- 管道传送由消息源发送过来的数据}@Beanpublic IntegrationFlow myFlow() {return IntegrationFlows ←---- 以方法链接方式通过一个构建器创建IntegrationFlow.from(this.integerMessageSource(), ←---- 以之前定义的MessageSource作为IntegrationFlow的来源c -> c.poller(Pollers.fixedRate(10))) ←---- 轮询MessageSource,对它传递的数据队列执行出队操作,取出数据.channel(this.inputChannel()).filter((Integer p) -> p % 2 == 0) ←---- 过滤出那些偶数.transform(Object::toString) ←---- 将由MessageSource 获取的整数转换为字符串类型.channel(MessageChannels.queue("queueChannel")) ←---- 将queueChannel作为该IntegrationFlow的输出管道.get(); ←---- 终止IntegrationFlow的构建执行,并返回结果}}
这段代码中,方法myFlow()构建IntegrationFlow时使用了Spring Integration DSL。它使用的是IntegrationFlow类提供的流畅构建器,该构建器采用的就是方法链接模式。这个例子中,最终的流会以固定的频率轮询MessageSource,生成一个整数序列,过滤出其中的偶数,再将它们转化为字符串,最终将结果发送给输出管道,这种行为与Java 8原生的Stream API非常像。该API允许你将消息发送给流中的任何一个组件,只要你知道它的inputChannel名。如果流始于一个直接管道(direct channel),而非一个MessageSource,你完全可以使用Lambda表达式定义该IntegrationFlow,如下所示:
@Beanpublic IntegrationFlow myFlow() {return flow -> flow.filter((Integer p) -> p % 2 == 0).transform(Object::toString).handle(System.out::println);}
如你所见,目前Spring Integration DSL中使用最广泛的模式是方法链接。这种模式非常适合IntegrationFlow构建器的主要用途: 创建一个执行消息传递和数据转换的流。然而,正如我们在上一个例子中看到的那样,它也并非只用一种模式,构建顶层对象时它也使用了Lambda表达式的函数序列(有些情况下,也是为了解决方法内部更加复杂的参数传递问题)。
