Skip to content

joel-jeremy/emissary

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

253 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Emissary (formerly Deezpatch)

License Gradle Build Code QL Sonatype Central codecov Coverage Quality Gate Status Maintainability Rating Reliability Rating Security Rating Vulnerabilities Bugs Duplicated Lines (%) Lines of Code Technical Debt Discord

A simple-to-use, no dependency, yet ๐Ÿ—ฒBLAZING FAST๐Ÿ—ฒ messaging library for decoupling messages (requests and events) and message handlers ๐Ÿš€

Emissary aims to take advantage of the simplicity of using the annotations for handlers (e.g. @RequestHandler/@EventHandler) without the drawbacks of reflection (slow).

Emissary aims to make it easy to build applications that apply the Command Query Responsibility Segregation (CQRS) pattern, but it is not in any way limited to that pattern only.

Like the project?

Please consider giving the repository a โญ. It means a lot! Thank you :)

Get Emissary

Important

Up until v1.1.0, the core library is published under the old deezpatch-core name. This has been renamed to emissary-core starting from v2.0.0 onwards.

Gradle

implementation "io.github.joel-jeremy.emissary:emissary-core:${version}"

Maven

<dependency>
    <groupId>io.github.joel-jeremy.emissary</groupId>
    <artifactId>emissary-core</artifactId>
    <version>${version}</version>
</dependency>

Java 9 Module Names

Important

Up until v1.1.0, the core library has the module name io.github.joeljeremy.deezpatch.core. This has been renamed to io.github.joeljeremy.emissary.core starting from v2.0.0 onwards.

Emissary jars are published with Automatic-Module-Name manifest attribute:

  • Core - io.github.joeljeremy.emissary.core

Module authors can use above module names in their module-info.java:

module foo.bar {
    requires io.github.joeljeremy.emissary.core;
}

What differentiates Emissary?

Performance

Emissary is orders of magnitude more performant than similar libraries. It takes advantage of java.lang.invoke.LambdaMetafactory to avoid the cost of invoking methods reflectively. This results in performance close to directly invoking the request handler and event handler methods!

