Spring Data Ingalls 版本的新特性

工程 | Mark Paluch | 2017年1月30日 | ...

正如您可能已经看到的,我们已正式发布 Spring Data Ingalls 版本列车。由于此版本包含过多功能,无法在发布公告中全部涵盖,因此我想利用这篇文章更深入地探讨此版本列车中 15 个模块的更改和功能。

杂项

版本列车依赖项的一个非常根本的更改是将 Spring Framework 升级到 4.3(当前为 4.3.6)作为基线。其他依赖项升级主要由底层存储驱动程序和实现的主要版本升级驱动,这些升级需要反映在这些模块公开的 API 的潜在重大更改中。

Ingalls 还附带一个新的 Spring Data 模块:Spring Data LDAP。Spring LDAP 项目已经提供 Spring Data 存储库支持有一段时间了。在出现一些故障和不兼容性之后,我们决定将 LDAP 存储库支持移到单独的 Spring Data 模块中,以便更紧密地与版本列车对齐。

模块设置的另一个重大变化是,Apache Cassandra 的 Spring Data 现在已成为核心模块,这意味着它现在由 Pivotal 的 Spring Data 团队维护。非常感谢之前的核心维护者 David Webb 和 Matthew T. Adams 为此做出的所有努力。

除了这些非常根本的更改之外,团队还一直在努力开发大量新功能。

  • 在转换子系统中使用方法句柄进行属性访问。

  • 支持基于 XML 和 JSON 的投影以用于 REST 有效负载(公共组件)。

  • 使用 Spring Data REST 进行跨源资源共享。

  • 更多 MongoDB 聚合框架运算符,用于数组、算术、日期和集合运算。

  • 支持 Redis 地理命令。

  • 升级到 Cassandra 3.0,支持在存储库查询方法中进行查询派生、用户定义类型、Java 8 类型(Optional、Stream)、JSR-310 和 ThreeTen Backport。

  • 支持 Javaslang 的Option、集合和映射类型,用于存储库查询方法。

这些是我想在这篇文章的其余部分讨论的内容。

性能改进

改进对象访问的方法句柄

我们版本列车的一个主要主题是改进对象到存储映射子系统如何从域类访问数据。传统上,Spring Data 使用反射来实现这一点,要么直接检查字段,要么调用属性的访问器方法。

尽管 Java 8 中反射的性能得到了显著提高,但我们仍然可以使用另一种方法来使性能接近本地访问:MethodHandle。如果将它们保存在类的静态字段中,它们的使用速度尤其快,这对我们来说是一个挑战,因为我们事先不知道要持久化的域类型的结构。但是,我们已经通过使用 ASM 生成定制工厂来直接调用构造函数,对域对象实例的创建应用了类似的优化。现在,我们将同样的想法应用于我们的PersistentPropertyAccessor实现:我们检查类型并使用 ASM 生成一个包含静态最终MethodHandle的类,然后我们的 API 使用这些类来读取和写入属性以避免反射。如果类公开公共 API(例如访问器),我们只使用这些 API。

如果您感兴趣,可以在此处找到实现代码。但是,请做好准备,ASM 代码可能读起来有点复杂。如果至少运行 Java 7,则使用对象到存储映射的所有 Spring Data 模块都会受益于此更改(即 JPA 除外)。您可以在请求此更改的工单中找到更多详细信息。我们已经看到了 20% 到 70% 的性能改进。

从聚合根发布域事件

Spring 应用事件通常用于在应用程序中发布技术事件。但是,它们也是一个很好的工具,可以通过使用域事件的基础结构来解耦系统的各个部分。这通常是这样实现的。

class OrderManagement {

  private final ApplicationEventPublisher publisher;
  private final OrderRepository orders;

  @Transactional
  void completeOrder(Order order) {

    OrderCompletedEvent event = order.complete();
    orders.save(order);
    publisher.publish(event);
  }
}

请查看聚合根如何生成事件,然后服务组件如何通过 Spring 的ApplicationEventPublisher发布该事件。该模式总体上很好,但涉及相当多的仪式,并在业务组件中引入了技术框架依赖项,而人们可能希望避免这种情况。

使用 Spring Data Ingalls 版本列车,存储库现在会检查传递给save(…)方法的聚合,以查找聚合方法注释@DomainEvents,调用该方法并通过事件发布者自动发布返回的对象。因此,假设一个Order.complete()实现如下所示(AbstractAggregateRoot是 Spring Data 提供的包含已注释方法的类型)

