Dependency Injection Framework

Keywords: Spring Android Google Java

What is Dependency Injection?

Instead of initiatively initializing dependencies by ourselves, dependency injection is introduced through external dependencies, which we call dependency injection.
And what are the benefits?

  • Decoupling, decoupling dependencies.
  • Convenient unit testing.

What is a dependency injection framework?

It's a framework to do dependency injection, so that we can put our focus on the core logic.
What a dependency injection framework needs to address is:

  1. Generate injected objects.
  2. The injected object is bound to a reference in the injected class.
  3. Extra injected object lifecycle management.

A framework is ok ay when it completes the first two parts, and the latter part is more like an additional feature based on this extension.
Typical examples, such as ButterKnife and Android Annotations, require only one annotation for the initialization of the Android control View control, thus eliminating the tedious work of findViewbyId and automatically completing the binding of objects and references. View's life cycle is then hosted by Activity or Fragment.
However, if we consider it from another aspect, the generation of objects has already been generated in inflate view, which is not so-called "dependency injection" because it is inconsistent with our understanding of the import from outside.
Here we find that View can be injected directly, haha, escape)

Implementation of different dependency injection frameworks

  • Spring IoC
    IoC well embodies one of the principles of object-oriented design-Hollywood rule: "Don't look for us, we look for you"; that is, the IoC container helps the object to find the corresponding dependent object and inject it, rather than the object to find it actively.

    Spring container

    That's how Spring advocates development. All classes are registered in the Spring container, telling Spring what you are and what you need, and then Spring will give you what you want when the system runs properly, and give you what you need at the same time. The creation and destruction of all classes are controlled by spring, which means that the life cycle of the control object is no longer the object that references it, but spring. For a specific object, it used to control other objects, but now all objects are controlled by spring, so this is called control inversion.
    Finally, using xml configuration, using java class reflection mechanism to generate instances, direct use.
  • Roboguice
    This is a dependency injection framework provided by google. Based on annotations, reflections, efficiency is definitely not the best in Android devices. Now google no longer maintains, but recommends other frameworks such as dagger and dagger2. If you are interested in this, you can refer to it. Performance comparison of dependency injection frameworks.
    If you are free, I will analyze the source code implementation in detail.
    // TODO partition line
  • dagger
    square must be a fine product. The injection framework is based on JSR-330, which is also referred to in dagger2.
    Here is an example of demo:
public class MainActivity extends AppCompatActivity {

    @Inject Test1 mTest1;
    @Inject TestManager15 mTestManager15;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ObjectGraph objectGraph = ObjectGraph.create(new Dagger1Module(), new ModelModule());
        objectGraph.inject(this);
        objectGraph.inject(mTest1);
        Log.d(MainActivity.class.getSimpleName(), "onCreate(): " + mTest1.toString());
        mTestManager15.start();
    }

}

dagger uses apt to generate part of the code. After compiling, we can see that the following directory has code, apt generated code.


apt generated code


Let's look at the internal implementation of this class, as follows:


Specific realization


You can see that this class is not referenced in the actual code, but this class completes the instantiation of the injected class in the attch method and the binding in injectMembers.
Obviously, this class is instantiated by reflection.

So we know how to implement dagger:

* Generate a single container class (injectadapter) by apt, which accomplishes the following two logical tasks:
1. Generate the logic of the injected object.
2. The logic that binds the injected object to the reference in the injected class.

* The injectadapter is instantiated by reflection to inject objects.

Specific implementation can be referred to Linker and the following source code:

/**
 * Handles loading/finding of modules, injection bindings, and static injections by use of a
 * strategy of "load the appropriate generated code" or, if no such code is found, create a
 * reflective equivalent.
 */
public final class FailoverLoader extends Loader {
  /*
   * Note that String.concat is used throughout this code because it is the most efficient way to
   * concatenate _two_ strings.  javac uses StringBuilder for the + operator and it has proven to
   * be wasteful in terms of both CPU and memory allocated.
   */