~ 1000% more throughput compared to other similar libraries (Spring's ApplicationEventPublisher, Pipelinr, EventBus)

~ 90% faster compared to other similar libraries (Spring's ApplicationEventPublisher, Pipelinr, EventBus)

Easy Integration with Dependency Injection (DI) Frameworks

Other libraries forces users to instantiate the handlers/subscribers during startup/creation-time (usually by manually instantiating them) e.g.

// Manually instantiate subscriber and all its possible dependencies
eventBus.subscribe(new MySubscriber(new MyDependency()))

This means that these handlers/subscribers are not managed by your application's DI framework and are essentially singleton instances.

Emissary takes a different approach by allowing users to leverage their application's DI framework to instantiate and manage the request/event handlers. This enables the following (non-exhaustive list):

  • Injection of services/dependencies
  • Configurable instance lifetime/scope (singleton / new instance per message, etc.)
  • Hooks to instance lifecycles (Spring or Jakarta CDI @PostConstruct and @PreDestroy, etc.)

Note

Emissary abstracts DI frameworks via an InstanceProvider interface. This interface can be implemented by forwarding requests to a DI framework such as Spring's ApplicationContext, Guice's Injector, Jakarta CDI's BeanContainer, etc. It can also be implemented by simply new-ing up the request/event handlers when not using any DI framework.

Emissary will get request/event handler instances from InstanceProvider every time a request/event is dispatched.

Example Integration with Spring Boot/ApplicationContext

// Spring features work on the request/event handlers.
@Component
public class MyEventHandler {
    private final MyDependency myDependency;

    // This works!
    public MyEventHandler(MyDependency myDependency) {
        this.myDependency = myDependency;
    }

    // This also works!
    @PostConstruct
    public void postConstruct() {}

    // This works too!
    @PreDestroy
    public void preDestroy() {}

    @EventHandler
    public void fooCreatedEvent(FooCreatedEvent event) {
        doSomethingAboutFoo();
        myDependency.doAnotherThingAboutFoo();
    }

    @EventHandler
    public void myEvent(MyEvent event) {
        doSomething();
        myDependency.doAnotherThing();
    }
}

@Configuration
public class MyConfiguration {
    @Bean
    public Emissary emissary(ApplicationContext applicationContext) {
        return Emissary.builder()
            // Set instanceProvider to Spring's ApplicationContext::getBean function.
            // This means that all instances required by Emissary will be resolved
            // from the Spring context.
            .instanceProvider(applicationContext::getBean)
            .requests(...)
            .events(MyEventHandler.class)
            .build();
    }
}
Spring Boot
// MyConfiguration is auto-detected by Spring Boot as long as it's in the proper location.

@RestController
public class MyFooController {
    private final Emissary emissary;

    public MyFooController(Emissary emissary) {
        this.emissary = emissary;
    }

    @PostMapping("/foo")
    public void newFoo(@RequestBody CreateFooRequest request) {
        Foo foo = createFoo(request);
        emissary.publish(new FooCreatedEvent(foo));
    }
}
Plain Spring ApplicationContext
public static void main(String[] args) {
    // Configure Spring context.
    try (var applicationContext = new AnnotationConfigApplicationContext(MyConfiguration.class)) {

        Emissary emissary = applicationContext.getBean(Emissary.class);

        emissary.publish(new MyEvent());
    }
}

Example Integration with Guice

public class MyModule extends AbstractModule {
    @Override
    protected void configure() {
        // Other bindings
    }

    @Provides
    Emissary emissary(Injector injector) {
        return Emissary emissary = Emissary.builder()
            // Set instanceProvider to Guice's Injector::getInstance function.
            // This means all instances required by Emissary will be resolved
            // from the Guice container.
            .instanceProvider(injector::getInstance)
            .requests(...)
            .events(...)
            .build();
    }
}

public static void main(String[] args) {
    // Configure Guice.
    Injector injector = Guice.createInjector(new MyModule())

    Emissary emissary = injector.getInstance(Emissary.class);

    emissary.publish(new MyEvent())
}

Example Integration with Jakarta CDI

@Produces
Emissary emissary(BeanContainer beanContainer) {
    return Emissary emissary = Emissary.builder()
        // Set instanceProvider get instances from CDI's BeanContainer.
        // This means all instances required by Emissary will be resolved
        // from the Jakarta CDI container.
        .instanceProvider(clazz -> beanContainer.select(clazz).get())
        .requests(...)
        .events(...)
        .build();
}

public static void main(String[] args) {
    SeContainerInitializer containerInitializer = SeContainerInitializer.newInstance();
    // Configure CDI container using initializer
    try (SeContainer container = containerInitializer.initialize()) {
        BeanContainer beanContainer = container.getBeanContainer();
        
        Emissary emissary = beanContainer.select(Emissary.class).get();

        emissary.publish(new MyEvent());
    }
}

Example with No DI framework

// Application.java

public static void main(String[] args) {
  Emissary emissary = Emissary.builder()
        // Set instanceProvider to own implementation
        .instanceProvider(Application::getInstance)
        .requests(...)
        .events(...)
        .build();
}

private static Object getInstance(Class<?> handlerType) {
  if (MyRequestHandler.class.equals(handlerType)) {
    return new MyRequestHandler();
  } else if (MyEventHandler.class.equals(handlerType)) {
    return new MyEventHandler();
  }

  throw new IllegalStateException("Failed to get instance for " + handlerType.getName() + ".");
}

Concepts and Terminologies

Requests

Requests are messages that either:

  1. Initiate a state change/mutation
  2. Retrieve/query data
public class CreateFooCommand implements Request<Void> {
    private final String name;
    
    public CreateFooCommand(String name) {
        this.name = name;
    }
    
    public String name() {
        return name;
    }
}

public class GetFooByNameQuery implements Request<Foo> {
    private final String name;
    
    public GetFooByNameQuery(String name) {
        this.name = name;
    }
    
    public String name() {
        return name;
    }
}

Request Handlers

Requests are handled by request handlers. Request handlers can be registered through the use of the @RequestHandler annotation.

A request must only have a single request handler.

(@RequestHandlers fully support methods with void return types! No need to set method return type to Void and return null for no reason.)

public class CreateFooCommandHandler {
    @RequestHandler
    public void handle(CreateFooCommand command) {
        insertFooToDatabase(command.name());
    }
}

public class GetFooQueryHandler {
    @RequestHandler
    public Foo handle(GetFooByNameQuery query) {
        return getFooFromDatabase(query.name());
    }
}

Request Dispatcher

Requests are dispatched to a single request handler and this can be done through a dispatcher.

public static void main(String[] args) {
    // Use Spring's application context as InstanceProvider in this example
    // but any other DI framework can be used e.g. Guice, Dagger, etc.
    ApplicationContext applicationContext = springApplicationContext();

    // Emissary implements the Dispatcher interface.
    Dispatcher dispatcher = Emissary.builder()
        .instanceProvider(applicationContext::getBean)
        .requests(config -> config.handlers(CreateFooCommandHandler.class, GetFooQueryHandler.class))
        .build();

    // Send command!
    dispatcher.send(new CreateFooCommand("Emissary"));

    // Send query!
    Optional<Pong> pong = dispatcher.send(new GetFooByNameQuery("Emissary"));
}

Events

Events are messages that indicate that something has occurred in the system.

public class FooCreatedEvent implements Event {
    private final String name;

    public FooCreatedEvent(String name) {
        this.name = name;
    }

    public String name() {
        return name;
    }
}

Event Handlers

Events are handled by event handlers. Event handlers can be registered through the use of the @EventHandler annotation.

An event can have zero or more event handlers.

public class FooEventHandler {
    @EventHandler
    public void notifyA(FooCreatedEvent event) {
        helloFoo(event.name());
    }

    @EventHandler
    public void notifyB(FooCreatedEvent event) {
        kumustaFoo(event.name());
    }
}

Event Publisher

Events are dispatched to zero or more event handlers and this can be done through a publisher.

public static void main(String[] args) {
    // Use Spring's application context as InstanceProvider in this example
    // but any other DI framework can be used e.g. Guice, Dagger, etc.
    ApplicationContext applicationContext = springApplicationContext();

    // Emissary implements the Publisher interface.
    Publisher publisher = Emissary.builder()
        .instanceProvider(applicationContext::getBean)
        .events(config -> config.handlers(FooEventHandler.class))
        .build();

    // Publish event!
    publisher.publish(new FooCreatedEvent("Emissary"));
}

Advanced Features

Custom Request/Event Handler Annotations

In cases where a project is built in such a way that bringing in external dependencies is considered a bad practice (e.g. domain layer/package in a Hexagonal (Ports and Adapters) architecture), Emissary provides a way to use custom request/event handler annotations (in addition to the built-in RequestHandler and EventHandler annotations) to annotate request/event handlers.

This way, Emissary can still be used without adding the core Emissary library as a dependency of a project's domain layer/package. Instead, it may be used in the outer layers/packages to wire things up.

// Let's say below classes are declared in a project's core/domain package:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AwesomeRequestHandler {}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AwesomeEventHandler {}

public class MyRequestHandler {
  @AwesomeRequestHandler
  public void handle(TestRequest request) {
    // Handle.
  }
}

public class MyEventHandler {
  @AwesomeEventHandler
  public void handle(TestEvent event) {
    // Handle.
  }
}

// To wire things up:

public static void main(String[] args) {
  // Use Spring's application context as InstanceProvider in this example
  // but any other DI framework can be used e.g. Guice, Dagger, etc.
  ApplicationContext applicationContext = springApplicationContext();

  // Register handlers and custom annotations.
  Emissary emissary = Emissary.builder()
      .instanceProvider(applicationContext::getBean)
      .requests(config -> config.handlerAnnotations(AwesomeRequestHandler.class).handlers(MyRequestHandler.class))
      .events(config -> config.handlerAnnotations(AwesomeEventHandler.class).handlers(MyEventHandler.class))
      .build();
}

Custom Invocation Strategies

The library provides Emissary.RequestHandlerInvocationStrategy and Emissary.EventHandlerInvocationStrategy interfaces as extension points to let users customize how request/event handler methods are invoked by the Dispatcher and Publisher.

Built-in implementations are:

Users can create a new implementation and override the defaults by:

// Register custom invocation strategy.
Emissary emissary = Emissary.builder()
    .requests(config -> config.invocationStrategy(new CustomRetryOnErrorInvocationStrategy()))
    .events(config -> config.invocationStrategy(new CustomOrderGuaranteedInvocationStrategy()))
    .build();

SonarQube Cloud

About

Emissary is simple, yet ๐Ÿ—ฒFAST๐Ÿ—ฒ messaging library for decoupling messages (requests and events) and message handlers ๐Ÿš€

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors