Spring Data Ingalls 系列有何新特性?

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

如你所见,我们刚刚宣布了 Spring Data Ingalls 系列的 GA 版本。由于该版本包含的功能实在太多,无法在一篇发布公告中全部涵盖,因此我想借此机会深入探讨 Ingalls 系列中 15 个模块所带来的变化和新特性。

后台整理

本次系列版本一个非常基础的变化是,将 Spring Framework 4.3(当前为 4.3.6)作为基线依赖。其他依赖项的升级大多是由于底层存储驱动程序和实现的主要版本升级,这可能导致所暴露 API 的潜在重大变更。

Ingalls 还包含了一个新的 Spring Data 模块:Spring Data LDAP。Spring LDAP 项目很早就提供了 Spring Data 仓库的支持。在经历了一些小问题和不兼容性后,我们决定将 LDAP 仓库支持移到一个独立的 Spring Data 模块中,以便更好地与系列版本保持一致。

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

除了这些非常基础的变化外,团队还在努力开发一系列新功能。

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

  • 支持 REST 载荷的 XML 和 JSON 格式的投影(Commons)。

  • Spring Data REST 支持跨域资源共享(Cross-origin resource sharing)。

  • MongoDB 聚合框架增加了更多用于数组、算术、日期和集合操作的运算符。

  • 支持 Redis Geo 命令。

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

  • 支持 Javaslang 的 Option、集合和 Map 类型用于仓库查询方法。

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

性能改进

使用 MethodHandles 改进对象访问

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

尽管 Java 8 中反射的性能得到了显著提升,但我们仍然可以使用另一种方式将性能提升到接近原生访问的水平:MethodHandle。如果它们被存储在类的静态字段中,它们的使用速度会特别快,这对我们来说是一个挑战,因为我们事先不知道要持久化的域类型的结构。然而,我们已经通过使用 ASM 生成专门的工厂来调用构造函数,对域对象实例的创建进行了类似的优化。现在,我们将同样的理念应用到了我们的 PersistentPropertyAccessor 实现上:我们检查类型并使用 ASM 生成一个类,该类持有静态的 final MethodHandle,我们的读写属性的 API 然后使用这些句柄来避免反射。如果类公开了公共 API(例如访问器),我们则直接使用它们。

如果您感兴趣,可以在这里找到实现代码。不过,请做好心理准备,ASM 代码可能有点难读。所有使用对象到存储映射的 Spring Data 模块(JPA 除外)都受益于此更改,前提是您至少运行 Java 7。您可以在请求该更改的 ticket中找到更多详细信息。我们已经看到了 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 系列版本,仓库现在会检查传递给 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 系列版本提供了投影(projection)功能,允许通过应用投影接口来自定义域对象的视图。投影可以在应用程序代码(仓库或手动实现的 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 pathXPath 表达式

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

如果您想在客户端使用此类载荷访问,您可以将相应的 HttpMessageConverter 实例注册到 RestTemplate 上:

@Configuration
class Config {

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

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

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: https://

HTTP/1.1 200 OK
Vary: Origin
ETag: "0"
Access-Control-Allow-Origin: https://
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

聚合运算符具有创建入口点,并以流畅的方式构建。多个聚合器分组在外观(facades)中,如 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 Aggegation Framework 示例

Redis Geo 索引

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

地理索引可与您的域类无缝集成。具有地理空间值的域对象可以被索引到 Geo 索引中,并通过 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 内的位置。请注意,@GeoIndexed 注解用于 location 字段,允许使用可用于派生地理空间查询方法的 geo-index。

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。此外,仓库查询方法也支持 Stream 作为返回类型。使用 Stream 不会预加载整个结果集,而是当您拉取流时,它会迭代结果。

最后,您还可以将 JSR-310 和 ThreeTen Backport 类型以及 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 仓库现在支持 Javaslang 的 Javaslang Option 和集合类型作为仓库查询方法的返回值。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);
}

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

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 社区所有即将举行的活动。

查看所有