or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

channel-support.mdcore-messaging.mdhandler-annotations.mdindex.mdmessage-converters.mdmessaging-templates.mdrsocket.mdsimp-configuration.mdstomp-websocket.md
tile.json

handler-annotations.mddocs/

Handler Annotations

Annotation-driven message handling with @MessageMapping, @Payload, @Header, @Headers, @DestinationVariable, @SendTo for declarative message processing using Spring's annotation programming model.

Capabilities

@MessageMapping Annotation

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");
    }
}

@Payload Annotation

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; }
}

@Header Annotation

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);
    }
}

@Headers Annotation

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());
    }
}

@DestinationVariable Annotation

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; }
}

@SendTo Annotation

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; }
}

@MessageExceptionHandler Annotation

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);
    }
}

Complete Controller Example

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; }
}