Micronaut Coherence

Integration between Micronaut and Oracle Coherence

Version: 5.0.4

1 Introduction

Oracle Coherence is a scalable, fault-tolerant, cloud-ready, distributed platform for building grid-based applications and reliably storing data. The product is used at scale, for both compute and raw storage, in a vast array of industries such as critical financial trading systems, high performance telecommunication products and e-commerce applications.

Micronaut features dedicated support to bootstrap Coherence and for both injection of Coherence resources into beans and injection of beans into Coherence resources. Micronaut Injection simplifies application code as Coherence maps, caches and topics are just injected instead of being obtained via Coherence APIs.

Using annotated event listener methods simplifies building reactive code that responds to Coherence cache events.

2 Release History

For this project, you can find a list of releases (with release notes) here:

Important Changes in Release 5.0.0

The following outlines backwards-incompatible changes made in this release:

gRPC

  • Removed the configuration of gRPC proxies within application configuration resources (such as application.yaml). Both gRPC clients and proxies are now configured in a similar fashion to extend client and proxies. Please see the Coherence gRPC docs for more details.

  • Removed the coherence-grpc-client module.

Coherence Configuration Client

  • Reworked how the client connects by using sessions instead of hard coding an address and port and forcing the client to use gRPC. By using a session, the client can be a storage-disabled cluster member, an extend client, or a gRPC client. See the Coherence Distributed Configuration documentation for details.

3 Coherence Quick Start

To add support for Oracle Coherence to an existing project, you should first add the Micronaut Coherence configuration to your build configuration. For example:

implementation("io.micronaut.coherence:micronaut-coherence")
<dependency>
    <groupId>io.micronaut.coherence</groupId>
    <artifactId>micronaut-coherence</artifactId>
</dependency>

Next, you also need to add the version of Coherence that your application will be using. For example, to use Coherence CE:

implementation("com.oracle.coherence.ce:coherence:24.03")
<dependency>
    <groupId>com.oracle.coherence.ce</groupId>
    <artifactId>coherence</artifactId>
    <version>24.03</version>
</dependency>

Coherence CE 24.03 and newer require JDK 17 as a minimum!

There is no minimal configuration required to bootstrap Coherence. If no specific configuration is provided Coherence will run with default values. Some Coherence functionality can be configured using System properties. Add these properties to the application configuration file, for example:

Configure Coherence
coherence.cluster=test-cluster
coherence.role=storage
coherence:
  cluster: test-cluster
  role: storage
[coherence]
  cluster="test-cluster"
  role="storage"
coherence {
  cluster = "test-cluster"
  role = "storage"
}
{
  coherence {
    cluster = "test-cluster"
    role = "storage"
  }
}
{
  "coherence": {
    "cluster": "test-cluster",
    "role": "storage"
  }
}

The above configuration sets the cluster name to test-cluster, and the role name to storage. Any Coherence system property (prefixed with coherence.) specified in the Micronaut configuration will then be picked up by Coherence at runtime.

4 Bootstrapping Coherence

The default behavior of the Coherence Micronaut framework is to use the Coherence bootstrap API introduced in Coherence CE 20.12 to configure and create Coherence instances. This means that Coherence resources in a Micronaut application are typically owned by a Coherence Session.

By default, Coherence will start a single Session configured to use the default Coherence configuration file. This behaviour can easily be configured, either traditionally using Coherence system properties or using explicit Micronaut configuration.

When using Micronaut, all Coherence resources used by application code should be injected. Applications should avoid calling Coherence APIs that create or initialise Coherence resources directly, especially static CacheFactory methods. If application code calls these Coherence APIs it may cause Coherence to be initialised too early in the start-up process before the Micronaut framework has initialised the Coherence extensions. A typical symptom of this would be that Coherence starts without picking up the correct configuration from the Micronaut framework.

4.1 Using the Default Session

The simplest configuration, applicable to the majority of applications that only require a single Session, is to just configure and use the default Session. If the application is a Coherence cluster member, or a Coherence*Extend client, then all that needs to be specified is the Coherence configuration file name (although even this is optional as Coherence will use a default file name if none is configured). Alternatively, if the application is a Coherence gRPC client, the default Session can be configured as a gRPC Session (see the Coherence gRPC).

As already mentioned in the Quick Start section, the name of the Coherence configuration file for the default session can be set in the Micronaut config, for example in the application configuration file:

coherence.sessions.default.config=coherence-config.xml
coherence:
  sessions:
    default:
      config: coherence-config.xml
[coherence]
  [coherence.sessions]
    [coherence.sessions.default]
      config="coherence-config.xml"
coherence {
  sessions {
    'default' {
      config = "coherence-config.xml"
    }
  }
}
{
  coherence {
    sessions {
      default {
        config = "coherence-config.xml"
      }
    }
  }
}
{
  "coherence": {
    "sessions": {
      "default": {
        "config": "coherence-config.xml"
      }
    }
  }
}

4.2 Configure Multiple Sessions

In the above configuration, the coherence section contains a sessions section which is the configuration of one or more sessions. The format is a map using the session name as the map key. In the above example there is a single session with the name default.

In the example below, there are two sessions configured, one named catalog and one named customer:

coherence.sessions.catalog.config=catalog-config.xml
coherence.sessions.customer.config=customer-config.xml
coherence:
  sessions:
    catalog:
      config: catalog-config.xml
    customer:
      config: customer-config.xml
[coherence]
  [coherence.sessions]
    [coherence.sessions.catalog]
      config="catalog-config.xml"
    [coherence.sessions.customer]
      config="customer-config.xml"
coherence {
  sessions {
    catalog {
      config = "catalog-config.xml"
    }
    customer {
      config = "customer-config.xml"
    }
  }
}
{
  coherence {
    sessions {
      catalog {
        config = "catalog-config.xml"
      }
      customer {
        config = "customer-config.xml"
      }
    }
  }
}
{
  "coherence": {
    "sessions": {
      "catalog": {
        "config": "catalog-config.xml"
      },
      "customer": {
        "config": "customer-config.xml"
      }
    }
  }
}
The default session will only exist when zero sessions are specifically configured, or the default session is specifically configured. For example, in the configuration below there wil be no default session, only a single session named catalog.
coherence.sessions.catalog.config=catalog-config.xml
coherence:
  sessions:
    catalog:
      config: catalog-config.xml
[coherence]
  [coherence.sessions]
    [coherence.sessions.catalog]
      config="catalog-config.xml"
coherence {
  sessions {
    catalog {
      config = "catalog-config.xml"
    }
  }
}
{
  coherence {
    sessions {
      catalog {
        config = "catalog-config.xml"
      }
    }
  }
}
{
  "coherence": {
    "sessions": {
      "catalog": {
        "config": "catalog-config.xml"
      }
    }
  }
}

In this example, there will be two sessions, one named catalog and the default session.

coherence.sessions.catalog.config=catalog-config.xml
coherence.sessions.default.config=coherence-config.xml
coherence:
  sessions:
    catalog:
      config: catalog-config.xml
    default:
      config: coherence-config.xml
[coherence]
  [coherence.sessions]
    [coherence.sessions.catalog]
      config="catalog-config.xml"
    [coherence.sessions.default]
      config="coherence-config.xml"
coherence {
  sessions {
    catalog {
      config = "catalog-config.xml"
    }
    'default' {
      config = "coherence-config.xml"
    }
  }
}
{
  coherence {
    sessions {
      catalog {
        config = "catalog-config.xml"
      }
      default {
        config = "coherence-config.xml"
      }
    }
  }
}
{
  "coherence": {
    "sessions": {
      "catalog": {
        "config": "catalog-config.xml"
      },
      "default": {
        "config": "coherence-config.xml"
      }
    }
  }
}

4.3 Session Configuration Properties

There are a number of fields that may be used to configure a Session, all of them are optional.

Configuration File Name

As already mentioned, the most common configuration to set will be the Coherence configuration file name. This is set using the config property. If not specified, the default value will be coherence-cache-config.xml.

coherence.sessions.default.config=coherence-config.xml
coherence:
  sessions:
    default:
      config: coherence-config.xml
[coherence]
  [coherence.sessions]
    [coherence.sessions.default]
      config="coherence-config.xml"
coherence {
  sessions {
    'default' {
      config = "coherence-config.xml"
    }
  }
}
{
  coherence {
    sessions {
      default {
        config = "coherence-config.xml"
      }
    }
  }
}
{
  "coherence": {
    "sessions": {
      "default": {
        "config": "coherence-config.xml"
      }
    }
  }
}

Scope Name

A scope name is typically used in an application where the Coherence cluster member has multiple sessions. The scope name is used to keep the sessions separate. The scope name will be applied to the session’s underlying ConfigurableCacheFactory and used to scope Coherence services. In this way multiple session configurations may use identical service names, which will be kept separate using the scope. On a Coherence cluster member each session should have a unique scope name.

In a client application, for example, a gRPC client, the scope name is used to map a client session to a corresponding scoped server session.

coherence.sessions.catalog.scope=Catalog
coherence.sessions.catalog.config=catalog-config.xml
coherence.sessions.customer.scope=Customer
coherence.sessions.customer.config=customer-config.xml
coherence:
  sessions:
    catalog:
      scope: Catalog
      config: catalog-config.xml
    customer:
      scope: Customer
      config: customer-config.xml
[coherence]
  [coherence.sessions]
    [coherence.sessions.catalog]
      scope="Catalog"
      config="catalog-config.xml"
    [coherence.sessions.customer]
      scope="Customer"
      config="customer-config.xml"
coherence {
  sessions {
    catalog {
      scope = "Catalog"
      config = "catalog-config.xml"
    }
    customer {
      scope = "Customer"
      config = "customer-config.xml"
    }
  }
}
{
  coherence {
    sessions {
      catalog {
        scope = "Catalog"
        config = "catalog-config.xml"
      }
      customer {
        scope = "Customer"
        config = "customer-config.xml"
      }
    }
  }
}
{
  "coherence": {
    "sessions": {
      "catalog": {
        "scope": "Catalog",
        "config": "catalog-config.xml"
      },
      "customer": {
        "scope": "Customer",
        "config": "customer-config.xml"
      }
    }
  }
}

