Using Java and .NET apps to connect to an Apache Ignite cluster

This article will focus on how to create an Apache Ignite cluster that can support the reading and writing of user-defined objects in a common storage format. This is particularly useful in situations where applications need to work with objects but these objects will be accessed by different programming languages and frameworks. Apache Ignite supports a binary format that is particularly useful for this task. We will look at how to achieve the goal of interoperability using some short programming examples.

Background

An Apache Ignite cluster may consist of nodes from a number of different supported platforms. These supported platforms include Java, .NET and C++. In this article, we will look at some code examples using Java and .NET.

In many organizations, different departments and teams may be working with different programming languages and frameworks. However, there may be a need for a common storage format to allow various tools to access the same data. Apache Ignite provides the flexibility for development teams to continue working with their favorite programming languages and tools and have the ability to work with the same data in the cluster.

My colleague Pavel Tupitsyn has a nice write-up of how to create a multi-platform Ignite cluster using Java and .NET. I followed the same steps to set-up and configure IntelliJ IDEA and Microsoft Visual Studio for the projects in the two IDEs.

For my examples, I chose to use a person object with two fields called name and city_id of type string and integer, respectively. These fields represent the name of the person and the code of the city where they live. More fields could be added to represent other data. For example, in an airline application, the person may represent a customer and additional data such as frequent-flyer status could be stored. In a company application, the person may represent an employee and additional data such as employee number could be stored. There are many more fields that could be defined. For demo purposes, however, we will keep the examples quite simple.

Figure 1 shows the Java code for the Person class.


public class Person {
    private String name;
    private Integer city_id;

    public Person(String name, Integer city_id) {
        this.name = name;
        this.city_id = city_id;
    }

    public String name() {
        return name;
    }

    public Integer city_id() {
        return city_id;
    }
}

Figure 1. Java Person class

Figure 2 shows the same in .NET.


internal class Person {
    private string name;
    private int city_id;

    public Person(string name, int city_id) {
        this.name = name;
        this.city_id = city_id;
    }

    public string Name() {
        return name;
    }

    public int City_id() {
        return city_id;
    }
}

Figure 2. .NET Person class

We can see the use of the same names and data types for the classes and their variables in both Java and .NET.

For the demos, there are four short applications. There is a read and write application for both Java and .NET. The goal is to show writing to the cache using Java and reading from the cache using .NET. Then repeat this by writing to the cache using .NET and reading from the cache using Java.

Modify the Java node configuration to co-exist with .NET nodes

The following configuration parameter has to be added to all Java cluster nodes so that they can co-exist with .NET counterparts:


// Configure Ignite to connect with .NET nodes
IgniteConfiguration cfg = new IgniteConfiguration().setBinaryConfiguration(
    new BinaryConfiguration().setNameMapper(new BinaryBasicNameMapper(true)));

Every object, stored in the Ignite cluster, keeps information about its class for the purpose of deserialization. Instead of keeping a class name in the serialized form, an Ignite node takes the name and converts it to a unique integer ID that is written in an object's result byte array. By default, Java uses the full class name (package name + simple name) for the ID calculation while .NET uses the simple name only (omitting the package name).

The configuration above ensures that Java nodes will use the simple name for the ID calculation. If we don't provide this code, the two different nodes will be unable to work together.

Java code

The outline for our main program is shown in Figure 3.


public static void main(String[] args) {

    // Configure Ignite to connect with .NET nodes
    IgniteConfiguration cfg = new IgniteConfiguration().setBinaryConfiguration(
        new BinaryConfiguration().setNameMapper(new BinaryBasicNameMapper(true)));

    // Start Ignite and retrieve cache
    Ignite ignite = Ignition.start(cfg);
    IgniteCache<Integer, Person> cache = ignite.getOrCreateCache("person");

    // Code to call cache put or get here
    Ignition.stop(false);
}

Figure 3. Main program

The Java code in Figure 4 will put data into the cache.


private static void putCache(IgniteCache<Integer, Person> cache) {

    System.out.println();
    System.out.println("> Cache put example started.");

    // Create some data and put it in the cache
    cache.put(1, new Person("John Doe", 3));
    cache.put(2, new Person("Jane Roe", 2));
    cache.put(3, new Person("Mary Major", 1));
    cache.put(4, new Person("Richard Miles", 2));

    System.out.println("> Stored values in cache.");
}

Figure 4. Cache put

