Skip to content

Commit 13da705

Browse files
committed
Add session lifecycle ApplicationEvent's
Issue: SPR-11578
1 parent 0745907 commit 13da705

File tree

6 files changed

+297
-4
lines changed

6 files changed

+297
-4
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.socket.messaging;
18+
19+
20+
import org.springframework.context.ApplicationEvent;
21+
import org.springframework.messaging.Message;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* Event raised when a new WebSocket client using a Simple Messaging Protocol
26+
* (e.g. STOMP) as the WebSocket sub-protocol issues a connect request.
27+
*
28+
* <p>Note that this is not the same as the WebSocket session getting established
29+
* but rather the client's first attempt to connect within the the sub-protocol,
30+
* for example sending the STOMP CONNECT frame.
31+
*
32+
* <p>The provided {@link #getMessage() message} can be examined to check
33+
* information about the connected user, The session id, and any headers
34+
* sent by the client, for STOMP check the class
35+
* {@link org.springframework.messaging.simp.stomp.StompHeaderAccessor}.
36+
* For example:
37+
*
38+
* <pre class="code">
39+
* StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
40+
* headers.getSessionId();
41+
* headers.getSessionAttributes();
42+
* headers.getPrincipal();
43+
* </pre>
44+
*
45+
* @author Rossen Stoyanchev
46+
* @since 4.0.3
47+
*/
48+
@SuppressWarnings("serial")
49+
public class SessionConnectEvent extends ApplicationEvent {
50+
51+
private final Message<byte[]> message;
52+
53+
54+
/**
55+
* Create a new SessionConnectEvent.
56+
*
57+
* @param source the component that published the event (never {@code null})
58+
* @param message the connect message
59+
*/
60+
public SessionConnectEvent(Object source, Message<byte[]> message) {
61+
super(source);
62+
Assert.notNull(message, "'message' must not be null");
63+
this.message = message;
64+
}
65+
66+
/**
67+
* Return the connect message.
68+
*/
69+
public Message<byte[]> getMessage() {
70+
return this.message;
71+
}
72+
73+
74+
@Override
75+
public String toString() {
76+
return "SessionConnectEvent: message=" + message;
77+
}
78+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.socket.messaging;
18+
19+
20+
import org.springframework.context.ApplicationEvent;
21+
import org.springframework.messaging.Message;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* A connected event represents the server response to a client's connect request.
26+
* See {@link org.springframework.web.socket.messaging.SessionConnectEvent}.
27+
*
28+
* @author Rossen Stoyanchev
29+
* @since 4.0.3
30+
*/
31+
@SuppressWarnings("serial")
32+
public class SessionConnectedEvent extends ApplicationEvent {
33+
34+
private final Message<byte[]> message;
35+
36+
37+
/**
38+
* Create a new event.
39+
*
40+
* @param source the component that published the event (never {@code null})
41+
* @param message the connected message
42+
*/
43+
public SessionConnectedEvent(Object source, Message<byte[]> message) {
44+
super(source);
45+
Assert.notNull(message, "'message' must not be null");
46+
this.message = message;
47+
}
48+
49+
/**
50+
* Return the connected message.
51+
*/
52+
public Message<byte[]> getMessage() {
53+
return this.message;
54+
}
55+
56+
57+
@Override
58+
public String toString() {
59+
return "SessionConnectedEvent: message=" + message;
60+
}
61+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.socket.messaging;
18+
19+
20+
import org.springframework.context.ApplicationEvent;
21+
import org.springframework.messaging.Message;
22+
import org.springframework.util.Assert;
23+
import org.springframework.web.socket.CloseStatus;
24+
25+
/**
26+
* Event raised when the session of a WebSocket client using a Simple Messaging
27+
* Protocol (e.g. STOMP) as the WebSocket sub-protocol is closed.
28+
*
29+
* <p>Note that this event may be raised more than once for a single session and
30+
* therefore event consumers should be idempotent and ignore a duplicate event..
31+
*
32+
* @author Rossen Stoyanchev
33+
* @since 4.0.3
34+
*/
35+
@SuppressWarnings("serial")
36+
public class SessionDisconnectEvent extends ApplicationEvent {
37+
38+
private final String sessionId;
39+
40+
private final CloseStatus status;
41+
42+
/**
43+
* Create a new event.
44+
*
45+
* @param source the component that published the event (never {@code null})
46+
* @param sessionId the disconnect message
47+
* @param closeStatus
48+
*/
49+
public SessionDisconnectEvent(Object source, String sessionId, CloseStatus closeStatus) {
50+
super(source);
51+
Assert.notNull(sessionId, "'sessionId' must not be null");
52+
this.sessionId = sessionId;
53+
this.status = closeStatus;
54+
}
55+
56+
/**
57+
* Return the session id.
58+
*/
59+
public String getSessionId() {
60+
return this.sessionId;
61+
}
62+
63+
/**
64+
* Return the status with which the session was closed.
65+
*/
66+
public CloseStatus getCloseStatus() {
67+
return this.status;
68+
}
69+
70+
@Override
71+
public String toString() {
72+
return "SessionDisconnectEvent: sessionId=" + this.sessionId;
73+
}
74+
}

spring-websocket/src/main/java/org/springframework/web/socket/messaging/StompSubProtocolHandler.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.apache.commons.logging.Log;
2929
import org.apache.commons.logging.LogFactory;
3030

31+
import org.springframework.context.ApplicationEventPublisher;
32+
import org.springframework.context.ApplicationEventPublisherAware;
3133
import org.springframework.messaging.Message;
3234
import org.springframework.messaging.MessageChannel;
3335
import org.springframework.messaging.simp.SimpMessageType;
@@ -57,7 +59,7 @@
5759
* @author Andy Wilkinson
5860
* @since 4.0
5961
*/
60-
public class StompSubProtocolHandler implements SubProtocolHandler {
62+
public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationEventPublisherAware {
6163

6264
/**
6365
* The name of the header set on the CONNECTED frame indicating the name
@@ -76,6 +78,8 @@ public class StompSubProtocolHandler implements SubProtocolHandler {
7678

7779
private UserSessionRegistry userSessionRegistry;
7880

81+
private ApplicationEventPublisher eventPublisher;
82+
7983

8084
/**
8185
* Configure the maximum size allowed for an incoming STOMP message.
@@ -120,6 +124,12 @@ public List<String> getSupportedProtocols() {
120124
return Arrays.asList("v10.stomp", "v11.stomp", "v12.stomp");
121125
}
122126

127+
@Override
128+
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
129+
this.eventPublisher = applicationEventPublisher;
130+
}
131+
132+
123133
/**
124134
* Handle incoming WebSocket messages from clients.
125135
*/
@@ -167,6 +177,11 @@ public void handleMessageFromClient(WebSocketSession session,
167177
headers.setUser(session.getPrincipal());
168178

169179
message = MessageBuilder.withPayload(message.getPayload()).setHeaders(headers).build();
180+
181+
if (SimpMessageType.CONNECT.equals(headers.getMessageType()) && this.eventPublisher != null) {
182+
this.eventPublisher.publishEvent(new SessionConnectEvent(this, message));
183+
}
184+
170185
outputChannel.send(message);
171186
}
172187
catch (Throwable ex) {
@@ -231,6 +246,11 @@ else if (SimpMessageType.MESSAGE.equals(headers.getMessageType())) {
231246

232247
try {
233248
message = MessageBuilder.withPayload(message.getPayload()).setHeaders(headers).build();
249+
250+
if (headers.getCommand() == StompCommand.CONNECTED && this.eventPublisher != null) {
251+
this.eventPublisher.publishEvent(new SessionConnectedEvent(this, (Message<byte[]>) message));
252+
}
253+
234254
byte[] bytes = this.stompEncoder.encode((Message<byte[]>) message);
235255
TextMessage textMessage = new TextMessage(bytes);
236256

@@ -329,6 +349,11 @@ public void afterSessionEnded(WebSocketSession session, CloseStatus closeStatus,
329349
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
330350
headers.setSessionId(session.getId());
331351
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
352+
353+
if (this.eventPublisher != null) {
354+
this.eventPublisher.publishEvent(new SessionDisconnectEvent(this, session.getId(), closeStatus));
355+
}
356+
332357
outputChannel.send(message);
333358
}
334359

spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.apache.commons.logging.Log;
2929
import org.apache.commons.logging.LogFactory;
3030

31+
import org.springframework.context.ApplicationEventPublisher;
32+
import org.springframework.context.ApplicationEventPublisherAware;
3133
import org.springframework.context.SmartLifecycle;
3234
import org.springframework.messaging.Message;
3335
import org.springframework.messaging.MessageChannel;
@@ -60,8 +62,8 @@
6062
* @author Andy Wilkinson
6163
* @since 4.0
6264
*/
63-
public class SubProtocolWebSocketHandler
64-
implements WebSocketHandler, SubProtocolCapable, MessageHandler, SmartLifecycle {
65+
public class SubProtocolWebSocketHandler implements WebSocketHandler,
66+
SubProtocolCapable, MessageHandler, SmartLifecycle, ApplicationEventPublisherAware {
6567

6668
private final Log logger = LogFactory.getLog(SubProtocolWebSocketHandler.class);
6769

@@ -84,6 +86,8 @@ public class SubProtocolWebSocketHandler
8486

8587
private volatile boolean running = false;
8688

89+
private ApplicationEventPublisher eventPublisher;
90+
8791

8892
public SubProtocolWebSocketHandler(MessageChannel clientInboundChannel, SubscribableChannel clientOutboundChannel) {
8993
Assert.notNull(clientInboundChannel, "ClientInboundChannel must not be null");
@@ -114,18 +118,24 @@ public List<SubProtocolHandler> getProtocolHandlers() {
114118
* Register a sub-protocol handler.
115119
*/
116120
public void addProtocolHandler(SubProtocolHandler handler) {
121+
117122
List<String> protocols = handler.getSupportedProtocols();
118123
if (CollectionUtils.isEmpty(protocols)) {
119124
logger.warn("No sub-protocols, ignoring handler " + handler);
120125
return;
121126
}
127+
122128
for (String protocol: protocols) {
123129
SubProtocolHandler replaced = this.protocolHandlers.put(protocol, handler);
124130
if ((replaced != null) && (replaced != handler) ) {
125131
throw new IllegalStateException("Failed to map handler " + handler
126132
+ " to protocol '" + protocol + "', it is already mapped to handler " + replaced);
127133
}
128134
}
135+
136+
if (handler instanceof ApplicationEventPublisherAware) {
137+
((ApplicationEventPublisherAware) handler).setApplicationEventPublisher(this.eventPublisher);
138+
}
129139
}
130140

131141
/**
@@ -178,6 +188,10 @@ public int getSendBufferSizeLimit() {
178188
return sendBufferSizeLimit;
179189
}
180190

191+
@Override
192+
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
193+
this.eventPublisher = eventPublisher;
194+
}
181195

182196
@Override
183197
public boolean isAutoStartup() {

0 commit comments

Comments
 (0)