From a9b88d6955ca1877056708417384286c8b75dd8b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Oct 2014 16:02:48 +0100 Subject: [PATCH] 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 --- spring-boot-dependencies/pom.xml | 2 +- spring-boot-docs/src/main/asciidoc/howto.adoc | 20 +++ .../client/SimpleClientWebSocketHandler.java | 7 +- .../config/SampleWebSocketsApplication.java | 12 ++ .../reverse/ReverseWebSocketEndpoint.java | 33 +++++ .../src/main/resources/static/index.html | 1 + .../src/main/resources/static/reverse.html | 140 ++++++++++++++++++ .../SampleWebSocketsApplicationTests.java | 56 +++++-- ...omContainerWebSocketsApplicationTests.java | 50 +++++-- 9 files changed, 293 insertions(+), 28 deletions(-) create mode 100644 spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java create mode 100644 spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/reverse.html rename spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/{echo => }/SampleWebSocketsApplicationTests.java (63%) diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index fe7fdc75d9..a205985bb2 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -98,7 +98,7 @@ 1.13 4.7.2 0.7-groovy-2.0 - 4.0.7.RELEASE + 4.0.8.BUILD-SNAPSHOT 1.3.6.RELEASE 3.0.2.RELEASE Dijkstra-SR4 diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index cec330ae72..a967d4931f 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -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 diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java index 1b3dc4ef89..a3bbfc2f1c 100644 --- a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java @@ -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 messagePayload; + @Autowired public SimpleClientWebSocketHandler(GreetingService greetingService, - CountDownLatch latch) { + CountDownLatch latch, AtomicReference 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(); } diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/config/SampleWebSocketsApplication.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/config/SampleWebSocketsApplication.java index 8d84d2fcd2..7bc9fd37a8 100644 --- a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/config/SampleWebSocketsApplication.java +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/config/SampleWebSocketsApplication.java @@ -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(); + } + } diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java new file mode 100644 index 0000000000..a7802edcd2 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java @@ -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()); + } +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html index 39069b15d7..e2b76b6e44 100644 --- a/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html @@ -25,6 +25,7 @@

Please select the sample you would like to try.

diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/reverse.html b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/reverse.html new file mode 100644 index 0000000000..a87d2e8255 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/reverse.html @@ -0,0 +1,140 @@ + + + + + WebSocket Examples: Reverse + + + + + +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/echo/SampleWebSocketsApplicationTests.java b/spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/SampleWebSocketsApplicationTests.java similarity index 63% rename from spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/echo/SampleWebSocketsApplicationTests.java rename to spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/SampleWebSocketsApplicationTests.java index cd85062126..cff0db5490 100644 --- a/spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/echo/SampleWebSocketsApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/SampleWebSocketsApplicationTests.java @@ -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 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 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 messagePayload = new AtomicReference(); + @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 diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/echo/CustomContainerWebSocketsApplicationTests.java b/spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/echo/CustomContainerWebSocketsApplicationTests.java index 7860c560d8..1df5822890 100644 --- a/spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/echo/CustomContainerWebSocketsApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-websocket/src/test/java/samples/websocket/echo/CustomContainerWebSocketsApplicationTests.java @@ -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 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 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 messagePayload = new AtomicReference(); + @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