先行一步
VMware 提供培训和认证,助力您的飞速发展。
了解更多Spring Data Moore 版本包含 16 个模块,完成了 700 多张工单。它涵盖了产品组合中的大量改进和新特性,并重点关注三个主要主题:响应式、Kotlin 和性能。此版本增加了声明式响应式事务和 Coroutines/Flow 支持等特性,并且查找方法速度提高了高达 60%*。
让我们先来看看 Moore 版本的一些响应式特性。
Lovelace 版本引入了对响应式事务的早期支持,采用了一种闭包式的风格,尚有改进空间。以下代码清单展示了这种风格
Lovelace 版本中的响应式事务(使用 MongoDB)
public Mono<Process> doSomething(Long id) {
return template.inTransaction().execute(txTemplate -> {
return txTemplate.findById(id)
.flatMap(it -> start(txTemplate, it))
.flatMap(it -> verify(it))
.flatMap(it -> finish(txTemplate, it));
}).next();
}
在前面的代码片段中,事务必须通过在闭包内使用支持事务的模板显式调用 inTransaction()
来启动,并在末尾调用 next()
将返回的 Flux
转换为 Mono
以满足方法签名,尽管 findById(…)
已经只发出单个元素。
显然,这不是执行响应式事务最直观的方式。因此,让我们看看使用声明式响应式事务支持来实现相同的流程。与 Spring 的事务支持一样,您需要一个组件来为您处理事务。对于响应式事务,MongoDB 和 R2DBC 模块目前提供了 ReactiveTransactionManager
。以下代码清单展示了这样的组件
@EnableTransactionManagement
class Config extends AbstractReactiveMongoConfiguration {
// …
@Bean
ReactiveTransactionManager mgr(ReactiveMongoDatabaseFactory f) {
return new ReactiveMongoTransactionManager(f);
}
}
从那里,您可以使用 @Transactional
注解方法,并依赖基础设施启动、提交和回滚事务流程,通过 Reactor Context 处理生命周期。这使得您可以将 Lovelace 版本中的代码转换为以下代码清单,从而无需使用带有作用域模板的闭包以及多余的 Flux
到 Mono
转换
Moore 版本中的声明式响应式事务(使用 MongoDB)
@Transactional
public Mono<Process> doSomething(Long id) {
return template.findById(id)
.flatMap(it -> start(template, it))
.flatMap(it -> verify(it))
.flatMap(it -> finish(template, it));
}
响应式家族的另一个值得注意的新成员来自社区模块之一,Spring Data Elasticsearch 现在基于完全响应式的 Elasticsearch REST 客户端提供了响应式模板和仓库支持,该客户端又基于 Spring 的 WebClient
。
该客户端通过暴露一个接近 Java High-Level REST Client 的熟悉 API,为日常搜索操作提供了一流的支持,并在需要的地方进行了必要的精简。模板和仓库 API 的结合使得您可以在需要时无缝过渡到响应式,而不会迷失方向。以下代码清单展示了如何配置 Elasticsearch 以使用响应式客户端
响应式 Elasticsearch
class Config extends AbstractReactiveElasticsearchConfiguration {
// …
@Bean
public ReactiveElasticsearchClient reactiveClient() {
return ReactiveRestClients.create(localhost());
}
}
@Autowired
ReactiveElasticsearchTemplate template;
//…
Criteria criteria = new Criteria("topics").contains("spring")
.and("date").greaterThanEqual(today())
Flux<Conference> result = template.find(new CriteriaQuery(criteria), Conference.class);
说到在过渡中迷失方向:Querydsl (← 纯 HTTP / 无 HTTPS) 提供了一种出色的方式来为多个数据存储定义类型安全的查询,并且已经支持非响应式数据访问相当长一段时间了。为了在响应式场景中支持它,我们添加了一个响应式执行层,让您可以运行基于 Predicate
的查询。将 ReactiveQuerydslPredicateExecutor
添加到仓库接口后,将提供所有入口点,如下例所示
响应式 Querydsl
interface SampleRepository extends …, ReactiveQuerydslPredicateExecutor<…> {
// …
}
@Autowired
SampleRepository repository;
// …
Predicate predicate = QCustomer.customer.lastname.eq("Matthews");
Flux<Customer> result = repository.findAll(predicate);
与 Moore 版本中增强的响应式支持一致,我们继续了从 Lovelace 版本开始的 Kotlin 之旅。特别是,我们为 Kotlin Coroutines 和 Flows 提供了多个扩展,例如提供了 awaitSingle()
和 asFlow()
等方法。以下方法使用了 awaitSingle()
方法
Kotlin 协程支持
val result = runBlocking {
operations.query<Person>()
.matching(query(where("lastname").isEqualTo("Matthews")))
.awaitSingle()
}
另一个利用 Kotlin 语言特性的重要增强是由社区贡献的,它为 Spring Data MongoDB criteria API 添加了类型安全的查询 DSL。这使您可以将诸如 query(where("lastname").isEqualTo("Matthews"))
的代码转换为以下表示法
Kotlin 类型安全查询
val people = operations.query<Person>()
.matching(query(Person::lastname isEqualTo "Matthews"))
.all()
在开发所有这些新特性的同时,我们也花了一些时间调查当前实现的潜在瓶颈,并发现了一些可以改进的地方。这包括在许多地方去除 Optional
、捕获 lambda 和流执行、添加缓存以及避免不必要的查找操作。最终,基准测试显示,对于 JPA 单属性查找方法(例如 findByTitle(…)
),吞吐量提高了近 60%。
这非常棒,而且投入的时间是值得的!然而,我想明确一点,所有基准测试都使用了避免任何开销的“洁净室”场景。如果您将它们转移到更真实的场景(例如,用实际的生产就绪数据库替换内存中的 H2 数据库),结果会大相径庭,因为性能瓶颈会转移到网络交互、查询执行和结果传输。改进仍然可见,但通常降至个位数百分比。基准测试可以在这个 GitHub 仓库中找到。
我们还改进了现有的钩子,通过从当前的基于 ApplicationEvent
的方法转向更直接的交互模型,来拦截实体在持久化操作期间的生命周期。EntityCallback
API 引入了对不可变类型的更好支持,提供了运行时保证,并且也无缝地集成到响应式流程中。当然,我们仍然支持并发布 ApplicationEvents
,但当需要对处理的实体进行更改时,我们强烈建议切换到 EntityCallbacks
。
在下面的示例中,BeforeConvertCallback
通过使用 wither
方法修改给定的不可变实体,该方法将 id
分配给实体的一个副本,然后返回该副本,并在下一步转换为特定于存储的表示形式
EntityCallback API
@Bean
BeforeConvertCallback<Person> beforeConvert() {
return (entity, collection) -> {
return entity.withId(…);
}
}
与 ApplicationEvents
不同(后者可以用 AsyncTaskExecutor
配置,使得操作何时执行变得不确定),EntityCallback
API 保证在实际事件触发之前立即被调用。即使在响应式流中也是如此。以下代码清单展示了它是如何工作的
响应式 EntityCallback API
@Bean
ReactiveBeforeConvertCallback<Person> beforeConvert() {
return (entity, collection) -> {
return Mono.just(entity.withId(…));
}
}
说到流(streams),Spring Data Redis 现在支持 Redis Streams,它与响应式流几乎没有关系,但它是一种新的 Redis 只追加数据结构,建模了一个日志,其中每个条目都包含一个 id(通常是时间戳加序列号)和多个键/值对。除了通常的操作,例如添加到日志和从中读取之外,Spring Data Redis 还提供了容器,允许无限监听和处理添加到日志的条目。它的工作方式类似于 tail -f
,但用于 Redis Stream。以下示例展示了一个 Redis 流监听器
Redis Streams 监听器
@Autowired
RedisConnectionFactory factory;
StreamListener<String, MapRecord<…>> listener =
(msg) -> {
// … msg.getId()
// … msg.getStream()
// … msg.getValue()
};
StreamMessageListenerContainer container = StreamMessageListenerContainer.create(factory));
container.receive(StreamOffset.fromStart("my-stream"), listener);
前面示例中的 StreamMessageListenerContainer
读取 my-stream
的所有现有条目,并接收关于新添加条目的通知。对于接收到的每条消息,都会调用 StreamListener
。单个容器可以从多个流接收消息。
当然,类似流的结构最好由响应式基础设施消费,如下例所示
StreamReceiver receiver = // …
receiver.receive(StreamOffset.fromStart("my-stream"))
.doOnNext(msg -> {
// …
})
.subscribe();
在 JPA 方面,一个微小的改进现在允许您为存储过程设置多个 OUT
参数,这些参数将在一个 Map
中返回。以下示例展示了如何实现这一点
JPA 存储过程的输出参数
@NamedStoredProcedureQuery(name = "User.s1p", procedureName = "s1p",
parameters = {
@StoredProcedureParameter(mode = IN, name = "in_1", type = …),
@StoredProcedureParameter(mode = OUT, name = "out_1", type = …),
@StoredProcedureParameter(mode = OUT, name = "out_2", type = …)})
@Table(name = "SD_User")
class User { … }
interface UserRepository extends JpaRepository<…> {
@Procedure(name = "User.s1p")
Map<String, Integer> callS1P(@Param("in_1") Integer arg);
}
在 JPA 的 @StoredProcedureParameter
注解中声明的所有输出参数最终都将在仓库查询方法返回的 Map
中可用。
对于 MongoDB,复杂的数据处理是通过 聚合 (Aggregations) 完成的,Spring Data 为此提供了一个带有操作和表达式抽象的特定(流式)API。然而,Stackoverflow 告诉我们,人们倾向于在命令行上构建他们的聚合,然后将其翻译成 Java 代码。这种翻译被证明是一个主要的痛点。
因此,我们借此机会引入了 @Aggregation
作为在仓库方法中直接运行聚合的一种方式。以下示例展示了如何实现这一点
声明式 MongoDB 聚合
interface OrderRepository extends CrudRepository<Order, Long> {
@Aggregation("{ $group : { _id : '$cust_id', total : { $sum : '$amount' }}}")
List<TotalByCustomer> totalByCustomer(Sort sort);
@Aggregation(pipeline = {
"{ $match : { customerId : ?0 }}",
"{ $count : total }"
})
Long totalOrdersForCustomer(String customerId);
}
与它的“亲戚” @Query
注解一样,@Aggregation
支持参数替换,如果由查询方法参数提供,还会向聚合添加排序,如前面的示例所示。我们甚至更进一步,对于返回简单类型的方法,例如前面示例中的 totalOrdersForCustomer 方法,提取单属性文档值。在这种情况下,$count
阶段返回一个文档,例如 {"total" : 101 }
,通常需要映射到普通的 org.bson.Document
或相应的领域类型。然而,由于该方法声明其返回类型为 Long
,我们检查结果文档并从中提取/转换值,从而无需使用专用类型。
为了暂时总结,我想提及其他模块中的一些附加特性。如果您对所有这些特性感兴趣,请查看我们的 发布 wiki 或参考各个模块的参考文档中的“新特性”部分。那么,话不多说,以下是此版本提供的更多改进
Gemfire/Apache Geode:改进的 SSL 支持和动态端口配置
JDBC:只读属性、SQL 生成和可嵌入加载选项
REST:利用 HATEOAS 1.0 及其所有很酷的功能!
MongoDB:响应式 GridFS、声明式排序规则支持和 JSON Schema 生成器
neo4j:空间类型和存在性投影
Apache Cassandra:范围查询、乐观锁和审计支持
Redis:集群缓存和非阻塞连接方法
Elasticsearch:高级 REST 客户端支持和非 Jackson 的实体映射
如果您想了解更多,这里是在德克萨斯州奥斯汀举行的 SpringOne 2019 上录制的一个 30 分钟的演示。