Spring Webflux & Angular

Overview

Spring Reactive web application with angular clarity and & reactive mongo db. Creates uber jar to deploy.

Github: https://github.com/gitorko/project60

Quick Overview

To deploy the application in a single command, clone the project, make sure no conflicting docker containers or ports are running and then run

1git clone https://github.com/gitorko/project60
2cd project60
3docker-compose -f docker/docker-compose.yml up 

Open http://localhost:8080/

Features

Clarity is an open source library that provides various Angular components.

Code

 1package com.demo.project60;
 2
 3import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
 4import static org.springframework.web.reactive.function.server.RouterFunctions.route;
 5import static org.springframework.web.reactive.function.server.ServerResponse.ok;
 6
 7import java.util.Arrays;
 8import java.util.List;
 9import java.util.Random;
10
11import com.demo.project60.domain.Customer;
12import com.demo.project60.repository.CustomerRepository;
13import lombok.extern.slf4j.Slf4j;
14import org.springframework.beans.factory.annotation.Value;
15import org.springframework.boot.CommandLineRunner;
16import org.springframework.boot.SpringApplication;
17import org.springframework.boot.autoconfigure.SpringBootApplication;
18import org.springframework.context.annotation.Bean;
19import org.springframework.core.io.Resource;
20import org.springframework.http.MediaType;
21import org.springframework.web.reactive.function.server.RouterFunction;
22import org.springframework.web.reactive.function.server.ServerResponse;
23import reactor.core.publisher.Flux;
24
25@SpringBootApplication
26@Slf4j
27public class Main {
28    public static void main(String[] args) {
29        SpringApplication.run(Main.class, args);
30    }
31
32    @Bean
33    public CommandLineRunner seedData(CustomerRepository customerRepository) {
34        return args -> {
35            log.info("Initializing repo!");
36            List<String> city = Arrays.asList("London", "New York", "Bangalore");
37            Flux<Customer> customers = Flux.range(1, 5).map(i -> {
38                int randomIndex = new Random().nextInt(2 - 0 + 1) + 0;
39                return new Customer(null, "first_" + i, "last_" + i, city.get(randomIndex));
40            });
41            customerRepository.deleteAll()
42                    .thenMany(customers.flatMap(customerRepository::save)
43                            .thenMany(customerRepository.findAll()))
44                    .subscribe(e -> log.info(e.toString()));
45            log.info("Data seed completed!");
46        };
47    }
48}
1package com.demo.project60.repository;
2
3import com.demo.project60.domain.Customer;
4import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
5
6public interface CustomerRepository extends ReactiveMongoRepository<Customer, String> {
7}
 1spring:
 2  main:
 3    banner-mode: "off"
 4  data:
 5    mongodb:
 6      database: test-db
 7      username: test
 8      password: test@123
 9      host: localhost
10      port: 27017
11      authentication-database: admin
 1import {Injectable} from '@angular/core';
 2import {HttpClient} from '@angular/common/http';
 3import {Observable} from 'rxjs';
 4import {Customer} from "../models/customer";
 5
 6@Injectable({
 7  providedIn: 'root'
 8})
 9export class RestService {
10
11  constructor(private http: HttpClient) {
12  }
13
14  public getCustomers(): Observable<Customer[]> {
15    return this.http.get<Customer[]>('/api/customer');
16  }
17
18  public saveCustomer(customer: Customer) {
19    return this.http.post('/api/customer', customer);
20  }
21
22  public deleteCustomer(id: any): Observable<any> {
23    return this.http.delete('/api/customer/' + id);
24  }
25
26  public getTime(): Observable<string> {
27    return this.http.get<string>('/api/time');
28  }
29}
 1<div class="content-container">
 2  <div class="content-area">
 3
 4    <div class="clr-row">
 5
 6      <div class="clr-col-12">
 7        <p style="text-align: center">
 8          <!-- interpolation & pipe -->
 9          Server Time: {{currentTime | date:'dd-MM-yyyy' }}