In the above example there are two sessions, catalog and customer. The catalog session has a scope name of Catalog, and the customer session` has the scope name Customer.

In a client application there might only be a single default session but this session needs to connect to a server that has multiple sessions configured. In this case the scope name is used to identify the server side session.

For example, assuming that the server is using the configuration above, with two sessions, catalog and customer. The client application only needs to connect to the catalog session, so it can be configured with a default session like this:

coherence.sessions.default.scope=Catalog
coherence.sessions.default.config=client-config.xml
coherence:
  sessions:
    default:
      scope: Catalog
      config: client-config.xml
[coherence]
  [coherence.sessions]
    [coherence.sessions.default]
      scope="Catalog"
      config="client-config.xml"
coherence {
  sessions {
    'default' {
      scope = "Catalog"
      config = "client-config.xml"
    }
  }
}
{
  coherence {
    sessions {
      default {
        scope = "Catalog"
        config = "client-config.xml"
      }
    }
  }
}
{
  "coherence": {
    "sessions": {
      "default": {
        "scope": "Catalog",
        "config": "client-config.xml"
      }
    }
  }
}

Session Type

There are three different types of session that can be configured:

  • server represents storage enabled cluster member session.

  • client represents a storage disabled cluster member or Coherence*Extend client session.

  • grpc is a gRPC client session (see the Coherence gRPC documentation).

The type of the session affects how the bootstrap API starts the session. The session type is configured with the type property:

coherence.sessions.default.type=client
coherence.sessions.default.scope=Catalog
coherence.sessions.default.config=client-config.xml
coherence:
  sessions:
    default:
      type: client
      scope: Catalog
      config: client-config.xml
[coherence]
  [coherence.sessions]
    [coherence.sessions.default]
      type="client"
      scope="Catalog"
      config="client-config.xml"
coherence {
  sessions {
    'default' {
      type = "client"
      scope = "Catalog"
      config = "client-config.xml"
    }
  }
}
{
  coherence {
    sessions {
      default {
        type = "client"
        scope = "Catalog"
        config = "client-config.xml"
      }
    }
  }
}
{
  "coherence": {
    "sessions": {
      "default": {
        "type": "client",
        "scope": "Catalog",
        "config": "client-config.xml"
      }
    }
  }
}
  • In this example the default session is a client session.

5 Micronaut Data with Coherence

Micronaut Data is a database access toolkit that uses Ahead of Time (AoT) compilation to pre-compute queries for repository interfaces that are then executed by a thin, lightweight runtime layer.

Micronaut Data is inspired by GORM and Spring Data, however improves on those solutions in the following ways:

  • No runtime model - Both GORM and Spring Data maintain a runtime metamodel that uses reflection to model relationships between entities. This model consumes significant memory and memory requirements grow as your application size grows. The problem is worse when combined with Hibernate which maintains its own metamodel as you end up with duplicate metamodels.

  • No query translation - Both GORM and Spring Data use regular expressions and pattern matching in combination with runtime generated proxies to translate a method definition on a Java interface into a query at runtime. No such runtime translation exists in Micronaut Data and this work is carried out by the Micronaut compiler at compilation time.

  • No Reflection or Runtime Proxies - Micronaut Data uses no reflection or runtime proxies, resulting in better performance, smaller stack traces and reduced memory consumption due to a complete lack of reflection caches (Note that the backing implementation, for example Hibernate, may use reflection).

  • Type Safety - Micronaut Data will actively check at compile time that a repository method can be implemented and fail compilation if it cannot.

This integration allows using Coherence as a data source for a Micronaut repositories

In addition to the above, this library provides abstract synchronous and asynchronous repositories that expose the power of Coherence when not using generated queries.

This documentation assumes with principals of Micronaut Data as described here.

5.1 Limitations

As Coherence is ultimately a key/value store, there are some limitations using the generated query facility offered by Micronaut.

The following auto generated query types/features are not supported

  • JOIN

  • ORDER BY; the statement will compile, however, it currently has no effect in Coherence’s query language. For the time being, use the APIs offered by the Coherence abstract repository classes.

  • pagination; this means you should not use Page or Slice as return types for such queries

  • When extending the Coherence Data AbstractCoherenceRepository or `AbstractCoherenceAsyncRepository, it must not implement any other Micronaut Data interfaces (e.g., CrudRepository, etc.)

5.2 Build

Gradle

If you’re using Gradle for your build, add the following dependencies (in addition to existing micronaut configuration):

build.gradle
annotationProcessor("io.micronaut.coherence.data:coherence-data:${version}")
...
implementation("io.micronaut.coherence.data:coherence-data:${version}");
...
testAnnotationProcessor("io.micronaut.coherence.data:coherence-data:${version}")

Maven

pom.xml
<dependencies>
  ...
  <dependency>
    <groupId>io.micronaut.coherence</groupId>
    <artifactId>micronaut-coherence-data</artifactId>
    <version>${version}</version>
  </dependency>
  ...
</dependencies>
...
<plugins>
  ...
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <annotationProcessorPaths combine.children="append">
        <annotationProcessorPath>
          <groupId>io.micronaut.coherence</groupId>
          <artifactId>micronaut-coherence-processor</artifactId>
          <version>${version}</version>
        </annotationProcessorPath>
      </annotationProcessorPaths>
      <compilerArgs>
        <arg>-Amicronaut.processing.group=todo.list.micronaut.server
        </arg>
        <arg>-Amicronaut.processing.module=todo-list-micronaut-server
        </arg>
      </compilerArgs>
    </configuration>
  </plugin>
  ...
</plugins>

5.3 Using Micronaut Data

Configuration

Add or update the Coherence configuration to enable Coherence repositories.

coherence.data.<repository-name>[0].session=<session-name>
coherence:
  data:
    <repository-name>:
      [session: <session-name>]
[coherence]
  [coherence.data]
    [[coherence.data.repository-name]]
      session="<session-name>"
coherence {
  data {
    <repositoryName> = [{
        session = "<session-name>"
      }]
  }
}
{
  coherence {
    data {
      <repository-name> = [{
          session = "<session-name>"
        }]
    }
  }
}
{
  "coherence": {
    "data": {
      "<repository-name>": [{
          "session": "<session-name>"
        }]
    }
  }
}

<repository-name> maps to the name of the Coherence cache. The optional session attribute specifies the name of the Coherence session that should be used to look up the specified cache. If the session attribute is omitted, then the default Coherence Session will be assumed. Review the main documentation for details on configuration sessions.

Any repositories that the developer wishes to use with Coherence must be annotated with the @CoherenceRepository annotation. The value provided to the annotation must match the <repository-name> identifier within the application configuration.

For example, let’s assume we have a model class representing a Book identified by a UUID. We can configure something like the following:

coherence.data.book=null
coherence:
  data:
    book:
[coherence]
  [coherence.data]
coherence {
  data {
    book = null
  }
}
{
  coherence {
    data {
      book = null
    }
  }
}
{
  "coherence": {
    "data": {
      "book": null
    }
  }
}
As any repository, like book above, that doesn’t have explicit session configuration, will use the default Coherence session. If that’s the desired configuration, then there is no need reference any repositories within the configuration.

and the Java-based Repository could look something like:

//...
import io.micronaut.coherence.data.annotation.CoherenceRepository;
import io.micronaut.data.repository.CrudRepository;
//...
@CoherenceRepository("book")
public interface BookRepository extends CrudRepository<Book, UUID> {
//    ...
}

NOTE:

The developer may then start adding queries to the interface for Micronaut to implement.

Repository Implementations

While it’s certainly possible to stick with the Repository interfaces defined by Micronaut (see above), this integration offers an abstract class that the Repository implementation may extend that exposes features unique to Coherence.

Don’t Do This

When extending the Coherence AbstractCoherenceRepository, developers must not also implement any of the standard Micronaut data stereotype interfaces (e.g., CrudRepository and the like). In fact, this is the recommended approach to be able to fully utilize Coherence.

//...
import io.micronaut.coherence.data.annotation.CoherenceRepository;
import io.micronaut.coherence.data.AbstractCoherenceRepository;
//...
@CoherenceRepository("book")
public abstract class CoherenceBookRepository extends AbstractCoherenceRepository<Book, UUID> {
}

or for an asynchronous repository:

//...
import io.micronaut.coherence.data.annotation.CoherenceRepository;
import io.micronaut.coherence.data.AbstractCoherenceAsyncRepository;
//...
@CoherenceRepository("book")
public abstract class CoherenceAsyncBookRepository extends AbstractCoherenceAsyncRepository<Book, UUID> {
}

Even though developers are restricted in what interfaces they can implement when extending AbstractCoherenceRepository, it does provide the same features of CrudRepository and more! We recommend reviewing the API docs in detail to get a feel of what is offered.

Example

To see the integration between Coherence and Micronaut Data in action, take a look at the Todo List example. The Micronaut section of the example defines a simple Task model as well as an implementation of AbstractCoherenceRepository that shows off a few features being offered by the API.

6 Inject Coherence Resources into Beans

Micronaut, and dependency injection in general, make it easy for application classes to declare the dependencies they need and let the runtime provide them when necessary. This makes the applications easier to develop, test and reason about, and the code extremely clean.

Micronaut Coherence allows you to do the same for Coherence objects, such as Session, NamedMap, NamedCache, ContinuousQueryCache, ConfigurableCacheFactory, Cluster, etc…​

6.1 Injecting NamedMap & NamedCache

Coherence NamedMap and NamedCache instances can be injected as beans in Micronaut applications. The mechanics of injecting NamedMap or NamedCache beans is identical, so any use of NamedMap in the examples below can be replaced with NamedCache. Other more specialized forms of NamedMap and NamedCache can also be injected, for example the asynchronous forms of both classes, and views.

The simplest way to inject a NamedMap is to just annotate the injection point with @jakarta.inject.Inject.

@Inject
private NamedMap<String, Person> people;

In this example the injection point field name is used to determine the cache name to inject, so a NamedMap bean with an underlying cache name of people will be injected.

Specify the Map/Cache Name

Sometimes the name of the map or cache being injected needs to be different to the injection point name. This is always the case when injecting into method parameters as the parameter names are lost by the time the injection point is processed. In this case we can use the @Name annotation to specify the underlying cache name.

The example below will inject a NamedMap that uses an underlying cache named people.

@Inject
@Name("people")
private NamedMap<String, Person> map;

The same applies when injecting a constructor or method parameter:

@Singleton
public class SomeBean {
    @Inject
    public SomeBean(@Name("people") NamedMap<String, Person> map) {
        // ToDo: initialize the bean...
    }
}

Specify the Owning Session Name

Whilst most applications probably use a single Coherence Session there are uses-cases where an application may have multiple sessions. In this case, when injecting a NamedMap the specific session can be specified by annotating the injection point with @SessionName.

In the previous examples where no @SessionName was specified Coherence will use the default session to obtain the caches.

Assuming the application has multiple sessions configured, one of which is named Catalog the following example injects a NamedMap from an underlying cache named products in the Catalog session.

@Inject
@SessionName("Catalog")
@Name("products")
private NamedMap<String, Product> map;

Again, the same annotation can be used on method parameter injection points.

@Controller
public class CatalogController {
    @Inject
    public CatalogController(@SessionName("Catalog") @Name("products")
                             NamedMap<String, Product> products) {
        // ToDo: initialize the bean...
    }
}

6.1.1 Injecting AsyncNamedMap & AsyncNamedCache

It is possible to inject the asynchronous classes AsyncNamedMap and AsyncNamedCache as beans in exactly the same way as described above. Just change the type of the injection point to be AsyncNamedMap or AsyncNamedCache.

@Inject
@Name("people")
private AsyncNamedMap<String, Person> map;

6.1.2 Injecting Views (CQC)

View (or ContinuousQueryCache) beans can be injected by specifying the @View annotation at the injection point. A view is a sub-set of the data in an underlying cache, controlled by a Filter.

@Inject
@Name("people")
@View                                    (1)
private NamedMap<String, Person> map;
1 The injection point has been annotated with @View, so the injected NamedMap will actually be an implementation of a ContinuousQueryCache.

