DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • High-Performance Reactive REST API and Reactive DB Connection Using Java Spring Boot WebFlux R2DBC Example
  • (Spring) Booting Java To Accept Digital Payments With USDC
  • Spring Boot, Quarkus, or Micronaut?
  • CRUD REST API With Jakarta Core Profile Running on Java SE

Trending

  • Optimizing Integration Workflows With Spark Structured Streaming and Cloud Services
  • Power BI Embedded Analytics — Part 2: Power BI Embedded Overview
  • Mastering Advanced Aggregations in Spark SQL
  • How the Go Runtime Preempts Goroutines for Efficient Concurrency
  1. DZone
  2. Coding
  3. Languages
  4. Exploring Hazelcast With Spring Boot

Exploring Hazelcast With Spring Boot

Discussing distributed caching with Hazelcast and Spring Boot, distributed locks, and user code deployment to invoke code execution via Hazelcast on a service.

By 
Ion Pascari user avatar
Ion Pascari
·
Dec. 30, 22 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
11.5K Views

Join the DZone community and get the full member experience.

Join For Free

For the use cases I am going to describe here, I have two services: courses-service and reviews-service:

  1. Courses-service provides CRUD operations for dealing with courses and instructors.
  2. Reviews-service is another CRUD operations provider for dealing with reviews for courses that are completely agnostic of courses from courses-service. 

Both apps are written in Kotlin using Spring Boot and other libraries. Having these two services, we are going to discuss distributed caching with Hazelcast and Spring Boot and see how we can use user code-deployment to invoke some code execution via Hazelcast on a service. 

Spoiler alert: The examples/use cases presented here are designed purely for the sake of demonstrating integration with some of Hazelcast’s capabilities. The discussed problems here can be solved in various ways and maybe even in better ways, so don’t spend too much on thinking, “why?” So, without further ado, let’s dive into code.

Note: here is the source code in case you want to follow along.

Simple Distributed Caching

We’ll focus on courses-service for now. Having this entity:

Kotlin
 
@Entity
@Table(name = "courses")
class Course(
    var name: String,
    @Column(name = "programming_language")
    var programmingLanguage: String,
    @Column(name = "programming_language_description", length = 3000, nullable = true)
    var programmingLanguageDescription: String? = null,
    @Enumerated(EnumType.STRING)
    var category: Category,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "instructor_id")
    var instructor: Instructor? = null
) : AbstractEntity() {

    override fun toString(): String {
        return "Course(id=$id, name='$name', category=$category)"
    }
}


And this method in CourseServiceImpl:

Kotlin
 
@Transactional
override fun save(course: Course): Course {
    return courseRepository.save(course)
}


I want to enhance every course that is saved with a programming language description for the programming language that has been sent by the user. For this, I created a Wikipedia API client that will make the following request every time a new course is added:

Plain Text
 
GET https://en.wikipedia.org/api/rest_v1/page/summary/java_(programming_language)


So, my method looks like this now:

Kotlin
 
@Transactional
override fun save(course: Course): Course {
    enhanceWithProgrammingLanguageDescription(course)
    return courseRepository.save(course)
}

private fun enhanceWithProgrammingLanguageDescription(course: Course) {
    wikipediaApiClient.fetchSummaryFor("${course.programmingLanguage}_(programming_language)")?.let { course.programmingLanguageDescription = it.summary }
}


Now here comes our use case, we want to cache the Wikipedia response so we don’t call every single time. Our courses will be mostly oriented to a set of popular programming languages like Java, Kotlin, C#, and popular programming languages. We also don’t want to decrease our save()’s performance by querying every time for mostly the same language. Also, this can act as a guard in case the API server is down.

Time to introduce Hazelcast!

Hazelcast is a distributed computation and storage platform for consistently low-latency querying, aggregation and stateful computation against event streams and traditional data sources. It allows you to quickly build resource-efficient, real-time applications. You can deploy it at any scale from small edge devices to a large cluster of cloud instances.

