Micronaut 中基于注释的 HTTP 过滤器

在本教程中,我们研究过滤器的一般概念以及 Micronaut 中的 HTTP 过滤器。我们会看到实现过滤器的不同选项以及一些实际用例。然后,我们展示基于注释的过滤器的示例,包括仅请求过滤器、仅响应过滤器以及两者。最后,我们花一些时间讨论路径模式和过滤器顺序等关键概念。

在本教程中,我们将介绍Micronaut 框架提供的带注释的 HTTP 过滤器。最初,Micronaut 中的 HTTP 过滤器更接近 Java EE过滤器接口和Spring Boot 过滤器方法。但随着最新主要版本的发布,过滤器现在可以基于注释,将请求和响应的过滤器分开。

在本教程中,我们将研究 Micronaut 中的 HTTP 过滤器。更具体地说,我们将重点介绍版本 4 中引入的服务器过滤器,即基于注释的过滤器方法。

HTTP 过滤器
HTTP 过滤器是作为 Java EE 中的接口引入的。它是所有 Java Web 框架中实现的“规范”。如文档所述:

过滤器是一个对象,它对资源(servlet 或静态内容)的请求或资源的响应(或两者)执行过滤任务。

实现 Java EE 接口的过滤器有一个doFilter()方法,该方法包含 3 个参数:ServletRequest、ServletResponse和FilterChain。这使我们能够访问请求对象和响应,并使用链将请求和响应传递给下一个组件。时至今日,即使是较新的框架仍可能使用相同或相似的名称和参数。

过滤器在一些常见的实际应用中非常有用:

  • 身份验证筛选器
  • 标头过滤器(从请求中检索值或在响应中添加值)
  • 指标过滤器(例如,记录请求执行时间时)
  • 日志过滤器
Micronaut 中的 HTTP 过滤器
Micronaut 中的 HTTP 过滤器在某种程度上遵循 Java EE过滤器规范。例如,Micronaut 的HttpFilter接口提供了一个doFilter()方法,该方法带有一个用于请求对象的参数和一个用于链对象的参数。请求参数允许我们过滤请求,然后使用链对象来处理它并返回响应。最后,如果需要,可以对响应对象进行更改。

在 Micronaut 4 中,引入了一些针对过滤器的新注释,这些注释仅针对请求、仅针对响应或两者提供了过滤方法。

Micronaut 使用@ServerFilter为接收的服务器请求和发送的响应提供过滤器。但它还使用@ClientFilter为我们的 REST 客户端提供针对第三方系统和微服务的请求的过滤器。

服务器过滤器具有一些使其非常灵活和有用的概念:

  • 接受一些模式来匹配我们想要过滤的路径
  • 可以排序,因为一些过滤器需要在其他过滤器之前执行(例如,身份验证检查过滤器应该始终放在第一个)
  • 提供有关过滤可能属于错误类型的响应的选项(例如过滤可抛出对象)
在接下来的段落中,我们将更详细地讨论其中一些概念。

过滤模式
Micronaut 中的 HTTP 过滤器根据路径特定于端点。要配置过滤器应用于哪个端点,我们可以设置一个模式来匹配路径。模式可以是不同的样式,如 ANT或REGEX,值是实际模式,如/endpoint*。

模式样式有不同的选项,但默认为AntPathMatcher,因为它在性能方面更高效。使用模式匹配时,Regex 是一种更强大的样式,但它比 Ant 慢得多。因此,当 Ant 不支持我们所需的样式时,我们应仅将其用作最后的选择。

