I’ve been playing with the Guice dependency injection library recently for a new Java bytecode analysis toolkit(more to come in the future) that I’m working on and ran into some issues relating to scope and factories.

Problem Overview

At a high level, I am trying to create a single context per bytecode method analysed so that while the program is working with it, it can find various dependencies that are only valid for this context. Therefore these contexts are what I am scoping under. Guice allows certain factories to be autogenerated but doesn’t allow them to be scoped, requiring a workaround.

Scoping in Guice

Out of the box, Guice comes with two default scopes: Single and NO_SCOPE. These control the way objects are created; if a type is marked as a Singleton, then Guice will only create a single instance of that object whenever it is needed and use that everywhere it needs to be injected. Conversely, new instances of types that are unscoped are freshly created every time they are needed.

Guice also comes with some servlet extensions that can scope instances per HTTP request and per HTTP session, which is similar to what I require, minus the web fluff.

Generated Factories

Say we have an object called CodeBlock:

public class CodeBlock {
    private final int id;
    private final CodeObservationManager manager;
    
    public CodeBlock(CodeObservationManager manager, int id) {
        this.manager = manager;
        this.id = id;
    }
}

The id is passed in by user code, whereas the CodeObservationManager is managed by Guice. We can use assisted injection to create a factory that autowires the dependencies and the user input like so:

public class CodeBlock {
    private final int id;
    private final CodeObservationManager manager;
    
    @Inject
    public CodeBlock(CodeObservationManager manager, @Assisted int id) {
        this.manager = manager;
        this.id = id;
    }
}

public interface CodeBlockFactory {
    CodeBlock create(int id);
}

We mark our runtime pararmeter with @Assisted and the entire constructor with @Inject so that Guice can work out what type of pararmeter each one is. The factory method also reflects this, so that we only need the id to autowire and create a new CodeBlock.

And in our case, we want one CodeObservationManager per method context to be shared between all CodeBlocks in our application.

The scope management code is very similar to the one in the Guice wiki so I won’t include it here.

In our module we can bind the CodeObservationManager to it’s implementation and mark it’s scope

bind(CodeObservationManager.class).to(DefaultCodeObservationManager.class).in(MethodScoped.class);

and we can easily autogenerate the assisted factory:

install(new FactoryModuleBuilder().build(CodeBlockFactory.class));

Now here’s where I got confused: CodeBlockFactory is created as a singleton instance, but we want our CodeBlocks to be created using a CodeObservationManager object from the current scope. Naively adding @MethodScoped to CodeBlockFactory won’t work as Guice will complain that scope annotations are not allowed on autogenerated factories.

we could try to implement the factory code ourselves:

public static class CodeBlockFactory {
    private final CodeObservationManager codeObservationManager;
    
    @Inject
    public CodeBlockFactory(CodeObservationManager codeObservationManager) {
        this.codeObservationManager = codeObservationManager;
    }

    public CodeBlock create(int id) {
        return new CodeBlock(codeObservationManager, id);
    }
}

and then in the module:

@Inject
@Provides
@MethodScoped
CodeBlockFactory provideCodeBlockFactory(CodeObservationManager codeObservationManager) {
    return new CodeBlockFactory(codeObservationManager);
}

and the scoped CodeObservationManager will be used to create a scoped CodeBlockFactory. This works exactly as expected: one factory per scope, using the one observation manager from that scope, but is significantly more verbose and error prone than the code from before.

Fortunately, the factory builder in Guice is quite clever. Instead of getting a CodeObservationManager object injected into it when the singleton instance of the factory is created, it takes a Provider<CodeObservationManager>. This provider is scoped and generated because we have a binding for CodeObservationManager so that whenever the singleton factory creates a new factory, it calls .get() on the provider, giving the scoped instance of CodeObservationManager! So our blocks get created using the correct instance of the manager! Therefore we can actually use the simple assisted injection code to achieve the same effect that we want.

Conclusion

Guice is well thought out, probably more than I initially gave it credit for. This behaviour is probably documented somewhere and I should’ve realised it sooner, but in the end I gained a lot more understanding of how scoping and factories work in Guice by trying to force my own software ideals onto the framework.