Credits / Notes taken from:
- Spring Boot and Angular Full Stack Development | 4 Hour Youtube Tutorial from AmigosCode
- Full Stack Spring Boot RESTful API with MySQL and Angular - Youtube Playlist - Direct Source from getarrays.io | Roland Toussaint "Junior"
- https://github.com/getarrays/server-backend/ - Spring Boot Backend
- https://github.com/getarrays/serverapp/ - Angular Frontend
Table of Contents (ToC):
- Spring Boot with Angular App: Server Ping Status Tracker
- Backend Spring Boot
- Project Setup
- Server Model, Repository, Service, Controller
- Database configuration
- Testing with Postman
- NEW - Exception Handling
- Frontend Angular
Prerequisites / Needs to be installed:
- Java. Check out my Study Notes on Java.
- Apache Maven (mvn).
- Node.js installed. We need it to use NPM (Node Package Manager) to create an Angular App.
- Angular. Check out my Study Notes on Angular.
- IntelliJ IDEA (Community or other) or Eclipse IDE.
- Visual Studio Code.
- Postman API Platform and HTTPie – API testing client... or you can use Insomnia instead
- MySQL 8.0 (448MB installer)
Currently installed on system:
where java
# C:\Program Files\Java\jdk-17.0.1\bin\java.exe
# C:\Program Files (x86)\Common Files\Oracle\Java\javapath\java.exe
java --version
# java 17.0.1 2021-10-19 LTS
# Java(TM) SE Runtime Environment (build 17.0.1+12-LTS-39)
# Java HotSpot(TM) 64-Bit Server VM (build 17.0.1+12-LTS-39, mixed mode, sharing)
java -version # for older versions (e.g. Java8 - build 1.8)
where mvn
# C:\Program Files\apache-maven-3.8.6\bin\mvn
# C:\Program Files\apache-maven-3.8.6\bin\mvn.cmd
mvn --version
# Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
# Maven home: C:\Program Files\apache-maven-3.8.6
# Java version: 17.0.1, vendor: Oracle Corporation, runtime: C:\Program Files\Java\jdk-17.0.1Note that mvn -version should yield the same Java version value as running java --version (Note that Maven takes the Java JDK from the path value within JAVA_HOME system environment variable). More info can be found here: https://mkyong.com/maven/maven-error-invalid-target-release-17/
mysqlsh --version
# C:\Program Files\MySQL\MySQL Shell 8.0\bin\mysqlsh.exe Ver 8.0.30 for Win64 on x86_64 - for MySQL 8.0.30 (MySQL Community Server (GPL))
docker --version
# Docker version 20.10.13, build a224086
docker-compose --version
# docker-compose version 1.29.2, build 5becea4c(Thursday, July 06, 2023, 19:32)
Project configuration:
- Project: Maven Project
- Spring Boot: Version 2.7.14 (July 2023)
- Project Metadata:
- Group (domain): "ENTER YOUR DOMAIN HERE" (for me it'll be com.radubulai)
- Artifact (the name of the application): serverpingstatustracker
- Name: serverpingstatustracker
- Description: Server Ping Status Tracker App with Angular Frontend and Spring Boot API Backend
- Package name (you could leave the autogenerated name): com.radubulai.serverpingstatustracker
- Packaging: Jar
- Java Version: 17
Dependencies:
- Spring Web - Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
- Spring Data JPA - Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate (ORM - Object Relational Mappind).
- MySQL JDBC driver
- Validation
I/O- Bean Validation with Hibernate validator. - Lombok
DEVELOPER TOOL- Java annotation library which helps to reduce boilerplate code.
The contents of 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.7.14-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.radubulai</groupId>
<artifactId>serverpingstatustracker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>serverpingstatustracker</name>
<description>Server Ping Status Tracker App with Angular Frontend and Spring Boot API Backend</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 2
To create a first model class, we right click on our main project package -> New -> Package -> "model".
Inside our "model" package, we right click -> New -> New Java Class -> Server.
Our server model will have the following fields (for now):
// /model/Server.java
import com.radubulai.serverpingstatustracker.enumeration.Status;
import java.io.Serializable;
/**
* @author Radu-Alexandru Bulai (https://radubulai.com)
* @version 1.0
* @since 06/07/2023
*/
public class Server implements Serializable {
private Long id;
private String ipAddress;
private String name;
private String network;
private Status status;
}Note: Importing the
Serializableinterface in theServerclass allows instances of the class to be serialized and deserialized. Serialization is the process of converting an object into a byte stream, which can be stored in a file or transmitted over a network. Deserialization is the reverse process, where the byte stream is converted back into an object. TheSerializableinterface itself doesn't require implementing any methods. It serves as a marker interface, indicating that the class is serializable. ThisServerclass can be serialized because all its fields (id,ipAddress,name,network, andstatus) are serializable or primitive types.
Now we create another enumeration package, and Status.java inside:
// /enumeration/Status.java
package com.radubulai.serverpingstatustracker.enumeration;
public enum Status {
SERVER_UP("SERVER_UP"),
SERVER_DOWN("SERVER_DOWN");
private final String status;
Status(String status) {
this.status = status;
}
public String getStatus() {
return this.status;
}
}Now we want this class to be in a database - that's why we added "Spring Data JPA (Java Persistence API)":
- We first add the
@Entityannotation from Java Persistence API (JPA). This marks the class as an entity, representing a table in a relational database. It indicates that instances of the Server class can be persisted and managed by an ORM (Object-Relational Mapping) framework, such as Hibernate. - We need to set our "PRIMARY KEY", for this we add
@Iddecorator. We also need to tell it how to generate this ID, so we add@GeneratedValue(strategy = AUTO): This JPA annotation specifies the generation strategy for theidfield. In this case, the value is set toAUTO, which means that the persistence provider (e.g., Hibernate) will determine the most appropriate strategy for generating the primary key values. @Column(unique = true): This JPA annotation specifies that theipAddressfield should be mapped to a database column, and theuniqueattribute indicates that the values in this column must be unique. It ensures that eachServerentity has a distinct IP address in the corresponding database column.
Note: from the "Lombok" library we'll use the following decorators:
@Data: This Lombok annotation generates boilerplate code for common methods such as getters, setters,equals(),hashCode(), andtoString(). It helps reduce the verbosity of the code by automatically generating these methods based on the class's fields.@NoArgsConstructor: This Lombok annotation generates a no-argument constructor for theServerclass. It allows frameworks like JPA to create instances of the class without having to provide constructor arguments explicitly. This constructor is useful in scenarios such as object instantiation through reflection or object population through deserialization.@AllArgsConstructor: This Lombok annotation generates a constructor with parameters for all fields in theServerclass. It provides a convenient way to initialize all the fields in a single constructor call. This constructor can be useful when creating instances of the class manually or when mapping data from external sources to theServerclass, andstatus) are serializable or primitive types.
Note: from the "Validation" (Hibernate Validator library) library we'll use the following:
@NotEmpty(message = "IP Address cannot be empty or null")indicates that theipAddressfield must not be empty or null. If an empty or null value is encountered during validation, a validation error with the specified message will be generated.
// /model/Server.java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Server implements Serializable {
@Id
@GeneratedValue(strategy = AUTO)
private Long id;
@Column(unique = true)
@NotEmpty(message = "IP Address cannot be empty or null")
private String ipAddress;
private String name;
private String network;
private Status status;
}Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 3
All the CRUD (Create, Read, Update, Delete) operations will be created under a "Server Repository" Java package (repository).
For the repository package:
- On our main project package -> create a new package called
repository - Inside
repositorypackage, we create a Java Interface calledServerRepository - This interface
ServerRepositorywill extend theJpaRepository. Also, when extending fromJpaRepositoryinterface, we need to specify the model type (Server) and the ID data type (Long):public interface ServerRepository extends JpaRepository<Server, Long>.
Note: We can CTRL+Click on
JpaRepositoryinterface to see its decompilled .class file (its code), and look over its methods, eg:findAll,saveAll,deleteAllInBatch, etc... (those are useful methods as we don't need to implement them from scratch)
We are going to use the default methods that are already implemented in the JpaRepository in order to manipulate the data within the database (create Servers, or delete Servers, etc).
However, we will create one additional custom method in order to find a Server entry by its IP Address: Server findByIpAddress(String ipAddress); (or findServerByIpAddress, either way is correct) - the equivalent MySQL query for this JPA Entity Mapping would be SELECT * FROM Server WHERE ipAddress = <ipAddress>;.
More notes: The process behind "translating
Server findByIpAddress(String ipAddress);in SQL and returning theServerJava Object:
JPA Entity Mapping: Assuming you have properly configured JPA with Hibernate or any other JPA implementation, and you have mapped the
Serverclass to a corresponding table in the database, JPA will handle the mapping between the Java object and the database table.Repository Interface: In JPA, you typically define a repository interface that extends
JpaRepository<Server, Long>or a similar interface. This interface provides various methods for performing CRUD (Create, Read, Update, Delete) operations on the entity.Method Declaration: By defining the method
findByIpAddress(String ipAddress)in the repository interface, you are specifying a custom query method. JPA will automatically generate the SQL query based on the method name and the rules defined in Spring Data JPA.SQL Generation: When the application starts up, Spring Data JPA analyzes the method name and parses it to determine the query's intent. In this case, the method name follows the naming convention of
findBy<PropertyName>. So, Spring Data JPA understands that you want to find aServerobject based on itsipAddressproperty.Query Execution: At runtime, when you invoke the
findByIpAddress()method with a specificipAddressparameter, Spring Data JPA generates the SQL query mentioned above. It replaces<ipAddress>with the actual parameter value and executes the query against the underlying MySQL database.Result Mapping: Once the query is executed, the database returns the result set, which consists of rows that match the specified
ipAddress. Spring Data JPA maps the result set to theServerentity class and returns the corresponding Java object or objects.In summary, the process involves the JPA entity mapping, defining a repository interface with the custom query method, SQL generation based on the method name, execution of the generated query, and mapping the query result back to Java objects. This allows you to easily perform database operations using familiar Java methods and have JPA handle the underlying SQL queries and result mapping for you.
Why JpaRepository is called "repository"?
The term "repository" in the context of Spring Data JPA refers to a pattern commonly used in software development for providing a standardized way to interact with a data source, such as a database. The repository pattern abstracts the underlying data access operations and provides a higher-level interface for working with data.
In the case of Spring Data JPA, the
JpaRepositoryinterface is an implementation of the repository pattern specifically designed for JPA (Java Persistence API). It combines the features of the repository pattern with the capabilities of JPA to simplify data access and provide a consistent interface for CRUD operations.
// ServerRepository.java
package com.radubulai.serverpingstatustracker.repository;
import com.radubulai.serverpingstatustracker.model.Server;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ServerRepository extends JpaRepository<Server, Long> {
Server findByIpAddress(String ipAddress);
void deleteServerById(Long id);
Optional<Server> findServerById(Long id);
}Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 4
So far we have the Server Model (data representation) and the Server Repository in order to manipulate the data (through CRUD operations in the SQL database). Next we will define the Server Service for the different features (or data processing within Java backend - Business logic) that we want to have in our application.
For the service package:
- On our main/base project package -> create a new package called
service - Inside
servicepackage, create a new Java Interface calledServerServiceI - Here in
ServerServiceIwe can define all our of methods that we will want to implement, such as:- retrieving all Servers from database:
Collection<Server> findAllServer(); - adding/creating a Server in database:
Server addServer(Server server); - updating a Server in database:
Server updateServer(Server server); - deleting a Server in database:
void deleteServer(Long id); - pinging a server in database:
Server ping(String ipAddress);
- retrieving all Servers from database:
// ServerServiceI.java
package com.radubulai.serverpingstatustracker.service;
import com.radubulai.serverpingstatustracker.model.Server;
import com.radubulai.serverpingstatustracker.repository.ServerRepository;
import java.util.Collection;
public interface ServerServiceI {
Collection<Server> findAllServers();
Server addServer(Server server);
Server updateServer(Server server);
void deleteServer(Long id);
Server pingServer(String ipAddress);
}- Inside
servicepackage we can create anotherimplementationpackage, and here we'll have theServerServiceImpl.javaclass- Inside
ServerServiceImpl.java, we create aServerRepositoryobject where we will use the defined SQL / Query methods - Now, usually after declaring this
serverRepositoryobject, we needed to initialize it by calling thepublic ServerServiceImpl(ServerRepository serverRepository) { this.serverRepository = serverRepository; }constructor - however, since we use the Lombok library, we can simply add the@RequiredArgsConstructorannotation - We also need to annotate the
ServerServiceclass repo with@Servicedecorator
- Inside
// ServerServiceImpl.java
package com.radubulai.serverpingstatustracker.service.implementation;
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class ServerServiceImpl implements ServerServiceI {
private final ServerRepository serverRepository;
}Notes on the annotations used:
@Service: This annotation is from the Spring Framework and is used to mark a class as a service component. It indicates that the class contains the business logic of the application. By annotating the class with@Service, it becomes eligible for auto-detection and can be injected into other Spring components, such as controllers.@RequiredArgsConstructor: This is a Lombok annotation that automatically generates a constructor with required arguments based on the class's final fields. In this case, since theServerRepositoryfield is marked asfinal, Lombok generates a constructor that accepts an instance ofServerRepositoryand assigns it to the field. This eliminates the need for explicitly defining a constructor in the class.@Transactional: This annotation is from the Spring Framework and is used to define the transactional behavior of a method or class. By annotating the class with@Transactional, all public methods in the class become transactional. Transactions ensure data consistency and integrity by enforcing ACID (Atomicity, Consistency, Isolation, Durability) properties when performing database operations.@Slf4j: This annotation is from Lombok and is used to generate a logger field in the class. It automatically creates a logger instance with the name "log" that can be used for logging messages within the class. The logging framework used depends on the project's configuration.
Now, we can define all of our business operations, such as:
1) pingServer(String ipAddress)
public Server pingServer(String ipAddress) throws IOException {
log.info("Pinging Server IP {}", ipAddress);
int IS_REACHABLE_TIMEOUT_IN_MILLIS = 10000;
Server server = serverRepository.findByIpAddress(ipAddress);
InetAddress inetAddress = InetAddress.getByName(ipAddress);
server.setStatus(inetAddress.isReachable(IS_REACHABLE_TIMEOUT_IN_MILLIS) ? SERVER_UP : SERVER_DOWN);
serverRepository.save(server);
return server;
}- retrieves a
Serverobject from theserverRepositoryby callingfindByIpAddress(JPA Entity Mapping ofSELECT * FROM Server WHERE ipAddress = <ipAddress>;SQL query) - uses the
InetAddress.getByNamemethod to obtain anInetAddressobject corresponding to the givenipAddress. This allows the function to perform network-related operations using the IP address. - sets the status of the
Serverobject based on whether the IP address is reachable. If the IP address is reachable within the given timeout, it sets the server's status to "SERVER_UP". Otherwise, it sets the status to "SERVER_DOWN".sets the status of theServerobject based on whether the IP address is reachable. If the IP address is reachable within the given timeout, it sets the server's status to "SERVER_UP". Otherwise, it sets the status to "SERVER_DOWN". - saves to database and returns the updated Server object
2) findAllServers()
public Collection<Server> findAllServers(int limit) {
log.info("Fetching {} servers", limit);
return serverRepository.findAll(PageRequest.of(0, limit)).toList();
}- retrieves a collection of
Serverobjects from theserverRepositoryby calling thefindAllmethod with aPageRequestobject. ThePageRequest.of(0, limit)method creates aPageRequestobject that specifies the page number (0indicates the first page) and the number of results to fetch (limitrepresents the maximum number of servers to fetch) - returns the collection of
Serverobjects
3) addServer(Server server)
- return the returned
Serverobject from calling the JPA Repositorysavemethod:return serverRepository.save(server);that has an equivalent query ofINSERT INTO servers (column1, column2, column3, ...) VALUES (value1, value2, value3, ...)
public Server addServer(Server server) {
log.info("Saving Server {}", server);
return serverRepository.save(server);
}4) updateServer(Server server)
- return
Serverobject from thesaveJPA (and underlying JPA provider - e.g. Hibernate) method with the equivalent query ofUPDATE servers SET column1 = value1, column2 = value2, column3 = value3, ... WHERE id = serverId
public Server updateServer(Server server) {
log.info("Updating Server {}", server);
return serverRepository.save(server);
}5) deleteServerById(Long id)
- since we can't use
serverRepo.delete(id);because thedeletemethod from inherited JPA Repo does not accept anyLongtype parameter (as an ID), we will need to create our ownserverRepo.deleteserverById(id);inside/repo/serverRepo.java
public void deleteServerById(Long id) {
log.info("Deleting Server with id={}", id);
serverRepository.deleteServerById(id);
}6) findServerById(Long id)
findServerById(Long id)needs to be defined in theServerRepository(returnsOptional<Server>)- if no server is found by id in db, do not return anything - throw a
ServerNotFoundExceptioninstead (usingorElseThrowJava8 method that receives a Java8 lambda function as parameter) - we define
ServerNotFoundException(that inheritsRuntimeException) separatedly in a packageexceptioninServerNotFoundExeption.java
// ServerServiceImpl.java
public Server findServerById(Long id) {
log.info("Fetching server with id={}", id);
return serverRepository.findServerById(id).orElseThrow(
() -> new ServerNotFoundException("Server by id " + id + " was not found")
);
}// ServerNotFoundExeption.java
package com.radubulai.serverpingstatustracker.exception;
public class ServerNotFoundException extends RuntimeException {
public ServerNotFoundException(String message) {
super(message);
}
}// ServerRepository.java
public interface ServerRepository extends JpaRepository<Server, Long> {
Server findServerByIpAddress(String ipAddress);
void deleteServerById(Long id);
Optional<Server> findServerById(Long id);
}Complete code from ServerServiceImpl.java:
// ServerServiceImpl.java
package com.radubulai.serverpingstatustracker.service.implementation;
import com.radubulai.serverpingstatustracker.exception.ServerNotFoundException;
import com.radubulai.serverpingstatustracker.model.Server;
import com.radubulai.serverpingstatustracker.repository.ServerRepository;
import com.radubulai.serverpingstatustracker.service.ServerServiceI;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Collection;
import static com.radubulai.serverpingstatustracker.enumeration.Status.*;
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class ServerServiceImpl implements ServerServiceI {
private final ServerRepository serverRepository;
@Override
public Collection<Server> findAllServers() {
return serverRepository.findAll();
}
public Collection<Server> findAllServers(int limit) {
log.info("Fetching {} servers", limit);
return serverRepository.findAll(PageRequest.of(0, limit)).toList();
}
public Page<Server> findAllServers(int pageNumber, int pageSize) {
log.info("Fetching {} servers (Page {})", pageSize, pageNumber);
Pageable pageable = PageRequest.of(pageNumber, pageSize);
return serverRepository.findAll(pageable);
}
public Server findServerById(Long id) {
log.info("Fetching server with id={}", id);
return serverRepository.findServerById(id).orElseThrow(
() -> new ServerNotFoundException("Server by id " + id + " was not found")
);
}
@Override
public Server addServer(Server server) {
log.info("Saving Server {}", server);
return serverRepository.save(server);
}
@Override
public Server updateServer(Server server) {
log.info("Updating Server {}", server);
return serverRepository.save(server);
}
@Override
public void deleteServerById(Long id) {
log.info("Deleting Server with id={}", id);
serverRepository.deleteServerById(id);
}
@Override
public Server pingServer(String ipAddress) throws IOException {
log.info("Pinging Server with ipAddress={}", ipAddress);
int IS_REACHABLE_TIMEOUT_IN_MILLIS = 10000;
Server server = serverRepository.findServerByIpAddress(ipAddress);
InetAddress inetAddress = InetAddress.getByName(ipAddress);
server.setStatus(inetAddress.isReachable(
IS_REACHABLE_TIMEOUT_IN_MILLIS) ? SERVER_UP : SERVER_DOWN);
serverRepository.save(server);
return server;
}
}(Saturday, July 08, 2023, 00:46)
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 8
Before implementing the Controller, we can create a Response class (under model package) that we can send back to the end user (browser) no matter the response to the request is an error or a succesfull retrieve/update/etc of data. The Response will include several properties such as:
timeStampstatusCode(the numerical status code)status(the correspondingHttpStatusenum value from Spring Framework, e.g.OKfor 200,CREATEDfor 201,MOVED_PERMANENTLYfor 301,FOUNDfor 302,BAD_REQUESTfor 400,UNAUTHORIZEDfor 401,NOT_FOUNDfor 404,INTERNAL_SERVER_ERRORfor 500, etc.)reason(a descriptive reason for the response)message(a human-readable message that can be shown to the end user)developerMessage(a more technical message for developers or for debugging purposes)data
Note, by default, if we do not implement such class, every response that our API will send will be the direct JSON data (and other details will be found in the header of the HTTP request).
package com.radubulai.serverpingstatustracker.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.experimental.SuperBuilder;
import org.springframework.http.HttpStatus;
import java.time.LocalDateTime;
import java.util.Map;
@Data
@SuperBuilder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Response {
protected LocalDateTime timeStamp;
protected int statusCode;
protected HttpStatus status;
protected String reason;
protected String message;
protected String developerMessage;
protected Map<?, ?> data;
}Annotations from above:
@Dataannotation from Lombok library generates boilerplate code for common methods such as getters, setters,equals(),hashCode(), andtoString()@SuperBuilderannotation from Lombok library allows for a fluent builder API for constructing instances of theResponseclass (see below its usage in ServerResource controller).@JsonInclude(JsonInclude.Include.NON_NULL)annotation from the Jackson library ensures that properties with null values are not included in the JSON serialization - it helps in producing a more concise and clean JSON response
org.springframework.http.HttpStatus.class
| Value | HttpStatus.Series | Reason Phrase |
|---|---|---|
| 100 | INFORMATIONAL | Continue |
| 101 | INFORMATIONAL | Switching Protocols |
| 102 | INFORMATIONAL | Processing |
| 103 | INFORMATIONAL | Checkpoint |
| 200 | SUCCESSFUL | OK |
| 201 | SUCCESSFUL | Created |
| 202 | SUCCESSFUL | Accepted |
| 203 | SUCCESSFUL | Non-Authoritative Information |
| 204 | SUCCESSFUL | No Content |
| 205 | SUCCESSFUL | Reset Content |
| 206 | SUCCESSFUL | Partial Content |
| 207 | SUCCESSFUL | Multi-Status |
| 208 | SUCCESSFUL | Already Reported |
| 226 | SUCCESSFUL | IM Used |
| 300 | REDIRECTION | Multiple Choices |
| 301 | REDIRECTION | Moved Permanently |
| 302 | REDIRECTION | Found |
| 303 | REDIRECTION | See Other |
| 304 | REDIRECTION | Not Modified |
| 307 | REDIRECTION | Temporary Redirect |
| 308 | REDIRECTION | Permanent Redirect |
| 400 | CLIENT_ERROR | Bad Request |
| 401 | CLIENT_ERROR | Unauthorized |
| 402 | CLIENT_ERROR | Payment Required |
| 403 | CLIENT_ERROR | Forbidden |
| 404 | CLIENT_ERROR | Not Found |
| 405 | CLIENT_ERROR | Method Not Allowed |
| 406 | CLIENT_ERROR | Not Acceptable |
| 407 | CLIENT_ERROR | Proxy Authentication Required |
| 408 | CLIENT_ERROR | Request Timeout |
| 409 | CLIENT_ERROR | Conflict |
| 410 | CLIENT_ERROR | Gone |
| 411 | CLIENT_ERROR | Length Required |
| 412 | CLIENT_ERROR | Precondition Failed |
| 413 | CLIENT_ERROR | Payload Too Large |
| 414 | CLIENT_ERROR | URI Too Long |
| 415 | CLIENT_ERROR | Unsupported Media Type |
| 416 | CLIENT_ERROR | Requested range not satisfiable |
| 417 | CLIENT_ERROR | Expectation Failed |
| 418 | CLIENT_ERROR | I'm a teapot |
| 419 | CLIENT_ERROR | Insufficient Space On Resource |
| 420 | CLIENT_ERROR | Method Failure |
| 421 | CLIENT_ERROR | Destination Locked |
| 422 | CLIENT_ERROR | Unprocessable Entity |
| 423 | CLIENT_ERROR | Locked |
| 424 | CLIENT_ERROR | Failed Dependency |
| 425 | CLIENT_ERROR | Too Early |
| 426 | CLIENT_ERROR | Upgrade Required |
| 428 | CLIENT_ERROR | Precondition Required |
| 429 | CLIENT_ERROR | Too Many Requests |
| 431 | CLIENT_ERROR | Request Header Fields Too Large |
| 451 | CLIENT_ERROR | Unavailable For Legal Reasons |
| 500 | SERVER_ERROR | Internal Server Error |
| 501 | SERVER_ERROR | Not Implemented |
| 502 | SERVER_ERROR | Bad Gateway |
| 503 | SERVER_ERROR | Service Unavailable |
| 504 | SERVER_ERROR | Gateway Timeout |
| 505 | SERVER_ERROR | HTTP Version not supported |
| 506 | SERVER_ERROR | Variant Also Negotiates |
| 507 | SERVER_ERROR | Insufficient Storage |
| 508 | SERVER_ERROR | Loop Detected |
| 509 | SERVER_ERROR | Bandwidth Limit Exceeded |
| 510 | SERVER_ERROR | Not Extended |
| 511 | SERVER_ERROR | Network Authentication Required |
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 9
For a newly created resource package:
- We can create a
ServerResource.javaclass (resource is a more usual term for the RESTful API Design Methodology - we could have call itServerController.javainstead) - Add the following annotations:
@RestController: This annotation is from the Spring Framework and combines the@Controllerand@ResponseBodyannotations. It marks the class as a RESTful controller, indicating that the class will handle incoming HTTP requests and produce HTTP responses. The@RestControllerannotation is typically used in Spring MVC or Spring WebFlux applications.@RequestMapping("/api/servers"): This annotation is from the Spring Framework and is used to map incoming requests to specific handler methods in the controller. It defines the base URL ("/api/servers") that this controller will handle. In this case, any request that starts with "/api/servers" will be directed to methods within this class for further processing.@RequiredArgsConstructor: This annotation is from the Lombok library and generates a constructor with required arguments for theServerResourceclass. SinceServerServiceImplis a required dependency forServerResource, Lombok generates a constructor that accepts an instance ofServerServiceImpland assigns it to theserverServicefield. This annotation eliminates the need to explicitly write the constructor code.
(Saturday, July 08, 2023, 23:41)
1) getAllServers
@RestController
@RequestMapping("/api/servers")
@RequiredArgsConstructor
public class ServerResource {
private final ServerServiceImpl serverService;
@GetMapping("")
public ResponseEntity<Response> getAllServers() {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("servers", serverService.findAllServers()))
.message("Servers retrieved")
.status(OK)
.statusCode(OK.value())
.build());
}
}@GetMapping(""): Spring Framework annotation that maps the function to handle GET requests on the base URL ("/api/servers"). The empty string within the@GetMappingannotation indicates that the function will handle GET requests to the base URL itself.public ResponseEntity<Response> getAllServers(): returns aResponseEntity<Response>object, which represents the HTTP response that will be sent back to the client. TheResponseclass is a custom class that encapsulates the response data.ResponseEntity.ok(...): method is used to create aResponseEntityobject with an HTTP status of 200 (OK). It indicates that the request was successful, and the response will contain the desired data.Response.builder(): uses the builder pattern (from Lombok library) to create a newResponseobject..timeStamp(now()): sets thetimeStampproperty of theResponseobject to the current timestamp..data(Map.of("servers", serverService.findAllServers())): sets thedataproperty of theResponseobject to a map containing a single key-value pair. The key is "servers" and the value is the result of calling thefindAllServers()method on theserverServiceinstance..message("Servers retrieved"): sets themessageproperty of theResponseobject to "Servers retrieved". It provides a human-readable message indicating the purpose of the response..status(OK): sets thestatusproperty of theResponseobject to theHttpStatus.OKenum value, indicating that the request was successful..statusCode(OK.value()): sets thestatusCodeproperty of theResponseobject to the numerical value of theHttpStatus.OKenum, which is 200..build(): builds the finalResponseobject using the configured properties and returns it.
2) pingServer
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 10
The pingServer function handles a GET request to ping a server with a specified IP address. It calls the pingServer method on the serverService instance, retrieves the server object, and constructs a Response object containing the server data, timestamp, and a message indicating the success or failure of the ping operation. The response is wrapped in a ResponseEntity object with an HTTP status of 200 (OK) and returned to the client.
@GetMapping("/ping/{ipAddress}")
public ResponseEntity<Response> pingServer(
@PathVariable("ipAddress") String ipAddress) throws IOException {
Server server = serverService.pingServer(ipAddress);
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("server", server))
.message(server.getStatus() == SERVER_UP ? "Ping success" : "Ping failed")
.status(OK)
.statusCode(OK.value())
.build());
}@GetMapping("/ping/{ipAddress}"): annotation from the Spring Framework that maps the function to handle GET requests on the URL "/api/servers/ping/{ipAddress}". The{ipAddress}is a path variable that will be dynamically replaced with the actual IP address provided in the URL.public ResponseEntity<Response> pingServer(@PathVariable("ipAddress") String ipAddress) throws IOException: handles the GET request and expects a path variable named "ipAddress" to be provided in the URL. The value of the path variable will be passed as theipAddressparameter of the function. It also specifies that the function may throw anIOException.Server server = serverService.pingServer(ipAddress): line calls thepingServermethod on theserverServiceinstance, passing theipAddressparameter. It retrieves aServerobject representing the server with the specified IP address.ResponseEntity.ok(...): is used to create aResponseEntityobject with an HTTP status of 200 (OK). It indicates that the request was successful, and the response will contain the desired data.Response.builder(): starts building a newResponseobject using the builder pattern..timeStamp(now()): sets thetimeStampproperty of theResponseobject to the current timestamp..data(Map.of("server", server)): sets thedataproperty of theResponseobject to a map containing a single key-value pair. The key is "server," and the value is theserverobject obtained from theserverService.pingServermethod..message(server.getStatus() == SERVER_UP ? "Ping success" : "Ping failed"): sets themessageproperty of theResponseobject based on the status of theserverobject. If the server status isSERVER_UP, the message is set to "Ping success"; otherwise, it is set to "Ping failed"..status(OK): sets thestatusproperty of theResponseobject to theHttpStatus.OKenum value, indicating that the request was successful..statusCode(OK.value()): sets thestatusCodeproperty of theResponseobject to the numerical value of theHttpStatus.OKenum, which is 200..build(): builds the finalResponseobject using the configured properties and returns it.
3) addServer
The addServer method handles an HTTP POST request to create a new server. It validates the request payload, adds the server using the serverService.addServer method, and constructs a Response object containing the server data, timestamp, and a success message. The response is wrapped in a ResponseEntity object with an HTTP status of 200 (OK) and returned to the client.
@PostMapping("")
public ResponseEntity<Response> addServer(@RequestBody @Valid Server server) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("server", serverService.addServer(server)))
.message("Server created")
.status(CREATED)
.statusCode(CREATED.value())
.build());
}public ResponseEntity<Response> addServer(@RequestBody @Valid Server server): method expects a JSON payload representing aServerobject in the request body. The@RequestBodyannotation binds the request body to theserverparameter. The@Validannotation validates theServerobject using the specified validation constraints (e.g., from the ServerModel -@Column(unique = true) @NotEmpty(message = "IP Address cannot be empty or null") private String ipAddress;).ResponseEntity.ok(...): method is used to create aResponseEntityobject with an HTTP status of 200 (OK). It indicates that the request was successful, and the response will contain the desired data. Note that if we usedcreateit would not let us return back the Server object that was added to database.Response.builder(): starts building a newResponseobject using the builder pattern..timeStamp(now()): sets thetimeStampproperty of theResponseobject to the current timestamp..data(Map.of("server", serverService.addServer(server))): sets thedataproperty of theResponseobject to a map containing a single key-value pair. The key is "server," and the value is theserverobject obtained from theserverService.addServermethod, which adds the server to the system..message("Server created"): sets themessageproperty of theResponseobject to "Server created". It provides a human-readable message indicating the successful creation of the server..status(CREATED): sets thestatusproperty of theResponseobject to theHttpStatus.CREATEDenum value.statusCode(CREATED.value()): sets thestatusCodeproperty of theResponseobject to the numerical value of theHttpStatus.CREATEDenum, which is 201..build(): builds the finalResponseobject using the configured properties and returns it.
4) getServerById
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 11
@GetMapping("/{id}")
public ResponseEntity<Response> getServerById(@PathVariable Long id) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("server", serverService.findServerById(id)))
.message("Server retrieved")
.status(OK)
.statusCode(OK.value())
.build());
}5) deleteServerById
Deletes a server by sending a DELETE request to the /api/servers/{id} endpoint, where {id} is the identifier of the server to be deleted.
@DeleteMapping("/{id}")
public ResponseEntity<Response> deleteServerById(@PathVariable Long id) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("deleted", serverService.deleteServerById(id)))
.message("Server deleted")
.status(OK)
.statusCode(OK.value())
.build());
}Complete code (so far) for ServerResource.java (Sunday, July 09, 2023, 00:38)
package com.radubulai.serverpingstatustracker.resource;
import com.radubulai.serverpingstatustracker.model.Response;
import com.radubulai.serverpingstatustracker.model.Server;
import com.radubulai.serverpingstatustracker.service.implementation.ServerServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.io.IOException;
import java.util.Map;
import static com.radubulai.serverpingstatustracker.enumeration.Status.*;
import static java.time.LocalDateTime.*;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.OK;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 08/07/2023
*/
@RestController
@RequestMapping("/api/servers")
@RequiredArgsConstructor
public class ServerResource {
private final ServerServiceImpl serverService;
@GetMapping("")
public ResponseEntity<Response> getAllServers() {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("servers", serverService.findAllServers()))
.message("Servers retrieved")
.status(OK)
.statusCode(OK.value())
.build());
}
@GetMapping("/{id}")
public ResponseEntity<Response> getServerById(@PathVariable Long id) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("server", serverService.findServerById(id)))
.message("Server retrieved")
.status(OK)
.statusCode(OK.value())
.build());
}
@PostMapping("")
public ResponseEntity<Response> addServer(@RequestBody @Valid Server server) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("server", serverService.addServer(server)))
.message("Server created")
.status(CREATED)
.statusCode(CREATED.value())
.build());
}
@PutMapping("")
public ResponseEntity<Response> updateServer(@RequestBody @Valid Server server) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("server", serverService.updateServer(server)))
.message("Server updated")
.status(OK)
.statusCode(OK.value())
.build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Response> deleteServerById(@PathVariable Long id) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("deleted", serverService.deleteServerById(id)))
.message("Server deleted")
.status(OK)
.statusCode(OK.value())
.build());
}
@GetMapping("/ping/{ipAddress}")
public ResponseEntity<Response> pingServer(
@PathVariable("ipAddress") String ipAddress) throws IOException {
Server server = serverService.pingServer(ipAddress);
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("server", server))
.message(server.getStatus() == SERVER_UP ? "Ping success" : "Ping failed")
.status(OK)
.statusCode(OK.value())
.build());
}
}Note on July 10, 2023, 22:00: Additional methods were added:
public Server pingServer(Long id) throws IOException- that finds the server by its id then pings itpublic Collection<Server> pingAllServers() throws IOException- retrieves all the servers then pings each one of them, returns the list of servers with all statuses updated (Note: could take long until the list is received on client browser)public Status pingServerOnly(String ipAddress) throws IOException- only pings server specified in URL@GetMapping("/ping/{ipAddress}")and returns its status (does not query the database)
After installing MySQL 8.0 (448MB installer), we can open "MySQL 8.0 Command Line Client" (from Windows Start Menu).
🔵 Note: (On Windows) If we cannot start the MySQL Server (eg. "MySQL Workbench" just crashes when we try to start the server):
- Open Windows Start Menu, search and open "Services", manually find
MySQL80service -> Right click it -> Start. - (does not work in my case) We can just run (as administrator) the executable from
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe. - See more here: Can't startup and connect to MySQL server.
Alternative: Using MySQL Docker image instead of installing MySQL 8.0 Server on Linux (or Windows) PC
- Install Docker.
- (In Linux) Check if Docker service is running by
systemctl status docker(usesystemctl start dockerif service is not running)- Run
sudo docker images -ato view current images- Run
sudo docker pull mysqlto pull the latest MySQL image from https://hub.docker.com/_/mysql- Run the below command that will create a MySQL server container with the following configurations:
- Container name: mysql-server
- Root password: yourpassword
- Database name: pingstatustracker
- Exposed port: 3306
sudo docker run -d \ --name mysql-server \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -e MYSQL_DATABASE=pingstatustracker \ -p 3306:3306 \ mysql:latest \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci \ --default-authentication-plugin=mysql_native_password \ --sql-mode=NO_ENGINE_SUBSTITUTION \ --innodb-flush-log-at-trx-commit=0Or in one line:
sudo docker run -d --name mysql-server -e MYSQL_ROOT_PASSWORD=yourpassword -e MYSQL_DATABASE=pingstatustracker -p 3306:3306 mysql:latest --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-authentication-plugin=mysql_native_password --sql-mode=NO_ENGINE_SUBSTITUTION --innodb-flush-log-at-trx-commit=0
Check if the MySQL container was created with
sudo docker container ls. You can also check images, containers, cache size withsudo docker system dfCheck if the MySQL container is running with
sudo docker container ls -a. If MySQL:latest container is up, you can start the Spring Boot Application by runningmvn spring-boot:run- if application ran with no errors, then the Spring Boot app did successfully connect to MySQL running as docker image!You can run
sudo docker exec -it mysql-server mysql -uroot -pyourpassword -D pingstatustrackerto enter the MySQL image and run some INSERT SQL queries, e.g.:INSERT INTO server (id, ip_address, name, network, status) VALUES (1, '8.8.8.8', 'Google 1', 'External', '0');Check http://localhost:8080/api/servers if servers from MySQL database running in Docker can be retrieved
To set up our database in our Java Spring Application, we need to go to src/main/resource/application.yml (Note: application.properties can be easily renamed/refactored to application.yml):
# application.yml
spring:
datasource:
# MySQL
url: jdbc:mysql://localhost:3306/pingstatustracker
username: root
password: yourpassword
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
format_sql: trueFor the MySQL setup:
- The default port for MySQL is 3306, so the DB address will be
localhost:3306 - the name of the database will be
pingstatustracker=> the address will be localhost:3306/pingstatustracker
The spring.jpa section contains configuration settings for JPA (Java Persistence API) and Hibernate, the ORM (Object-Relational Mapping) framework.
show-sql: trueenables logging of SQL statements executed by Hibernate, providing visibility into the generated SQL queries (Note that this should always be disabled in production).hibernate.ddl-auto:createspecifies that Hibernate should automatically create the database schema based on the entity mappings. This will create the necessary tables when the application starts. Note that this setting is typically used in development and should be handled differently in production.updatemeans that Hibernate will update the database schema based on the entity classes' definitions if necesary.
properties.hibernate.dialectspecifies the dialect to use for the MySQL database. In this case, theorg.hibernate.dialect.MySQL5InnoDBDialectdialect is selected, which is suitable for MySQL version 5 and InnoDB storage engine.properties.hibernate.format_sql: trueenables the formatting of SQL statements logged by Hibernate, making them more readable for debugging purposes.
We can now create the pingstatustracker MySQL database:
- Open "MySQL 8.0 Command Line Client", write:
create database pingstatustracker; - We can check with
show databases;command
We can run the Java Application from Terminal, in the main project directory:
mvn spring-boot:runAfter running the app, the Server table from the pingstatustracker database is created automatically in MySQL and can be seen via MySQL Workbench App:
- On MySQL Workbench -> Click on "Database" from the menu -> "Reverse Engineer(CTRL+B)" -> next, next, next -> Select your schema -> "execute".
(Sunday, July 09, 2023, 23:35)
Note: We can also interogate (or insert some data into) our MySQL database from the MySQL 8.0 Command Line Client.exe:
show databases;
use pingstatustracker;
show tables;
select * from server;We can insert the following data, where:
0= "SERVER_UP" (Ping success)1= "SERVER_DOWN" (Ping failed)
INSERT INTO server (id, ip_address, name, network, status) VALUES (1, '8.8.8.8', 'Google 1', 'External', '0');
INSERT INTO server (id, ip_address, name, network) VALUES (2, '4.4.4.4', 'Google 2', 'External');
INSERT INTO server (id, ip_address, name, network) VALUES (3, '192.168.0.1', 'Server 1', 'Office');
INSERT INTO server (id, ip_address, name) VALUES (4, '192.168.0.3', 'License');- Note: To remove all rows from the server table, effectively deleting all the inserted entries, run
DELETE FROM server;
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 12
You can create an account and download Postman from here: https://www.postman.com/downloads/
Run the application (mvn spring-boot:run in terminal).
🟢 Note that our Java Spring application runs on port 8080 by default (http://localhost:8080/). So we can already make a GET request to http://localhost:8080/api/servers in our browser (Chrome/Edge/Firefox/etc).
In Postman:
🔵 Send a GET request to http://localhost:8080/api/servers (findAllServers)
🔵 Send a GET request to <http://localhost:8080/api/servers/> (findAllServers)
🔵 Send a POST request with new Server information
- Open a new tab in Postman with the URL of http://localhost:8080/servers
- Set the request type to POST request
- Click on "Body" subtab
- check the "raw" radio button
- select "JSON" format
- write a JSON without specifying the id (the id will be generated by Spring JPA)
{
"ipAddress": "192.168.0.2",
"name": "Default IP",
"network": "Home",
"status": null
}Another example:
- for the following JSON
{
"ipAddress": "192.168.1.1",
"name": "Linksys",
"network": "Home",
"status": null
}- the server will return:
{
"timeStamp": "2023-07-11T21:26:11.1401562",
"statusCode": 201,
"status": "CREATED",
"message": "Server created",
"data": {
"server": {
"id": 7,
"ipAddress": "192.168.1.1",
"name": "Linksys",
"network": "Home",
"status": null
}
}
}🔵 Send UPDATE request to update a server
- Open a new tab in Postman with the URL of http://localhost:8080/servers
- Set the request type to PUT request
- Click on "Body" subtab, check the "raw" radio button, and select "JSON" format
- Instead of this entry (that we currenly have in our database):
"server": {
"id": 7,
"ipAddress": "192.168.1.1",
"name": "Linksys",
"network": "Home",
"status": null
}- We'll send this (where the
nameandimageUrlwas changed)
{
"id": 7,
"ipAddress": "192.168.1.1",
"name": "Linksys Updated",
"network": "Home 2",
"status": null
}🔵 Sent GET request to http://localhost:8080/api/servers/ping/8.8.8.8
🔵 Sent GET request to http://localhost:8080/api/servers/ping/ to ping and retrieve all servers
(Saturday, February 03, 2024, 20:35)
Resources:
- Amigoscode - Spring Boot 2 Tutorial | How To Handle Exceptions
- Teddy Smith - Spring Boot 2.7.4 For Beginners - Exception Handling
- GitHub Repository https://github.com/teddysmithdev/pokemon-review-springboot
- Spring Boot | REST API Request Validation & Exception Handling Realtime Example | JavaTechie 38m
- Under
com.radubulai.serverpingstatustrackercreateexceptionpackage - Under
exceptionpackage, createServerNotFoundException.javaclass
// ServerNotFoundException.java
package com.radubulai.serverpingstatustracker.exception;
public class ServerNotFoundException extends RuntimeException {
public ServerNotFoundException(String message) {
super(message);
}
public ServerNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}- Under
exceptionpackage, createApiException.javaclass
// ApiException.java
package com.radubulai.serverpingstatustracker.exception;
import org.springframework.http.HttpStatus;
import java.time.LocalDateTime;
public class ApiException {
private final LocalDateTime timeStamp;
private final int statusCode;
private final HttpStatus status;
private final String reason;
private final String message;
private final String developerMessage;
public ApiException(LocalDateTime timeStamp,
int statusCode,
HttpStatus status,
String reason,
String message,
String developerMessage) {
this.timeStamp = timeStamp;
this.statusCode = statusCode;
this.status = status;
this.reason = reason;
this.message = message;
this.developerMessage = developerMessage;
}
public LocalDateTime getTimeStamp() {
return timeStamp;
}
public int getStatusCode() {
return statusCode;
}
public HttpStatus getStatus() {
return status;
}
public String getReason() {
return reason;
}
public String getMessage() {
return message;
}
public String getDeveloperMessage() {
return developerMessage;
}
}- Under
exceptionpackage, createApiExceptionHandler.javaclass
// ApiExceptionHandler.java
package com.radubulai.serverpingstatustracker.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.net.UnknownHostException;
import java.sql.SQLIntegrityConstraintViolationException;
import static java.time.LocalDateTime.now;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 03-Feb-2024
*/
@Slf4j
@ControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(value = {ServerNotFoundException.class})
public ResponseEntity<Object> handleServerNotFoundException(ServerNotFoundException e) {
HttpStatus notFound = HttpStatus.NOT_FOUND;
ApiException apiException = new ApiException(
now(),
notFound.value(),
notFound,
"ServerNotFoundException",
e.getMessage(),
e.toString()
);
log.error(e.toString());
return new ResponseEntity<>(apiException, notFound);
}
@ExceptionHandler(value = {UnknownHostException.class})
public ResponseEntity<Object> handleUnknownHostException(UnknownHostException e) {
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
ApiException apiException = new ApiException(
now(),
httpStatus.value(),
httpStatus,
"UnknownHostException",
e.getMessage(),
e.toString()
);
log.error(e.toString());
e.printStackTrace();
return new ResponseEntity<>(apiException, httpStatus);
}
@ExceptionHandler(value = {SQLIntegrityConstraintViolationException.class})
public ResponseEntity<Object> handleSQLIntegrityConstraintViolationException(
SQLIntegrityConstraintViolationException e) {
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
ApiException apiException = new ApiException(
now(),
httpStatus.value(),
httpStatus,
"SQLIntegrityConstraintViolationException",
e.getMessage(),
e.toString()
);
log.error(e.toString());
e.printStackTrace();
return new ResponseEntity<>(apiException, httpStatus);
}
}Difference in Responses with exception handling vs no exception handling in Spring Boot:
Example 01. Making a GET request to a non-existing server:
- Postman: GET http://localhost:8080/api/servers/349
// Response
{
"timeStamp": "2024-02-03T20:47:17.0402758",
"statusCode": 200,
"status": "OK",
"message": "Server retrieved",
"data": {
"server": {
"id": 349,
"ipAddress": "dns.adguard.com",
"name": "DNS Adguard",
"network": "External",
"details": "One of the best servers for DNS with ad block",
"status": "SERVER_UP"
}
}
}- Postman: GET http://localhost:8080/api/servers/350 (with no Spring Boot exception handling)
// Response
{
"timestamp": "2024-02-03T18:49:00.842+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/servers/350"
}- Postman: GET http://localhost:8080/api/servers/350 (with Spring Boot exception handling)
// Response
{
"timeStamp": "2024-02-03T20:49:32.0222647",
"statusCode": 404,
"status": "NOT_FOUND",
"reason": "ServerNotFoundException",
"message": "Server by id 350 was not found",
"developerMessage": "com.radubulai.serverpingstatustracker.exception.ServerNotFoundException: Server by id 350 was not found"
}Example 02. Making a POST request to a server which IpAddress value (8.8.8.8) already exists in database
- Postman: POST http://localhost:8080/api/servers
// Request Payload
{
"ipAddress": "8.8.8.8",
"name": "test",
"network": "test network",
"status": "SERVER_DOWN"
}// Response with no custom Spring Boot exception handling
{
"timestamp": "2024-02-03T16:46:59.537+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/servers/"
}// Response with custom Spring Boot exception handling
{
"timeStamp": "2024-02-03T18:48:16.7354346",
"statusCode": 400,
"status": "BAD_REQUEST",
"reason": "SQLIntegrityConstraintViolationException",
"message": "Duplicate entry '8.8.8.8' for key 'server.UK_96tx503up4941ibvsnhh8itdi'",
"developerMessage": "java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '8.8.8.8' for key 'server.UK_96tx503up4941ibvsnhh8itdi'"
}POST http://localhost:8080/api/servers 400 (Bad Request)
server.service.ts:133 HttpErrorResponse {headers: HttpHeaders, status: 400, statusText: 'OK', url: 'http://localhost:8080/api/servers', ok: false, …}error: developerMessage: "java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '8.8.8.8' for key 'server.UK_96tx503up4941ibvsnhh8itdi'"message: "Duplicate entry '8.8.8.8' for key 'server.UK_96tx503up4941ibvsnhh8itdi'"reason: "SQLIntegrityConstraintViolationException"status: "BAD_REQUEST"statusCode: 400timeStamp: "2024-02-03T18:41:03.5251476"[[Prototype]]: Objectheaders: HttpHeaders {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}message: "Http failure response for http://localhost:8080/api/servers: 400 OK"name: "HttpErrorResponse"ok: falsestatus: 400statusText: "OK"url: "http://localhost:8080/api/servers"[[Prototype]]: HttpResponseBase
POST http://localhost:8080/api/servers 500 (Internal Server Error)
server.service.ts:133 HttpErrorResponse {headers: HttpHeaders, status: 500, statusText: 'OK', url: 'http://localhost:8080/api/servers', ok: false, …}error: error: "Internal Server Error"path: "/api/servers"status: 500timestamp: "2024-02-03T16:43:15.929+00:00"[[Prototype]]: Objectheaders: HttpHeaders {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}message: "Http failure response for http://localhost:8080/api/servers: 500 OK"name: "HttpErrorResponse"ok: falsestatus: 500statusText: "OK"url: "http://localhost:8080/api/servers"[[Prototype]]: HttpResponseBase
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 13
We will use Node.js and Angular CLI to create our Front-End Angular Application!
Currently installed on system:
node --version
# v18.16.1
npm --version
# 9.5.1
ng version
# _ _ ____ _ ___
# / \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
# / △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
# / ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
# /_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
# |___/
#
# Angular CLI: 16.1.4
# Node: 18.16.1
# Package Manager: npm 9.5.1
# OS: win32 x64
#
# Package Version
# ------------------------------------------------------
# @angular-devkit/architect 0.1601.4 (cli-only)
# @angular-devkit/core 16.1.4 (cli-only)
# @angular-devkit/schematics 16.1.4 (cli-only)
# @schematics/angular 16.1.4 (cli-only)(Tuesday, July 11, 2023, 23:42)
In main app's folder after installing Node.js:
# Install the Angular CLI: To install the Angular CLI globally,
# open a terminal window and run the following command
# (You will run this only once):
npm install -g @angular/cli
# To create a new project (a new workspace for an app)
ng new serverpingstatustracker-app
# Change directory
cd serverpingstatustracker-app
# The Angular CLI includes a server, for you to build and serve your app locally
ng serve --open- Would you like to add Angular routing? (y/N): y
- Which stylesheet format would you like to use?: CSS
// package.json
{
"name": "serverpingstatustracker-app",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^16.1.0",
"@angular/common": "^16.1.0",
"@angular/compiler": "^16.1.0",
"@angular/core": "^16.1.0",
"@angular/forms": "^16.1.0",
"@angular/platform-browser": "^16.1.0",
"@angular/platform-browser-dynamic": "^16.1.0",
"@angular/router": "^16.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.1.4",
"@angular/cli": "~16.1.4",
"@angular/compiler-cli": "^16.1.0",
"@types/jasmine": "~4.3.0",
"jasmine-core": "~4.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.1.3"
}
}package.json: this fo;e contains the metadata and configuration for your project, including the dependencies and scripts. It serves as a manifest for your application, specifying what libraries and versions are required.package-lock.json: this file is automatically generated bynpmwhen you install or update dependencies. It provides a detailed and deterministic description of the dependency tree, including the specific versions of each dependency installed. It ensures that all developers working on the project get the same dependency versions, helping to maintain consistency and avoid dependency conflicts.
Brief explanation of above packages:
-
rxjs (~7.8.0): RxJS is imported by default in Angular. RxJS is a library for reactive programming (opposite of procedural programming) using Observables, enabling you to work with asynchronous data streams. The version "~7.8.0" indicates that any version starting with 7.8.x is acceptable, allowing minor updates but keeping the major version fixed.
-
tslib (^2.3.0): tslib is a runtime library for TypeScript that provides helper functions used by the generated JavaScript code. The "^2.3.0" version specifier means that any version equal to or higher than 2.3.0 can be used, allowing both minor and patch updates.
-
zone.js (~0.13.0): Zone.js is a library that helps with managing asynchronous operations in JavaScript. It provides execution context and hooks into asynchronous operations. The version "~0.13.0" indicates that any version starting with 0.13.x is acceptable, allowing minor updates.
-
jasmine-core (~4.6.0): Jasmine is a behavior-driven development (BDD) testing framework for JavaScript. It provides a clean and readable syntax for writing tests. The version "~4.6.0" indicates that any version starting with 4.6.x is acceptable, allowing minor updates.
-
karma (~6.4.0): Karma is a test runner for JavaScript that enables you to execute tests in multiple browsers simultaneously. It provides a simple and consistent way to run tests, capture browsers, and generate test reports. The version "~6.4.0" means that any version starting with 6.4.x is acceptable, allowing minor updates.
-
karma-chrome-launcher (~3.2.0): Karma Chrome Launcher is a plugin for Karma that launches Google Chrome to run tests in it. The version "~3.2.0" means that any version starting with 3.2.x is acceptable, allowing minor updates.
-
karma-coverage (~2.2.0): Karma Coverage is a plugin for Karma that generates code coverage reports for your tests, showing how much of your code is covered by tests. The version "~2.2.0" indicates that any version starting with 2.2.x is acceptable, allowing minor updates.
-
karma-jasmine (~5.1.0): Karma Jasmine is a plugin for Karma that integrates the Jasmine testing framework, allowing you to write and run Jasmine tests using Karma. The version "~5.1.0" means that any version starting with 5.1.x is acceptable, allowing minor updates.
-
karma-jasmine-html-reporter (~2.1.0): Karma Jasmine HTML Reporter is a plugin for Karma that generates an HTML report with the results of Jasmine tests executed by Karma. The version "~2.1.0" indicates that any version starting with 2.1.x is acceptable, allowing minor updates.
First, we can create a folder that will contain all the enumerations within the Angular Project:
- create
./src/app/enum/status.enum.ts
// status.enum.ts
export enum Status {
ALL = "ALL",
SERVER_UP = "SERVER_UP",
SERVER_DOWN = "SERVER_DOWN",
}- with
ALLwe will have the option to select (from font-end/UI) all the servers no matter the status - we can then filter the servers based on the SERVER_UP or SERVER_DOWN statuses.
- create
./src/app/enum/data-state.enum.tsthat will represent the "Data state" of the data that is in progress to be retrieved
// data-state.enum.ts
export enum DataState {
LOADING_STATE = "LOADING_STATE",
LOADED_STATE = "LOADED_STATE",
ERROR_STATE = "ERROR_STATE",
}- create
./src/app/interface/server.ts - define all the attributes that the Server model (in Spring API & database) has:
// server.ts
import { Status } from "../enum/status.enum";
export interface Server {
id: number;
ipAddress: string;
name: string;
network: string;
status: Status;
}- create
./src/app/interface/custom-response.ts - define all the attributes that the Response model (in Spring API) has:
- the response that the client gets back from Spring API BackEnd could either have a server or multiple servers as data
// custom-response.ts
import { Server } from "./server";
export interface CustomResponse {
timeStamp: Date;
statusCode: number;
status: string;
reason: string;
message: string;
developerMessage: string;
data: { servers?: Server[]; server?: Server };
}(Saturday, July 15, 2023)
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 15
For a reactive approach, we will habe the state of the entire application at any given moment:
- create
./src/app/interface/app-state.ts- the state will be defined from the above enum
enum DataState { LOADING_STATE = 'LOADING_STATE', LOADED_STATE = 'LOADED_STATE', ERROR_STATE = 'ERROR_STATE' } appDatawill be generic (<T>)- since we cannot get the data and an error at the same time,
appData?anderror?will be both optional
- the state will be defined from the above enum
// app-state.ts
import { DataState } from "../enum/data-state.enum";
export interface AppState<T> {
dataState: DataState;
appData?: T;
error?: string;
}Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 16
First, before working on server service, un the Angular's main /src/app/app.module.ts we need to import: import { HttpClientModule } from '@angular/common/http';, and also add it to the imports array: imports: [BrowserModule, HttpClientModule],. 🟠 Note: If we don't import the HttpClientModule we will receive the following error from Angular in our browser console: ERROR NullInjectorError: R3InjectorError(AppModule)[EmployeeService -> HttpClient -> HttpClient -> HttpClient]: NullInjectorError: No provider for HttpClient!
// app.module.ts
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClientModule } from "@angular/common/http";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule, HttpClientModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}We will now generate the "server" service that will contain all the methods for the HTTP Requests. With Angular's CLI, in the Angular project path, run:
ng generate service services/serverIn server.service.ts we will use Angular's HttpClient in order to make HTTP requests (get, post, put, delete) that'll be injected in the constructor: export class ServerService { constructor(private http: HttpClient) {} }
The procedural approach of implementing the ServerService class that provides methods to interact with the backend API would be the following:
// server.service.ts - The procedural approach
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { CustomResponse } from "../interfaces/custom-response";
@Injectable({ providedIn: "root" }) // The Injectable decorator is used to mark the ServerService as a service that can be injected with dependencies. It allows the service to be injected into other components or services.
export class ServerService {
constructor(private http: HttpClient) {}
getServers(): Observable<CustomResponse> {
return this.http.get<CustomResponse>("http://localhost:8080/api/servers");
}
getServerById(serverId: number): Observable<CustomResponse> {
return this.http.get<CustomResponse>(
`http://localhost:8080/api/servers/${serverId}`
);
}
addServer(server: Server): Observable<Server> {
const methodName = "addServer() ";
console.debug(methodName + "Request Sent: " + JSON.stringify(server));
return this.http.post<Server>(`http://localhost:8080/api/servers`, server);
}
}However, we will implement the ServerService class using the reactive approach with RxJS operators:
servers$ = <Observable<CustomResponse>>...: declares a propertyservers$of typeObservable<CustomResponse>. The dollar sign convention ($) is a good practice to indicate that this property is an observable.this.http.get<CustomResponse>(${this.apiUrl}/servers): makes an HTTP GET request to the API endpoint${this.apiUrl}/serversusing theHttpClientservice'sgetmethod. It expects the response to be of typeCustomResponse.pipe(tap(console.log), catchError(this.handleError)): uses thepipeoperator to chain multiple operators to the observable. Thetapoperator is used to perform a side effect of logging the response to the console, while thecatchErroroperator is used to handle any errors that may occur during the HTTP request.handleError(handleError: any): Observable<never> { ... }: This method defines an error handler function namedhandleErrorwhich takes anerrorparameter of typeany. The return type isObservable<never>, indicating that it returns an observable that never emits any values. However, in the provided code, the implementation of thehandleErrormethod is incomplete and throws aMethod not implementederror when called.
The reactive approach using RxJS operators allows for a more streamlined and declarative way of handling asynchronous operations. It promotes composing and transforming observables using operators, making the code more concise and readable. Operators like tap and catchError provide powerful ways to perform side effects and error handling within the observable pipeline.
Compared to the procedural approach in the previous example, this reactive approach separates the observable creation and configuration (servers$) from the actual usage of the observable. The reactive approach also leverages the power of RxJS operators to handle errors and perform side effects, making it more flexible and easier to manage complex asynchronous flows.
// server.service.ts - The reactive approach
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { catchError, tap } from "rxjs/operators";
import { CustomResponse } from "../interfaces/custom-response";
@Injectable({ providedIn: "root" })
export class ServerService {
private readonly apiUrl = "http://localhost:8080/api";
constructor(private http: HttpClient) {}
servers$ = <Observable<CustomResponse>>(
this.http
.get<CustomResponse>(`${this.apiUrl}/servers`)
.pipe(tap(console.log), catchError(this.handleError))
);
handleError(handleError: any): Observable<never> {
return throwError("Method not implemented.");
}
}(Monday, July 17, 2023, 22:34)
The complete server.service.ts using the reactive approach
// server.service.ts - The reactive approach | Complete code
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { catchError, tap } from "rxjs/operators";
import { CustomResponse } from "../interfaces/custom-response";
import { Server } from "../interfaces/server";
import { Status } from "../enums/status.enum";
@Injectable({ providedIn: "root" })
export class ServerService {
private readonly apiUrl = "http://localhost:8080/api";
constructor(private http: HttpClient) {}
servers$ = <Observable<CustomResponse>>(
this.http
.get<CustomResponse>(`${this.apiUrl}/servers`)
.pipe(tap(console.log), catchError(this.handleError))
);
filter$ = (status: Status, response: CustomResponse) =>
<Observable<CustomResponse>>new Observable<CustomResponse>((subscriber) => {
console.log(response);
const servers = response.data?.servers || [];
const filteredServers = servers.filter(
(server) => server.status === status
);
const message =
status === Status.ALL
? `Servers filtered by ${status} status`
: filteredServers.length > 0
? `Servers filtered by ${
status === Status.SERVER_UP ? "SERVER UP" : "SERVER DOWN"
} status`
: `No servers of ${status} found`;
subscriber.next({
...response,
message,
data: {
servers: filteredServers,
},
});
subscriber.complete();
}).pipe(tap(console.log), catchError(this.handleError));
getServerById = (serverId: number) =>
<Observable<CustomResponse>>(
this.http
.get<CustomResponse>(`${this.apiUrl}/servers/${serverId}`)
.pipe(tap(console.log), catchError(this.handleError))
);
addServer$ = (server: Server) =>
<Observable<CustomResponse>>(
this.http
.post<CustomResponse>(`${this.apiUrl}/servers`, server)
.pipe(tap(console.log), catchError(this.handleError))
);
updateServer$ = (server: Server) =>
<Observable<CustomResponse>>(
this.http
.put<CustomResponse>(`${this.apiUrl}/servers`, server)
.pipe(tap(console.log), catchError(this.handleError))
);
deleteServerById = (serverId: number) =>
<Observable<CustomResponse>>(
this.http
.delete<CustomResponse>(`${this.apiUrl}/servers/${serverId}`)
.pipe(tap(console.log), catchError(this.handleError))
);
pingServerById = (serverId: number) =>
<Observable<CustomResponse>>(
this.http
.get<CustomResponse>(`${this.apiUrl}/servers/${serverId}/ping`)
.pipe(tap(console.log), catchError(this.handleError))
);
pingServerByIpAddress = (ipAddress: string) =>
<Observable<CustomResponse>>(
this.http
.get<CustomResponse>(`${this.apiUrl}/servers/ping/${ipAddress}`)
.pipe(tap(console.log), catchError(this.handleError))
);
handleError(error: HttpErrorResponse): Observable<never> {
console.error(error);
return throwError(() => new Error(`Error Code: ${error.status}`));
}
}For the filter$ function that performs filtering on a CustomResponse object based on the provided status parameter. Here's a breakdown of the function:
(status: Status, response: CustomResponse) =>: takes two parameters:statusof typeStatusandresponseof typeCustomResponse.<Observable<CustomResponse>>new Observable<CustomResponse>((subscriber) => { ... }): creates a new instance of an observable that emits values of typeCustomResponse. It takes a subscriber function as an argument.const servers = response.data?.servers || [];: initializes theserversvariable with the value ofresponse.data.serversif it exists; otherwise, it assigns an empty array toservers. This guards againstresponse.dataorresponse.data.serversbeingundefined.const filteredServers = servers.filter((server) => server.status === status);: filters theserversarray based on the providedstatus, creating a new arrayfilteredServersthat only contains servers with a matching status.const message = ...: This block assigns the appropriate message based on the conditionals:- If
statusisStatus.ALL, it setsmessageto'Servers filtered by ${status} status'. - If
filteredServers.lengthis greater than 0, it setsmessageto'Servers filtered by SERVER UP status'or'Servers filtered by SERVER DOWN status'. - If none of the above conditions are met, it sets
messageto'No servers of ${status} found'.
- If
subscriber.next({ ... }): emits a newCustomResponseobject to the subscriber, which includes the modifiedresponseobject:- The
messageproperty is updated based on the filtering results. - The
dataproperty is updated with the filtered servers.
- The
subscriber.complete();: signals the completion of the observable stream..pipe(tap(console.log), catchError(this.handleError)): pipes the observable to apply additional operators:- The
tap(console.log)operator logs the emitted values to the console. - The
catchError(this.handleError)operator catches and handles any errors that occur during the observable stream, using thehandleErrormethod of theServerService.
- The
In summary, the filter$ function filters servers based on the provided status and emits a modified CustomResponse object as an observable. The emitted response includes a message indicating the filtering results and the filtered server data.
@Componentdecorator provides metadata for the component, such as the selector, main template URL, and CSS stylesappState$!: Observable<AppState<CustomResponse>>;: declares theappState$property as an observable of typeAppState<CustomResponse>. The!indicates that the property will be assigned a value later.- The constructor initializes the
serverServiceproperty with an instance of theServerServiceinjected through dependency injection so we can use all the methods defined there. ngOnInit(): voidis a lifecycle hook that runs when the component is initializedthis.appState$ = this.serverService.getServersPinged$().pipe(...): assigns theappState$property to the result of thegetServersPinged$()method call from theserverService. The method is assumed to return an observable that emitsCustomResponsedata.map((response) => { return { dataState: DataState.LOADED_STATE, appData: response }; }):mapoperator transforms the emittedresponsedata by wrapping it in an object withdataStateandappDataproperties. It sets thedataStatetoDataState.LOADED_STATEand assigns theresponsetoappData.startWith({ dataState: DataState.LOADING_STATE }):startWithoperator emits an initial value{ dataState: DataState.LOADING_STATE }before the actual values emitted by the observable. This helps indicate the loading state when the component first initializes.catchError((error: string) => { return of({ dataState: DataState.ERROR_STATE, error }); }):catchErroroperator catches any errors that occur in the observable stream and handles them. It transforms the error into an object withdataStateset toDataState.ERROR_STATEand includes the error message.
In summary, this code sets up the appState$ observable in the AppComponent class. It fetches data from the serverService, transforms the response using the map operator, handles loading state with startWith, and catches and handles errors with catchError. The appState$ observable will emit different states (LOADING_STATE, LOADED_STATE, or ERROR_STATE) based on the server response and error conditions.
// app.component.ts
import { Component, OnInit } from "@angular/core";
import { ServerService } from "./services/server.service";
import { Observable, catchError, map, of, startWith } from "rxjs";
import { AppState } from "./interfaces/app-state";
import { CustomResponse } from "./interfaces/custom-response";
import { DataState } from "./enums/data-state.enum";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
})
export class AppComponent implements OnInit {
appState$!: Observable<AppState<CustomResponse>>;
constructor(private serverService: ServerService) {}
ngOnInit(): void {
this.appState$ = this.serverService.getServersPinged$().pipe(
map((response) => {
return { dataState: DataState.LOADED_STATE, appData: response };
}),
startWith({ dataState: DataState.LOADING_STATE }),
catchError((error: string) => {
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}
}<!-- app.component.html -->
<div>{{ appState$ | async | json }}</div>{{ appState$ | async | json }}: is an Angular template expression enclosed within double curly braces ({{ }}). It binds the value of theappState$observable to the content of the<div>element.appState$: is theappState$property defined in theAppComponentclass, which is an observable of typeAppState<CustomResponse>.async:asyncpipe is used to subscribe to theappState$observable and automatically handle the subscription and unsubscription. It allows the template to display the latest emitted value from the observable.json: Thejsonpipe is used to format the value as a JSON string. It converts the emitted value from the observable into a string representation in JSON format.
So, in summary, this HTML template code displays the JSON string representation of the latest emitted value from the appState$ observable within the <div> element. It leverages the async pipe to handle the subscription and updates the displayed value whenever the observable emits a new value.
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 20
Now:
- if we start the application, namely:
- starting the MySQL Server (Start Menu, search and open "Services", manually find MySQL80 service -> Right click it -> Start)
- starting the SpringBoot Back-end Server (
mvn spring-boot:runand test on http://localhost:8080/api/servers) - starting the Front-end Angular Application (
ng serve --openon http://localhost:4200/)
- we will run into the following CORS error:
Access to XMLHttpRequest at 'http://localhost:8080/api/servers/ping' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
To solve this issue, we need to tell the back-end to allow the front-end app to run on requested url (origin) in order to access the resources.
On the SpringBoot project, in the main application class ServerpingstatustrackerApplication we need to add the following CORS Configuration after the main() function:
// ServerpingstatustrackerApplication.java
package com.radubulai.serverpingstatustracker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
@SpringBootApplication
public class ServerpingstatustrackerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerpingstatustrackerApplication.class, args);
}
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:4200", "http://localhost:8081"));
corsConfiguration.setAllowedHeaders(Arrays.asList("Origin", "Access-Control-Allow-Origin", "Content-Type",
"Accept", "Authorization", "Origin, Accept", "X-Requested-With",
"Access-Control-Request-Method", "Access-Control-Request-Headers"));
corsConfiguration.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization",
"Access-Control-Allow-Origin", "Access-Control-Allow-Credentials"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}Now we can successfully make requests from frontend:
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 21
We will use the following (NOT FINISHED) HTML + CSS code (Note these may change completely untill the end of tutorial):
<!-- app.component.html -->
<!-- <div>{{ appState$ | async | json }}</div> -->
<!-- Navigation bar -->
<nav class="navbar navbar-expand-lg navbar-dark">
<h1 style="font-size: 1rem">
<a class="navbar-brand" style="color: white">Server Ping Status Tracker</a>
</h1>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarColor02"
aria-controls="navbarColor02"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarColor02">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#addServerModal" data-toggle="modal"
>Add Server <span class="sr-only">(current)</span></a
>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/"
>All employees <span class="sr-only">(current)</span></a
>
</li>
<li class="nav-item">
<a routerLink="/about" class="nav-link"
>About <span class="sr-only">(current)</span></a
>
</li>
</ul>
<div>
<select
name="status"
ngModel="ALL"
class="btn btn-primary"
title="Filter Severs by Status"
>
<option value="ALL">ALL</option>
<option value="SERVER_UP">SERVER UP</option>
<option value="SERVER_DOWN">SERVER DOWN</option>
</select>
</div>
<div
class="dark-mode-icon nav-item"
title="Toggle between light/dark theme"
>
<i class="fa fa-moon-o fa-2x mx-2"></i>
</div>
</div>
</nav>
<div class="container">
<div class="table-responsive">
<div class="table-wrapper">
<ng-container
*ngIf="appState$ | async as appState"
[ngSwitch]="appState.dataState"
>
<ng-container *ngSwitchCase="">
<div class="col-md-12 text-center">
<div class="spinner-border text-info" role="status"></div>
</div>
</ng-container>
<ng-container>
<table class="table table-striped table-hover" id="servers">
<thead>
<tr>
<th>IP Address</th>
<th>Name</th>
<th>Network</th>
<th>Status</th>
<th>Ping</th>
<th>Actions</th>
</tr>
</thead>
<tbody
*ngFor="
let server of (appState$ | async)?.appData?.data?.servers;
let i = index
"
>
<tr>
<td>{{ server.ipAddress }}</td>
<td>{{ server.name }}</td>
<td>{{ server.network }}</td>
<td>
<span
class="badge"
[ngClass]="[
server.status === Status.SERVER_UP
? ' badge-success'
: ' badge-danger'
]"
>
{{ server.status === Status.SERVER_UP ? "SERVER UP" :
"SERVER DOWN" }}
</span>
</td>
<td>
<a style="cursor: pointer">
<i *ngIf="" class="material-icons" title="Ping server"
></i
>
<i
class="fa fa-spinner fa-spin"
style="font-size: 24px"
></i>
</a>
</td>
<td>
<a class="delete" data-toggle="modal" style="cursor: pointer"
><i
class="material-icons"
data-toggle="tooltip"
title="Delete"
></i
></a
>
</td>
</tr>
</tbody>
</table>
</ng-container>
<!-- <ng-container *ngSwitchCase="DataState.ERROR_STATE">
<div class="alert-danger">
{{ appState.error }}
</div>
</ng-container> -->
</ng-container>
</div>
</div>
</div>
<!-- Add server Modal HTML -->
<div id="addServerModal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<form>
<div class="modal-header">
<h4 class="modal-title">Add Server</h4>
<button
type="button"
class="close"
data-dismiss="modal"
aria-hidden="true"
>
×
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>IP</label>
<input
type="text"
ngModel
name="ipAddress"
class="form-control"
required
/>
</div>
<div class="form-group">
<label>Name</label>
<input
type="text"
ngModel
name="name"
class="form-control"
required
/>
</div>
<div class="row">
<div class="col-xl-6 col-lg-6 col-md-6 col-sm-6 col-6">
<div class="form-group">
<label>Memory</label>
<input
type="text"
ngModel
name="memory"
class="form-control"
required
/>
</div>
</div>
<div class="col-xl-6 col-lg-6 col-md-6 col-sm-6 col-6">
<div class="form-group">
<label>Type</label>
<input
type="text"
ngModel
name="type"
class="form-control"
required
/>
</div>
</div>
</div>
<div class="form-group">
<label>Status</label>
<select
name="status"
ngModel="SERVER_DOWN"
class="form-control"
name="status"
required
>
<option value="SERVER_UP">SERVER UP</option>
<option value="SERVER_DOWN">SERVER DOWN</option>
</select>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-warning"
id="closeModal"
data-dismiss="modal"
>
Cancel
</button>
<!-- <button
type="submit"
[disabled]="serverForm.invalid || (isLoading$ | async)"
class="btn btn-success"
>
<i *ngIf="isLoading$ | async" class="fas fa-spinner fa-spin"></i>
<span *ngIf="isLoading$ | async">Saving...</span>
<span *ngIf="!(isLoading$ | async)">Add</span>
</button> -->
</div>
</form>
</div>
</div>
</div>
<!-- <notifier-container></notifier-container> -->Importing Bootstrap CSS and Jquery for Bootstrap builtin HTML "components" such as modals
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Server Ping Status Tracker by Radu-Alexandru Bulai</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
defer
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js"
defer
></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"
defer
></script>
</head>
<body>
<app-root></app-root>
</body>
</html>/* styles.css */
@import "https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css";
@import "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.css";
:root {
--background-color: hsl(201, 33%, 98%);
--background-color-secondary: hsl(0, 0%, 100%);
--primary-color: hsl(200, 27%, 37%);
--secondary-color: hsl(200, 33%, 24%);
--primary-font-color: #000;
--container-color: hsl(0, 0%, 97%);
}
[data-theme="dark"] {
--background-color: hsl(207, 29%, 6%);
--background-color-secondary: hsl(206, 28%, 10%);
--primary-color: hsl(216, 85%, 80%);
--secondary-color: hsl(210, 79%, 75%);
--primary-font-color: #fff;
--container-color: hsl(207, 29%, 8%);
}
::selection {
background: var(--primary-color);
color: #fff;
}
body {
background-color: var(--background-color);
font-size: 16px;
color: var(--primary-font-color);
transition: background-color 300ms ease-in, color 300ms ease-in;
}
@media (min-width: 1200px) {
.container {
max-width: 1400px;
}
}
.container {
margin-top: 2rem;
}
.modal-content {
background-color: var(--container-color);
}
.modal-footer {
border-top: none !important;
}
.navbar {
background-color: hsl(200, 33%, 10%);
}
.nav-link:hover {
cursor: pointer;
}
@media screen and (max-width: 992px) {
.nav-item {
display: flex;
justify-content: center;
}
}
input {
color: var(--primary-font-color) !important;
background-color: var(--background-color-secondary) !important;
transition: background-color 300ms ease-in !important;
}
.card {
background-color: var(--container-color);
border: none;
box-shadow: 1px 2px 5px 1px rgba(0, 0, 0, 0.1);
margin-bottom: 1rem;
border-radius: 1rem;
transition: background-color 300ms ease-in;
}
.card--name {
font-size: 1.2rem;
font-weight: 600;
color: var(--primary-color);
}
.card--img {
aspect-ratio: 1/1;
object-fit: cover;
}
.card .list-group-item {
background-color: transparent;
}
.dark-mode-icon {
color: #fff;
cursor: pointer;
}
.w-60 {
width: 4rem;
}
.btn-primary {
background-color: var(--primary-color) !important;
border-color: var(--primary-color) !important;
}
.btn-primary:hover {
background-color: var(--secondary-color) !important;
border-color: var(--primary-color) !important;
}
.btn-primary:disabled {
color: #fff;
background-color: gray;
border-color: gray;
}
.social-links li a {
-webkit-border-radius: 50%;
background-color: var(--primary-color);
border-radius: 50%;
color: #fff;
display: inline-block;
height: 30px;
line-height: 30px;
text-align: center;
width: 30px;
font-size: 12px;
transition: background-color 200ms ease-in;
}
.social-links li a:hover {
background-color: var(--secondary-color);
}
a {
color: var(--primary-color);
}
a:hover {
color: var(--secondary-color);
}
.alert-title {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 1rem;
}/* app.component.css */Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 21
First, by using Bootstrap v4.x CSS Spinner (and fontawesome 6.4.0) and based on our LOADING/LOADED appState$.dataState, we can use ngSwitch and ngSwitchCase for displaying different ng-containers for different states of the application.
<!-- app.component.html -->
<ng-container
*ngIf="appState$ | async as appState"
[ngSwitch]="appState.dataState"
>
<ng-container *ngSwitchCase="DataState.LOADING_STATE">
<div class="col-md-12 text-center">
<div class="spinner-border text-info" role="status"></div>
</div>
</ng-container>
...
<ng-container *ngSwitchCase="DataState.LOADED_STATE">
<table>
...
</table>
</ng-container>
...
<ng-container *ngSwitchCase="DataState.ERROR_STATE">
<div class="alert-danger">{{ appState.error }}</div>
</ng-container>
</ng-container>// app.component.ts
// Here we add readonly DataState = DataState;
...
import { DataState } from './enums/data-state.enum';
import { Status } from './enums/status.enum';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
appState$!: Observable<AppState<CustomResponse>>;
readonly Status = Status;
readonly DataState = DataState;
constructor(private serverService: ServerService) {}
ngOnInit(): void {
this.appState$ = this.serverService.servers$.pipe(
map((response) => {
return { dataState: DataState.LOADED_STATE, appData: response };
}),
startWith({ dataState: DataState.LOADING_STATE }),
catchError((error: string) => {
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}
}(Thursday, July 20, 2023, 22:56)
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 22
<!-- app.component.html -->
<div class="container">
<div class="table-responsive">
<ng-container
*ngIf="appState$ | async as appState"
[ngSwitch]="appState.dataState"
>
<ng-container *ngSwitchCase="DataState.LOADING_STATE">
<div class="col-md-12 text-center">
<div
title="Loading Servers..."
class="spinner-border text-info"
role="status"
></div>
</div>
</ng-container>
<ng-container *ngSwitchCase="DataState.LOADED_STATE">
<table class="table table-hover" id="servers">
<thead>
<tr>
<th>#</th>
<th>IP Address</th>
<th>Name</th>
<th>Network</th>
<th>Status</th>
<th>Ping</th>
<th>Actions</th>
</tr>
</thead>
<tbody
*ngFor="
let server of appState.appData?.data?.servers;
let i = index
"
>
<tr>
<td title="Server ID: {{ server.id }}">{{ i + 1 }}</td>
<td>{{ server.ipAddress }}</td>
<td>{{ server.name }}</td>
<td>{{ server.network }}</td>
<td>
<span
class="badge"
[ngClass]="[
server.status === Status.SERVER_UP
? ' badge-success'
: ' badge-danger'
]"
>
{{ server.status === Status.SERVER_UP ? "SERVER UP" : "SERVER
DOWN" }}
</span>
</td>
<td>
<a style="cursor: pointer">
<i
class="fa fa-globe"
title="Ping Server"
style="font-size: 1.5rem"
></i>
<i
class="fa fa-spinner fa-spin"
style="font-size: 1.5rem"
></i>
</a>
</td>
<td>
<a class="delete" data-toggle="modal" style="cursor: pointer"
><i
class="fa fa-trash-o fa-1x mx-2"
title="Delete Server"
style="font-size: 1.5rem"
></i
></a>
</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-container *ngSwitchCase="DataState.ERROR_STATE">
<div class="alert-danger">{{ appState.error }}</div>
</ng-container>
</ng-container>
</div>
</div>Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 23
We want that (from the above screenshot), when we click the "ping server" button to show a spinnig loading icon while we wait for a response.
For this, we can define the following in app.component.ts:
private filterSubject = new BehaviorSubject<string>('');:- A
BehaviorSubjectis a type ofSubjectthat always emits the most recent value to its subscribers, even if they subscribe after the value has been emitted. It is initialized with an empty string''.
- A
readonly filterStatus$ = this.filterSubject.asObservable();:- creates a public read-only
ObservablenamedfilterStatus$, which is derived from thefilterSubjectusing theasObservable()method. This ensures that external components can only subscribe to thefilterStatus$observable and cannot modify its value directly. This observable will emit the values of thefilterSubject.
- creates a public read-only
In app.component.html:
*ngIf="... (filterStatus$ | async) === '' || (filterStatus$ | async) !== server.ipAddress":- checks if the value emitted by the
filterStatus$observable is an empty string''or not equal toserver.ipAddress. The server IP address is used to filter the servers, so if there's no filtering (filterStatus$is an empty string) or if the filtering is not applied to the current server, the first<i>element with the classfa fa-tower-broadcast(an icon representing a server) is displayed (this would be the default since the declared string is empty).
- checks if the value emitted by the
*ngIf="(filterStatus$ | async) == server.ipAddress":- checks if the value emitted by the
filterStatus$observable is equal to the currentserver.ipAddress. If it matches, the second<i>element with the classfa fa-circle-notch fa-spin(an icon representing a spinning circle) is displayed. This typically indicates that the server is being actively pinged or processed.
- checks if the value emitted by the
Note: We use .fa-spin CSS Class for the .fa-spinner icon (from fontawesome 6.4.0) for the HTML loading icon.
(Saturday, July 22, 2023)
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 25
// app.component.ts
import { Component, OnInit } from "@angular/core";
import { ServerService } from "./services/server.service";
import {
BehaviorSubject,
Observable,
catchError,
map,
of,
startWith,
} from "rxjs";
import { AppState } from "./interfaces/app-state";
import { CustomResponse } from "./interfaces/custom-response";
import { DataState } from "./enums/data-state.enum";
import { Status } from "./enums/status.enum";
/**
* @author Radu-Alexandru Bulai
* @version 1.0.0
* @since 18/07/2023
*/
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
})
export class AppComponent implements OnInit {
appState$!: Observable<AppState<CustomResponse>>;
readonly Status = Status;
readonly DataState = DataState;
private ipAddressSubjectWhenPinging = new BehaviorSubject<string>("");
readonly ipAddressStatusWhenPinging$ =
this.ipAddressSubjectWhenPinging.asObservable();
private serversCopyDataSubject = new BehaviorSubject<CustomResponse>(null!);
// Assert that null can be assigned by using the non-null assertion operator !
// This assumes that the data passed to serversDataCopySubject (the UI copy of servers)
// will be updated before any subscribers access it, ensuring that it won't actually be null.
constructor(private serverService: ServerService) {}
ngOnInit(): void {
this.appState$ = this.serverService.servers$.pipe(
map((response) => {
this.serversCopyDataSubject.next(response);
return { dataState: DataState.LOADED_STATE, appData: response };
}),
startWith({ dataState: DataState.LOADING_STATE }),
catchError((error: string) => {
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}
pingServerByItsIpAddress(ipAddress: string): void {
// Assign ip string to show a spinning loading icon while pinging
this.ipAddressSubjectWhenPinging.next(ipAddress);
this.appState$ = this.serverService.pingServerByIpAddress$(ipAddress).pipe(
map((response) => {
const serversCopy = this.serversCopyDataSubject.value.data.servers;
const indexOfPingedServer = serversCopy!.findIndex(
(server) => server.id === response.data.server!.id
);
// Update the Server from serversCopy in UI after it has been pinged
serversCopy![indexOfPingedServer] = response.data.server!;
// Assign empty string to stop showing spinning loading icon
this.ipAddressSubjectWhenPinging.next("");
return {
dataState: DataState.LOADED_STATE,
appData: this.serversCopyDataSubject.value,
};
}),
startWith({
dataState: DataState.LOADED_STATE,
appData: this.serversCopyDataSubject.value,
}),
catchError((error: string) => {
this.ipAddressSubjectWhenPinging.next("");
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}
}app.component.ts - pingServerByItsIpAddress():
this.ipAddressSubjectWhenPinging.next(ipAddress);from the HTML code, we added a condition that whenever this "variable" (that has its observer onreadonly ipAddressStatusWhenPinging$ = this.ipAddressSubjectWhenPinging.asObservable();) is populated, we will show a loading spinner in UI (if thisipAddressSubjectWhenPingingis empty then we will show the "Ping" button in UI)this.serverService.pingServerByIpAddress$will make the ping request to the REST SpringBoot API (by callingpingServermethod from SpringBoot) which will return a CustomResponse containing the updated server (with its status updated).- we will keep a copy of all the Servers that are already rendered/retrieved in the UI, this copy will be inside
serversCopyDataSubject = new BehaviorSubject<CustomResponse>(null!);(and the servers data will be inside the value property, such asconst serversCopy = this.serversCopyDataSubject.value.data.servers;) - once we retrieve the response from backend server:
- now we need to update our Server from the Servers list from UI (stored as a copy)
- we will identify the server (that has been pinged) from our copy of servers list in UI by founding its index
- to find the equivalent server in our UI, we will search the list by comparing the IDs of any server from our UI list vs the id of updated server retrieved as response from backend
- note that this
pingServerByItsIpAddresscalled fromapp.component.htmlwill return (inthis.appState$) the entire copy of servers list from UI that contains the updated server, therefore the UI will be updated this.ipAddressSubjectWhenPinging.next('');assign an empty string to show back the "Ping" button in UI (app.component.html) instead of spinning loading icon
<!-- app.component.html -->
<tbody
*ngFor="
let server of appState.appData?.data?.servers;
let i = index
"
>
<tr>
<td title="Server ID: {{ server.id }}">{{ i + 1 }}</td>
<td>{{ server.ipAddress }}</td>
<td>{{ server.name }}</td>
<td>{{ server.network }}</td>
<td>
<span
class="badge"
[ngClass]="[
server.status === Status.SERVER_UP
? ' badge-success'
: ' badge-danger'
]"
>
{{ server.status === Status.SERVER_UP ? "SERVER UP" : "SERVER DOWN" }}
</span>
</td>
<td>
<a
class="edit"
style="cursor: pointer"
(click)="pingServerByItsIpAddress(server.ipAddress)"
>
<i
*ngIf="
(ipAddressStatusWhenPinging$ | async) === '' ||
(ipAddressStatusWhenPinging$ | async) !== server.ipAddress
"
class="fa fa-tower-broadcast fa-1x"
title="Ping Server"
style="font-size: 1.5rem"
></i>
<i
*ngIf="
(ipAddressStatusWhenPinging$ | async) === server.ipAddress
"
class="fa fa-circle-notch fa-spin"
style="font-size: 1.5rem"
></i>
</a>
</td>
<td>
<a class="edit" data-toggle="modal" style="cursor: pointer"
><i
class="fa fa-pen fa-1x mx-2"
title="Delete Server"
style="font-size: 1.5rem"
></i
></a>
<a class="delete" data-toggle="modal" style="cursor: pointer"
><i
class="fa fa-trash fa-1x mx-2"
title="Delete Server"
style="font-size: 1.5rem"
></i
></a>
</td>
</tr>
</tbody>Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 27
Implement Angular Filter By Status method and call it in UI
- Refactor
filterByStatus$fromserver.service.tscomponent- to work with "ALL" option (that just returns the unmodified list of servers)
- simplify message by either "Servers filtered by %{status} status" or "No servers of ${status} status were found"
// server.service.ts
filterByStatus$ = (status: Status, response: CustomResponse) =>
<Observable<CustomResponse>>new Observable<CustomResponse>((subscriber) => {
console.log(response);
const servers = response.data?.servers || [];
const filteredServers =
status === Status.ALL
? servers
: servers.filter((server) => server.status === status);
const message =
filteredServers.length > 0
? `Servers filtered by ${status} status`
: `No servers of ${status} status were found`;
subscriber.next({
...response,
message,
data: {
servers: filteredServers,
},
});
subscriber.complete();
}).pipe(tap(console.log), catchError(this.handleError));- Implement
filterServersByStatusinapp.component.ts- we will call
this.serverService.filterByStatus$(status, this.serversCopyDataSubject.value)with a copy of our data from our UI (namelythis.serversCopyDataSubject.value)- we don't want to overwrite the entire data when we filter! (therefore the original list with all servers will remain in the copy of
this.serversCopyDataSubject.valueand onlyappState$will be altered with missing Servers after filtering)
- we don't want to overwrite the entire data when we filter! (therefore the original list with all servers will remain in the copy of
- after the copy of our data has been filtered by
this.serverService.filterByStatus$method, the filtered list of servers will come as aresponse - with this response we will update our main
appState$
- we will call
// app.component.ts
filterServersByStatus(event: Event): void {
const statusValue: String = (event.target as HTMLInputElement).value;
const status: Status = Status[statusValue as keyof typeof Status];
this.appState$ = this.serverService
.filterByStatus$(status, this.serversCopyDataSubject.value)
.pipe(
map((response) => {
return {
dataState: DataState.LOADED_STATE,
appData: response,
};
}),
startWith({
dataState: DataState.LOADED_STATE,
appData: this.serversCopyDataSubject.value,
}),
catchError((error: string) => {
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}- Call
filterServersByStatusfromapp.component.html
<select
name="status"
(change)="filterServersByStatus($event)"
class="btn btn-primary"
title="Filter Severs by Status"
>
<option value="ALL">ALL SERVERS</option>
<option value="SERVER_UP">SERVERS UP</option>
<option value="SERVER_DOWN">SERVERS DOWN</option>
</select>We can also go a step further and retain the Status Filtered within our application UI using a BehaviorSubject: private statusSubject = new BehaviorSubject<Status>( Status['ALL' as keyof typeof Status] );. It might be useful later, since we now have a bug that: when we filter the list by status and we ping a server, we will show to the user all servers instead of keeping the filtered list as it was.
(Saturday, July 22, 2023, 15:37 - Radu-Alexandru Bulai)
Resources on issues encountered:
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 28
Since we are going to use Angular Forms (where user will submit a form with the new server details), we first need to import the FormsModule in app.module.ts:
// app.module.ts
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClientModule } from "@angular/common/http";
import { FormsModule } from "@angular/forms";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule, HttpClientModule, FormsModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}Implement Angular Add new server method and call it in UI:
- first call
this.serverService.addServer$that receivesaddServerForm.value(NgForm) which is a JSON object containing all the keys (HTML propertynamefor each<input>) and values when a user submits a form - the response from the Back-End REST Spring API will be the new server that has been created
- we will modify this response by adding the current servers in UI (copy of servers) and then appending the new server to the list
- after that, we reset the HTML Form that uses sees, and assign the false state for the
this.isServerLoadingSubject.next(false);variable (we will show a loading spinning icon while we wait for a response from the backend), and we can also close the modal on UI
// app.component.ts
onAddServer(addServerForm: NgForm): void {
this.isServerLoadingSubject.next(true);
this.appState$ = this.serverService
.addServer$(addServerForm.value as Server)
.pipe(
map((response) => {
const currentServers =
this.currentServersCopyDataSubject.value?.data?.servers || [];
this.currentServersCopyDataSubject.next({
...response,
data: {
servers: [...currentServers, response.data.server!],
},
});
addServerForm.resetForm({ status: this.Status.SERVER_DOWN });
this.isServerLoadingSubject.next(false);
document.getElementById('closeModal')?.click();
return {
dataState: DataState.LOADED_STATE,
appData: this.currentServersCopyDataSubject.value,
};
}),
startWith({
dataState: DataState.LOADED_STATE,
appData: this.currentServersCopyDataSubject.value,
}),
catchError((error: string) => {
this.isServerLoadingSubject.next(false);
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}In order to have a loading spinning icon right after the user adds the new server (and waits for a response from backend), we can:
- define a
isServerRequestLoadingSubject = new BehaviorSubject<boolean>(false);where we will keep a "state" of loading - on the very start of
onAddServermethod, we can set this variable to true, then set it back on false- when the request is completed and we received (and process) the response from backend server
- or when we receive an error
// app.component.ts
private isServerRequestLoadingSubject = new BehaviorSubject<boolean>(false);
readonly isServerRequestLoading$ = this.isServerRequestLoadingSubject.asObservable();- on HTML, we will use the observable of
isServerRequestLoadingSubjectto show/hide the "Add" button or "Saving..." disabled button along with a spinner icon
<!-- app.component.html -->
<!-- Add server Modal HTML -->
<div id="addServerModal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<form #addServerForm="ngForm" (ngSubmit)="onAddServer(addServerForm)">
<div class="modal-header">
<div class="modal-title">Add Server</div>
<button
type="button"
class="close"
data-dismiss="modal"
aria-hidden="true"
>
×
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>IP Address / FQDN</label>
<input
type="text"
ngModel
name="ipAddress"
class="form-control"
required
/>
</div>
<div class="form-group">
<label>Name</label>
<input
type="text"
ngModel
name="name"
class="form-control"
required
/>
</div>
<div class="form-group">
<label>Network</label>
<input
type="text"
ngModel
name="network"
class="form-control"
required
/>
</div>
<div class="form-group">
<label>Status</label>
<select
name="status"
ngModel="SERVER_DOWN"
class="form-control"
name="status"
required
>
<option value="SERVER_UP">SERVER UP</option>
<option value="SERVER_DOWN">SERVER DOWN</option>
</select>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn"
id="closeModal"
data-dismiss="modal"
>
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
[disabled]="addServerForm.invalid || (isServerLoading$ | async)"
>
<span *ngIf="!(isServerLoading$ | async)">Add</span>
<span *ngIf="isServerLoading$ | async">Saving...</span>
<i
*ngIf="isServerLoading$ | async"
class="fas fa-spinner fa-spin"
></i>
</button>
</div>
</form>
</div>
</div>
</div>(Sunday, July 23, 2023, 15:52)
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 30
Implement Angular Delete Server method and call it in UI
app.component.ts: implementonDeleteServermethod that will receive a server object as parameter (we will use this server to filter out our Servers Copy list that is currently displayed in UI) -> this is needed because the "Delete" method from SpringBoot backend does not send back the whole server that was just been deleted- once we retrieved the succesful resposne form backend, we filter out the server that has been deleted from our servers UI list
// app.component.ts
onDeleteServer(server: Server): void {
this.appState$ = this.serverService.deleteServerById$(server.id).pipe(
map((response) => {
this.currentServersCopyDataSubject.next({
...response,
data: {
servers:
this.currentServersCopyDataSubject.value.data.servers?.filter(
(serverToDelete) => serverToDelete.id !== server.id
),
},
});
return {
dataState: DataState.LOADED_STATE,
appData: this.currentServersCopyDataSubject.value,
};
}),
startWith({
dataState: DataState.LOADED_STATE,
appData: this.currentServersCopyDataSubject.value,
}),
catchError((error: string) => {
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}<!-- app.component.html -->
<table class="table table-hover" id="servers">
<thead>
<tr>
<th>#</th>
<th>Address</th>
<th>Name</th>
<th>Network</th>
<th>Status</th>
<th>Ping</th>
<th></th>
</tr>
</thead>
<tbody
*ngFor="
let server of appState.appData?.data?.servers;
let i = index
"
>
<tr>
<td title="Server ID: {{ server.id }}">{{ i + 1 }}</td>
<td>{{ server.ipAddress }}</td>
<td>{{ server.name }}</td>
<td>{{ server.network }}</td>
<td>
<span
class="badge"
[ngClass]="[
server.status === Status.SERVER_UP
? ' badge-success'
: ' badge-danger'
]"
>
{{ server.status === Status.SERVER_UP ? "SERVER UP" : "SERVER DOWN" }}
</span>
</td>
<td>
<a
class="edit"
style="cursor: pointer"
(click)="onPingServerByItsIpAddress(server.ipAddress)"
>
<i
*ngIf="
(ipAddressStatusWhenPinging$ | async) === '' ||
(ipAddressStatusWhenPinging$ | async) !== server.ipAddress
"
class="fa fa-tower-broadcast fa-1x"
title="Ping Server"
style="font-size: 1.5rem"
></i>
<i
*ngIf="
(ipAddressStatusWhenPinging$ | async) === server.ipAddress
"
class="fa fa-circle-notch fa-spin"
style="font-size: 1.5rem"
></i>
</a>
</td>
<td class="d-flex flex-row">
<a class="edit" data-toggle="modal" style="cursor: pointer"
><i
class="fa fa-pen fa-1x mx-2"
title="Delete Server"
style="font-size: 1.5rem"
></i
></a>
<a
(click)="onDeleteServer(server)"
class="delete"
data-toggle="modal"
style="cursor: pointer"
><i
class="fa fa-trash fa-1x mx-2"
title="Delete Server"
style="font-size: 1.5rem"
></i
></a>
</td>
</tr>
</tbody>
</table>This part is not included in the Main Tutorial of Full Stack Spring Boot RESTful API with MySQL and Angular
(Monday, July 24, 2023, 22:59 - Radu-Alexandru Bulai)
Since the edit modal will need access to the Server object that we want to edit (in order to populate the input fields): we either need to include the HTML modal with the edit <form> right inside the servers list. However, for a better code separation we can use a different approach:
- we can create a separate
openModalmethod where the "current to edit" server will be transmitted - we store the server locally in a separate
editServer$variable/observable (currently inapp.component.ts) - we can access the
editServer$server anywhere inapp.component.html
The onOpenModal function (in app.component.ts) will have a "modalMode" parameter what will determine which modal will be opened: onOpenModal(server: Server, modalMode: String): void {}
- in
onOpenModalwe will create a button (by default, when we createElement button, its default type is "type=submit", but we can change it to "type=button") - the button attribute
data-togglewill be "modal" (needed for Bootstrap4) - the button attribute
data-targetwill be dynamic (received from the function parameter) - we want to add these buttons (that opens a specific modal) dinamically to our UI (
app.component.html): we can first add an ID to the container div in HTML:<div class="container" id="main-container"> - get the container in
app.component.ts:const container = document.getElementById('main-container'); - append the created button to the div container (in DOM) and click it:
container?.appendChild(button); button.click();
// app.component.ts
private editServerSubject = new BehaviorSubject<Server>(null!);
readonly editServer$ = this.editServerSubject.asObservable();
...
onOpenModal(server: Server, modalMode: String): void {
const container = document.getElementById('main-container');
const button = document.createElement('button');
button.type = 'button';
button.style.display = 'none';
button.setAttribute('data-toggle', 'modal');
if (modalMode == 'edit') {
this.editServerSubject.next(server);
button.setAttribute('data-target', '#editServerModal');
}
container?.appendChild(button);
button.click();
button.remove();
}For the onUpdateServer method:
this.isServerRequestLoadingSubject.next(true): sets the value of the isServerRequestLoadingSubject subject to true to indicate that a server update request is currently in progress (until we get a response from backend).this.appState$ = this.serverService.updateServer$(updateServerForm.value as Server).pipe(...);: calls theupdateServer$method from theserverServiceand pipes the resulting observable through various operators likemap,startWith, andcatchError- Inside the
mapoperator, the function processes the server update response:- update the
currentServersCopyDataSubjectwith the updated server data and set theisServerRequestLoadingSubjecttofalseafter the update operation completes (we will show a loading spinning icon while we wait for a response from the backend) document.getElementById('closeEditModal')?.click();: close the edit modal after the server update operation is completed- Note that unlike "adding" action, we do not need to reset the "edit" HTML form sincethe form will always be populated with selected server's values
- update the
- Finally, the
mapoperator returns an object with thedataStateset toDataState.LOADED_STATEandappDataset to the value ofcurrentServersCopyDataSubject
// app.component.ts
onUpdateServer(updateServerForm: NgForm): void {
this.isServerRequestLoadingSubject.next(true);
this.appState$ = this.serverService
.updateServer$(updateServerForm.value as Server)
.pipe(
map((response) => {
const currentServers =
this.currentServersCopyDataSubject.value?.data?.servers || [];
const indexOfUpdatedServer = currentServers!.findIndex(
(server) => server.id === response.data.server!.id
);
currentServers![indexOfUpdatedServer] = response.data.server!;
this.isServerRequestLoadingSubject.next(false);
document.getElementById('closeEditModal')?.click();
return {
dataState: DataState.LOADED_STATE,
appData: this.currentServersCopyDataSubject.value,
};
}),
startWith({
dataState: DataState.LOADED_STATE,
appData: this.currentServersCopyDataSubject.value,
}),
catchError((error: string) => {
this.isServerRequestLoadingSubject.next(false);
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}In app.component.html
- Each input field will have its value populated (by default) with the current server's data that can be retrieved by the
editServer$observable:ngModel="{{ (editServer$ | async)?.name }}" - We will use the same observable of
isServerRequestLoadingSubjectto show/hide the "Update" button or "Updating..." disabled button along with a loading spinner icon - Note that we will need to have a hidden input (of server's id) in order to have a complete Server object (without missing any properties) for when we will call the updateEmployee method with the UPDATE (PUT) request to our backend SpringBoot API:
<input type="hidden" ngModel="{{ (editServer$ | async)?.id }}" name="id" />
<!-- app.component.html -->
<a
(click)="onOpenModal(server, 'edit')"
class="edit"
data-toggle="modal"
style="cursor: pointer"
><i
class="fa fa-pen fa-1x mx-2"
title="Edit Server"
style="font-size: 1.5rem"
></i
></a>
...
<!-- Update server Modal HTML -->
<div id="editServerModal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<form
#editServerForm="ngForm"
(ngSubmit)="onUpdateServer(editServerForm)"
>
<div class="modal-header">
<div class="modal-title">
Edit Server "{{ (editServer$ | async)?.name }}"
</div>
<button
type="button"
class="close"
data-dismiss="modal"
aria-hidden="true"
>
×
</button>
</div>
<div class="modal-body">
<input
type="hidden"
ngModel="{{ (editServer$ | async)?.id }}"
name="id"
/>
<div class="form-group">
<label>IP Address / FQDN</label>
<input
type="text"
ngModel="{{ (editServer$ | async)?.ipAddress }}"
name="ipAddress"
class="form-control"
required
/>
</div>
<div class="form-group">
<label>Name</label>
<input
type="text"
ngModel="{{ (editServer$ | async)?.name }}"
name="name"
class="form-control"
required
/>
</div>
<div class="form-group">
<label>Network</label>
<input
type="text"
ngModel="{{ (editServer$ | async)?.network }}"
name="network"
class="form-control"
/>
</div>
<div class="form-group">
<label>Status</label>
<select
name="status"
ngModel="{{ (editServer$ | async)?.status }}"
class="form-control"
name="status"
required
>
<option value="SERVER_UP">SERVER UP</option>
<option value="SERVER_DOWN">SERVER DOWN</option>
</select>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn"
id="closeEditModal"
data-dismiss="modal"
>
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
[disabled]="
editServerForm.invalid || (isServerRequestLoading$ | async)
"
>
<span *ngIf="!(isServerRequestLoading$ | async)">Update</span>
<span *ngIf="isServerRequestLoading$ | async">Updating...</span>
<i
*ngIf="isServerRequestLoading$ | async"
class="fas fa-circle-notch fa-spin"
></i>
</button>
</div>
</form>
</div>
</div>
</div>(Saturday, July 29, 2023, 23:39)
// server.service.ts
searchServersByKeyword$ = (keyword: String, response: CustomResponse) =>
<Observable<CustomResponse>>new Observable<CustomResponse>((subscriber) => {
const currentServers = response.data?.servers || [];
const searchedText = keyword.toLowerCase();
const resultedServers: Server[] = currentServers.filter(
(server) =>
server.name.toLowerCase().indexOf(searchedText) !== -1 ||
server.ipAddress.toLowerCase().indexOf(searchedText) !== -1 ||
server.network.toLowerCase().indexOf(searchedText) !== -1
);
// --- Older less eficient method ---
// let resultedServers: Server[] = [];
// for (const server of currentServers) {
// if (
// server.name.toLowerCase().indexOf(searchedText) !== -1 ||
// server.ipAddress.toLowerCase().indexOf(searchedText) !== -1 ||
// server.network.toLowerCase().indexOf(searchedText) !== -1
// ) {
// resultedServers.push(server);
// }
// }
subscriber.next({
...response,
message: "Servers resulted by search",
data: {
servers: resultedServers,
},
});
subscriber.complete();
}).pipe(tap(console.log), catchError(this.handleError));// app.components.ts or servers.component.ts
onSearchServers(keyword: String): void {
this.appState$ = this.serverService
.searchServersByKeyword$(
keyword,
this.currentServersCopyDataSubject.value
)
.pipe(
map((response) => {
return {
dataState: DataState.LOADED_STATE,
appData: response,
};
}),
startWith({
dataState: DataState.LOADED_STATE,
appData: this.currentServersCopyDataSubject.value,
}),
catchError((error: string) => {
return of({ dataState: DataState.ERROR_STATE, error });
})
);
}<!-- app.components.html or servers.component.html -->
<form class="form-inline">
<input
(ngModelChange)="onSearchServers(keyword.value)"
#keyword="ngModel"
ngModel
name="keyword"
type="search"
id="searchName"
class="form-control"
style="border: none !important"
title="Search servers by name, address or network"
placeholder="Search servers..."
/>
</form>
Refactoring...
Refactor: split app.component in separate servers & header components
- Create server and header components with
ng generate component components/headerandng generate component components/servers- Move all properties and methods from app.component.ts to servers.component.ts + Edit onOpenModal method to have additional "add" modalMode (and add click event listener on "Add Server" button from the header/navbar)
- Move all HTML from app.component.html to servers.component.html
- Within "header.component.ts" Pass/Inject the serversComponent: ServersComponent in constructor + Implement own onOpenModal method that calls this.serversComponent.onOpenModal(server, modalMode);
- Move the
<nav>element from app.component.html/servers.component.html to header.component.html- In "app.module.ts" Add
providers: [ServersComponent]in order to fix "NullInjectorError: No provider for ServersComponent!" error(Wednesday, July 26, 2023, 23:47 - Radu-Alexandru Bulai)
(Saturday, July 29, 2023, 13:25)
Reuse of personal notes
- from https://github.com/radualexandrub/Study/blob/master/Angular/README.md#angular-router
- and commit from previous EmployeeManager application https://github.com/radualexandrub/SpringBoot-Angular-EmployeeManagerApp/commit/572dbe053a617cd1f11730a9daee5444494adb67#diff-5468d09e26643f32f6d6f60deb0960d87f4f5bd8e6da893a27453e3f9ccd9116
1) Create About page by running ng generate component components/pages/aboutPage
2) In app.module.ts
- import "Routes", define "appRoutes" where routes are specified
- add
imports: [RouterModule.forRoot(appRoutes, { enableTracing: false })](this should be set on true for debugging purposes and false in production)
// app.module.ts
const appRoutes: Routes = [
{ path: "", component: ServersComponent },
{ path: "about", component: AboutPageComponent },
];
...
@NgModule({
declarations: [
AppComponent,
FooterComponent,
ServersComponent,
HeaderComponent,
AboutPageComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
FormsModule,
RouterModule.forRoot(appRoutes, { enableTracing: false }),
],
providers: [ServersComponent],
bootstrap: [AppComponent],
})3) In app.component.html, instead of <app-servers> component we add <router-outlet>
<!-- app.component.html -->
<app-header></app-header>
<router-outlet></router-outlet>
<app-footer></app-footer>4) Add contents to about-page.component.html
5) In header.component.ts
- make sure "private router: Router" is passed in contructor
- make sure "hasRoute" and "hasRouteIncluded" methods are defined
// header.component.ts
constructor(
private serversComponent: ServersComponent,
private router: Router
) {}
...
hasRoute(route: string): boolean {
return this.router.url === route;
}
hasRouteIncluded(route: string): boolean {
return this.router.url.includes(route);
}6) In header.component.html
- add
*ngIf="hasRoute('/')"on "Add Server" button to hide it from navbar when we are in "About" page - add
*ngIf="hasRoute('/about')"to show Servers(/) link when in "About" page - add
[ngClass]="{ active: hasRoute('/about') }"to show "About" link page as active
Dockerize Application with docker-compose (Angular, Spring Boot, MySQL)
- Create and configure "Angular.Dockerfile"
- Note that we use "outputPath": "dist/out" configured in "angular.json" as Angular build location
- Create .dockerignore
- Create and configure "Spring.Dockerfile"
- Note that we use Java 17 (FROM openjdk:17-jdk-slim)
- Create and configure docker-compose.yml (contains MySQL Server image)
- For MySQL, the db name from mysql-db service (MYSQL_DATABASE: pingstatustracker) should be reflected under the Back-End Spring Service property of SPRING_DATASOURCE_URL, as well as username and password
- Create environment.prod.ts Add Angular env variables from development environment.ts
- Create nginx-custom.con Add 'Access-Control-Allow-Origin' headers for Nginx Server (where browser clients will connect to) for CORS
Endpoints:
- Angular will run on http://localhost:8081/
- SpringBoot API will run on http://localhost:8080/api/servers
- MySQL will run on port 3306
- Note: You can enter the MySQL docker container by running
docker ps -a, then copy the container ID, then rundocker exec -it containerID mysql -uradu -pradu123456 -D pingstatustracker-- e.g. for running queries directly on db:INSERT INTO server (id, ip_address, name, network) VALUES (3, '192.168.0.1', 'Server 1', 'Office');Resources used: - https://github.com/radualexandrub/SpringBoot-Angular-EmployeeManagerApp/commit/7ecee0943a11e0d10c4cde6a238feae752fbb249 - https://www.javachinna.com/angular-nginx-spring-boot-mysql-docker-compose/ (Monday, July 31, 2023, 23:27 - Radu-Alexandru Bulai)
(Saturday, August 26, 2023, 10:18)
https://www.npmjs.com/package/angular-notifier - A well designed, fully animated, highly customizable, and easy-to-use notification library for your Angular 2+ application.
Step 1) Install the angular-notifier library using npm i angular-notifier.
Step 2) In app.module.ts, import and add to imports: [] the BrowserAnimationsModule - import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
Step 3) We will turn the above Angular Notifier library into a module which will be bringed to app.module.ts.
- Create in Angular
/src/appdirectory a file callednotification.module.ts(could be a copy paste ofapp.module.ts) - Add angular-notifier's options
const notifierDefaultOptions: NotifierOptions = {}
// notification.module.ts
import { NgModule } from "@angular/core";
import { NotifierModule, NotifierOptions } from "angular-notifier";
const customNotifierOptions: NotifierOptions = {
position: {
horizontal: {
position: "right",
distance: 12,
},
vertical: {
position: "top",
distance: 12,
gap: 10,
},
},
theme: "material",
behaviour: {
autoHide: 3000,
onClick: false,
onMouseover: "pauseAutoHide",
showDismissButton: true,
stacking: 4,
},
animations: {
enabled: true,
show: {
preset: "slide",
speed: 300,
easing: "ease",
},
hide: {
preset: "fade",
speed: 300,
easing: "ease",
offset: 50,
},
shift: {
speed: 300,
easing: "ease",
},
overlap: 150,
},
};
@NgModule({
imports: [NotifierModule.withConfig(customNotifierOptions)],
exports: [NotifierModule],
})
export class NotificationModule {}Step 4) In app.module.ts, import the above created module import { NotificationModule } from './notification.module'; and add it to imports: []
Step 5) In styles.css, import the styles from the module (using ~): @import "~angular-notifier/styles"; ... or just use @import "../node_modules/angular-notifier/styles.css";
Step 6) In app.component.html we can simply add <notifier-container></notifier-container>
Step for demo only) In app.component.ts (or servers.component.ts for refactored code)
- in the
constructor, add/inject as parameterprivate notifier: NotifierService - we can add
this.notifier.notify('success', 'You are awesome! I mean it!');on a function likengOnInit()for a demo example - Or add
this.notifier.notify('success', "Servers filtered by ${statusValue}");inonFilterServersByStatus()method
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 34
Step 7) Under /src/app/services, we can create our own notification.service.ts (or run CLI command ng generate service services/notification)
- Logic is: our
notification.service.tswill have direct methods to call each type of message, e.g. instead of calling (in ourapp.component.ts) anotifierService.notify('success', 'Message for success'), we would call anotificationService.onSuccess('Message for success').
// notification.service.ts
import { Injectable } from "@angular/core";
import { NotifierService } from "angular-notifier";
@Injectable({
providedIn: "root",
})
export class NotificationService {
private readonly notifier: NotifierService;
constructor(notifierService: NotifierService) {
this.notifier = notifierService;
}
onDefaultMessage(message: string) {
this.notifier.notify(Type.DEFAULT, message);
}
onInfoMessage(message: string) {
this.notifier.notify(Type.INFO, message);
}
onSuccessMessage(message: string) {
this.notifier.notify(Type.SUCCESS, message);
}
onWarningMessage(message: string) {
this.notifier.notify(Type.WARNING, message);
}
onErrorMessage(message: string) {
this.notifier.notify(Type.ERROR, message);
}
}
enum Type {
DEFAULT = "default",
INFO = "info",
SUCCESS = "success",
WARNING = "warning",
ERROR = "error",
}Step 8) In app.component.ts (or servers.component.ts for refactored code):
import { NotificationService } from 'src/app/services/notification.service';- in the
constructor, add/inject as parameterprivate notification: NotificationService - under each success method, we can call
this.notification.onSuccessMessage("A success message");
Full Stack Spring Boot RESTful API with MySQL and Angular | RxJs State Management - Part 35
Performance Advantages:
-
Default (Procedural Approach): In the default approach, Angular uses the
checkAlwaysstrategy, where it constantly checks for changes in components and updates the UI. This means that Angular will continuously monitor the application's state and trigger UI updates whenever there's a potential change / Constantly watching if every variable changes. -
OnPush (Reactive Approach): With the OnPush change detection strategy, Angular takes a more optimized approach. It only looks for changes when certain triggers occur:
- When an
@Inputproperty changes (typically initiated by user interaction). - When
events are emitted(for example, through event binding). - When
observables emit new values(used extensively in this project).
- When an
By using the OnPush strategy, Angular reduces the frequency of UI updates, leading to improved performance and responsiveness. This is particularly beneficial when dealing with complex and data-intensive applications.
// Example of forcing OnPush strategy in Angular:
// app.component.ts
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
changeDetection: ChangeDetectionStrategy.OnPush,
// This will force Angular to only detect changes based on specific triggers
// such as @Input changes, event emissions, or observable emissions
})
export class AppComponent {
// If this variable changes its value over time,
// the changes will not be automatically detected
name = "Radu";
}By explicitly applying the OnPush change detection strategy to a component, as shown in the example, Angular restricts change detection to specific triggers. This can significantly enhance performance by reducing unnecessary UI updates and improving the efficiency of rendering operations.
Here's an extended example of how the OnPush strategy works with different triggers like @Input changes, event emissions, and observable emissions:
- The
titleproperty is marked with@Input(), which means it can change based on external changes (typically from a parent component). - An observable (
data$) is created using aBehaviorSubjectto simulate an asynchronous data source. The value of this observable changes over time. - The
buttonClicksproperty is used to track how many times a button is clicked. This is a simple event emission scenario.
By using the OnPush change detection strategy, Angular will only perform change detection when any of these triggers are activated:
- When the
titleproperty, marked as@Input(), changes from its parent component. - When the observable (
data$) emits a new value. - When the button is clicked and the
buttonClicksvalue changes.
import { Component, Input, ChangeDetectionStrategy } from "@angular/core";
import { BehaviorSubject } from "rxjs";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
// @Input change trigger
@Input() title: string = "";
// Observable emissions trigger
dataSubject = new BehaviorSubject<string>("Initial Value");
data$ = this.dataSubject.asObservable();
// Event emissions trigger
buttonClicks = 0;
constructor() {
// Simulating changes over time
setInterval(() => {
// Update the Observable value
this.dataSubject.next("Updated Value");
// Increment button clicks
this.buttonClicks++;
}, 1000);
}
}This approach minimizes the frequency of change detection cycles and results in better performance, as Angular avoids unnecessary re-renders when no relevant triggers are activated.
Building an app in Angular using the Reactive approach versus the Procedural approach involves different ways of structuring and managing the codebase, particularly when dealing with asynchronous operations, state management, and data flow.
1. Procedural Approach: In the Procedural approach, you typically handle asynchronous operations and state changes using callbacks, promises, and imperative programming techniques. This approach is more sequential and involves writing code that explicitly defines the steps to be taken.
- Code Structure: Code tends to be organized around actions and events. You might have multiple callbacks or nested promises to manage asynchronous operations.
- State Management: State changes are handled explicitly by updating variables or calling functions in response to events.
- Data Flow: Data flow can be more complex to manage, as you need to ensure that the right data is available at the right time.
- Error Handling: Error handling can involve try-catch blocks or explicit checks for error conditions.
Advantages of the Procedural Approach:
- Familiarity: If developers are more comfortable with imperative programming, the procedural approach might feel more familiar.
- Simplicity for Simple Scenarios: For simpler use cases, a procedural approach might require less overhead than setting up and managing observables.
2. Reactive Approach: The Reactive approach utilizes observables and reactive programming principles to manage asynchronous operations and state changes. This approach is more declarative and focuses on describing how the data should behave over time.
- Code Structure: Code is organized around data streams and how they transform over time. Observables are used to represent these streams of data.
- State Management: State changes are managed by observing streams of data and reacting to changes using operators like
map,filter, andmerge.- Data Flow: Data flow is more streamlined, as observables provide a structured way to handle asynchronous data.
- Error Handling: Error handling is built into the observable pipeline, allowing you to catch and handle errors more effectively.
Advantages of the Reactive Approach:
- Declarative: Reactive code is more declarative, making it easier to understand the data flow and transformations.
- Streamlined Data Flow: Observables provide a clear way to handle asynchronous data flow and updates.
- Composable: Operators allow you to compose and manipulate data streams in a modular way.
- Error Handling: Observables have built-in error handling capabilities, making it easier to handle errors.
Choosing between these approaches depends on factors like the complexity of your application, the team's familiarity with reactive programming, and the level of control you need over asynchronous operations and data flow.
In recent years, the Reactive approach has gained popularity for its ability to handle complex asynchronous scenarios more elegantly. It also aligns well with modern frontend patterns and is often used in combination with other technologies like Redux or NgRx for state management in Angular applications.
The statement "Reactive programming aligns well with modern frontend patterns" refers to how reactive programming principles fit naturally with some of the common patterns and practices used in modern front-end development. Here are a few patterns and concepts that are closely related to reactive programming:
- Component-Based Architecture: Modern front-end development often follows a component-based architecture, where UI components are self-contained and reusable. Reactive programming's focus on data flow and isolation fits well with the idea of managing component state and interactions using observables.
- State Management: Reactive programming is used extensively in state management solutions like Redux, NgRx (for Angular), and MobX. These libraries utilize observables to manage application state in a predictable and efficient manner. Reactive programming's emphasis on data streams aligns with the need to manage and propagate state changes across the application.
- Event-Driven Programming: In reactive programming, observables are a natural fit for representing and handling events in the application. Modern frontend patterns often involve handling various user interactions, network requests, and other events. Reactive programming simplifies managing these asynchronous event streams.
- UI Reactivity: Reactive programming helps achieve responsive and dynamic user interfaces. Changes in data are automatically propagated to the UI components that depend on that data. This aligns with the goal of creating interactive and reactive user interfaces.
- Data Transformation and Pipelines: Modern front-end applications deal with complex data transformations, filtering, mapping, and combining data from multiple sources. Reactive programming's operators provide a powerful and composable way to perform these operations on data streams.
- Asynchronous Operations: Web applications often involve asynchronous operations like API calls, user interactions, timers, and more. Reactive programming provides a structured and unified approach to handle these asynchronous operations and their outcomes.
- Real-Time Applications: Applications that require real-time updates, such as collaborative editing, chat applications, or live data dashboards, can benefit from reactive programming's ability to handle continuous data streams and updates.
- Functional Programming: Reactive programming shares some functional programming concepts, such as immutability, pure functions, and transformations. Modern front-end development often embraces functional programming principles for code organization and maintainability. While not all modern front-end patterns explicitly require reactive programming, its principles align with the demands of building interactive, responsive, and data-driven web applications. It offers a more structured and elegant way to manage complex asynchronous operations and state changes, which is crucial in today's complex front-end landscapes.
(Sunday, August 27, 2023, 12:16)






























