├── .gitignore ├── README.md ├── pom.xml └── src └── main ├── java └── hello │ ├── Application.java │ ├── CounterHandler.java │ ├── CounterService.java │ └── WebSocketConfig.java └── resources ├── application.properties └── static ├── app ├── angular2-websocket.ts ├── app.component.ts ├── app.module.ts └── main.ts ├── index.html ├── style.css ├── systemjs.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | target 3 | **/*.iml 4 | **/*.tmp 5 | .idea 6 | .gradle 7 | */target/** 8 | */build/** 9 | **/*.log 10 | **/.settings 11 | **/.classpath 12 | **/.project 13 | **/*.class 14 | **/bin 15 | **/lib 16 | **/src/main/generated 17 | **/node_modules/* 18 | .DS_Store 19 | target 20 | /.apt_generated/ 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng2-spring-websocket-demo 2 | Demo Application for Spring Boot, WebSocket and Angular2 3 | 4 | Source code for blog post: 5 | http://mmrath.com/post/websockets-with-angular2-and-spring-boot/ 6 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.mmrath.demo 7 | ng2-spring-websocket-demo 8 | 0.1.0 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 1.4.0.RELEASE 14 | 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-websocket 20 | 21 | 22 | 23 | 24 | 1.8 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-maven-plugin 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/hello/Application.java: -------------------------------------------------------------------------------- 1 | package hello; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/hello/CounterHandler.java: -------------------------------------------------------------------------------- 1 | package hello; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.springframework.web.socket.TextMessage; 5 | import org.springframework.web.socket.WebSocketSession; 6 | import org.springframework.web.socket.handler.TextWebSocketHandler; 7 | 8 | @Component 9 | public class CounterHandler extends TextWebSocketHandler { 10 | 11 | WebSocketSession session; 12 | 13 | // This will send only to one client(most recently connected) 14 | public void counterIncrementedCallback(int counter) { 15 | System.out.println("Trying to send:" + counter); 16 | if (session != null && session.isOpen()) { 17 | try { 18 | System.out.println("Now sending:" + counter); 19 | session.sendMessage(new TextMessage("{\"value\": \"" + counter + "\"}")); 20 | } catch (Exception e) { 21 | e.printStackTrace(); 22 | } 23 | } else { 24 | System.out.println("Don't have open session to send:" + counter); 25 | } 26 | } 27 | 28 | @Override 29 | public void afterConnectionEstablished(WebSocketSession session) { 30 | System.out.println("Connection established"); 31 | this.session = session; 32 | } 33 | 34 | @Override 35 | protected void handleTextMessage(WebSocketSession session, TextMessage message) 36 | throws Exception { 37 | if ("CLOSE".equalsIgnoreCase(message.getPayload())) { 38 | session.close(); 39 | } else { 40 | System.out.println("Received:" + message.getPayload()); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/hello/CounterService.java: -------------------------------------------------------------------------------- 1 | package hello; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.scheduling.annotation.Scheduled; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class CounterService { 11 | 12 | private AtomicInteger counter = new AtomicInteger(0); 13 | 14 | @Autowired 15 | CounterHandler counterHandler; 16 | 17 | @Scheduled(fixedDelay = 1000) 18 | public void sendCounterUpdate() { 19 | counterHandler.counterIncrementedCallback(counter.incrementAndGet()); 20 | } 21 | 22 | Integer getValue() { 23 | return counter.get(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/hello/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package hello; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 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 | @EnableScheduling 13 | public class WebSocketConfig implements WebSocketConfigurer { 14 | 15 | @Autowired 16 | CounterHandler counterHandler; 17 | 18 | @Override 19 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 20 | registry.addHandler(counterHandler, "/counter"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8088 -------------------------------------------------------------------------------- /src/main/resources/static/app/angular2-websocket.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Observable} from "rxjs/Observable"; 3 | import {Scheduler} from "rxjs/Rx"; 4 | import {Subject} from "rxjs/Subject"; 5 | 6 | 7 | 8 | 9 | @Injectable() 10 | export class $WebSocket { 11 | 12 | private reconnectAttempts = 0; 13 | private sendQueue = []; 14 | private onOpenCallbacks = []; 15 | private onMessageCallbacks = []; 16 | private onErrorCallbacks = []; 17 | private onCloseCallbacks = []; 18 | private readyStateConstants = { 19 | 'CONNECTING': 0, 20 | 'OPEN': 1, 21 | 'CLOSING': 2, 22 | 'CLOSED': 3, 23 | 'RECONNECT_ABORTED': 4 24 | }; 25 | private normalCloseCode = 1000; 26 | private reconnectableStatusCodes = [4000]; 27 | private socket: WebSocket; 28 | private dataStream: Subject; 29 | private internalConnectionState: number; 30 | constructor(private url:string, private protocols?:Array, private config?: WebSocketConfig ) { 31 | var match = new RegExp('wss?:\/\/').test(url); 32 | if (!match) { 33 | throw new Error('Invalid url provided'); 34 | } 35 | this.config = config ||{ initialTimeout: 500, maxTimeout : 300000, reconnectIfNotNormalClose :false}; 36 | this.dataStream = new Subject(); 37 | } 38 | 39 | connect(force:boolean = false) { 40 | var self = this; 41 | if (force || !this.socket || this.socket.readyState !== this.readyStateConstants.OPEN) { 42 | self.socket =this.protocols ? new WebSocket(this.url, this.protocols) : new WebSocket(this.url); 43 | 44 | self.socket.onopen =(ev: Event) => { 45 | // console.log('onOpen: %s', ev); 46 | this.onOpenHandler(ev); 47 | }; 48 | self.socket.onmessage = (ev: MessageEvent) => { 49 | // console.log('onNext: %s', ev.data); 50 | self.onMessageHandler(ev); 51 | this.dataStream.next(ev); 52 | }; 53 | this.socket.onclose = (ev: CloseEvent) => { 54 | // console.log('onClose, completed'); 55 | self.onCloseHandler(ev); 56 | }; 57 | 58 | this.socket.onerror = (ev: ErrorEvent) => { 59 | // console.log('onError', ev); 60 | self.onErrorHandler(ev); 61 | this.dataStream.error(ev); 62 | }; 63 | 64 | } 65 | } 66 | send(data) { 67 | var self = this; 68 | if (this.getReadyState() != this.readyStateConstants.OPEN &&this.getReadyState() != this.readyStateConstants.CONNECTING ){ 69 | this.connect(); 70 | } 71 | return Observable.create((observer) => { 72 | if (self.socket.readyState === self.readyStateConstants.RECONNECT_ABORTED) { 73 | observer.next('Socket connection has been closed'); 74 | } 75 | else { 76 | self.sendQueue.push({message: data}); 77 | self.fireQueue(); 78 | } 79 | 80 | }); 81 | }; 82 | 83 | getDataStream():Subject{ 84 | return this.dataStream; 85 | } 86 | 87 | onOpenHandler(event: Event) { 88 | this.reconnectAttempts = 0; 89 | this.notifyOpenCallbacks(event); 90 | this.fireQueue(); 91 | }; 92 | notifyOpenCallbacks(event) { 93 | for (let i = 0; i < this.onOpenCallbacks.length; i++) { 94 | this.onOpenCallbacks[i].call(this, event); 95 | } 96 | } 97 | fireQueue() { 98 | while (this.sendQueue.length && this.socket.readyState === this.readyStateConstants.OPEN) { 99 | var data = this.sendQueue.shift(); 100 | 101 | this.socket.send( 102 | isString(data.message) ? data.message : JSON.stringify(data.message) 103 | ); 104 | // data.deferred.resolve(); 105 | } 106 | } 107 | 108 | notifyCloseCallbacks(event) { 109 | for (let i = 0; i < this.onCloseCallbacks.length; i++) { 110 | this.onCloseCallbacks[i].call(this, event); 111 | } 112 | } 113 | 114 | notifyErrorCallbacks(event) { 115 | for (var i = 0; i < this.onErrorCallbacks.length; i++) { 116 | this.onErrorCallbacks[i].call(this, event); 117 | } 118 | } 119 | 120 | onOpen(cb) { 121 | this.onOpenCallbacks.push(cb); 122 | return this; 123 | }; 124 | 125 | onClose(cb) { 126 | this.onCloseCallbacks.push(cb); 127 | return this; 128 | } 129 | 130 | onError(cb) { 131 | this.onErrorCallbacks.push(cb); 132 | return this; 133 | }; 134 | 135 | 136 | onMessage(callback, options) { 137 | if (!isFunction(callback)) { 138 | throw new Error('Callback must be a function'); 139 | } 140 | 141 | this.onMessageCallbacks.push({ 142 | fn: callback, 143 | pattern: options ? options.filter : undefined, 144 | autoApply: options ? options.autoApply : true 145 | }); 146 | return this; 147 | } 148 | 149 | onMessageHandler(message: MessageEvent) { 150 | var pattern; 151 | var self = this; 152 | var currentCallback; 153 | for (var i = 0; i < self.onMessageCallbacks.length; i++) { 154 | currentCallback = self.onMessageCallbacks[i]; 155 | currentCallback.fn.apply(self, [message]); 156 | } 157 | 158 | }; 159 | onCloseHandler(event: CloseEvent) { 160 | this.notifyCloseCallbacks(event); 161 | if ((this.config.reconnectIfNotNormalClose && event.code !== this.normalCloseCode) || this.reconnectableStatusCodes.indexOf(event.code) > -1) { 162 | this.reconnect(); 163 | } else { 164 | this.dataStream.complete(); 165 | } 166 | }; 167 | 168 | onErrorHandler(event) { 169 | this.notifyErrorCallbacks(event); 170 | }; 171 | 172 | 173 | 174 | 175 | 176 | reconnect() { 177 | this.close(true); 178 | var backoffDelay = this.getBackoffDelay(++this.reconnectAttempts); 179 | var backoffDelaySeconds = backoffDelay / 1000; 180 | // console.log('Reconnecting in ' + backoffDelaySeconds + ' seconds'); 181 | setTimeout( this.connect(), backoffDelay); 182 | return this; 183 | } 184 | 185 | close(force: boolean) { 186 | if (force || !this.socket.bufferedAmount) { 187 | this.socket.close(); 188 | } 189 | return this; 190 | }; 191 | // Exponential Backoff Formula by Prof. Douglas Thain 192 | // http://dthain.blogspot.co.uk/2009/02/exponential-backoff-in-distributed.html 193 | getBackoffDelay(attempt) { 194 | var R = Math.random() + 1; 195 | var T = this.config.initialTimeout; 196 | var F = 2; 197 | var N = attempt; 198 | var M = this.config.maxTimeout; 199 | 200 | return Math.floor(Math.min(R * T * Math.pow(F, N), M)); 201 | }; 202 | 203 | setInternalState(state) { 204 | if (Math.floor(state) !== state || state < 0 || state > 4) { 205 | throw new Error('state must be an integer between 0 and 4, got: ' + state); 206 | } 207 | 208 | this.internalConnectionState = state; 209 | 210 | } 211 | 212 | /** 213 | * Could be -1 if not initzialized yet 214 | * @returns {number} 215 | */ 216 | getReadyState() { 217 | if (this.socket == null) 218 | { 219 | return -1; 220 | } 221 | return this.internalConnectionState || this.socket.readyState; 222 | } 223 | 224 | 225 | 226 | } 227 | 228 | export interface WebSocketConfig { 229 | initialTimeout:number; 230 | maxTimeout:number ; 231 | reconnectIfNotNormalClose: boolean 232 | } 233 | -------------------------------------------------------------------------------- /src/main/resources/static/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import {$WebSocket} from './angular2-websocket'; 3 | 4 | @Component({ 5 | selector: 'my-app', 6 | template: ` 7 | Counter Value is: {{counter}} 8 | 9 | ` 10 | }) 11 | export class AppComponent { 12 | 13 | counter: string = 'not known'; 14 | ws: $Websocket; 15 | constructor() { 16 | this.ws = new $WebSocket("ws://localhost:8088/counter"); 17 | } 18 | 19 | subscribe($event) { 20 | console.log("trying to subscribe to ws"); 21 | this.ws = new $WebSocket("ws://localhost:8088/counter"); 22 | this.ws.send("Hello"); 23 | this.ws.getDataStream().subscribe( 24 | res => { 25 | var count = JSON.parse(res.data).value; 26 | console.log('Got: ' + count); 27 | this.counter = count; 28 | }, 29 | function(e) { console.log('Error: ' + e.message); }, 30 | function() { console.log('Completed'); } 31 | ); 32 | } 33 | } 34 | 35 | /* 36 | Copyright 2016 Google Inc. All Rights Reserved. 37 | Use of this source code is governed by an MIT-style license that 38 | can be found in the LICENSE file at http://angular.io/license 39 | */ 40 | -------------------------------------------------------------------------------- /src/main/resources/static/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppComponent } from './app.component'; 5 | 6 | @NgModule({ 7 | imports: [ BrowserModule ], 8 | declarations: [ AppComponent ], 9 | bootstrap: [ AppComponent ] 10 | }) 11 | export class AppModule { } 12 | 13 | 14 | /* 15 | Copyright 2016 Google Inc. All Rights Reserved. 16 | Use of this source code is governed by an MIT-style license that 17 | can be found in the LICENSE file at http://angular.io/license 18 | */ -------------------------------------------------------------------------------- /src/main/resources/static/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | platformBrowserDynamic().bootstrapModule(AppModule); 6 | 7 | 8 | /* 9 | Copyright 2016 Google Inc. All Rights Reserved. 10 | Use of this source code is governed by an MIT-style license that 11 | can be found in the LICENSE file at http://angular.io/license 12 | */ -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | angular playground 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | loading... 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/static/style.css: -------------------------------------------------------------------------------- 1 | /* Master Styles */ 2 | h1 { 3 | color: #369; 4 | font-family: Arial, Helvetica, sans-serif; 5 | font-size: 250%; 6 | } 7 | h2, h3 { 8 | color: #444; 9 | font-family: Arial, Helvetica, sans-serif; 10 | font-weight: lighter; 11 | } 12 | body { 13 | margin: 2em; 14 | } 15 | body, input[text], button { 16 | color: #888; 17 | font-family: Cambria, Georgia; 18 | } 19 | a { 20 | cursor: pointer; 21 | cursor: hand; 22 | } 23 | button { 24 | font-family: Arial; 25 | background-color: #eee; 26 | border: none; 27 | padding: 5px 10px; 28 | border-radius: 4px; 29 | cursor: pointer; 30 | cursor: hand; 31 | } 32 | button:hover { 33 | background-color: #cfd8dc; 34 | } 35 | button:disabled { 36 | background-color: #eee; 37 | color: #aaa; 38 | cursor: auto; 39 | } 40 | 41 | /* Navigation link styles */ 42 | nav a { 43 | padding: 5px 10px; 44 | text-decoration: none; 45 | margin-top: 10px; 46 | display: inline-block; 47 | background-color: #eee; 48 | border-radius: 4px; 49 | } 50 | nav a:visited, a:link { 51 | color: #607D8B; 52 | } 53 | nav a:hover { 54 | color: #039be5; 55 | background-color: #CFD8DC; 56 | } 57 | nav a.active { 58 | color: #039be5; 59 | } 60 | 61 | /* items class */ 62 | .items { 63 | margin: 0 0 2em 0; 64 | list-style-type: none; 65 | padding: 0; 66 | width: 24em; 67 | } 68 | .items li { 69 | cursor: pointer; 70 | position: relative; 71 | left: 0; 72 | background-color: #EEE; 73 | margin: .5em; 74 | padding: .3em 0; 75 | height: 1.6em; 76 | border-radius: 4px; 77 | } 78 | .items li:hover { 79 | color: #607D8B; 80 | background-color: #DDD; 81 | left: .1em; 82 | } 83 | .items li.selected:hover { 84 | background-color: #BBD8DC; 85 | color: white; 86 | } 87 | .items .text { 88 | position: relative; 89 | top: -3px; 90 | } 91 | .items { 92 | margin: 0 0 2em 0; 93 | list-style-type: none; 94 | padding: 0; 95 | width: 24em; 96 | } 97 | .items li { 98 | cursor: pointer; 99 | position: relative; 100 | left: 0; 101 | background-color: #EEE; 102 | margin: .5em; 103 | padding: .3em 0; 104 | height: 1.6em; 105 | border-radius: 4px; 106 | } 107 | .items li:hover { 108 | color: #607D8B; 109 | background-color: #DDD; 110 | left: .1em; 111 | } 112 | .items li.selected { 113 | background-color: #CFD8DC; 114 | color: white; 115 | } 116 | 117 | .items li.selected:hover { 118 | background-color: #BBD8DC; 119 | } 120 | .items .text { 121 | position: relative; 122 | top: -3px; 123 | } 124 | .items .badge { 125 | display: inline-block; 126 | font-size: small; 127 | color: white; 128 | padding: 0.8em 0.7em 0 0.7em; 129 | background-color: #607D8B; 130 | line-height: 1em; 131 | position: relative; 132 | left: -1px; 133 | top: -4px; 134 | height: 1.8em; 135 | margin-right: .8em; 136 | border-radius: 4px 0 0 4px; 137 | } 138 | /* everywhere else */ 139 | * { 140 | font-family: Arial, Helvetica, sans-serif; 141 | } 142 | 143 | 144 | /* 145 | Copyright 2016 Google Inc. All Rights Reserved. 146 | Use of this source code is governed by an MIT-style license that 147 | can be found in the LICENSE file at http://angular.io/license 148 | */ -------------------------------------------------------------------------------- /src/main/resources/static/systemjs.config.js: -------------------------------------------------------------------------------- 1 | var angularVersion; 2 | if(window.AngularVersionForThisPlunker === 'latest'){ 3 | angularVersion = ''; //picks up latest 4 | } 5 | else { 6 | angularVersion = '@' + window.AngularVersionForThisPlunker; 7 | } 8 | 9 | System.config({ 10 | //use typescript for compilation 11 | transpiler: 'typescript', 12 | //typescript compiler options 13 | typescriptOptions: { 14 | emitDecoratorMetadata: true 15 | }, 16 | paths: { 17 | 'npm:': 'https://unpkg.com/' 18 | }, 19 | //map tells the System loader where to look for things 20 | map: { 21 | 'app': './app', 22 | '@angular/core': 'npm:@angular/core'+ angularVersion + '/bundles/core.umd.js', 23 | '@angular/common': 'npm:@angular/common' + angularVersion + '/bundles/common.umd.js', 24 | '@angular/compiler': 'npm:@angular/compiler' + angularVersion + '/bundles/compiler.umd.js', 25 | '@angular/platform-browser': 'npm:@angular/platform-browser' + angularVersion + '/bundles/platform-browser.umd.js', 26 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic' + angularVersion + '/bundles/platform-browser-dynamic.umd.js', 27 | '@angular/http': 'npm:@angular/http' + angularVersion + '/bundles/http.umd.js', 28 | '@angular/router': 'npm:@angular/router' + angularVersion +'/bundles/router.umd.js', 29 | '@angular/forms': 'npm:@angular/forms' + angularVersion + '/bundles/forms.umd.js', 30 | '@angular/animations': 'npm:@angular/animations' + angularVersion + '/bundles/animations.umd.js', 31 | '@angular/platform-browser/animations': 'npm:@angular/platform-browser' + angularVersion + '/bundles/platform-browser-animations.umd.js', 32 | '@angular/animations/browser': 'npm:@angular/animations' + angularVersion + '/bundles/animations-browser.umd.js', 33 | 34 | 'tslib': 'npm:tslib@1.6.1', 35 | 'rxjs': 'npm:rxjs', 36 | 'typescript': 'npm:typescript@2.2.1/lib/typescript.js' 37 | }, 38 | //packages defines our app package 39 | packages: { 40 | app: { 41 | main: './main.ts', 42 | defaultExtension: 'ts' 43 | }, 44 | rxjs: { 45 | defaultExtension: 'js' 46 | } 47 | } 48 | }); -------------------------------------------------------------------------------- /src/main/resources/static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": true, 11 | "suppressImplicitAnyIndexErrors": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------