This is a copy of a post, originally written on Medium. I've since rehosted it here.


If you haven’t read Part 1 or Part 2 you should definitely do that first.

As I mentioned at the end of Part 2, we’re pretty much done with building out Spring’s DI. We took care of @Qualifier annotations in the previous part, but so far it’s only been implemented on @Autowired fields. Let’s explore other places in which the @Qualifier annotation can be used.

Step 8: @Qualifier and @Bean methods

As we implemented last time, methods annotated with the @Bean annotation will be invoked as the dependency graph gets resolved. Any parameters to these methods will come from the application context as it’s invoked. In the previous part, we discussed the need for the @Qualifier annotation on @Autowired fields, since it’s unclear to the graph resolver which instance of the type to provide; the @Qualifier annotation allows us to specify by name which instance of the type should be provided. Let’s see what it takes to add this to @Bean methods (spoiler alert: it’s very simple).

First, let’s make a couple modifications to the @Qualifier annotation. The current code specifies a @Target(ElementType.FIELD) which limits the annotation’s usage to fields only.

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Qualifier {
    String value();
}
src/main/java/co/kenrg/hooke/annotations/Qualifier.java

Now, we’ve added the ElementType.PARAMETER to the list so it’s enabled on parameters to methods as well. I’ve also made another small modification; previously the value() method had a default value of "", but I’ve realized that that makes no sense. Specifying a @Qualifier annotation without specifying the name should be considered invalid, and by not having a default value here in the @interface it will throw a compile-time error if no name is passed into the annotation.

Now that our annotation is modified, let’s see what else needs to be changed. Right now in the getDependencyUnitsForBeanMethods within DependencyCollectors, we call into a helper method getDependenciesFromParameters to return a List<Pair<String, Class>> to us. Previously, we had hard-coded null as the left-hand side of the Pair because at that time we didn’t care about the exact name of the dependency. Let’s see what changes now that we have the ability to add @Qualifier annotations on parameters:

private static List<Pair<String, Class>> getDependenciesFromParameters(Parameter[] parameters) {
    List<Pair<String, Class>> dependencies = Lists.newArrayList();
    for (Parameter parameter : parameters) {
        Annotations annotations = getAnnotations(parameter::getAnnotations);
        Qualifier qAnn = annotations.get(Qualifier.class);
        String name = (qAnn != null && !qAnn.value().isEmpty())
            ? qAnn.value()
            : null;
        dependencies.add(Pair.of(name, parameter.getType()));
    }
    return dependencies;
}
getDependenciesFromParameters method in src/main/java/co/kenrg/hooke/application/graph/DependencyCollectors.java

This is similar to code we’ve seen in the getDependencyUnitForComponent method, where we have a left-hand side of null if we don’t care about the name of the dependency and we only care about the type; if we do care about the name, then the name will be the left-hand side. With this, let’s modify our Configuration1.java class within our example application and verify that our code works!

// Configuration1.java
@Configuration
public class Configuration1 {

    @Bean
    public String superSecretApiKey() {
        return "asoiudhfjkn";
    }

    @Bean
    public String exclamation() {
        return "!";
    }

    @Bean
    public String greeting(@Qualifier("exclamation") String exclamation) {
        return "Howdy" + exclamation;
    }

    @Bean
    public Component2 component2(Component3 component3) {
        return new Component2(component3);
    }
}

The differences here are the added exclamation() method and the dependency added to the greeting() method. Note that without that @Qualifier on the parameter, this would fail since there are many instances of String within the dependency graph. Making this change and then running the application will print out the message “Component1 says ‘Hello’; Component2 says ‘Howdy!’” which is similar to what we’ve been seeing, only now Component2 is a little more emphatic.

See, I told you that wouldn’t be too difficult. The code so far has been pushed to the Github repo under the tag step-8-qualifier-and-bean-methods.

Step 9: Constructor-based Injection

