Spring boot+ Angular + Websockets

Spring boot+ Angular + Websockets

How to setup and initiate a socket communication using angular and spring boot by creating a simple group chat app!

My Project setup =()=>

  1. JDK 11+
  2. Spring Boot
  3. Intellij idea
  4. Maven
  5. Angular 13

BACK END

Spring boot depencies = () =>

The following depencies need to add in your "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.6.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.chit.chat</groupId>
    <artifactId>chat</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>touch and feel web sockets in springboot</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>5.3.19</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
            <version>5.3.19</version>
        </dependency>
    </dependencies>

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

</project>

Create a websocket configuration class =()=>

Create a Config Package/Folder and create a new java class called "SocketConfiguration.java"

package com.chit.chat.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

import java.util.Collections;

@Configuration
@EnableWebSocketMessageBroker
public class SocketConfiguration implements WebSocketMessageBrokerConfigurer {


    @Override
    public void registerStompEndpoints ( StompEndpointRegistry registry ) {
        registry.addEndpoint ( "/socket" ).setAllowedOrigins( String.valueOf ( Collections.singletonList("*") ) ).withSockJS ();
    }

    @Override
    public void configureMessageBroker ( org.springframework.messaging.simp.config.MessageBrokerRegistry registry ) {
        registry.setApplicationDestinationPrefixes ( "/app" );
        registry.enableSimpleBroker ( "/topic" );

    }
}

Configuration : This annotation indicates that , this particular class is a spring configuration class. EnableWebSocketMessageBroker : This annotaion is used to enable websocket message server.

registerStompEndpoints() function will help to define our socket endpoint where client will be able to connect with our websocket. We are using STOMP as message handling protocol.

configureMessageBroker() function will help to route the message between users

Create a POJO class =()=>

Create a Model Class "ChitChatPojoModel" to define our necessary variables

package com.chit.chat.models;



public class ChitChatPojoModel {

    private MessageType type;
    private String content;
    private String user;

    public enum MessageType{
        JOIN,
        CHAT,
        LEAVE
    }
    public MessageType getType ( ) {
        return type;
    }

    public void setType ( MessageType type ) {
        this.type = type;
    }

    public String getContent ( ) {
        return content;
    }

    public void setContent ( String content ) {
        this.content = content;
    }

    public String getUser ( ) {
        return user;
    }

    public void setUser ( String user ) {
        this.user = user;
    }
}

Create a controller Class=()=>

package com.chit.chat.controllers;

import com.chit.chat.listenerEvents.SocketEventListener;
import com.chit.chat.models.ChitChatPojoModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

@Controller
public class ChitChatController {

    private static final Logger logger = LoggerFactory.getLogger ( SocketEventListener.class );

    @MessageMapping("sendMessage")
    @SendTo("/topic/group")
    public ChitChatPojoModel sendMessage( @Payload ChitChatPojoModel message,SimpMessageHeaderAccessor headerAccessor ){
        String username = (String) headerAccessor.getSessionAttributes ().get("username");
        if(username != null) {
            logger.info ( "User" + username );
            ChitChatPojoModel newMessage = new ChitChatPojoModel ();
            newMessage.setUser ( username );
            newMessage.setType ( ChitChatPojoModel.MessageType.CHAT );
            newMessage.setContent (message.getContent ()  );
            return newMessage;
        }else{
            return null;
        }
    }


    @MessageMapping("addUser")
    @SendTo("/topic/group")
    public ChitChatPojoModel addUser( @Payload ChitChatPojoModel message, SimpMessageHeaderAccessor headerAccessor ){
        // adding username in socket session
        headerAccessor.getSessionAttributes ().put ( "username" ,message.getUser ());
        return message;
    }

}

We are handling two kind of interactions here, one is adding a new user and other is transfering messages between clients , these be will handled by the functions addUser() and sendMessage() respectively.

The annotation "MessageMapping" will help to route between sendMessage() and addUser() functions. If we want to add a new user from the client side, we have to send that information via /app/addUser . The path /app/sendMessage is using to send messages between clients.

Create EventListeners to track the connection and disconnection events = ()=>

package com.chit.chat.listenerEvents;

import com.chit.chat.models.ChitChatPojoModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;


@Component
public class SocketEventListener {
    private static final Logger logger = LoggerFactory.getLogger ( SocketEventListener.class );

    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    @EventListener
    public void handleSocketConnectListener ( SessionConnectedEvent event ) {
        logger.info ( "A new connection request has arrived" );
    }

    @EventListener
    public void handleSocketDisconnectListener ( SessionDisconnectEvent event ) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap ( event.getMessage () );
        String username = (String) headerAccessor.getSessionAttributes ().get("username");
        if(username != null){
            logger.info ( "User"+username+"Disconnected" );
            ChitChatPojoModel message = new ChitChatPojoModel ();
            message.setType ( ChitChatPojoModel.MessageType.LEAVE );
            message.setUser ( username );
            messagingTemplate.convertAndSend ( "/topic/group",message );
        }
    }
}

FRONT END

create an angular project using "angular-cli" with following dependencies

package.json =()=>

{
  "name": "chit-chat",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve --proxy-config proxy.conf.json",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~13.3.0",
    "@angular/common": "~13.3.0",
    "@angular/compiler": "~13.3.0",
    "@angular/core": "~13.3.0",
    "@angular/forms": "~13.3.0",
    "@angular/localize": "~13.3.0",
    "@angular/platform-browser": "~13.3.0",
    "@angular/platform-browser-dynamic": "~13.3.0",
    "@angular/router": "~13.3.0",
    "@fortawesome/angular-fontawesome": "^0.10.2",
    "@fortawesome/fontawesome-svg-core": "^6.1.1",
    "@fortawesome/free-solid-svg-icons": "^6.1.1",
    "@ng-bootstrap/ng-bootstrap": "^12.0.2",
    "@popperjs/core": "^2.10.2",
    "@stomp/stompjs": "^5.4.2",
    "bootstrap": "^5.1.3",
    "cors": "^2.8.5",
    "express-http-proxy": "^1.6.3",
    "port": "^0.8.1",
    "rxjs": "~7.5.0",
    "sockjs-client": "^1.6.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.11.4"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~13.3.0",
    "@angular/cli": "~13.3.0",
    "@angular/compiler-cli": "~13.3.0",
    "@types/auth0-js": "^9.14.6",
    "@types/jasmine": "~3.10.0",
    "@types/node": "^12.11.1",
    "@types/sockjs-client": "^1.5.1",
    "jasmine-core": "~4.0.0",
    "karma": "~6.3.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.1.0",
    "karma-jasmine": "~4.0.0",
    "karma-jasmine-html-reporter": "~1.7.0",
    "typescript": "~4.6.2"
  }
}

app.module.ts =()=>

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ChatUIModule } from './components/chat-ui/chat-ui.module';
import { EnterChatRoomModule } from './components/enter-chat-room/enter-chat-room.module';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, EnterChatRoomModule, ChatUIModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

app-routing.module.ts =()=>

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

//lazy loading setup
const routes: Routes = [
  {
    path: 'user',
    loadChildren: () =>
      import('./components/enter-chat-room/enter-chat-room.module').then(
        (m) => m.EnterChatRoomModule
      ),
  },
  {
    path: 'chat-ui',
    loadChildren: () =>
      import('./components/chat-ui/chat-ui.module').then((m) => m.ChatUIModule),
  },
  { path: '', redirectTo: 'user', pathMatch: 'full' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

app.component.ts =()=>

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'chitChat';
}

app.component.html =()=>

<router-outlet> </router-outlet>

create two components named "EnterChatRoomComponent" & "ChatUIComponent" which it's own modules which will load lazily.

enter-chat-room.module.ts =()=>

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { EnterChatRoomRoutingModule } from './enter-chat-room-routing.module';
import { EnterChatRoomComponent } from './enter-chat-room.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [EnterChatRoomComponent],
  imports: [
    CommonModule,
    EnterChatRoomRoutingModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  exports: [EnterChatRoomComponent],
})
export class EnterChatRoomModule {}

enter-chat-room-routing.module.ts =()=>

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { EnterChatRoomComponent } from './enter-chat-room.component';

const routes: Routes = [{ path: '', component: EnterChatRoomComponent }];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class EnterChatRoomRoutingModule {}

enter-chat-room.component.ts =()=>

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MessagingService } from 'src/app/services/messaging.service';