In the above example, no Filter has been specified, so the default behaviour is to use an AlwaysFilter. This means that the view will contain all the entries from the underlying cache (typically a distributed cache). As a ContinuousQueryCache will hold keys and values locally in deserialized form this can often a better approach than using a replicated cache.

Specify a View Filter

Filters are specified for views using a special filter binding annotation. These are annotations that are themselves annotated with the meta-annotation @FilterBinding. The Micronaut Coherence framework comes with some built in implementations, for example @AlwaysFilter, and @WhereFilter, and it is simple to implement other as required by applications (see the Filter Binding Annotation section for more details).

For example, if there was a cache named "people", containing Person instances, and the application required a view of that cache just containing People where the lastName attribute is equal to Simpson the @WhereFilter filter binding annotation could be used to specify the Filter. The @WhereFilter annotation produces a Filter created from a Coherence CohQL where clause, in this case lastName == 'Simpson'.

@Inject
@Name("people")
@View
@WhereFilter("lastName = 'Simpson'")
private NamedMap<String, Person> simpsons;

Other built-in or custom filter binding annotations can be used. Multiple filter-binding annotations can be added to the same injection point to build up more complex views. The Filter instances produced from each filter binding annotation will all be collected together in an AllFilter, which will logically AND then together.

For example:

@Inject
@Name("people")
@View
@WhereFilter("lastName = 'Simpson'")
@WhereFilter("age > 10")
private NamedMap<String, Person> simpsons;

The view injected above will be all People with a lastName attribute equal to Simpson and an age attribute greater than 10. Equivalent to lastName = 'Simpson' && age > 10.

Specify a View Transformer

The values in a view map do not have to be the same as the value sin the underlying cache, a ValueExtractor can be used to transform the actual cache value into a different value in the view. ValueExtractor are specified for views using a special extractor binding annotation. These are annotations that are themselves annotated with the meta-annotation @ExtractorBinding. The Micronaut Coherence framework comes with some built in implementations, for example @PropertyExtractor, and it is simple to implement other as required by applications (see the Extractor Binding Annotation section for more details).

For example, if there was a cache named "people", containing Person instances, and the application required a view where the value was just the age attribute of each Person rather than the whole cache value. A @PropertyExtractor annotation could be used to specify that the values should be transformed using a property extractor.

@Inject
@View                                       (1)
@Name("people")                             (2)
@PropertyExtractor("age")                   (3)
private NamedMap<String, Integer> ages;     (4)
1 The @View annotation specifies that a view will be injected rather than a raw NamedMap.
2 The name of the underlying map for the view is people.
3 The @PropertyExtractor annotation specifies that a ValueExtractor should be used to transform the underlying cache values into different values in the view. In this case the @PropertyExtractor annotation will produce a value extractor to extract the age property.
4 Note that the map injected is now a NamedMap<String, Integer> with generic types of String and Integer because the values have been transformed from Person to Integer.

Multiple extractor bindings can be applied to the injection point, in which case the view value will be a List of the extracted attributes.

Custom extractor binding annotations can be created to fulfil more complex transformations.

6.2 Injecting NamedTopic

Coherence NamedTopic instances can be injected as beans in Micronaut applications.

An alternative way to write message driven applications and TransferEvent`microservices instead of directly injecting `NamedTopic, Publisher or Subscriber beans is to use Micronaut Coherence Messaging.

The simplest way to inject a NamedTopic is to just annotate the injection point with @jakarta.inject.Inject.

@Inject
private NamedTopic<Person> people;

In this example the injection point field name is used to determine the topic name to inject, so a NamedTopic bean with an underlying topic name of people will be injected.

As an alternative to using a NamedTopic directly in code, Coherence Micronaut also supports annotating methods directly as publishers and subscribers using the same approach as Micronaut Messaging. See the Micronaut Messaging with Coherence Topics section of the documentation.

Specify the Topic Name

Sometimes the name of the topic being injected needs to be different to the injection point name. This is always the case when injecting into method parameters as the parameter names are lost by the time the injection point is processed. In this case we can use the @Name annotation to specify the underlying cache name.

The example below will inject a NamedTopic that uses an underlying topic named orders.

@Inject
@Name("people")
private NamedTopic<Order> orders;

The same applies when injecting a constructor or method parameter:

@Singleton
public class SomeBean {
    @Inject
    public SomeBean(@Name("orders") NamedTopic<Order> topic) {
        // ToDo:
    }
}

Specify the Session Name

Whilst most applications probably use a single Coherence Session there are uses-cases where an application may have multiple sessions. In this case, when injecting a NamedTopic the specific session can be specified by annotating the injection point with @SessionName.

In the previous examples where no @SessionName was specified Coherence will use the default session to obtain the caches.

For example, assume the application has multiple sessions configured, one of which is named Customers For example, assume the application has multiple sessions configured, one of which is named Customers the following code snippet injects a NamedTopic using an underlying topic named orders in the Customers session.

@Inject
@SessionName("Customers")
@Name("orders")
private NamedTopic<Order> topic;

Again, the same annotation can be used on method parameter injection points.

@Controller
public class OrderProcessor {
    @Inject
    public OrderProcessor(@SessionName("Customers") @Name("orders")
                          NamedTopic<Order> orders) {
        // ToDo:
    }
}

6.2.1 Injecting a NamedTopic Publisher

If application code only needs to publish messages to a Coherence NamedTopic then instead of injecting a NamedTopic bean, a Publisher bean can be injected.

The simplest way to inject a Publisher is just to annotate the injection point of type Publisher with @Inject, for example:

@Inject
private Publisher<Order> orders;

The example above will inject a Publisher bean, the name of the underlying NamedTopic will be taken from the name of the injection point, in this case orders.

Specify the Topic Name

If the name of the injection point cannot be used as the NamedTopic name, which is always the case with injection points that are method or constructor parameters, then the @Name annotation can be used to specify the topic name.

For example, both of the code snippets below inject a Publisher that published to the orders topic:

@Inject
@Name("orders")
private Publisher<Order> orders;
@Controller
public class OrderController {
    @Inject
    public OrderController(@Name("orders") Publisher<Order> topic) {
        // ToDo:
    }
}

Specify the Owning Session

As with injection of NamedTopics, in applications using multiple Session instances, the name of the Session that owns the underlying NamedTopic can be specified when injecting a Publisher by adding the @SessionName annotation.

@Inject
@Name("orders")
@SessionName("Customers")
private Publisher<Order> orders;

6.2.2 Injecting a NamedTopic Subscriber

If application code only needs to subscribe to messages from a Coherence NamedTopic then instead of injecting a NamedTopic bean, a Subscriber bean can be injected.

The simplest way to inject a Subscriber is just to annotate the injection point of type Subscriber with @Inject, for example:

@Inject
private Subscriber<Order> orders;

The example above will inject a Subscriber bean, the name of the underlying NamedTopic will be taken from the name of the injection point, in this case orders.

Specify the Topic Name

If the name of the injection point cannot be used as the NamedTopic name, which is always the case with injection points that are method or constructor parameters, then the @Name annotation can be used to specify the topic name.

For example, both of the code snippets below inject a Subscriber that subscribe to the orders topic:

@Inject
@Name("orders")
private Subscriber<Order> orders;
@Controller
public class OrderController {
    @Inject
    public OrderController(@Name("orders") Subscriber<Order> topic) {
        // ToDo:
    }
}

Specify the Owning Session

As with injection of NamedTopics, in applications using multiple Session instances, the name of the Session that owns the underlying NamedTopic can be specified when injecting a Subscriber by adding the @SessionName annotation.

@Inject
@Name("orders")
@SessionName("Customers")
private Subscriber<Order> orders;

6.2.2.1 Filtering Topic Messages

A Subscriber can filter the messages that it receives from the NamedTopic by using a Filter. Filtering of messages takes place on the server, so for use cases where the application is only interested in a sub-set of messages, this can be more efficient than bringing all messages back to the client for processing.

Filters are specified for a Subscriber using a special filter binding annotation. These are annotations that are themselves annotated with the meta-annotation @FilterBinding. The Micronaut Coherence framework comes with some built in implementations, for example @AlwaysFilter, and @WhereFilter, and it is simple to implement other as required by applications (see the Filter Binding Annotation section for more details).

For example, both of the code snippets below inject a Subscriber that subscribe to the orders topic, but only receives Order messages where the productId attribute is AB1234. the @WhereFilter filter binding annotation is used to specify the Filter, as @WhereFilter takes a Coherence CohQL where clause and converts it to a Filter.

@Inject
@Name("orders")
@WhereFilter("productId = 'AB1234'")
private Subscriber<Order> orders;
@Controller
public class OrderController {
    @Inject
    public OrderController(@Name("orders") @WhereFilter("productId = 'AB1234'") Subscriber<Order> topic) {
        // ToDo:
    }
}

6.2.2.2 Transforming Topic Messages

A Subscriber can be configured to receive just a part of a NamedTopic message using a ValueExtractor. The message transformation happens on the server so in use-cases where a Subscriber only requires a part of a message, it can be much more efficient to extract the required parts on the server instead of bringing the full payload back to the client.

ValueExtractors are specified for a Subscriber using a special extractor binding annotation. These are annotations that are themselves annotated with the meta-annotation @ExtractorBinding. The Micronaut Coherence framework comes with some built in implementations, for example @PropertyExtractor, and @PofExtractor, and it is simple to implement other as required by applications (see the Extractor Binding Annotation section for more details).

For example, assuming that there is a topic named orders containing Order instances as the payload. If a Subscriber is required to subscribe to the orders topic, but only needs to receive the productId values, then a @PropertyExtractor annotation could be used. A @PropertyExtractor specifies the name of a property to extract.

Both code snippets below will inject a Subscriber that just receives the productId property of Orders objects from the orders topic.

@Inject
@Name("orders")
@PropertyExtractor("productId")
private Subscriber<Order> orders;
@Controller
public class OrderController {
    @Inject
    public OrderController(@Name("orders") @PropertyExtractor("productId")
                           Subscriber<Order> topic) {
        // ToDo:
    }
}

6.3 Injecting a Session

Sometimes it might not be possible to inject a Coherence resource, such as NamedMap or NamedCache directly because the name of the resource to be injected is not known until runtime. In this case it makes sense to inject a Session instance which can then be used to obtain other resources.

The simplest way to inject a Session is just to annotate a field, method parameter, or other injection point.

