Spring Boot + Hazelcast Hello World Example
In a previous tutorial we saw what is Hazelcast and it's architecture. In this tutorial we will be implementing a hello world example using Spring Boot and Hazelcast. We will be creating a Spring Boot application to save, retrieve and delete User Accounts for a Banking Application. We will be making use of mysql database and hazelcast.
To better understand the need for hazelcast we will be creating the application in following steps.
- First we will create a monolithic Spring Boot Application
- We will then be implementing local cache using HashMap
- We will then deploy the Spring Boot Application as a distributed system
- Finally we will be making use of Hazelcast for our Spring Boot Application
Video Tutorial
-
Monolithic Spring Boot Application -
This will be the standard directory layout for maven project structure-
We need to start by creating a Maven pom.xml(Project Object Model) file. The pom.xml file contains the project configuration details.<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.codeusingjava</groupId> <artifactId>boot-hazel</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies> <!-- This configuration is needed to run spring boot application as a jar --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.codeusingjava.BankApplication</mainClass> </configuration> </plugin> </plugins> </build> </project>
package com.codeusingjava.model; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "user") public class UserAccount { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "accountNumber") private String accountNumber; @Column(name = "name") private String name; @Column(name = "address") private String address; @Column(name = "balance") private long balance; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public long getBalance() { return balance; } public void setBalance(long balance) { this.balance = balance; } public String getAccountNumber() { return accountNumber; } public void setAccountNumber(String accountNumber) { this.accountNumber = accountNumber; } }
Create the repository by extending the JpaRepository. By extending JpaRepository we get a bunch of generic CRUD methods into our type that allows saving , deleting and retrieving Users.package com.codeusingjava.db; import org.springframework.data.jpa.repository.JpaRepository; import com.codeusingjava.model.UserAccount; public interface UserRepository extends JpaRepository<UserAccount, Long> { UserAccount findByAccountNumber(String accountNumber); UserAccount deleteByAccountNumber(String accountNumber); }
Create the RestController for performing UserAccount operations. We will be autowiring the UserRepository and use it to perform database operations.package com.codeusingjava.controller; import java.util.List; import java.util.Map; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.codeusingjava.db.UserRepository; import com.codeusingjava.model.UserAccount; @RestController @RequestMapping(path = "users") public class UserController { @Autowired private UserRepository userRepository; @GetMapping(path = { "/get/{accountNumber}" }) public UserAccount getUser(@PathVariable("accountNumber") String accountNumber) { return userRepository.findByAccountNumber(accountNumber); } @PostMapping("/add") public void createUser(@RequestBody UserAccount user) { userRepository.save(user); } @DeleteMapping(path = { "/{accountNumber}" }) public UserAccount deleteUser(@PathVariable("accountNumber") String accountNumber) { return userRepository.deleteByAccountNumber(accountNumber); } }
package com.codeusingjava; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BankApplication { public static void main(String[] args) { SpringApplication.run(BankApplication.class, args); } }
Finally create the application.properties file with database properties as follows-spring.datasource.url=jdbc:mysql://localhost/bank?createDatabaseIfNotExist=true&autoReconnect=true&useSSL=false spring.datasource.username=root spring.datasource.password=root spring.datasource.platform=mysql spring.datasource.initialization-mode=always spring.jpa.hibernate.ddl-auto=update spring.jackson.serialization.fail-on-empty-beans=false server.port=8081
Start the Spring Boot Application and perform the insert/retrieve and delete operations using Advanced Rest Client.
-
We will be implementing a simple local cache for better performance
In the SpringBoot Main class create a bean of type Map-package com.codeusingjava; import java.util.HashMap; import java.util.Map; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import com.codeusingjava.model.UserAccount; @SpringBootApplication public class BankApplication { public static void main(String[] args) { SpringApplication.run(BankApplication.class, args); } @Bean public Map<String, UserAccount> accountMap() { return new HashMap<>(); } }
In the controller class autowire the accountMap and use it as a cache-package com.codeusingjava.controller; import java.util.List; import java.util.Map; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.codeusingjava.db.UserRepository; import com.codeusingjava.model.UserAccount; @RestController @RequestMapping(path = "users") public class UserController { @Autowired private UserRepository userRepository; @Autowired private Map<String, UserAccount> accountMap; @GetMapping(path = { "/get/{accountNumber}" }) public UserAccount getUser(@PathVariable("accountNumber") String accountNumber) { //first check if accountMap has the userAccount details, if yes then return it. Else fetch it from database. UserAccount userAccount = (accountMap.get(accountNumber) != null) ? accountMap.get(accountNumber) : userRepository.findByAccountNumber(accountNumber); return userAccount; } @PostMapping("/add") public void createUser(@RequestBody UserAccount user) { //save user account in cache accountMap.put(user.getAccountNumber(), user); userRepository.save(user); } @DeleteMapping(path = { "/delete/{accountNumber}" }) public UserAccount deleteUser(@PathVariable("accountNumber") String accountNumber) { //remove from both cache and database accountMap.remove(accountNumber); return userRepository.deleteByAccountNumber(accountNumber); } }
-
We will now be deploying the application as a distributed microservice for better load balancing-
Open command prompt and start the application on different ports- Use the command -java -Dserver.port='port-number' -jar boot-hazel-0.0.1-SNAPSHOT.jar
If we now suppose start the application on port 8081, 8082,8083
We now insert multiple bank accounts using port 8081. If we now try to retrieve the bank account details using application running on port 8082 and 8083 then they will not be having the bank details in the accountMap Cache and will need to fetch it from database. -
We will now be making use of Hazelcast cache
In this tutorial we are making use of spring boot version 2.2.5.RELEASE. The managed hazelcast version provided by spring boot is 3.12.6. So we will be downloading the hazelcast-management-center-3.12.6 Go to Hazelcast downloads page and download hazelcast management center 3.12.6
Unzip it and start the bat file-
If we now go to localhost:8080 we see the management console. Currently there are no hazelcast instances.
We will now modify the pom.xml to add the hazelcast dependency.<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.codeusingjava</groupId> <artifactId>boot-hazel</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.hazelcast</groupId> <artifactId>hazelcast</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.codeusingjava.BankApplication</mainClass> </configuration> </plugin> </plugins> </build> </project>
package com.codeusingjava; import java.util.HashMap; import java.util.Map; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.hazelcast.config.Config; import com.hazelcast.config.ManagementCenterConfig; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.IMap; import com.codeusingjava.model.UserAccount; @SpringBootApplication @Configuration public class BankApplication { public static void main(String[] args) { SpringApplication.run(BankApplication.class, args); } /* * @Bean public Map<String, UserAccount> accountMap() { return new * HashMap<>(); } */ @Bean public Config hazelCastConfig() { return new Config().setManagementCenterConfig( new ManagementCenterConfig().setEnabled(true).setUrl("http://localhost:8080/hazelcast-mancenter")); } @Bean public HazelcastInstance hazelcastInstance(Config hazelCastConfig) { return Hazelcast.newHazelcastInstance(hazelCastConfig); } @Bean public Map<String, UserAccount> accountMap(HazelcastInstance hazelcastInstance) { return hazelcastInstance.getMap("accountMap"); } }
Any object that we are making use for the Hazelcast IMap should be serializable. So we need to make changes to the UserAccount class to make it serializable.package com.codeusingjava.model; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "user") public class UserAccount implements Serializable { private static final long serialVersionUID = 1L; @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "accountNumber") private String accountNumber; @Column(name = "name") private String name; @Column(name = "address") private String address; @Column(name = "balance") private long balance; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public long getBalance() { return balance; } public void setBalance(long balance) { this.balance = balance; } public String getAccountNumber() { return accountNumber; } public void setAccountNumber(String accountNumber) { this.accountNumber = accountNumber; } }