├── .gitignore ├── .travis.yml ├── LICENSE ├── Procfile ├── README.md ├── pom.xml ├── with-sockjs ├── .babelrc ├── README.md ├── package.json ├── pom.xml ├── src │ └── main │ │ ├── java │ │ └── lahsivjar │ │ │ └── spring │ │ │ └── websocket │ │ │ └── template │ │ │ ├── Application.java │ │ │ ├── ChatHistoryDao.java │ │ │ ├── MessageController.java │ │ │ ├── WebConfig.java │ │ │ └── WebSocketConfig.java │ │ ├── resources │ │ ├── application.properties │ │ └── templates │ │ │ └── index.html │ │ └── webapp │ │ └── js │ │ └── index.js └── webpack.config.js └── without-sockjs ├── pom.xml └── src └── main ├── java └── lahsivjar │ └── spring │ └── websocket │ └── template │ ├── Application.java │ ├── CustomWebSocketHandler.java │ └── WebSocketConfig.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | dynamic_resources 5 | node_modules 6 | target 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: trusty 3 | jdk: 4 | - oraclejdk8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vishal Raj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -jar with-sockjs/target/with-sockjs-0.1-SNAPSHOT.jar 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-websocket-template 2 | [![Build Status](https://travis-ci.org/lahsivjar/spring-websocket-template.svg?branch=master)](https://travis-ci.org/lahsivjar/spring-websocket-template) 3 | 4 | Provides a template for [spring](https://spring.io/) websocket usage (with and without SockJS support) 5 | 6 | The project provides 2 modules: 7 | 8 | * `without-sockjs`: Plain websocket integration with [Spring Boot](https://projects.spring.io/spring-boot/) 9 | * `with-sockjs`: Websocket integration with [Spring Boot](https://projects.spring.io/spring-boot/) for backend and [React](https://reactjs.org/) for frontend. Checkout [README](with-sockjs/README.md) for more information. This module is also deployed at: https://react-websocket.herokuapp.com/index 10 | 11 | ## Installation 12 | 13 | ``` 14 | mvn install 15 | ``` 16 | 17 | ## Issues 18 | 19 | Report any issues or bugs to https://github.com/lahsivjar/spring-websocket-template/issues Pull requests are welcomed. 20 | 21 | ## License 22 | 23 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 24 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | lahsivjar.spring.websocket.template 8 | spring-ws-template-parent 9 | 0.1-SNAPSHOT 10 | pom 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 1.5.4.RELEASE 16 | 17 | 18 | 19 | 20 | 1.5.4.RELEASE 21 | 4.2.2.RELEASE 22 | 23 | 24 | 25 | 26 | 27 | org.springframework 28 | spring-websocket 29 | ${springframework.version} 30 | 31 | 32 | org.springframework 33 | spring-messaging 34 | ${springframework.version} 35 | 36 | 37 | 38 | 39 | 40 | without-sockjs 41 | with-sockjs 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /with-sockjs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react" 5 | ], 6 | "plugins": [ 7 | ["transform-class-properties"] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /with-sockjs/README.md: -------------------------------------------------------------------------------- 1 | # spring-websocket-template (with SockJS) 2 | 3 | The template provides WebSocket integration with [Spring Boot](https://projects.spring.io/spring-boot/) with [STOMP](https://stomp.github.io/) and [SockJS](https://github.com/sockjs) fallback options. The template also provides [ReactJS](https://reactjs.org/) integration and a functioning web interface using React UI components. 4 | 5 | ## Usage 6 | 7 | 1. Installation 8 | ``` 9 | mvn install 10 | ``` 11 | 2. Start spring boot server 12 | ``` 13 | mvn spring-boot:run 14 | ``` 15 | 3. Open http://localhost:8080/index in browser 16 | 17 | ## Demonstration 18 | 19 | https://react-websocket.herokuapp.com/index 20 | 21 | ## React components used 22 | 23 | * [react-stomp](https://github.com/lahsivjar/react-stomp) 24 | * [react-talk](https://github.com/lahsivjar/react-talk) 25 | 26 | ## Issues 27 | 28 | Report any issues or bugs to https://github.com/lahsivjar/spring-websocket-template/issues Pull requests are welcomed. 29 | -------------------------------------------------------------------------------- /with-sockjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-websocket-with-react-template", 3 | "version": "1.0.0", 4 | "description": "Spring websocket with react template", 5 | "main": "./src/main/resources/dynamic_resources/bundle.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/lahsivjar/spring-websocket-template.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "websocket", 16 | "spring", 17 | "springboot", 18 | "maven" 19 | ], 20 | "author": "", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/lahsivjar/spring-websocket-template/issues" 24 | }, 25 | "homepage": "https://github.com/lahsivjar/spring-websocket-template#readme", 26 | "devDependencies": { 27 | "babel-cli": "^6.26.0", 28 | "babel-loader": "^7.1.2", 29 | "babel-plugin-transform-class-properties": "^6.24.1", 30 | "babel-preset-env": "^1.6.1", 31 | "babel-preset-react": "^6.24.1", 32 | "babel-preset-stage-2": "^6.24.1", 33 | "react": "^16.2.0", 34 | "react-dom": "^16.2.0", 35 | "webpack": "~3.4.1" 36 | }, 37 | "dependencies": { 38 | "json-fetch": "^7.4.0", 39 | "randomstring": "^1.1.5", 40 | "react-stomp": "^4.2.0", 41 | "react-talk": "^1.0.0", 42 | "username-generator": "^1.0.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /with-sockjs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | with-sockjs 9 | 10 | 11 | lahsivjar.spring.websocket.template 12 | spring-ws-template-parent 13 | 0.1-SNAPSHOT 14 | .. 15 | 16 | 17 | 18 | 1.8 19 | 1.8 20 | ${springboot.version} 21 | 3.0.0 22 | 1.3 23 | v6.10.0 24 | 4.3.0 25 | false 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-thymeleaf 32 | 33 | 34 | org.springframework 35 | spring-websocket 36 | 37 | 38 | org.springframework 39 | spring-messaging 40 | 41 | 42 | com.google.guava 43 | guava 44 | 24.0-jre 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-maven-plugin 53 | ${springboot.maven.plugin.version} 54 | 55 | 56 | 57 | repackage 58 | 59 | 60 | 61 | 62 | 63 | maven-clean-plugin 64 | ${maven.clean.plugin.version} 65 | 66 | 67 | 68 | src/main/resources/dynamic_resources 69 | node_modules 70 | 71 | 72 | 73 | 74 | 75 | com.github.eirslett 76 | frontend-maven-plugin 77 | ${frontend.maven.plugin.version} 78 | 79 | target 80 | ${skip.frontend} 81 | 82 | 83 | 84 | node-npm-install 85 | 86 | install-node-and-npm 87 | 88 | 89 | ${node.version} 90 | ${npm.version} 91 | 92 | 93 | 94 | npm-install 95 | 96 | npm 97 | 98 | 99 | install 100 | 101 | 102 | 103 | webpack 104 | 105 | webpack 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /with-sockjs/src/main/java/lahsivjar/spring/websocket/template/Application.java: -------------------------------------------------------------------------------- 1 | package lahsivjar.spring.websocket.template; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | public static void main(String[] args) throws Exception { 9 | SpringApplication.run(Application.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /with-sockjs/src/main/java/lahsivjar/spring/websocket/template/ChatHistoryDao.java: -------------------------------------------------------------------------------- 1 | package lahsivjar.spring.websocket.template; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.UUID; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.stream.Collectors; 8 | 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.google.common.cache.Cache; 12 | import com.google.common.cache.CacheBuilder; 13 | 14 | @Component 15 | public class ChatHistoryDao { 16 | 17 | // A simple cache for temporarily storing chat data 18 | private final Cache> chatHistoryCache = CacheBuilder 19 | .newBuilder().maximumSize(20).expireAfterWrite(10, TimeUnit.MINUTES) 20 | .build(); 21 | 22 | public void save(Map chatObj) { 23 | this.chatHistoryCache.put(UUID.randomUUID(), chatObj); 24 | } 25 | 26 | public List> get() { 27 | return chatHistoryCache.asMap().values().stream() 28 | .sorted((c1, c2) -> Long.valueOf(c1.get("timestamp")) 29 | .compareTo(Long.valueOf(c2.get("timestamp")))) 30 | .collect(Collectors.toList()); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /with-sockjs/src/main/java/lahsivjar/spring/websocket/template/MessageController.java: -------------------------------------------------------------------------------- 1 | package lahsivjar.spring.websocket.template; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.messaging.handler.annotation.MessageMapping; 8 | import org.springframework.messaging.handler.annotation.Payload; 9 | import org.springframework.messaging.handler.annotation.SendTo; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RestController 14 | public class MessageController { 15 | 16 | @Autowired 17 | private ChatHistoryDao chatHistoryDao; 18 | 19 | /* 20 | * This MessageMapping annotated method will be handled by 21 | * SimpAnnotationMethodMessageHandler and after that the Message will be 22 | * forwarded to Broker channel to be forwarded to the client via WebSocket 23 | */ 24 | @MessageMapping("/all") 25 | @SendTo("/topic/all") 26 | public Map post(@Payload Map message) { 27 | message.put("timestamp", Long.toString(System.currentTimeMillis())); 28 | chatHistoryDao.save(message); 29 | return message; 30 | } 31 | 32 | @RequestMapping("/history") 33 | public List> getChatHistory() { 34 | return chatHistoryDao.get(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /with-sockjs/src/main/java/lahsivjar/spring/websocket/template/WebConfig.java: -------------------------------------------------------------------------------- 1 | package lahsivjar.spring.websocket.template; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 5 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 7 | 8 | @Configuration 9 | public class WebConfig extends WebMvcConfigurerAdapter { 10 | 11 | @Override 12 | public void addViewControllers(ViewControllerRegistry registry) { 13 | registry.addViewController("/index").setViewName("index"); 14 | } 15 | 16 | @Override 17 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 18 | registry.addResourceHandler("/dynamic_resources/**") 19 | .addResourceLocations("classpath:/dynamic_resources/"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /with-sockjs/src/main/java/lahsivjar/spring/websocket/template/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package lahsivjar.spring.websocket.template; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 7 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void registerStompEndpoints(StompEndpointRegistry registry) { 15 | registry.addEndpoint("/handler").withSockJS(); 16 | } 17 | 18 | @Override 19 | public void configureMessageBroker(MessageBrokerRegistry brokerRegistry) { 20 | /* 21 | * The application destination prefix is an arbitrary prefix to 22 | * differentiate between messages that need to be routed to 23 | * message-handling methods for application level work vs messages to be 24 | * routed to the broker to broadcast to subscribed clients. After 25 | * application level work is finished the message can be routed to 26 | * broker for broadcasting. 27 | */ 28 | brokerRegistry.setApplicationDestinationPrefixes("/app"); 29 | 30 | /* 31 | * The list of destination prefixes provided in this are based on what 32 | * broker is getting used. In this case we will use in-memory broker 33 | * which doesn't have any such requirements. For the purpose of 34 | * maintaining convention the "/topic" and the "/queue" prefixes are 35 | * chosen. The convention dictates usage of "/topic" destination for 36 | * pub-sub model targeting many subscribers and the "/queue" destination 37 | * for point to point messaging. 38 | */ 39 | brokerRegistry.enableSimpleBroker("/topic", "/queue"); 40 | 41 | /* 42 | * For configuring dedicated broker use the below code. 43 | */ 44 | // brokerRegistry.enableStompBrokerRelay("/topic", "/queue"); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /with-sockjs/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.org.springframework.web=INFO 2 | 3 | spring.profiles.active=production 4 | 5 | server.port=${PORT:8080} 6 | -------------------------------------------------------------------------------- /with-sockjs/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spring WebSockets with ReactJs 6 | 7 | 8 |
Refresh page to chat as another user
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /with-sockjs/src/main/webapp/js/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDom from "react-dom"; 3 | import SockJsClient from "react-stomp"; 4 | import UsernameGenerator from "username-generator"; 5 | import Fetch from "json-fetch"; 6 | import { TalkBox } from "react-talk"; 7 | 8 | const randomstring = require("randomstring"); 9 | 10 | class App extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | // randomUserId is used to emulate a unique user id for this demo usage 14 | this.randomUserName = UsernameGenerator.generateUsername("-"); 15 | this.randomUserId = randomstring.generate(); 16 | this.state = { 17 | clientConnected: false, 18 | messages: [] 19 | }; 20 | } 21 | 22 | onMessageReceive = (msg, topic) => { 23 | this.setState(prevState => ({ 24 | messages: [...prevState.messages, msg] 25 | })); 26 | } 27 | 28 | sendMessage = (msg, selfMsg) => { 29 | try { 30 | this.clientRef.sendMessage("/app/all", JSON.stringify(selfMsg)); 31 | return true; 32 | } catch(e) { 33 | return false; 34 | } 35 | } 36 | 37 | componentWillMount() { 38 | Fetch("/history", { 39 | method: "GET" 40 | }).then((response) => { 41 | this.setState({ messages: response.body }); 42 | }); 43 | } 44 | 45 | render() { 46 | const wsSourceUrl = window.location.protocol + "//" + window.location.host + "/handler"; 47 | return ( 48 |
49 | 52 | 53 | { this.clientRef = client }} 55 | onConnect={ () => { this.setState({ clientConnected: true }) } } 56 | onDisconnect={ () => { this.setState({ clientConnected: false }) } } 57 | debug={ false }/> 58 |
59 | ); 60 | } 61 | } 62 | 63 | ReactDom.render(, document.getElementById("root")); 64 | -------------------------------------------------------------------------------- /with-sockjs/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./src/main/webapp/js/index.js", 5 | devtool: "inline-source-map", 6 | output: { 7 | path: path.join(__dirname, "/src/main/resources/dynamic_resources"), 8 | filename: "bundle.js" 9 | }, 10 | module: { 11 | loaders: [ 12 | { 13 | test: path.join(__dirname, "./src/main/webapp/js"), 14 | loader: "babel-loader", 15 | query: { 16 | presets: ["env", "react", "stage-2"] 17 | } 18 | } 19 | ] 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /without-sockjs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | without-sockjs 9 | 10 | 11 | lahsivjar.spring.websocket.template 12 | spring-ws-template-parent 13 | 0.1-SNAPSHOT 14 | .. 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-thymeleaf 21 | 22 | 23 | org.springframework 24 | spring-websocket 25 | 26 | 27 | org.springframework 28 | spring-messaging 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /without-sockjs/src/main/java/lahsivjar/spring/websocket/template/Application.java: -------------------------------------------------------------------------------- 1 | package lahsivjar.spring.websocket.template; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | public static void main(String[] args) throws Exception { 9 | SpringApplication.run(Application.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /without-sockjs/src/main/java/lahsivjar/spring/websocket/template/CustomWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package lahsivjar.spring.websocket.template; 2 | 3 | import java.io.IOException; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.web.socket.TextMessage; 8 | import org.springframework.web.socket.WebSocketSession; 9 | import org.springframework.web.socket.handler.TextWebSocketHandler; 10 | 11 | public class CustomWebSocketHandler extends TextWebSocketHandler { 12 | 13 | private static final Logger logger = LoggerFactory.getLogger(CustomWebSocketHandler.class); 14 | 15 | /* 16 | * WebSocket is a very thin layer over TCP and, by default, leaves it upto 17 | * the application to interpret the meaning of message. Since, in this 18 | * example/template we don't use any messaging sub protocol (which is 19 | * allowed by WebSocket RFC-6455) we need to handle the message in its raw 20 | * form or possibly build a custom framework. 21 | */ 22 | @Override 23 | public void handleTextMessage(WebSocketSession session, TextMessage message) { 24 | try { 25 | TextMessage ackMessage = new TextMessage("ACK: " + message.getPayload()); 26 | session.sendMessage(ackMessage); 27 | } catch (IOException e) { 28 | logger.warn("Failed to send message to session {}, ignoring quietly", session.getId()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /without-sockjs/src/main/java/lahsivjar/spring/websocket/template/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package lahsivjar.spring.websocket.template; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.WebSocketHandler; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 7 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 8 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 9 | 10 | @Configuration 11 | @EnableWebSocket 12 | public class WebSocketConfig implements WebSocketConfigurer { 13 | 14 | @Override 15 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 16 | registry.addHandler(webSocketHandler(), "/handler") 17 | // for ease of testing allow all origins 18 | .setAllowedOrigins("*"); 19 | } 20 | 21 | @Bean 22 | public WebSocketHandler webSocketHandler() { 23 | return new CustomWebSocketHandler(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /without-sockjs/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.org.springframework.web=DEBUG 2 | --------------------------------------------------------------------------------