Match messages based on destination patterns, message types, or custom criteria with support for path variables and composite matching logic.
Core interface for determining if a message should be matched. Foundation for all message matching functionality.
/**
* Strategy interface for matching messages.
*
* @param <T> the message payload type
* @since 4.0
*/
public interface MessageMatcher<T> {
/**
* Returns true if this matcher matches the given message.
*
* @param message the message to match
* @return true if message matches
*/
boolean matches(Message<? extends T> message);
/**
* Returns match result with extracted path variables.
*
* @param message the message to match
* @return match result including extracted variables
* @since 6.5
*/
default MatchResult matcher(Message<? extends T> message) {
return matches(message) ? MatchResult.match() : MatchResult.notMatch();
}
/**
* Matcher that matches every message.
*/
MessageMatcher<Object> ANY_MESSAGE = (message) -> true;
}Usage Example:
// Custom matcher implementation
MessageMatcher<Object> customMatcher = (message) -> {
String destination = (String) message.getHeaders()
.get("simpDestination");
return destination != null && destination.startsWith("/app/admin");
};
// Use with authorization manager
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.matchers(customMatcher).hasRole("ADMIN")
.build();Result of a message matching operation, including match status and any extracted path variables.
/**
* Result of a message matching operation.
*
* @since 6.5
*/
public interface MatchResult {
/**
* Returns whether the message matched.
*
* @return true if matched
*/
boolean isMatch();
/**
* Returns extracted path variables from pattern matching.
*
* @return map of variable names to values (empty if no variables)
*/
Map<String, String> getVariables();
/**
* Creates a match result with no variables.
*
* @return match result
*/
static MatchResult match() {
return match(Collections.emptyMap());
}
/**
* Creates a match result with extracted variables.
*
* @param variables the extracted path variables
* @return match result
*/
static MatchResult match(Map<String, String> variables);
/**
* Creates a non-match result.
*
* @return non-match result
*/
static MatchResult notMatch();
}Matches messages by destination path patterns using Spring's PathPattern with support for path variable extraction.
/**
* MessageMatcher that matches based on destination path patterns.
* Supports path variables like /app/user/{userId}/*.
*
* @since 6.5
*/
public final class PathPatternMessageMatcher implements MessageMatcher<Object> {
/**
* Matcher for messages with null destination.
*/
public static final MessageMatcher<Object> NULL_DESTINATION_MATCHER;
/**
* Creates builder with default PathPatternParser.
*
* @return builder instance
*/
public static Builder withDefaults();
/**
* Creates builder with custom PathPatternParser.
*
* @param parser the path pattern parser to use
* @return builder instance
*/
public static Builder withPathPatternParser(PathPatternParser parser);
/**
* Returns true if message destination matches the pattern.
*
* @param message the message to match
* @return true if destination matches pattern
*/
public boolean matches(Message<?> message);
/**
* Returns match result with extracted path variables.
*
* @param message the message to match
* @return match result with variables
*/
public MatchResult matcher(Message<?> message);
}Builder API:
public static final class Builder {
/**
* Creates matcher for the given destination pattern (any message type).
*
* @param pattern the destination pattern (e.g., "/app/user/**")
* @return path pattern matcher
*/
public PathPatternMessageMatcher matcher(String pattern);
/**
* Creates matcher for the given type and destination pattern.
*
* @param type the STOMP message type to match
* @param pattern the destination pattern
* @return path pattern matcher
*/
public PathPatternMessageMatcher matcher(SimpMessageType type, String pattern);
}Usage Examples:
// Match any message to /app/chat/**
PathPatternMessageMatcher chatMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher("/app/chat/**");
// Match MESSAGE type to /app/user/**
PathPatternMessageMatcher userMessageMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher(SimpMessageType.MESSAGE, "/app/user/**");
// Extract path variables
PathPatternMessageMatcher userMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher("/app/user/{userId}/profile");
Message<?> message = // ... message with destination "/app/user/123/profile"
MatchResult result = userMatcher.matcher(message);
if (result.isMatch()) {
String userId = result.getVariables().get("userId"); // "123"
}
// Match messages with null destination
MessageMatcher<Object> nullDestMatcher =
PathPatternMessageMatcher.NULL_DESTINATION_MATCHER;Matches messages by STOMP message type (CONNECT, SUBSCRIBE, MESSAGE, UNSUBSCRIBE, DISCONNECT).
/**
* MessageMatcher that matches based on STOMP message type.
*
* @since 4.0
*/
public class SimpMessageTypeMatcher implements MessageMatcher<Object> {
/**
* Creates matcher for the specified message type.
*
* @param typeToMatch the STOMP message type to match
*/
public SimpMessageTypeMatcher(SimpMessageType typeToMatch);
/**
* Returns true if message has the matching type.
*
* @param message the message to match
* @return true if message type matches
*/
public boolean matches(Message<?> message);
}Usage Examples:
// Match CONNECT messages
MessageMatcher<Object> connectMatcher =
new SimpMessageTypeMatcher(SimpMessageType.CONNECT);
// Match SUBSCRIBE messages
MessageMatcher<Object> subscribeMatcher =
new SimpMessageTypeMatcher(SimpMessageType.SUBSCRIBE);
// Match MESSAGE type
MessageMatcher<Object> messageMatcher =
new SimpMessageTypeMatcher(SimpMessageType.MESSAGE);
// Use with authorization
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.matchers(connectMatcher).permitAll()
.matchers(subscribeMatcher, messageMatcher).authenticated()
.build();Composite matcher that returns true only if ALL contained matchers match (AND logic).
/**
* Composite MessageMatcher that matches if ALL matchers match.
*
* @param <T> the message payload type
* @since 4.0
*/
public final class AndMessageMatcher<T> extends AbstractMessageMatcherComposite<T> {
/**
* Creates AND matcher from list of matchers.
*
* @param messageMatchers list of matchers to combine
*/
public AndMessageMatcher(List<MessageMatcher<T>> messageMatchers);
/**
* Creates AND matcher from varargs matchers.
*
* @param messageMatchers matchers to combine
*/
public AndMessageMatcher(MessageMatcher<T>... messageMatchers);
/**
* Returns true if ALL matchers match the message.
*
* @param message the message to match
* @return true if all matchers match
*/
public boolean matches(Message<? extends T> message);
}Usage Example:
// Match messages that are both MESSAGE type AND to /app/admin/** destination
MessageMatcher<Object> typeMatcher =
new SimpMessageTypeMatcher(SimpMessageType.MESSAGE);
PathPatternMessageMatcher destMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher("/app/admin/**");
MessageMatcher<Object> andMatcher =
new AndMessageMatcher<>(typeMatcher, destMatcher);
// Use with authorization
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.matchers(andMatcher).hasRole("ADMIN")
.build();Composite matcher that returns true if ANY contained matcher matches (OR logic).
/**
* Composite MessageMatcher that matches if ANY matcher matches.
*
* @param <T> the message payload type
* @since 4.0
*/
public final class OrMessageMatcher<T> extends AbstractMessageMatcherComposite<T> {
/**
* Creates OR matcher from list of matchers.
*
* @param messageMatchers list of matchers to combine
*/
public OrMessageMatcher(List<MessageMatcher<T>> messageMatchers);
/**
* Creates OR matcher from varargs matchers.
*
* @param messageMatchers matchers to combine
*/
public OrMessageMatcher(MessageMatcher<T>... messageMatchers);
/**
* Returns true if ANY matcher matches the message.
*
* @param message the message to match
* @return true if any matcher matches
*/
public boolean matches(Message<? extends T> message);
}Usage Example:
// Match messages to either /app/public/** OR /app/guest/**
PathPatternMessageMatcher publicMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher("/app/public/**");
PathPatternMessageMatcher guestMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher("/app/guest/**");
MessageMatcher<Object> orMatcher =
new OrMessageMatcher<>(publicMatcher, guestMatcher);
// Use with authorization
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.matchers(orMatcher).permitAll()
.build();Base class for composite message matchers (And/Or). Provides common functionality.
/**
* Abstract base for composite MessageMatcher implementations.
*
* @param <T> the message payload type
* @since 4.0
*/
public abstract class AbstractMessageMatcherComposite<T> implements MessageMatcher<T> {
/**
* Returns the list of contained matchers.
*
* @return list of matchers
*/
public final List<MessageMatcher<T>> getMessageMatchers();
/**
* Logger for subclasses.
*/
protected final Log logger = LogFactory.getLog(getClass());
/**
* Deprecated logger field (use 'logger' instead).
* @deprecated since 5.4, use 'logger' field instead
*/
@Deprecated
protected final Log LOGGER = LogFactory.getLog(getClass());
}PathPatternMessageMatcher uses Spring's PathPattern syntax:
? - matches one character* - matches zero or more characters within a path segment** - matches zero or more path segments{name} - matches a path segment and captures it as a variable{name:[regex]} - matches using regex and captures as variableExamples:
"/app/chat" // Exact match
"/app/chat/*" // Matches /app/chat/room1, /app/chat/room2
"/app/chat/**" // Matches /app/chat/room1/message
"/app/user/{userId}" // Matches /app/user/123, captures userId=123
"/app/{type}/{id}" // Matches /app/user/123, captures type=user, id=123
"/app/file/*.{ext}" // Matches /app/file/doc.pdf, captures ext=pdfMessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.simpDestMatchers("/app/admin/**").hasRole("ADMIN")
.simpDestMatchers("/app/user/**").hasRole("USER")
.simpDestMatchers("/app/public/**").permitAll()
.build();MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
.simpTypeMatchers(SimpMessageType.MESSAGE).authenticated()
.simpTypeMatchers(SimpMessageType.SUBSCRIBE).authenticated()
.build();MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
// Only MESSAGE type to /app/chat/**
.simpMessageDestMatchers("/app/chat/**").hasRole("USER")
// Only SUBSCRIBE type to /topic/**
.simpSubscribeDestMatchers("/topic/**").authenticated()
.build();// Custom authorization using path variables
AuthorizationManager<MessageAuthorizationContext<?>> userAuthManager =
(authentication, context) -> {
String userId = context.getVariables().get("userId");
String authenticatedUser = authentication.get().getName();
return new AuthorizationDecision(authenticatedUser.equals(userId));
};
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.simpDestMatchers("/app/user/{userId}/**").access(userAuthManager)
.build();// Complex AND condition: MESSAGE type to admin destinations
MessageMatcher<Object> typeMatcher =
new SimpMessageTypeMatcher(SimpMessageType.MESSAGE);
PathPatternMessageMatcher adminMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher("/app/admin/**");
MessageMatcher<Object> combined = new AndMessageMatcher<>(typeMatcher, adminMatcher);
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.matchers(combined).hasRole("ADMIN")
.build();// Complex OR condition: Public or guest destinations
PathPatternMessageMatcher publicMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher("/app/public/**");
PathPatternMessageMatcher guestMatcher =
PathPatternMessageMatcher.withDefaults()
.matcher("/app/guest/**");
MessageMatcher<Object> publicOrGuest =
new OrMessageMatcher<>(publicMatcher, guestMatcher);
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.matchers(publicOrGuest).permitAll()
.build();public class HeaderValueMatcher implements MessageMatcher<Object> {
private final String headerName;
private final String expectedValue;
public HeaderValueMatcher(String headerName, String expectedValue) {
this.headerName = headerName;
this.expectedValue = expectedValue;
}
@Override
public boolean matches(Message<?> message) {
Object headerValue = message.getHeaders().get(headerName);
return expectedValue.equals(headerValue);
}
}
// Usage
MessageMatcher<Object> customMatcher =
new HeaderValueMatcher("customHeader", "expectedValue");
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.matchers(customMatcher).authenticated()
.build();public class RegexDestinationMatcher implements MessageMatcher<Object> {
private final Pattern pattern;
public RegexDestinationMatcher(String regex) {
this.pattern = Pattern.compile(regex);
}
@Override
public boolean matches(Message<?> message) {
String destination = (String) message.getHeaders()
.get("simpDestination");
return destination != null && pattern.matcher(destination).matches();
}
@Override
public MatchResult matcher(Message<?> message) {
String destination = (String) message.getHeaders()
.get("simpDestination");
if (destination == null) {
return MatchResult.notMatch();
}
Matcher matcher = pattern.matcher(destination);
if (!matcher.matches()) {
return MatchResult.notMatch();
}
// Extract named groups as variables
Map<String, String> variables = new HashMap<>();
// ... extract groups from matcher
return MatchResult.match(variables);
}
}Common message headers used in matching:
simpMessageType - The SimpMessageType (CONNECT, MESSAGE, SUBSCRIBE, etc.)simpDestination - The destination path (e.g., "/app/chat")simpUser - The authenticated user (Principal)simpSessionId - The WebSocket session IDsimpSubscriptionId - The subscription ID (for SUBSCRIBE messages)Example accessing headers:
MessageMatcher<Object> customMatcher = (message) -> {
MessageHeaders headers = message.getHeaders();
SimpMessageType type = (SimpMessageType) headers.get("simpMessageType");
String destination = (String) headers.get("simpDestination");
Principal user = (Principal) headers.get("simpUser");
// Custom matching logic
return type == SimpMessageType.MESSAGE &&
destination != null &&
destination.startsWith("/app/admin") &&
user != null;
};Always handle null values when accessing message headers:
public class SafeDestinationMatcher implements MessageMatcher<Object> {
@Override
public boolean matches(Message<?> message) {
if (message == null || message.getHeaders() == null) {
return false;
}
String destination = (String) message.getHeaders().get("simpDestination");
return destination != null && destination.startsWith("/app/admin");
}
}Custom matchers should handle exceptions gracefully:
public class RobustMatcher implements MessageMatcher<Object> {
@Override
public boolean matches(Message<?> message) {
try {
// Matching logic
return performMatch(message);
} catch (Exception e) {
logger.error("Matcher error", e);
// Fail-secure: return false on error
return false;
}
}
}Causes:
Solutions:
// Debug: Log actual message headers
MessageMatcher<Object> debugMatcher = (message) -> {
logger.debug("Message headers: {}", message.getHeaders());
logger.debug("Destination: {}", message.getHeaders().get("simpDestination"));
logger.debug("Type: {}", message.getHeaders().get("simpMessageType"));
return true;
};Causes:
Solutions:
// Correct: Use {variable} syntax
PathPatternMessageMatcher.withDefaults()
.matcher("/app/user/{userId}/**")
// Wrong: No variable extraction
PathPatternMessageMatcher.withDefaults()
.matcher("/app/user/*/**")Causes:
Solutions:
// Ensure all matchers are non-null
List<MessageMatcher<Object>> matchers = Arrays.asList(
typeMatcher,
destMatcher
);
// Remove any nulls
matchers.removeIf(Objects::isNull);
MessageMatcher<Object> composite = new AndMessageMatcher<>(matchers);