You can read about lots of places where Hazelcast is the appropriate solution and all the advantages on their homepage.

When it comes to integrating a Spring Boot app with Hazelcast (embedded), it is straightforward. There are a few ways of configuring Hazelcast, via XML, YAML, or the programmatic way. Also, there is a nice integration with Spring Boot’s cache support via @EnableCaching and @Cacheable annotations. I picked the programmatic way of configuring Hazelcast and the manual way of using it—a bit more control and less magic.

Here are the dependencies:

Kotlin
 
implementation("com.hazelcast:hazelcast:5.2.1")
implementation("com.hazelcast:hazelcast-spring:5.2.1")

 

And here is the configuration that we are going to add to courses-service:

Kotlin
 
@Configuration
class HazelcastConfiguration {

    companion object {
        const val WIKIPEDIA_SUMMARIES = "WIKIPEDIA_SUMMARIES"
    }

    @Bean
    fun managedContext(): SpringManagedContext {
        return SpringManagedContext()
    }


    @Bean
    fun hazelcastConfig(managedContext: SpringManagedContext): Config {
        val config = Config()
        config.managedContext = managedContext
        config.networkConfig.isPortAutoIncrement = true
        config.networkConfig.join.multicastConfig.isEnabled = true
        config.networkConfig.join.multicastConfig.multicastPort = 5777
        config.userCodeDeploymentConfig.isEnabled = true
        config.configureWikipediaSummaries()
        return config
    }



    private fun Config.configureWikipediaSummaries() {
        val wikipediaSummaries = MapConfig()
        wikipediaSummaries.name = WIKIPEDIA_SUMMARIES
        wikipediaSummaries.isStatisticsEnabled = true
        wikipediaSummaries.backupCount = 1
        wikipediaSummaries.evictionConfig.evictionPolicy = EvictionPolicy.LRU
        wikipediaSummaries.evictionConfig.size = 10000
        wikipediaSummaries.evictionConfig.maxSizePolicy = MaxSizePolicy.PER_NODE

        addMapConfig(wikipediaSummaries)
    }
}


So we declare a managedContext() bean, which is a container-managed context initialized with a Spring context implementation that is going to work along with the @SpringAware annotation to allow us to initialize/inject fields in deserialized instances. We’ll take a look at why we need this later when we discuss user code deployment.

Then, we declare a hazelcastConfig() bean, which represents the brains of the whole integration. We set the managedContext, enable user code deployment, and set the networkConfig’s join option to “multicast.” Basically, the NetworkConfig is responsible for defining how a member will interact with other members or clients—there are multiple parameters available for configuration like port, isPortAutoIncrement, sslConfig, restApiConfig, joinConfig, and others. We configured the isPortAutoIncrement to true to allow hazelcastInstance to auto-increment the port if the picked one is already in use until it runs out of free ports. Also, we configured the JoinConfig, which contains multiple member/client join configurations like Eureka, Kubernetes, and others. We enable MulticastConfig, which allows Hazelcast members to find each other without the need to know concrete addresses via multicasting to everyone listening. Also, I encountered some issues with the port used by Multicast, so I set it to a hard-coded one to avoid the address already in use.

Then we configure a Map config that is going to act as our distributed cache. MapConfig contains the configuration of an IMap—concurrent, distributed, observable, and queryable map:

  • name: the name of IMap, WIKIPEDIA_SUMMARIES.
  • isStatisticsEnabled: this is to enable IMap’s statistics like the total number of hits and others.
  • backupCount: number of synchronous backups, where “0” means no backup.
  • evictionConfig.evictionPolicy: can be LRU (Least Recently Used), LFU (Least Frequently Used), NONE, and RANDOM. 
  • evictionConfig.size: the size used by MaxSizePolicy.
  • evictionConfig.maxSizePolicy: PER_NODE (policy based on the maximum number of entries per the Hazelcast instance). 

