Spring Data 2020.0 - 新特性与重要变更 - 概述

工程 | Christoph Strobl | 2020 年 11 月 06 日 | ...

基于 Spring Framework 5.3 的 Spring Data 2020.0 已发布,其中包含各种存储模块的大量新特性。除了在里程碑公告中发布亮点之外,我们还希望通过一系列博客文章更详细地描述这些新特性。这些文章将涵盖(但不限于)以下内容:

  • Data Commons 中的响应式上下文访问特性。
  • R2DBC 中的生命周期回调和对 Spring R2DBC 的基准支持。
  • 通用 Repository 和特定于存储的 Redis 缓存指标。
  • Neo4J 响应式 repository。.
  • Spring Data for Apache Geode 的分页 repository API。
  • Spring Data Elasticsearch 的 Geoshape、索引和响应式搜索改进。
  • MongoDB 的部分过滤和聚合提示。
  • JDBC 模块的 @Bean 行映射器。
  • Apache Cassandra 抽象的每语句键空间定义。
  • 声明式 Couchbase N1QL 查询。

在本系列的第一部分,我们将花时间介绍一些通用主题以及 commons 模块中对特定存储实现有影响的某些方面。

我们从最显而易见的变化开始:版本控制方案。在遵循按字典顺序(致敬著名计算机科学家)的版本控制方案 7 年多之后,我们切换到了基于 CalVer 的版本控制方案。这主要会影响那些通过 spring-data-releasetrain artifact 管理 Spring Data artifact 版本的用户,该 artifact 已更改为 spring-data-bom 并采用新的方案。

org.springframework.data
spring-data-bom
2020.0.0

各个模块继续使用数字版本控制方案。

尽管进行了这些基础设施方面的更改,当前版本仍明确侧重于扩展响应式特性、提供更多应用程序洞察、指标以及在使用 GraalVM Native Image 时提供更好的开发者体验。同时,我们也在一些地方添加了一些小改进,希望让您的日常工作更加便捷。

其中一项增强功能是在 Projection 中添加了对 java.lang.Optional 的支持。这并非颠覆性的改变,但如果您想避免 null 值,它会很有帮助。

interface User {

	String getNickname();
	Optional<byte[]> getPicture();
}

在性能洞察方面(稍后会详细介绍),我们现在将 repository 的初始化延迟到首次使用时,这可以加快应用程序的启动时间。

我们还花时间对整个生产代码库进行了 Delombok 处理,以提高可读性和调试便利性。

响应式上下文访问

本次发布为完善响应式特性贡献了又一个重要部分。添加响应式上下文访问使我们能够在响应式流程中实现响应式审计和 SpEL 上下文扩展的评估等特性。在此之前,审计和 SpEL 在某种程度上已经可能。缺失的部分是访问上下文详情的能力,例如在 WebFlux 设置中通常由 Spring Security 提供的认证主体。

关于上下文,您有两种选择:

  • 随调用一起传递上下文
  • 在带外存储上下文详情(例如,使用 ThreadLocal

审计和 SpEL 上下文扩展的命令式实现依赖于 ThreadLocal 存储。然而,订阅响应式管道会移除关于管道将在哪些线程上具体化的任何假设。因此,这种方法变得不可用。这时第二种选择就发挥作用了,即随调用一起传递上下文详情。Project Reactor 的 Context 特性允许将上下文数据附加到订阅中。为了访问上下文,需要一个使用 Publisher 类型的 API。因此,在本版本中,我们引入了使用 ReactiveAuditorAwareReactiveEvaluationContextExtension 进行审计的响应式 API。

interface ReactiveAuditorAware<T> {  
	Mono<T> getCurrentAuditor();  
}

interface ReactiveEvaluationContextExtension extends ExtensionIdAware {  
	Mono<? extends EvaluationContextExtension> getExtension();  
}

您可以将这两个接口的实现公开为 Spring bean。Spring Data 会检测到它们并用于响应式审计和上下文扩展。

响应式审计

响应式审计将可审计实体与时间戳和当前用户(“审计者”)关联起来。用 @CreatedBy@LastModifiedDate 注解的属性在插入和更新时由 Spring Data 填充。在以前的版本中,用 @CreatedDate@LastModifiedDate 注解的属性是通过实体回调更新的。从本版本开始,审计者使用专门的实体回调从 ReactiveAuditorAware 传播到实体中。

使用 Spring Security 的 ReactiveSecurityContextHolder 实现 ReactiveAuditorAware 的示例代码如下:

class ReactiveUsernameAuditor implements ReactiveAuditorAware<String>{  

  	@Override  
  	public Mono<String> getCurrentAuditor() {  
    	  	return ReactiveSecurityContextHolder.getContext()  
              	  	.map(SecurityContext::getAuthentication)  
              	  	.map(Authentication::getPrincipal)  
              	  	.map(Object::toString);  
  	}
}

您需要两个步骤来为您的存储模块启用响应式审计。首先,通过您的存储特有的 @EnableReactive…Auditing 注解启用响应式审计。其次,注册一个 ReactiveAuditorAware bean。响应式审计支持 Cassandra、Elasticsearch、MongoDB、Neo4j 和 R2DBC。

以下配置片段展示了如何启用响应式审计。您可以在 Github 上找到完整示例

@Configuration  
@EnableReactiveCassandraAuditing  
class ApplicationConfiguration {  
  
	@Bean  
	ReactiveAuditorAware<String> reactiveAuditorAware() {  
		return () -> Mono.just("the-static-auditor-name");  
	}  
}

请注意,该示例产生一个名为 the-static-auditor-name 的静态审计者。使用从上下文派生的动态审计者需要额外的组件,例如 Spring Security 和 WebFlux。请注意,以前有一些存储通过 @Enable…Auditing 启用了响应式审计的日期部分。随着 @EnableReactive…Auditing 的引入,这种情况不再存在,@Enable…Auditing 仅启用命令式审计。如果您需要响应式审计,请确保相应地调整您的配置。

响应式 Spring Expression Language ("SpEL") 上下文扩展

SpEL 上下文扩展是一个 SPI,允许插入应用程序或库特定的扩展,这些扩展提供可以通过 SpEL 表达式在各种地方使用的功能。上下文扩展可以包含应用程序特定的状态,暴露领域特定的功能,或提供对与传入应用程序请求关联的上下文数据的访问。SpEL 上下文功能的一个典型例子是访问与经过身份验证或匿名应用程序用户发出的 HTTP 请求关联的安全上下文。

与响应式审计类似,任何按请求划分的上下文都需要从订阅者的 Context 中检索。在本版本中,我们引入了 ReactiveEvaluationContextProvider,它是 EvaluationContextProvider 的响应式变体。ReactiveEvaluationContextProvider 允许延迟检索 EvaluationContext。通过返回 Mono,上下文提供者可以访问订阅者的 Context。响应式上下文扩展需要实现 ReactiveEvaluationContextExtension,以便它们可以参与延迟上下文扩展解析。响应式扩展可以提取上下文数据并将其传递给实际的 EvaluationContextExtension,后者为 Spring Data 的 SpEL 评估机制提供功能。

请注意,响应式 SpEL 支持仅适用于响应式 repository 查询方法。其他组件中的 SpEL 表达式(例如,MongoDB @Document 中的集合名称或 Cassandra @Table 名称)在解析时不会考虑响应式上下文扩展,因为该 API 无法访问订阅者的 Context

SpEL 上下文扩展解析的改进

自诞生以来,如果查询中包含至少一个 SpEL 表达式,Spring Data 就会尝试在处理 SpEL 表达式之前解析所有已注册的 SpEL 上下文扩展。上下文解析可能开销很大。而且,有时上下文可能不存在。考虑以下查询方法:

@Query("SELECT * FROM person WHERE tenant = :#{tenantId}")
List<Person> findAllPeopleForTenant();

@Query("SELECT * FROM person WHERE tenant = :#{tenantId} or 1=?#{hasRole('ROLE_ADMIN') ? 1 : 0")
List<Person> findAllPeople();

这两个查询方法都使用了 SpEL 表达式,第二个查询方法引用了 Spring Security 的 SpEL 上下文扩展。当调用第一个方法(findAllPeopleForTenant),它只考虑 tenantId 时,Spring Data 也会加载 Spring Security 的扩展。如果在安全上下文之外调用该方法,方法调用将失败,因为 Spring Security 无法解析安全上下文。在 CommandLineRunnerApplicationEventLister 或生命周期方法(例如 @PostConstruct)中调用是典型的没有提供安全上下文的示例。

class MyComponent {

  @Autowired PersonRepository repository;
  
  @PostConstruct
  public void postConstruct() {
    TenantIdHolder.setCurrentTenant(4711);

    // this method fails with an exception because it attempts to resolve
    // the security context although it's not required in for the actual query.
    repository.findAllPeopleForTenant(); 
  }
}

随着响应式 SpEL 上下文扩展的引入,我们改进了 SPI,使其只解析实际需要的扩展。org.springframework.data.spel.ExpressionDependencies 对 SpEL 表达式进行静态分析并收集符号(属性引用和方法调用)。通过 EvaluationContextProvider 获取 EvaluationContext 时接受 ExpressionDependencies,以检查哪些扩展提供了这些符号,并仅加载可以满足依赖要求的扩展。这一更改对大多数应用程序应该是透明的,因为它只触及内部代码。请注意,如果查询仅引用可以解析的上下文功能,依赖分析使得例如在 @PostConstruct 中运行启用 SpEL 的查询成为可能。与查询无关的上下文扩展不再尝试解析,这一变化可能会反映在您的应用程序中。该优化适用于命令式和响应式代码路径,应能带来性能提升。

Repository 指标

了解应用程序运行时的情况对于某些业务至关重要。持久层(尤其是)可能是性能问题的罪魁祸首,这可能是由于网络或服务器端查询执行缓慢造成的。通过直接连接到 Spring Data 执行基础设施,Repository 指标现在为您提供了数据访问层运行时性能的真实来源。附加到 repository 的监听器会在每次调用接口时收到通知,提供关于 repository 本身、被调用的方法、传递的参数以及执行结果(成功、错误和取消信号)的信息,但不包含实际结果。

对于同步 repository 接口,测量从查询方法调用开始,包括解析潜在的带注解查询以及评估通过 SpEL 表达式提供值的扩展所需的时间(如前一节所述),并在返回可能的转换结果时结束。

对于响应式 repository,实际的订阅标志着调用的入口点,而完成或取消信号标志着测量的结束。

目前,配置方式并不是最方便的,因为它需要一个 BeanPostProcessor 通过添加调用监听器来操纵 repository 工厂 bean。但是,未来的 Spring Boot 版本将在 actuator 端点中提供对 repository 指标的专门支持。

class RepositoryMetricsPostProcessor implements BeanPostProcessor { 
   public Object postProcessBeforeInitialization(Object bean, String beanName) {

     if (bean instanceof RepositoryFactoryBeanSupport) {
         RepositoryFactoryBeanSupport<?, ?, ?> repositoryFactoryBean = (...) bean;      
         repositoryFactoryBean.addRepositoryFactoryCustomizer(repositoryFactory -> {
         	repositoryFactory.addInvocationListener(System.out::println);
         });
     }
   return bean;
   }
}

为了让您看到实际效果,我们为您准备了一个小的指标示例。该示例会将调用持续时间打印到控制台。

PersonRepository.save(Object): 2 ms – SUCCESS

PersonRepository.save(Object): 2 ms – SUCCESS

PersonRepository.findAll(): 32 ms – SUCCESS

PersonRespository.findByName(String): 1 ms - SUCCESS

GraalVM Native Image

谈到指标和性能,人们可能很快会想到“缩容至零”场景、启动速度以及 GraalVM Native Image。Spring Data 团队一直在努力推进这方面的工作,从而在使用 Spring Data repository 编译 Native Image 时提供了改进的开发者体验。这些努力包括简单的开关来禁用某些功能,例如优化实体实例化场景的代码生成、编译提示,以及作为 spring-graalvm-native 项目一部分的 SpringDataComponentProcessor。该组件处理器会检查 Spring Data repository、领域类型和方法签名。基于这些信息,它会将所有必需的代理、资源和反射配置添加到 Native Image 中,使其正常工作。这还包括任何特定于存储的注解以及命令式和响应式接口的自定义实现。如果您感到好奇,不妨试试

至此,我们的本系列第一部分就结束了。请持续关注此空间,以获取与您喜欢的存储相关的更新,并查看 Spring Data 示例,以便在此期间参考。

订阅 Spring 电子报

通过 Spring 电子报保持联系

订阅

抢占先机

VMware 提供培训和认证,助您加速发展。

了解更多

获取支持

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

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部