Annotations

Using a few lines of code, activate and configure the VLINGO XOOM components.

There's an API defacto standard that developers always deserve freedom of choice in terms of either crafting each part of their own code, or to accelerate the development steps by taking advantage of default configurations and shorthand elements. The VLINGO XOOM platform aims to provide technical autonomy, no matter the strategy you choose, even allowing the combination of both.

Generally speaking, VLINGO XOOM annotations fit well when you want more succinct code. From the application initialization to the persistence resources, we provide a brief description of each annotation.

Annotation

Description

@Xoom

Its main purpose is to instantiate and setup essential elements of a VLINGO XOOM application or microservice:

@ResourceHandlers

@AutoDispatch

Provides a faster and straightforward way to dispatch REST requests to the domain model.

@Persistence

@EnableQueries

@Projections / @Projection

@Adapters /

@Adapter

@DataObjects

Enables the auto-creation of database tables for data objects.

When using any kind of IDEs, be aware that they usually do not evaluate compile-time annotations as soon as you declare it or immediately when you open a project with an IDE for the first time. In this case, IDEs may notify one or more compilation errors. That can be easily avoided just by building your project when you first open it or at the time you declare annotation(s).

For a practical understanding of annotations, the next section brings more details.

@Xoom

It's widely known that the starting point of any Java program is a static main() method. In addition, when creating of a VLINGO XOOM application or microservice, we must instantiate World, and pass it to other classes that use it for actors operation. So, that's what the @Xoom annotation mainly provides.

  • An Initializer class with a static main() method, which is the hook for JVM in the application execution

  • Creation of World and Stage with the AddressFactory option, making the Stage instance accessible for classes that request Actor operations directly from it

Besides that, @Xoom takes care of reading the properties file with the Mailbox definition and starts a XOOM HTTP server. All of those tasks are achieved simply by annotating a basic class.

@Xoom(name="xoom-app-name")
public class AppInitializer {

}

The application name attribute is the minimal information for running application with @Xoom but there are also other attributes. Here's the full list.

Attribute

Type

Description

Required

name

String

Sets the application name.

Yes

blocking

Boolean

True or false for respectively synchronous or asynchronous actor messaging. The default value is false.

No

addressFactory

Annotation

Sets the AddressFactory type: BASIC, UUID, GRID; and IdentityGenerator type:RANDOM, TIME_BASED, NAME_BASED

No

The next code snippet shows the @Xoom attributes with non-default values.

@Xoom(name = "app-name", blocking = true,
        addressFactory = @AddressFactory(type = GRID, generator = RANDOM))
public class AppInitializer {

}

Often, application initialization tasks need to be refined by some handwritten code. In the case where this is needed when using the @Xoom annotation, an initializer class may implement the interface io.vlingo.xoom.turbo.XoomInitializationAware.

@Xoom(name = "app-name")
public class AppInitializer implements XoomInitializationAware {
  
  @Override
  public void onInit(final Stage stage) { 
     //Here, add some logic that depends on Stage   
  }
  
  @Override
  public Configuration configureServer(final Stage stage, final String[] args) {
     //Define a custom server configuration
  }
}

@ResourceHandlers

This is a @Xoom-related annotation for REST resources initialization. First, your custom resource class must extend io.vlingo.xoom.http.resource.DynamicResourceHandler. This resource class should implement a constructor that takes a Stage instance parameter. The resource class must also implement a routes() method, through which fluent route mappings are possible, see XOOM HTTP.

public class InsuranceResource extends DynamicResourceHandler {
    
    public InsuranceResource (final Stage stage) {
        super(stage);
    }

    public Completes<ObjectResponse<Insurance>> retrieveInsurances() {
       // retrieve Insurance instances
    }

    @Override
    public Resource<?> routes() {
        return resource("Insurances", get("/insurances").handle(this::retrieveInsurances));
    }
}

To wire an instance of an InsuranceResource, annotate the initializer class providing the package containing the REST resource classes.

@Xoom(name = "app-name")
@ResourceHandlers(packages = {"io.vlingo.xoomapp.resources"})
public class AppInitializer {
  
}

Optionally you may decide to provide a number of resource classes instead of the package name.

@Xoom(name = "app-name")
@ResourceHandlers({InsuranceResource.class, OtherResource.class ...})
public class AppInitializer {
  

}

As described below, @ResourceHandlers supports two optional attributes, but only one must be set.

Attribute

Type

Description

Required

value

Class <? extends DynamicResourceHandler> []

An array of DynamicResourceHandler subclasses.

No

packages

String []

An array of Java package names that each contains some number of DynamicResourceHandler subclasses.

No

@AutoDispatch

