How does @MessageMapping resolve handler-method arguments, and what role does MessageHandlerMethodFactory play?
answer
- factory builds InvocableHandlerMethod from bean+method
- resolvers: @Payload / @Header / @Headers / Message / @DestinationVariable
- first supportsParameter wins; Payload is the fallback
- MessageConverter deserializes payload, Validator validates
- same factory powers Jms/Rabbit/Kafka listeners + STOMP/RSocket
basics
~20 sSpring turns an annotated @MessageMapping method into an invocable handler and, for each parameter, picks an argument resolver based on annotations like @Payload, @Header, or the type Message. MessageHandlerMethodFactory is the component that builds that invocable method and configures the resolvers.
solid answer
~40 s@MessageMapping methods aren't the raw MessageHandler interface — Spring adapts them. A MessageHandlerMethodFactory (default: DefaultMessageHandlerMethodFactory) takes a bean + Method and produces an InvocableHandlerMethod, wiring in a list of HandlerMethodArgumentResolvers and return-value handlers. At runtime, when a message arrives, the framework calls that InvocableHandlerMethod, which walks each parameter and asks the resolvers which one supportsParameter; the winner extracts the value from the Message. Typical resolvers: PayloadMethodArgumentResolver (@Payload or an unannotated POJO — it deserializes via a MessageConverter and can @Valid-validate), HeaderMethodArgumentResolver (@Header), HeadersMethodArgumentResolver (@Headers → the whole map), MessageMethodArgumentResolver (the full Message<?>), and DestinationVariableMethodArgumentResolver (@DestinationVariable from the destination template). This same factory underpins the @JmsListener/@RabbitListener/@KafkaListener adapters and STOMP/RSocket messaging, which is why @Payload/@Header work uniformly across them.
code
java · 20 linesimport org.springframework.messaging.handler.annotation.*;
import org.springframework.stereotype.Controller;
@Controller
public class OrderController {
// Each parameter is filled by a different HandlerMethodArgumentResolver,
// wired by the MessageHandlerMethodFactory:
@MessageMapping("/orders/{id}") // destination template
public Ack place(
@DestinationVariable String id, // DestinationVariableMethodArgumentResolver
@Payload @jakarta.validation.Valid OrderDto order, // PayloadMethodArgumentResolver (+ validation)
@Header("tenant") String tenant, // HeaderMethodArgumentResolver
@Headers java.util.Map<String,Object> all) { // HeadersMethodArgumentResolver
return new Ack(id, tenant, order);
}
}
record OrderDto(String sku, int qty) {}
record Ack(String id, String tenant, OrderDto order) {}go deeper
Know that @Payload gets the body and @Header gets a header; the framework fills the parameters for you.
Name the common resolvers and that MessageHandlerMethodFactory builds the invocable method.
Explain supportsParameter ordering, the Payload fallback, MessageConverter/Validator roles, and that the same factory powers listener annotations and STOMP.
Discuss customizing resolvers/converters, ordering pitfalls, and unifying binding semantics across transports.
## The problem it solves The low-level contract is `MessageHandler.handleMessage(Message<?>)` — one opaque `Message`. But you want to write ergonomic methods like: ```java @MessageMapping("/orders/{id}") public Ack place(@DestinationVariable String id, @Payload OrderDto order, @Header("tenant") String tenant) { ... } ``` Something must **bridge** the single-argument `Message` into these typed, annotated parameters. That bridge is built by a **`MessageHandlerMethodFactory`**. ## MessageHandlerMethodFactory `org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory` has one job: `InvocableHandlerMethod createInvocableHandlerMethod(Object bean, Method method)`. The default implementation, **`DefaultMessageHandlerMethodFactory`**, holds: - a **`MessageConverter`** (default `GenericMessageConverter`/`CompositeMessageConverter`) used to convert/deserialize payloads, - an optional **`Validator`** (for `@Valid`/`@Validated` payloads), - a **`HandlerMethodArgumentResolverComposite`** — the ordered list of resolvers, - and return-value handlers. It produces an **`InvocableHandlerMethod`**: a wrapper around the bean+method that knows how to fill every argument and invoke it. ## Argument resolution at runtime When a message is dispatched to the method, the `InvocableHandlerMethod` iterates over each `MethodParameter` and consults the resolver composite. Each **`HandlerMethodArgumentResolver`** answers `boolean supportsParameter(MethodParameter)`; the **first** that supports it is asked to `resolveArgument(parameter, message)`. Built-in resolvers (registered by `DefaultMessageHandlerMethodFactory.initArgumentResolvers()`), roughly in order: - **`HeaderMethodArgumentResolver`** — `@Header("x")` → a single header value (with `defaultValue`, `required`, type conversion). - **`HeadersMethodArgumentResolver`** — `@Headers` on a `Map`, or a parameter of type `MessageHeaders` → the whole header map. - **`DestinationVariableMethodArgumentResolver`** — `@DestinationVariable` → a URI-template variable captured from the destination (e.g. `{id}`). - **`MessageMethodArgumentResolver`** — a parameter typed `Message<?>` → the whole message. - **`PayloadMethodArgumentResolver`** — the **catch-all/last**: handles `@Payload` *and any un-annotated, non-simple parameter*. It uses the `MessageConverter` to convert the payload to the target type and, if a `Validator` is set and the param is `@Valid`/`@Validated`, validates it (throwing `MethodArgumentNotValidException`). Because `PayloadMethodArgumentResolver` is the fallback, an un-annotated POJO parameter is treated as the payload — that's why `@Payload` is often optional. ## Where this actually runs You rarely call the factory yourself. It's used by the annotation infrastructure: - **STOMP/WebSocket**: `SimpAnnotationMethodMessageHandler` (adds `@DestinationVariable`, `@Header`, `SimpMessageHeaderAccessor`, `Principal`, `@SendTo` handling). - **RSocket**: `RSocketMessageHandler`. - **JMS/AMQP/Kafka listeners**: `@JmsListener`/`@RabbitListener`/`@KafkaListener` go through a `MessageHandlerMethodFactory` (via `MessagingMessageListenerAdapter`) — which is exactly why `@Payload`/`@Header`/`@Headers` behave consistently there. ## Customization & gotchas - To add a **custom argument resolver** (or plug your own `Validator`/`MessageConverter`) for listeners, override `configureMessageListenerContainerFactory` style config, or declare a `DefaultMessageHandlerMethodFactory` bean and set it on the annotation config (`JmsListenerConfigurer#configureJmsListeners` / `getMessageHandlerMethodFactory`). For WebSocket, override `WebSocketMessageBrokerConfigurer#addArgumentResolvers`. - **Resolver order matters**: since the first supporting resolver wins and `PayloadMethodArgumentResolver` supports almost anything, custom resolvers must be registered *before* it. - **`@Header` vs `@Payload` confusion**: forgetting `@Header` on a String param means it's treated as the payload (converted from the body), a frequent bug. - Validation only fires if a `Validator` is configured on the factory **and** the payload param carries `@Valid`/`@Validated`. - This is *not* Spring MVC's `HandlerMethodArgumentResolver` from `web` — it's the messaging-side sibling (`org.springframework.messaging.handler.invocation`), though the design mirrors it. ## When to care Know this when: building STOMP/RSocket endpoints, customizing how `@KafkaListener`/`@RabbitListener` bind arguments, adding a custom `@Header`-like annotation, or debugging why a parameter isn't being populated (usually the wrong resolver won).
- If a String parameter has no annotation, how is it resolved?PayloadMethodArgumentResolver is the fallback and supports un-annotated parameters, so an unannotated arg is treated as the payload and converted from the message body via the MessageConverter — often a bug when you meant to read a header.
- How do you add a custom argument resolver so it wins over @Payload's fallback?Register it on the MessageHandlerMethodFactory (or via the framework hook, e.g. WebSocketMessageBrokerConfigurer#addArgumentResolvers) so it appears before PayloadMethodArgumentResolver — the first resolver whose supportsParameter returns true is used, and Payload is the catch-all.
saying these in an interview costs you the question
- Confusing messaging HandlerMethodArgumentResolver with Spring MVC's web one
- Thinking @Payload is always required (it's the fallback resolver)
- Assuming validation runs without a configured Validator and @Valid
- Believing @MessageMapping methods implement MessageHandler directly rather than being adapted via InvocableHandlerMethod