annotationProcessor("io.micronaut.microstream:micronaut-microstream-annotations")
Table of Contents
Micronaut MicroStream
Micronaut integration with MicroStream
Version: 2.6.1-SNAPSHOT
1 Introduction
Integration with MicroStream.
2 Release History
For this project, you can find a list of releases (with release notes) here:
3 What's New
Since Micronaut MicroStream 2, you can use Storage Targets such as PostgreSQL, Amazon S3 and DynamoDB.
4 Dependency
To start, add the following dependencies to your classpath.
<annotationProcessorPaths>
<path>
<groupId>io.micronaut.microstream</groupId>
<artifactId>micronaut-microstream-annotations</artifactId>
</path>
</annotationProcessorPaths>
For Kotlin, add the micronaut-microstream-annotations dependency in kapt or ksp scope. For Groovy annotation processing, the following implementation scope is adequate.
|
implementation("io.micronaut.microstream:micronaut-microstream-annotations")
<dependency>
<groupId>io.micronaut.microstream</groupId>
<artifactId>micronaut-microstream-annotations</artifactId>
</dependency>
implementation("io.micronaut.microstream:micronaut-microstream")
<dependency>
<groupId>io.micronaut.microstream</groupId>
<artifactId>micronaut-microstream</artifactId>
</dependency>
5 Configuration
A Micronaut application can have more than one MicroStream instances. Each MicroStream instance represents one coherent entity graph of persistent data.
You can use the same values described in the MicroStream Configuration documentation.
The following configuration example configures two beans of type EmbeddedStorageConfigurationProvider
with Name Bean Qualifiers: orange
and blue
.
microstream:
storage:
orange: (1)
root-class: 'io.micronaut.microstream.docs.OneData' (2)
storage-directory: build/microstream${random.shortuuid}
channel-count: 4
blue: (2)
root-class: 'io.micronaut.microstream.docs.AnotherData' (2)
storage-directory: build/microstream${random.shortuuid}
channel-count: 4
channel-directory-prefix: 'channel_'
data-file-prefix: 'channel_'
data-file-suffix: '.dat'
1 | Specify a different name qualifier for each MicroStream instance |
2 | Specify the class of the entity graph’s root. |
6 Root Instance
The following example creates a Root Instance to store Customer
s:
package io.micronaut.microstream.docs;
import io.micronaut.core.annotation.NonNull;
import java.util.HashMap;
import java.util.Map;
public class Data {
private Map<String, Customer> customers = new HashMap<>();
@NonNull
public Map<String, Customer> getCustomers() {
return this.customers;
}
}
package io.micronaut.microstream.docs
import io.micronaut.core.annotation.Introspected
@Introspected // (1)
class Data {
Map<String, Customer> customers = [:]
}
package io.micronaut.microstream.docs
import io.micronaut.core.annotation.Introspected
@Introspected // (1)
data class Data(val customers: MutableMap<String, Customer> = mutableMapOf())
And Customer
is defined as:
package io.micronaut.microstream.docs;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
@Serdeable // (1)
public class Customer {
@NonNull
@NotBlank
private final String id;
@NonNull
@NotBlank
private String firstName;
@Nullable
private String lastName;
public Customer(@NonNull String id, @NonNull String firstName, @Nullable String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}
@NonNull
public String getId() {
return id;
}
public void setFirstName(@NonNull String firstName) {
this.firstName = firstName;
}
public void setLastName(@Nullable String lastName) {
this.lastName = lastName;
}
@NonNull
public String getFirstName() {
return firstName;
}
@Nullable
public String getLastName() {
return lastName;
}
}
package io.micronaut.microstream.docs
import io.micronaut.core.annotation.Introspected
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import jakarta.validation.constraints.NotBlank
@Introspected
class Customer {
@NonNull
@NotBlank
String id
@NonNull
@NotBlank
String firstName
@Nullable
String lastName
Customer(@NonNull String id, @NonNull String firstName, @Nullable String lastName) {
this.id = id
this.firstName = firstName
this.lastName = lastName
}
}
package io.micronaut.microstream.docs
import io.micronaut.core.annotation.Introspected
@Introspected
class Customer(val id: String, var firstName: String, var lastName: String?)
1 | The type is annotated with @Serdeable to enable serialization/deserialization |
You specify the Root class via configuration:
microstream:
storage:
main: (1)
root-class: 'io.micronaut.microstream.docs.Data' (2)
storage-directory: '/Users/sdelamo/Documents/MicroStreamData'
1 | Specify a name qualifier for the MicroStream instance |
2 | Specify the class of the entity graph’s root. |
7 Annotations
Micronaut MicroStream ships the following around method annotations. They simplify storing objects in an associated Store Manager.
These annotations wrap the decorated method to ensure thread isolation.
It stores the method parameters specified in the annotation value. |
|
It stores the method return |
|
It stores the root object. |
|
Micronaut MicroStream maps other annotations to this annotation. You will not use this annotation directly. |
If you work with multiple MicroStream instances, you can supply the name qualifier to the annotation to specify the instance you are working with.
Moreover, you can specify the StoringStrategy via the annotations. By default, LAZY
is the default storing mode.
8 Storage
The following example shows how to create implementations for this repository:
package io.micronaut.microstream.docs;
import io.micronaut.core.annotation.NonNull;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
public interface CustomerRepository {
@NonNull
Customer save(@NonNull @NotNull @Valid CustomerSave customerSave);
void update(@NonNull @NotBlank String id,
@NonNull @NotNull @Valid CustomerSave customerSave);
@NonNull
Optional<Customer> findById(@NonNull @NotBlank String id);
void deleteById(@NonNull @NotBlank String id);
}
package io.micronaut.microstream.docs
import io.micronaut.core.annotation.NonNull
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
interface CustomerRepository {
@NonNull
Customer save(@NonNull @NotNull @Valid CustomerSave customer)
void update(@NonNull @NotBlank String id,
@NonNull @NotNull @Valid CustomerSave customer)
@NonNull
Optional<Customer> findById(@NonNull @NotBlank String id)
void deleteById(@NonNull @NotBlank String id)
}
package io.micronaut.microstream.docs
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
interface CustomerRepository {
fun save(customerSave: @Valid CustomerSave): Customer
fun update(id: @NotBlank String, customerSave: @Valid CustomerSave)
fun findById(id: @NotBlank String): Customer?
fun deleteById(id: @NotBlank String)
}
8.1 StorageManager
The following example shows how to create a repository which injects the StorageManager
and retrieves the Root Instance:
@Singleton
public class CustomerRepositoryImpl implements CustomerRepository {
private final StorageManager storageManager;
public CustomerRepositoryImpl(StorageManager storageManager) { // (1)
this.storageManager = storageManager;
}
@Override
@NonNull
public Customer save(@NonNull @NotNull @Valid CustomerSave customerSave) {
return XThreads.executeSynchronized(() -> { // (2)
String id = UUID.randomUUID().toString();
Customer customer = new Customer(id, customerSave.getFirstName(), customerSave.getLastName());
data().getCustomers().put(id, customer);
storageManager.store(data().getCustomers()); // (3)
return customer;
});
}
@Override
public void update(@NonNull @NotBlank String id,
@NonNull @NotNull @Valid CustomerSave customerSave) {
XThreads.executeSynchronized(() -> { // (2)
Customer c = data().getCustomers().get(id);
c.setFirstName(customerSave.getFirstName());
c.setLastName(customerSave.getLastName());
storageManager.store(c); // (3)
});
}
@Override
@NonNull
public Optional<Customer> findById(@NonNull @NotBlank String id) {
return Optional.ofNullable(data().getCustomers().get(id));
}
@Override
public void deleteById(@NonNull @NotBlank String id) {
XThreads.executeSynchronized(() -> { // (2)
data().getCustomers().remove(id);
storageManager.store(data().getCustomers()); // (3)
});
}
private Data data() {
return (Data) storageManager.root();
}
}
@Singleton
class CustomerRepositoryImpl implements CustomerRepository {
private final StorageManager storageManager
CustomerRepositoryImpl(StorageManager storageManager) { // (1)
this.storageManager = storageManager
}
@Override
@NonNull
Customer save(@NonNull @NotNull @Valid CustomerSave customerSave) {
XThreads.executeSynchronized(new Supplier<Customer>() { // (2)
@Override
Customer get() {
String id = UUID.randomUUID().toString()
Customer customer = new Customer(id, customerSave.getFirstName(), customerSave.getLastName())
data().getCustomers().put(id, customer)
store(data().getCustomers()) // (3)
customer
}
})
}
@Override
void update(@NonNull @NotBlank String id,
@NonNull @NotNull @Valid CustomerSave customerSave) {
XThreads.executeSynchronized(new Runnable() { // (2)
@Override
void run() {
Customer c = data().getCustomers().get(id)
c.setFirstName(customerSave.getFirstName())
c.setLastName(customerSave.getLastName())
store(c) // (3)
}
})
}
@Override
@NonNull
Optional<Customer> findById(@NonNull @NotBlank String id) {
Optional.ofNullable(data().getCustomers().get(id))
}
@Override
void deleteById(@NonNull @NotBlank String id) {
XThreads.executeSynchronized(new Runnable() { // (2)
@Override
void run() {
data().getCustomers().remove(id)
store(data().getCustomers()) // (3)
}
})
}
private void store(Object instance) {
storageManager.store(instance)
}
private Data data() {
(Data) storageManager.root()
}
}
@Singleton
class CustomerRepositoryImpl(private val storageManager: StorageManager) // (1)
: CustomerRepository {
override fun save(customerSave: CustomerSave): Customer {
val id = UUID.randomUUID().toString()
val customer = Customer(id, customerSave.firstName, customerSave.lastName)
XThreads.executeSynchronized { // (2)
data.customers[customer.id] = customer
storageManager.store(data.customers) // (3)
}
return customer
}
override fun update(id : String, customerSave: CustomerSave) {
XThreads.executeSynchronized { // (2)
val customer : Customer? = data.customers[id]
if (customer != null) {
with(customer) {
firstName = customerSave.firstName
lastName = customerSave.lastName
}
storageManager.store(customer) // (3)
}
}
}
@NonNull
override fun findById(id: @NotBlank String): Customer? {
return data.customers[id]
}
override fun deleteById(id: @NotBlank String) {
XThreads.executeSynchronized { // (2)
data.customers.remove(id)
storageManager.store(data.customers) // (3)
}
}
private val data: Data
get() {
val root = storageManager.root()
return if (root is Data) root else throw RuntimeException("Root is not Data")
}
}
1 | If your Micronaut application has only a single MicroStream Instance, you don’t need to specify a name qualifier to inject StorageManager . |
2 | When you are working with MicroStream technology in a multi-threaded environment, reading and writing to this shared object graph must be synchronized. |
3 | To store a newly created object, store the "owner" of the object. |
8.2 Working with Annotations
The following example shows how an equivalent implementation which leverages the Micronaut MicroStream annotations to simplify object storage.
@Singleton
public class CustomerRepositoryStoreImpl implements CustomerRepository {
private final RootProvider<Data> rootProvider;
public CustomerRepositoryStoreImpl(RootProvider<Data> rootProvider) { // (1)
this.rootProvider = rootProvider;
}
@Override
@NonNull
public Customer save(@NonNull @NotNull @Valid CustomerSave customerSave) {
return addCustomer(rootProvider.root().getCustomers(), customerSave);
}
@Override
public void update(@NonNull @NotBlank String id,
@NonNull @NotNull @Valid CustomerSave customerSave) {
updateCustomer(id, customerSave);
}
@Override
@NonNull
public Optional<Customer> findById(@NonNull @NotBlank String id) {
return Optional.ofNullable(rootProvider.root().getCustomers().get(id));
}
@Override
public void deleteById(@NonNull @NotBlank String id) {
removeCustomer(rootProvider.root().getCustomers(), id);
}
@StoreReturn // (2)
@Nullable
protected Customer updateCustomer(@NonNull String id,
@NonNull CustomerSave customerSave) {
Customer c = rootProvider.root().getCustomers().get(id);
if (c != null) {
c.setFirstName(customerSave.getFirstName());
c.setLastName(customerSave.getLastName());
return c;
}
return null;
}
@StoreParams("customers") // (3)
protected Customer addCustomer(@NonNull Map<String, Customer> customers,
@NonNull CustomerSave customerSave) {
Customer customer = new Customer(UUID.randomUUID().toString(),
customerSave.getFirstName(),
customerSave.getLastName());
customers.put(customer.getId(), customer);
return customer;
}
@StoreParams("customers") // (3)
protected void removeCustomer(@NonNull Map<String, Customer> customers,
@NonNull String id) {
customers.remove(id);
}
}
@Singleton
class CustomerRepositoryStoreImpl implements CustomerRepository {
private final RootProvider<Data> rootProvider
CustomerRepositoryStoreImpl(RootProvider<Data> rootProvider) { // (1)
this.rootProvider = rootProvider
}
@Override
@NonNull
Customer save(@NonNull @NotNull @Valid CustomerSave customerSave) {
return addCustomer(rootProvider.root().customers, customerSave)
}
@Override
void update(@NonNull @NotBlank String id,
@NonNull @NotNull @Valid CustomerSave customerSave) {
updateCustomer(id, customerSave)
}
@Override
@NonNull
Optional<Customer> findById(@NonNull @NotBlank String id) {
Optional.ofNullable(rootProvider.root().customers[id])
}
@Override
void deleteById(@NonNull @NotBlank String id) {
removeCustomer(rootProvider.root().customers, id)
}
@StoreReturn // (2)
@Nullable
protected Customer updateCustomer(@NonNull String id,
@NonNull CustomerSave customerSave) {
Customer c = rootProvider.root().customers[id]
if (c != null) {
c.with {
firstName = customerSave.firstName
lastName = customerSave.lastName
}
return c
}
null
}
@StoreParams("customers") // (3)
protected Customer addCustomer(@NonNull Map<String, Customer> customers,
@NonNull CustomerSave customerSave) {
Customer customer = new Customer(UUID.randomUUID().toString(),
customerSave.firstName,
customerSave.lastName)
customers[customer.id] = customer
customer
}
@StoreParams("customers") // (3)
protected void removeCustomer(@NonNull Map<String, Customer> customers,
@NonNull String id) {
customers.remove(id)
}
}
@Singleton
open class CustomerRepositoryStoreImpl(private val rootProvider: RootProvider<Data>) // (1)
: CustomerRepository {
override fun save(customerSave: @Valid CustomerSave): Customer {
return addCustomer(rootProvider.root().customers, customerSave)
}
override fun update(id : @NotBlank String,
customerSave: @Valid CustomerSave) {
updateCustomer(id, customerSave)
}
@NonNull
override fun findById(id: @NotBlank String): Customer? {
return rootProvider.root().customers[id]
}
override fun deleteById(id: @NotBlank String) {
removeCustomer(rootProvider.root().customers, id)
}
@StoreReturn // (2)
@Nullable
open fun updateCustomer(id: String, customerSave: CustomerSave): Customer? {
val c: Customer? = rootProvider.root().customers[id]
return if (c != null) {
c.firstName = customerSave.firstName
c.lastName = customerSave.lastName
c
} else null
}
@StoreParams("customers") // (3)
open fun addCustomer(customers: MutableMap<String, Customer>, customerSave: CustomerSave): Customer {
val customer = Customer(
UUID.randomUUID().toString(),
customerSave.firstName,
customerSave.lastName
)
customers[customer.id] = customer
return customer
}
@StoreParams("customers") // (3)
open fun removeCustomer(customers: MutableMap<String, Customer>, id: String) {
customers.remove(id)
}
}
1 | You can also inject an instance of RootProvider to easily access the MicroStream Root Instance. If your Micronaut application has only a single MicroStream Instance, you don’t need to specify a name qualifier to inject it. |
2 | The rule is: "The Object that has been modified has to be stored!". |
3 | To store a newly created object, store the "owner" of the object. |
Micronaut MicroStream annotations only work for synchronous methods. It does not do any logic for methods returning a Publisher or a CompletableFuture . For those scenarios, use directly the StorageManager .
|
9 Storage Targets
MicroStream supports a variety of storage targets. Through an abstracted file system (AFS), it is possible to connect to a lot of different back ends. The AFS allows to use folders and files, like in all common file systems, but with different connectors it is possible to use different solutions as the actual storage
By default, the Micronaut Framework MicroStream integration will use a local directory as a backing store for the object graph.
You can switch this to a different target via configuration. Currently supported targets are:
9.1 Postgres
Configuration of a Postgres storage target requires a Postgres JDBC datasource to be defined
To get started, add the following dependencies:
runtimeOnly("io.micronaut.sql:micronaut-jdbc-hikari")
<dependency>
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
<scope>runtime</scope>
</dependency>
runtimeOnly("org.postgresql:postgresql")
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
runtimeOnly("one.microstream:microstream-afs-sql")
<dependency>
<groupId>one.microstream</groupId>
<artifactId>microstream-afs-sql</artifactId>
<scope>runtime</scope>
</dependency>
The following defines a datasource named default
that connects to a Postgres database:
datasources.default.url=jdbc:postgresql://host:port/database
datasources.default.username=«user»
datasources.default.password=«password»
datasources.default.driverClassName=org.postgresql.Driver
datasources:
default:
url: jdbc:postgresql://host:port/database
username: «user»
password: «password»
driverClassName: org.postgresql.Driver
[datasources]
[datasources.default]
url="jdbc:postgresql://host:port/database"
username="\u00abuser\u00bb"
password="\u00abpassword\u00bb"
driverClassName="org.postgresql.Driver"
datasources {
'default' {
url = "jdbc:postgresql://host:port/database"
username = "\u00ABuser\u00BB"
password = "\u00ABpassword\u00BB"
driverClassName = "org.postgresql.Driver"
}
}
{
datasources {
default {
url = "jdbc:postgresql://host:port/database"
username = "«user»"
password = "«password»"
driverClassName = "org.postgresql.Driver"
}
}
}
{
"datasources": {
"default": {
"url": "jdbc:postgresql://host:port/database",
"username": "«user»",
"password": "«password»",
"driverClassName": "org.postgresql.Driver"
}
}
}
The Postgres Storage Target can then be configured as follows:
microstream.postgres.storage.default.table-name=microstream
microstream.postgres.storage.default.root-class=com.example.model.Root
microstream:
postgres:
storage:
default: # (1)
table-name: microstream # (2)
root-class: com.example.model.Root # (3)
[microstream]
[microstream.postgres]
[microstream.postgres.storage]
[microstream.postgres.storage.default]
table-name="microstream"
root-class="com.example.model.Root"
microstream {
postgres {
storage {
'default' {
tableName = "microstream"
rootClass = "com.example.model.Root"
}
}
}
}
{
microstream {
postgres {
storage {
default {
table-name = "microstream"
root-class = "com.example.model.Root"
}
}
}
}
}
{
"microstream": {
"postgres": {
"storage": {
"default": {
"table-name": "microstream",
"root-class": "com.example.model.Root"
}
}
}
}
}
1 | The name for this Storage Manager |
2 | The name of the table to use in the database |
3 | The root class of the object graph to store |
The storage manager will first attempt to look up a DataSource with the same name as the Storage Manager, and if this is not found, it will attempt to look up an un-named DataSource.
If you require to use a DataSource with a different name, this can be configured via the datasource-name
property as below.
microstream.postgres.storage.backing.table-name=microstream
microstream.postgres.storage.backing.root-class=com.example.model.Root
microstream.postgres.storage.backing.datasource-name=default
microstream:
postgres:
storage:
backing:
table-name: microstream
root-class: com.example.model.Root
datasource-name: default # (1)
[microstream]
[microstream.postgres]
[microstream.postgres.storage]
[microstream.postgres.storage.backing]
table-name="microstream"
root-class="com.example.model.Root"
datasource-name="default"
microstream {
postgres {
storage {
backing {
tableName = "microstream"
rootClass = "com.example.model.Root"
datasourceName = "default"
}
}
}
}
{
microstream {
postgres {
storage {
backing {
table-name = "microstream"
root-class = "com.example.model.Root"
datasource-name = "default"
}
}
}
}
}
{
"microstream": {
"postgres": {
"storage": {
"backing": {
"table-name": "microstream",
"root-class": "com.example.model.Root",
"datasource-name": "default"
}
}
}
}
}
1 | The name of the DataSource to use |
9.2 S3
Configuration of an S3 storage target requires a bean of type S3Client
and configuration.
To get started, add the following dependencies:
runtimeOnly("software.amazon.awssdk:s3")
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<scope>runtime</scope>
</dependency>
runtimeOnly("io.micronaut.aws:micronaut-aws-sdk-v2")
<dependency>
<groupId>io.micronaut.aws</groupId>
<artifactId>micronaut-aws-sdk-v2</artifactId>
<scope>runtime</scope>
</dependency>
runtimeOnly("one.microstream:microstream-afs-aws-s3")
<dependency>
<groupId>one.microstream</groupId>
<artifactId>microstream-afs-aws-s3</artifactId>
<scope>runtime</scope>
</dependency>
The configuration can then be defined as follows:
microstream.s3.storage.default.bucket-name=microstream
microstream.s3.storage.default.root-class=com.example.model.Root
microstream:
s3:
storage:
default: # (1)
bucket-name: microstream # (2)
root-class: com.example.model.Root # (3)
[microstream]
[microstream.s3]
[microstream.s3.storage]
[microstream.s3.storage.default]
bucket-name="microstream"
root-class="com.example.model.Root"
microstream {
s3 {
storage {
'default' {
bucketName = "microstream"
rootClass = "com.example.model.Root"
}
}
}
}
{
microstream {
s3 {
storage {
default {
bucket-name = "microstream"
root-class = "com.example.model.Root"
}
}
}
}
}
{
"microstream": {
"s3": {
"storage": {
"default": {
"bucket-name": "microstream",
"root-class": "com.example.model.Root"
}
}
}
}
}
1 | The name for the created Storage Manager |
2 | The name of the S3 bucket to use for storage |
3 | The root class of the object graph to store |
The storage manager will first attempt to look up an S3Client with the same name as the Storage Manager, and if this is not found, it will attempt to look up an un-named S3Client bean.
If you require to use an S3Client with a different name, this can be configured via the s3-client-name
property as below.
microstream.s3.storage.default.s3-client-name=my-s3-client
microstream.s3.storage.default.bucket-name=microstream
microstream.s3.storage.default.root-class=com.example.model.Root
microstream:
s3:
storage:
default:
s3-client-name: my-s3-client # (1)
bucket-name: microstream
root-class: com.example.model.Root
[microstream]
[microstream.s3]
[microstream.s3.storage]
[microstream.s3.storage.default]
s3-client-name="my-s3-client"
bucket-name="microstream"
root-class="com.example.model.Root"
microstream {
s3 {
storage {
'default' {
s3ClientName = "my-s3-client"
bucketName = "microstream"
rootClass = "com.example.model.Root"
}
}
}
}
{
microstream {
s3 {
storage {
default {
s3-client-name = "my-s3-client"
bucket-name = "microstream"
root-class = "com.example.model.Root"
}
}
}
}
}
{
"microstream": {
"s3": {
"storage": {
"default": {
"s3-client-name": "my-s3-client",
"bucket-name": "microstream",
"root-class": "com.example.model.Root"
}
}
}
}
}
1 | The name of the S3Client bean to use |
9.3 DynamoDB
Configuration of an DynamoDB storage target requires a DynamoDbClient bean to be defined.
To get started, add the following dependencies:
runtimeOnly("software.amazon.awssdk:dynamodb")
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
<scope>runtime</scope>
</dependency>
runtimeOnly("io.micronaut.aws:micronaut-aws-sdk-v2")
<dependency>
<groupId>io.micronaut.aws</groupId>
<artifactId>micronaut-aws-sdk-v2</artifactId>
<scope>runtime</scope>
</dependency>
runtimeOnly("one.microstream:microstream-afs-aws-dynamodb")
<dependency>
<groupId>one.microstream</groupId>
<artifactId>microstream-afs-aws-dynamodb</artifactId>
<scope>runtime</scope>
</dependency>
The configuration can then be defined as follows:
microstream.dynamodb.storage.default.table-name=microstream
microstream.dynamodb.storage.default.root-class=com.example.model.Root
microstream:
dynamodb:
storage:
default: # (1)
table-name: microstream # (2)
root-class: com.example.model.Root # (3)
[microstream]
[microstream.dynamodb]
[microstream.dynamodb.storage]
[microstream.dynamodb.storage.default]
table-name="microstream"
root-class="com.example.model.Root"
microstream {
dynamodb {
storage {
'default' {
tableName = "microstream"
rootClass = "com.example.model.Root"
}
}
}
}
{
microstream {
dynamodb {
storage {
default {
table-name = "microstream"
root-class = "com.example.model.Root"
}
}
}
}
}
{
"microstream": {
"dynamodb": {
"storage": {
"default": {
"table-name": "microstream",
"root-class": "com.example.model.Root"
}
}
}
}
}
1 | The name for the created Storage Manager |
2 | The name of the DynamoDB table to use for storage |
3 | The root class of the object graph to store |
If the DynamoDB table does not exist, MicroStream creates it. |
The storage manager will first attempt to look up an DynamoDbClient
with the same name as the Storage Manager, and if this is not found, it will attempt to look up an un-named DynamoDbClient
bean.
If you require to use an DynamoDbClient
with a different name, this can be configured via the dynamo-db-client-name
property as below.
microstream.dynamodb.storage.default.dynamo-db-client-name=my-dynamo-client
microstream.dynamodb.storage.default.table-name=microstream
microstream.dynamodb.storage.default.root-class=com.example.model.Root
microstream:
dynamodb:
storage:
default:
dynamo-db-client-name: my-dynamo-client # (1)
table-name: microstream
root-class: com.example.model.Root
[microstream]
[microstream.dynamodb]
[microstream.dynamodb.storage]
[microstream.dynamodb.storage.default]
dynamo-db-client-name="my-dynamo-client"
table-name="microstream"
root-class="com.example.model.Root"
microstream {
dynamodb {
storage {
'default' {
dynamoDbClientName = "my-dynamo-client"
tableName = "microstream"
rootClass = "com.example.model.Root"
}
}
}
}
{
microstream {
dynamodb {
storage {
default {
dynamo-db-client-name = "my-dynamo-client"
table-name = "microstream"
root-class = "com.example.model.Root"
}
}
}
}
}
{
"microstream": {
"dynamodb": {
"storage": {
"default": {
"dynamo-db-client-name": "my-dynamo-client",
"table-name": "microstream",
"root-class": "com.example.model.Root"
}
}
}
}
}
1 | The name of the DynamoDbClient bean to use |
10 Cache
MicroStream can be used as a cache abstraction layer.
10.1 Cache Configuration
To use the MicroStream cache abstraction, you must declare a dependency on
implementation("io.micronaut.microstream:micronaut-microstream-cache")
<dependency>
<groupId>io.micronaut.microstream</groupId>
<artifactId>micronaut-microstream-cache</artifactId>
</dependency>
You can then define a cache by adding the following configuration to your application:
microstream:
cache:
counter: (1)
key-type: java.lang.String
value-type: java.lang.Long
1 | Define a cache named counter which has String keys and Long values. |
This cache can then be used via the following:
package io.micronaut.microstream.docs;
import io.micronaut.cache.annotation.CacheConfig;
import io.micronaut.cache.annotation.CachePut;
import io.micronaut.cache.annotation.Cacheable;
import jakarta.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
@Singleton
@CacheConfig("counter") // (1)
public class CounterService {
Map<String, Long> counters = new HashMap<>();
@Cacheable // (2)
public Long currentCount(String name) {
return counters.get(name);
}
@CachePut(parameters = {"name"}) // (3)
public Long setCount(String name, Long count) {
counters.put(name, count);
return count;
}
}
package io.micronaut.microstream.docs
import io.micronaut.cache.annotation.CacheConfig
import io.micronaut.cache.annotation.CachePut
import io.micronaut.cache.annotation.Cacheable
import jakarta.inject.Singleton
@Singleton
@CacheConfig("counter") // (1)
class CounterService {
Map<String, Long> counters = [:]
@Cacheable // (2)
Long currentCount(String name) {
return counters.get(name)
}
@CachePut(parameters = ["name"]) // (3)
Long setCount(String name, Long count) {
counters.put(name, count)
count
}
}
package io.micronaut.microstream.docs
import io.micronaut.cache.annotation.CacheConfig
import io.micronaut.cache.annotation.CachePut
import io.micronaut.cache.annotation.Cacheable
import jakarta.inject.Singleton
import java.util.HashMap
@Singleton
@CacheConfig("counter") // (1)
open class CounterService {
val counters: MutableMap<String, Long> = HashMap()
@Cacheable // (2)
open fun currentCount(name: String): Long? = counters[name]
@CachePut(parameters = ["name"]) // (3)
open fun setCount(name: String, count: Long): Long {
counters[name] = count
return count
}
}
1 | Use the counter cache. |
2 | The result of this call will be cached. |
3 | Setting the counter will invalidate the cache for this key. |
10.2 Cache Storage
If caching that persists across restarts is required, you can back a MicroStream cache with a Storage Manager.
microstream:
storage:
backing: (1)
storage-directory: "${storageDirectory}"
channel-count: 4
cache:
counter: (2)
key-type: java.lang.String
value-type: java.lang.Long
storage: backing (3)
1 | Define a storage manager called backing |
2 | Define a cache called counter to store Strings keys and Long values |
3 | Configure the cache to use the backing storage manager |
The above example will then persist across restarts.
11 Metrics
You can enable MicroStream Metrics collection by enabling Micrometer Metrics.
If you do not wish to collect MicroStream metrics, you can set micronaut.metrics.binders.microstream.enabled
to false
in application.yml
.
12 Health
Health Endpoint exposes the status of the MicroStream Instances in your Micronaut Application.
After you add the management
dependency,
the /health
endpoint of a Micronaut application with one MicroStream instance named blue
will return:
{
"name": "application",
"status": "UP",
"details": {
"microstream.blue": {
"name": "application",
"status": "UP",
"details": {
"startingUp": false,
"running": true,
"active": true,
"acceptingTasks": true,
"shuttingDown": false,
"shutdown": false
}
},
...
}
}
By default, the details visible above are only shown to authenticated users. See the Health Endpoint documentation for how to configure that setting. |
If you wish to disable the MicroStream health check while still using the management
dependency you can set the property endpoints.health.microstream.enabled
to false
in your application configuration.
endpoints:
health:
microstream:
enabled: false
13 REST API
The MicroStream Storage isn’t a typical database server with administrative tooling, it is a pure java persistence engine which runs embedded in your application. When developing a MicroStream application, it is useful to be able to inspect the graph that is currently in your storage.
The micronaut-microstream-rest
library exposes the MicroStream REST API from inside your Micronaut application.
13.1 Enabling the REST API
To enable it, add the following dependency to your application.
developmentOnly("io.micronaut.microstream:micronaut-microstream-rest")
<dependency>
<groupId>io.micronaut.microstream</groupId>
<artifactId>micronaut-microstream-rest</artifactId>
<scope>provided</scope>
</dependency>
You also need to enable it in your configuration, as for security it is disabled by default.
microstream:
rest:
enabled: true
It will output a warning each time your application is started if the REST endpoint is enabled that this should not be deployed to production.
13.2 Configuration options
The API path is /microstream
by default.
When multiple storage managers are defined, the URL must be suffixed by the manager name /microstream/«storage-name» .
|
This prefix can be configured via the configuration.
microstream:
rest:
path: custom-prefix
The above configuration would expose the REST API at /custom-prefix
. (or /custom-prefix/«storage-name»
if you have multiple storage managers defined).
13.3 GUI
The simplest way of interacting with this REST API is to use the MicroStream client GUI.
Instructions on downloading and running this can be found here.
14 Repository
You can find the source code of this project in this repository: