Skip to content
This repository was archived by the owner on Apr 16, 2025. It is now read-only.

Dependency Injection

Danila Rassokhin edited this page Jan 28, 2023 · 2 revisions

DI container

Methods:

  • getBean(String, Class)
    • Gets bean by it's name and class or throws BeanNotFoundException if bean doesn't exists
  • getBean(Class)
    • Gets bean of given class.
    • Throws BeanNotFoundException if no beans with given class are exist
    • Throws BeanConflictException if there more than one bean of given class exists
  • searchBean(String, Class)
    • Searches for bean by it's name and class and returns Optional
  • searchBean(Class)
    • Searches for bean of given class and returns Optional
  • loadConfiguration(Class)
    • Loads configuration class
    • Configuration class must be annotated as @Configuration
    • Configuration class may contain annotations @ComponentScan(String[]) to provide packages with GameBean classes, @Components(Class[]) to provide specific GameBean classes and methods annotated as GameBean
  • loadBean(Class)
    • Loads class annotated as @GameBean into DI container

Example

Start using DI container with creating configuration class:

//Configuration class must be annotated as @Configuration
@Configuration
public class BasicConfiguration {

}

To load this configuration into DI container use loadConfiguration

BasicComponentManager.getDiContainer().loadConfiguration(BasicConfiguration.class);

Sometimes DI container can't scan packages for classes you point to with @ComponentScan. Then you can use @Components:

@Configuration
@Components({MyBeanA.class, MyBeanB.class})
public class BasicConfiguration {

}

Game beans

You can create beans from classes and methods with @GameBean. This annotation has some properties you need to know about:

  • name - specifies bean name. If not specified, then bean will get method or class name in lower case

  • policy - bean creation policy. Default singleton - create once and always reuse it, object - create bean only on getBean call

  • order - specifies order in which annotated methods in configuration will be executed

  • variant - specifies variant this bean will be used in. Only beans with variant, which equals to variant specified in BasicDIContainer will be loaded. You can create beans for different use cases, build platforms, etc.

Bean variants

You can specify variant in BasicDIContainer constructor. @GameBean has two constants:

  • DEFAULT_VARIANT - default variant used in empty BasicDIContainer constructor
  • GLOBAL_VARIANT - default variant used in @GameBean. Beans with GLOBAL_VARIANT will be loaded anyway
//Create DI container with default variant
BasicDIContainer defaultContainer = new BasicDIContainer();
//Create DI container with Android variant
BasicDIContainer diContainer = new BasicDIContainer("Android");

To specify variant in game bean use variant parameter

public interface ItemVariant {
  String getVariant();
}

//Will be load if DI container variant is set to Android
@GameBean(variant = "Android")
public class AndroidVariant implements ItemVariant {
  @Override
  public String getVariant() {
    return "Android";
  }
}

//Will be loaded anyway, cause default variant is GameBean.GLOBAL_VARIANT
@GameBean
public class WindowsVariant implements ItemVariant {
  @Override
  public String getVariant() {
    return "Windows";
  }
}

Bean creation strategy

Bean creating strategy from methods

Sort methods in class by parameters count, then sorts method by order() in `@GameBean` then tries to search existed beans to inject in parameters. Configuration classes are bean providers. DIContainer loads them unordered, so methods in configuration class shouldn't rely on methods from other configuration classes or game bean classes.

Bean creating strategy from classes

Tries to create new instance of class. If class has a constructor annotated with `@Autofill` has parameters then tries to search for existing beans

Example

//Configuration class must be annotated as @Configuration
@Configuration
//Will scan ru.danilarassokhin.example.component package for @GameBean classes and @Configuration classes
@ComponentScan("ru.danilarassokhin.example.component")
@Components({GameItem.class})
public class BasicConfiguration {

  private long globalIdGenerator = -1;

  //Will create bean of type BasicObjectCaster and name "objCaster"
  @GameBean(name = "objCaster")
  public BasicObjectCaster objCaster() {
    return new BasicObjectCaster();
  }

  //Will add bean information of type Long and name "globalIdGenerator".
  //Will create new object every time when you call getBean
  //Will give new Long value every time you call getBean("globalIdGenerator", Long.class)
  @GameBean(name = "globalIdGenerator", policy = GameBeanCreationPolicy.OBJECT, order = 1)
  public Long generateId() {
    return ++globalIdGenerator;
  }

  //Will create bean with type GameItem and name "createitem"
  //Before method call will inject random bean of type Long as parameter if such bean exists
  @GameBean(policy = GameBeanCreationPolicy.OBJECT, order = 2)
  public GameItem createItem(@Qualifier("globalIdGenerator") Long id) {
    GameItem item = new GameItem();
    item.setId(id);
    return item;
  }

  //Use @Qualifier to specify bean name to inject
  @GameBean(name = "generate", order = 3)
  public Long generate(@Qualifier("globalIdGenerator") Long id) {
    return ++globalIdGenerator;
  }

  //Will create bean with type GameItem and name "gameItem"
  //Will inject bean with name "globalIdGenerator" as Long arg
  @GameBean(name = "gameItem", policy = GameBeanCreationPolicy.OBJECT, order = 4)
  public GameItem createItemWithIdGenerator(@Qualifier("generate") Long id) {
    return createItem(id);
  }

}

To create bean from class:

//Will create bean with name objCaster
@GameBean(name = "objCaster")
public final class BasicObjectCaster implements GameObjectCaster {
}

After beans loading you can get them:

//Get bean by it's name and class
BasicObjectCaster basicObjectCaster = diContainer.getBean("objCaster", BasicObjectCaster.class);
//There is only one bean of type BasicObjectCaster, so bean name is not required
basicObjectCaster = diContainer.getBean(BasicObjectCaster.class);
//Get bean as interface
GameComponent simpleGameItem = diContainer.getBean("simpleGameItem", GameComponent.class);

If bean doesn't exists then getBean will throw BeanNotFoundException.

Auto injection of beans

Beans from DI container can be injected in class fields, constructor and method parameters. There two ways to inject beans:

  • implicit, while other bean creation
  • explicit, through BasicComponentCreator

Both ways have a common part. Element must be annotated with @Autofill

public interface MyService {}

@GameBean(name = "MyServiceA")
public class MyServiceA implements MyService {}

@GameBean(name = "MyServiceB")
public class MyServiceB implements MyService {}

public class MyClass {
    //Will inject bean of type MyService with name myservice
    @Autofill
    private MyService myService;
    
    //Will inject beans of type MyService, MyServiceB into constructor parameters
    @Autofill
    public MyClass(MyService myServiceA, MyServiceB myServiceB) {}
}

In example above beans will be injected into class field and constructor parameters. But in example above BeanConflictException will be thrown. That is because field and constructor of class MyClass requires parameter of type MyService, but there are more than one bean of type MyService exist: MyServiceA and MyServiceB. In such a situation you need to specify name of bean you want to inject with @Qualifier annotation. So, MyClass above should be changed to:

public class MyClass {
    //Will inject bean of type MyService with name MyServiceA
    @Autofill
    @Qualifier("MyServiceA")
    private MyService myService;
    
    //Will inject beans of type MyService with name MyServiceA, MyServiceB into constructor parameters
    @Autofill
    public MyClass(@Qualifier("MyServiceA") MyService myServiceA, MyServiceB myServiceB) {}
}

Now you have two ways to create object and inject beans into it

GameBean way

Make MyClass game bean and put it into DI container:

@GameBean
public class MyClass {
    //Will inject bean of type MyService with name MyServiceA
    @Autofill
    @Qualifier("MyServiceA")
    private MyService myService;
    
    //Will inject beans of type MyService with name MyServiceA, MyServiceB into constructor parameters
    @Autofill
    public MyClass(@Qualifier("MyServiceA") MyService myServiceA, MyServiceB myServiceB) {}
}

//Load bean into container
diContainer.loadBean(MyClass.class);

BasicComponentCreator way

Use BasicComponentCreator to create object of any class:

MyClass myClassObj = BasicComponentCreator.create(MyClass.class);

Post creation methods

If you want to invoke some method after constructor, create methods with no parameters and annotate it with @Autofill

public class MyClass {
    @Autofill
    public void afterCreation() {
      System.out.println("I've been created!")
    }
}

BeanScanner

DIContainer uses different implementations of BeanScanner to determine if class should be recognized as a bean.

Add new BeanScanner

Create new implementation of BeanScanner and define predicate to determine if class should be recognized as a bean. Then add it to container with diContainer.addBeanScanner(beanScanner)

Example

This is example of RepositoryScanner class:

public class RepositoryScanner implements BeanScanner {

  @Override
  public boolean shouldBeLoaded(Class<?> c) {
    return c.isAnnotationPresent(Repository.class);
  }
}
basicDIContainer.addBeanScanner(new RepositoryScanner());

BeanFactory

DI container uses different implementations of BeanFactory to:

  1. Determine if BeanDefinition should be created
  2. Determine if bean instance should be created
  3. Create BeanDefinition from given class

Add new BeanFactory

You can create your own bean factory to process custom annotations and classes.

Example

This is example from RepositoryBeanFactory:

public class RepositoryBeanFactory implements BeanFactory {

  @Override
  public BeanDefinition createBeanMetaInformationFromClass(Class<?> beanClass) {
    Repository annotation = ComponentAnnotationProcessor.findAnnotation(
        beanClass, Repository.class);
    if (annotation == null) {
      throw new RuntimeException("No @Repository annotation specified!");
    }
    String beanName = annotation.value();
    if (beanName.isEmpty()) {
      beanName = beanClass.getSimpleName().toLowerCase();
    }
    BeanDefinition beanDefinitionData = new BeanDefinition();
    beanDefinitionData.setName(beanName);
    beanDefinitionData.setVariant(GameBean.GLOBAL_VARIANT);
    beanDefinitionData.setCreationPolicy(GameBeanCreationPolicy.SINGLETON);
    beanDefinitionData.setRealType(beanClass);
    beanDefinitionData.setBean(BasicStorage.createRepository(cast(beanClass)));
    beanDefinitionData.setCreated(true);
    beanDefinitionData.setReady(true);
    return beanDefinitionData;
  }

  // We want to create BeanDefinition from beanClass if it is a repository
  @Override
  public boolean isShouldBeProcessed(Class<?> beanClass) {
    return isStorageRepository(beanClass);
  }

  // We don't need to create bean instance, cause our repository is an interface
  // and we already created it's BeanDefinition in createBeanMetaInformationFromClass()
  @Override
  public boolean isShouldBeCreated(Class<?> beanClass) {
    return false;
  }

  private Class<? extends StorageRepository> cast(Class<?> beanClass) {
    return (Class<? extends StorageRepository>) beanClass;
  }

  private boolean isStorageRepository(Class<?> beanClass) {
    try {
      cast(beanClass);
      return true;
    } catch (ClassCastException e) {
      return false;
    }
  }
}
diContainer.addBeanFactory(new RepositoryBeanFactory());

Clone this wiki locally