Spring Data Fowler 有什么新功能?

工程 | Thomas Darimont | 2015 年 3 月 26 日 | ...

Spring Data Fowler 发布列车的 GA(通用可用性)版本标志着 6 个月开发的终点线。是时候让您了解此版本的内容并简要概述各项功能了。Fowler 发布列车的主要主题是性能改进和增强的 Java 8 支持,这些主要体现在 Spring Data JPA 和 MongoDB 模块中,但许多其他模块也看到了显著的改进。

升级到 Spring Data Fowler 发布列车的最简单方法是使用 Spring Boot 并将 spring-data-releasetrain.version 属性配置为 Fowler-RELEASE。如果您尚未使用 Spring Boot,请将 Spring Data BOM 添加到您的 Maven POM 的 <dependencyManagement /> 部分。

总体主题

Java 8 Stream 在 Repository 方法中的支持

Java 8 的一个重要新特性是 Stream API,它允许 Java 开发者定义一个操作管道,用于对对象流进行操作,但只有最终的操作才会真正触发 Stream 中元素的消费。

在数据访问的上下文中,将查询执行结果以 Stream 形式提供是一个非常有用的用例,因为它可以防止查询方法的调用者阻塞,直到所有项目都被读取。更不用说这里更高效的内存使用。

通过 Fowler 版本,我们引入了对 Java 8 Stream 作为 repository 中 finder 方法返回类型的支持。在 MongoDB 和 JPA 模块中,您现在可以像这样声明 finder 方法

interface CustomerRepository extends Repository<Customer, Long> {

  Stream<Customer> findByLastname(String lastname);
}

调用此方法将执行支持 repository 方法的查询,并在第一个结果可用时立即返回。为了在 JPA 中实现这一点,我们使用了特定于持久性提供程序的 API,因为 JPA 本身只提供了将查询结果作为 List 获取的方式。Repository 客户端现在可以在 try-with-resources 块中使用方法调用的结果。这将确保为遍历流而保持打开的资源最终会被关闭

try (Stream<Customer> customers = repository.findByLastname("Matthews")) {
  customers.filter(…).map(…).collect(…);
}

JSR-310 和 ThreeTen Backport 支持

为了方便地在领域对象中持久化 无时区 JSR-310 类型(JDK 8 中新引入的日期/时间 API),我们在 MongoDB 和 JPA 模块中添加了针对相关类型的转换器。对于尚未能升级到 Java 8 的开发者,我们为 ThreeTen Backport 项目 添加了一组类似的转换器,这样即使您仍然使用 Java 7,也可以在代码中开始使用这些类型。将来切换到 Java 8 只需简单地更改包名即可。

在 MongoDB 模块中,相应的 Converter 实现会自动可用。对于 JPA,您只需将 Jsr310JpaConvertersThreeTenBackPortJpaConverters 注册到您的持久性提供程序。如果您在 Spring 中使用 LocalContainerEntityManagerFactoryBean 设置 JPA 环境,只需将 org.springframework.data.jpa.convert.threeten….threetenbp 添加到要扫描的包中。使用 Spring Boot,只需将上述类添加到 @EntityScan 声明中即可

@EntityScan(
  basePackageClasses = { Application.class, Jsr310JpaConverters.class }
)
@SpringBootApplication
class Application { … }

此设置将确保您的应用程序包和 Spring Data JPA 的 JSR-310 转换器都将被扫描并传递给持久性提供程序。您可以在我们的 Spring Data 示例仓库 中找到一个完整的示例。请注意,由于转换器只是将 JSR-310 类型转换为传统的 Date 实例,因此 仅支持无时区 类型(例如 LocalDateTime 等)。

MongoDB

3.0 服务器和驱动程序支持

Spring Data Fowler 支持最新、最优秀的 MongoDB 3.0 服务器版本。虽然该版本已经可以与 2.13.0 版本的 MongoDB Java 驱动程序一起使用,但我们也确保了 Spring Data MongoDB 可以与即将推出的 3.0 版本的 Java 驱动程序完美配合。因此,开发者可以自由选择他们将使用哪个版本,或者何时升级到新驱动程序。有关驱动程序和服务器版本之间兼容性的通用信息,请务必查看 MongoDB 文档。但请注意,后续开发将明确侧重于服务器和驱动程序的 3.0 系列。

