Spring Data Release Ingalls 的新特性?

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

您可能已经看到,我们刚刚宣布了 Spring Data release train Ingalls 的正式发布 (GA)。由于该版本包含太多特性无法在发布公告中全部涵盖,我想通过这篇文章更深入地介绍该版本中 15 个模块带来的变化和特性。

杂项

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

Ingalls 还附带了一个新的 Spring Data 模块:Spring Data LDAP。Spring LDAP 项目提供 Spring Data repository 支持已经有一段时间了。在出现一些小问题和不兼容后,我们决定将 LDAP repository 支持移到一个独立的 Spring Data 模块中,使其与发布列车更紧密地对齐。

模块设置的另一个重大变化是 Spring Data for Apache Cassandra 现在已成为核心模块,这意味着它现在以及将来都将由 Pivotal 的 Spring Data 团队维护。这是一个感谢前核心维护者 David Webb 和 Matthew T. Adams 为之付出的所有努力的好机会。

除了这些非常基础的变化之外,团队还致力于一系列新特性:

  • 在转换子系统中,使用 MethodHandle 进行属性访问。

  • 支持基于 XML 和 JSON 的 REST 负载投影 (Commons)

  • 使用 Spring Data REST 实现跨域资源共享

  • 更多用于数组、算术、日期和集合操作的 MongoDB Aggregation Framework 运算符。

  • 支持 Redis 地理位置 (Geo) 命令。

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

  • 支持 Javaslang 的 Option、集合和映射类型作为 repository 查询方法的返回类型。

这些是我将在本文剩余部分讨论的内容。

性能改进

使用 MethodHandle 改进对象访问

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

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

如果您有兴趣,可以在此处找到实现代码。但是,请做好准备,ASM 代码可能有点难以阅读。如果您运行的是 Java 7 或更高版本,所有使用对象到存储映射的 Spring Data 模块(例如 JPA 除外)都将受益于此变更。您可以在请求此变更的工单中找到更多详细信息。我们看到了 20% 到 70% 的性能提升。

从聚合根发布领域事件

Spring Application Events 通常用于在应用程序内部发布技术事件。然而,它们也是通过使用领域事件基础设施来解耦系统部分的好工具。这通常是这样实现的:

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 版本列车中,repository 现在会检查传递给 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 中的分页查询现在受益于一种改进的获取策略,该策略更积极地尝试避免执行 count 查询。构建 Page 需要获取的数据以及查询返回的总记录数。虽然数据查询可以通过范围选择和索引进行优化,但 count 查询通常非常昂贵,因为它需要扫描表或索引。如果您请求的是最后一页,且该页未完全填满,我们可以跳过计数记录,因为总元素数量可以从偏移量和结果页中的项目数计算得出。

MongoDB DBRef 解析

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

基于 XML 和 JSON 的 REST 负载投影

EvansHopper 版本列车附带了投影特性,允许通过应用投影接口来定制对现有领域对象的视图。投影可用于应用程序代码(repository 或手动实现的 Spring MVC controller)或与 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 pathXPath 表达式

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

如果您想在客户端使用这种负载访问方式,只需在 RestTemplate 上注册相应的 HttpMessageConverter 实例即可:

@Configuration
class Config {

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

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

使用 Spring Data REST 实现跨域资源共享

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

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

GET /customers/1 HTTP/1.1
Origin: http://localhost

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

导出的领域类和 repository 可以使用 @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 Aggregation Framework 运算符

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 Expression Language (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 repositories 使用。让我们看一个示例:

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 repositories 进行查询。以下示例显示了领域类和 repository 接口的声明:

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 关键字声明 repository 查询,您可以使用靠近 Point 或在 Circle 范围内的地理空间查询。请注意,在 location 上使用的 @GeoIndexed 注解允许使用可以与派生的地理空间查询方法一起使用的地理位置索引。

Spring Data for Apache Cassandra

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 Attached Secondary Index) 索引的谓词。在此上下文中,查询派生对主键或具有二级索引的列没有特定偏好。目前尚不支持 AllowFiltering。此外,repository 查询方法还支持 Stream 作为返回类型。使用 Stream 不会预加载整个结果集,而是在您拉取流时迭代结果。

最后,您现在可以在领域类中使用 JSR-310 和 ThreeTen back-port 类型以及 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 用作单个属性或集合类型的一部分。如果您使用 schema 创建,则用户定义类型会在应用程序启动时在数据存储中创建。从概念上讲,UDT 是值对象,这意味着对 UDT 值的更新(通过保存领域对象)会导致替换整个值。

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

支持 Javaslang

Spring Data repositories 现在支持 JavaslangOption 和集合类型作为 repository 查询方法的返回值。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 repositories 支持已经有一段时间了。在 Ingalls 版本中,我们将该支持提取到了一个独立的 Spring Data 模块中,以便我们对内部 SPI 所做的更改可以更快地传播到基于 LDAP 的实现。

如果您是现有的 Spring LDAP repositories 用户,您会受到此更改的影响,需要对项目进行两项更改:

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

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

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

结论

希望我对 Ingalls 发布列车的新特性做了快速的概述。我们期待您通过我们的 Gitter 频道 提供反馈。另外,请随时在我们的 JIRA 中报告您发现的任何错误。编程愉快!

订阅 Spring 邮件列表

通过 Spring 邮件列表保持联系

订阅

抢先一步

VMware 提供培训和认证,助您快速提升。

了解更多

获得支持

通过一项简单的订阅,Tanzu Spring 为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

即将举办的活动

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

查看全部