在 Play 2.6 中,如何编写从 parent 请求转发 headers 的 WS 客户端过滤器?
In Play 2.6, how to write a WS Client filter that forwards headers from a parent request?
如果我有一个名为 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
class.
- 可以修改
FooBarFilter
class
- 您可以创建 HTTP 控制器过滤器(
play.api.mvc.(Essential)Filter
如果有帮助。
- 您可以创建其他 classes/objects/etc
- 您不能修改控制器(因为在我们的情况下,我们不能期望修改所有现有控制器。
- 即使在控制器和 WSClient 调用之间有一个 "service" 层并且不涉及在任何地方向下传递 objects,该解决方案也应该有效。
- 该解决方案可以改变其他 Play/Akka 机制,例如默认的 Dispatcher
我还没有尝试将它放入实际代码中并测试它是否有效,但这里有一个想法:从 Play 2.1 Http.Context is propagated even across async call. And there is Http.Context._requestHeader 开始看起来像。所以你可以尝试做的是改变 MyWSClient
和 FooBarFilter
像这样:
@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"
}
}
想法很简单:在创建 WSRequest
时从 Http.Context.current()
中提取 header 并使用 WSRequestFilter
将其附加到请求中
更新:让它在 Scala 中工作 API
正如评论中指出的那样,这种方法在 Scala API 中不起作用,因为 Http.Context
未初始化且未在线程之间传递。要使其工作,需要更高级别的魔法。即你需要:
- 简单:过滤器将为 Scala-handled 个请求初始化
Http.Context
- 困难:为 Akka's default dispatcher 覆盖
ExecutorServiceConfigurator
以创建将在线程切换之间传递 Http.Context
的自定义 ExecutorService
。
过滤器很简单:
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"
不要忘记用你的真实包更新“so
”。此外,如果您使用更多自定义执行程序或 ExecutionContext
s,您还应该修补(包装)它们以在异步调用中传递 Http.Context
。
如果我有一个名为 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
class. - 可以修改
FooBarFilter
class - 您可以创建 HTTP 控制器过滤器(
play.api.mvc.(Essential)Filter
如果有帮助。 - 您可以创建其他 classes/objects/etc
- 您不能修改控制器(因为在我们的情况下,我们不能期望修改所有现有控制器。
- 即使在控制器和 WSClient 调用之间有一个 "service" 层并且不涉及在任何地方向下传递 objects,该解决方案也应该有效。
- 该解决方案可以改变其他 Play/Akka 机制,例如默认的 Dispatcher
我还没有尝试将它放入实际代码中并测试它是否有效,但这里有一个想法:从 Play 2.1 Http.Context is propagated even across async call. And there is Http.Context._requestHeader 开始看起来像。所以你可以尝试做的是改变 MyWSClient
和 FooBarFilter
像这样:
@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"
}
}
想法很简单:在创建 WSRequest
时从 Http.Context.current()
中提取 header 并使用 WSRequestFilter
更新:让它在 Scala 中工作 API
正如评论中指出的那样,这种方法在 Scala API 中不起作用,因为 Http.Context
未初始化且未在线程之间传递。要使其工作,需要更高级别的魔法。即你需要:
- 简单:过滤器将为 Scala-handled 个请求初始化
Http.Context
- 困难:为 Akka's default dispatcher 覆盖
ExecutorServiceConfigurator
以创建将在线程切换之间传递Http.Context
的自定义ExecutorService
。
过滤器很简单:
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"
不要忘记用你的真实包更新“so
”。此外,如果您使用更多自定义执行程序或 ExecutionContext
s,您还应该修补(包装)它们以在异步调用中传递 Http.Context
。