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.
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 staticmain()
method, which is the hook for JVM in the application execution - Creation of
World
andStage
with theAddressFactory
option, making theStage
instance accessible for classes that requestActor
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
}
}
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 |
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.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 @AutoDispatch
handler 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
@AutoDispatch
resources, you are freed from annotating your bootstrap class with @ResourceHandlers
. 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());
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 |
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 |
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.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);
...
}
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 selectedStorage 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 | Using CQRS, two separate stores will be provided for the Command Model and Query Model. The default value is false. | 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
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<?> | The QueriesActor 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 QueryStateStoreProvider
the 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))));
}
}
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> | A ProjectionActor class | true |
becauseOf | Class<?>[] | An Array of supported DomainEvents | true |
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 {
}
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 modified 2yr ago