通常,我们鼓励大家在 JavaConfig 中优先使用 MongoClient 而不是 Mongo,或者使用新引入的 XML 元素 <mongo:mongo-client /><mongo:client-options />。更多信息请参考参考文档

GeoJSON 支持

自 MongoDB 引入 GeoJSON 作为处理地理结构的格式以来已有一段时间了。这些数据结构在类似地球的球体上操作,因此不能与 2D 索引一起使用。话虽如此,使用起来非常简单直观,因为我们提供了专用的类型来支持 GeoJSON。这些类型既可以用在您的领域类型中,也可以用作查询参数。

@Document
class Store {

  @Id String id;

  /**
   * The location is stored in GeoJSON format:
   * { "type" : "Point", "coordinates" : [ x, y ] }
   */
  GeoJsonPoint location;
}

interface StoreRepository extends CrudRepository<Store, String> {

  List<Store> findByLocationWithin(Polygon polygon);
}

repo.findByLocationWithin(
  new GeoJsonPolygon(
    new Point(-73.992514, 40.758934),
    new Point(-73.961138, 40.760348),
    new Point(-73.991658, 40.730006),
    new Point(-73.992514, 40.758934)));

这将创建以下要在 MongoDB 中执行的查询

{
  "location": {
    "$geoWithin": {
      "$geometry": {
        "type": "Polygon",
        "coordinates": [[
           [-73.992514,40.758934],
           [-73.961138,40.760348],
           [-73.991658,40.730006],
           [-73.992514,40.758934]
        ]]
      }
    }
  }
}

请注意,StoreRepository.findByLocationWithin(…) 仍然接受 Polygon。使用 GeoJsonPolygonfindByLocationWithin(…) 将使用 $geometry 操作符和 GeoJSON 表示形式创建查询。有关用法和限制的更多详细信息,请参阅 MongoDB 手册

执行 MongoDB 存储脚本

MongoDB 允许通过直接发送原始源脚本或调用先前存储的脚本来在服务器上执行 JavaScript 函数。我们通过新引入的 ScriptOperations 接口公开了此功能,该接口可以从 MongoOperations 获取。

ScriptOperations ops = mongoOperations.scriptOps();
ExecutableMongoScript script = new ExecutableMongoScript("function(x) { return x; }");
Object r1 = ops.execute(script, "Direct function execution.")

ops.register(new NamedMongoScript("echo", script));
Object r2 = ops.call("echo", "Call stored function.");

后续版本将增强服务器端脚本支持,我们将增加返回类型转换、用于从 repository 方法调用存储过程的注解以及对 $where 操作符的支持。

对象到存储转换的性能改进

对象到存储的映射子系统在性能方面进行了重大改进。我们对 Commons 和存储模块进行了分析,在某些地方引入了缓存,与 Evans 发布列车相比,每秒操作次数有了相当可观的提升(尽管大部分改进也已移植回 Evans 的服务版本)。

Performance improvements in Spring Data Fowler

正如您所见,我们在读取访问方面的每秒操作次数增加了一倍以上,并且在写入操作方面也接近这个数字。

从数据存储读取大量对象时,会花费大量时间通过反射创建对象实例。通过 Fowler 发布列车,我们引入了一个新的默认 EntityInstantiator,它通过使用 ASM 在运行时为领域对象创建工厂类来解决这个瓶颈。这个工厂类直接调用领域类的构造函数,这比通过反射这样做要快得多。如果您对这些巧妙的细节感兴趣,这里 是为我们实现这一功能的类。

Redis

HyperLogLog

Redis HyperLogLog 命令提供了一种高效的解决方案来计算独特事物,而无需记住已遇到的元素。例如,这适用于按 IP 统计唯一页面访问次数。

HyperLogLogOperations hll = redisTemplate.opsForHyperLogLog();

hll.add(today(), "8.8.8.8", "8.8.4.4");
hll.size(today()); // Unique page visits today = 2

