4 minutes
Guice Scoped Factories
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 CodeBlock
s 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.