class Order extends AbstractAggregateRoot {

  Order complete() {
    register(new OrderCompletedEvent(this));
    return this;
  }
}

客户端代码可以简化为

class OrderManagement {

  private final OrderRepository orders;

  @Transactional
  void completeOrder(Order order) {
    repository.save(order.complete());
  }
}

如您所见,不再引用 Spring 基础结构。事件发布由负责它的组件处理:聚合根。在参考文档中阅读有关此新机制的更多信息。目前,团队中正在围绕域事件进行更高级的想法。请关注此空间以获取更多更新。

分页

使用 Spring Data MongoDB 和 Spring Data JPA 的分页查询现在受益于改进的获取策略,该策略更积极地尝试避免执行计数查询。构建Page需要获取的数据和通常由查询返回的总记录数。虽然可以使用范围选择和索引优化数据查询,但计数查询非常昂贵,因为它们需要扫描表或索引。如果您请求最后一个仅部分填充的页面,我们可以跳过计数记录,因为元素总数可以根据偏移量和结果页面中的项目数量计算出来。

MongoDB DBRef 解析

Spring Data MongoDB 的DBRef获取进行了另一项与性能相关的更改。如果集合中的引用指向相同的数据库集合,则会使用单个批量操作来获取引用的集合。这意味着,我们可以基本上使用单个查询来读取相关的集合,而不是为每个元素读取一个查询。

用于 REST 有效负载的基于 XML 和 JSON 的投影

Evans 和 Hopper 版本的发布列车都包含了投影功能,允许通过应用投影接口来自定义现有领域对象的视图。投影可以在应用程序代码(仓库或手动实现的 Spring MVC 控制器)中使用,或者与 Spring Data REST 一起使用,通过 Web 端点公开领域对象的专用视图。投影还可以用于绑定表单提交(详情请参阅此示例)。在 Ingalls 版本中,我们现在扩展了该支持以处理 JSON 和 XML 请求。

@RestController
class UserController {

  /**
   * Receiving POST requests supporting both JSON and XML.
   */
  @PostMapping(value = "/")
  HttpEntity<String> post(@RequestBody UserPayload user) {

    return ResponseEntity
      .ok(String.format("firstname: %s, lastname: %s",
        user.getFirstname(), user.getLastname()));
  }
}

@ProjectedPayload
public interface UserPayload {

  @XBRead("//firstname")
  @JsonPath("$..firstname")
  String getFirstname();

  @XBRead("//lastname")
  @JsonPath("$..lastname")
  String getLastname();
}

投影接口使用 @ProjectedPayload 注解启用投影,投影方法注解包含 JSON 路径XPath 表达式

如果省略这些属性注解,我们将假设默认值(例如,上面示例中的 $.firstname/firstname 等)。这里的基本思想是——与其使用对象结构来映射传入的数据,不如直接指向您感兴趣的有效负载部分。使用 JSON 路径表达式或 XPath 可以让您对要访问的元素的实际位置更宽松,这样有效负载结构的更改不一定会破坏使用者。请查看上面示例如何在文档中的任何位置查找 firstname。如果生成 JSON 的一方突然将其嵌套到例如 user 文档或 XML 子节点中,则不需要更改使用者代码。

如果要在客户端使用这种有效负载访问,只需在 RestTemplate 上注册相应的 HttpMessageConverter 实例。

@Configuration
class Config {

  @Bean
  RestTemplateBuilder builder() {
    return new RestTemplateBuilder()
      .additionalMessageConverters(new ProjectingJackson2HttpMessageConverter())
      .additionalMessageConverters(new XmlBeamHttpMessageConverter());
  }
}

投影绑定支持使用 JsonPath 评估 JSON 路径表达式,并使用 XMLBeam 评估 XPath 表达式。您可以在 Spring Data 示例仓库 中找到此内容的完整示例。

Spring Data REST 的跨源资源共享

使用浏览器内部的客户端 JavaScript 请求受到 同源策略 的限制。默认情况下,禁止从应用程序服务器以外的其他来源请求数据,因为这是一个跨源请求。启用跨源资源共享 (CORS) 需要目标服务器提供 CORS 头,以便与每个 HTTP 响应一起发送。Spring Data REST 的 Ingalls 版本现在允许您轻松地

@CrossOrigin
public interface CustomerRepository extends CrudRepository<Customer, Long> {}

GET /customers/1 HTTP/1.1
Origin: https://127.0.0.1

HTTP/1.1 200 OK
Vary: Origin
ETag: "0"
Access-Control-Allow-Origin: https://127.0.0.1
Access-Control-Allow-Credentials: true
Last-Modified: Tue, 24 Jan 2017 09:38:01 GMT
Content-Type: application/hal+json;charset=UTF-8

导出的领域类和仓库可以使用 @CrossOrigin 注解启用 CORS,并且可以使用该注解来自定义设置。对于更全局的配置,可以使用 RepositoryRestConfigurer.configureRepositoryRestConfiguration(…) 来完全控制所有 Spring Data REST 公开的资源的 CORS 设置。

@Component
public class SpringDataRestCustomization extends
  RepositoryRestConfigurerAdapter {

  @Override
  public void configureRepositoryRestConfiguration(
    RepositoryRestConfiguration config) {

    config.getCorsRegistry().addCorsMapping("/person/**")
      .allowedOrigins("http://domain2.com")
      .allowedMethods("PUT", "DELETE")
      .allowedHeaders("header1", "header2", "header3")
      .exposedHeaders("header1", "header2")
      .allowCredentials(false).maxAge(3600);
  }
}

参考文档 中可以找到更多相关详细信息。

新的 MongoDB 聚合框架操作符

MongoDB 团队定期添加 新的聚合框架操作符。随着 Ingalls 版本列车的发布,我们抓住机会增强了 Spring Data MongoDB 可用操作符的集合,使其与 MongoDB 操作符以及您与这些操作符的交互方式保持一致。此版本增加了对以下聚合操作符和聚合阶段的原生支持。

聚合操作符

  • $anyElementTrue$allElementsTrue$setEquals$setIntersection$setUnion$setDifference$setIsSubset

  • $filter$in$indexOfArray$range$reverseArray$reduce$zip

  • $indexOfBytes$indexOfCP$split$strLenBytes$strLenCP$substrCP

  • $stdDevPop$stdDevSamp

  • $abs$ceil$exp$floor$ln$log$log10$pow$sqrt$trunc

  • $arrayElementAt$concatArrays$isArray

  • $literal$let

  • $dayOfYear$dayOfMonth$dayOfWeek$year$month$week$hour$minute$second$millisecond$dateToString$isoDayOfWeek$isoWeek$isoWeekYear

  • $count$cond$ifNull$map$switch$type

聚合阶段

  • $facet$bucket$bucketAuto

  • $replaceRoot$unwind$graphLookup

聚合操作符具有创建入口点,并以流畅的风格构建。多个聚合器分组在外观中,例如 ArrayOperatorsArithmeticOperators 等等。字段引用和聚合表达式可以在入口点方法中使用。聚合阶段操作符的入口点可通过 Aggregation 访问。

Aggregation.newAggregation(
  project()
    .and(ArrayOperators.arrayOf("instock").concat("ordered")).as("items")
);

Aggregation.newAggregation(
  project()
    .and(ArithmeticOperators.valueOf("quizzes").sum()).as("quizTotal")
);

Aggregation.newAggregation(
  group().stdDevSamp("age").as("ageStdDev")
);

Aggregation.newAggregation(Employee.class,
  match(Criteria.where("name").is("Andrew")),
  graphLookup("employee")
    .startWith("reportsTo")
    .connectFrom("reportsTo")
    .connectTo("name")
    .depthField("depth")
    .maxDepth(5)
    .as("reportingHierarchy"));

Aggregation.newAggregation(bucketAuto("field", 5)
  .andOutputExpression("netPrice + tax").as("total")
);

可以通过分别实现 AggregationOperationAggregationExpression 来使用任何当前不支持的聚合操作符和表达式。另请注意,其中一些操作符是在最近的 MongoDB 版本中引入的,只能与这些版本一起使用。

越来越多的操作符开辟了一套全新的可能性,可以将它们彼此结合起来。操作符可以以各种组合嵌套,这有时会导致难以阅读的代码。

newAggregation(
  project()
    .and(ConditionalOperators.when(Criteria.where("a").gte(42))
      .then("answer")
      .otherwise("no-answer"))
      .as("deep-tought")
);

为了简化此代码,我们现在支持 Spring 表达式语言 (SpEL) 表达式来制定如下投影

newAggregation(
  project()
    .andExpression("cond(a >= 42, 'answer', 'no-answer')")
    .as("deep-tought")
);

聚合中的 SpEL 支持并非完全是新事物。事实上,它自 Spring Data MongoDB 1.6 以来就已经可用。到目前为止,它支持算术运算(例如 '$items.price' * '$items.quantity')。Ingalls 在这里添加的新内容是,现在聚合操作符可以表示为接受参数的函数。您可以使用字段名称将字段传递给聚合操作符。然后,聚合框架将评估 SpEL 表达式并为聚合操作符创建 BSON 文档。

SpEL 的网关是 AggregationSpELExpression.expressionOf(…),它允许在您可以传入 AggregationExpression 的任何地方传入 SpEL 表达式。

newAggregation(
  group("number")
    .first(expressionOf("cond(a >= 42, 'answer', 'no-answer')"))
    .as("deep-tought")
)

请参阅 参考文档MongoDB 聚合框架示例 以了解更多详细信息。

Redis 地理索引

Redis 3.2 支持地理索引,并且我们从社区收到了关于地理索引的大力支持。我们随 Ingalls 一起提供地理索引支持,可以通过 RedisTemplate 和 Redis 仓库获得。让我们来看一个例子。

geoOperations.geoAdd("Sicily", new Point(13.361389, 38.115556), "Arigento");
geoOperations.geoAdd("Sicily", new Point(15.087269, 37.502669), "Catania");
geoOperations.geoAdd("Sicily", new Point(13.583333, 37.316667), "Palermo");

GeoResults<GeoLocation<String>> result =
  geoOperations.geoRadiusByMember("Sicily", "Palermo",
    new Distance(100, DistanceUnit.KILOMETERS));

List<String> geohashes = geoOperations.geoHash("Sicily", "Arigento", "Catania");
List<Point> points = geoOperations.geoPos("Sicily", "Arigento", "Palermo");

地理索引与您的领域类无缝集成。具有地理空间值的领域对象可以在地理索引中建立索引,并通过 Redis 仓库进行查询。以下示例显示了领域类和仓库接口声明。

public class City {

  @Id String id;
  String name;

  @GeoIndexed Point location;
}

public interface CityRepository extends Repository<City, String> {

  List<City> findByLocationNear(Point point, Distance distance);
}

使用 NearWithin 关键字声明仓库查询允许您使用靠近 Point 或在 Circle 内的地理空间查询。请注意,location 上使用的 @GeoIndexed 注解允许使用地理索引,该索引可与派生的地理空间查询方法一起使用。

用于 Apache Cassandra 的 Spring Data

Spring Data for Apache Cassandra 现在是 Spring Data 团队维护的核心模块。除了开发工作的主要所有权发生变化外,Ingalls 版本列车还对该模块本身进行了一系列值得注意的更改。

我们升级到了 Datastax Java Driver 3.1,因此 Spring Data for Apache Cassandra 现在支持 Apache Cassandra 3.0(1.2、2.0、2.1、2.2 和 3.0,最高可达 3.9)。

此版本还支持查询派生,因此您不必使用字符串查询,而是可以从查询方法名称派生 Apache Cassandra CQL 查询。

public interface BasicUserRepository extends Repository<User, Long> {

  /**
   * Derived query method.
   * Creates {@code SELECT * FROM users WHERE username = ?0}.
   */
  User findUserByUsername(String username);

  /**
   * Derived query method using SASI (SSTable Attached Secondary Index)
   * features through the {@code LIKE} keyword.
   * This query corresponds with
   * {@code SELECT * FROM users WHERE lastname LIKE '?0'}.
   * {@link User#lastname} is not part of the
   * primary key so it requires a secondary index.
   */
  List<User> findUsersByLastnameStartsWith(String lastnamePrefix);
}

您可以在我们的 示例库 中找到 Spring Data for Apache Cassandra 的查询派生示例。

查询派生支持 Apache Cassandra 提供的所有谓词,并附带 SASI(SSTable 附加二级索引)索引的谓词。在这种情况下,查询派生对主键或具有二级索引的列没有偏好。目前尚不支持 AllowFiltering 功能。此外,存储库查询方法也支持 Stream 作为返回类型。使用 Stream 不会预加载整个结果集,而是在您从流中提取数据时迭代结果。

总而言之,您现在可以在域类中使用 JSR-310 和 ThreeTen 向后移植类型以及 JodaTime 类型,这些类型作为 Java 8 支持的一部分添加。JSR-310 类型将转换为本地 Apache Cassandra 数据类型。有关详细信息,请参阅修订后的 参考文档 或我们的 Java 8 示例

public class Order {

  @Id String id;
  LocalDate orderDate;
  ZoneId zoneId;
}

public interface OrderRepository extends Repository<Order, String> {

  /**
   * Method parameters are converted according the registered
   * Converters into Cassandra types.
   */
  @Query("SELECT * from pizza_orders WHERE orderdate = ?0 and zoneid = ?1 ALLOW FILTERING")
  Order findOrderByOrderDateAndZoneId(LocalDate orderDate, ZoneId zoneId);

  /**
   * String-based query using native data types.
   */
  @Query("SELECT * from pizza_orders WHERE orderdate = ?0 and zoneid = ?1 ALLOW FILTERING")
  Order findOrderByDate(com.datastax.driver.core.LocalDate orderDate, String zoneId);

  /**
   * Streaming query.
   */
  Stream<Order> findAll();
}

可以通过注册自定义转换来配置数据类型支持。有关这方面的详细信息,请务必查看 GitHub 上 专门介绍此内容的示例

最后一个值得注意的功能是用户定义类型 (UDT)。使用 Ingalls,您现在可以使用嵌入在域类中的映射用户定义类型,也可以使用本机 UDTValue 类型。

@Table
public class Person {

  @Id int id;

  String firstname, lastname;
  Address current;
  List<Address> previous;

  @CassandraType(type = Name.UDT, userTypeName = "address")
  UDTValue alternative;
}

@UserDefinedType
public class Address {
  String street, zip, city;
}

显式映射的用户定义类型将结构化值映射到底层的 UDTValue,以便您可以在处理映射时继续使用域类,而 Spring Data for Apache Cassandra 会负责处理。

UDT 值存储在行中,这使得映射的 UDT 成为嵌入对象。您可以将 UDT 用作单个属性或集合类型的一部分。如果您使用模式创建,则用户定义类型将在应用程序启动时在数据存储中创建。UDT 从概念上讲是值对象,这意味着更新 UDT 值(通过保存域对象)会导致替换整个值。

有关特定功能的详细信息,请参阅修订后的 参考文档UDT 示例

支持 Javaslang

Spring Data 存储库现在支持 JavaslangOption 和集合类型作为存储库查询方法的返回值。Option 可用作 JDK 8 的 Optional 的替代方案,Seq 可用作 JDK 的 List 的替代方案。Javaslang 的 SetMap 也受支持,并从它们的 JDK 对应物透明地映射。

public interface PersonRepository extends Repository<Person, Long> {

    Option<Person> findById(Long id);

    Seq<Person> findByFirstnameContaining(String firstname);
}

有关更多信息,请参阅 带有 Javaslang 的 JPA 示例

Spring Data LDAP

Spring LDAP 项目已经支持 Spring Data 存储库有一段时间了。在 Ingalls 版本中,我们将该支持提取到一个 Spring Data 模块中,以便我们可以更快地将对内部 SPI 的更改传播到基于 LDAP 的实现。

如果您是现有的 Spring LDAP 存储库用户,则会受到此更改的影响,需要对您的项目进行两处更改。

  1. Spring Data LDAP 添加到您的项目依赖项中。

  2. 将存储库组件的包从 org.springframework.ldap.repository 更改为 org.springframework.data.ldap.repository

也就是说,Spring LDAP 2.3.0 已经删除了其存储库支持,如果您按照上述步骤操作,您可以继续使用 Spring Data LDAP 1.0 的 LDAP 存储库。通过查看我们的 Spring Data LDAP 示例,了解更多关于 LDAP 存储库的信息。

结论

我希望我已经向您简要概述了 Ingalls 版本列车的新功能。我们期待您通过我们的 Gitter 频道 提供反馈。此外,请随时在我们 JIRA 中报告您发现的任何错误。编码愉快!

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加快您的进度。

了解更多

获得支持

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

了解更多

即将举行的活动

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

查看全部