1

如果我有一个名为的控制器HomeController接收GET /foo带有 header的请求X-Foo: Bar,我想创建一个 WS 客户端过滤器,它将RequestHeader在上下文中读取并将 header 值复制到传出的 WS 请求。

示例控制器:

import play.api.libs.ws.{StandaloneWSRequest, WSClient, WSRequest, WSRequestExecutor, WSRequestFilter}
import play.api.mvc._

import scala.concurrent.ExecutionContext

@Singleton
class HomeController @Inject()(cc: ControllerComponents,
                               myWsClient: MyWSClient)
                              (implicit executionContext: ExecutionContext)
  extends AbstractController(cc) {

  def index = Action.async {
    myWsClient.url("http://www.example.com")
      .get()
      .map(res => Ok(s"${res.status} ${res.statusText}"))(executionContext)
  }
}

引入过滤器的 WSClient 包装器:

@Singleton
class MyWSClient @Inject()(delegate: WSClient, fooBarFilter: FooBarFilter) extends WSClient {
  override def underlying[T]: T = delegate.underlying.asInstanceOf[T]

  override def url(url: String): WSRequest = {
    delegate.url(url)
      .withRequestFilter(fooBarFilter)
  }

  override def close(): Unit = delegate.close()
}

最后是 WS 过滤器本身:

@Singleton
class FooBarFilter extends WSRequestFilter {
  override def apply(executor: WSRequestExecutor): WSRequestExecutor = {
    (request: StandaloneWSRequest) => {
      request.addHttpHeaders(("X-Foo", "<...>")) // INSERT CORRECT VALUE HERE!
      executor.apply(request)
    }
  }
}

最后,期望是请求GET http://www.example.com包含 header X-Foo: Bar

使这更有趣的特殊要求是:

  • 你可以修改MyWsClient类。
  • 你可以修改FooBarFilter
  • 您可以创建 HTTP 控制器过滤器(play.api.mvc.(Essential)Filter如果有帮助的话。
  • 您可以创建其他类/对象/等
  • 您不能修改控制器(因为在我们的情况下,我们不能期望所有现有的控制器都被修改。
  • 即使在控制器和 WSClient 调用之间有一个“服务”层并且不涉及到处传递对象,该解决方案也应该可以工作。
  • 该解决方案可以更改其他 Play/Akka 机制,例如默认的 Dispatcher
4

1 回答 1

2

我没有尝试将它放入实际代码中并测试它是否有效,但这里有一个想法:看起来自从 Play 2.1 Http.Context 甚至通过 async call 传播。还有Http.Context._requestHeader。所以你可以尝试做的是改变MyWSClientFooBarFilter喜欢这样:

@Singleton
class MyWSClient @Inject()(delegate: WSClient) extends WSClient {
  override def underlying[T]: T = delegate.underlying.asInstanceOf[T]

  override def url(url: String): WSRequest = {
    val fooHeaderOption = Http.Context.current()._requestHeader().headers.get(FooHeaderFilter.fooHeaderName)
    val baseRequest = delegate.url(url)
    if (fooHeaderOption.isDefined)
      baseRequest.withRequestFilter(new FooHeaderFilter(fooHeaderOption.get))
    else
      baseRequest
  }

  override def close(): Unit = delegate.close()

  class FooHeaderFilter(headerValue: String) extends WSRequestFilter {

    import FooHeaderFilter._

    override def apply(executor: WSRequestExecutor): WSRequestExecutor = {
      (request: StandaloneWSRequest) => {
        request.addHttpHeaders((fooHeaderName, headerValue))
        executor.apply(request)
      }
    }
  }

  object FooHeaderFilter {
    val fooHeaderName = "X-Foo"
  }

}

这个想法很简单:从创建Http.Context.current()时提取标头并使用WSRequestWSRequestFilter

更新:让它在 Scala API 中工作

正如评论中指出的那样,这种方法在 Scala API 中不起作用,因为Http.Context没有初始化并且没有在线程之间传递。要使其发挥作用,需要更高级别的魔法。即你需要:

  1. 简单:一个过滤器,将为Http.ContextScala 处理的请求初始化
  2. 硬:覆盖AkkaExecutorServiceConfigurator默认调度程序以创建ExecutorServiceHttp.Context在线程切换之间传递的自定义。

过滤器很简单:

import play.mvc._
@Singleton
class HttpContextFilter @Inject()(implicit ec: ExecutionContext) extends EssentialFilter {
  override def apply(next: EssentialAction) = EssentialAction { request => {
    Http.Context.current.set(new Http.Context(new Http.RequestImpl(request), null))
    next(request)
  }
  }
}

并将其添加到play.filters.enabledapplication.conf

困难的部分是这样的:

class HttpContextWrapperExecutorService(val delegateEc: ExecutorService) extends AbstractExecutorService {
  override def isTerminated = delegateEc.isTerminated

  override def awaitTermination(timeout: Long, unit: TimeUnit) = delegateEc.awaitTermination(timeout, unit)

  override def shutdownNow() = delegateEc.shutdownNow()

  override def shutdown() = delegateEc.shutdown()

  override def isShutdown = delegateEc.isShutdown

  override def execute(command: Runnable) = {
    val newContext = Http.Context.current.get()
    delegateEc.execute(() => {
      val oldContext = Http.Context.current.get() // might be null!
      Http.Context.current.set(newContext)
      try {
        command.run()
      }
      finally {
        Http.Context.current.set(oldContext)
      }
    })
  }
}


class HttpContextExecutorServiceConfigurator(config: Config, prerequisites: DispatcherPrerequisites) extends ExecutorServiceConfigurator(config, prerequisites) {
  val delegateProvider = new ForkJoinExecutorConfigurator(config.getConfig("fork-join-executor"), prerequisites)

  override def createExecutorServiceFactory(id: String, threadFactory: ThreadFactory): ExecutorServiceFactory = new ExecutorServiceFactory {
    val delegateFactory = delegateProvider.createExecutorServiceFactory(id, threadFactory)

    override def createExecutorService: ExecutorService = new HttpContextWrapperExecutorService(delegateFactory.createExecutorService)
  }
}

并在使用时注册

akka.actor.default-dispatcher.executor = "so.HttpContextExecutorServiceConfigurator"

不要忘记so用你的真实包更新“”。此外,如果您使用更多自定义执行程序或ExecutionContexts,您应该修补(包装)它们以及传递Http.Context异步调用。

于 2018-01-30T23:12:21.343 回答