Sinobu

Concept

Dependency Injection (DI) is a mechanism that solves various problems related to component dependencies in 'a nice way'. Component dependency refers to the relationship from upper layer to lower layer, such as Controller → Service → Repository in a general layered architecture. 'A nice way' means that the framework will take care of the problem without the developer having to work hard manually.

In modern Java application development, DI is an almost indispensable mechanism. The detailed explanation of the DI concept is left to another website.

DI Container

Unlike other DI frameworks, there is no explicit DI container in Sinobu. It has only one container implicitly inside, but the user is not aware of it. Therefore, there is also no need to define dependencies explicitly by means of code or external files. All dependencies are automatically resolved based on type.

Injection Type

Commonly, there are four main types in which a client can receive injected services.

  • Constructor injection, where dependencies are provided through a client's class constructor.
  • Setter injection, where the client exposes a setter method which accepts the dependency.
  • Field injection, where the client exposes a field which accepts the dependency.
  • Interface injection, where the dependency's interface provides an injector method that will inject the dependency into any client passed to it.

Sinobu supports constructor injection exclusively. Other forms of injection are intentionally not supported, as they compromise object safety and are unlikely to be supported in the future.

Constructor Injection

The most common form of dependency injection is for a class to request its dependencies through its constructor. This ensures the client is always in a valid state, since it cannot be instantiated without its necessary dependencies.

public void objectInjection() {
    class Injectable {
    }

    class Injection {
        private Injectable value;

        Injection(Injectable injectable) {
            this.value = injectable;
        }
    }

    assert I.make(Injection.class).value != null;
}

Injectable Type

Any type can be injectable, but there are a few types that receive special treatment.

  • Primitives - A default value (0 for int, false for boolean) is assigned.
public void primitiveIntInjection() {
    class Injection {
        private int value;

        Injection(int injectable) {
            this.value = injectable;
        }
    }

    assert I.make(Injection.class).value == 0;
}
public void primitiveBooleanInjection() {
    class Injection {
        private boolean value;

        Injection(boolean injectable) {
            this.value = injectable;
        }
    }

    assert I.make(Injection.class).value == false;
}
  • Lifestyle - The resolution of dependencies can be delayed until the user actually needs it. Type variables must be correctly specified.
public void objectLazyInjection() {
    class Injection {
        private Lifestyle<Person> lazy;

        Injection(Lifestyle<Person> injectable) {
            this.lazy = injectable;
        }
    }

    assert I.make(Injection.class).lazy.get() != null;
}
  • Class - The currently processing model type. This feature is mainly available when implementing the special generic Lifestyle.
public void typeInjection() {
    class Injection {
        private Class type;

        Injection(Class injectable) {
            this.type = injectable;
        }
    }

    assert I.make(Injection.class).type == null;
}

Priority

If only one constructor is defined for the class being injected, it is used. If more than one constructor is defined, it must detect which constructor is to be used.

The constructor with the Managed annotation has the highest priority.

public void priorityManaged() {
    class Injection {

        private int value;

        Injection() {
            value = 10;
        }

        // This constructore is used.
        @Managed
        Injection(int injectable) {
            this.value = injectable;
        }
    }

    assert I.make(Injection.class).value == 0;
}

Next priority is given to constructors with the Inject annotation. The Inject annotation targets all annotations with the simple name 'Inject', so annotations such as jakarta.inject.Inject used in JSR330 etc. can also be used.

public void priorityInject() {
    class Injection {

        private int value;

        Injection() {
            value = 10;
        }

        // This constructore is used.
        @Inject
        Injection(int injectable) {
            this.value = injectable;
        }
    }

    assert I.make(Injection.class).value == 0;
}

If no annotation is found, the constructor with the lowest number of arguments is used.

public void priorityMinParam() {
    class Injection {

        private int value;

        // This constructore is used.
        Injection() {
            value = 10;
        }

        Injection(int injectable) {
            this.value = injectable;
        }
    }

    assert I.make(Injection.class).value == 10;
}

If several constructors with the smallest number of arguments are defined, the first constructor found among them is used. (which is implementation-dependent in the JDK)

public void priorityMinParams() {
    class Injection {

        private int value;

        // This constructore is used.
        Injection(boolean injectable) {
            value = injectable ? 1 : 10;
        }

        Injection(int injectable) {
            this.value = injectable;
        }
    }

    assert I.make(Injection.class).value == 10;
}

Circular Reference

One of the problems with constructor injection is that it cannot resolve circular dependencies. To partially solve this problem, Sinobu provides a delayed dependency injection method, but it does not completely solve any situation. If a solution is not possible, an error will occur.

public static class CircularLifestyleA {

    private Lifestyle<CircularLifestyleB> other;

    private CircularLifestyleA(Lifestyle<CircularLifestyleB> other) {
        this.other = other;
    }
}
public static class CircularLifestyleB {

    private Lifestyle<CircularLifestyleA> other;

    private CircularLifestyleB(Lifestyle<CircularLifestyleA> other) {
        this.other = other;
    }
}
public void circularDependenciesWithProvider() {
    CircularLifestyleA circularA = I.make(CircularLifestyleA.class);
    assert circularA.other != null;

    CircularLifestyleB circularB = I.make(CircularLifestyleB.class);
    assert circularB.other != null;
}
Lifestyle ManagementObject Modeling