@Controller
public class MyBean {
    @Inject
    private Session session;

or into a constructor:

@Controller
public class MyBean {
    @Inject
    public MyBean(Session session) {
        // ToDo...
    }
}

Both examples above will inject the default Session instance into the injection point.

Specify a Session Name

For most applications that only use a single Session the simple examples above will be all that is required. Some applications though may use multiple named Session instances, in which case the Session name need to be specified. This can be done by adding the @Name annotation to the injection point.

@Controller
public class MyBean {
    @Inject
    @Name("Catalog")
    private Session session;

or into a constructor:

@Controller
public class MyBean {
    @Inject
    public MyBean(@Name("Catalog") Session session) {
        // ToDo...
    }
}

In both examples above the Session injected will be the Catalog session. The named Session must have previously been configured when bootstrapping Coherence.

7 Events

Event driven patterns are a common way to build scalable applications and microservices. Coherence produces a number of events that can be used by applications to respond to data changes and other actions in Coherence.

There are two types of events in Coherence. There are MapEvents, that in a traditional Coherence application are subscribed to using MapListener and there are a number of different types of Event, that are in a traditional Coherence application are subscribed to using an EventInterceptor.

Micronaut makes subscribing to both of these event types much simpler, using observer methods annotated with @CoherenceEventListener.

For example,

@CoherenceEventListener
void onEvent(CoherenceLifecycleEvent event) {
    // ToDo: process event...
}

The method above receives all events of type CoherenceLifecycleEvent emitted during the lifetime of the application.

The actual events received can be controlled further by annotating the method or parameter.

7.1 MapEvent Listeners

Listening for changes to data in Coherence is a common use case in applications. Typically, this involves creating an implementation of a MapListener and adding that listener to a NamedMap or NamedCache. Using Coherence Micronaut makes this much simpler by just requiring Micronaut beans with suitably annotated observer methods that will receive events.

MapEvent Observer Methods

A MapEvent observer method is a method on a Micronaut bean that is annotated with @CoherenceEventListener. The method has a void return type and takes a single method parameter of type MapEvent, typically this has the generic types of the underlying map/cache key and value.

For example, assuming that there is a map/cache named people, with keys of type String and values of type Person, and the application has logic that should be executed each time a new Person is inserted into the map:

import com.tangosol.util.MapEvent;
import io.micronaut.coherence.annotation.*;

@Controller                                                    (1)
public class PersonController {

    @CoherenceEventListener                                    (2)
    public void onNewPerson(@MapName("people")                 (3)
                            @Inserted                          (4)
                            MapEvent<String, Person> event) {
        // ToDo: process the event
    }
}
1 The PersonController is a simple Micronaut bean, in this case a Controller.
2 The onNewPerson method is annotated with @CoherenceEventListener making it a Coherence event listener.
3 The @MapName("people") annotation specifies the name of the map to receive events from, in this case people.
4 The @Inserted annotation specified that only Inserted events should be sent to this method.

The above example is very simple, there are a number of other annotations that control what events are received from where.

Specify the Map/Cache name

By default, a MapEvent observer method would receive events for all maps/caches. In practice though, this would not be a very common use case and normally an observer method would be for a specific cache.

The Coherence Micronaut API contains two annotations for specifying the map name @MapName, or cache name @CacheName. Both annotations take a single String value that is the name of the map or cache that events should be received from.

For example,

@CoherenceEventListener
public void onEvent(MapEvent<String, String> event) {
    // ToDo: process the event
}

The above method receives events for all caches.

@CoherenceEventListener
public void onEvent(@MapName("foo")  (1)
                    MapEvent<String, String> event) {
    // ToDo: process the event
}
1 The above method receives events for the map named foo.
@CoherenceEventListener
public void onEvent(@CacheName("foo")  (1)
                    MapEvent<String, String> event) {
    // ToDo: process the event
}
1 The above method receives events for the cache named bar.

Specify the Cache Service name

As well as restricting the received events to a specific map or cache name, events can be restricted to only events from a specific cache service. In Coherence all caches are owned by a cache service, which have a unique name. By default, a MapEvent observer method would receive events for a matching cache name on all services. If an applications Coherence configuration has multiple services, the events can be restricted to just specific services using the @ServiceName annotation.

For example,

@CoherenceEventListener
public void onEvent(@MapName("foo")  (1)
                    MapEvent<String, String> event) {
    // ToDo: process the event
}
1 The above method receives events for the map named foo on all cache services.
@CoherenceEventListener
public void onEvent(@MapName("foo")
                    @ServiceName("Storage")  (1)
                    MapEvent<String, String> event) {
    // ToDo: process the event
}
1 The above method receives events for the cache named foo owned by the cache service named Storage.
@CoherenceEventListener
public void onEvent(@ServiceName("Storage")  (1)
                    MapEvent<String, String> event) {
    // ToDo: process the event
}
1 The above method receives events for the all caches owned by the cache service named Storage as there is no @MapName or @CacheName annotation.

Specify the Owning Session name

In applications that use multiple Sessions there may be a situation where more than one session has a map with the same name, and an observer method needs to restrict the events it receives to a specific session. The events can be restricted to maps and/or caches in specific sessions using the @SessionName annotation.

For example,

@CoherenceEventListener
public void onEvent(@MapName("orders")  (1)
                    MapEvent<String, String> event) {
    // ToDo: process the event
}
1 The above method receives events for the map named orders in all sessions.
@CoherenceEventListener
public void onEvent(@MapName("orders")
                    @SessionName("Customer")  (1)
                    MapEvent<String, Order> event) {
    // ToDo: process the event
}
1 The above method receives events for the map named orders owned by the Session named Customer.
@CoherenceEventListener
public void onEvent(@SessionName("Customer")  (1)
                    MapEvent<String, Order> event) {
    // ToDo: process the event
}
1 The above method receives events for the all caches owned by the Session named Customer as there is no @MapName or @CacheName annotation.

In an application with multiple sessions, events can be routed by session, for example:

@CoherenceEventListener
public void onCustomerOrders(@SessionName("Customer")  (1)
                             @MapName("orders")
                             MapEvent<String, Order> event) {
    // ToDo: process the event
}

@CoherenceEventListener
public void onCatalogOrders(@SessionName("Catalog")   (2)
                            @MapName("orders")
                            MapEvent<String, Order> event) {
    // ToDo: process the event
}
1 The onCustomerOrders will receive events for the orders map owned by the Session named Customer.
2 The onCatalogOrders will receive events for the orders map owned by the Session named Catalog.

7.1.1 Receive Specific Event Types

There are three types of event that a MapEvent observer method can receive, Insert, Update and Delete. By default, an observer method will receive all events for the map (or maps) it applies to. This can be controlled using the following annotations:

Zero or more of the above annotations can be used to annotate the MapEvent parameter of the observer method.

For example,

@CoherenceEventListener
public void onEvent(@MapName("test")
                    @Inserted        (1)
                    MapEvent<String, String> event) {
    // ToDo: process the event
}
1 Only Insert events for the map test will be received.
@CoherenceEventListener
public void onEvent(@MapName("test")
                    @Inserted @Deleted       (1)
                    MapEvent<String, String> event) {
    // ToDo: process the event
}
1 Only Insert and Delete events for the map test will be received.
@CoherenceEventListener
public void onEvent(@MapName("test") MapEvent<String, String> event) {
    // ToDo: process the event
}

All events for the map test will be received.

7.1.2 Filtering Events

The MapEvents received by an observer method can be further restricted by applying a filter. Filters are applied by annotating the method with a filter binding annotation, which is a link to a factory that creates a specific instance of a Filter. Event filters applied in this way are executed on the server, which can make receiving events more efficient for clients as the event will not be sent from the server at all.

The Micronaut Coherence framework comes with some built in implementations, for example @AlwaysFilter, and @WhereFilter, and it is simple to implement other as required by applications (see the Filter Binding Annotation section for more details).

For example, assume there is a map named people with keys of type String and values of type People, and an observer method needs to receive events for all values where the age property is 18 or over. A custom filter binding annotation could be written to create the required Filter, but as the condition is very simple, in this example the built in @WhereFilter filter binding annotation will be used with a where clause of age >= 18.

@WhereFilter("age >= 18")     (1)
@CoherenceEventListener
@MapName("people")
public void onAdult(MapEvent<String, Person> people) {
    // ToDo: process event...
}
1 The @WhereFilter annotation is applied to the method.

The onAdult method above will receive all events emitted from the people map, but only for entries where the value of the age property of the entry value is >= 18.

7.1.3 Transforming Events

In some use-cases the MapEvent observer method does not require the whole map or cache value to process, it might only require one, or a few, properties of the value, or it might require some calculated value. This can be achieved by using an event transformer to transform the values that will be received by the observer method. The transformation takes place on the server before the event is emitted to the method. This can improve efficiency on a client in cases where the cache value is large but the client only requires a small part of that value because only the required values are sent over the wire to the client.

In the Coherence Micronaut framework, event values are transformed using a ValueExtractor. A ValueExtractor is a simple interface that takes in one value and transforms it into another value. The ValueExtractor is applied to the event value; events contain both a new and old values and the extractor is applied to both as applicable. For Insert events there is only a new value, for Update events there will be both a new and old value and for Delete events there will only be an old value. The extractor is not applied to the event key.

The ValueExtractor to use for a MapEvent observer method is indicated by annotating the method with an extractor binding annotation. An extractor binding is an annotation that is itself annotated with the meta-annotation @ExtractorBinding. The extractor binding annotation is a link to a corresponding ExtractorFactory that will build an instance of a ValueExtractor.

For example, assuming that there is a NamedMap with the name orders that has keys of type String and values of type Order; the Order class has a customerId property of type String. A MapEvent observer method is only interested in the customerId for an order so the built-in extractor binding annotation @PropertyExtractor can be used to just extract the customerId from the event:

@CoherenceEventListener
@PropertyExtractor("customerId")                        (1)
public void onOrder(@MapName("orders")                  (2)
                    MapEvent<String, String> event) {   (3)
    // ToDo: process event...
}
1 The method is annotated with @PropertyExtractor to indicate that a ValueExtractor that just extracts the customerId property should be used to transform the event.
2 The map name to receive events from is set to orders
3 Note that the generic types of the MapEvent parameter are now MapEvent<String, String> instead of MapEvent<String, Order> because the event values will have been transformed from an Order into just the String customerId.

It is possible to apply multiple filter binding annotations to a method. In this case the extractors are combined into a Coherence ChainedExtractor, which will return the extracted values as a java.util.List.

Expanding on the example above, if the Order class also has an orderId property of type Long, and an observer method, only interested in Insert events needs both the customerId and orderId, then the method can be annotated with an two @PropertyExtractor annotations:

@CoherenceEventListener
@PropertyExtractor("customerId")                     (1)
@PropertyExtractor("orderId")
public void onOrder(@Inserted                        (2)
                    @MapName("orders")
                    MapEvent<String, List<Object>> event) {  (3)
    List list = event.getNewValue();
    String customerId = (String) list.get(0);        (4)
    Long orderId = (Long) list.get(1);
    // ...
}
1 The method is annotated with two @PropertyExtractor annotations, one to extract customerId and one to extract orderId.
2 The method parameter is annotated with @Inserted so that the method only receives Insert events.
3 The MapEvent parameter not has a key of type String and a value of type List<Object>, because the values from the multiple extractors will be returned in a List. We cannot use a generic value narrower than Object for the list because it will contain a String and a Long.
4 The extracted values can be obtained from the list, they will be in the same order that the annotations were applied to the method.

7.2 Coherence Event Interceptors

Coherence produces many events in response to various serverside and client side actions. For example, Lifecycle events for Coherence itself, maps and cache, Entry events when data in maps and caches changes, Partition events for partition lifecycle and distribution, EntryProcessor events when invoked on a map or cache, etc. In a stand-alone Coherence application these events are subscribed to using a EventInterceptor implementation registered to listen to specific event types.

The Coherence Micronaut API makes subscribing to these events much simpler, by using the same approach used for Micronaut events, namely annotated event observer methods. A Coherence event observer method is a method annotated with @CoherenceEventListener that has a void return type and a single parameter of the type of event to be received. The exact events received can be further controlled by applying other annotations to the method or event parameter. The annotations applied will vary depending on the type of the event.

Event Types

The different types of event that can be observed are listed below:

Most of the events above only apply on storage enabled cluster members. For example, an EntryEvent will only be emitted for mutation of an entry on the storage enabled cluster member that owns that entry. Whereas lifecycle events may be emitted on all members, such as CacheLifecycle event that may be emitted on any member when a cache is created, truncated, or destroyed.

7.2.1 Coherence Lifecycle Events

CoherenceLifecycleEvents are emitted to indicate the lifecycle of a Coherence instance.

To subscribe to CoherenceLifecycleEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type CoherenceLifecycleEvent.

CoherenceLifecycleEvent are emitted by Coherence instances and will only be received in the same JVM, which could be a cluster member or a client.

For example, the onEvent method below will receive lifecycle events for all Coherence instances in the current application:

@CoherenceEventListener
public void onEvent(CoherenceLifecycleEvent event) {
    // ToDo: process the event
}

Receive Specific CoherenceLifecycleEvent Types

There are four different types of CoherenceLifecycleEvent. By adding the corresponding annotation to the method parameter the method will only receive the specified events.

