Skip to content

Commit a9b88d6

Browse files
committed
Document need for ServerEndpointExporter and show its use in a sample
Traditionally, a @ServerEndpoint-annotated bean is found by a servlet container initialiser, however Boot does not run servlet container initialisers when an embedded container is being used. To be able to use @serverendpoint in a Boot app that uses embedded Tomcat a ServerEndpointExporter bean must be declared. This commit updates the documentation to describe this requirement and also updates the WebSockets sample to illustrate the use of ServerEndpointExporter. The version of Spring Framework has been updated to 4.0.8.BUILD-SNAPSHOT. This picks up the fix for SPR-12340. Closes gh-1722
1 parent 595f387 commit a9b88d6

File tree

9 files changed

+293
-28
lines changed

9 files changed

+293
-28
lines changed

spring-boot-dependencies/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
<snakeyaml.version>1.13</snakeyaml.version>
9999
<solr.version>4.7.2</solr.version>
100100
<spock.version>0.7-groovy-2.0</spock.version>
101-
<spring.version>4.0.7.RELEASE</spring.version>
101+
<spring.version>4.0.8.BUILD-SNAPSHOT</spring.version>
102102
<spring-amqp.version>1.3.6.RELEASE</spring-amqp.version>
103103
<spring-batch.version>3.0.2.RELEASE</spring-batch.version>
104104
<spring-data-releasetrain.version>Dijkstra-SR4</spring-data-releasetrain.version>

spring-boot-docs/src/main/asciidoc/howto.adoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,26 @@ change the version properties, e.g. for a simple webapp or service:
617617

618618

619619

620+
[[howto-create-websocket-endpoints-using-serverendpoint]]
621+
=== Create WebSocket endpoints using @ServerEndpoint
622+
If you want to use `@ServerEndpoint` in a Spring Boot application that used an embedded
623+
container, you must declare a single `ServerEndpointExporter` `@Bean`:
624+
625+
[source,java,indent=0,subs="verbatim,quotes,attributes"]
626+
----
627+
@Bean
628+
public ServerEndpointExporter serverEndpointExporter() {
629+
return new ServerEndpointExporter();
630+
}
631+
----
632+
633+
This bean will register any `@ServerEndpoint` annotated beans with the underlying
634+
WebSocket container. When deployed to a standalone servlet container this role is
635+
performed by a servlet container initializer and the `ServerEndpointExporter` bean is
636+
not required.
637+
638+
639+
620640
[[howto-spring-mvc]]
621641
== Spring MVC
622642

spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package samples.websocket.client;
1818

1919
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.atomic.AtomicReference;
2021

