How to connect your Quarkus application to Infinispan

Infinispan is a distributed in-memory key/value data grid. An in-memory data grid is a form of middleware that stores sets of data for use in one or more applications, primarily in memory. There are different clients available to connect to a remote/embedded Infinispan server, In this tutorial we will learn how to connect to Infinispan using Quarkus extension.

Starting Infinispan

For the purpose of this tutorial, we will be running a local Infinispan 10 server with this cache definition in infinispan.xml:

  <cache-container default-cache="local">
      <transport cluster="${infinispan.cluster.name}" stack="${infinispan.cluster.stack:tcp}" node-name="${infinispan.node.name:}"/>
      <local-cache name="local"/>
      <invalidation-cache name="invalidation" mode="SYNC"/>
      <replicated-cache name="repl-sync" mode="SYNC"/>
      <distributed-cache name="dist-sync" mode="SYNC"/>
   </cache-container>

From the bin folder of Infinispan, run:

 ./server.sh

As an alternative, you can also run Infinispan with Docker as follows:

docker run -it -p 11222:11222 infinispan/server:latest

Creating the Quarkus Infinispan project

In order to create the Quarkus application, we will need the following set of dependencies:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-infinispan-client</artifactId>
</dependency>

Resteasy and jsonb will be needed to set up a CRUD REST application. The dependency quarkus-infinispan-client is needed to connect to Infinispan from Quarkus

There is an extra dependency needed in order to start and connect to an embedded Infinispan server, however we will be discussing this later.

Here is our main Application class:

@ApplicationScoped
public class InfinispanClientApp {

    private static final Logger LOGGER = LoggerFactory.getLogger("InfinispanClientApp");
    @Inject
    @Remote("local")
    RemoteCache<String, Customer> cache;

    void onStart(@Observes StartupEvent ev) {
        cache.addClientListener(new EventPrintListener());
        Customer c = new Customer("1","John","Smith");
        cache.put("1", c);
    }

    @ClientListener
    static class EventPrintListener {

        @ClientCacheEntryCreated
        public void handleCreatedEvent(ClientCacheEntryCreatedEvent e) {
            LOGGER.info("Someone has created an entry: " + e);
        }

        @ClientCacheEntryModified
        public void handleModifiedEvent(ClientCacheEntryModifiedEvent e) {
            LOGGER.info("Someone has modified an entry: " + e);
        }

        @ClientCacheEntryRemoved
        public void handleRemovedEvent(ClientCacheEntryRemovedEvent e) {
            LOGGER.info("Someone has removed an entry: " + e);
        }

    }
}

A couple of things to notice:

When the application is bootstrapped, we are connecting to the RemoteCache (“local”), setting up a ClientListener on it and we also add one key into it.

As we need to marshall/unmarshall the Java class Customer using Infinispan, we will use annotation based Serialization by setting the @ProtoField annotation on fields which need to be serialized:

public class Customer {
    private String id;
    private String name;
    private String surname;
    
    @ProtoField(number = 1)
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
    
    @ProtoFactory
    public Customer(String id, String name, String surname) {
        this.id = id;
        this.name = name;
        this.surname = surname;
    }

    @ProtoField(number = 2)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    @ProtoField(number = 3)
    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", surname='" + surname + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Customer)) return false;
        Customer customer = (Customer) o;
        return Objects.equals(id, customer.id) &&
                Objects.equals(name, customer.name) &&
                Objects.equals(surname, customer.surname);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, surname);
    }

    public Customer() {
    }
}

Then we need also a SerializationContextInitializer interface with an annotation on it to specify configuration settings:

@AutoProtoSchemaBuilder(includeClasses = { Customer.class}, schemaPackageName = "customer_list")
public interface CustomerContextInitializer extends SerializationContextInitializer {
}

That’s it. We are done with Infinispan. We will now add a REST Endpoint to allow CRUD Operations on our RemoteCache:

Path("/infinispan")
@ApplicationScoped
public class InfinispanEndpoint {

    @Inject
    @Remote("local")
    RemoteCache<String, Customer> cache;


