SBE Domain Mapper
The SBE Domain Mapper generates proxy and adapter classes that convert between SBE (Simple Binary Encoding) encoded messages and domain representations. This guide will walk you through setting up the tool in a Gradle project.
Prerequisites
-
Java 17 or later.
-
Access to the private Artifactory repository where the
sbe-domain-mapping-processorartifact is hosted. -
SBE schema files for the messages you want to send and receive.
Gradle Setup
1. Add the Private Artifactory Repository
SBE Domain Mapper is a Premium Aeron component. The binaries are accessible from the Adaptive Artifactory.
-
sbe-domain-mapping-processor- Main module.<groupId>io.aeron.premium.sbe.extensions</groupId> <artifactId>sbe-domain-mapping-processor</artifactId>
-
sbe-domain-mapping-annotations- Annotations.<groupId>io.aeron.premium.sbe.extensions</groupId> <artifactId>sbe-domain-mapping-annotations</artifactId>
2. Add SBE Schema Files
-
Place your SBE schema files (XML) in a directory in your project, e.g.,
src/main/resources/sbe. -
Create a manifest file named
sbe-schema-manifest.txtin thesrc/main/resourcesdirectory. This file should list the relative paths to your SBE schema files, one per line. For example:sbe/schema1.xml sbe/schema2.xml
-
Ensure the
src/main/resourcesdirectory (or the directory containing the manifest and schema files) is visible to the annotation processor by adding it to theannotationProcessorclasspath in yourbuild.gradlefile:dependencies { compileOnly "io.aeron.premium.sbe.extensions:sbe-domain-mapping-annotations:$VERSION" annotationProcessor "io.aeron.premium.sbe.extensions:sbe-domain-mapping-processor:$VERSION" annotationProcessor files("src/main/resources") }
4. Add Compile Options
Adjust Java Compilation task to open java.base/jdk.internal.misc because Agrona makes use of an internal JDK module and the annotation processor uses Agrona for SBE IR.
tasks.withType(JavaCompile::class).configureEach {
options.isFork = true
options.forkOptions.jvmArgs = listOf("--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED")
}
Handling Schemas in a Separate Module
If the SBE schema files are defined in a separate Gradle module (e.g., :sbe-schemas), you can include the resources
from that module in the annotationProcessor classpath like this:
dependencies {
annotationProcessor "io.aeron.premium.sbe.extensions:sbe-domain-mapping-processor:$VERSION"
annotationProcessor project(":sbe-schemas")
}
Ensure the :sbe-schemas module includes the schema files and manifest in its src/main/resources directory.
Key Annotations
@GenerateAdapterBaseClass
Triggers the generation of a base adapter class that decodes SBE messages, adapts representations, and calls appropriate
event handler methods where handlers are listed in handlerClasses and the methods are annotated with @SbeMessage.
The generated class serves as a bridge between SBE messages and domain logic.
The generated adapters can make use of @DomainFactory, @ValueConverter, and @DataViewFactory methods to adapt
between SBE and domain representations. These are discussed in subsequent sections.
@GenerateProxyBaseClass
Triggers the generation of a base proxy class that handles the encoding of SBE messages for each of the proxy methods
where the proxy class is listed in targetClasses and the method is annotated with @SbeMessage. The generated class
serves as a bridge between domain logic and SBE messages.
The generated proxies can make use of @ValueConverter and @DataViewFactory methods to adapt between domain and SBE
representations. These are discussed in subsequent sections.
@SbeMessage
Marks a method in a handler class as capable of processing a specific SBE message. The annotated method can either
accept the SBE fields directly as parameters or accept a domain event created via a @DomainFactory.
This annotation is also used for methods on proxy interfaces to indicate that calling the method will encode a particular SBE message.
Example:
public class Participants
{
@SbeMessage("com.example.sbe.AddParticipantCommand")
public void addParticipant(long participantId, long correlationId, String name)
{
// Handle SBE message fields directly
}
@SbeMessage("com.example.sbe.CreateParticipantEvent")
public void onCreateParticipant(@SbePath(SbePath.SELF) ParticipantCreatedEvent event)
{
// Handle a domain event created via a @DomainFactory
}
}
@DomainFactory
Marks a constructor, instance method, static method, or type (with a single constructor) as a conversion for a specific
SBE type or message. The annotated executable’s parameters should match the fields of the SBE type or message, though
adjustments can be made using @SbePath. Fields may be omitted if they are not needed in the domain.
The annotation processor will only use @DomainFactory methods, when generating adapters, that are accessible and made
available by the @UseElementsFromPackage annotation (described in a subsequent section).
Example:
// Constructor mapping
@DomainFactory("com.example.sbe.ParticipantEvent")
public record ParticipantCreatedEvent(long participantId, String name)
{
// Create a domain event from SBE fields
}
// Static factory method mapping
@DomainFactory("com.example.sbe.AuctionEvent")
public static AuctionCreatedEvent createAuction(long auctionId, String description)
{
// Create a domain event from SBE fields
}
@ValueConverter
Makes an instance or static method available as a converter between types. For example, to convert backwards and
forwards between a long timestamp value (an SBE representation) and a java.time.Instant object (a domain
representation). A value converter must accept a single parameter and return a non-void type.
When generating adapter code that converts an SBE field/data value into a parameter of a domain handler or domain factory, the annotation processor will look for a matching converter method that accepts the "default Java SBE field representation type" and returns the domain’s parameter type.
When generating proxy code that converts the value an accessor returns into an SBE field or data value, the annotation processor will look for a matching converter method that accepts the domain accessor’s return type and returns the "default Java SBE field representation type".
The annotation processor will only use @ValueConverter methods, when generating proxies and adapters, that are
accessible and made available by the @UseElementsFromPackage annotation (described in a subsequent section).
Example:
public class TimestampConverter
{
@ValueConverter
public static Instant toInstant(long timestamp)
{
return Instant.ofEpochMilli(timestamp);
}
@ValueConverter
public static long toLong(Instant instant)
{
return instant.toEpochMilli();
}
}
Default Java SBE field representation types:
-
byteforchar -
byte[]for an array ofcharwith a null character set -
Stringfor an array ofcharwith a non-null character set -
byteforint8 -
byte[]for an array ofint8 -
shortforuint8 -
short[]for an array ofuint8 -
shortforint16 -
short[]for an array ofint16 -
intforuint16 -
int[]for an array ofuint16 -
intforint32 -
int[]for an array ofint32 -
longforuint32 -
long[]for an array ofuint32 -
longforint64 -
long[]for an array ofint64 -
longforuint64 -
long[]for an array ofuint64 -
floatforfloat32 -
float[]for an array offloat32 -
doubleforfloat64 -
double[]for an array offloat64 -
byte[]for variable-lengthdatawith a null character set -
Stringfor variable-lengthdatawith a non-null character set
@DataViewFactory
Marks a constructor, instance method, static method, or type (with a single constructor) as a factory for a data view.
A data view is typically a flyweight object that provides a view of some underlying SBE data. For example:
- Agrona’s AsciiSequenceView is a view over an ascii sequence in a DirectBuffer, and
- Agrona’s UnsafeBuffer can be a view over a sequence of bytes in a DirectBuffer.
Users can either use these Agrona classes or provide their own data view implementations. The data view factory method or constructor must have no parameters.
For use in generated adapters, the returned data view class must expose a
void wrap(DirectBuffer buffer, int offset, int length) method. The annotation processor will generate an adapter
that calls this method to wrap the data view around the SBE field or data section when the data view type matches
the domain handler or factory’s parameter type and the matching SBE path is either an array or a variable-length data
section.
For use in generated proxies, the returned data view class must either:
-
be a subclass of
DirectBuffer, where the underlying data is represented by all the bytes from0tocapacity()(much likeUnsafeBuffer), or -
expose
DirectBuffer buffer(),int offset(), andint length()methods (much likeAsciiSequenceView).
In cases where the data view is only applicable to data with a specific character set, a charset property can be
specified.
The annotation processor will only process data views with @DataViewFactory methods, when generating proxies and
adapters, that are accessible and made available by the @UseElementsFromPackage annotation (described in a subsequent
section).
@SbePath
Specifies a dot-separated path to a field within an SBE message or composite, relative to the current mapping context
of the @DomainFactory or @SbeMessage. This allows for navigating nested or composite structures within SBE messages.
When a parameter is not annotated, the parameter name (if available) will be used as the SBE field path.
@SbePath(SbePath.SELF)
Specifies that the mapping context for a parameter is the same as the current mapping context of the
@DomainFactory or @SbeMessage. This is useful for mapping SBE messages into domain events. It is also useful for
modelling structures in the domain that aren’t directly represented in the SBE schema, e.g., due to constraints around
message evolution where structure in the SBE message via composites could not change over time.
Example:
@DomainFactory("com.example.sbe.MyMessage")
public record Foo(
int bar,
@SbePath(SbePath.SELF) Baz baz
)
{
}
@DomainFactory("com.example.sbe.MyMessage")
public record Baz(
@SbePath("bazFoo") int foo,
@SbePath("bazBar") int bar
)
{
}
<sbe:message name="MyMessage" id="1">
<field name="bar" id="1" type="int32"/>
<field name="bazFoo" id="2" type="int32"/>
<field name="bazBar" id="3" type="int32" sinceVersion="2"/>
</sbe:message>
@SbeEnumCaseName
Maps a specific SBE enum value to a case name, typically used for converting SBE enums into domain-specific enums or constants.
@UseElementsFromPackage
Specifies a package, from which elements annotated with @DomainFactory, @ValueConverter, and @DataViewFactory,
that the annotation processor can use when generating code, e.g., to perform conversions. This annotation is used in
conjunction with @GenerateAdapterBaseClass to control which mappings are available for the generated adapter.
Parameter Mapping and Unmapped Parameters
Parameter Mapping within Generated Adapters
Generated adapters will associate SBE fields to parameters using the following rules, in order of precedence:
-
@SbePath: If a parameter is annotated with@SbePath, the generator uses the specified path to locate the field in the SBE message. -
Parameter Name: If no
@SbePathis provided, the generator attempts to match the parameter name to an SBE field name.
The parameter associated with an SBE field may or may not have a matching type. Generated adapters can convert between SBE and domain representations using the following approaches:
-
When the SBE type is a message or a composite:
-
The adapter can use a
@DomainFactoryto convert the complex SBE type into a domain object where: -
The parameter type matches the type returned by the factory
-
When the SBE type is a block field or variable-length data section:
-
The adapter can use a
@ValueConverterto convert the SBE type into a domain representation where: -
The handler’s parameter type matches the type returned by the converter
-
The converter’s parameter type matches the default Java SBE field representation type
-
-
When the SBE type is an array field or variable-length data section:
-
The adapter can use a data view to represent the underlying data where:
-
The adapter can construct a reusable data view via a
@DataViewFactorymethod -
The data view type matches the handler’s parameter type
-
-
If annotated with a charset, the factory’s charset matches the SBE field’s charset
-
In cases where multiple conversions are applicable, i.e., the conversion is ambiguous, the annotation processor will generate an error. In cases where a conversion is possible and the default SBE Java representation type matches the handler’s parameter type, the generated adapter will use the conversion.
If the generator cannot map a parameter using these rules, it will generate an abstract method in the base adapter class for you to supply the missing argument manually.
Handling Unmapped Parameters and Repeating Groups
For parameters that are not automatically mapped (including repeating groups), the code generator creates an abstract method in both cases of the base adapter and base proxy classes.
For the adapter, this method allows you to supply the missing argument or handle repeating groups manually.
For example:
protected abstract SomeType supplyUnmappedParameter(SomeSbeMessageDecoder decoder);
protected abstract MyListType mapRepeatingGroup(SomeSbeMessageDecoder decoder);
For the proxy, this method allows you to encode repeating groups manually.
For example:
protected abstract SomeType supplyUnmappedParameter(SomeProxyMethodParamater parameter);
protected abstract void mapRepeatingGroup(SomeSbeMessageEncoder encoder, SomeProxyMethodParamater parameter);
Handling Return Types
If the handler methods return a different type to the adapter’s return type, the code generator will create an abstract method to map the handler’s result to the adapter’s return type.
Example: Mapping boolean to ControlledFragmentHandler.Action
A common use case is converting a boolean return type from the handler (e.g., to indicate success or backpressure) to
Aeron’s ControlledFragmentHandler.Action. Here’s an example implementation:
@Override
protected ControlledFragmentHandler.Action mapHandlerResult(boolean handlerResult)
{
return handlerResult ? ControlledFragmentHandler.Action.CONTINUE : ControlledFragmentHandler.Action.ABORT;
}
In this example:
-
If the handler returns
true, the adapter returnsControlledFragmentHandler.Action.CONTINUEto indicate that processing should continue. -
If the handler returns
false, the adapter returnsControlledFragmentHandler.Action.ABORTto indicate that processing should stop (e.g., due to backpressure).
Parameter Mapping within Generated Proxies
Generated adapters will associate SBE fields to parameters using the following rules, in order of precedence:
-
@SbePath: If a parameter or accessor method is annotated with@SbePath, the generator uses the specified path to locate the field in the SBE message. -
Parameter or Accessor Method Name: If no
@SbePathis provided, the generator attempts to match the parameter name or accessor method name (without thegetprefix) to an SBE field name.
The parameter or accessor associated with an SBE field may or may not have a matching type. Generated proxies can convert between SBE and domain representations using the following approaches:
-
When the SBE type is a block field or variable-length data section:
-
The proxy can use a
@ValueConverterto convert the domain representation into an SBE representation where:-
The proxy method’s parameter type (or domain event’s accessor) matches the type accepted by the converter
-
The converter’s return type matches the default Java SBE field representation type
-
-
-
When the SBE type is an array field or variable-length data section:
-
The proxy can use a data view to represent the data being supplied where:
-
The data view type matches the proxy method’s parameter (or domain event’s accessor) type
-
-
If annotated with a charset, the factory’s charset matches the SBE field’s charset
-
In cases where multiple conversions are applicable, i.e., the conversion is ambiguous, the annotation processor will generate an error. In cases where a conversion is possible and the default SBE Java representation type matches the handler’s parameter type, the generated proxy will use the conversion.
If the generator cannot map a parameter using these rules, it will generate an abstract method in the base proxy class for you to supply the missing SBE value manually.
Troubleshooting
Missing SBE Schemas
If the tool cannot find a message in the available SBE schemas, it will produce an error message during compilation. To resolve this:
-
Ensure the
sbe-schema-manifest.txtfile exists and contains the correct paths to your SBE schema files. -
Verify that the schema files are in the specified location.
-
Ensure the
src/main/resourcesdirectory (or the directory containing the manifest and schema files) is included in theannotationProcessorclasspath in yourbuild.gradlefile.