文档

§过滤器

Play 提供了一个简单的过滤器 API,用于将全局过滤器应用于每个请求。

§过滤器与操作组合

过滤器 API 旨在用于无差别地应用于所有路由的跨领域问题。例如,以下是过滤器的常见用例

相反,操作组合 旨在用于特定于路由的问题,例如身份验证和授权、缓存等。如果您的过滤器不是您希望应用于每个路由的过滤器,请考虑使用操作组合,它更强大。并且不要忘记,您可以创建自己的操作构建器,将您自己定义的操作集组合到每个路由,以最大程度地减少样板代码。

§一个简单的日志记录过滤器

以下是 Play Framework 中一个简单的过滤器,它计时并记录请求执行所需的时间,该过滤器实现了 Filter 特性

import java.util.concurrent.CompletionStage;
import java.util.function.Function;
import javax.inject.Inject;
import org.apache.pekko.stream.Materializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.mvc.*;

public class LoggingFilter extends Filter {

  private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);

  @Inject
  public LoggingFilter(Materializer mat) {
    super(mat);
  }

  @Override
  public CompletionStage<Result> apply(
      Function<Http.RequestHeader, CompletionStage<Result>> nextFilter,
      Http.RequestHeader requestHeader) {
    long startTime = System.currentTimeMillis();
    return nextFilter
        .apply(requestHeader)
        .thenApply(
            result -> {
              long endTime = System.currentTimeMillis();
              long requestTime = endTime - startTime;

              log.info(
                  "{} {} took {}ms and returned {}",
                  requestHeader.method(),
                  requestHeader.uri(),
                  requestTime,
                  result.status());

              return result.withHeader("Request-Time", "" + requestTime);
            });
  }
}

让我们了解一下这里发生了什么。首先要注意的是 apply 方法的签名。第一个参数 nextFilter 是一个函数,它接受一个请求头并产生一个结果。第二个参数 requestHeader 是传入请求的实际请求头。

nextFilter 参数表示过滤器链中的下一个操作。调用它将导致操作被调用。在大多数情况下,您希望在将来的某个时间点调用它。如果由于某种原因您想阻止请求,您可能决定不调用它。

在调用链中的下一个过滤器之前,我们保存一个时间戳。调用下一个过滤器会返回一个CompletionStage<Result>,它最终会被兑现。有关异步结果的更多详细信息,请查看处理异步结果章节。然后,我们通过调用带有闭包的thenApply方法来操作未来的Result,该闭包接受一个Result。我们计算请求所花费的时间,记录它并通过调用result.withHeader("Request-Time", "" + requestTime)将其发送回响应头中的客户端。

§使用过滤器

使用过滤器的最简单方法是在根包中提供HttpFilters接口的实现,名为Filters。通常,您应该扩展DefaultHttpFilters类并将您的过滤器传递给可变参数构造函数

import javax.inject.Inject;
import play.filters.gzip.GzipFilter;
import play.http.DefaultHttpFilters;

public class Filters extends DefaultHttpFilters {
  @Inject
  public Filters(GzipFilter gzip, LoggingFilter logging) {
    super(gzip, logging);
  }
}

如果您想在不同的环境中使用不同的过滤器,或者不想将此类放在根包中,您可以通过在application.conf中将play.http.filters设置为类的完全限定类名来配置 Play 应该在哪里找到该类。例如

play.http.filters=com.example.Filters

§过滤器在哪里适合?

过滤器在路由器查找完操作后包装操作。这意味着您不能使用过滤器来转换路径、方法或查询参数以影响路由器。但是,您可以通过直接从过滤器调用该操作来将请求定向到不同的操作,但请注意,这将绕过过滤器链的其余部分。如果您确实需要在调用路由器之前修改请求,更好的方法是将您的逻辑放在HttpRequestHandler中。

由于过滤器是在路由完成之后应用的,因此可以通过RequestHeader上的attrs映射从请求中访问路由信息。例如,您可能希望记录针对操作方法的时间。在这种情况下,您可能需要更新过滤器以使其看起来像这样

import java.util.concurrent.CompletionStage;
import java.util.function.Function;
import javax.inject.Inject;
import org.apache.pekko.stream.Materializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.api.routing.HandlerDef;
import play.mvc.*;
import play.routing.Router;

public class RoutedLoggingFilter extends Filter {

  private static final Logger log = LoggerFactory.getLogger(RoutedLoggingFilter.class);

  @Inject
  public RoutedLoggingFilter(Materializer mat) {
    super(mat);
  }

  @Override
  public CompletionStage<Result> apply(
      Function<Http.RequestHeader, CompletionStage<Result>> nextFilter,
      Http.RequestHeader requestHeader) {
    long startTime = System.currentTimeMillis();
    return nextFilter
        .apply(requestHeader)
        .thenApply(
            result -> {
              HandlerDef handlerDef = requestHeader.attrs().get(Router.Attrs.HANDLER_DEF);
              String actionMethod = handlerDef.controller() + "." + handlerDef.method();
              long endTime = System.currentTimeMillis();
              long requestTime = endTime - startTime;

              log.info("{} took {}ms and returned {}", actionMethod, requestTime, result.status());

              return result.withHeader("Request-Time", "" + requestTime);
            });
  }
}

注意:路由属性是 Play 路由器的功能。如果您使用自定义路由器,这些参数可能不可用。

§更强大的过滤器

Play 提供了一个更低级的过滤器 API,称为EssentialFilter,它允许您完全访问请求主体。此 API 允许您使用另一个操作来包装EssentialAction

以下是上面过滤器示例的EssentialFilter重写版本

import java.util.concurrent.Executor;
import javax.inject.Inject;
import org.apache.pekko.util.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.libs.streams.Accumulator;
import play.mvc.*;

public class EssentialLoggingFilter extends EssentialFilter {

  private static final Logger log = LoggerFactory.getLogger(EssentialLoggingFilter.class);

  private final Executor executor;

  @Inject
  public EssentialLoggingFilter(Executor executor) {
    super();
    this.executor = executor;
  }

  @Override
  public EssentialAction apply(EssentialAction next) {
    return EssentialAction.of(
        request -> {
          long startTime = System.currentTimeMillis();
          Accumulator<ByteString, Result> accumulator = next.apply(request);
          return accumulator.map(
              result -> {
                long endTime = System.currentTimeMillis();
                long requestTime = endTime - startTime;

                log.info(
                    "{} {} took {}ms and returned {}",
                    request.method(),
                    request.uri(),
                    requestTime,
                    result.status());

                return result.withHeader("Request-Time", "" + requestTime);
              },
              executor);
        });
  }
}

除了创建一个新的 EssentialAction 来包装传入的 next 操作之外,关键的区别在于,当我们调用 next 时,我们会得到一个 Accumulator。您可以使用 through 方法将此与 Pekko Streams Flow 组合起来,对流进行一些转换,如果您愿意的话。然后我们 map 迭代器的结果,从而处理它。

注意:虽然看起来有两个不同的过滤器 API,但实际上只有一个,即 EssentialFilter。早期示例中更简单的 Filter API 扩展了 EssentialFilter,并通过创建一个新的 EssentialAction 来实现它。传入的回调通过为 Result 创建一个 promise 来使其看起来跳过了主体解析,而主体解析和操作的其余部分则异步执行。

下一步:错误处理


发现此文档中的错误?此页面的源代码可以在 此处 找到。在阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛,与社区开始对话。