Often, a considerable part of the effort while building an application is not on its vital elements but on recurrent and straightforward tasks such as mapping logic, components configuration, and general application settings. Facing this discrepancy, @AutoDispatch enables you to invest less in writing REST resources and broker/bus exchange message listeners so that you are able to focus on the core of your application, that is, the domain model.

The next code snippet shows how this convenient annotation transforms your REST resource implementation:

@AutoDispatch(path="/products", handlers = ProductResourceHandlers.class)
@Queries(protocol = ProductQueries.class, actor = ProductQueriesActor.class)
@Model(protocol = Product.class, actor = ProductEntity.class, data = ProductData.class)
public interface ProductResource {

  @Route(method = POST, handler = ProductResourceHandlers.REGISTRATION)
  @ResponseAdapter(handler = ProductResourceHandlers.ADAPT_STATE)
  Completes<Response> register(@Body final ProductData data);

  @Route(method = PATCH, path = "/{id}/profit-margin", handler = ProductResourceHandlers.PROFIT_MARGIN)
  @ResponseAdapter(handler = ProductResourceHandlers.ADAPT_STATE)
  Completes<Response> applyProfitMargin(@Id final String id, @Body final ProductData data);

  @Route(method = GET, handler = ProductResourceHandlers.ALL_PRODUCTS)
  Completes<Response> allProducts();

}

Yes, an interface with only abstract methods is the primary piece for implementing an@AutoDispatch resource. Now, following the order of how each annotation is placed in the code, let's understand how it works.

@AutoDispatch

Starting with @AutoDispatch , which is the root annotation and accepts these values:

Attribute

Type

Description

Required

path

String

URI root path

Yes

handlers

Class<?>

A mapping configuration class relating aggregate/queries methods and its indexes.

Yes

Here we open a parenthesis to clarify the ProductResourceHandlers class, declared in the @AutoDispatchhandler attribute in the previous code snippet. Looking at it, we can see the aggregate methods to which REST requests are going to be forwarded.

public class ProductResourceHandlers {

  public static final int REGISTRATION = 0;
  public static final int PROFIT_MARGIN = 1;
  public static final int ALL_PRODUCTS = 2;
  public static final int ADAPT_STATE = 3;

  public static final HandlerEntry<Three<Completes<ProductState>, Stage, ProductData>> REGISTRATION_HANDLER =
          HandlerEntry.of(REGISTRATION , ($stage, data) -> Product.open($stage, data.creditLimitThreshold));

  public static final HandlerEntry<Three<Completes<ProductState>, Product, ProductData>> PROFIT_MARGIN_HANDLER =
          HandlerEntry.of(PROFIT_MARGIN , (product, data) -> product.applyProfitMargin(data.profitMargin));

  public static final HandlerEntry<Two<ProductData, ProductState>> ADAPT_STATE_HANDLER =
          HandlerEntry.of(ADAPT_STATE, ProductData::from);

  public static final HandlerEntry<Two<Completes<Collection<ProductData>>, ProductQueries>> QUERY_ALL_HANDLER =
          HandlerEntry.of(ALL_PRODUCTS, $queries -> $queries.allProducts());

}

The HandlerEntry relates an integer index to a function that invokes an aggregate method. Through that index, an aggregate/queries method can be set as the handler of a specific route as showed in the ProductResource interface:

@Route(method = POST, handler = ProductResourceHandlers.REGISTRATION)
...
Completes<Response> register(@Body final ProductData data);

The return type of the mapped method, along with the types of its parameters are respectively defined by the HandlerEntry generics. In other words, the first generic type, from left to right, is always the method return type, so, in the HandlerEntry corresponding to the Product Registration,Completes<ProductState> is the return type when Product.register is invoked while Stage and ProductData are the parameter types.

... HandlerEntry<Three<Completes<ProductState>, Stage, ProductData>> REGISTRATION_HANDLER =
     HandlerEntry.of(REGISTRATION , ($stage, data) -> Product.register($stage, data.name)); 

Usually, the generic types of a HandlerEntry match with the parameter types supported by its corresponding route. However, in the previous example, Stage is also a required parameter but it's not in the route signature. Fortunately, at compile-time, Stage and Logger are automatically included as an instance member in the auto-dispatch resource class. Just be aware that, for accessing any instance member inside a HandlerEntry function, you need always to use the $ + memberName pattern (e.g $stage, $logger).

Regarding the supported number of parameters, HandlerEntry is served by a set of interfaces for parameter type declaration which allow you to inform two to five parameter types:

public interface Handler {

    @FunctionalInterface
    interface Two<A, B> extends Handler {
        A handle(B b);
    }

    @FunctionalInterface
    interface Three<A, B, C> extends Handler {
        A handle(B b, C c);
    }

    @FunctionalInterface
    interface Four<A, B, C, D> extends Handler {
        A handle(B b, C c, D d);
    }

