【问题标题】:spring reactive get locale in repository @Query春季反应在存储库@Query中获取语言环境
【发布时间】:2021-07-05 19:39:10
【问题描述】:

我在反应式 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 都会受到这种噪音/样板的影响,我想知道我是否可以在不将其作为参数传递的情况下以某种方式在 @Query("... where locale = :locale") 中使用/注入 locale.language

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

方法

来自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?

第二步

接下来我发现我可以通过在我的存储库上使用BeanPostProcessor 将扩展添加到响应式存储库(实际上我正在使用基于CoroutineCrudRepository 的 kotlin 协程)我的自定义 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"

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

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

第三步

我被困在如何获取请求上下文以填充 SpEL 扩展中的区域设置,如步骤 2 所示。这是我尝试的:

    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) }
    }

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

Context does not contain key: org.springframework.web.filter.reactive.ServerWebExchangeContextFilter.EXCHANGE_CONTEXT

据我了解,没有填充上下文,因为 Mono 不是从 spring 控制器返回的,而是用于 Query SpEL 和 我猜这不是同一个上下文链

我认为这一定是可能的,因为它类似于提供主体和安全上下文的方式,但我不完全理解 ReactiveSecurityContextHolder 的工作原理——尤其是不知道它是如何为 SpEL 上下文填充的。事实上,在我的默认配置中,它甚至没有被填充——我再次认为是因为它不是同一个上下文链。这里的另一个区别是 spring 将安全上下文显式设置为 ReactorContextWebFilter 中的反应上下文。

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

【问题讨论】:

  • 您可以在过滤器中获取标题并将其放置在响应式上下文中,然后从更下方的上下文中提取它。
  • 你能展示一下如何做到这一点吗?我不知道如何将它放在上下文中。
  • 文档可以比我更好地解释这一点。所以请阅读,尝试一下,如果你不能让它工作,用你有什么,你遇到什么问题等来更新你的问题。我们将从那里帮助你。 projectreactor.io/docs/core/release/reference/#context
  • 感谢您的提示,但是反应器上下文不是与 SpEL 上下文不同的上下文吗?有关如何使用ReactiveEvaluationContextProvider 的示例将非常有帮助,因为我所有的尝试都没有显示出与原始问题不同的任何内容。
  • 我又花了一天的时间讨论这个主题,并且能够使我的 SpEL 扩展工作,但我仍然不明白如何将反应式上下文填充到 SpEL 上下文中。我对问题进行了编辑,以显示到目前为止我的解决方法的所有问题和步骤的更多详细信息。

标签: spring-boot spring-webflux


【解决方案1】:

原来添加过滤器的作品(如下)。我仍然不完全理解 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) }
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-10-29
    • 2018-02-10
    • 1970-01-01
    • 2018-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多