@Component({
  selector: 'app-enter-chat-room',
  templateUrl: './enter-chat-room.component.html',
  styleUrls: ['./enter-chat-room.component.scss'],
})
export class EnterChatRoomComponent implements OnInit {
  constructor(
    private formBuilder: FormBuilder,
    private message: MessagingService
  ) {}
  enterChatRoom!: FormGroup;
  ngOnInit(): void {
    this.enterChatRoom = this.formBuilder.group({
      username: ['', [Validators.required, Validators.minLength(4)]],
    });
  }
  onSubmit() {
    if (this.enterChatRoom.valid) {
      this.message.connect(this.enterChatRoom.value.username);
      this.enterChatRoom.reset();
    }
  }
}

enter-chat-room.component.html =()=>

<div
  class="container-fluid wrapper d-flex justify-content-center align-items-center"
>
  <div class="card user-section-flex d-flex justify-content-around bg-dark">
    <form [formGroup]="enterChatRoom" (ngSubmit)="onSubmit()">
      <section class="d-flex justify-content-around align-items-center">
        <div
          class="user-flex d-flex justify-content-between align-items-center"
        >
          <div>
            <label
              for="user"
              id="__username__label"
              class="form-label text-white"
              >Enter Username</label
            >
          </div>

          <div>
            <input
              type="text"
              class="form-control"
              name="username"
              id="__username"
              placeholder="Enter Username"
              formControlName="username"
            />
          </div>
        </div>
        <div>
          <button
            [disabled]="!enterChatRoom.valid"
            type="submit"
            class="btn btn-primary"
          >
            Let's Chat
          </button>
        </div>
      </section>
    </form>
  </div>
</div>

chat-ui.module.ts =()=>


import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ChatUIRoutingModule } from './chat-ui-routing.module';
import { ChatUIComponent } from './chat-ui.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [ChatUIComponent],
  imports: [
    CommonModule,
    ChatUIRoutingModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  exports: [ChatUIComponent],
})
export class ChatUIModule {}

chat-ui-routing.module.ts =()=>

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ChatUIComponent } from './chat-ui.component';

const routes: Routes = [{ path: '', component: ChatUIComponent }];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class ChatUIRoutingModule {}

chat-ui.component.ts =()=>

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MessagingService } from 'src/app/services/messaging.service';

@Component({
  selector: 'app-chat-ui',
  templateUrl: './chat-ui.component.html',
  styleUrls: ['./chat-ui.component.scss'],
})
export class ChatUIComponent implements OnInit {
  readonly typedText = this.message.message;
  constructor(
    private formBuilder: FormBuilder,
    private readonly message: MessagingService
  ) {}
  chatUI!: FormGroup;
  ngOnInit(): void {
    this.chatUI = this.formBuilder.group({
      message: ['', [Validators.required]],
    });
  }

  onSubmit() {
    if (this.chatUI.valid) {
      this.message.sendMessage(this.chatUI.value.message);
      this.chatUI.reset();
    }
  }
}

chat-ui.component.html =()=>

<div
  class="container-fluid container__wrapper d-flex justify-content-center align-items-center"
>
  <form
    [formGroup]="chatUI"
    class="form-element d-flex justify-content-center align-items-center"
    (ngSubmit)="onSubmit()"
  >
    <div class="card-wrapper d-flex justify-content-center">
      <div class="card bg-dark text-white flex-column card-custom shadow">
        <div class="card-header"><h1>CHIT CHAT</h1></div>
        <div class="card-body d-flex flex-column justify-content-between">
          <div class="message-area bg-light text-dark">
            <ng-container *ngFor="let item of typedText | async">
              <ng-container></ng-container>
              <ng-container></ng-container>
              <ng-container *ngIf="item.type === 'JOIN'">
                <div>
                  <span>
                    <strong>{{ item.user }}:</strong>
                  </span>
                  <span style="color: green"><strong>Joined</strong> </span>
                </div></ng-container
              >
              <ng-container *ngIf="item.type === 'LEAVE'">
                <div>
                  <span>
                    <span style="color: red"><strong>left</strong> </span>
                  </span>
                  <span>LEFT </span>
                </div></ng-container
              >
              <ng-container *ngIf="item.type === 'CHAT'">
                <div>
                  <span>
                    <strong>{{ item.user }}:</strong>
                  </span>
                  <span style="color: blue">{{ item.content }} </span>
                </div></ng-container
              >
            </ng-container>
          </div>
          <div class="typing-area-container">
            <div class="d-flex typing-area bg-dark">
              <div class="input-box">
                <input
                  type="text"
                  class="form-control"
                  name="message"
                  id="__message"
                  placeholder="Type your message here"
                  formControlName="message"
                />
              </div>
              <div>
                <button [disabled]="!chatUI.valid" class="btn btn-secondary">
                  Send
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </form>
</div>