  private final Memoizer<Class<?>, ModuleAdapter<?>> loadedAdapters =
      new Memoizer<Class<?>, ModuleAdapter<?>>() {
        @Override protected ModuleAdapter<?> create(Class<?> type) {
          ModuleAdapter<?> result =
              instantiate(type.getName().concat(MODULE_ADAPTER_SUFFIX), type.getClassLoader());
          if (result == null) {
            throw new IllegalStateException("Module adapter for " + type + " could not be loaded. "
                + "Please ensure that code generation was run for this module.");
          }
          return result;
        }
      };

  /**
   * Obtains a module adapter for {@code module} from the first responding resolver.
   */
  @SuppressWarnings("unchecked") // cache ensures types match
  @Override public <T> ModuleAdapter<T> getModuleAdapter(Class<T> type) {
    return (ModuleAdapter<T>) loadedAdapters.get(type);
  }

  @Override public Binding<?> getAtInjectBinding(
      String key, String className, ClassLoader classLoader, boolean mustHaveInjections) {
    Binding<?> result = instantiate(className.concat(INJECT_ADAPTER_SUFFIX), classLoader);
    if (result != null) {
      return result; // Found loadable adapter, returning it.
    }
    Class<?> type = loadClass(classLoader, className);
    if (type.equals(Void.class)) {
      throw new IllegalStateException(
          String.format("Could not load class %s needed for binding %s", className, key));
    }
    if (type.isInterface()) {
      return null; // Short-circuit since we can't build reflective bindings for interfaces.
    }
    return ReflectiveAtInjectBinding.create(type, mustHaveInjections);
  }

  @Override public StaticInjection getStaticInjection(Class<?> injectedClass) {
    StaticInjection result = instantiate(
          injectedClass.getName().concat(STATIC_INJECTION_SUFFIX), injectedClass.getClassLoader());
    if (result != null) {
      return result;
    }
    return ReflectiveStaticInjection.create(injectedClass);
  }
}
  • dagger2
    dagger2 is further optimized to facilitate the features of the interface and the powerful functions of apt, completely free of reflection, all classes, binding implementation are relying on automatically generated code.
    When the external entry class is generated at compile time, the logical interaction depends on the interface.
    Several concepts need to be understood when using dagger2:
@ Inject: This annotation is usually used where you need to rely. In other words, you use it to tell Dagger that the class or field needs dependency injection. In this way, Dagger constructs an instance of this class and satisfies their dependencies.

@ Module: The methods in the Modules class specifically provide dependencies, so we define a class annotated with @Module so that Dagger knows where to find the required dependencies when constructing an instance of the class. An important feature of modules is that they are designed to be partitioned and grouped together (for example, in our app, there are multiple modules that can be grouped together).

@ Provide: In modules, the method we define is to use this annotation to tell Dagger that we want to construct objects and provide these dependencies.

@ Component: Components are essentially an injector, or a bridge between @Inject and @Module. Their main function is to connect the two parts. Components can provide instances of all defined types, such as: We have to annotate an interface with @Component and list all @Modules to make up the component. If we miss any one, we will report an error at compile time. All components can know the range of dependencies through its modules.

@ Scope: Scopes are very useful. Dagger2 can limit the scope of annotations by customizing annotations. An example will be shown later, which is a very powerful feature because, as mentioned earlier, it is not necessary for each object to know how to manage their instance. In the scope example, we annotate a class with a custom @PerActivity, so the object lives the same as activity. Simply put, we can define granularity of all ranges (@PerFragment, @PerUser, etc.).

Qualifier: We can use this annotation when the class type is insufficient to identify a dependency. For example, in Android, we need different types of context, so we can define qualifier annotations @ForApplication and @ForActivity, so when we inject a context, we can tell Dagger what type of context we want.

Then, let's analyze a simple example of dagger2:
Defined Subcomponent:

/**
 * Scope: activity, it will be instantiated when the MainActivity starts.
 */