使用过滤器时我们需要的一些样式示例包括:

  • /将匹配任何路径
  • /filters-annotations/将匹配 filters-annotations 下的所有路径,例如 /filters-annotations/endpoint1 和 /filters-annotations/endpoint2
  • /filters-annotations/*1将匹配“filters-annotations”下的所有路径,但仅当以“1”结尾时
  • /endpoint1将匹配所有以 'endpoint1' 结尾的路径
  • /endpoint*将匹配所有以“endpoint”结尾的路径以及末尾的任何额外内容
其中,在默认的FilterPatternStyle.ANT样式中:
  • *匹配零个或多个字符
  • 匹配路径中的零个或多个子目录
Micronaut 中基于注释的服务器过滤器
Micronaut 中带注释的 HTTP 过滤器是在 Micronaut 主版本 4 中添加的,也称为过滤器方法。过滤器方法允许我们将特定过滤器与请求或响应区分开来。在使用基于注释的过滤器之前,我们只有一种方法可以定义过滤器,并且过滤请求或响应的方法也一样。这样我们就可以分离关注点,从而使我们的代码更简洁、更易读。

过滤方法仍然允许我们定义一个过滤器,该过滤器既可以访问请求,也可以修改响应(如果需要),使用FilterContinuation。

 过滤方法
根据我们是否要过滤请求或响应,我们可以使用@RequestFilter或@ResponseFilter注释。在类级别,我们仍然需要一个注释来定义过滤器,即 @ServerFilter 。过滤的路径和过滤器的顺序是在类级别定义的。我们还可以选择按过滤方法应用路径模式。

让我们将所有这些信息结合起来创建一个ServerFilter,它有一个过滤请求的方法和另一个过滤响应的方法:

@Slf4j
@ServerFilter(patterns = { "</strong>/endpoint*" })
public class CustomFilter implements Ordered {
    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING)
    public void filterRequest(HttpRequest<?> request) {
        String customRequestHeader = request.getHeaders()
          .get(CUSTOM_HEADER_KEY);
        log.info(
"request header: {}", customRequestHeader);
    }
    @ResponseFilter
    public void filterResponse(MutableHttpResponse<?> res) {
        res.getHeaders()
          .add(X_TRACE_HEADER_KEY,
"true");
    }
}

filterRequest ()方法带有@RequestFilter注解,并接受HTTPRequest参数。这使我们能够访问请求。然后,它读取并记录请求中的标头。在实际示例中,这可能会做更多事情,例如根据传递的标头值拒绝请求。

filterResponse ()方法带有@ResponseFilter注释,并接受MutableHttpResponse参数,该参数是我们将要返回给客户端的响应对象。不过,在响应之前,此方法会在响应中添加一个标头。

请记住,请求可能已被我们拥有的另一个具有较低顺序的过滤器处理,并且可能接下来被另一个具有较高顺序的过滤器处理。同样,响应可能已被具有较高顺序的过滤器处理,并且随后将应用具有较低顺序的过滤器。有关更多信息,请参阅“过滤器顺序”段落。

延续Continuation
过滤方法是一个很好的功能,可以让我们的代码保持干净整洁。但是,仍然需要有过滤相同请求和响应的方法。Micronaut 提供了延续来满足这一要求。方法上的注释与请求中的注释@RequestFilter相同,但参数不同。我们还必须在类上使用@ServerFilter注释。

我们需要访问请求并在响应中使用值的一个典型示例是分布式系统中分布式跟踪模式的跟踪标头。从高层次上讲,我们使用标头来跟踪请求,以便我们了解如果返回错误,它在哪个步骤失败了。为此,我们需要在每个请求/消息中传递“request-id”或“trace-id”,如果服务与另一个服务通信,它会传递相同的值:

@Slf4j
@ServerFilter(patterns = { "<strong>/endpoint*" })
@Order(1)
public class RequestIDFilter implements Ordered {
    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING)
    public void filterRequestIDHeader(
        HttpRequest<?> request, 
        FilterContinuation<MutableHttpResponse<?>> continuation
    ) {
        String requestIdHeader = request.getHeaders().get(REQUEST_ID_HEADER_KEY);
        if (requestIdHeader == null || requestIdHeader.trim().isEmpty()) {
            requestIdHeader = UUID.randomUUID().toString();
            log.info(
               
"request ID not received. Created and will return one with value: [{}]"
                requestIdHeader
            );
        } else {
            log.info(
"request ID received. Request ID: [{}]", requestIdHeader);
        }
        MutableHttpResponse<?> res = continuation.proceed();
        res.getHeaders().add(REQUEST_ID_HEADER_KEY, requestIdHeader);
    }
}

filterRequestIDHeader ()方法带有@RequestFilter注解,并具有一个HttpRequest和一个FilterContinuation参数。我们从请求参数获取请求,并检查“Request-ID”标头是否有值。如果没有,我们将创建一个,并在任何情况下记录该值。

通过使用continuation.proceed()方法,我们可以访问响应对象。然后,我们在响应中添加与“Request-ID”标头相同的标头和值,以传播到客户端。

过滤顺序
在许多用例中,让特定过滤器在其他过滤器之前或之后执行是有意义的。Micronaut 中的 HTTP 过滤器提供了两种方法来处理过滤器执行的顺序。一种是@Order注释,另一种是实现Ordered接口。两者都是在类级别。

排序的工作方式是,我们提供一个 int 值,它是过滤器的执行顺序。对于请求过滤器,它很简单。顺序 -5 将在顺序 2 之前执行,顺序 2 将在顺序 4 之前执行。对于响应过滤器,情况正好相反。顺序 4 将首先应用,然后是顺序 2,最后是顺序 -5。

当我们实现接口时,我们需要手动重写getOrder()方法。它默认为零:

@Filter(patterns = { "</strong>/*1" })
public class PrivilegedUsersEndpointFilter implements HttpServerFilter, Ordered {
   
// filter methods ommited
    @Override
    public int getOrder() {
        return 3;
    }
}

当我们使用注释时,我们只需要设置值:

@ServerFilter(patterns = { "**/endpoint*" })
@Order(1)
public class RequestIDFilter implements Ordered {
   
// filter methods ommited
}

请注意,测试@Order注释和实现Ordered接口的组合会导致不当行为,因此选择两种方法中的一种并将其应用于任何地方是一种很好的做法。