Create "MessagingService" and "MessagingStateService", to communicate between client and websockets

messaging-state.service.ts =()=>

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { CompatClient, Stomp } from '@stomp/stompjs';
import * as SockJS from 'sockjs-client';
import { MessagingStateService } from './messaging-state.service';

@Injectable({
  providedIn: 'root',
})
export class MessagingService extends MessagingStateService {
  constructor(private router: Router) {
    super();
  }
  private stompClient!: CompatClient;
  connect(username: string) {
    const _this = this;
    const socket = new SockJS('/socket-client/socket');
    this.stompClient = Stomp.over(socket);
    this.stompClient.connect(
      {},
      () => {
        _this.stompClient.subscribe('/topic/group', (data: any) => {
          this.handleMessage(JSON.parse(data.body));
          this.router.navigateByUrl('/chat-ui');
        });
        _this.stompClient.send(
          '/app/addUser',
          {},
          JSON.stringify({ user: username, type: 'JOIN' })
        );
      },
      this.onError
    );
  }

  onError = (err: any) => {
    console.log('Error: ' + err);
  };

  sendMessage(message: string) {
    this.stompClient.send(
      '/app/sendMessage',
      {},
      JSON.stringify({ user: '', type: 'CHAT', content: message })
    );
  }
  handleMessage(message: any) {
    switch (message.type) {
      case 'JOIN':
        this.router.navigateByUrl('/chat-ui');
        this.setMessage(message);
        break;
      case 'LEAVE':
        this.setMessage(message);
        break;
      default:
        this.setMessage(message);
        break;
    }
  }
}

messaging.service.ts =()=>

This class will take care of the hand shake between client and websocket. Also this will maintain the stable connection.


import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { CompatClient, Stomp } from '@stomp/stompjs';
import * as SockJS from 'sockjs-client';
import { MessagingStateService } from './messaging-state.service';

@Injectable({
  providedIn: 'root',
})
export class MessagingService extends MessagingStateService {
  constructor(private router: Router) {
    super();
  }
  private stompClient!: CompatClient;
  connect(username: string) {
    const _this = this;
    const socket = new SockJS('/socket-client/socket');
    this.stompClient = Stomp.over(socket);
    this.stompClient.connect(
      {},
      () => {
        _this.stompClient.subscribe('/topic/group', (data: any) => {
          this.handleMessage(JSON.parse(data.body));
          this.router.navigateByUrl('/chat-ui');
        });
        _this.stompClient.send(
          '/app/addUser',
          {},
          JSON.stringify({ user: username, type: 'JOIN' })
        );
      },
      this.onError
    );
  }

  onError = (err: any) => {
    console.log('Error: ' + err);
  };

  sendMessage(message: string) {
    this.stompClient.send(
      '/app/sendMessage',
      {},
      JSON.stringify({ user: '', type: 'CHAT', content: message })
    );
  }
  handleMessage(message: any) {
    switch (message.type) {
      case 'JOIN':
        this.router.navigateByUrl('/chat-ui');
        this.setMessage(message);
        break;
      case 'LEAVE':
        this.setMessage(message);
        break;
      default:
        this.setMessage(message);
        break;
    }
  }
}

Please find the screenshots for reference =()=>

Screenshot (1).png

Screenshot (2).png

Screenshot (3).png

Screenshot (4).png

Screenshot (5).png

Screenshot (6).png

Screenshot (7).png

Screenshot (8).png

FROND END:https://github.com/Jishnuvas/chit-chat-angular-front-end

BACK END : https://github.com/Jishnuvas/chit-chat-spring-boot-back-end