Spring Boot——Using WebSocket to build an interactive web app

时间:2022-03-12 00:56:13

Spring Boot——Using WebSocket to build an interactive web app

 

 

Using WebSocket to build an interactive web application

This guide walks you through the process of creating a “Hello, world” application that sends messages back and forth between a browser and a server. WebSocket is a thin, lightweight layer above TCP. This makes it suitable for using “subprotocols” to embed messages. In this guide, we use STOMP messaging with Spring to create an interactive web application.

What You Will build

You will build a server that accepts a message that carries a user’s name. In response, the server will push a greeting into a queue to which the client is subscribed.

What You Need

How to complete this guide

Like most Spring Getting Started guides, you can start from scratch and complete each step or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.

To start from scratch, move on to Starting with Spring Initializr.

To skip the basics, do the following:

When you finish, you can check your results against the code in gs-messaging-stomp-websocket/complete.

Starting with Spring Initializr

For all Spring applications, you should start with the Spring Initializr. The Initializr offers a fast way to pull in all the dependencies you need for an application and does a lot of the set up for you. This example needs only the Websocket dependency. The following image shows the Initializr set up for this sample project:

Spring Boot——Using WebSocket to build an interactive web app
  The preceding image shows the Initializr with Maven chosen as the build tool. You can also use Gradle. It also shows values of com.example and messaging-stomp-websocket as the Group and Artifact, respectively. You will use those values throughout the rest of this sample.

The following listing shows the pom.xml file that is created when you choose Maven:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>messaging-stomp-websocket</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>messaging-stomp-websocket</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

The following listing shows the build.gradle file that is created when you choose Gradle:

plugins {
	id ‘org.springframework.boot‘ version ‘2.2.2.RELEASE‘
	id ‘io.spring.dependency-management‘ version ‘1.0.8.RELEASE‘
	id ‘java‘
}

group = ‘com.example‘
version = ‘0.0.1-SNAPSHOT‘
sourceCompatibility = ‘1.8‘

repositories {
	mavenCentral()
}

dependencies {
	implementation ‘org.springframework.boot:spring-boot-starter-websocket‘
	testImplementation(‘org.springframework.boot:spring-boot-starter-test‘) {
		exclude group: ‘org.junit.vintage‘, module: ‘junit-vintage-engine‘
	}
}

test {
	useJUnitPlatform()
}

Adding Dependencies

The Spring Initializr does not provide everything you need in this case. For Maven, you need to add the following dependencies:

<dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.3.7</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.1.0</version> </dependency>

The following listing shows the finished pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>messaging-stomp-websocket</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>messaging-stomp-websocket</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

If you use Gradle, you need to add the following dependencies:

implementation ‘org.webjars:webjars-locator-core‘ implementation ‘org.webjars:sockjs-client:1.0.2‘ implementation ‘org.webjars:stomp-websocket:2.3.3‘ implementation ‘org.webjars:bootstrap:3.3.7‘ implementation ‘org.webjars:jquery:3.1.0‘

The following listing shows the finished build.gradle file:

plugins {
	id ‘org.springframework.boot‘ version ‘2.2.2.RELEASE‘
	id ‘io.spring.dependency-management‘ version ‘1.0.8.RELEASE‘
	id ‘java‘
}

group = ‘com.example‘
version = ‘0.0.1-SNAPSHOT‘
sourceCompatibility = ‘1.8‘

repositories {
	mavenCentral()
}

dependencies {
	implementation ‘org.springframework.boot:spring-boot-starter-websocket‘
	testImplementation(‘org.springframework.boot:spring-boot-starter-test‘) {
		exclude group: ‘org.junit.vintage‘, module: ‘junit-vintage-engine‘
	}
}

test {
	useJUnitPlatform()
}

Create a Resource Representation Class

Now that you have set up the project and build system, you can create your STOMP message service.

Begin the process by thinking about service interactions.

The service will accept messages that contain a name in a STOMP message whose body is a JSON object. If the name is Fred, the message might resemble the following:

{ "name": "Fred" }