Having this configuration, all that is left is to adjust our enhanceWithProgrammingLanguageDescription method to use the above configured IMap to cache the fetched Wikipedia summaries:

Kotlin
 
private fun enhanceWithProgrammingLanguageDescription(course: Course) {
    val summaries = hazelcastInstance.getMap<String, WikipediaApiClientImpl.WikipediaSummary>(WIKIPEDIA_SUMMARIES)
    log.debug("Fetched hazelcast cache [$WIKIPEDIA_SUMMARIES] = [${summaries}(${summaries.size})]  ")
    summaries.getOrElse(course.programmingLanguage) {
        wikipediaApiClient.fetchSummaryFor("${course.programmingLanguage}_(programming_language)")?.let {
            log.debug("No cache value found, using wikipedia's response $it to update $course programming language description")
            summaries[course.programmingLanguage] = it
            it
        }
    }?.let { course.programmingLanguageDescription = it.summary }
}


Basically, we are using the autowired Hazelcast instance to retrieve our configured IMap. Each instance is a member and/or client in a Hazelcast cluster. When you want to use Hazelcast’s distributed data structures, you must first create an instance. In our case, we simply autowire it as it will be created by the previously defined config. After getting a hold of the distributed Map, it is a matter of some simple checks. If we have a summary for the programming language key in our map, then we use that one. If not, we fetch it from Wikipedia API, add it to the map, and use it.

Now, if we are to start our app, we’ll first see the huge Hazelcast banner and the following lines, meaning that Hazelcast has started:

Plain Text
 
INFO 30844 --- [           main] com.hazelcast.core.LifecycleService      : [ip]:5701 [dev] [5.2.1] [ip]:5701 is STARTING
INFO 30844 --- [           main] c.h.internal.cluster.ClusterService      : [ip]:5701 [dev] [5.2.1] 
Members {size:1, ver:1} [
	Member [ip]:5701 - e2a90d3e-b112-4e78-aa42-58a959d9273d this
]
INFO 30844 --- [           main] com.hazelcast.core.LifecycleService      : [ip]:5701 [dev] [5.2.1] [ip]:5701 is STARTED


If we execute the following HTTP request:

Plain Text
 
POST http://localhost:8081/api/v1/courses
Content-Type: application/json

{
  "name": "C++ Development",
  "category": "TUTORIAL",
  "programmingLanguage" : "C++",
  "instructor": {
    "name": "Bjarne Stroustrup"
  }
}


We’ll see in the logs:

Plain Text
 
DEBUG 30844 --- [nio-8080-exec-1] i.e.c.s.i.CourseServiceImpl$Companion    : Fetched hazelcast cache [WIKIPEDIA_SUMMARIES] = [IMap{name='WIKIPEDIA_SUMMARIES'}(0)]  
INFO 30844 --- [nio-8080-exec-1] e.c.s.i.WikipediaApiClientImpl$Companion : Request GET:https://en.wikipedia.org/api/rest_v1/page/summary/C++_(programming_language)
INFO 30844 --- [nio-8080-exec-1] e.c.s.i.WikipediaApiClientImpl$Companion : Received response from Wikipedia...
DEBUG 30844 --- [nio-8080-exec-1] i.e.c.s.i.CourseServiceImpl$Companion    : No cache value found, using wikipedia...


That we retrieved the previously configured IMap for Wikipedia summaries, but its size is “0;” therefore, the update took place using the Wikipedia’s API. Now, if we are to execute the same request again, we’ll notice a different behavior:

Plain Text
 
DEBUG 30844 --- [nio-8080-exec-3] i.e.c.s.i.CourseServiceImpl$Companion    : Fetched hazelcast cache [WIKIPEDIA_SUMMARIES] = [IMap{name='WIKIPEDIA_SUMMARIES'}(1)]  


Now the IMap has a size of “1” since it was populated by our previous request, so no request to Wikipedia’s API can be observed. The beauty and simplicity of Hazelcast’s integration comes when we start another instance of our app on a different port using -Dserver.port=8081, and we witness the distributed cache in action.