2122
import org.apache.commons.logging.Log;
2223
import org.apache.commons.logging.LogFactory;
@@ -33,11 +34,14 @@ public class SimpleClientWebSocketHandler extends TextWebSocketHandler {
3334

3435
private final CountDownLatch latch;
3536

37+
private final AtomicReference<String> messagePayload;
38+
3639
@Autowired
3740
public SimpleClientWebSocketHandler(GreetingService greetingService,
38-
CountDownLatch latch) {
41+
CountDownLatch latch, AtomicReference<String> message) {
3942
this.greetingService = greetingService;
4043
this.latch = latch;
44+
this.messagePayload = message;
4145
}
4246

4347
@Override
@@ -51,6 +55,7 @@ public void handleTextMessage(WebSocketSession session, TextMessage message)
5155
throws Exception {
5256
this.logger.info("Received: " + message + " (" + this.latch.getCount() + ")");
5357
session.close();
58+
this.messagePayload.set(message.getPayload());
5459
this.latch.countDown();
5560
}
5661

spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/config/SampleWebSocketsApplication.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
2828
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
2929
import org.springframework.web.socket.handler.PerConnectionWebSocketHandler;
30+
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
3031

3132
import samples.websocket.client.GreetingService;
3233
import samples.websocket.client.SimpleGreetingService;
3334
import samples.websocket.echo.DefaultEchoService;
3435
import samples.websocket.echo.EchoService;
3536
import samples.websocket.echo.EchoWebSocketHandler;
37+
import samples.websocket.reverse.ReverseWebSocketEndpoint;
3638
import samples.websocket.snake.SnakeWebSocketHandler;
3739

3840
@Configuration
@@ -76,4 +78,14 @@ public WebSocketHandler snakeWebSocketHandler() {
7678
return new PerConnectionWebSocketHandler(SnakeWebSocketHandler.class);
7779
}
7880

81+
@Bean
82+
public ReverseWebSocketEndpoint reverseWebSocketEndpoint() {
83+
return new ReverseWebSocketEndpoint();
84+
}
85+
86+
@Bean
87+
public ServerEndpointExporter serverEndpointExporter() {
88+
return new ServerEndpointExporter();
89+
}
90+
7991
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2012-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 samples.websocket.reverse;
18+
19+
import java.io.IOException;
20+
21+
import javax.websocket.OnMessage;
22+
import javax.websocket.Session;
23+
import javax.websocket.server.ServerEndpoint;
24+
25+
@ServerEndpoint("/reverse")
26+
public class ReverseWebSocketEndpoint {
27+
28+
@OnMessage
29+
public void handleMessage(Session session, String message) throws IOException {
30+
session.getBasicRemote().sendText(
31+
"Reversed: " + new StringBuilder(message).reverse());
32+
}
33+
}

spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<p>Please select the sample you would like to try.</p>
2626
<ul>
2727
<li><a href="./echo.html">Echo</a></li>
28+
<li><a href="./reverse.html">Reverse</a></li>
2829
<li><a href="./snake.html">Snake</a></li>
2930
</ul>
3031
</body>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<!DOCTYPE html>
18+
<html>
19+
<head>
20+
<title>WebSocket Examples: Reverse</title>
21+
<style type="text/css">
22+
#connect-container {
23+
float: left;
24+
width: 400px
25+
}
26+
27+
#connect-container div {
28+
padding: 5px;
29+
}
30+
31+
#console-container {
32+
float: left;
33+
margin-left: 15px;
34+
width: 400px;
35+
}
36+
37+
#console {
38+
border: 1px solid #CCCCCC;
39+
border-right-color: #999999;
40+
border-bottom-color: #999999;
41+
height: 170px;
42+
overflow-y: scroll;
43+
padding: 5px;
44+
width: 100%;
45+
}
46+
47+
#console p {
48+
padding: 0;
49+
margin: 0;
50+
}
51+
</style>
52+
<script type="text/javascript">
53+
var ws = null;
54+
55+
function setConnected(connected) {
56+
document.getElementById('connect').disabled = connected;
57+
document.getElementById('disconnect').disabled = !connected;
58+
document.getElementById('reverse').disabled = !connected;
59+
}
60+
61+
function connect() {
62+
var target = document.getElementById('target').value;
63+
ws = new WebSocket(target);
64+
ws.onopen = function () {
65+
setConnected(true);
66+
log('Info: WebSocket connection opened.');
67+
};
68+
ws.onmessage = function (event) {
69+
log('Received: ' + event.data);
70+
};
71+
ws.onclose = function () {
72+
setConnected(false);
73+
log('Info: WebSocket connection closed.');
74+
};
75+
}
76+
77+
function updateTarget() {
78+
if (window.location.protocol == 'http:') {
79+
document.getElementById('target').value = 'ws://' + window.location.host + document.getElementById('target').value;
80+
} else {
81+
document.getElementById('target').value = 'wss://' + window.location.host + document.getElementById('target').value;
82+
}
83+
}
84+
85+
function disconnect() {
86+
if (ws != null) {
87+
ws.close();
88+
ws = null;
89+
}
90+
setConnected(false);
91+
}
92+
93+
function reverse() {
94+
if (ws != null) {
95+
var message = document.getElementById('message').value;
96+
log('Sent: ' + message);
97+
ws.send(message);
98+
} else {
99+
alert('WebSocket connection not established, please connect.');
100+
}
101+
}
102+
103+
function log(message) {
104+
var console = document.getElementById('console');
105+
var p = document.createElement('p');
106+
p.style.wordWrap = 'break-word';
107+
p.appendChild(document.createTextNode(message));
108+
console.appendChild(p);
109+
while (console.childNodes.length > 25) {
110+
console.removeChild(console.firstChild);
111+
}
112+
console.scrollTop = console.scrollHeight;
113+
}
114+
</script>
115+
</head>
116+
<body onload="updateTarget()">
117+
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable
118+
Javascript and reload this page!</h2></noscript>
119+
<div>
120+
<div id="connect-container">
121+
<div>
122+
<input id="target" type="text" size="40" style="width: 350px" value="/reverse"/>
123+
</div>
124+
<div>
125+
<button id="connect" onclick="connect();">Connect</button>
126+
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
127+
</div>
128+
<div>
129+
<textarea id="message" style="width: 350px">Here is a message!</textarea>
130+
</div>
131+
<div>
132+
<button id="reverse" onclick="reverse();" disabled="disabled">Reverse message</button>
133+
</div>
134+
</div>
135+
<div id="console-container">
136+
<div id="console"></div>
137+
</div>
138+
</div>
139+
</body>
140+
</html>
Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@
1414
* limitations under the License.
1515
*/
1616