Right now, the only form of dependency injection that we’ve built out is field-based dependency injection; we declare a class annotated with @Component and add the @Autowired annotation on its fields to “ask” for instances of those fields’ types from the application context. Constructor-based injection doesn’t make use of the @Autowired annotation at all and in some ways feels more natural and more java-like, since it doesn’t require the injection container to reach into the class’s (potentially private!) fields in order to establish the dependency graph. I’m not going to go too much into the differences and benefits of either approach (you can look at what the official Spring documentation has to say about that, if you scroll down to the “Constructor-based or setter-based DI?” section), but suffice it to say that constructor-based injection is both very common and easy to implement.

In the getDependencyUnitForComponent method within DependencyCollectors we are currently just invoking the default constructor for the component class passed in, when instantiating the dependency within the DependencyUnit's lambda function:

Constructor constructor = componentClass.getConstructors()[0];
Object instance = constructor.newInstance();

(Side note, this code will actually fail at runtime if the component class in question had a constructor defined that wasn’t a no-args constructor.) Now that we’re supporting constructor-based injection though, the constructor to use needs to be determined earlier on, when we’re figuring out the list of dependencies this particular class has. Luckily we already have a helper method getDependenciesFromParameters, so the addition here is really pretty minimal. The only other thing that needs to change is the code within the DependencyUnit's lambda; now we just need to invoke the constructor that we’ve determined ahead of time, passing in instances from the application context, to obtain our instance. Here’s the entirety of the getDependencyUnitForComponent method now, though only a few lines have changed:

public static DependencyUnit getDependencyUnitForComponent(Class componentClass, DependencyInstanceGetter dependencyInstanceGetter) {
    List<Pair<String, Class>> dependencies = Lists.newArrayList();

    Constructor[] constructors = componentClass.getConstructors();
    if (constructors.length > 1) {
        throw new IllegalStateException("Cannot instantiate class with no default constructor: " + componentClass);
    }
    Constructor constructor = constructors[0];
    List<Pair<String, Class>> constructorDependencies = getDependenciesFromParameters(constructor.getParameters());

    Map<Field, Pair<String, Class>> fields = Maps.newHashMap();
    for (Field field : componentClass.getDeclaredFields()) {
        Annotations annotations = getAnnotations(field::getAnnotations);
        Autowired autowiredAnn = annotations.get(Autowired.class);
        Qualifier qualifierAnn = annotations.get(Qualifier.class);

        if (autowiredAnn != null) {
            String depName = qualifierAnn != null && !qualifierAnn.value().isEmpty()
                ? qualifierAnn.value()
                : null;
            fields.put(field, Pair.of(depName, field.getType()));
        }
    }

    dependencies.addAll(fields.values());
    dependencies.addAll(constructorDependencies);

    return new DependencyUnit(
        null,
        componentClass,
        dependencies,
        () -> {
            try {
                List<Object> constructorParameters = dependencyInstanceGetter.getDependencyInstances(constructorDependencies);
                Object instance = constructor.newInstance(constructorParameters.toArray());

                for (Map.Entry<Field, Pair<String, Class>> entry : fields.entrySet()) {
                    Field field = entry.getKey();
                    Pair<String, Class> dependency = entry.getValue();

                    Object value = dependencyInstanceGetter.getDependencyInstances(Lists.newArrayList(dependency)).get(0);
                    if (!field.isAccessible()) field.setAccessible(true);
                    field.set(instance, value);
                }

                return instance;
            } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
                throw new IllegalStateException("Could not invoke constructor for class" + componentClass, e);
            }
        }
    );
}
getDependencyUnitForComponent src/main/java/co/kenrg/hooke/application/graph/DependencyCollectors.java

One cool thing here is that we actually get support for @Qualifier annotations on constructor parameters for free, since we’re using the getDependenciesFromParameters method. To test this, let’s remove the @Bean method from Configuration1.java which had been creating an instance of Component2 and instead add the @Component annotation to Component2. When we run our example application now, we should see the same output as in Step 8, only this time the Component2 instance is being instantiated via constructor injection.