    @GET
    @Path("{customerId}")
    @Produces("application/json")
    public Response get(@PathParam("customerId") String id) {
        Customer customer = cache.get(id);
        System.out.println("Got customer " +customer);
        return Response.ok(customer).status(200).build();
    }



    @POST
    @Produces("application/json")
    @Consumes("application/json")
    public Response create(Customer customer) {
        cache.put(customer.getId(), customer);
        System.out.println("Created customer " +customer);
        return Response.ok(customer).status(201).build();
    }

    @PUT
    @Produces("application/json")
    @Consumes("application/json")
    public Response update(Customer customer) {
        cache.put(customer.getId(), customer);
        System.out.println("Updated customer " +customer);
        return Response.ok(customer).status(202).build();
    }

    @DELETE
    @Path("{customerId}")
    public Response delete(@PathParam("customerId") String id) {
        cache.remove(id);
        System.out.println("Deleted customer "+id);
        return Response.ok().status(202).build();
    }
}

As you can see, once that we get injected the RemoteCache, we can apply the basic get/put/remove operations accordingly on the HTTP methods.

Finally, into our application.properties, we have set the address of Infinispan server:

quarkus.infinispan-client.server-list=localhost:11222

Running the Quarkus application

Start the Quarkus application in dev mode:

mvn clean install quarkus:dev

We can test our application using the REST Endpoints. For example, to get the customer with id “1”:

curl http://localhost:8080/infinispan/1
{"id":"1","name":"John","surname":"Smith"}

To add a new Customer:

curl -d '{"id":"2", "name":"Clark","surname":"Kent"}' -H "Content-Type: application/json" -X POST http://localhost:8080/infinispan

To update an existing Customer:

curl -d '{"id":"2", "name":"Peter","surname":"Parker"}' -H "Content-Type: application/json" -X PUT http://localhost:8080/infinispan

And finally to delete one Customer:

curl -X DELETE http://localhost:8080/infinispan/2

Source code for this tutorial: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/infinispan-demo

Embedding Infinispan in Quarkus applications

It is also possible to bootstrap an Infinispan cluster from within a Quarkus application using the embedded API. For this purpos you will need an extra dependency in your pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-infinispan-embedded</artifactId>
</dependency>

With this dependency in place, you can inject the org.infinispan.manager.EmbeddedCacheManager in your code:

@Inject
EmbeddedCacheManager emc;

This will give us a default local (i.e. non-clustered) CacheManager. You can also build a cluster of servers from a configuration file, say dist.xml:

ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.clustering().cacheMode(CacheMode.DIST_SYNC);

List<EmbeddedCacheManager> managers = new ArrayList<>(3);
try {
    String oldProperty = System.setProperty("jgroups.tcp.address", "127.0.0.1");
    for (int i = 0; i < 3; i++) {
        EmbeddedCacheManager ecm = new DefaultCacheManager(
                Paths.get("src", "main", "resources", "dist.xml").toString());
        ecm.start();
        managers.add(ecm);
        // Start the default cache
        ecm.getCache();
    }

And here’s a sample dist.xml (which used tcpping) for our cluster:

<infinispan
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="urn:infinispan:config:10.1 http://www.infinispan.org/schemas/infinispan-config-10.1.xsd
                          urn:org:jgroups http://www.jgroups.org/schema/jgroups-4.0.xsd"
      xmlns="urn:infinispan:config:10.1"
      xmlns:ispn="urn:infinispan:config:10.1">
   <jgroups>
       <stack name="tcpping" extends="tcp">
           <MPING ispn:stack.combine="REMOVE" xmlns="urn:org:jgroups"/>
           <TCPPING async_discovery="true"
                    initial_hosts="${initial_hosts:127.0.0.1[7800],127.0.0.1[7801]}"
                    port_range="0" ispn:stack.combine="INSERT_AFTER" ispn:stack.position="TCP" xmlns="urn:org:jgroups"/>
       </stack>
   </jgroups>

   <cache-container name="test" default-cache="dist">
       <transport cluster="test" stack="tcpping"/>
      <distributed-cache name="dist">
         <memory>
            <object size="21000"/>
         </memory>
      </distributed-cache>
   </cache-container>
</infinispan>
Found the article helpful? if so please follow us on Socials