Plain Text
 
INFO 8172 --- [           main] com.hazelcast.core.LifecycleService      : [ip]:5702 [dev] [5.2.1] [ip]:5702 is STARTING
INFO 8172 --- [           main] c.h.i.cluster.impl.MulticastJoiner       : [ip]:5702 [dev] [5.2.1] Trying to join to discovered node: [ip]:5701
INFO 8172 --- [.IO.thread-in-0] c.h.i.server.tcp.TcpServerConnection     : [ip]:5702 [dev] [5.2.1] Initialized new cluster connection between /ip:55309 and /ip:5701
INFO 8172 --- [ration.thread-0] c.h.internal.cluster.ClusterService      : [ip]:5702 [dev] [5.2.1] 
Members {size:2, ver:2} [
	Member [ip]:5701 - 69d6721d-179b-4dc8-8163-e3cb00e703eb
	Member [ip]:5702 - 9b689155-d4c3-4169-ae53-ff5d687f7ad2 this
]
INFO 8172 --- [           main] com.hazelcast.core.LifecycleService      : [ip]:5702 [dev] [5.2.1] [ip]:5702 is STARTED


We see that MulticastJoiner discovered an already running Hazelcast node on port 5701, running together with our first courses-service instance on port 8080. A new cluster connection is made, and we see in the “Members” list both Hazelcast nodes on ports 5701 and 5702. Now, if we are to make a new HTTP request to create a course on the 8081 instance, we’ll see the following:

Plain Text
 
DEBUG 8172 --- [nio-8081-exec-4] i.e.c.s.i.CourseServiceImpl$Companion    : Fetched hazelcast cache [WIKIPEDIA_SUMMARIES] = [IMap{name='WIKIPEDIA_SUMMARIES'}(1)]  


Distributed Locks

Another useful feature that comes with Hazelcast is the API for distributed locks. Suppose our enhanceWithProgrammingLanguageDescription method is a slow intensive operation dealing with cache and other resources and we wouldn’t want other threads on the same instance or even other requests on a different instance to interfere or alter something until the operation is complete. So, here comes FencedLock into play—a linearizable, distributed, and reentrant implementation of the Lock. It is consistent and partition tolerant in the sense that if a network partition occurs, it will stay available on, at most, one side of the partition. Mostly, it offers the same API as the Lock interface. So, with this in mind, let’s try and guard our so-called “critical section.”

Kotlin
 
private fun enhanceWithProgrammingLanguageDescription(course: Course) {
    val lock = hazelcastInstance.cpSubsystem.getLock(SUMMARIES_LOCK)
    if (!lock.tryLock()) throw LockAcquisitionException(SUMMARIES_LOCK, "enhanceWithProgrammingLanguageDescription")
    Thread.sleep(2000)
    val summaries = hazelcastInstance.getMap<String, WikipediaApiClientImpl.WikipediaSummary>(WIKIPEDIA_SUMMARIES)
    log.debug("Fetched hazelcast cache [$WIKIPEDIA_SUMMARIES] = [${summaries}(${summaries.size})]  ")
    summaries.getOrElse(course.programmingLanguage) {
        wikipediaApiClient.fetchSummaryFor("${course.programmingLanguage}_(programming_language)")?.let {
            log.debug("No cache value found, using wikipedia's response $it to update $course programming language description")
            summaries[course.programmingLanguage] = it
            it
        }
    }?.let { course.programmingLanguageDescription = it.summary }
    lock.unlock()
}


As you can see, the implementation is quite simple. We obtain the lock via the cpSubsystem’s, getLock(), and then we try acquiring the lock with tryLock(). The locking will succeed if the acquired lock is available or already held by the current thread, and it will immediately return true. Otherwise, it will return false, and a LockAcquisitionException will be thrown. Next, we simulate some intensive work by sleeping for two seconds with Thread.sleep(2000), and in the end, we release the acquired lock with unlock(). If we run a single instance of our app on port 8080 and try two subsequent requests, one will pass, and the other one will fail with:

Plain Text
 
ERROR 28956 --- [nio-8081-exec-6] e.c.w.r.e.RESTExceptionHandler$Companion : Exception while handling request [summaries-lock] could not be acquired for [enhanceWithProgrammingLanguageDescription] operation. Please try again.
inc.evil.coursecatalog.common.exceptions.LockAcquisitionException: [summaries-lock] could not be acquired for [enhanceWithProgrammingLanguageDescription] operation. Please try again


The same goes if we are to make one request to an 8080 instance of our app and the next one in the two seconds timeframe to the 8081 instance; the first request will succeed while the second one will fail.

User Code Deployment

Now, let’s switch our attention to reviews-service and remember—this service is totally unaware of courses; it is just a way to add reviews for some course_id. With this in mind, we have this entity:

Kotlin
 
@Table("reviews")
data class Review(
    @Id
    var id: Int? = null,
    var text: String,
    var author: String,
    @Column("created_at")
    @CreatedDate
    var createdAt: LocalDateTime? = null,
    @LastModifiedDate
    @Column("last_modified_at")
    var lastModifiedAt: LocalDateTime? = null,
    @Column("course_id")
    var courseId: Int? = null
)


And we have this method in ReviewServiceImpl :

Kotlin
 
override suspend fun save(review: Review): Review {
    return reviewRepository.save(review).awaitFirst()
}


So, our new silly feature request would be to somehow check for the existence of the course that the review has been written for. How can we do that? The most obvious choice would be to invoke a REST endpoint on courses-service to check if we have a course for the review’s course_id, but that is not what this article is about. We have Hazelcast, right? We are going to deploy some user code from reviews-service that courses-service is aware of and can execute it via Hazelcast’s user code deployment.

To do that, we need to create some kind of API or gateway module that we are going to publish as an artifact, so courses-service can implement it, and reviews-service can depend on and use it to deploy the code. First things first, let’s design the new module as a courses-api module:

Kotlin
 
plugins {
    id("org.springframework.boot") version "2.7.3"
    id("io.spring.dependency-management") version "1.0.13.RELEASE"
    kotlin("jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"
    kotlin("plugin.jpa") version "1.3.72"
    `maven-publish`
}

group = "inc.evil"
version = "0.0.1-SNAPSHOT"

repositories {
    mavenCentral()
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            groupId = "inc.evil"
            artifactId = "courses-api"
            version = "1.1"

            from(components["java"])
        }
    }
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.6.4")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
    implementation("org.apache.commons:commons-lang3:3.12.0")
    implementation("com.hazelcast:hazelcast:5.2.1")
    implementation("com.hazelcast:hazelcast-spring:5.2.1")

    testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
}

tasks.getByName<Test>("test") {
    useJUnitPlatform()
}


Nothing fancy here, except the maven-publish plugin that we’ll use to publish the artifact to the local maven repository.

Here is the interface that courses-service will implement, and reviews-service will use:

Kotlin
 
interface CourseApiFacade {
    fun findById(id: Int): CourseApiResponse
}

data class InstructorApiResponse(
    val id: Int?,
    val name: String?,
    val summary: String?,
    val description: String?
)

data class CourseApiResponse(
    val id: Int?,
    val name: String,
    val category: String,
    val programmingLanguage: String,
    val programmingLanguageDescription: String?,
    val createdAt: String,
    val updatedAt: String,
    val instructor: InstructorApiResponse
)


Having this module properly configured, we can add it as a dependency in courses-service:

Kotlin
 
implementation(project(":courses-api"))


And implement the exposed interface like this:

Kotlin
 
@Component
class CourseApiFacadeImpl(val courseService: CourseService) : CourseApiFacade {