Like I said, this was pretty easy to implement! As always, the code so far has been pushed to the Github repo, and is available at the step-9-constructor-based-injection tag.

Step 10: @Autowired methods

We only have a couple of things left to do, and one of them is @Autowired methods. Using the @Autowired annotation on methods has a very similar effect to using the @Bean annotation on methods, the only difference being that the @Bean methods’ return value will be entered into the application context (and therefore cannot have a return type of void) and @Autowired methods’ return value will be ignored (if it’s non-void). One of the main purposes of @Autowired methods is to perform Setter-based injection (which is mentioned in the Spring documentation link I put above discussing constructor-based injection); the idea is that for each dependency your class has, you have a setter method annotated with @Autowired which Spring will invoke and pass in the requested dependency instance so you can assign it to a field within your class. This is discouraged in favor of constructor-based injection. Another use case is if your class needs an instance of some dependency from the application context for some other reason, like passing it into a constructor for one of your class’s fields; this is commonly seen when using JdbcTemplate:

class SomeDao {
    private JdbcTemplate jdbcTemplate;
    @Autowired
    public void setupJdbc(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
}

Let’s get into this and start adding some new code. I created a new class to house this bit of functionality (and the functionality of the next section, too) called DependencyPostSetupHandlers, and a method in there called invokeAutowiredMethodsForBeans. It should accept a list of instances from our application context, as well as a DependencyInstanceGetter (which we’d seen before when handling @Bean methods on classes). The main strategy here is very simple: find each method on each bean’s class that has the @Autowired method, find any dependencies it needs according to its parameters, and invoke it. Here’s what it looks like:

public static void invokeAutowiredMethodsForBeans(List<Object> beanInstances, DependencyInstanceGetter dependencyInstanceGetter) {
    for (Object instance : beanInstances) {
        for (Method method : instance.getClass().getMethods()) {
            boolean hasAutowired = getAnnotations(method::getAnnotations).contains(Autowired.class);

            if (hasAutowired) {
                final List<Pair<String, Class>> dependencies = getDependenciesFromParameters(method.getParameters());
                List<Object> args = dependencyInstanceGetter.getDependencyInstances(dependencies);
                try {
                    method.invoke(instance, args.toArray());
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw new IllegalStateException("Could not invoke @Autowired method " + method, e);
                }
            }
        }
    }
}
invokeAutowiredMethodsForBeans method in src/main/java/co/kenrg/hooke/application/DependencyPostSetupHandlers.java

Note that the getDependenciesFromParameters method is the same as in DependencyCollectors. I’ve pulled that out into a shared utility class called Parameters.java so it can be accessed from both places. Before we can test this out though, we need to make a couple of changes to Application.java. In order to invoke the @Autowired methods on all of our instances, we need to get all instances out of the application context. We currently have this method

public List<Object> getBeans(List<Pair<String, Class>> beans)

but it seems excessive to create a Pair to pass in when we know we won’t be caring about the name of the parameter (since all we care about is the type). So let’s make a new method in there really quickly:

public List<Object> getBeansOfTypes(Collection<Class> types) {
    List<Object> values = Lists.newArrayList();
    for (Class type : types) {
        Object instance = allBeans.get(type);
        if (instance == null) {
            throw new IllegalStateException("No bean available of type " + type);
        }
        values.add(instance);
    }

    return values;
}
getBeansOfTypes method in src/main/java/co/kenrg/hooke/application/ApplicationContext.java

So now, we can add these 2 lines to the end of the run method in Application:

List<Object> allInstances = 
    applicationContext.getBeansOfTypes(componentClasses);
invokeAutowiredMethodsForBeans(
  allInstances,
  applicationContext::getBeans
);

Before we can test this out, we need to modify the annotation to support being applied to a method since right now it’s only valid on fields. This is very similar to what we had done with the @Qualifier annotation back in Step 8, except now it’s adding ElementType.METHOD instead of ElementType.FIELD. Finally, let’s test this out by adding an @Autowired method to Component2:

@Autowired
public void setup(@Qualifier("superSecretApiKey") String apiKey) {
    System.out.printf("API key: %s\n", apiKey);
}

Now, when we run our application we should see the “API key: …” message print out right before our normal output gets printed. Awesome! We now have @Autowired support on methods! The code so far has been pushed to the Github repo, and is available at the step-10-autowired-methods tag.

There’s just one final thing to do now before we’re done…

Step 11: @PostConstruct methods

Methods annotated with @PostConstruct function in a very similar way to those annotated with @Autowired. So much so, in fact, that I debated whether to even include this section at all. Methods annotated with @PostConstruct will be invoked by Spring after the dependency graph has been resolved, and after all @Autowired methods have been invoked. Basically, it’s the last thing that will happen during the setup phase of the application. These methods cannot have parameters, and can have a return type of void (its return value will be thrown away so it doesn’t really matter). Let’s add this method to DependencyPostSetupHandlers:

public static void invokePostConstructMethodsForBeans(List<Object> beanInstances) {
    for (Object instance : beanInstances) {
        for (Method method : instance.getClass().getMethods()) {
            boolean hasPostConstruct = getAnnotations(method::getAnnotations).contains(PostConstruct.class);

            if (hasPostConstruct) {
                if (method.getParameterCount() != 0) {
                    throw new IllegalStateException("Lifecycle method annotation @PostConstruct requires a no-arg method");
                }
                try {
                    method.invoke(instance);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    throw new IllegalStateException("Could not invoke @Autowired method " + method, e);
                }
            }
        }
    }
}
invokePostConstructMethodForBeans method in src/main/java/co/kenrg/hooke/application/DependencyPostSetupHandlers.java

Then, let’s call it within the run method in Application, passing in the allInstances` Set(same as with theinvokeAutowiredMethodsForBeans` method).

