├── .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 |
--------------------------------------------------------------------------------