10        </p>
11
12        <h2 style="text-align: center">Customers</h2>
13
14        <clr-datagrid>
15          <clr-dg-placeholder class="content-center">No Customers!</clr-dg-placeholder>
16          <clr-dg-column [clrDgField]="'id'">ID</clr-dg-column>
17          <clr-dg-column [clrDgField]="'firstName'">First Name</clr-dg-column>
18          <clr-dg-column [clrDgField]="'lastName'">Last Name</clr-dg-column>
19          <clr-dg-column [clrDgField]="'city'">City</clr-dg-column>
20          <clr-dg-column>Action</clr-dg-column>
21          <!-- structural directive -->
22          <clr-dg-row clr-dg-row *clrDgItems="let customer of customers">
23            <clr-dg-cell>{{customer.id}}</clr-dg-cell>
24            <clr-dg-cell>{{customer.firstName}}</clr-dg-cell>
25            <clr-dg-cell>{{customer.lastName}}</clr-dg-cell>
26            <clr-dg-cell>{{customer.city}}</clr-dg-cell>
27            <clr-dg-cell>
28              <cds-icon shape="trash" style="cursor: pointer; color: blue" (click)="deleteCustomer(customer)">
29              </cds-icon>
30            </clr-dg-cell>
31          </clr-dg-row>
32          <clr-dg-footer>{{customers.length}} customers</clr-dg-footer>
33        </clr-datagrid>
34
35        <div class="clr-col-12">
36          <form class="clr-form clr-form-horizontal">
37            <div class="clr-form-control">
38              <label for="firstName" class="clr-control-label">First Name</label>
39              <div class="clr-control-container">
40                <div class="clr-input-wrapper">
41                  <!-- two way data binding -->
42                  <input type="text" [(ngModel)]="customer.firstName" id="firstName" name="firstName"
43                         placeholder="Placeholder" class="clr-input"/>
44                </div>
45              </div>
46            </div>
47            <div class="clr-form-control">
48              <label for="lastName" class="clr-control-label">Last Name</label>
49              <div class="clr-control-container">
50                <div class="clr-input-wrapper">
51                  <input [(ngModel)]="customer.lastName" type="text" id="lastName" name="lastName"
52                         placeholder="Placeholder" class="clr-input"/>
53                </div>
54              </div>
55            </div>
56            <div class="clr-form-control">
57              <div class="clr-control-container">
58                <!-- event binding -->
59                <button type="submit" class="btn btn-primary" (click)="saveCustomer()">Save</button>
60              </div>
61            </div>
62          </form>
63        </div>
64
65      </div>
66    </div>
67  </div>
68</div>
 1import {Component, OnInit} from '@angular/core';
 2import {Customer} from "../models/customer";
 3import {RestService} from "../services/rest.service";
 4import {ClarityIcons, trashIcon} from "@cds/core/icon";
 5
 6@Component({
 7  selector: 'app-home',
 8  templateUrl: './home.component.html',
 9  styleUrls: ['./home.component.css']
10})
11export class HomeComponent implements OnInit {
12
13  customers: Customer[] = [];
14  customer: Customer = new Customer();
15  currentTime = '';
16
17  constructor(private restService: RestService) {
18    ClarityIcons.addIcons(trashIcon);
19  }
20
21  ngOnInit() {
22    this.getCustomers();
23  }
24
25  getCustomers(): void {
26    this.customer = new Customer();
27    this.restService.getCustomers().subscribe(data => {
28      this.customers = data;
29    });
30    this.restService.getTime().subscribe(data => {
31      this.currentTime = data;
32    });
33  }
34
35  saveCustomer(): void {
36    this.restService.saveCustomer(this.customer)
37      .subscribe(data => {
38        this.getCustomers();
39      }, error => {
40        console.log(error);
41      });
42  }
43
44  deleteCustomer(customer: Customer): void {
45    console.log('delete: ' + customer.id);
46    this.restService.deleteCustomer(customer.id)
47      .subscribe(data => {
48        this.getCustomers();
49      }, error => {
50        console.log(error);
51      });
52  }
53
54}

Setup

Project 60

Spring WebFlux & Angular, Reactive MongoDB, Clarity, Docker

https://gitorko.github.io/spring-webflux-angular/

Version

Check version

1$java --version
2openjdk 17.0.3 2022-04-19 LTS
3
4node --version
5v16.16.0
6
7yarn --version
81.22.18

Mongo DB

1docker run --name my-mongo -e MONGO_INITDB_ROOT_USERNAME=test -e MONGO_INITDB_ROOT_PASSWORD=test@123 -p 27017:27017 -d mongo 
2docker ps

Dev

To run the backend in dev mode.

1./gradlew clean build
2./gradlew bootRun

To Run UI in dev mode

1cd ui
2yarn install
3yarn build
4yarn start

Open http://localhost:4200/

Prod

To run as a single jar, both UI and backend are bundled to single uber jar.

1./gradlew cleanBuild
2cd build/libs
3java -jar project60-1.0.0.jar

Open http://localhost:8080/

Docker

1./gradlew cleanBuild
2docker build -f docker/Dockerfile --force-rm -t project60:1.0.0 .
3docker images |grep project60
4docker tag project60:1.0.0 gitorko/project60:1.0.0
5docker push gitorko/project60:1.0.0
6docker-compose -f docker/docker-compose.yml up

Commands

1ng new ui
2cd ui
3yarn add @cds/core @clr/icons @clr/angular @clr/ui

proxy.config.json redirects the client calls

1{
2  "/api/*": {
3    "target": "http://localhost:8080/",
4    "secure": false,
5    "logLevel": "debug"
6  }
7}

Modify package.json file, change the start & build command to

1"start": "ng serve --proxy-config proxy.config.json --open",
2"build": "ng build --prod",

Update the routing.The useHash:true will be useful when we deploy the application in a single uber jar later. If we dont use this then the back button on the application will run into errors. It uses a hash based routing instead of the default location based routing.

If you run into the error

1Error: initial exceeded maximum budget.

Update the budget in angular.json file

1"maximumWarning": "4mb",
2"maximumError": "5mb"

References

Angular Clartiy Spring Boot Spring Webflux

comments powered by Disqus