    @FunctionalInterface
    interface Five<A, B, C, D, E> extends Handler {
        A handle(A a, B b, C c, D d, E e);
    }

}

If your application only have@AutoDispatchresources, you are freed from annotating your bootstrap class with @ResourceHandlers.

@Model / @Queries

The @Model and @Queries class-level annotations meets the condition that a @AutoDispatch route must perform operations on a specific aggregate entity or read data from a query actor. That implies you have to use at least one of these annotations and, at most, one of each. Here's the supported field list:

Attribute

Type

Description

Required

protocol

Class<?>

The aggregate / queries protocol (interface) class

true

actor

Class<? extends EntityActor/ Actor>

true

data

Class<?>

The adapted model type to be serialized in the response body. Only supported in the @Model annotation.

true

Note that, using @Queries, you are able to access the queries actor inside the HandlerEntry function through the instance member named as $queries :

public static final HandlerEntry<Two<Completes<Collection<ProductData>>, ProductQueries>> QUERY_ALL_HANDLER =
          HandlerEntry.of(ALL_PRODUCTS, $queries -> $queries.allProducts());

@Route

The @Route annotation has to be used at method-level mainly declaring a HTTP method. Optionally, you may inform a relative URI subpath and its handler index.

Attribute

Type

Description

Required

method

io.vlingo.xoom.http.Method

The supported HTTP method for a route

Yes

path

String

The route subpath relative to the root path. If not informed, the root path is set by default.

No

handler

int

The index of the handler that will be invoked. If not informed, the annotated method must be concrete.

No

@ResponseAdapter

Also, a method-level annotation that enables a function call to adapt the request output. It only supports a single attribute.

Attribute

Type

Description

Required

handler

int

The index of the handler that will be invoked to adapt the request output.

Yes

@Id

This useful annotation is applied to an entity id present in the route parameter. Under the hood, @Id indicates a route operation that is performed on an existing entity and makes VLINGO/XOOM responsible for loading it. So, the benefit is that you can just take care of using the loaded entity inside the HandlerEntry function.

Let's go back to ProductResource and see how it works:

...

public interface ProductResource {

  @Route(method = PATCH, path = "/{id}/profit-margin", handler = ProductResourceHandlers.PROFIT_MARGIN)
  @ResponseAdapter(handler = ProductResourceHandlers.ADAPT_STATE)
  Completes<Response> applyProfitMargin(@Id final String id, @Body final ProductData data);

  ...
}

The code slice above shows /profit-margin route which has an id parameter properly annotated with @Id . Afterwards, its handler function is much simpler because it's benefited by the auto-loaded entity:

  public static final HandlerEntry<Three<Completes<ProductState>, Product, ProductData>> PROFIT_MARGIN_HANDLER =
          HandlerEntry.of(PROFIT_MARGIN , (product, data) -> product.applyProfitMargin(data.profitMargin));

Finally, here, the product parameter is exactly the corresponding entity to the id passed in the route parameter.

@Body

Annotating a route parameter with @Body simply tells XOOM HTTP to deserialize the request payload into that parameter. In the following example, the payload is deserialized into a ProductData object.

public interface ProductResource {

  @Route(method = POST, handler = ProductResourceHandlers.REGISTRATION)
  @ResponseAdapter(handler = ProductResourceHandlers.ADAPT_STATE)
  Completes<Response> register(@Body final ProductData data);
 
  ... 
}

@Persistence

Regarding persistence configuration, through the onInit() method you can choose to manually create the code for the application infrastructure, as in the Hello, World example.

Alternatively, adopt@Persistence for a more succinct way to set up the persistence. By using that annotation, VLINGO XOOM supports auto-configuration of:

  • The selected Storage Type for the Domain Model;

  • When using CQRS, a State Store for the Query Model and the selected Storage Type for the Command Model;

  • Datasource connection, including schema/tables creation;

The example below shows how to enable persistence auto-configuration.

@Persistence(basePackage = "io.vlingo.xoom.turbo.annotation", storageType = STATE_STORE)
public class PersistenceSetup {

}

The next table describes @Persistence attributes:

Attribute

Type

Description

Required

basePackage

String

The project's base package

true

storageType

StorageType

true

cqrs

Boolean

false

The database credentials and other configuration parameters can be informed in three ways:

  • vlingo-xoom.properties

  • Environment variables;

  • Combining both;

The supported properties and environment variables are listed below:

Property Name

Environment Variable

Description

Required

database

VLINGO_XOOM_DATABASE

Database Type*. If IN_MEMORY, other properties are not required.

true

database.name

VLINGO_XOOM_DATABASE_NAME

Schema name.

true

database.driver

VLINGO_XOOM_DATABASE_DRIVER

