spring 反应式获取存储库中的语言环境 @Query

时间:2021-04-09 23:09:29

标签: spring-boot spring-webflux

我在响应式 Spring Boot 应用程序中本地化了实体。

根据 Accept-Language 标头,当前实现可以正常工作:

控制器

@RestController
class TaskController(val taskService: TaskService) {
  @GetMapping("/tasks")
  suspend fun getTasks(locale: Locale): Map<Long?,Task> {
    return taskService.getTasks(locale).toList().associateBy(Task::id)
  }
}

服务

@Service
class TaskServiceImpl(val taskRepository : TaskRepository) : TaskService {
  override fun getTasks(locale: Locale): Flow<Task> {
    return taskRepository.findWithLocale(locale.language)
  }

存储库

interface TaskRepository : CoroutineCrudRepository<Task, Long> {
  @Query("SELECT * FROM task t LEFT JOIN task_l10n tl10n ON tl10n.task_id=t.id WHERE tl10n.locale = :locale")
  fun findWithLocale(locale: String): Flow<Task>
}

问题

如您所见,我需要将 locale 从控制器传递到服务,然后传递到存储库。因为我们的很多 API 都会受到这种噪音/样板的影响,我想知道我是否可以以某种方式在 locale.language 中使用/注入 @Query("... where locale = :locale") 而不将它作为参数传递?

在 MVC 中,我们至少可以在服务级别使用 LocaleContextHolder.getLocale() 并消除一些噪音,但它在反应式堆栈中不可用,因为它绑定到线程而不是协程。< /p>

方法

this spring.io post 我希望能够以请求感知方式设置 SpEL 评估上下文 - 即访问当前请求的语言环境。

类似的东西

@Query("... WHERE tl10n.locale = ?#{locale().language}"

但我无法弄清楚这些事情:

  1. 如何基于每个请求将响应式上下文置于 SpEL 上下文中?
  2. 如何使用请求的语言环境填充响应式上下文?

步骤 1

首先,我发现交换将通过调试 Spring 如何解析控制器方法的参数来提供对语言环境的访问:它是在 ServerWebExchangeMethodArgumentResolver 中完成的

exchange.getLocaleContext().getLocale()

因此,如果我可以从 SpEL 上下文访问 exchange,我会很高兴。

?#{exchange.getLocaleContext().getLocale()}

这是不可能的,因为 SpEL 上下文不知道反应性上下文:

Property or field 'exchange' cannot be found on object of type 'java.lang.Object[]' - maybe not public or not valid?

步骤 2

接下来我发现我可以通过使用 CoroutineCrudRepository我的存储库以在 BeanPostProcessor 方法中使用我的自定义 SpEL 扩展:

@Query("... ?#{locale()}")

这很好用 欢呼吧 - 但如您所见,我在创建扩展程序时将语言环境硬编码为 /** * Adds extensions to the SpEL evaluation context. */ @Configuration class RepositorySpELExtensionConfiguration { companion object { // list of provided extensions val contextProviderWithExtensions = ReactiveExtensionAwareQueryMethodEvaluationContextProvider(listOf(ReactiveLocaleAwareSpELExtension.INSTANCE)) } /** * Registers the customizer to the context to make spring aware of the bean post processor. */ @Bean fun spELContextInRepositoriesCustomizer(): AddExtensionsToRepositoryBeanPostProcessor { return AddExtensionsToRepositoryBeanPostProcessor() } /** * Sets the [contextProviderWithExtensions] for SpEL in the [R2dbcRepositoryFactoryBean]s which makes the extensions * usable in `@Query(...)` methods. */ class AddExtensionsToRepositoryBeanPostProcessor : BeanPostProcessor { override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any { if (bean is R2dbcRepositoryFactoryBean<*, *, *>) { bean.addRepositoryFactoryCustomizer { it.setEvaluationContextProvider(contextProviderWithExtensions) } } return bean } } /** * Makes the [LocaleAwareSpELExtension] available in a reactive context. */ enum class ReactiveLocaleAwareSpELExtension : ReactiveEvaluationContextExtension { INSTANCE; override fun getExtension(): Mono<out EvaluationContextExtension> { return Mono.just(LocaleAwareSpELExtension("en")) } override fun getExtensionId(): String { ReactiveQueryMethodEvaluationContextProvider.DEFAULT return "localeAwareSpELExtension" } } /** * Provides the requests locale as SpEL extension. * * Use it like this: * ``` * @Query("... WHERE locale = :#{locale()}") * ``` */ class LocaleAwareSpELExtension(private val locale: String) : EvaluationContextExtension { override fun getRootObject(): LocaleAwareSpELExtension { return this } override fun getExtensionId(): String { return "localeAwareSpELExtension" } @Suppress("unused") // Potentially used by `@Query(...) methods. fun locale(): String { return locale } } }

"en"

在调试这一行时,我知道它是在每个请求的基础上执行的,这让我希望我应该能够以某种方式使用请求上下文来填充语言环境。

步骤 3

我被困在如何获取请求上下文以填充 SpEL 扩展中的语言环境,如第 2 步所示。这是我尝试的:

    override fun getExtension(): Mono<out EvaluationContextExtension> {
      return Mono.just(LocaleAwareSpELExtension("en"))
    }

但交换在上下文中不可用:

    override fun getExtension(): Mono<out EvaluationContextExtension> {
      return Mono.deferContextual { it.get<Mono<ServerWebExchange>>(ServerWebExchangeContextFilter.EXCHANGE_CONTEXT_ATTRIBUTE) }
        .map { it.localeContext.locale?.language ?: "en" }
        .map { LocaleAwareSpELExtension(it) }
    }

据我所知,上下文没有被填充,因为 Mono 不是从 spring 控制器返回的,而是在查询 SpEL 中使用的,我猜这不是同一个上下文链?< /p>

我认为这一定是可能的,因为它类似于提供主体和安全上下文的方式,但我不完全理解 Context does not contain key: org.springframework.web.filter.reactive.ServerWebExchangeContextFilter.EXCHANGE_CONTEXT 是如何工作的 - 特别是不完全了解它是如何为 SpEL 上下文填充的.事实上,在我的默认配置中,它甚至没有被填充——我再次认为是因为它不是同一个上下文链。这里的另一个区别是 spring 将安全上下文显式设置为 ReactorContextWebFilter 中的反应上下文。

我想我现在可以创建自己的反应式请求过滤器来填充区域设置上下文,但我不知道如何使反应式上下文可用于 SpEL 上下文。

1 个答案:

答案 0 :(得分:0)

事实证明添加过滤器有效(如下)。我仍然不完全理解 SpEL VS 反应式上下文的上下文填充在这种情况下是如何工作的,但它确实有效..

@Component
class ReactorContextLocaleWebFilter : WebFilter {

  companion object {
    val KEY = ReactorContextLocaleWebFilter::class.java
  }

    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
    return chain.filter(exchange).contextWrite { context: Context ->
      if (context.hasKey(KEY))
        context
      else
        withLocaleContext(context, exchange)
    }
  }

  private fun withLocaleContext(mainContext: Context, exchange: ServerWebExchange): Context {
    return mainContext
      .putAll(Mono.just(exchange.localeContext.locale?.language ?: "en").`as` { lang: Mono<String> ->
        Context.of(KEY, lang).readOnly()
      })
  }

}

然后调整扩展创建以使用网络过滤器:

override fun getExtension(): Mono<out EvaluationContextExtension> {
      return Mono.deferContextual { it.get<Mono<String>>(ReactorContextLocaleWebFilter.KEY) }
        .map { LocaleAwareSpELExtension(it) }
    }
相关问题