hll.add(today(), "198.153.192.40", "8.8.8.8");
hll.size(today()); // Unique page visits today = 3
hll.size(today(), yesterday()); // Unique page visits today and yesterday

Gemfire

到目前为止,GemFire 模块最显著的变化是完全支持 GemFire 8。GemFire 8 自 7.0.2 版本以来引入了多项新变化,包括一个新的基于集群的配置服务

启用该服务后,开发者可以在 Gfsh 中记录他们的操作和类似模式的更改,例如添加区域、创建索引、配置磁盘存储等。当开发者在集群中启动新的 GemFire 节点时,该成员将自动从新的基于集群的配置服务(托管在 locator 中)获取其配置。

虽然 Spring Data GemFire 的基于 XML 命名空间的配置仍然是流行的选择,特别是在高度迭代、反馈周期短的开发过程中,Spring Data GemFire 增加了对新的基于集群的配置的支持,其行为类似于 Spring Data Gemfire 对 GemFire 原生 `cache.xml` 格式的支持。

要在 Spring 配置的 GemFire 节点中启用基于集群的配置,开发者只需在 <gfe:cache /> 元素上设置 use-cluster-configuration 属性,如下所示

<gfe:cache id="gemfireCache" use-cluster-configuration="true" … />

Spring Data GemFire 将首先请求并应用集群范围的配置,然后再应用 XML 命名空间特定的配置。您可以将 XML 配置元数据视为增强集群配置服务发送的集群配置。

有关 GemFire 新的集群配置服务的更多信息,请参阅 GemFire 用户指南SGF-226 获取更多详细信息。

Spring Data REST

Spring Data REST 在 Fowler 版本中也看到了广泛的改进。其中最显著的改进之一是检查更多实体元数据并用于填充响应头。例如,通过 @Version 注解支持乐观锁定的存储现在将把实体版本用作 ETag 头,以便客户端可以利用它们触发条件 GET 请求。

与此密切相关的是,使用 Spring Data 审计支持的实体将自动将其最后修改日期传播到项目资源响应的 LastModified 头中

class Customer {

  @Version Long version;
  @LastModifiedDate LocalDate lastModifiedDate;
}
curl -v http://…/customers/1

Etag: 1
Last-Modified: Tue, 24 Mar 2015 12:34:56 GMT

JSON Schema

Spring Data REST 的 Fowler 版本还提供了改进的 JSON Schema 支持。默认情况下,ALPS 文档中为领域类型公开的表示描述符会指向 Schema。在 Starbucks 示例 中,您可以看到链接的呈现方式如下

curl http://…/alps/stores

{
  "version": "1.0",
  "descriptors": [ {
    "id": "store-representation",
    "href": "http://localhost:8080/stores/schema",
    "descriptors": [ … ]
  }],
  …
}

点击该链接将显示商店的 JSON Schema 文档

{
  "title": "example.stores.Store",
  "properties": {
    "address": {
      "$ref": "#/descriptors/address"
    },
    "name": {
      "type": "string"
    }
  },
  "descriptors": {
    "address": {
      "type": "object",
      "properties": {
        "zip": {
          "type": "string"
        },
        "city": {
          "type": "string"
        },
        "street": {
          "type": "string"
        },
        "location": {
          "$ref": "#/descriptors/point"
        }
      }
    },
    "point": {
      "type": "object",
      "properties": {
        "x": {
          "type": "number"
        },
        "y": {
          "type": "number"
        }
      }
    }
  },
  "type": "object",
  "$schema": "https://json-schema.fullstack.org.cn/draft-04/schema#"
}

请注意我们如何从领域类型及其 Jackson 映射中推导出 Schema 的基本特征。必需属性可以通过使用 @JsonProperty(required = true) 来确定,日期/时间类型被正确发现和宣传。您可以在 RepositoryRestConfiguration.metadataConfiguration() 上注册自定义的 JSON Schema 格式或模式

@Configuration
static class SampleConfiguration extends RepositoryRestMvcConfiguration {

  @Override
  protected void configureRepositoryRestConfiguration(
    RepositoryRestConfiguration config) {
    config.metadataConfiguration().
      registerJsonSchemaFormat(JsonSchemaFormat.EMAIL, EmailAddress.class);
  }
}

假设 EmailAddress 是一个您已经调整 Jackson 使其呈现为纯 String 的值对象,此配置将使所有 EmailAddress 类型的属性在 JSON Schema 文档中显示时 format 设置为 email

Solr

文档分数改进

Fowler 版本增加了对 realtime-get 的支持,允许从索引读取未提交的更改。SolrTemplate 上提供了 getById 方法。另一个值得注意的增加是 @Score 注解,其灵感来自于 Spring Data MongoDB 中可用的 @TextScore。该注解允许检索文档分数,并将隐式添加必要的参数,这样在检索文档查询匹配分数时就不再需要显式添加 @Query(fields={"*", "score"} 了。

其他

提升 Projection

Spring Data REST 在 Evans 发布列车中发布了一项称为 projections 的功能。在 Fowler 版本中,我们将支持该功能的基础设施移至 Spring Data Commons 并进行了一些调整,以便其他项目无需额外依赖即可使用它。该功能的核心是一个 ProjectionFactory,它允许您为由其他对象(例如 Map)支持的接口创建对象实例。

interface Customer {

  String getFirstname();

  String getLastname();

  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();
}

现在可以使用 ProjectionFactory 将此接口转换为对象

Map<String, Object> map = new HashMap<>();
map.put("firstname", "Dave");
map.put("lastname", "Matthews");

ProjectionFactory factory = new SpelAwareProjectionFactory();
Customer customer = factory.createProjection(Customer.class, map)

assertThat(customer.getFirstname(), is("Dave"));
assertThat(customer.getLastname(), is("Matthews"));
assertThat(customer.getFullName(), is("Dave Matthews"));

如您所见,我们选择了一个 Map 来支持创建的 projection 实例。底层创建了一个配备方法拦截器的 JDK 代理,该拦截器(如果代理由 Map 支持)会将对访问器(accessor)的调用委托给 Map 中的属性查找。使用 @Value 注解的方法将对其注解的 SpEL 表达式进行求值。如果您在 SpelAwareProjectionFactory 上配置了 BeanFactory,您甚至可以从这些表达式中引用 Spring bean,从而触发更复杂的计算。

如果支持查找没有返回可赋值给声明的返回类型的值,则会使用标准的 ConversionService 进行简单转换,然后进行递归 projection 步骤。

有关如何在 Spring MVC 控制器中使用 projections 的示例,请参阅 StackOverflow 上的此回答

Spring MVC 中的 Projections

现在,Spring MVC 控制器实现可以使用 projection 机制,仅通过接口创建表单支持对象。在您的 Spring 配置中使用 @EnableSpringDataWebSupport(在 Boot 中自动激活)将解析一个 ProxyingHandlerMethodArgumentResolver,它将自动为接口创建一个代理实例,并将相应的请求参数绑定到该实例

interface Form {

  @NotBlank String getName();
  @NotBlank String getText();
}

@Controller
@RequestMapping(value = "/guestbook")
class GuestbookController {

  @RequestMapping(method = RequestMethod.GET)
  String guestbook(Form form, Model model) { … }

  @RequestMapping(method = RequestMethod.POST)
  String guestbook(@Valid Form form, Errors errors, Model model) { … }
}

请注意接口在接受 GET 请求的方法中如何用于向即将渲染的视图提供一个空的表单支持对象。接收 POST 请求的方法使用 Form 来表明它希望将表单数据绑定到代理实例并应用验证。

总结

尽管这篇文章很长,但我们几乎没有触及 Spring Data Fowler 发布的所有新功能的皮毛。您可能想探索 发布列车维基 寻找更多亮点,并依次查看指向工单和相关提交的链接,因为它们通常包含能够很好地展示各项功能的测试用例。

此外,前面提到的 Spring Data 示例仓库 有很多可供玩耍和探索的东西。

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

领先一步

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

了解更多

获取支持

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

了解更多

即将举办的活动

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

查看全部