    override fun findById(id: Int): CourseApiResponse = courseService.findById(id).let {
        CourseApiResponse(
            id = it.id,
            name = it.name,
            category = it.category.toString(),
            programmingLanguage = it.programmingLanguage,
            programmingLanguageDescription = it.programmingLanguageDescription,
            createdAt = it.createdAt.toString(),
            updatedAt = it.updatedAt.toString(),
            instructor = InstructorApiResponse(it.instructor?.id, it.instructor?.name, it.instructor?.summary, it.instructor?.description)
        )
    }

}


Now back to reviews-service where all the magic will happen. First of all, we want to add the required dependencies for Hazelcast:

Kotlin
 
implementation("com.hazelcast:hazelcast:5.2.1")
implementation("com.hazelcast:hazelcast-spring:5.2.1")


Previously, I mentioned that we are going to use that interface from courses-api. We can run the publishMavenPublicationToMavenLocal gradle task on courses-api to get our artifact published, and then we can add the following dependency to reviews-service:

Kotlin
 
implementation("inc.evil:courses-api:1.1")


Now, is time to set up a Callable implementation on reviews-service that will be responsible for the code execution on courses-service, so, here it is:

Kotlin
 
@SpringAware
class GetCourseByIdCallable(val id: Int) : Callable<CourseApiResponse?>, Serializable {

    @Autowired
    @Transient
    private lateinit var courseApiFacade: CourseApiFacade

    override fun call(): CourseApiResponse = courseApiFacade.findById(id)
}


Here we used the @SpringAware annotation to mark this class as a bean in Spring-Hazelcast way via SpringManagedContext since this class is going to be deployed in the cluster. Other than that, we have our courseApiFacade autowired and used in the overridden method call() from Callable interface. I decided to write another class to ease the Callable submission to the Hazelcast’s IExecutorService to act as some kind of a facade:

Kotlin
 
@Component
class HazelcastGateway(private val hazelcastInstance: HazelcastInstance) {

    companion object {
        private const val EXECUTOR_SERVICE_NAME = "EXECUTOR_SERVICE"
    }

    fun <R> execute(executionRequest: Callable<R>): R {
        val ex = hazelcastInstance.getExecutorService(EXECUTOR_SERVICE_NAME)
        return ex.submit(executionRequest).get(15000L, TimeUnit.MILLISECONDS)
    }
}


We have a single method execution that is responsible to submit the passe—in Callable to the retrieved IExecutorService via the autowired hazelcastInstance. Now, this IExectuorService is a distributed implementation of ExecutorService that enables the running of Runnables/Callables on the Hazelcast cluster and has some additional methods that allows running the code on a particular member or multiple members. For example, we’ve used the submit, but there is also submitToMember and submitToAllMembers. For Runnable, there are the equivalents that start with execute***.

Now, let’s use our newly defined HazelcastGateway in the save method from ReviewServiceImpl.

Kotlin
 
override suspend fun save(review: Review): Review {
    runCatching {
        hazelcastGateway.execute(GetCourseByIdCallable(review.courseId!!)).also { log.info("Call to hazelcast ended with $it") }
    }.getOrNull() ?: throw NotFoundException(CourseApiResponse::class, "course_id", review.courseId.toString())
    return reviewRepository.save(review).awaitFirst()
}


The logic is as follows: before saving, we try to find the course by course_id from review by running GetCourseByIdCallable in our Hazelcast cluster. If we have an exception (CourseApiFacadeImpl will throw a NotFoundException if the requested course was not found), we swallow it and throw a reviews-service NotFoundException stating that the course could’ve not been retrieved. If a course was returned by our Callable, we proceed to save it—that’s it.

All that is left is to configure Hazelcast, and I left this one last since we needed some of these classes to configure it. 

Kotlin
 
@Configuration
class HazelcastConfiguration {


    @Bean
    fun managedContext(): SpringManagedContext {
        return SpringManagedContext()
    }

