Spring Data Moore 的新特性

工程 | Christoph Strobl | 2019 年 10 月 8 日 | ...

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 中的代码转换为以下清单,无需使用带有作用域模板的闭包以及多余的 FluxMono 的转换

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));
}

响应式 Elasticsearch 存储库

响应式家族中的另一个值得注意的补充可以在一个社区模块中找到,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

说到在转换过程中迷路: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);

Kotlin 协程和 MongoDB Criteria API DSL 支持

与 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 存储库中找到。

新的实体回调 API

我们还通过放弃当前基于 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(…));
  }
}

支持 Redis 流

说到流,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 存储过程的多个输出参数

在 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 聚合

使用 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 分钟的演示文稿。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部