The following Java code in Figure 5 will get data from the cache.


private static void getCache(IgniteCache cache) {

    System.out.println();
    System.out.println("> Cache get example started.");

    System.out.println("> Retrieved person instance from cache: " + cache.get(1).name());
    System.out.println("> Retrieved person instance from cache: " + cache.get(2).name());
    System.out.println("> Retrieved person instance from cache: " + cache.get(3).name());
    System.out.println("> Retrieved person instance from cache: " + cache.get(4).name());
}

Figure 5. Cache get

.NET code

The outline for our main program is shown in Figure 6.


static void Main(string[] args) {

    // Register Person type
    var cfg = new IgniteConfiguration {
        BinaryConfiguration = new BinaryConfiguration {
            NameMapper = new BinaryBasicNameMapper { IsSimpleName = true }
        }
    };

    // Start Ignite and retrieve cache
    var ignite = Ignition.Start(cfg);
    var cache = ignite.GetOrCreateCache("person");

    // Code to call cache put or get here
}

Figure 6. Main program

The following .NET code in Figure 7 will put data into the cache.


private static void PutCache(ICache cache) {

    Console.WriteLine();
    Console.WriteLine("> Cache put example started.");

    // Create some data and put it in the cache
    cache.Put(4, new Person("John Doe", 3));
    cache.Put(3, new Person("Jane Roe", 2));
    cache.Put(2, new Person("Mary Major", 1));
    cache.Put(1, new Person("Richard Miles", 2));

    Console.WriteLine("> Stored values in cache.");
}

Figure 7. Cache put

The following .NET code in Figure 8 will get data from the cache.


private static void GetCache(ICache cache) {

    Console.WriteLine();
    Console.WriteLine("> Cache get example started.");

    Console.WriteLine("> Retrieved person instance from cache: " + cache.Get(1).Name());
    Console.WriteLine("> Retrieved person instance from cache: " + cache.Get(2).Name());
    Console.WriteLine("> Retrieved person instance from cache: " + cache.Get(3).Name());
    Console.WriteLine("> Retrieved person instance from cache: " + cache.Get(4).Name());
}

Figure 8. Cache get

The code appears very similar. However, closer inspection of the code shows that Java uses the following:


    // Create some data and put it in the cache
    cache.put(1, new Person("John Doe", 3));
    cache.put(2, new Person("Jane Roe", 2));
    cache.put(3, new Person("Mary Major", 1));
    cache.put(4, new Person("Richard Miles", 2));

and .NET uses the following:


    // Create some data and put it in the cache
    cache.Put(4, new Person("John Doe", 3));
    cache.Put(3, new Person("Jane Roe", 2));
    cache.Put(2, new Person("Mary Major", 1));
    cache.Put(1, new Person("Richard Miles", 2));

The order of the keys is reversed for .NET. This allows us to verify that data are indeed different when stored from Java and .NET.

Since these are client programs, we need to ensure that there is at least another node running in the cluster that continues to operate and maintain the cache that we are writing to and reading from. This additional node also needs to be configured for binary data.

Write to cache using Java, read from cache using .NET

Let's run the Java program to write into the cache. The output in IntelliJ IDEA is as follows:

> Cache put example started.
> Stored values in cache.
[18:09:35] Ignite node stopped OK [uptime=00:00:00:391]

Process finished with exit code 0

Now let's run the .NET program to read from the cache. The output in Microsoft Visual Studio is as follows:

NETGet

Write to cache using .NET, read from cache using Java

Let's run the .NET program to write into the cache. The output in Microsoft Visual Studio is as follows:

NETPut

Now let's run the Java program to read from the cache. The output in IntelliJ IDEA is as follows:

> Cache get example started.
> Retrieved person instance from cache: Richard Miles
> Retrieved person instance from cache: Mary Major
> Retrieved person instance from cache: Jane Roe
> Retrieved person instance from cache: John Doe
[18:15:04] Ignite node stopped OK [uptime=00:00:00:266]

Process finished with exit code 0

Summary

Departments and development teams in organizations may use different programming languages and frameworks. It can sometimes be difficult to use the same business objects across these different development environments. However, Apache Ignite provides the flexibility to use user-defined objects across a number of popular programming languages, such as Java, .NET and C++. These objects can also be quite complex. By using Apache Ignite’s internal binary representation, it is possible to easily map these objects between these multiple programming languages.