§过滤器
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 来使其看起来跳过了主体解析,而主体解析和操作的其余部分则异步执行。
下一步:错误处理
发现此文档中的错误?此页面的源代码可以在 此处 找到。在阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛,与社区开始对话。