Now let’s add the @PostConstruct annotation to the printMessage method in Component1. Note that the @PostConstruct annotation is something that’s included in the javax.annotation package, and isn’t something we have to define ourselves. If we were to run our application now, we’d see the ‘Hello’…’Howdy!’ message print out twice, once for the @PostConstruct invocation of the printMessage method, and once for the code we added to ExampleHookeApplication's main method. Now that we have the @PostConstruct functionality though, we can remove those 3 lines from the main method in our example application, so it should now just be

public class ExampleHookeApplication {
    public static void main(String[] args) {
        HookeApplication.start(ExampleHookeApplication.class);
    }
}

So, that’s pretty much it for this step. The code is available at the project’s Github repo, and at the step-11-postconstruct-methods tag.

Conclusion

There have been a lot of topics covered in this 3-part series. We’ve built out an implementation of Spring’s dependency injection container from scratch! We’ve talked a lot about how Spring works internally, about reflection, and about lots of other things too.

I have 2 real goals with these “Let’s Write X” posts/series. The first one is that I just really enjoy reverse-engineering something and figuring out how it works. Considering all of the cases and design decisions that the original engineers had made all the way back when a project was first started is really interesting to me. The second goal is to help demystify how commonly-used tools/libraries work. I’ve always had at least a working knowledge of Spring, but after researching and building it out myself I feel like I’ve learned a lot. My hope is that you have too, and the next time you see a cryptic and pages-long stack trace from Spring you’ll maybe have some clearer picture of the error’s cause. If these types of posts interest you, you should stay tuned. I have more in the works as I attempt to rebuild pretty much all of the tools and libraries that I use on a daily basis.

That’s not to say that they will be exactly as performant or feature-complete. There is so much that I’ve left out here in terms of functionality and performance (for example, I could probably be more optimized in the calls to the reflection API that I use), but that’s not the point. As it says in the README for this project, the goal was not to write a drop-in replacement for Spring, but merely to learn a bit about how it works. I hope you’ve learned something too!

Thank you for reading!