The qualified class name of JDBC Driver

true

database.url

VLINGO_XOOM_DATABASE_URL

Connection URL

true

database.username

VLINGO_XOOM_DATABASE_USERNAME

Database username

true

database.password

VLINGO_XOOM_DATABASE_PASSWORD

Password

false

database.originator

VLINGO_XOOM_DATABASE_ORIGINATOR

Id for the data origin

true

*Supported Database types: IN_MEMORY, POSTGRES, HSQLDB, MYSQL, YUGA_BYTE.

In case of CQRS, you can inform parameters of the Query Model Database just adding "query" before the word "database". For instance, the property database becomes query.database, the environment variable VLINGO_XOOM_DATABASE becomes VLINGO_XOOM_QUERY_DATABASE.

Here's an example of a database configuration in the vlingo-xoom.properties:

database=POSTGRES
database.name=XOOM_APP_CMD_MODEL
database.driver=org.postgresql.Driver
database.url=jdbc:postgresql://localhost/
database.username=admin
database.password=pwd
database.originator=CMD

query.database=HSQLDB
query.database.name=XOOM_APP_QUERY_MODEL
query.database.driver=org.hsqldb.jdbcDriver
query.database.url=jdbc:hsqldb:mem:
query.database.username=sa
query.database.password=pwd
query.database.originator=QUERY

The following annotations are children of @Persistence and can be only used along with it.

@EnableQueries

@EnableQueries is an annotation correlated to @Persistence and provides the proper way to make VLINGO XOOM responsible for handling your query actor implementations. In other words, using this essential annotation, these implementations become a QueryStateStoreProvider property, so we only need to take care of accessing it and using it. For a practical understanding, take a look at the following configuration class:

@Persistence(cqrs=true ...)
@EnableQueries({
        @QueriesEntry(protocol = CustomerQueries.class, actor = CustomerQueriesActor.class),
        @QueriesEntry(protocol = ProductQueries.class, actor = ProductQueriesActor.class),
})
public class PersistenceSetup {


}

@EnableQueries only accepts a combination of actor/protocol classes surrounded by the@QueriesEntry annotation which supports the following types:

Attribute

Type

Description

Required

protocol

Class<?>

TheQueriesActor protocol class

true

actor

Class<?>[]

An Array of supported DomainEvents

true

Having the @EnableQueries properly configured, which also includes the attribute cqrs of@Persistence set to true, makes the compile-time generated QueryStateStoreProviderthe holder of all mapped queries actors. So, as an illustration, let's say we need to use the CustomerQueries serving a rest resource:

public class CustomerResource {

  public Completes<Response> queryAllCustomers() {
    final CustomerQueries customerQueries = QueryModelStateStoreProvider.instance().customerQueries;
    return customerQueries.allCustomers().andThenTo(data -> Completes.withSuccess(Response.of(Ok, serialized(data))));
  }
  
}

@Projections

When using CQRS, @Projections relates the projections to their supported events, so DomainEvents can be projected into the Query Model.

@Persistence(...)
@Projections({
        @Projection(actor = CustomerProjectionActor.class, 
                becauseOf = {CustomerRegistered.class, CustomerNotified.class}),
        @Projection(actor = ProductProjectionActor.class, 
                becauseOf = {ProductDelivered.class, ProductSoldOut.class})
})
public class PersistenceSetup {

}

@Projections accepts only a list of @Projection with a pair of attributes:

Attribute

Type

Description

Required

actor

Class<? extends

StateStoreProjectionActor>

AProjectionActor class

true

becauseOf

Class<?>[]

An Array of supported DomainEvents

true

@Adapters

Add @Adapters for Aggregate/Entity state translation, so the object state within a service or application can be serialized to be persisted, and vice-versa. Here's how to set up:

@Persistence(...)
@Adapters({CustomerState.class, ProductState.class})
public class PersistenceSetup {

}

Keep in mind that @Adapters and @Projections are not dependent but both can be naturally used together as follows:

@Persistence(...)
@Projections({
        @Projection(actor = CustomerProjectionActor.class, 
                becauseOf = {CustomerRegistered.class, CustomerNotified.class}),
        @Projection(actor = ProductProjectionActor.class, 
                becauseOf = {ProductDelivered.class, ProductSoldOut.class})
})
@Adapters({CustomerState.class, ProductState.class})
public class PersistenceSetup {

}

@DataObjects

With @DataObjects , VLINGO XOOM can also take care of the creation of database tables for data objects, which are commonly used for holding your query model data. All you need to do is to map the data objects as demonstrated below:

@Persistence(...)
@DataObjects({CustomerData.class, ProductData.class})
public class PersistenceSetup {

}

Once data objects are mapped, if the database tables do not already exist, it will be automatically created during the application startup.

Last updated