Writing a Filter
Suppose you wish to trace each request to the Micronaut “Hello World” example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations.
You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete.
As an example, consider this TraceService
that uses Project Reactor to compose an I/O operation:
A TraceService Example using Reactive Streams
import io.micronaut.http.HttpRequest;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.inject.Singleton;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@Singleton
public class TraceService {
private static final Logger LOG = LoggerFactory.getLogger(TraceService.class);
Publisher<Boolean> trace(HttpRequest<?> request) {
return Mono.fromCallable(() -> { (1)
LOG.debug("Tracing request: {}", request.getUri());
// trace logic here, potentially performing I/O (2)
return true;
}).subscribeOn(Schedulers.boundedElastic()) (3)
.flux();
}
}
A TraceService Example using Reactive Streams
import io.micronaut.http.HttpRequest
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import jakarta.inject.Singleton
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.util.concurrent.Callable
@Singleton
class TraceService {
private static final Logger LOG = LoggerFactory.getLogger(TraceService.class)
Flux<Boolean> trace(HttpRequest<?> request) {
Mono.fromCallable(() -> { (1)
LOG.debug('Tracing request: {}', request.uri)
// trace logic here, potentially performing I/O (2)
return true
}).flux().subscribeOn(Schedulers.boundedElastic()) (3)
}
}
A TraceService Example using Reactive Streams
import io.micronaut.http.HttpRequest
import org.slf4j.LoggerFactory
import jakarta.inject.Singleton
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
@Singleton
class TraceService {
private val LOG = LoggerFactory.getLogger(TraceService::class.java)
internal fun trace(request: HttpRequest<*>): Flux<Boolean> {
return Mono.fromCallable {
(1)
LOG.debug("Tracing request: {}", request.uri)
// trace logic here, potentially performing I/O (2)
true
}.subscribeOn(Schedulers.boundedElastic()) (3)
.flux()
}
}
1 | The reactor:Mono[] type creates logic that executes potentially blocking operations to write the trace data from the request |
2 | Since this is just an example, the logic does nothing yet |
3 | The Schedulers.boundedElastic executes the logic |
You can then inject this implementation into your filter definition:
An Example HttpServerFilter
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@Filter("/hello/**") (1)
public class TraceFilter implements HttpServerFilter { (2)
private final TraceService traceService;
public TraceFilter(TraceService traceService) { (3)
this.traceService = traceService;
}
}
An Example HttpServerFilter
import io.micronaut.http.HttpRequest
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.filter.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import org.reactivestreams.Publisher
@Filter("/hello/**") (1)
class TraceFilter implements HttpServerFilter { (2)
private final TraceService traceService
TraceFilter(TraceService traceService) { (3)
this.traceService = traceService
}
}
An Example HttpServerFilter
import io.micronaut.http.HttpRequest
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.filter.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import org.reactivestreams.Publisher
@Filter("/hello/**") (1)
class TraceFilter((2)
private val traceService: TraceService)(3)
: HttpServerFilter {
}
1 | The Filter annotation defines the URI pattern(s) the filter matches |
2 | The class implements the HttpServerFilter interface |
3 | The previously defined TraceService is injected via constructor |
The final step is to write the doFilter
implementation of the HttpServerFilter interface.
The doFilter implementation
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain) {
return Flux.from(traceService
.trace(request)) (1)
.switchMap(aBoolean -> chain.proceed(request)) (2)
.doOnNext(res ->
res.getHeaders().add("X-Trace-Enabled", "true") (3)
);
}
The doFilter implementation
@Override
Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain) {
traceService
.trace(request) (1)
.switchMap({ aBoolean -> chain.proceed(request) }) (2)
.doOnNext({ res ->
res.headers.add("X-Trace-Enabled", "true") (3)
})
}
The doFilter implementation
override fun doFilter(request: HttpRequest<*>,
chain: ServerFilterChain): Publisher<MutableHttpResponse<*>> {
return traceService.trace(request) (1)
.switchMap { aBoolean -> chain.proceed(request) } (2)
.doOnNext { res ->
res.headers.add("X-Trace-Enabled", "true") (3)
}
}
1 | TraceService is invoked to trace the request |
2 | If the call succeeds, the filter resumes request processing using Project Reactor‘s switchMap method, which invokes the proceed method of the ServerFilterChain |
3 | Finally, the Project Reactor‘s doOnNext method adds a X-Trace-Enabled header to the response. |
The previous example demonstrates some key concepts such as executing logic in a non-blocking manner before proceeding with the request and modifying the outgoing response.
The examples use Project Reactor, however you can use any reactive framework that supports the Reactive streams specifications |
The Filter annotation uses AntPathMatcher for path matching. The mapping matches URLs using the following rules:
? matches one character
* matches zero or more characters
** matches zero or more subdirectories in a path
Pattern | Example Matched Paths |
---|---|
| any path |
| customer/joy, customer/jay |
| customer/adam/id, com/amy/id |
| customer/adam, customer/adam/id, customer/adam/name |
| customer/index.html, customer/adam/profile.html, customer/adam/job/description.html == Error States The publisher returned from |