17-
package samples.websocket.echo;
17+
package samples.websocket;
1818

1919
import java.util.concurrent.CountDownLatch;
2020
import java.util.concurrent.TimeUnit;
21+
import java.util.concurrent.atomic.AtomicReference;
2122

2223
import org.apache.commons.logging.Log;
2324
import org.apache.commons.logging.LogFactory;
24-
import org.junit.Before;
2525
import org.junit.Test;
2626
import org.junit.runner.RunWith;
2727
import org.springframework.beans.factory.annotation.Value;
2828
import org.springframework.boot.CommandLineRunner;
29-
import org.springframework.boot.SpringApplication;
29+
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
30+
import org.springframework.boot.builder.SpringApplicationBuilder;
3031
import org.springframework.boot.test.IntegrationTest;
3132
import org.springframework.boot.test.SpringApplicationConfiguration;
3233
import org.springframework.context.ConfigurableApplicationContext;
@@ -54,42 +55,64 @@ public class SampleWebSocketsApplicationTests {
5455

5556
private static Log logger = LogFactory.getLog(SampleWebSocketsApplicationTests.class);
5657

57-
private static String WS_URI;
58-
5958
@Value("${local.server.port}")
60-
private int port;
59+
private int port = 1234;
6160

62-
@Before
63-
public void init() {
64-
WS_URI = "ws://localhost:" + this.port + "/echo/websocket";
61+
@Test
62+
public void echoEndpoint() throws Exception {
63+
ConfigurableApplicationContext context = new SpringApplicationBuilder(
64+
ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class)
65+
.properties(
66+
"websocket.uri:ws://localhost:" + this.port + "/echo/websocket")
67+
.run("--spring.main.web_environment=false");
68+
long count = context.getBean(ClientConfiguration.class).latch.getCount();
69+
AtomicReference<String> messagePayloadReference = context
70+
.getBean(ClientConfiguration.class).messagePayload;
71+
context.close();
72+
assertEquals(0, count);
73+
assertEquals("Did you say \"Hello world!\"?", messagePayloadReference.get());
6574
}
6675

6776
@Test
68-
public void runAndWait() throws Exception {
69-
ConfigurableApplicationContext context = SpringApplication.run(
70-
ClientConfiguration.class, "--spring.main.web_environment=false");
77+
public void reverseEndpoint() throws Exception {
78+
ConfigurableApplicationContext context = new SpringApplicationBuilder(
79+
ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class)
80+
.properties("websocket.uri:ws://localhost:" + this.port + "/reverse")
81+
.run("--spring.main.web_environment=false");
7182
long count = context.getBean(ClientConfiguration.class).latch.getCount();
83+
AtomicReference<String> messagePayloadReference = context
84+
.getBean(ClientConfiguration.class).messagePayload;
7285
context.close();
7386
assertEquals(0, count);
87+
assertEquals("Reversed: !dlrow olleH", messagePayloadReference.get());
7488
}
7589

7690
@Configuration
7791
static class ClientConfiguration implements CommandLineRunner {
7892

93+
@Value("${websocket.uri}")
94+
private String webSocketUri;
95+
7996
private final CountDownLatch latch = new CountDownLatch(1);
8097

98+
private final AtomicReference<String> messagePayload = new AtomicReference<String>();
99+
81100
@Override
82101
public void run(String... args) throws Exception {
83102
logger.info("Waiting for response: latch=" + this.latch.getCount());
84-
this.latch.await(10, TimeUnit.SECONDS);
85-
logger.info("Got response: latch=" + this.latch.getCount());
103+
if (this.latch.await(10, TimeUnit.SECONDS)) {
104+
logger.info("Got response: " + this.messagePayload.get());
105+
}
106+
else {
107+
logger.info("Response not received: latch=" + this.latch.getCount());
108+
}
86109
}
87110

88111
@Bean
89112
public WebSocketConnectionManager wsConnectionManager() {
90113

91114
WebSocketConnectionManager manager = new WebSocketConnectionManager(client(),
92-
handler(), WS_URI);
115+
handler(), this.webSocketUri);
93116
manager.setAutoStartup(true);
94117

95118
return manager;
@@ -102,7 +125,8 @@ public StandardWebSocketClient client() {
102125

103126
@Bean
104127
public SimpleClientWebSocketHandler handler() {
105-
return new SimpleClientWebSocketHandler(greetingService(), this.latch);
128+
return new SimpleClientWebSocketHandler(greetingService(), this.latch,
129+
this.messagePayload);
106130
}
107131

108132
@Bean

0 commit comments

Comments
 (0)