  • Starting - a Coherence instance is about to start, use the @Starting annotation

  • Started - a Coherence instance has started, use the @Started annotation

  • Stopping - a Coherence instance is about to stop, use the @Stopping annotation

  • Stopped - a Coherence instance has stopped, use the @Stopped annotation

For example, the method below will only receive Started and Stopped events.

@CoherenceEventListener
public void onEvent(@Started @Stopped CoherenceLifecycleEvent event) {
    // ToDo: process the event
}

Receive CoherenceLifecycleEvents for a Specific Coherence Instance

Each Coherence instance in an application has a unique name. The observer method can be annotated to only receive events associated with a specific Coherence instance by using the @Name annotation.

For example, the method below will only receive events for the Coherence instance named customers:

@CoherenceEventListener
public void onEvent(@Name("customers") CoherenceLifecycleEvent event) {
    // ToDo: process the event
}

The method in this example will receive events for the default Coherence instance:

@CoherenceEventListener
public void onEvent(@Name(Coherence.DEFAULT_NAME) CoherenceLifecycleEvent event) {
    // ToDo: process the event
}

7.2.2 Session Lifecycle Events

SessionLifecycleEvents are emitted to indicate the lifecycle event of a Session instance.

To subscribe to SessionLifecycleEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type SessionLifecycleEvent.

SessionLifecycleEvent are emitted by Session instances and will only be received in the same JVM, which could be a cluster member or a client.

For example, the onEvent method below will receive lifecycle events for all Session instances in the current application:

@CoherenceEventListener
public void onEvent(SessionLifecycleEvent event) {
    // ToDo: process the event
}

Receive Specific SessionLifecycleEvent Types

There are four different types of SessionLifecycleEvent. By adding the corresponding annotation to the method parameter the method will only receive the specified events.

  • Starting - a Coherence instance is about to start, use the @Starting annotation

  • Started - a Coherence instance has started, use the @Started annotation

  • Stopping - a Coherence instance is about to stop, use the @Stopping annotation

  • Stopped - a Coherence instance has stopped, use the @Stopped annotation

For example, the method below will only receive Started and Stopped events.

@CoherenceEventListener
public void onEvent(@Started @Stopped SessionLifecycleEvent event) {
    // ToDo: process the event
}

Receive SessionLifecycleEvents for a Specific Session Instance

Each Session instance in an application has a name. The observer method can be annotated to only receive events associated with a specific Session instance by using the @Name annotation.

For example, the method below will only receive events for the Session instance named customers:

@CoherenceEventListener
public void onEvent(@Name("customers") SessionLifecycleEvent event) {
    // ToDo: process the event
}

The method in this example will receive events for the default Coherence instance:

@CoherenceEventListener
public void onEvent(@Name(Coherence.DEFAULT_NAME) SessionLifecycleEvent event) {
    // ToDo: process the event
}

7.2.3 ConfigurableCacheFactory Lifecycle Events

LifecycleEvent are emitted to indicate the lifecycle of a ConfigurableCacheFactory instance.

To subscribe to LifecycleEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type LifecycleEvent.

LifecycleEvent are emitted by ConfigurableCacheFactory instances and will only be received in the same JVM, which could be a cluster member or a client.

For example, the onEvent method below will receive lifecycle events for all ConfigurableCacheFactory instances in the current application:

@CoherenceEventListener
public void onEvent(LifecycleEvent event) {
    // ToDo: process the event
}

Receive Specific LifecycleEvent Types

There are four different types of LifecycleEvent. By adding the corresponding annotation to the method parameter the method will only receive the specified events.

  • Activating - a ConfigurableCacheFactory instance is about to be activated, use the @Activating annotation

  • Activated - a ConfigurableCacheFactory instance has been activated, use the @Activated annotation