To model the message that carries the name, you can create a plain old Java object with a name property and a corresponding getName() method, as the following listing (from src/main/java/com/example/messagingstompwebsocket/HelloMessage.java) shows:

package com.example.messagingstompwebsocket; public class HelloMessage { private String name; public HelloMessage() { } public HelloMessage(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }

Upon receiving the message and extracting the name, the service will process it by creating a greeting and publishing that greeting on a separate queue to which the client is subscribed. The greeting will also be a JSON object, which as the following listing shows:

{ "content": "Hello, Fred!" }

To model the greeting representation, add another plain old Java object with a contentproperty and a corresponding getContent() method, as the following listing (from src/main/java/com/example/messagingstompwebsocket/Greeting.java) shows:

package com.example.messagingstompwebsocket; public class Greeting { private String content; public Greeting() { } public Greeting(String content) { this.content = content; } public String getContent() { return content; } }

Spring will use the Jackson JSON library to automatically marshal instances of type Greetinginto JSON.

Next, you will create a controller to receive the hello message and send a greeting message.

Create a Message-handling Controller

In Spring’s approach to working with STOMP messaging, STOMP messages can be routed to @Controller classes. For example, the GreetingController (from src/main/java/com/example/messagingstompwebsocket/GreetingController.java) is mapped to handle messages to the /hello destination, as the following listing shows:

package com.example.messagingstompwebsocket; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import org.springframework.web.util.HtmlUtils; @Controller public class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, "   HtmlUtils.htmlEscape(message.getName())   "!"); } }

This controller is concise and simple, but plenty is going on. We break it down step by step.

The @MessageMapping annotation ensures that, if a message is sent to the /hellodestination, the greeting() method is called.

The payload of the message is bound to a HelloMessage object, which is passed into greeting().

Internally, the implementation of the method simulates a processing delay by causing the thread to sleep for one second. This is to demonstrate that, after the client sends a message, the server can take as long as it needs to asynchronously process the message. The client can continue with whatever work it needs to do without waiting for the response.

After the one-second delay, the greeting() method creates a Greeting object and returns it. The return value is broadcast to all subscribers of /topic/greetings, as specified in the @SendTo annotation. Note that the name from the input message is sanitized, since, in this case, it will be echoed back and re-rendered in the browser DOM on the client side.

Configure Spring for STOMP messaging

Now that the essential components of the service are created, you can configure Spring to enable WebSocket and STOMP messaging.

Create a Java class named WebSocketConfig that resembles the following listing (from src/main/java/com/example/messagingstompwebsocket/WebSocketConfig.java):

package com.example.messagingstompwebsocket; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket").withSockJS(); } }

WebSocketConfig is annotated with @Configuration to indicate that it is a Spring configuration class. It is also annotated with @EnableWebSocketMessageBroker. As its name suggests, @EnableWebSocketMessageBroker enables WebSocket message handling, backed by a message broker.

The configureMessageBroker() method implements the default method in WebSocketMessageBrokerConfigurer to configure the message broker. It starts by calling enableSimpleBroker() to enable a simple memory-based message broker to carry the greeting messages back to the client on destinations prefixed with /topic. It also designates the /app prefix for messages that are bound for methods annotated with @MessageMapping. This prefix will be used to define all the message mappings. For example, /app/hello is the endpoint that the GreetingController.greeting() method is mapped to handle.

The registerStompEndpoints() method registers the /gs-guide-websocket endpoint, enabling SockJS fallback options so that alternate transports can be used if WebSocket is not available. The SockJS client will attempt to connect to /gs-guide-websocket and use the best available transport (websocket, xhr-streaming, xhr-polling, and so on).

Create a Browser Client

With the server-side pieces in place, you can turn your attention to the JavaScript client that will send messages to and receive messages from the server side.

Create an index.html file similar to the following listing (from src/main/resources/static/index.html):

<!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/main.css" rel="stylesheet"> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/app.js"></script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn‘t support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div id="main-content" class="container"> <div class="row"> <div class="col-md-6"