Explain ChannelInterceptor: its callbacks, how it applies to subscribable vs pollable channels, and what you'd use it for.
answer
- preSend / postSend / afterSendCompletion (all channels)
- preReceive / postReceive / afterReceiveCompletion (pollable only)
- preSend returns null => veto/filter the message
- afterSendCompletion = finally (runs on exception too)
- @GlobalChannelInterceptor + WireTap; ExecutorChannelInterceptor wraps the handler
basics
~20 sA ChannelInterceptor is a hook attached to a channel that fires around send and receive. Its callbacks (preSend, postSend, afterSendCompletion, preReceive, postReceive, afterReceiveCompletion) let you inspect, transform, filter, log, or measure messages without changing endpoints. Receive callbacks only apply to pollable channels.
solid answer
~40 s`ChannelInterceptor` (in `org.springframework.messaging.support`) wraps a channel's send/receive so you can cross-cut behaviour without touching producers or consumers. **Send callbacks** fire on every channel: `preSend` (before dispatch — can modify the message or return `null` to *veto* the send), `postSend` (after dispatch, with a `sent` flag), and `afterSendCompletion` (always, even on exception — good for cleanup/metrics). **Receive callbacks** apply only to `PollableChannel`s (there's an actual `receive()`): `preReceive`, `postReceive` (can inspect/replace the received message or return `null` to drop it), and `afterReceiveCompletion`. You attach them via `channel.addInterceptor(...)`, or register a `@GlobalChannelInterceptor` (with a pattern) to apply across many channels. Spring's `WireTap` is itself a ChannelInterceptor. Typical uses: logging/tracing, metrics, security checks, message enrichment, and conditional filtering.
code
java · 30 linesimport org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.stereotype.Component;
@Component
@GlobalChannelInterceptor(patterns = "orders*") // applies to all matching channels
public class AuditInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
if (message.getHeaders().get("blocked", Boolean.class) == Boolean.TRUE) {
return null; // veto: message is dropped, not dispatched
}
// could also return a modified/enriched message here
return message;
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel,
boolean sent, Exception ex) {
// runs even when dispatch threw -> ideal for metrics/cleanup
if (ex != null) {
// record failure metric / release resources
}
}
// preReceive/postReceive would only fire for a PollableChannel (e.g. QueueChannel)
}go deeper
Know an interceptor is a hook around send/receive used for logging.
Name the six callbacks and that receive callbacks are pollable-only; preSend can transform the message.
Explain veto-by-null, afterSendCompletion-as-finally, interceptor chain ordering, and global registration/WireTap.
Discuss ExecutorChannelInterceptor for handler-scope concerns (security/observability), nesting/cleanup semantics, and designing cross-cutting concerns at the channel layer vs endpoints.
**What it is:** `org.springframework.messaging.support.ChannelInterceptor` is an interface whose implementations are attached to a `MessageChannel` (which implements `InterceptableChannel`, exposing `addInterceptor`). It provides aspect-style hooks around the channel's `send()` and, for pollable channels, `receive()` — letting you cross-cut concerns (logging, tracing, metrics, security, transformation, filtering) uniformly at the pipe level, independent of the endpoints on either side. **The callbacks (all have default no-op/pass-through implementations, so you override only what you need):** - `preSend(Message, MessageChannel)` — called before the message is dispatched. You may return a modified `Message`, the same message, or `null`. Returning `null` **aborts the send** (a veto/filter). This is where enrichment or authorization gating happens. - `postSend(Message, MessageChannel, boolean sent)` — called after the send attempt; the `sent` flag says whether dispatch succeeded (e.g. false if a point-to-point dispatcher found no subscriber and didn't throw). Runs before the send() call returns. - `afterSendCompletion(Message, MessageChannel, boolean sent, Exception ex)` — always called after send completes, whether it succeeded or threw (`ex` non-null on failure). This is the place for cleanup, timing/metrics finalization, and error accounting. Analogous to a finally block. - `preReceive(MessageChannel)` — **PollableChannel only**; before `receive()` pulls. Return `false` to skip receiving. - `postReceive(Message, MessageChannel)` — **PollableChannel only**; after a message is received. May return a modified message or `null` to discard it. - `afterReceiveCompletion(Message, MessageChannel, Exception)` — **PollableChannel only**; always after receive completes (success, empty, or exception). **Subscribable vs pollable:** Subscribable channels (DirectChannel, ExecutorChannel, PublishSubscribeChannel) only trigger the *send* callbacks, because there is no explicit receive step — the channel pushes to handlers. Pollable channels (QueueChannel, PriorityChannel, RendezvousChannel) trigger *both* send and receive callbacks. So a `postReceive` interceptor on a DirectChannel would simply never fire. **Ordering & multiple interceptors:** Interceptors form a chain. On send, `preSend` runs in registration order; the corresponding `afterSendCompletion` runs in *reverse* order (nested, like a stack), and only for interceptors whose `preSend` actually ran. If an early `preSend` returns null, later interceptors' preSend are skipped and completion is invoked appropriately. Understanding this nesting matters when interceptors allocate/free resources (e.g. tracing spans, MDC entries). **Registration options:** 1. Programmatic: `channel.addInterceptor(new MyInterceptor())` or `setInterceptors(list)`. 2. Global: annotate a `ChannelInterceptor` bean with `@GlobalChannelInterceptor(patterns = "orders*", order = 0)` to auto-apply to all channels whose bean name matches the pattern — powerful for uniform tracing/metrics across a whole app. **Built-in interceptors:** `WireTap` is a `ChannelInterceptor` that (in `preSend`) copies each message to a secondary channel for tapping/monitoring without disturbing the main flow. Spring's messaging/observability also uses interceptors under the hood (e.g. security context propagation, Micrometer observation) — many via `ExecutorChannelInterceptor`, a sub-interface with extra `beforeHandle`/`afterMessageHandled` callbacks for executor/direct dispatch so behaviour can wrap the actual *handler* invocation (used e.g. by Spring Security's `SecurityContextChannelInterceptor` to set the SecurityContext around handling). **Use cases:** structured logging and distributed tracing; Micrometer metrics (count/latency per channel); security (propagate/clear SecurityContext, authorization gating in preSend); message enrichment/normalization; conditional filtering (preSend returns null); auditing; wire-tapping for debugging. **Gotchas:** (1) Returning null from preSend silently drops the message — easy to misdiagnose as 'lost messages'. (2) Receive callbacks won't fire on subscribable channels — don't rely on postReceive for a DirectChannel. (3) afterSendCompletion runs even on failure — put resource cleanup there, not only in postSend (which is skipped on exception). (4) Global interceptor patterns match channel *bean names*; anonymous/default channels may not match. (5) For wrapping the actual handler execution (not just the send), you need `ExecutorChannelInterceptor`, not the plain interface.
- You added a postReceive interceptor to a DirectChannel and it never fires. Why?DirectChannel is a SubscribableChannel — it pushes messages to handlers and has no receive() step, so only the send-side callbacks (preSend/postSend/afterSendCompletion) fire. Receive callbacks (preReceive/postReceive/afterReceiveCompletion) only run on PollableChannels like QueueChannel or PriorityChannel.
- How would you drop/filter a message at the channel level without a filter endpoint, and what's the risk?Implement a ChannelInterceptor whose preSend returns null — the send is aborted and the message is discarded. The risk is silence: there's no exception and no obvious trace, so 'missing messages' become hard to debug. Prefer logging the drop, and consider a real filter with a discard channel when observability matters.
- What is ExecutorChannelInterceptor for, versus a plain ChannelInterceptor?ExecutorChannelInterceptor extends ChannelInterceptor with beforeHandle/afterMessageHandled callbacks that wrap the actual handler invocation (relevant for DirectChannel/ExecutorChannel where dispatch invokes the handler). It lets you set up/tear down context around handling itself — e.g. SecurityContextChannelInterceptor uses it to populate the SecurityContext on the handling thread.
saying these in an interview costs you the question
- Thinking preReceive/postReceive fire on DirectChannel or PublishSubscribeChannel
- Not knowing preSend returning null aborts the send
- Putting cleanup only in postSend (skipped on exception) instead of afterSendCompletion
- Assuming @GlobalChannelInterceptor matches by class rather than channel bean-name pattern
- Believing a plain ChannelInterceptor can wrap the handler execution (that needs ExecutorChannelInterceptor)