# Annotated Controllers
应用程序可以使用注解的 @Controller
类来处理来自客户端的消息。这样的类可以声明 @MessageMapping
、 @SubscribeMapping
和@ExceptionHandler
方法,如以下主题所述。
# @MessageMapping
你可以使用 @MessageMapping
来注解那些根据 目的地路由消息的方法。它在方法层面和类型层面都被支持。在类型层面上,@MessageMapping
被用来表达控制器中所有方法的共享映射。
默认情况下,映射值是 Ant 风格的路径模式(例如 /thing*
,/thing/**
),包括对模板变量的支持(例如 /thing/{id}
)。这些值可以通过 @DestinationVariable
方法参数进行引用。应用程序也可以切换到点状分隔的映射目的地约定,如 点状分隔符中所解释 (opens new window)。
# 支持的方法参数
下表描述了方法参数。
Method argument | Description |
---|---|
Message | 为了获得完整的信息。 |
MessageHeaders | 用于访问信息中的 header |
MessageHeaderAccessor, SimpMessageHeaderAccessor, and StompHeaderAccessor | 用于通过类型化的访问器方法访问 header。 |
@Payload | 用于访问消息的有效载荷,由配置的 MessageConverter 转换(例如,从 JSON)。 |
这个注解的存在不是必须的,因为默认情况下,如果没有其他参数被匹配,它就会被假定。
你可以用 @javax.validation.Valid
或 Spring 的 @Validated
来注解有效载荷参数,以使有效载荷参数被自动验证。 |
| @Header | 用于访问一个特定的 header -- 如果有必要,同时使用org.springframework.core.convert.Converter.Converter 进行类型转换。 |
| @Headers | 用于访问消息中的所有 header。这个参数必须是可分配给 java.util.Map.Message
的。 |
| @DestinationVariable | 用于访问从消息目的地提取的模板变量。必要时,数值会被转换为声明的方法参数类型。 |
| java.security.Principal | 反映在 WebSocket HTTP 握手时登录的用户。 |
# 返回值
默认情况下,@MessageMapping
方法的返回值通过匹配的 MessageConverter 被序列化为一个有效载荷,并作为一个消息发送到brokerChannel,从那里被广播给订阅者。外发消息的目的地与内发消息的目的地相同,但前缀为 /topic
。
你可以使用 @SendTo
和 @SendToUser
注解来定制输出消息的目的地。@SendTo
是用来定制目标目的地或指定多个目的地的。@SendToUser
用来指导输出消息只给与输入消息相关的用户。参见 用户目的地 (opens new window)。
你可以在同一个方法上同时使用 @SendTo
和 @SendToUser
,而且在类的层面上也支持这两种注解,在这种情况下,它们作为类中方法的默认值。然而,请记住,任何方法级的 @SendTo
或 @SendToUser
注解都会覆盖类级的任何此类注解。
消息可以被异步处理,@MessageMapping
方法可以返回 ListenableFuture
、CompleteableFuture
或 CompletionStage
。
请注意,@SendTo
和 @SendToUser
只是一种便利,相当于使用 SimpMessagingTemplate 来发送消息。如果有必要,对于更高级的场景,@MessageMapping
方法可以直接使用 SimpMessagingTemplate。这可以代替返回一个值,也可能是除了返回一个值之外。参见 发送消息 (opens new window)。
# @SubscribeMapping
@SubscribeMapping
与 @MessageMapping
类似,但只缩小了对订阅信息的映射。它支持与 @MessageMapping
相同的方法参数。然而对于返回值,默认情况下,消息被直接发送到客户端(通过 clientOutboundChannel,对订阅的响应),而不是发送到经纪人(通过 brokerChannel,作为 **广播 **给匹配的订阅)。添加 @SendTo
或 @SendToUser
会重写这一行为,并代替发送至经纪人。(简单说:@SubscribeMapping 的返回值是指针对订阅者,而 @MessageMapping 是针对所有订阅者)
这在什么时候是有用的?假设经纪人被映射到 /topic
和 /queue
,而应用控制器被映射到 /app
。在这种设置中,经纪人存储了所有对 /topic
和 /queue
的订阅,这些订阅是为了重复广播,而应用程序不需要参与。客户端也可以订阅一些 /app
的目的地,控制器可以在不涉及代理的情况下返回一个值,而不需要再次存储或使用该订阅(实际上是一个一次性的请求 - 回复交换)。这方面的一个用例是在启动时用初始数据填充一个用户界面。
这在什么时候没有用?不要试图将经纪人和控制器映射到同一个目标前缀,除非你想让两者独立处理消息,包括订阅,因为某些原因。入站消息是平行(并行)处理的。不能保证一个经纪人或一个控制器先处理一个给定的消息。如果目标是在订阅被存储并准备好进行广播时得到通知,如果服务器支持的话,客户端应该要求得到一个收据(简单的经纪人不支持)。例如,使用 Java STOMP (opens new window) 客户端,你可以做以下事情来添加一个收据:
@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// 在初始化过程中
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// 当订阅...
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
// 订阅就绪...
});
2
3
4
5
6
7
8
9
10
11
12
13
14
一个服务器端的选择是在 brokerChannel 上 注册 (opens new window) 一个 ExecutorChannelInterceptor,并实现 afterMessageHandled 方法,该方法在消息(包括订阅)被处理后被调用。
:::tips 需要认真阅读:
- @SubscribeMapping:客户端是需要 订阅
/app/路径
,而不是 发送 消息到该路径;订阅后就会收到响应的消息,可以用于做数据的初始化 - @MessageMapping:客户端是** 发送**
/app/路径
,方法响应的消息会被默认发送到/topic/路径
上,简单说如果没有 @MessageMapping("/task") 注解的方法,只要客户端订阅了/topic/task
,后端也可以通过 其他方式发送消息 (opens new window) 到/topic/task
上 :::
# 一个例子
在前面的 例子中改造 (opens new window),后端控制器里面
package cn.mrcode.study.springdocsread.websocket;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;
/**
* @author mrcode
*/
@Controller
public class StompController {
/**
* @param greeting
* @return 返回值是广播给所有人
*/
// 需要注意的是:客户端需要发送消息到 /app/greeting
// 响应的消息,会默认广播到 /topic/greeting 上,只要订阅了 /topic/greeting 的订阅者都能收到
@MessageMapping("/greeting")
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
private String getTimestamp() {
return System.currentTimeMillis() + "";
}
/**
* @return 返回值只返回给订阅的人;
*/
// 需要注意的是:前端需要订阅 /app/greeting2
// 也就是说,只要有订阅 /app/greeting2,订阅成功后,该订阅者就会收到这里返回的消息
@SubscribeMapping("/greeting2")
public String handle2() {
return "[ 单个消息" + getTimestamp() + ": ";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>STOMP</title>
</head>
<body>
</body>
<script type="text/javascript" src="/node_modules/webstomp-client/dist/webstomp.min.js"></script>
<script type="text/javascript"
src="https://cdnjs.loli.net/ajax/libs/sockjs-client/1.6.0/sockjs.js"></script>
<script>
// 这里使用 sockJs 库链接
var socket = new SockJS("http://localhost:8080/portfolio");
// 文章上说可以使用 WebSocket 链接。 实际上我这里测试不可以,会报错
// var socket = new WebSocket("ws://localhost:8080/portfolio");
var stompClient = webstomp.over(socket);
// 链接上 服务器时
stompClient.connect({}, function (frame) {
console.log(frame)
// 订阅消息
stompClient.subscribe("/topic/greeting", msg => {
console.log("收到订阅的消息广播:" + msg.body)
})
// 订阅消息
stompClient.subscribe("/app/greeting2", msg => {
console.log("收到初始化的订阅消息:" + msg.body)
})
// 链接上服务器时,像服务器发送一个消息
stompClient.send("/app/greeting", "我的第一个消息")
})
</script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
控制台显示如下 可以看到,只在订阅成功后,就能收到消息,而不是需要通过发送后触发后端的消息广播。
# @MessageExceptionHandler
一个应用程序可以使用 @MessageExceptionHandler
方法来处理来自 @MessageMapping
方法的异常。你可以在注解本身中声明异常,如果你想获得对异常实例的访问,也可以通过方法参数来声明。下面的例子通过一个方法参数声明了一个异常:
@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}
2
3
4
5
6
7
8
9
10
11
@MessageExceptionHandler
方法支持灵活的方法签名,支持与 [@MessageMapping](#uCbzp)
方法相同的方法参数类型和返回值。
通常情况下,@MessageExceptionHandler
方法适用于它们所声明的 @Controller
类(或类层次结构)。如果你想让这些方法在更大范围内应用(跨控制器),你可以在一个标有 @ControllerAdvice
的类中声明它们。这与 Spring MVC 中的 类似支持 (opens new window) 相当。