带注释的控制器
应用程序可以使用带有注解的@Controller类来处理来自客户端的消息。这样的类可以声明@MessageMapping、@SubscribeMapping和@ExceptionHandler方法,如下述主题所描述:
@MessageMapping
你可以使用@MessageMapping来标注根据消息目的地进行路由的方法。该注解既可以在方法级别使用,也可以在类型级别使用。在类型级别,@MessageMapping用于表达控制器中所有方法的共享映射规则。
默认情况下,映射值采用Ant风格的路径模式(例如/thing*、/thing/**),同时支持模板变量(例如/thing/{id})。这些值可以通过@DestinationVariable方法参数来引用。应用程序也可以切换为使用点号分隔的映射规则,具体内容请参阅点号作为分隔符。
支持的方法参数
下表描述了该方法参数:
| 方法参数 | 说明 |
|---|---|
Message | 用于访问完整的消息内容。 |
MessageHeaders | 用于访问消息中的头部信息(headers)。 |
MessageHeaderAccessor, SimpMessageHeaderAccessor, 和 StompHeaderAccessor | 通过类型化的访问方法来访问头部信息。(typed accessor methods) |
@Payload | 用于访问消息的负载(payload),该负载会通过配置的 MessageConverter 进行转换(例如,从 JSON 转换)。如果未指定其他参数,系统默认会使用此注解;因此,此注解并非必需。 你可以为负载参数添加 @jakarta.validationValid 或 Spring 的 @Validated 注解,以实现自动验证。 |
@Header | 用于访问特定的头部信息;如有需要,还可以通过 org.springframework.core.convertconverter.Converter 进行类型转换。 |
@Headers | 用于访问消息中的所有头部信息。此参数必须能够赋值给 java.util.Map 类型。 |
@DestinationVariable | 用于访问从消息目的地(message destination)中提取的模板变量(template variables)。根据需要,这些值会转换为声明的方法参数类型。 |
java.security.Principal | 反映在 WebSocket HTTP 握手(HTTP handshake)时登录的用户信息。 |
返回值
默认情况下,@MessageMapping 方法的返回值会通过匹配的 MessageConverter 被序列化为有效载荷(payload),然后以 Message 的形式发送到 brokerChannel,再从那里广播给订阅者。出站消息的目的地与入站消息的目的地相同,但会在目的地前加上 /topic 前缀。
你可以使用@SendTo和@SendToUser注释来自定义输出消息的发送目的地。@SendTo用于自定义目标地址或指定多个目的地。@SendToUser则用于将输出消息仅发送给与输入消息关联的用户。请参阅用户目的地。
你可以在同一个方法上同时使用@SendTo和@SendToUser,并且这两者都在类级别被支持,在这种情况下,它们会成为该类中方法的默认设置。然而,请记住,任何方法级别的@SendTo或@SendToUser注解都会覆盖类级别上的此类注解。
消息可以异步处理,@MessageMapping 方法可以返回 ListenableFuture、CompletableFuture 或 CompletionStage。
请注意,@SendTo 和 @SendToUser 只是一种便利性功能,实质上是利用 SimpMessagingTemplate 来发送消息。在需要更复杂的场景下,@MessageMapping 方法也可以直接使用 Simp MessagingTemplate 来完成消息发送。这样做既可以替代返回值的功能,也可以与返回值同时使用。详情请参阅 发送消息。
@SubscribeMapping
@SubscribeMapping 与 @MessageMapping 类似,但它将映射范围限制在订阅消息上。它支持与 @MessageMapping 相同的 方法参数。然而,对于返回值,默认情况下,消息会直接发送给客户端(通过 clientOutboundChannel,作为对订阅的响应),而不是发送给 broker(通过 brokerChannel,作为对匹配订阅的广播)。添加 @SendTo 或 @SendToUser 可以覆盖这一行为,使消息发送给 broker。
这种情况在什么时候会派上用场呢?假设代理(broker)被映射到 /topic 和 /queue,而应用程序控制器(application controllers)被映射到 /app。在这种设置下,代理会存储所有用于重复广播的针对 /topic 和 /queue 的订阅信息,应用程序无需参与其中。客户端也可以订阅某个 /app 目标,控制器可以在不涉及代理的情况下响应该订阅并返回一个值,且无需再次存储或使用该订阅信息(实际上就是一次性的请求-响应交换)。这种场景的一个用途是在启动时用初始数据填充用户界面(UI)。
在什么情况下这会没有用呢?除非你有特殊原因希望broker和controller能够独立处理消息(包括订阅请求),否则不要试图将它们映射到同一个目标前缀下。传入的消息会被并行处理,无法保证哪个组件会先处理某条消息。如果你的目标是希望在订阅信息被存储并准备好进行广播时收到通知,那么客户端应该检查服务器是否支持接收确认信息(简单的broker不支持这一功能)。例如,使用Java STOMP客户端时,你可以按照以下方式添加接收确认的机制:
@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(receiptHeaders -> {
// Subscription ready...
});
一种服务器端的做法是在brokerChannel上注册一个ExecutorChannelInterceptor,并实现afterMessageHandled方法。该方法会在消息(包括订阅消息)被处理完成后被调用。
@MessageExceptionHandler
应用程序可以使用@MessageExceptionHandler方法来处理来自@MessageMapping方法的异常。如果你想访问异常实例,可以在注解本身中声明异常,或者通过方法参数来声明。以下示例是通过方法参数来声明异常的:
@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}
@MessageExceptionHandler 方法支持灵活的方法签名,并且支持与 @MessageMapping 方法相同的方法参数类型和返回值。
通常,@MessageExceptionHandler 方法在声明它们的 @Controller 类(或类层次结构)中生效。如果你希望这些方法具有更全局的适用范围(即在多个控制器中都有效),可以在一个标记有 @ControllerAdvice 的类中声明它们。这与 Spring MVC 中提供的类似支持是类似的。