  • Disposing - a ConfigurableCacheFactory instance is about to be disposed, use the @Disposing annotation

For example, the method below will only receive Activated and Disposing events.

@CoherenceEventListener
public void onEvent(@Activated @Disposing LifecycleEvent event) {
    // ToDo: process the event
}

7.2.4 Cache Lifecycle Events

CacheLifecycleEvent are emitted to indicate the lifecycle of a cache instance.

To subscribe to CacheLifecycleEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type CacheLifecycleEvent.

For example, the onEvent method below will receive lifecycle events for all caches.

@CoherenceEventListener
public void onEvent(CacheLifecycleEvent event) {
    // ToDo: process the event
}

Receive Specific CacheLifecycleEvent Types

There are three types of `CacheLifecycleEvent:

  • Created - a cache instance has been created, use the @Created annotation

  • Truncated - a cache instance has been truncated (all data was removed), use the @Truncated annotation

  • Destroyed - a cache has been destroyed (destroy is a cluster wide operation, so the cache is destroyed on all members of the cluster and clients) use the @Destroyed annotation

For example, the method below will only receive Created and Destroyed events for all caches.

@CoherenceEventListener
public void onEvent(@Created @Destroyed CacheLifecycleEvent event) {
    // ToDo: process the event
}

Receive CacheLifecycleEvents for a Specific NamedMap or NamedCache

To only receive events for a specific NamedMap annotate the method parameter with the @MapName annotation. To only receive events for a specific NamedCache annotate the method parameter with the @CacheName annotation.

The @MapName and @CacheName annotations are actually interchangeable so use whichever reads better for your application code, i.e. if your code is dealing with NamedMap used @MapName. At the storage level, where the events are generated a NamedMap and NamedCache are the same.

The method below will only receive events for the map named orders:

@CoherenceEventListener
public void onEvent(@MapName("orders") CacheLifecycleEvent event) {
    // ToDo: process the event
}

Receive CacheLifecycleEvents from a Specific Cache Service

Caches are owned by a Cache Service, it is possible to restrict events received by a method to only those related to caches owned by a specific service by annotating the method parameter with the @ServiceName annotation.

The method below will only receive events for the caches owned by the service named StorageService:

@CoherenceEventListener
public void onEvent(@ServiceName("StorageService") CacheLifecycleEvent event) {
    // ToDo: process the event
}

Receive CacheLifecycleEvents from a Specific Session

A typical use case is to obtain NamedCache and NamedMap instances from a Session. It is possible to restrict events received by a method to only those related to caches owned by a specific Session by annotating the method parameter with the @SessionName annotation.

The method below will only receive events for the caches owned by the Session named BackEnd:

@CoherenceEventListener
public void onEvent(@SessionName("BackEnd") CacheLifecycleEvent event) {
    // ToDo: process the event
}

7.2.5 Entry Events

An EntryEvent is emitted when a EntryProcessor is invoked on a cache. These events are only emitted on the storage enabled member that is the primary owner of the entry that the EntryProcessor is invoked on.

To subscribe to EntryEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type EntryEvent.

For example, the onEvent method below will receive entry events for all caches.

@CoherenceEventListener
public void onEvent(EntryEvent event) {
    // ToDo: process the event
}

Receive Specific EntryEvent Types

There are a number of different EntryEvent types.

  • Inserting - an entry is being inserted into a cache, use the @Inserting annotation

  • Inserted - an entry has been inserted into a cache, use the @Inserted annotation

  • Updating - an entry is being updated in a cache, use the @Updating annotation

  • Updated - an entry has been updated in a cache, use the @Updated annotation

  • Deleting - an entry is being deleted from a cache, use the @Deleting annotation

  • Deleted - an entry has been deleted from a cache, use the @Deleted annotation

To restrict the EntryEvent types received by a method apply one or more of the annotations above to the method parameter. For example, the method below will receive Inserted and Deleted events.

@CoherenceEventListener
public void onEvent(@Inserted @Deleted EntryEvent event) {
    // ToDo: process the event
}

The event types fall into two categories, pre-events (those name *ing) and post-events, those named *ed). Pre-events are emitted synchronously before the entry is mutated. Post-events are emitted asynchronously after the entry has been mutated.

As pre-events are synchronous the listener method should not take a long time to execute as it is blocking the cache mutation and could obviously be a performance impact. It is also important that developers understand Coherence reentrancy as the pre-events are executing on the Cache Service thread so cannot call into caches owned by the same service.

Receive EntryEvents for a Specific NamedMap or NamedCache

To only receive events for a specific NamedMap annotate the method parameter with the @MapName annotation. To only receive events for a specific NamedCache annotate the method parameter with the @CacheName annotation.

The @MapName and @CacheName annotations are actually interchangeable so use whichever reads better for your application code, i.e. if your code is dealing with NamedMap used @MapName. At the storage level, where the events are generated a NamedMap and NamedCache are the same.

The method below will only receive events for the map named orders:

@CoherenceEventListener
public void onEvent(@MapName("orders") EntryEvent event) {
    // ToDo: process the event
}

Receive EntryEvents from a Specific Cache Service

Caches are owned by a Cache Service, it is possible to restrict events received by a method to only those related to caches owned by a specific service by annotating the method parameter with the @ServiceName annotation.

The method below will only receive events for the caches owned by the service named StorageService:

@CoherenceEventListener
public void onEvent(@ServiceName("StorageService") EntryEvents event) {
    // ToDo: process the event
}

Receive EntryEvents from a Specific Session

A typical use case is to obtain NamedCache and NamedMap instances from a Session. It is possible to restrict events received by a method to only those related to caches owned by a specific Session by annotating the method parameter with the @SessionName annotation.

The method below will only receive events for the caches owned by the Session named BackEnd:

@CoherenceEventListener
public void onEvent(@SessionName("BackEnd") EntryEvents event) {
    // ToDo: process the event
}

7.2.6 EntryProcessor Events

An EntryProcessorEvent is emitted when a mutation occurs on an entry in a cache. These events are only emitted on the storage enabled member that is the primary owner of the entry.

To subscribe to EntryProcessorEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type EntryProcessorEvent.

For example, the onEvent method below will receive entry events for all caches.

@CoherenceEventListener
public void onEvent(EntryProcessorEvent event) {
    // ToDo: process the event
}

Receive Specific EntryProcessorEvent Types

There are a number of different EntryProcessorEvent types.

  • Executing - an EntryProcessor is being invoked on a cache, use the @Executing annotation

  • Executed - an EntryProcessor has been invoked on a cache, use the @Executed annotation

To restrict the EntryProcessorEvent types received by a method apply one or more of the annotations above to the method parameter. For example, the method below will receive Executed events.

@CoherenceEventListener
public void onEvent(@Executed EntryProcessorEvent event) {
    // ToDo: process the event
}

The event types fall into two categories, pre-event ('Executing') and post-event (Executed). Pre-events are emitted synchronously before the EntryProcessor is invoked. Post-events are emitted asynchronously after the EntryProcessor has been invoked.

As pre-events are synchronous the listener method should not take a long time to execute as it is blocking the EntryProcessor invocation and could obviously be a performance impact. It is also important that developers understand Coherence reentrancy as the pre-events are executing on the Cache Service thread so cannot call into caches owned by the same service.

Receive EntryProcessorEvents for a Specific NamedMap or NamedCache

To only receive events for a specific NamedMap annotate the method parameter with the @MapName annotation. To only receive events for a specific NamedCache annotate the method parameter with the @CacheName annotation.

The @MapName and @CacheName annotations are actually interchangeable so use whichever reads better for your application code, i.e. if your code is dealing with NamedMap used @MapName. At the storage level, where the events are generated a NamedMap and NamedCache are the same.

The method below will only receive events for the map named orders:

@CoherenceEventListener
public void onEvent(@MapName("orders") EntryProcessorEvent event) {
    // ToDo: process the event
}

Receive EntryProcessorEvents from a Specific Cache Service

Caches are owned by a Cache Service, it is possible to restrict events received by a method to only those related to caches owned by a specific service by annotating the method parameter with the @ServiceName annotation.

The method below will only receive events for the caches owned by the service named StorageService:

@CoherenceEventListener
public void onEvent(@ServiceName("StorageService") EntryProcessorEvents event) {
    // ToDo: process the event
}

Receive EntryProcessorEvents from a Specific Session

A typical use case is to obtain NamedCache and NamedMap instances from a Session. It is possible to restrict events received by a method to only those related to caches owned by a specific Session by annotating the method parameter with the @SessionName annotation.

The method below will only receive events for the caches owned by the Session named BackEnd:

@CoherenceEventListener
public void onEvent(@SessionName("BackEnd") EntryProcessorEvents event) {
    // ToDo: process the event
}

7.2.7 Partition Level Transaction Events

A TransactionEvent is emitted in relation to all mutations in a single partition in response to executing a single request. These are commonly referred to as partition level transactions. For example, an EntryProcessor that mutates more than one entry (which could be in multiple caches) as part of a single invocation will cause a partition level transaction to occur encompassing all of those cache entries.

Transaction events are emitted by storage enabled cache services, they will only e received on the same member that the partition level transaction occurred.

To subscribe to TransactionEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type TransactionEvent.

For example, the onEvent method below will receive all transaction events emitted by storage enabled cache services in the same JVM.

@CoherenceEventListener
public void onEvent(TransactionEvent event) {
    // ToDo: process the event
}

Receive Specific TransactionEvent Types

There are a number of different TransactionEvent types.

  • Committing - A COMMITTING event is raised prior to any updates to the underlying backing map. This event will contain all modified entries which may span multiple backing maps. Use the @Committing annotation

  • Committed - A COMMITTED event is raised after any mutations have been committed to the underlying backing maps. This event will contain all modified entries which may span multiple backing maps. Use the @Committed annotation

To restrict the TransactionEvent types received by a method apply one or more of the annotations above to the method parameter. For example, the method below will receive Committed events.

@CoherenceEventListener
public void onEvent(@Committed TransactionEvent event) {
    // ToDo: process the event
}

Receive TransactionEvent from a Specific Cache Service

Caches are owned by a Cache Service, it is possible to restrict events received by a method to only those related to caches owned by a specific service by annotating the method parameter with the @ServiceName annotation.

The method below will only receive events for the caches owned by the service named StorageService:

@CoherenceEventListener
public void onEvent(@ServiceName("StorageService") TransactionEvent event) {
    // ToDo: process the event
}

7.2.8 Partition Transfer Events

A TransferEvent captures information concerning the transfer of a partition for a storage enabled member. Transfer events are raised against the set of BinaryEntry instances that are being transferred.

TransferEvents are dispatched to interceptors while holding a lock on the partition being transferred, blocking any operations for the partition. Event observer methods should therefore execute as quickly as possible of hand-off execution to another thread.

To subscribe to TransferEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type TransferEvent.

For example, the onEvent method below will receive all transaction events emitted by storage enabled cache services in the same JVM.

@CoherenceEventListener
public void onEvent(TransferEvent event) {
    // ToDo: process the event
}

Receive Specific TransferEvent Types

There are a number of different TransferEvent types.

  • Arrived - This TransferEvent is dispatched when a set of BinaryEntry instances have been transferred to the local member or restored from backup.The reason for the event (primary transfer from another member or restore from backup) can be derived as follows:

TransferEvent event;
boolean restored = event.getRemoteMember() == event.getLocalMember();

Use the @Arrived annotation to restrict the received events to arrived type.

  • Assigned - This TransferEvent is dispatched when a partition has been assigned to the local member. This event will only be emitted by the ownership senior during the initial partition assignment. Use the @Assigned annotation to restrict received events.

  • Departing - This TransferEvent is dispatched when a set of BinaryEntry are being transferred from the local member. This event is followed by either a Departed or Rollback event to indicate the success or failure of the transfer. Use the @Departing annotation to restrict received events.

  • Departed - This TransferEvent is dispatched when a partition has been successfully transferred from the local member. To derive the BinaryEntry instances associated with the transfer, consumers should subscribe to the Departing event that would precede this event. Use the @Departed annotation to restrict received events.

  • Lost - This TransferEvent is dispatched when a partition has been orphaned (data loss may have occurred), and the ownership is assumed by the local member. This event is only be emitted by the ownership senior. Use the @Lost annotation to restrict received events.

  • Recovered - This TransferEvent is dispatched when a set of BinaryEntry instances have been recovered from a persistent storage by the local member. Use the @Recovered annotation to restrict received events.

  • Rollback - This TransferEvent is dispatched when partition transfer has failed and was therefore rolled back. To derive the BinaryEntry instances associated with the failed transfer, consumers should subscribe to the Departing event that would precede this event. Use the @Rollback annotation to restrict received events.

To restrict the TransferEvent types received by a method apply one or more of the annotations above to the method parameter. For example, the method below will receive Lost events.

@CoherenceEventListener
public void onEvent(@Lost TransferEvent event) {
    // ToDo: process the event
}

Multiple type annotations may be used to receive multiple types of TransferEvent.

Receive TransferEvent from a Specific Cache Service

Caches are owned by a Cache Service, it is possible to restrict events received by a method to only those related to caches owned by a specific service by annotating the method parameter with the @ServiceName annotation.

The method below will only receive events for the caches owned by the service named StorageService:

@CoherenceEventListener
public void onEvent(@ServiceName("StorageService") TransferEvent event) {
    // ToDo: process the event
}

7.2.9 Unsolicited Commit Events

An UnsolicitedCommitEvent captures changes pertaining to all observed mutations performed against caches that were not directly caused (solicited) by the partitioned service. These events may be due to changes made internally by the backing map, such as eviction, or referrers of the backing map causing changes.

Unsolicited commit events are emitted by storage enabled cache services, they will only e received on the same member.

To subscribe to UnsolicitedCommitEvent simply create a Micronaut bean with a listener method annotated with @CoherenceEventListener. The method should have a single parameter of type UnsolicitedCommitEvent.

For example, the onEvent method below will receive all Unsolicited commit events emitted by storage enabled cache services in the same JVM.

@CoherenceEventListener
public void onEvent(UnsolicitedCommitEvent event) {
    // ToDo: process the event
}

8 Messaging with Coherence Topics

Micronaut Coherence integration provides support for message driven applications by integrating Micronaut Messaging and Coherence topics.

A Coherence NamedTopic is analogous to a queue or pub/sub topic, depending on the configuration and application code. Messages published to the topic are stored in Coherence caches, so topics are scalable and performant.

A typical stand-alone Coherence application would create a NamedTopic along with Publisher or Subscriber instances to publish to or subscribe to topics. Injection of topics into Micronaut applications is already covered in Injecting NamedTopics. With Micronaut messaging this becomes much simpler.

With Micronaut Coherence Messaging publishers and subscribers beans are created by writing suitably annotated interfaces.

8.1 Define Publishers - @CoherencePublisher

To create a topic Publisher that sends messages you can simply define an interface that is annotated with @CoherencePublisher.

For example the following is a trivial @CoherencePublisher interface:

ProductClient.java
import io.micronaut.coherence.annotation.CoherencePublisher;
import io.micronaut.coherence.annotation.Topic;

@CoherencePublisher  (1)
public interface ProductClient {

    @Topic("my-products") (2)
    void sendProduct(String message); (3)

    void sendProduct(@Topic String topic, String message); (4)
}
1 The @CoherencePublisher annotation is used to designate this interface as a message publisher.
2 The @Topic annotation indicates which topics the message should be published to
3 The method defines a single parameter, which is the message value. In this case the values being published are String instances, but they could be any type that can be serialized by Coherence.
4 It is also possible for the topic to be dynamic by making it a method argument annotated with .

At compile time Micronaut will produce an implementation of the above interface. You can retrieve an instance of ProductClient either by looking up the bean from the ApplicationContext or by injecting the bean with @Inject:

Using ProductClient
ProductClient client = applicationContext.getBean(ProductClient.class);
client.sendProduct("Blue Trainers");

Note that since the sendProduct method returns void this means the method will send the message and block until the message has been sent. You can return a Future to support non-blocking message delivery.

Reactive and Non-Blocking Method Definitions

The @CoherencePublisher annotation supports the definition of reactive return types (such as Flowable or Reactor Flux) as well as Futures.

The following sections cover possible method signatures and behaviour:

Single Value and Return Type

Single<Book> sendBook(Single<Book> book);

The implementation will return a Single that when subscribed to will subscribe to the passed Single and send the emitted item as a message emitting the item again if successful or an error otherwise.

Flowable Value and Return Type

Flowable<Book> sendBooks(Flowable<Book> book);

The implementation will return a Flowable that when subscribed to will subscribe to the passed Flowable and for each emitted item will send a message emitting the item again if successful or an error otherwise.

Reactor Flux Value and Return Type

Flux<RecordMetadata> sendBooks(Flux<Book> book);

The implementation will return a Reactor Flux that when subscribed to will subscribe to the passed Flux and for each emitted item will send a message emitting the resulting message if successful or an error otherwise.

8.2 Define Subscribers - @CoherenceTopicListener

To listen to Coherence topic messages you can use the @CoherenceTopicListener annotation to define a message listener.

The following example will listen for messages published by the ProductClient in the previous section:

ProductListener.java
import io.micronaut.coherence.annotation.CoherenceTopicListener;
import io.micronaut.coherence.annotation.Topic;

@CoherenceTopicListener   (1)
public class ProductListener {

    @Topic("my-products")   (2)
    public void receive(String product) { (3)
        System.out.println("Got Product - " + product);
    }
}
1 The @CoherenceTopicListener annotation to indicate that this bean is a Coherence topic listener.
2 The @Topic annotation is again used to indicate which topic to subscribe to.
3 The receive method defines single arguments that will receive the message value, in this case the message is of type String.

Method Parameter Bindings

When using a Coherence topic Subscriber directly in application code, the receive method returns an Element, which contains the message value and metadata. The annotated subscriber method can take various parameter types that will bind to the element itself, the message, or the metadata fields of the element.

For example

@CoherenceTopicListener
@Topic("my-products")
public void receive(Element<Product> product) {
    // ... process message ...
}

The method above will be passed the Element received from the topic. By receiving the element, the method has access to the message value and all the metadata stored with the message.

Alternatively, just select parts of the metadata can be passed as parameters.

Parameter Name Parameter Type Description

channel

int or Integer

The channel in the topic that the message was published to

position

com.tangosol.net.topic.Position,

The opaque representation of the message’s position in the channel in the topic.

timestamp

java.time.Instant, long or Long

The time that the message was received on the server

any

com.tangosol.net.topic.Subscriber

The underlying subscriber the message was received from

any

com.tangosol.util.Binary

The message value in serialized form. Message values are lazily deserialized in the received element, so if a handler method just needs the serialized form of the message it can be slightly more efficient to take a Binary parameter.

Some parameters in the table above have fixed parameter names, i.e., channel, position and timestamp. When using these parameters in annotated message handler methods both the parameter name and type must match those in the table above. This is to avoid confusion where the message value type is the same as one of the metadata types where the binding logic would not know what to bind to which parameter. For example, in the unlikely scenario where the message value was just an int and the annotated method was something like public void processMessage(int c, int v) where c is supposed to represent the channel and v the value, the binder would not be able to work this out.

Committing Messages

An important part of Coherence topic subscribers is committing messages to notify the server that they have been processed and guaranteeing at least once delivery. When using Micronaut Coherence messaging every message will be committed after the handler method has successfully processed the message. This behaviour can be controlled by adding a commit strategy to the @CoherenceTopicListener annotation.

Default Commit Behaviour

If no commitStrategy field has been provided to the @CoherenceTopicListener annotation the default behaviour is to synchronously call Element.commit() for every message received.

@CoherenceTopicListener
@Topic("my-products")
public void receive(Element<Product> product) {
    // ... process message ...
}

No commitStrategy field has been supplied to the @CoherenceTopicListener annotation.

Setting Commit Strategy

The @CoherenceTopicListener commitStrategy field is an enumeration of type CommitStrategy with three values, SYNC, ASYNC and MANUAL.

  • CommitStrategy.SYNC - This strategy is the default, and will synchronously commit every message upon successful completion of the handler method, by calling Element.commit().

@CoherenceTopicListener(commitStrategy = CommitStrategy.SYNC)
@Topic("my-products")
public void receive(Product product) {
    // ... process message ...
}
  • CommitStrategy.ASYNC - This strategy will asynchronously commit every message upon successful completion of the handler method, by calling Element.commitAsync().

@CoherenceTopicListener(commitStrategy = CommitStrategy.ASYNC)
@Topic("my-products")
public void receive(Product product) {
    // ... process message ...
}
  • CommitStrategy.MANUAL - This strategy will not automatically commit messages, all handling of commits must be done as part of the handler method or by some external process.

@CoherenceTopicListener(commitStrategy = CommitStrategy.MANUAL)
@Topic("my-products")
public void receive(Element<Product> product) {
    // ... process message ...

    // manually commit the element
    element.commit();
}

In the example above a MANUAL commit strategy has used. The element will be committed by the application code at the end of the handler method. To be able to manually commit a message the method must take the Element as a parameter so that application code can access the commit methods.

8.2.1 Forwarding Messages with @SendTo

On any @@CoherenceTopicListener method that returns a value, you can use the @SendTo annotation to forward the return value to the topic or topics specified by the @SendTo annotation.

The key of the original ConsumerRecord will be used as the key when forwarding the message.

filename.java
import io.micronaut.coherence.annotation.*;
import io.micronaut.messaging.annotation.SendTo;

@CoherenceTopicListener
public class ProductListener {

    @Topic("awesome-products")      (1)
    @SendTo("product-quantities")   (2)
    public int receive(Product product) {
        System.out.println("Got Product - " + product.getName() + " by " + product.getBrand());
        return product.getQuantity(); (3)
    }
}
1 The topic subscribed to is awesome-products
2 The topic to send the result to is product-quantities
3 The return value is used to indicate the value to forward

You can also do the same using Reactive programming:

filename.java
import io.micronaut.coherence.annotation.*;
import io.micronaut.messaging.annotation.SendTo;
import io.reactivex.Single;
import io.reactivex.functions.Function;

@CoherenceTopicListener
public class ProductListener {

    @Topic("awesome-products")       (1)
    @SendTo("product-quantities")    (2)
    public Single<Integer> receiveProduct(Single<Product> productSingle) {
        return productSingle.map(product -> {
            System.out.println("Got Product - " + product.getName() + " by " + product.getBrand());
            return product.getQuantity();  (3)
        });
    }
}
1 The topic subscribed to is awesome-products
2 The topic to send the result to is product-quantities
3 The return is mapped from the single to the value of the quantity

9 Filter Binding Annotations

Filter binding annotations are normal annotations that are themselves annotated with the @FilterBinding meta-annotation. A filter binding annotation represents a Coherence Filter and is used to specify a Filter in certain injection points, for example a View (CQC), NamedTopic Subscriber beans, event listeners, etc.

There are three parts to using a filter binding:

  • The filter binding annotation

  • An implementation of a FilterFactory that is annotated with the filter binding annotation. This is a factory that produces the required Filter.

  • Injection points annotated with the filter binding annotation.

For example; assume there is a Coherence NamedMap with the name people that contains Person instances for the value. Among the various properties on the Person class is a property called gender and property called age. Now assume we want to inject a view that only shows adult males, we would need a Filter that has a condition like `gender == "male" && age > 18".

Create the filter binding annotation

First create a simple annotation, it could be called something like AdultMales

@FilterBinding                         (1)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface AdultMales {         (2)
}
1 The annotation class is annotated with @FilterBinding
2 The annotation name is AdultMales

In this case the annotation does not need any other attributes.

Create the FilterFactory

Now create the FilterFactory implementation that will produce instances of the required Filter.

import com.tangosol.util.Filter;
import com.tangosol.util.Filters;
import io.micronaut.coherence.FilterFactory;

@AdultMales    (1)
@Singleton     (2)
public class AdultMalesFilterFactory<Person> implements FilterFactory<AdultMales, Person> {
    @Override
    public Filter<Person> create(AdultMales annotation) {       (3)
        Filter<Person> male = Filters.equal("gender", "male");
        Filter<Person> adult = Filters.greaterEqual(Extractors.extract("age"), 18);
        return Filters.all(male, adult);
    }
}
1 The class is annotated with the AdultMales filter binding annotation
2 The class must be a Micronaut bean, in this case a singleton
3 The create method uses the Coherence filters API to create the required filter.

The parameter to the create method is the annotation used on the injection point. In this case the annotation has no values, but if it did we could access those values to customize how the filter is created.

For example, we could have just called the annotation @Adults and made the gender a parameter like this:

@FilterBinding
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Adults {
    String value();
}

And changed the filter factory:

@Adult
@Singleton
public class AdultFilterFactory<Person> implements FilterFactory<Adults, Person> {
    @Override
    public Filter<Person> create(Males annotation) {       (3)
        Filter<Person> male = Filters.equal("gender", annotation.value());
        Filter<Person> adult = Filters.greaterEqual(Extractors.extract("age"), 18);
        return Filters.all(male, adult);
    }
}

Annotate the Injection Point

Now the application code where the view is to be injected can use the custom filter binding annotation.

@View               (1)
@AdultMales         (2)
@Name("people")     (3)
private NamedMap<String, Person> adultMales;
1 The @View annotation indicates that this is a view rather than a plain NamedMap
2 The @AdultMales annotation links to the custom filter factory to use to create the filter for the view
3 The @Name annotation indicates the underlying cache/map name to use for the view

10 Extractor Binding Annotations

ValueExtractor binding annotations are normal annotations that are themselves annotated with the @ExtractorBinding meta-annotation. An extractor binding annotation represents a Coherence ValueExtractor and is used to specify a ValueExtractor in certain injection points, for example a View (CQC), NamedTopic Subscriber beans, MapEvent listeners, etc.

There are three parts to using an extractor binding:

  • The extractor binding annotation

  • An implementation of a ExtractorFactory that is annotated with the extractor binding annotation. This is a factory that produces the required ValueExtractor.

  • Injection points annotated with the extractor binding annotation.

For example; assume there is a Coherence NamedMap with the name people that contains Person instances for the value. Among the various properties on the Person class is a property called age. Now assume we want to inject a view that uses the age property for the value, we would need a ValueExtractor that extracts the age property.

Create the extractor binding annotation

First create a simple annotation, it could be called something like PersonAge

@ExtractorBinding                         (1)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface PersonAge {            (2)
}
1 The annotation class is annotated with @ExtractorBinding
2 The annotation name is PersonAge

In this case the annotation does not need any other attributes.

Create the ExtractorFactory

Now create the ExtractorFactory implementation that will produce instances of the required ValueExtractor.

import com.tangosol.util.Extractors;
import com.tangosol.util.ValueExtractor;
import io.micronaut.coherence.ValueExtractorFactory;

@PersonAge     (1)
@Singleton     (2)
public class PersonAgeExtractorFactory<Person> implements ExtractorFactory<AdultMales, Person> {
    @Override
    public ValueExtractor<Person, Integer> create(PersonAge annotation) {       (3)
        return Extractors.extract("age");
    }
}
1 The class is annotated with the PersonAge filter binding annotation
2 The class must be a Micronaut bean, in this case a singleton
3 The create method uses the Coherence Extractors API to create the required extractor, in this case a trivial property extractor.

The parameter to the create method is the annotation used on the injection point. In this case the annotation has no values, but if it did we could access those values to customize how the ValueExtractor is created.

Annotate the Injection Point

Now the application code where the view is to be injected can use the custom extractor binding annotation.

@View               (1)
@PersonAge          (2)
@Name("people")     (3)
private NamedMap<String, Integer> ages;   (4)
1 The @View annotation indicates that this is a view rather than a plain NamedMap
2 The @PersonAge annotation links to the custom extractor factory used to create the ValueExtractor for the view
3 The @Name annotation indicates the underlying cache/map name to use for the view
4 Note that the NamedMap generics are now String and Integer not String and Person as the Person values from the underlying cache are transformed into Integer values by extracting just the age property.

11 Injection into Deserialized Objects

Using Micronaut to inject Coherence objects into your application classes, and Micronaut beans into Coherence-managed objects will allow you to support many use cases where dependency injection may be useful, but it doesn’t cover an important use case that is somewhat specific to Coherence.

Coherence is a distributed system, and it uses serialization in order to send both the data and the processing requests from one cluster member (or remote client) to another, as well as to store data, both in memory and on disk.

Processing requests, such as entry processors and aggregators, have to be deserialized on a target cluster member(s) in order to be executed. In some cases, they could benefit from dependency injection in order to avoid service lookups.

Similarly, while the data is stored in a serialized, binary format, it may need to be deserialized into user supplied classes for server-side processing, such as when executing entry processors and aggregators. In this case, data classes can often also benefit from dependency injection (in order to support Domain-Driven Design (DDD), for example).

While these transient objects are not managed by the Micronaut container, Coherence Micronaut does support their injection during deserialization, but for performance reasons requires that you explicitly opt in by implementing com.oracle.coherence.inject.Injectable interface.

Making transient classes Injectable

While not technically a true marker interface, com.oracle.coherence.inject.Injectable can be treated as such for all intents and purposes. All you need to do is add it to the implements clause of your class in order for injection on deserialization to kick in:

public class InjectableBean
        implements Injectable, Serializable {

    @Inject
    private Converter<String, String> converter;

    private String text;

    InjectableBean() {
    }

    InjectableBean(String text) {
        this.text = text;
    }

    String getConvertedText() {
        return converter.convert(text);
    }
}

Assuming that you have the following Converter service implementation in your application, it will be injected into InjectableBean during deserialization, and the getConvertedText method will return the value of the text field converted to upper case:

@ApplicationScoped
public class ToUpperConverter
        implements Converter<String, String> {
    @Override
    public String convert(String s) {
        return s.toUpperCase();
    }
}
If your Injectable class has @PostConstruct callback method, it will be called after the injection. However, because we have no control over object’s lifecycle after that point, @PreDestroy callback will never be called).

You should note that the above functionality is not dependent on the serialization format and will work with both Java and POF serialization (or any other custom serializer), and for any object that is deserialized on any Coherence member (or even on a remote client).

While the deserialized transient objects are not true Micronaut managed beans, being able to inject Micronaut managed dependencies into them upon deserialization will likely satisfy most dependency injection requirements you will ever have in those application components.

12 Injecting Beans into Coherence Configuration

You can inject Micronaut beans into the standard Coherence cache configuration file by using the Coherence Micronaut custom namespace handler.

Custom namespace handlers are a standard feature of Coherence that allow the cache configuration file to be customized considerably.

The Coherence Micronaut custom namespace handler provides a single additional XML element named <bean> that is used to declare a named Micronaut bean for injection into the configuration.

To use the Coherence Micronaut custom namespace handler it must be declared in the XML configuration file alongside the other XSD values:

<?xml version="1.0"?>
<cache-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://xmlns.oracle.com/coherence/coherence-cache-config"
        xmlns:m="class://io.micronaut.coherence.namespace.MicronautNamespaceHandler"   (1)
        xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-cache-config coherence-cache-config.xsd">
1 The namespace is declared as xmlns:m="class://io.micronaut.coherence.namespace.MicronautNamespaceHandler" where io.micronaut.coherence.namespace.MicronautNamespaceHandler is actually the class implementing the custom namespace handler. The m prefix in xmlns:m means that the custom XML elements should be prefixed with m: so in this case <m:bean>beanName</m:bean>

The <bean> element can be used anywhere in the configuration that an instance element would be used, for example when declaring interceptors, cache stores, listeners, etc.

For examples, with the following entry event interceptor:

import jakarta.inject.Singleton;

import com.tangosol.net.events.EventInterceptor;
import com.tangosol.net.events.annotation.EntryEvents;
import com.tangosol.net.events.annotation.Interceptor;
import com.tangosol.net.events.partition.cache.EntryEvent;

@Singleton
@Named("Foo")   (1)
@Interceptor
@EntryEvents({EntryEvent.Type.INSERTED, EntryEvent.Type.UPDATED, EntryEvent.Type.REMOVED})
public class MyInterceptor implements EventInterceptor<EntryEvent<?, ?>> {
    @Override
    public void onEvent(EntryEvent<?, ?> event) {
        // process the event.
    }
}
1 The interceptor is a @Singleton bean @Named with the name Foo

This bean can be referenced in the cache configuration file, for example as an interceptor in a cache mapping:

<?xml version="1.0"?>
<cache-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://xmlns.oracle.com/coherence/coherence-cache-config"
        xmlns:m="class://io.micronaut.coherence.namespace.MicronautNamespaceHandler"
        xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-cache-config coherence-cache-config.xsd">

