Annotation-driven message handling with @MessageMapping, @Payload, @Header, @Headers, @DestinationVariable, @SendTo for declarative message processing using Spring's annotation programming model.
Maps messages to handler methods based on destination patterns.
/**
* Annotation for mapping a Message onto message-handling methods by matching
* the message destination to the annotation's value.
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MessageMapping {
/**
* Destination patterns to match. Ant-style path patterns are supported.
* At the type level, this defines a prefix for methods.
* At the method level, this defines the actual mapping pattern.
*/
String[] value() default {};
}Usage Examples:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
// Simple destination mapping
@MessageMapping("/chat")
public void handleChat(String message) {
System.out.println("Chat message: " + message);
}
// Multiple destinations
@MessageMapping({"/hello", "/hi", "/greet"})
public String handleGreeting(String name) {
return "Hello, " + name + "!";
}
// Path patterns with wildcards
@MessageMapping("/topic/*")
public void handleTopic(String message) {
System.out.println("Topic message: " + message);
}
// Ant-style patterns
@MessageMapping("/orders/**")
public void handleOrders(String order) {
System.out.println("Order: " + order);
}
}
// Type-level prefix
@Controller
@MessageMapping("/app")
public class ApplicationController {
// Actual destination: /app/data
@MessageMapping("/data")
public void handleData(String data) {
System.out.println("Data: " + data);
}
// Actual destination: /app/status
@MessageMapping("/status")
public void handleStatus() {
System.out.println("Status check");
}
}Extracts and binds the message payload to a method parameter, with optional validation and SpEL expression support.
/**
* Annotation that binds a method parameter to the payload of a message.
* Can also apply validation and use SpEL expressions.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Payload {
/**
* A SpEL expression to extract a subset of the payload.
* Default is empty (use entire payload).
*/
String expression() default "";
/**
* Whether payload content is required.
* Default is true, leading to an exception if no payload is present.
*/
boolean required() default true;
}Usage Examples:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import jakarta.validation.Valid;
@Controller
public class PayloadExampleController {
// Extract entire payload
@MessageMapping("/user/create")
public void createUser(@Payload User user) {
// user is the deserialized payload
System.out.println("Creating user: " + user.getName());
}
// With validation
@MessageMapping("/user/validate")
public void validateUser(@Valid @Payload User user) {
// Validation performed before method invocation
System.out.println("Valid user: " + user.getName());
}
// Optional payload
@MessageMapping("/user/optional")
public void optionalUser(@Payload(required = false) User user) {
if (user != null) {
System.out.println("User provided: " + user.getName());
} else {
System.out.println("No user provided");
}
}
// SpEL expression to extract field
@MessageMapping("/user/name")
public void extractName(@Payload("name") String name) {
System.out.println("User name: " + name);
}
// Complex SpEL expression
@MessageMapping("/order/total")
public void calculateTotal(@Payload("items.![price].sum()") Double total) {
System.out.println("Order total: " + total);
}
// Without @Payload annotation (implicit)
@MessageMapping("/simple")
public void handleSimple(String message) {
// String parameter automatically bound to payload
}
}
class User {
private String name;
private String email;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}Binds a method parameter to a message header value.
/**
* Annotation which indicates that a method parameter should be bound to
* a message header.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Header {
/**
* The name of the header to bind to.
* Alias for name().
*/
String value() default "";
/**
* The name of the header to bind to.
*/
String name() default "";
/**
* Whether the header is required.
* Default is true, leading to an exception if the header is missing.
*/
boolean required() default true;
/**
* The default value to use as a fallback when the header is not present.
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
}Usage Examples:
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
@Controller
public class HeaderExampleController {
// Extract single header
@MessageMapping("/with-sender")
public void handleWithSender(String message,
@Header("sender") String sender) {
System.out.println("Message from " + sender + ": " + message);
}
// Using name attribute
@MessageMapping("/with-priority")
public void handleWithPriority(String message,
@Header(name = "priority") int priority) {
System.out.println("Priority " + priority + " message: " + message);
}
// Optional header
@MessageMapping("/optional-header")
public void handleOptional(String message,
@Header(value = "timestamp", required = false) Long timestamp) {
if (timestamp != null) {
System.out.println("Message timestamp: " + timestamp);
}
}
// Header with default value
@MessageMapping("/with-default")
public void handleWithDefault(String message,
@Header(value = "language", defaultValue = "en") String language) {
System.out.println("Message language: " + language);
}
// Type conversion
@MessageMapping("/typed-headers")
public void handleTypedHeaders(String message,
@Header("count") Integer count,
@Header("enabled") Boolean enabled,
@Header("timestamp") Long timestamp) {
// Automatic type conversion from header values
}
// Multiple headers
@MessageMapping("/multi-headers")
public void handleMultiHeaders(@Header("sessionId") String sessionId,
@Header("userId") String userId,
@Header("deviceId") String deviceId,
String message) {
System.out.println("Session: " + sessionId + ", User: " + userId);
}
}Binds a method parameter to all message headers as a Map.
/**
* Annotation which indicates that a method parameter should be bound to
* the headers of a message. The annotated parameter must be assignable to
* Map with String keys and Object values.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Headers {
}Usage Examples:
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.MessageHeaders;
import java.util.Map;
@Controller
public class HeadersExampleController {
// Bind to Map
@MessageMapping("/all-headers")
public void handleAllHeaders(String message,
@Headers Map<String, Object> headers) {
System.out.println("Received " + headers.size() + " headers");
headers.forEach((key, value) ->
System.out.println(key + ": " + value)
);
}
// Bind to MessageHeaders
@MessageMapping("/message-headers")
public void handleMessageHeaders(String message,
@Headers MessageHeaders headers) {
System.out.println("Message ID: " + headers.getId());
System.out.println("Timestamp: " + headers.getTimestamp());
}
// Access specific headers from map
@MessageMapping("/selective-headers")
public void handleSelectiveHeaders(String message,
@Headers Map<String, Object> headers) {
String sender = (String) headers.get("sender");
Integer priority = (Integer) headers.get("priority");
System.out.println("Sender: " + sender + ", Priority: " + priority);
}
// Combined with other parameters
@MessageMapping("/combined")
public void handleCombined(@Payload User user,
@Header("sender") String sender,
@Headers Map<String, Object> allHeaders) {
System.out.println("User: " + user.getName());
System.out.println("Sender: " + sender);
System.out.println("Total headers: " + allHeaders.size());
}
}Extracts URI template variables from the message destination.
/**
* Annotation which indicates that a method parameter should be bound to
* a template variable in a destination template string.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DestinationVariable {
/**
* The name of the destination template variable to bind to.
*/
String value() default "";
}Usage Examples:
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
@Controller
public class DestinationVariableController {
// Single path variable
@MessageMapping("/user/{userId}")
public void handleUser(@DestinationVariable String userId, String message) {
System.out.println("Message for user " + userId + ": " + message);
}
// Multiple path variables
@MessageMapping("/room/{roomId}/user/{userId}")
public void handleRoomMessage(@DestinationVariable String roomId,
@DestinationVariable String userId,
String message) {
System.out.println("Room " + roomId + ", User " + userId + ": " + message);
}
// Type conversion
@MessageMapping("/order/{orderId}")
public void handleOrder(@DestinationVariable Long orderId, String update) {
System.out.println("Order " + orderId + " update: " + update);
}
// Named binding
@MessageMapping("/product/{id}/category/{cat}")
public void handleProduct(@DestinationVariable("id") String productId,
@DestinationVariable("cat") String category,
String data) {
System.out.println("Product " + productId + " in " + category);
}
// Combined with other annotations
@MessageMapping("/chat/{roomId}")
public void handleChat(@DestinationVariable String roomId,
@Payload ChatMessage message,
@Header("sender") String sender) {
System.out.println("Room: " + roomId + ", Sender: " + sender);
System.out.println("Message: " + message.getText());
}
}
class ChatMessage {
private String text;
public String getText() { return text; }
public void setText(String text) { this.text = text; }
}Specifies the destination(s) where the method's return value should be sent.
/**
* Annotation that indicates the return value of a message-handling method
* should be sent as a Message to the specified destination(s).
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SendTo {
/**
* One or more destinations to send the method's return value to.
* At the type level, this defines a default destination prefix.
*/
String[] value() default {};
}Usage Examples:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
@Controller
public class SendToController {
// Send return value to topic
@MessageMapping("/greet")
@SendTo("/topic/greetings")
public String handleGreeting(String name) {
return "Hello, " + name + "!";
}
// Multiple destinations
@MessageMapping("/broadcast")
@SendTo({"/topic/news", "/queue/archive"})
public String handleBroadcast(String message) {
return "BROADCAST: " + message;
}
// Type-level SendTo
@Controller
@SendTo("/topic/responses")
public class ResponseController {
@MessageMapping("/process")
public String process(String data) {
return "Processed: " + data;
// Sent to /topic/responses
}
}
// Return complex object (converted by MessageConverter)
@MessageMapping("/data")
@SendTo("/topic/results")
public Result handleData(String input) {
return new Result(input, System.currentTimeMillis());
}
// No @SendTo means no automatic reply
@MessageMapping("/no-reply")
public void handleNoReply(String message) {
System.out.println("Processing: " + message);
// No reply sent
}
}
class Result {
private String data;
private long timestamp;
public Result(String data, long timestamp) {
this.data = data;
this.timestamp = timestamp;
}
public String getData() { return data; }
public long getTimestamp() { return timestamp; }
}Handles exceptions thrown by message handler methods.
/**
* Annotation for handling exceptions from message-handling methods.
* Can handle specific exception types.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MessageExceptionHandler {
/**
* Exceptions to handle. If empty, handles all exceptions.
*/
Class<? extends Throwable>[] value() default {};
}Usage Examples:
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
@Controller
public class ExceptionHandlingController {
@MessageMapping("/process")
@SendTo("/topic/results")
public String processData(String data) {
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("Data cannot be empty");
}
if (data.equals("error")) {
throw new ProcessingException("Failed to process");
}
return "Success: " + data;
}
// Handle specific exception
@MessageExceptionHandler(IllegalArgumentException.class)
@SendTo("/topic/errors")
public String handleIllegalArgument(IllegalArgumentException ex) {
return "Validation error: " + ex.getMessage();
}
// Handle multiple exception types
@MessageExceptionHandler({ProcessingException.class, RuntimeException.class})
@SendTo("/topic/errors")
public String handleProcessingError(Exception ex) {
return "Processing failed: " + ex.getMessage();
}
// Handle all exceptions (no specific type)
@MessageExceptionHandler
public void handleAnyException(Throwable ex) {
System.err.println("Error occurred: " + ex.getMessage());
}
// Access Message that caused exception
@MessageExceptionHandler(DataException.class)
@SendTo("/topic/errors")
public String handleDataException(DataException ex, Message<?> message) {
return "Error processing message " + message.getHeaders().getId() + ": " + ex.getMessage();
}
}
class ProcessingException extends RuntimeException {
public ProcessingException(String message) {
super(message);
}
}
class DataException extends RuntimeException {
public DataException(String message) {
super(message);
}
}import org.springframework.messaging.handler.annotation.*;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;
import jakarta.validation.Valid;
import java.util.Map;
@Controller
@MessageMapping("/api")
public class CompleteMessageController {
// Simple message handler
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public String hello(String name) {
return "Hello, " + name;
}
// Complex handler with all annotation types
@MessageMapping("/chat/{roomId}")
@SendTo("/topic/chat/{roomId}")
public ChatResponse handleChat(
@DestinationVariable String roomId,
@Valid @Payload ChatMessage message,
@Header("userId") String userId,
@Header(value = "sessionId", required = false) String sessionId,
@Headers Map<String, Object> allHeaders) {
System.out.println("Room: " + roomId);
System.out.println("User: " + userId);
System.out.println("Session: " + sessionId);
return new ChatResponse(userId, message.getText(), System.currentTimeMillis());
}
// User-specific response
@MessageMapping("/private")
@SendToUser("/queue/reply")
public String handlePrivate(@Payload String message, @Header("userId") String userId) {
return "Private reply to " + userId + ": " + message;
}
// Exception handling
@MessageExceptionHandler
@SendTo("/topic/errors")
public ErrorResponse handleException(Exception ex) {
return new ErrorResponse(ex.getMessage(), System.currentTimeMillis());
}
}
class ChatMessage {
private String text;
public String getText() { return text; }
public void setText(String text) { this.text = text; }
}
class ChatResponse {
private String userId;
private String message;
private long timestamp;
public ChatResponse(String userId, String message, long timestamp) {
this.userId = userId;
this.message = message;
this.timestamp = timestamp;
}
public String getUserId() { return userId; }
public String getMessage() { return message; }
public long getTimestamp() { return timestamp; }
}
class ErrorResponse {
private String error;
private long timestamp;
public ErrorResponse(String error, long timestamp) {
this.error = error;
this.timestamp = timestamp;
}
public String getError() { return error; }
public long getTimestamp() { return timestamp; }
}