mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
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
This commit is contained in:
parent
595f387fb2
commit
a9b88d6955
@ -98,7 +98,7 @@
|
||||
<snakeyaml.version>1.13</snakeyaml.version>
|
||||
<solr.version>4.7.2</solr.version>
|
||||
<spock.version>0.7-groovy-2.0</spock.version>
|
||||
<spring.version>4.0.7.RELEASE</spring.version>
|
||||
<spring.version>4.0.8.BUILD-SNAPSHOT</spring.version>
|
||||
<spring-amqp.version>1.3.6.RELEASE</spring-amqp.version>
|
||||
<spring-batch.version>3.0.2.RELEASE</spring-batch.version>
|
||||
<spring-data-releasetrain.version>Dijkstra-SR4</spring-data-releasetrain.version>
|
||||
|
@ -617,6 +617,26 @@ change the version properties, e.g. for a simple webapp or service:
|
||||
|
||||
|
||||
|
||||
[[howto-create-websocket-endpoints-using-serverendpoint]]
|
||||
=== Create WebSocket endpoints using @ServerEndpoint
|
||||
If you want to use `@ServerEndpoint` in a Spring Boot application that used an embedded
|
||||
container, you must declare a single `ServerEndpointExporter` `@Bean`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
@Bean
|
||||
public ServerEndpointExporter serverEndpointExporter() {
|
||||
return new ServerEndpointExporter();
|
||||
}
|
||||
----
|
||||
|
||||
This bean will register any `@ServerEndpoint` annotated beans with the underlying
|
||||
WebSocket container. When deployed to a standalone servlet container this role is
|
||||
performed by a servlet container initializer and the `ServerEndpointExporter` bean is
|
||||
not required.
|
||||
|
||||
|
||||
|
||||
[[howto-spring-mvc]]
|
||||
== Spring MVC
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
package samples.websocket.client;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@ -33,11 +34,14 @@ public class SimpleClientWebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
private final CountDownLatch latch;
|
||||
|
||||
private final AtomicReference<String> messagePayload;
|
||||
|
||||
@Autowired
|
||||
public SimpleClientWebSocketHandler(GreetingService greetingService,
|
||||
CountDownLatch latch) {
|
||||
CountDownLatch latch, AtomicReference<String> message) {
|
||||
this.greetingService = greetingService;
|
||||
this.latch = latch;
|
||||
this.messagePayload = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -51,6 +55,7 @@ public class SimpleClientWebSocketHandler extends TextWebSocketHandler {
|
||||
throws Exception {
|
||||
this.logger.info("Received: " + message + " (" + this.latch.getCount() + ")");
|
||||
session.close();
|
||||
this.messagePayload.set(message.getPayload());
|
||||
this.latch.countDown();
|
||||
}
|
||||
|
||||
|
@ -27,12 +27,14 @@ import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
import org.springframework.web.socket.handler.PerConnectionWebSocketHandler;
|
||||
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
|
||||
|
||||
import samples.websocket.client.GreetingService;
|
||||
import samples.websocket.client.SimpleGreetingService;
|
||||
import samples.websocket.echo.DefaultEchoService;
|
||||
import samples.websocket.echo.EchoService;
|
||||
import samples.websocket.echo.EchoWebSocketHandler;
|
||||
import samples.websocket.reverse.ReverseWebSocketEndpoint;
|
||||
import samples.websocket.snake.SnakeWebSocketHandler;
|
||||
|
||||
@Configuration
|
||||
@ -76,4 +78,14 @@ public class SampleWebSocketsApplication extends SpringBootServletInitializer im
|
||||
return new PerConnectionWebSocketHandler(SnakeWebSocketHandler.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReverseWebSocketEndpoint reverseWebSocketEndpoint() {
|
||||
return new ReverseWebSocketEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServerEndpointExporter serverEndpointExporter() {
|
||||
return new ServerEndpointExporter();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package samples.websocket.reverse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.websocket.OnMessage;
|
||||
import javax.websocket.Session;
|
||||
import javax.websocket.server.ServerEndpoint;
|
||||
|
||||
@ServerEndpoint("/reverse")
|
||||
public class ReverseWebSocketEndpoint {
|
||||
|
||||
@OnMessage
|
||||
public void handleMessage(Session session, String message) throws IOException {
|
||||
session.getBasicRemote().sendText(
|
||||
"Reversed: " + new StringBuilder(message).reverse());
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@
|
||||
<p>Please select the sample you would like to try.</p>
|
||||
<ul>
|
||||
<li><a href="./echo.html">Echo</a></li>
|
||||
<li><a href="./reverse.html">Reverse</a></li>
|
||||
<li><a href="./snake.html">Snake</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
|
@ -0,0 +1,140 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket Examples: Reverse</title>
|
||||
<style type="text/css">
|
||||
#connect-container {
|
||||
float: left;
|
||||
width: 400px
|
||||
}
|
||||
|
||||
#connect-container div {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#console-container {
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
#console {
|
||||
border: 1px solid #CCCCCC;
|
||||
border-right-color: #999999;
|
||||
border-bottom-color: #999999;
|
||||
height: 170px;
|
||||
overflow-y: scroll;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#console p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
var ws = null;
|
||||
|
||||
function setConnected(connected) {
|
||||
document.getElementById('connect').disabled = connected;
|
||||
document.getElementById('disconnect').disabled = !connected;
|
||||
document.getElementById('reverse').disabled = !connected;
|
||||
}
|
||||
|
||||
function connect() {
|
||||
var target = document.getElementById('target').value;
|
||||
ws = new WebSocket(target);
|
||||
ws.onopen = function () {
|
||||
setConnected(true);
|
||||
log('Info: WebSocket connection opened.');
|
||||
};
|
||||
ws.onmessage = function (event) {
|
||||
log('Received: ' + event.data);
|
||||
};
|
||||
ws.onclose = function () {
|
||||
setConnected(false);
|
||||
log('Info: WebSocket connection closed.');
|
||||
};
|
||||
}
|
||||
|
||||
function updateTarget() {
|
||||
if (window.location.protocol == 'http:') {
|
||||
document.getElementById('target').value = 'ws://' + window.location.host + document.getElementById('target').value;
|
||||
} else {
|
||||
document.getElementById('target').value = 'wss://' + window.location.host + document.getElementById('target').value;
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (ws != null) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
setConnected(false);
|
||||
}
|
||||
|
||||
function reverse() {
|
||||
if (ws != null) {
|
||||
var message = document.getElementById('message').value;
|
||||
log('Sent: ' + message);
|
||||
ws.send(message);
|
||||
} else {
|
||||
alert('WebSocket connection not established, please connect.');
|
||||
}
|
||||
}
|
||||
|
||||
function log(message) {
|
||||
var console = document.getElementById('console');
|
||||
var p = document.createElement('p');
|
||||
p.style.wordWrap = 'break-word';
|
||||
p.appendChild(document.createTextNode(message));
|
||||
console.appendChild(p);
|
||||
while (console.childNodes.length > 25) {
|
||||
console.removeChild(console.firstChild);
|
||||
}
|
||||
console.scrollTop = console.scrollHeight;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="updateTarget()">
|
||||
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable
|
||||
Javascript and reload this page!</h2></noscript>
|
||||
<div>
|
||||
<div id="connect-container">
|
||||
<div>
|
||||
<input id="target" type="text" size="40" style="width: 350px" value="/reverse"/>
|
||||
</div>
|
||||
<div>
|
||||
<button id="connect" onclick="connect();">Connect</button>
|
||||
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="message" style="width: 350px">Here is a message!</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<button id="reverse" onclick="reverse();" disabled="disabled">Reverse message</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="console-container">
|
||||
<div id="console"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -14,19 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package samples.websocket.echo;
|
||||
package samples.websocket;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@ -54,42 +55,64 @@ public class SampleWebSocketsApplicationTests {
|
||||
|
||||
private static Log logger = LogFactory.getLog(SampleWebSocketsApplicationTests.class);
|
||||
|
||||
private static String WS_URI;
|
||||
|
||||
@Value("${local.server.port}")
|
||||
private int port;
|
||||
private int port = 1234;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
WS_URI = "ws://localhost:" + this.port + "/echo/websocket";
|
||||
@Test
|
||||
public void echoEndpoint() throws Exception {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class)
|
||||
.properties(
|
||||
"websocket.uri:ws://localhost:" + this.port + "/echo/websocket")
|
||||
.run("--spring.main.web_environment=false");
|
||||
long count = context.getBean(ClientConfiguration.class).latch.getCount();
|
||||
AtomicReference<String> messagePayloadReference = context
|
||||
.getBean(ClientConfiguration.class).messagePayload;
|
||||
context.close();
|
||||
assertEquals(0, count);
|
||||
assertEquals("Did you say \"Hello world!\"?", messagePayloadReference.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runAndWait() throws Exception {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(
|
||||
ClientConfiguration.class, "--spring.main.web_environment=false");
|
||||
public void reverseEndpoint() throws Exception {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class)
|
||||
.properties("websocket.uri:ws://localhost:" + this.port + "/reverse")
|
||||
.run("--spring.main.web_environment=false");
|
||||
long count = context.getBean(ClientConfiguration.class).latch.getCount();
|
||||
AtomicReference<String> messagePayloadReference = context
|
||||
.getBean(ClientConfiguration.class).messagePayload;
|
||||
context.close();
|
||||
assertEquals(0, count);
|
||||
assertEquals("Reversed: !dlrow olleH", messagePayloadReference.get());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClientConfiguration implements CommandLineRunner {
|
||||
|
||||
@Value("${websocket.uri}")
|
||||
private String webSocketUri;
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
private final AtomicReference<String> messagePayload = new AtomicReference<String>();
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
logger.info("Waiting for response: latch=" + this.latch.getCount());
|
||||
this.latch.await(10, TimeUnit.SECONDS);
|
||||
logger.info("Got response: latch=" + this.latch.getCount());
|
||||
if (this.latch.await(10, TimeUnit.SECONDS)) {
|
||||
logger.info("Got response: " + this.messagePayload.get());
|
||||
}
|
||||
else {
|
||||
logger.info("Response not received: latch=" + this.latch.getCount());
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketConnectionManager wsConnectionManager() {
|
||||
|
||||
WebSocketConnectionManager manager = new WebSocketConnectionManager(client(),
|
||||
handler(), WS_URI);
|
||||
handler(), this.webSocketUri);
|
||||
manager.setAutoStartup(true);
|
||||
|
||||
return manager;
|
||||
@ -102,7 +125,8 @@ public class SampleWebSocketsApplicationTests {
|
||||
|
||||
@Bean
|
||||
public SimpleClientWebSocketHandler handler() {
|
||||
return new SimpleClientWebSocketHandler(greetingService(), this.latch);
|
||||
return new SimpleClientWebSocketHandler(greetingService(), this.latch,
|
||||
this.messagePayload);
|
||||
}
|
||||
|
||||
@Bean
|
@ -18,13 +18,16 @@ package samples.websocket.echo;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
|
||||
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
@ -60,8 +63,6 @@ public class CustomContainerWebSocketsApplicationTests {
|
||||
|
||||
private static int PORT = SocketUtils.findAvailableTcpPort();
|
||||
|
||||
private static final String WS_URI = "ws://localhost:" + PORT + "/ws/echo/websocket";
|
||||
|
||||
@Configuration
|
||||
protected static class CustomContainerConfiguration {
|
||||
@Bean
|
||||
@ -71,31 +72,59 @@ public class CustomContainerWebSocketsApplicationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runAndWait() throws Exception {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(
|
||||
ClientConfiguration.class, "--spring.main.web_environment=false");
|
||||
public void echoEndpoint() throws Exception {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class)
|
||||
.properties("websocket.uri:ws://localhost:" + PORT + "/ws/echo/websocket")
|
||||
.run("--spring.main.web_environment=false");
|
||||
long count = context.getBean(ClientConfiguration.class).latch.getCount();
|
||||
AtomicReference<String> messagePayloadReference = context
|
||||
.getBean(ClientConfiguration.class).messagePayload;
|
||||
context.close();
|
||||
assertEquals(0, count);
|
||||
assertEquals("Did you say \"Hello world!\"?", messagePayloadReference.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reverseEndpoint() throws Exception {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(
|
||||
ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class)
|
||||
.properties("websocket.uri:ws://localhost:" + PORT + "/ws/reverse").run(
|
||||
"--spring.main.web_environment=false");
|
||||
long count = context.getBean(ClientConfiguration.class).latch.getCount();
|
||||
AtomicReference<String> messagePayloadReference = context
|
||||
.getBean(ClientConfiguration.class).messagePayload;
|
||||
context.close();
|
||||
assertEquals(0, count);
|
||||
assertEquals("Reversed: !dlrow olleH", messagePayloadReference.get());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClientConfiguration implements CommandLineRunner {
|
||||
|
||||
@Value("${websocket.uri}")
|
||||
private String webSocketUri;
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
private final AtomicReference<String> messagePayload = new AtomicReference<String>();
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
logger.info("Waiting for response: latch=" + this.latch.getCount());
|
||||
this.latch.await(10, TimeUnit.SECONDS);
|
||||
logger.info("Got response: latch=" + this.latch.getCount());
|
||||
if (this.latch.await(10, TimeUnit.SECONDS)) {
|
||||
logger.info("Got response: " + this.messagePayload.get());
|
||||
}
|
||||
else {
|
||||
logger.info("Response not received: latch=" + this.latch.getCount());
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketConnectionManager wsConnectionManager() {
|
||||
|
||||
WebSocketConnectionManager manager = new WebSocketConnectionManager(client(),
|
||||
handler(), WS_URI);
|
||||
handler(), this.webSocketUri);
|
||||
manager.setAutoStartup(true);
|
||||
|
||||
return manager;
|
||||
@ -108,7 +137,8 @@ public class CustomContainerWebSocketsApplicationTests {
|
||||
|
||||
@Bean
|
||||
public SimpleClientWebSocketHandler handler() {
|
||||
return new SimpleClientWebSocketHandler(greetingService(), this.latch);
|
||||
return new SimpleClientWebSocketHandler(greetingService(), this.latch,
|
||||
this.messagePayload);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
Loading…
Reference in New Issue
Block a user