    <caching-scheme-mapping>
        <cache-mapping>
            <cache-name>*</cache-name>
            <scheme-name>distributed-scheme</scheme-name>
            <interceptors>
                <interceptor>
                    <instance>
                        <m:bean>Foo</m:bean>   (1)
                    </instance>
                </interceptor>
            </interceptors>
        </cache-mapping>
1 The Foo interceptor will be injected into the cache mapping.

13 Using Coherence to store HTTP sessions

To use Coherence as a Micronaut HTTP session store add the micronaut-coherence-session dependency:

implementation("io.micronaut.coherence:micronaut-coherence-session")
<dependency>
    <groupId>io.micronaut.coherence</groupId>
    <artifactId>micronaut-coherence-session</artifactId>
</dependency>

Also, add a Coherence dependency - either Coherence CE or commercial version:

implementation("com.oracle.coherence.ce:coherence:23.03")
<dependency>
    <groupId>com.oracle.coherence.ce</groupId>
    <artifactId>coherence</artifactId>
    <version>23.03</version>
</dependency>

Enable Coherence sessions via configuration in the application configuration file:

Enabling Coherence Sessions
micronaut.session.http.coherence.enabled=true
micronaut.session.http.coherence.cache-name=http-session-cache
micronaut:
  session:
    http:
      coherence:
        enabled: true
        cache-name: http-session-cache
[micronaut]
  [micronaut.session]
    [micronaut.session.http]
      [micronaut.session.http.coherence]
        enabled=true
        cache-name="http-session-cache"
micronaut {
  session {
    http {
      coherence {
        enabled = true
        cacheName = "http-session-cache"
      }
    }
  }
}
{
  micronaut {
    session {
      http {
        coherence {
          enabled = true
          cache-name = "http-session-cache"
        }
      }
    }
  }
}
{
  "micronaut": {
    "session": {
      "http": {
        "coherence": {
          "enabled": true,
          "cache-name": "http-session-cache"
        }
      }
    }
  }
}
  • cache-name (optional) names the cache that will be used for sessions. By default http-sessions cache will be used.

Coherence cache will be obtained using default Coherence session.

In case that cache is configured to use POF serialization, additional POF configuration for the class io.micronaut.coherence.httpsession.CoherenceSessionStore$CoherenceHttpSession has to be added to the POF config. User can pick appropriate value for type-id.

    <user-type>
      <type-id>2001</type-id>
      <class-name>io.micronaut.coherence.httpsession.CoherenceSessionStore$CoherenceHttpSession</class-name>
    </user-type>

14 Using Coherence as Micronaut Cache implementation

To use Coherence as the caching implementation, add it as a dependency to your application:

implementation("io.micronaut.coherence:micronaut-coherence-cache")
<dependency>
    <groupId>io.micronaut.coherence</groupId>
    <artifactId>micronaut-coherence-cache</artifactId>
</dependency>

Also, add Coherence dependency - either Coherence CE or commercial version:

implementation("com.oracle.coherence.ce:coherence:23.03")
<dependency>
    <groupId>com.oracle.coherence.ce</groupId>
    <artifactId>coherence</artifactId>
    <version>23.03</version>
</dependency>

When using the @Cacheable and other Cache Annotations, Micronaut will use default Coherence session to obtain cache instance for caching. It’s up to the user to configure Coherence specific cache.

You can also add Coherence module to your project using mn CLI feature:

Create a Micronaut application with Coherence cache module
$ mn create-app hello-world -f cache-coherence

To disable Coherence:

coherence.cache.enabled=false
coherence:
  cache:
    enabled: false
[coherence]
  [coherence.cache]
    enabled=false
coherence {
  cache {
    enabled = false
  }
}
{
  coherence {
    cache {
      enabled = false
    }
  }
}
{
  "coherence": {
    "cache": {
      "enabled": false
    }
  }
}

15 Using Coherence as a Distributed Configuration provider

To use Coherence to store configuration values, add following dependencies:

implementation("io.micronaut.coherence:micronaut-coherence-distributed-configuration")
<dependency>
    <groupId>io.micronaut.coherence</groupId>
    <artifactId>micronaut-coherence-distributed-configuration</artifactId>
</dependency>

Also, add Coherence dependency - either Coherence CE or commercial version:

implementation("com.oracle.coherence.ce:coherence:23.03")
<dependency>
    <groupId>com.oracle.coherence.ce</groupId>
    <artifactId>coherence</artifactId>
    <version>23.03</version>
</dependency>

To enable support simply add the following configuration to your bootstrap configuration file:

Integrating with Oracle Coherence
micronaut.application.name=hello-world
micronaut.config-client.enabled=true
coherence.sessions.config.type=client
coherence.configuration.client.enabled=true
coherence.configuration.client.session=client
micronaut:
  application:
    name: hello-world
  config-client:
    enabled: true

coherence:
  sessions:
    config:
      type: client
  configuration:
    client:
      enabled: true
      session: client
[micronaut]
  [micronaut.application]
    name="hello-world"
  [micronaut.config-client]
    enabled=true
[coherence]
  [coherence.sessions]
    [coherence.sessions.config]
      type="client"
  [coherence.configuration]
    [coherence.configuration.client]
      enabled=true
      session="client"
micronaut {
  application {
    name = "hello-world"
  }
  configClient {
    enabled = true
  }
}
coherence {
  sessions {
    config {
      type = "client"
    }
  }
  configuration {
    client {
      enabled = true
      session = "client"
    }
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
    config-client {
      enabled = true
    }
  }
  coherence {
    sessions {
      config {
        type = "client"
      }
    }
    configuration {
      client {
        enabled = true
        session = "client"
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    },
    "config-client": {
      "enabled": true
    }
  },
  "coherence": {
    "sessions": {
      "config": {
        "type": "client"
      }
    },
    "configuration": {
      "client": {
        "enabled": true,
        "session": "client"
      }
    }
  }
}

This will create a configuration client using the session named client to connect to a Coherence proxy server over extend and allow the application to lookup property sources for the application from Coherence.

Table 1. Configuration Resolution Precedence
Cache Description

application

Configuration shared by all applications

[APPLICATION_NAME]

Application specific configuration

application-[ENV_NAME]

Configuration shared by all applications for an active environment name

[APPLICATION_NAME]-[ENV_NAME]

Application specific configuration for an active environment name