@ActivityScope
@Subcomponent(modules = {MainModule.class})
public interface MainComponent {

    void inject(MainActivity activity);

}

Then we write the following logic in MainActivity:

public class MainActivity extends AppCompatActivity {

    @Inject
    @Named("AppName")
    String mAppName;
    @Inject
    @Named("UserName")
    String mUserName;
    @Inject
    @Named("title")
    String mTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        MainComponent mainComponent = App.userComponent().plusMain();
        mainComponent.inject(this);

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ActionBar actionBar = getSupportActionBar();
        actionBar.setTitle(mTitle);

        TextView textView = (TextView) findViewById(R.id.text_view);
        textView.setText(mAppName);
        TextView userView = (TextView) findViewById(R.id.user_view);
        userView.setText(getString(R.string.current_user, mUserName));
    }

}

We injected three String strings and passed a reference to MainActivity to the MainComponent interface. Obviously, we know that after injection, mainComponent gets MainActivity, and the next step is to assign a value to the member variable of @Inject.
Let's look at the specific apt generated classes:

@Generated(
  value = "dagger.internal.codegen.ComponentProcessor",
  comments = "https://google.github.io/dagger"
)
public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {
  private final Provider<String> mAppNameProvider;

  private final Provider<String> mUserNameProvider;

  private final Provider<String> mTitleProvider;

  public MainActivity_MembersInjector(
      Provider<String> mAppNameProvider,
      Provider<String> mUserNameProvider,
      Provider<String> mTitleProvider) {
    assert mAppNameProvider != null;
    this.mAppNameProvider = mAppNameProvider;
    assert mUserNameProvider != null;
    this.mUserNameProvider = mUserNameProvider;
    assert mTitleProvider != null;
    this.mTitleProvider = mTitleProvider;
  }

  public static MembersInjector<MainActivity> create(
      Provider<String> mAppNameProvider,
      Provider<String> mUserNameProvider,
      Provider<String> mTitleProvider) {
    return new MainActivity_MembersInjector(mAppNameProvider, mUserNameProvider, mTitleProvider);
  }

  @Override
  public void injectMembers(MainActivity instance) {
    if (instance == null) {
      throw new NullPointerException("Cannot inject members into a null reference");
    }
    instance.mAppName = mAppNameProvider.get();
    instance.mUserName = mUserNameProvider.get();
    instance.mTitle = mTitleProvider.get();
  }

  public static void injectMAppName(MainActivity instance, Provider<String> mAppNameProvider) {
    instance.mAppName = mAppNameProvider.get();
  }

  public static void injectMUserName(MainActivity instance, Provider<String> mUserNameProvider) {
    instance.mUserName = mUserNameProvider.get();
  }

  public static void injectMTitle(MainActivity instance, Provider<String> mTitleProvider) {
    instance.mTitle = mTitleProvider.get();
  }
}

dagger2 avoids the problem that dagger generates implementation classes from apt by reflection through the definition of interface, and further improves the efficiency of injection, which is really worth learning.

Extended thinking

But there are also some problems with dagger 2:

  1. We are using a framework or a third-party class library, which is easy to learn and use first, but some of the concepts of dagger 2 really make novices "daunted" and it does require a certain cost to learn, and can not be used.
  2. The management of object life cycle, how to ensure that memory leaks do not occur, largely depends on users.

Based on the above two points, we can do some optimization expansion:
The first is the definition of interface. We hope to provide users with some simple, usable and flexible api s. In code generation, we can not only use apt, but also use javasist and other tools to change classes during compilation.
Second, we can build an "object container" that manages the life cycle of an object, just like Spring.

TODO

The above analysis is only a superficial summary, some details of the implementation such as how to inject and Scope are not analyzed, you can refer to the link at the end of the article, in addition, I will continue to improve the document.

Reference resources

Posted by webdes03 on Thu, 13 Jun 2019 12:36:03 -0700