在Play 2.6中,如何编写从父请求转发标头的WS Client过滤器?

时间:2018-01-30 18:04:01

标签: scala playframework playframework-2.6

如果我有一个名为HomeController的控制器收到GET /foo请求,标题为X-Foo: Bar,我想创建一个WS客户端过滤器来读取{{1}在上下文中,将标头值复制到传出的WS请求。

示例控制器:

RequestHeader

引入过滤器的WSClient包装器:

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

最后是WS过滤器本身:

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

最后,期望请求@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

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

  • 您可以修改X-Foo: Bar类。
  • 您可以修改MyWsClient
  • 如果有帮助,您可以创建HTTP控制器过滤器(FooBarFilter
  • 您可以创建其他类/对象/ etc
  • 您无法修改控制器(因为在我们的情况下,我们不能指望修改所有现有控制器。
  • 即使控制器和WSClient调用之间存在“服务”层,并且不涉及在任何地方传递对象,解决方案也应该有效。
  • 该解决方案可以改变其他Play / Akka机制,例如默认的Dispatcher

1 个答案:

答案 0 :(得分:1)

我还没有尝试将其放入实际代码并测试它是否有效但这是一个想法:它看起来像Play 2.1 Http.Context is propagated even across 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()时从WSRequest中提取标题,然后使用WSRequestFilter

将其附加到请求中

更新:在Scala API中使用

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

  1. 简单:为Scala处理的请求初始化Http.Context的过滤器
  2. 硬:覆盖Akka's default dispatcherExecutorServiceConfigurator以创建将在线程切换之间传递ExecutorService的自定义Http.Context
  3. 过滤器很简单:

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

    然后将其添加到application.conf

    中的play.filters.enabled

    困难的部分是这样的:

    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"
    

    不要忘记更新&#34; so&#34;和你一起真正的包裹。此外,如果您使用更多自定义执行程序或ExecutionContext,您也应该修补(换行)它们以便在异步调用中传递Http.Context