领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多Spring Data Moore 发布了 16 个模块,完成了 700 多个工单。它包含了大量改进和新特性,涵盖了整个产品组合,并重点关注三个主要主题:响应式、Kotlin 和性能。该版本增加了声明式响应式事务和协程/流支持等功能,并且查找器方法的速度提高了高达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 上下文处理生命周期。这使您可以将 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 高级 REST 客户端的熟悉 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 协程和流提供了几个扩展,例如提供 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
通过使用将 id
分配给实体副本的 wither
方法修改给定的不可变实体,然后返回该副本,并在下一步将其转换为存储特定的表示形式
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(…));
}
}
说到流,Spring Data Redis 现在支持 Redis 流,这与响应式流几乎没有关系,但它是一种新的 Redis 追加式数据结构,模拟一个日志,其中每个条目包含一个 ID(通常是时间戳加上序列号)和多个键值对。除了通常的操作,例如添加到日志和从中读取之外,Spring Data Redis 还提供了容器,允许无限监听和处理添加到日志中的条目。它的工作原理类似于 tail -f
,但适用于 Redis 流。以下示例展示了一个 Redis 流监听器
Redis 流监听器
@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,复杂的数据处理是通过 聚合 来完成的,Spring Data 为其提供了一个特定的(流畅的)API,其中包含对操作和表达式的抽象。但是,Stack Overflow 教会我们,人们倾向于在命令行中创建他们的聚合,然后将其转换为 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 模式生成器
neo4j:空间类型和存在投影
Apache Cassandra:范围查询、乐观锁和审计支持
Redis:集群缓存和非阻塞连接方法
Elasticsearch:高级 REST 客户端支持和非 Jackson 基于实体映射
如果您想了解更多信息,这里有一个在德克萨斯州奥斯汀举行的 SpringOne 2019 上录制的 30 分钟的演示文稿。