Event Handlers
This section describes how to register event handlers on services. In CAP everything that happens at runtime is an event that is sent to a service. With event handlers the processing of these events can be extended or overridden. Event handlers can be used to handle CRUD events, implement actions and functions and to handle asynchronous events from a messaging service.
Introduction to Event Handlers
CAP allows you to register event handlers for events on services. An event handler is simply a Java method. Event handlers enable you to add custom business logic to your application by either extending the processing of an event, or by completely overriding its default implementation.
TIP
Event handlers are a powerful means to extend CAP. Did you know, that most of the built-in features provided by CAP are implemented using event handlers?
Common events are the CRUD events (CREATE
, READ
, UPDATE
, DELETE
), which are handled by the different kinds of CQN-based services. These events are most typically triggered, when an HTTP-based protocol adapter (for example OData V4) executes a CQN statement on an Application Service to fulfill the HTTP request. The CAP Java SDK provides a lot of built-in event handlers (also known as Generic Providers) that handle CRUD operations out of the box and implement the handling of many CDS annotations. Applications most commonly use event handlers on CRUD events to extend the event processing by using the Before
and After
phase.
Actions and Functions that are defined by an Application Service in its model definition are mapped to events as well. Therefore, to implement the business logic of an action or function, you need to register event handlers as well. Event handlers that implement the core processing of an event should be registered using the On
phase.
Events in CAP can have parameters and - in case they are synchronous - a return value. The CAP Java SDK uses Event Contexts to provide a type-safe way to access parameters and return values. In the case of CRUD events the corresponding Event Contexts provide for example access to the CQN statement. Event Contexts can be easily obtained in an event handler.
Event Phases
Events are processed in three phases that are executed consecutively: Before
, On
, and After
. When registering an event handler the phase in which the event handler should be called, needs to be specified. The CAP Java SDK provides an annotation for each event phase (@Before
, @On
, and @After
). These annotations can be used on event handler methods to indicate which phase of the event processing the method handles.
It's possible to register multiple event handlers for each event phase. Handlers within the same event phase are never executed concurrently. In case concurrency is desired, it needs to be explicitly implemented within an event handler. Note that by default there is no guaranteed order in which the handlers of the same phase are called.
The following subsections describe the semantics of the three phases in more detail.
Before
The Before
phase is the first phase of the event processing. This phase is intended for filtering, validation, and other types of preprocessing of the incoming parameters of an event. There can be an arbitrary number of Before
handlers per event.
The processing of the Before
phase is completed when one of the following conditions applies:
- All registered
Before
handlers were successfully called. Execution continues with theOn
phase. - A handler completes the event processing by setting a return value or setting the state of an event to completed. In this case, any remaining registered
Before
andOn
handlers are skipped and execution continues with theAfter
phase. - A handler throws an exception. In this case, event processing is terminated immediately.
On
The On
phase is started after the Before
phase, as long as no return value is yet provided and no exception occurred. It's meant to implement the core processing of the event. There can be an arbitrary number of On
handlers per event, although as soon as the first On
handler successfully completes the event processing, all remaining On
handlers are skipped.
The On
phase is completed when one of the following conditions applies:
- A handler completes the event processing by setting a result value or setting the state of an event to completed. In this case, any remaining registered
On
handlers are skipped and execution continues with theAfter
phase. - A handler throws an exception. In this case, event processing is terminated immediately.
In case of synchronous events, if after the On
phase, no handler completed the event processing, it's considered an error and the event processing is aborted with an exception. However when registering an On
handler for an asynchronous event it is not recommended to complete the event processing, as other handlers might not get notified of the event anymore. In that case CAP ensures to auto-complete the event, once all On
handlers have been executed.
After
The After
phase is only started after the On
phase is completed successfully. Handlers are therefore guaranteed to have access to the result of the event processing. This phase is useful for post-processing of the return value of the event or triggering side-effects. A handler in this phase can also still abort the event processing by throwing an exception. No further handlers of the After
phase are called in this case.
Event Contexts
The EventContext is the central interface, that provides information about the event to the event handler. The EventContext interface is a general interface that can be used with every event, it provides:
- Name of the event
- Entity targeted by the event
- Service the event was sent to
- Parameters and return value
- Request Context: User information, tenant-specific CDS model, headers and query parameters
- ChangeSet Context: Transactional boundaries of the event
- Service Catalog
- CDS Runtime
Parameters and the return value can be obtained and stored as key-value pairs in the Event Context using its get
and put
methods.
EventContext context = EventContext.create("myEvent", null);
// set parameters
context.put("parameter1", "MyParameter1");
context.put("parameter2", 2);
srv.emit(context); // process event
// access return value
Object result = context.get("result");
Using the get
and put
methods has several drawbacks: The API is neither type-safe nor is it clear what the correct keys for different event parameters are. To solve these issues it is possible to overlay the general Event Context with an event-specific Event Context, which provides typed getters and setters for the parameters of a specific event. For each event that the CAP Java SDK provides out-of-the-box (for example the CRUD events) a corresponding Event Context is provided.
Let's have a look at an example. The CdsReadEventContext interface is the READ
event-specific Event Context. As one of the parameters of the READ
event is a CqnSelect it provides a CqnSelect getCqn()
method. The return value of a READ
event is a Result. The context therefore also provides a Result getResult()
and a setResult(Result r)
method. You can use the as
method provided by the general Event Context to overlay it:
CdsReadEventContext context = genericContext.as(CdsReadEventContext.class);
CqnSelect select = context.getCqn();
context.setResult(Collections.emptyList());
Result result = context.getResult();
The getter and setter methods, still operate on the simple get/put API shown in the previous example. They just provide a type-safe layer on top of it. The as
method makes use of Java Proxies behind the scenes. Therefore, an interface definition is all that is required to enable this functionality.
TIP
Use these event-specific type-safe Event Context interfaces whenever possible.
For actions or functions defined in the CDS model the CAP Java SDK Maven Plugin can automatically generate Event Context objects, which provide type-safe access to the action or function parameters and allow to set the return values.
Completing the Event Processing
The Event Context also provides means to indicate the completion of the core processing of the event. This is important to finish the On
phase of a synchronous event. In case the synchronous event does not have a return value the setCompleted()
method should be used to indicate the completion of the core processing of the event.
context.setCompleted();
In case the synchronous event has a return value the setResult(...)
method of the event-specific Event Context automatically triggers the setCompleted()
method as well.
context.setResult(myResult);
Explicitly Proceeding the On Handler Execution
An event handler registered to the On phase
can call proceed()
on the Event Context to explicitly proceed executing the remaining registered On
handlers. This allows the handler to pre- and post-process the Event Context in a single method, without fully overwriting the core processing of the event. It also enables catching and handling exceptions thrown by an underlying handler.
@On(event = "myEvent")
void wrapMyEvent(EventContext context) {
context.put("param", "Adjusted"); // pre-process
context.proceed(); // delegate to underlying handler
context.put("result", 42); // post-process
}
Calling proceed()
from a Before
or After
event handler is not allowed and will raise an exception. If an On
handler has already completed the event processing, calling proceed()
will not have any effects.
Defining Custom EventContext Interfaces
In certain cases you might want to define your own custom event-specific Event Context interfaces. Simply define an interface, which extends the general EventContext
interface. Use the @EventName
annotation to indicate for which event this context should be used. Getters and setters defined in the interface automatically operate on the get
and put
methods of the general Event Context. In case you want to define the key they use for this, you can use the @CdsName
annotation on the getter and setter method.
@EventName("myEvent")
public interface MyEventContext extends EventContext {
static MyEventContext create() {
return EventContext.create(MyEventContext.class, null);
}
@CdsName("Param")
String getParam();
void setParam(String param);
void setResult(Integer result);
Integer getResult();
}
TIP
For actions or functions defined in the CDS model the CAP Java SDK Maven Plugin can automatically generate Event Context objects, which provide type-safe access to the action or function parameters and allow to set the return values.
Event Handler Classes
Event handler classes contain one or multiple event handler methods. You can use them to group event handlers, for example for a specific service. The class can also define arbitrary methods, which aren't event handler methods, to provide functionality reused by multiple event handlers.
In Spring Boot, event handler classes are Spring beans. This enables you to use the full range of Spring Boot features in your event handlers, such as Dependency Injection or Scopes.
The following example defines an event handler class:
import org.springframework.stereotype.Component;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.ServiceName;
@Component
@ServiceName("AdminService")
public class AdminServiceHandler implements EventHandler {
// ...
}
- The annotation
@Component
instructs Spring Boot to create a bean instance from this class. - The
EventHandler
marker interface is required for CAP to identify the class as an event hander class among all beans and scan it for event handler methods. - The optional
@ServiceName
annotation can be used to specify the default service, which event handlers are registered on. It is possible to override this value for specific event handler methods.
TIP
The CAP Java SDK Maven Plugin generates interfaces for services in the CDS model. These interfaces provide String constants with the fully qualified name of the service. In case the service name is based on the CDS model it is recommended to use these constants with the @ServiceName
annotation.
It is possible to specify multiple service names. Event handlers are registered on all of these services.
@ServiceName(["AdminService", "CatalogService"])
The type
attribute of the @ServiceName
annotation can be used to register event handlers on all services of a certain type:
@ServiceName(value = "*", type = ApplicationService.class)
Event Handler Annotations
Event handler methods need to be annotated with one of the following annotations: @Before
, @On
, or @After
. The annotation defines, during which phase of the event processing the event handler is called.
Each of these annotations can define the following attributes:
service
: The services the event handler is registered on. It's optional, if a@ServiceName
annotation is specified on class-level.serviceType
: The type of services the event handler is registered on, for example,ApplicationService.class
. Can be used together withservice = "*"
to register an event handler on all services of a certain type.event
: The events the event handler is registered on. The event handler is invoked in case any of the events specified matches the current event. Use*
to match any event. It's optional, if the event can be inferred through a Event Context argument in the handler signature.entity
: The target entities the event handler is registered on. The event handler is invoked in case any of the entities specified matches the current entity. Use*
to match any entity. It's optional, if the entity can be inferred through a POJO-based argument in the handler signature. If no value is specified or can be inferred it defaults to*
.
TIP
The interfaces of different service types provide String constants for the events they support (see for example the CqnService). The CAP Java SDK Maven Plugin generates interfaces for entities in the CDS model, which provide String constants with their fully qualified name. It is recommended to use these constants with the event
or entity
attributes of the annotations.
// registers on multiple events
@Before(event = { "CREATE", "UPDATE" }, entity = "AdminService.Books")
// overrides the default service on class-level
// registers on any entity
@On(service = "CatalogService", event = "READ")
// usage of String constants is recommended
@After(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
Event Handler Method Signatures
The most basic signature of an event handler method is public void process(EventContext context)
. However event-specific Event Context and entity data arguments and certain return values are supported as well and can be freely combined. It is even valid for event handler methods to have no arguments at all. Handler methods don't necessarily have to be public methods. They can also be methods with protected, private, or package visibility.
Event Context Arguments
The Event Context is the central interface that provides information about the event to the event handler. An event handler can get access to the general EventContext
by simply declaring an argument of that type in its method:
@Before(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
public void readBooks(EventContext context) { }
It is also possible to directly refer to event-specific Event Context interfaces in your arguments. In that case the general Event Context is automatically overlayed with the event-specific one:
@Before(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
public void readBooks(CdsReadEventContext context) { }
If an event-specific Event Context argument is used and the event handler annotation declares an event as well, the argument is automatically validated during startup of the application. Alternatively it is possible to let CAP infer the event for the event handler registration from the Event Context argument:
@Before(entity = Books_.CDS_NAME)
public void readBooks(CdsReadEventContext context) { }
TIP
The mapping between an Event Context interface and an event, is based on the @EventName
annotation of the Event Context interface.
In case an event handler is registered on multiple events only the general Event Context argument can be used. At runtime, the corresponding event-specific Event Context can be overlayed explicitly, if access to event-specific parameters is required:
@Before(event = { CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE }, entity = Books_.CDS_NAME)
public void changeBooks(EventContext context) {
if(context.getEvent().equals(CqnService.EVENT_CREATE)) {
CdsCreateEventContext ctx = context.as(CdsCreateEventContext.class);
// ...
} else {
CdsUpdateEventContext ctx = context.as(CdsUpdateEventContext.class);
// ...
}
}
Entity Data Arguments
When adding business logic to an Application Service event handlers most commonly need to access entity data. Entity data can be directly accessed in the event handler method, by using an argument of type CdsData
:
@Before(event = { CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE },
entity = Books_.CDS_NAME)
public void changeBooks(List<CdsData> data) { }
The
CdsData
interface extendsMap<String, Object>
with some additional JSON serialization capabilities and therefore provides a generic data access capability.
The CAP Java SDK Maven Plugin can generate data accessor interfaces for entities defined in the CDS model. These interfaces allow for a typed access to data and can be used in arguments as well:
@Before(event = { CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE },
entity = Books_.CDS_NAME)
public void changeBooks(List<Books> books) { }
TIP
To learn more about typed access to data and how entity data is handled in CAP Java SDK, have a look at Working with Data.
If an entity data argument is used and the event handler annotation declares an entity as well, the argument is automatically validated during startup of the application. Alternatively it is possible to let CAP infer the entity for the event handler registration from the entity data argument:
@Before(event = { CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE })
public void changeBooks(List<Books> books) { }
TIP
The mapping between a data accessor interface and an entity, is based on the @CdsName
annotation of the accessor interface.
Entity data arguments only work on CRUD events of CQN-based services. In addition they work with the draft-specific CRUD events provided by Draft Services.
The origin from which the entity data is provided depends on the phase of the event processing. During the Before
and On
phase it is obtained from the CQN statement. The CQN statement contains the entity data that was provided by the service client. However during the After
phase the entity data is obtained from the Result
object, which is provided as the return value of the event to the service client. Some CQN statements such as for example CqnSelect
, which is used with READ
events, don't allow to carry data. In these cases entity data arguments are set to null
.
There are different flavours of entity data arguments. Besides using List<Books>
it is also possible to use Stream<Books>
:
@Before(event = { CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE })
public void changeBooks(Stream<Books> books) { }
It is also possible to use non-collection-based entity arguments, such as Books
. However if multiple data rows are available at runtime an exception will be thrown in that case:
@Before(event = { CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE })
public void changeBook(Books book) { }
TIP
Entity data arguments are safely modifiable. During the Before
and On
phase changes affect the data carried by the CQN statement. During the After
phase changes affect the return value of the event.
Return Values
The return value of an event can be set by returning a value in an event handler method:
@On(entity = Books_.CDS_NAME)
public Result readBooks(CdsReadEventContext context) {
return db.run(context.getCqn());
}
In case an event handler method of the Before
or On
phase has a return value it automatically completes the event processing, once it is executed. Event handler methods of the After
phase that have a return value, replace the return value of the event.
Only return values that extend Iterable<? extends Map<String, Object>>
are supported. The Result
object or a list of entity data (for example List<Books>
) fulfill this requirement.
@On(entity = Books_.CDS_NAME)
public List<Books> readBooks(CdsReadEventContext context) {
Books book = Struct.create(Books.class);
// ...
return Arrays.asList(book);
}
Event handler methods with return values only work on CRUD events of CQN-based services or the draft-specific CRUD events provided by Draft Services.
TIP
To learn how to build your own Result objects, have a look at the Result Builder API
Ordering of Event Handler Methods
You can influence the order in which the event handlers are executed by means of CAP annotation @HandlerOrder. It defines the order of handler methods within each phase of events. You may use constants HandlerOrder.EARLY
or HandlerOrder.LATE
to place one handler earlier or later relative to the handlers without the annotation. Note that handlers with the same @HandlerOrder
are executed in a deterministic, but arbitrary sequence.
Generic handlers typically are executed by the framework before HandlerOrder.EARLY
and after HandlerOrder.LATE
:
- Generic framework handlers
- Custom handlers, annotated with
HandlerOrder.EARLY
- Custom handlers for phases
@Before
,@On
, and@After
- Custom handlers, annotated with
HandlerOrder.LATE
- Generic framework handlers
For example, in the following snippet, several methods are bound to the same phase of the READ
event for the same entity and are executed one after another:
@After(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
@HandlerOrder(HandlerOrder.EARLY)
public void firstHandler(EventContext context) {
// This handler is executed first
}
@After(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
public void defaultHandler(EventContext context) {
// This one is the second
}
@After(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
@HandlerOrder(HandlerOrder.LATE)
public void lastHandler(EventContext context) {
// This one is the last
}
CAP Java always executes event handlers in the order specified by the annotations, even if the handlers are defined in separate classes.
In addition, CAP Java respects the Spring Framework annotation @Order
and executes the handlers, that are registered in such annotated beans, in the order defined by that annotation. If the @HandlerOrder
annotation is specified, this overrides the order defined by @Order
.