    @Bean
    fun hazelcastClientConfig(managedContext: SpringManagedContext): ClientConfig {
        val config = ClientConfig()
        config.managedContext = managedContext
        config.connectionStrategyConfig.isAsyncStart = true
        config.userCodeDeploymentConfig.isEnabled = true
        config.classLoader = HazelcastConfiguration::class.java.classLoader
                config.userCodeDeploymentConfig.addClass(GetCourseByIdCallable::class.java)

        return config
    }

}


Regarding the managedContext bean, there’s nothing different from our previous member’s config. Now the hazelcastConfig bean is different. First of all, it is a ClientConfig now, meaning that reviews-service’s embedded Hazelcast will be a client to the courses-service Hazelcast member. We set the managedContext, the connectionStrategyConfig to be async (the client won’t wait for a connection to cluster, it will throw exceptions until connected and ready), and we will enable the user code deployment. Then we add GetCourseByIdCallable to be sent to the cluster.

Now, having courses-service running and if we are to start reviews-service, we’ll see the following log:

Plain Text
 
INFO 34472 --- [nt_1.internal-1] com.hazelcast.core.LifecycleService      : hz.client_1 [dev] [5.2.1] HazelcastClient 5.2.1 (20221114 - 531032a) is CLIENT_CONNECTED
INFO 34472 --- [nt_1.internal-1] c.h.c.i.c.ClientConnectionManager        : hz.client_1 [dev] [5.2.1] Authenticated with server [ip]:5701:69d6721d-179b-4dc8-8163-e3cb00e703eb, server version: 5.2.1, local address: /127.0.0.1:57050


The client is authenticated with courses-service’s Hazelcast member, where “69d6721d-179b-4dc8-8163-e3cb00e703eb” is the ID of the connected to server. Now, let’s try a request to add a review for an existing course (reviews-service is using GraphQL).

Plain Text
 
GRAPHQL http://localhost:8082/graphql
Content-Type: application/graphql

mutation { createReview(request: {text: "Amazing, loved it!" courseId: 2 author: "Mike Scott"}) {
    id
    text
    author
    courseId
    createdAt
    lastModifiedAt
}
}


In the logs, we’ll notice:

Plain Text
 
INFO 34472 --- [actor-tcp-nio-1] i.e.r.s.i.ReviewServiceImpl$Companion    : Call to hazelcast ended with CourseApiResponse(id=2, name=C++ Development, category=TUTORIAL, programmingLanguage=C++ ...)


And in the courses-service logs, we’ll notice the code execution:

Plain Text
 
DEBUG 12608 --- [ached.thread-24] i.e.c.c.aop.LoggingAspect$Companion      : before :: execution(public inc.evil.coursecatalog.model.Course inc.evil.coursecatalog.service.impl.CourseServiceImpl.findById(int))


Meaning that the request executed successfully. If we try the same request for a non-existent course, let’s say for ID 99, we’ll observe the NotFoundException in reviews-service: 

Plain Text
 
WARN 34472 --- [actor-tcp-nio-2] .w.g.e.GraphQLExceptionHandler$Companion : Exception while handling request: CourseApiResponse with course_id equal to [99] could not be found! 
inc.evil.reviews.common.exceptions.NotFoundException: CourseApiResponse with course_id equal to [99] could not be found!


Conclusion

All-right folks, this is basically it. I hope you got a good feel of what Hazelcast is like. We took a look at how to design a simple distributed cache using Hazelcast and Spring Boot, made use of distributed locks to protect critical sections of code, and in the end, we’ve seen how Hazelcast’s user code deployment can be used to run some code in the cluster. In case you’ve missed it, all the source code can be found here.

Happy coding!

API Apache Maven Hazelcast Plain text Kotlin (programming language) Spring Boot REST XML YAML Java (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • High-Performance Reactive REST API and Reactive DB Connection Using Java Spring Boot WebFlux R2DBC Example
  • (Spring) Booting Java To Accept Digital Payments With USDC
  • Spring Boot, Quarkus, or Micronaut?
  • CRUD REST API With Jakarta Core Profile Running on Java SE

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!