Skip to content

DependencyInjection

Frieder edited this page Feb 6, 2024 · 1 revision

Dependency Injection

Motivation

We use our own implementation of dependency injection called SoyDI. The reason is that neither Weld nor Dagger does work in our use case well with PF4J.

DI Basics

To make a class injectable it must be annotated with a bean definition. Currently we have DefaultBean and SingletonBean. The DefaultBean will be a new instance for each inject. The SingletonBean will always be the same instance. Further exactly one constructor must be annotated with @Inject (Note: This is not the jakarta @Inject). The constructor must have no parameters or only parameters that are injectable on its own. Otherwise the class can't be injected (see also assisted inject).

So a simple example looks like this:

@DefaultBean
class Foo {

  @Inject
  Foo(){

  }
}

@DefaultBean
class MyClass {

  private final Foo foo;

  @Inject
  MyClass(Foo foo){
    this.foo = foo;
  }

}

Constructors should be package-private, cause factories are in the same namespace.

Factories

Factories are autogenerated during compile time. You may know this approach from Lombok. So for each bean there will be a factory called BeanNameFactory. Usually those factories are called automatically if you inject a bean. But of course those factories can also be injected. If you do so it allows you to do lazy and multi injection. Means, that an instance is generated when you call create() on the factory. You can do this multiple times or at the point in time you need it.

// Foo stays the same
// FooFactory is generated

@DefaultBean
class MyClass {

  private final FooFactory foo;

  @Inject
  MyClass(FooFactory foo){
    this.fooFactory = fooFactory;
  }

  ...
    Foo myFoo = this.fooFactory.create();
  ...

}

Assisted Inject

Sometimes ... and especially in UI applications ... there is a need to provide not injectable parameters. E.g. a connectionId or a specific DTO. Those parameters must be marked with @Assisted. The usage of assisted inject brings some limitations. The most important one is, that your bean is not auto injectable anymore, you must use the factory. Further the create method of the factory requires the assisted parameters.

// Foo stays the same
// factories are generated

@DefaultBean
class MyClass {

  private final FooFactory foo;

  private final String connectionId;

  @Inject
  MyClass(FooFactory foo,
          @Assisted String connectionId){
    this.fooFactory = fooFactory;
    this.connectionId = connectionId;
  }
}

@DefaultBean
class MyClass2 {

  private MyClassFactory myClassFactory;

  @Inject
  MyClass2(MyClassFactory myClassFactory){
    this.myClassFactory = myClassFactory;
  }

  ...
    String connectionId = ...
    MyClass myClass = this.myClassFactory.create(connectonId);
  ...
}

Events

Listen to events

Soy beans can listen to events. If you want to listen to an event you have to use @Observes annotation. Either you provide the annotion at method level if you do not need the event object or you use it on parameter level if you need the event object.

@DefaultBean
public class MyObserver {

  @Observes(MyEvent.class)
  public void onMyEvent(){

  }

  public void onMyEvent(@Observes MyEvent event){

  }
}

It is possible to listen to multiple event classes by providing a list on method level. Listening to multiple event classes it not possible if you want to access the event object.

@DefaultBean
public class MyObserver {

  @Observes({MyEvent.class,
             MyEvent2.class,
             MyEvent3.class
            })
  public void onMyEvent(){

  }
}

Throw Events

In order to throw an event you have to create an event class. This is a class that implements Event. This is an interface that does not require any implementation.

Than you have to inject SoyEvents into your bean, that can be used to fire or fireAsync events.

public class MyEvent implements Event {

}

@DefaultBean
public class MySender {

  private final SoyEvents soyEvents;

  @Inject
  MySender(SoyEvents soyEvents){
    this.soyEvents = soyEvents;
  }

  ...
    MyEvent myEvent = new MyEvent();
    soyEvents.fire(myEvent);
  ...
}

Events may contain attributes of course. If fireAsync is used, the thread won't wait till all observers are called. The fire methods will return a number, that is the count of called observers.

Usually events are sent to existing beans. Means DefaultBeans that still have a reference or SingletonBeans that are already initialized. But it is also possible to automatically produce a bean on events. Therefore you can use @Observers(autocreate=true). This will create the bean automatically if there are no active observers listen to a specific event. Of course this won't work for beans using assited inject.

Event Filtering

Sometimes events must be filteres. E.g. you want to receive events only for a specific connectionId. To do this you must use @ObservesFilter annotation on a method on the observer and the event. The annotation must be filled with a filter id, which is just a string. Events will only be delivered if the bean and the event have the same value for a given observes filter id.

public class MyEvent implements Event {

  @ObservesFilter("connectionId")
  public String getConnectionId(){
    return this.connectionId;
  }
}

@DefaultBean
public class MySender {

  @ObservesFilter("connectionId")
  public String getConnectionId(){
    return this.connectionId;
  }

  @Observes(MyEvent.class)
  public void onMyEvent(){

  }
}

Initialization

In order to activate dependency injection you have to scan at least one package.

SoyDi.scan("org.correomqtt", true);

The boolean parameter tells Soy to not scan external jars, which will speed up the scan process. You have to set this to false if you want to scan plugins for example. Also it might be required to tell Soy to use additional class loaders. This can be done with SoyDi.addClassLoader(classloader);.

In order to inject something you have to start with at least one bean. Think of it as the root node of your dependency tree. Usually you should have one root only. But in general it is possible to have multiple roots. SingletonBeans will be shared among roots, there is no separation. Same for events.

SoyDi.inject(MainApplication.class)

Assisted beans can not be used as root nodes. You have to use an autocreatable bean that injects a factory for this.

Clone this wiki locally