Guice Scopes: Everything They Don’t Tell You

Posted under Software development On By Austin Sharp

Seeq uses Guice for dependency injection. When the codebase was small, a DI framework seemed like overkill – we liked to inject dependencies to make unit testing easier, but it was easy to do so manually and we understood everything that was going on. As our codebase has grown we’ve seen some antipatterns that a DI framework helps reduce:

  • Constructor arguments that are only used as arguments to other constructors
  • Objects violating the law of Demeter by reaching several objects away because they need some system singleton but don’t want to add an argument to their constructors and update unit tests, etc
  • Objects using ThreadLocal or similar mechanisms to make a non-singleton dependency shared within the lifetime of a job or API request

The last example led to some particularly messy issues. One example was that of already-closed database connections leaking between requests or jobs run in the same threadpool. This seemed like the kind of problem that we weren’t the first to encounter, and we did find some prior art in the idea of Request Scopes.

What’s a Scope?

In dependency injection frameworks, a scope determines when an injected object will be reused. The most common scope is a Singleton – the injected object is reused for the lifetime of the application, everywhere it is injected. The Jersey framework uses HK2 for injection, which comes with a built-in RequestScope that is actually the default – any object injected into a request will be reused for any other injection points of the same type in that same request, unless otherwise specified. Guice, on the other hand, defaults to no scope at all, so any unspecified objects (i.e. not annotated with @Singleton or another scope) will be created anew for each injection point.

One thing that threw me off initially is that Guice’s docs on Scopes weren’t exactly what I was looking for. Almost all the examples use the Singleton scope, and the built-in RequestScoped is always mentioned in tandem with Guice Servlets, which we don’t use. I just wanted RequestScoped without Servlets, and couldn’t tell if that was a legitimate use. I spent a while on StackOverflow and happened to also find recommendations from Guice contributors to not use scopes, and started to worry that I’d be sued by a crack legal team for Guice malpractice and have my keyboard taken away!

RequestScoped: Just Do It

Well, since you’re reading this, you can probably surmise that I still have my keyboard, and our application code is intact too. (Note to any Nike lawyers reading this: the section header is a joke!) It turns out that you can just read the source code, and it’s not all that scary. While it’s convenient to pull in the guice-servlet project to get the RequestScoped class and save some reimplementation, nothing else servlet related is necessary to make use of the scope.

Instead, the solution comes in the form of “manual scopes”, which just means explicitly telling Guice when a scope starts and ends. If you’re using Guice servlets, you can have Guice do this for you, but otherwise you simply place a bit of code in the places you want a scope to start or end, such as a server’s pre-request and post-request filters, a migration, or any other application entrypoint:

ServletScopes.scopeRequest(Map.of()).open()

Scopes are also auto-closeable, so Java’s try-with-resources or Kotlin’s .use can close them:

try (var ignored = ServletScopes.scopeRequest(Map.of()).open()) { 
    // Do some injection within the scope, and scoped objects will be reused. 
}

Or wrap a runnable:

Callable<Object> scopedCallable = ServletScopes.scopeRequest(
    myCallable, 
    Map.of(
       Key.get(RequestScopedFoo.class), myFoo, 
       Key.get(RequestScopedBar.class), myBar));

Note that both methods accept a map of objects to seed into the scope – we used this for things like request metadata that should be shared for the request’s lifetime. It’s convenient when those objects are already available when opening the scope, and might be an opportunity to move classes being shared in some other fashion to the simplicity of injection.

This got us most of what we needed. We did have to figure out a few more tricks along the way though.

The Gory Details

One particular challenge was a custom factory class that could provide either read or read-write access to a database (via a JDBC DataSource). We wanted the first usage of this factory in an API request to determine whether the request as a whole was restricted to read-only activity. We set up a couple different Guice annotations for injection points to declare whether they:

  • definitively wanted read-only (and to force the rest of the request to do the same)
  • could work with anything (usually shared utility code that could be read-only, but shouldn’t make the ‘decision’ for the request as a whole)
  • definitively wanted read-write

Whatever was requested first should then be used for the duration of the request, and if a read-only object was used for an attempted write, that meant we had found a bug and would fix it. This whole thing is a bit messy and it would be great to replace it with some compile-time checks, but that’s a whole separate effort and the goal here was just to get Guice working and hopefully replace our custom provider class and its state.

The issue was that simply binding the implementing class as RequestScoped didn’t work – the scoping is on the binding as a whole, including the target interface, and in our case it was the same implementation being used at both the read-only and read-write injection points, but not the same interface. In the end the solution was to create a few Providers – one for read-only, one for read-write, and (crucially) a shared, RequestScoped Provider that was injected into both of the other two Providers.

class DBAccessGuiceModule : AbstractModule() {
    @Override
    override fun configure() {
        bindScope(RequestScoped::class.java, ServletScopes.REQUEST)
        bind(DBAccessRequestScoper::class.java).`in`(RequestScoped::class.java)

        bind(DBAccess::class.java).toProvider(ReadDBAccessProvider::class.java)
        bind(WriteDBAccess::class.java).toProvider(WriteDBAccessProvider::class.java)
    }

    class ReadDBAccessProvider : Provider<DBAccess> {
        @Inject @ReadOnlyQualifier
        lateinit var readDataSource: DataSource

        @Inject
        lateinit var scoper: Provider<DBAccessRequestScoper>

        override fun get(): DBAccess {
            scoper.get()!!.getDBAccess(readDataSource)
        }
    }

    class WriteDBAccessProvider : Provider<DBAccess> {
        @Inject @WriteQualifier
        lateinit var writeDataSource: DataSource

        @Inject
        lateinit var scoper: Provider<DBAccessRequestScoper>

        override fun get(): DBAccess {
            scoper.get()!!.getDBAccess(writeDataSource)
        }
    }

    @RequestScoped
    class DBAccessRequestScoper() {
        private var alreadyReturnedDBAccess: MyDBAccess? = null

        fun getDBAccess(dataSource: DataSource): DBAccess {
            if (alreadyReturnedDBAccess == null) {
                alreadyReturnedDBAccess = MyDBAccess(dataSource)
            }
            return alreadyReturnedDBAccess!!
        }
    }
}

This way, whichever specific Provider is invoked first will be saved in the DBAccessRequestScoper until the RequestScope closes. It’s not the cleanest solution, but it is a definite improvement on what we had before, and it centralizes all the messiness in a single Guice module. (Side note: small, composable Guice modules are quite nice; I can’t believe we spent several years with an entire application’s worth of bindings in one big module.)

Another small learning: if you inject a Provider, rather than an instance, the RequestScope used applies when the Provider is invoked, not where it’s injected. In other words, the Provider has no state, and invoking the Provider is the same as Guice injecting an instance directly at that current time. This makes sense but wasn’t what I expected initially.

One last “gotcha” remained: we had a couple places that wanted to opt out of this request scoping entirely and get the same object but untethered and unshared via Guice. Should be easy, right? Just inject it when you aren’t in a request scope? Nope. If a binding is scoped, Guice errors if there’s no current scope. OK, I think that makes sense. Then just change ReadDBAccessProvider and WriteDBAccessProvider to check if they’re in a scope to see if they should use the shared Provider, right?

It’s easy to see if you’re currently in a scope, right? Right? Well, see for yourself:

fun isInRequestScope(): Boolean =
  try {
    // Despite the procedural name, this just returns the current context,            
    // if there is one, otherwise throws OutOfScopeException.            
    ServletScopes.transferRequest()
    true        
  } catch (e: OutOfScopeException) {
    false        
  }

Blech. Nice Kotlin syntax can’t hide how ugly that is, but we never found a way around it. The only other idea was to use reflection to read ServletScopes.requestScopeContext, the ThreadLocal field that keeps track of the current scope, but control-flow-by-reflection sounds even worse (and slower) than control-flow-by-exception.

Still, using Scopes has been a clear success. We’ve solved our original problem of state leaking between requests, removed a lot of complex code, and centralized the remaining complexity in a nice tidy Guice module. Despite the name, RequestScoped is a pretty broadly applicable tool – it can be used to share injected instances within a migration, a job, a request, a callback in response to a configuration change, or any other blob of work.

Personally, I learned a few things from this endeavor. First, the old adage Use the Source, Luke is a great starting point. I was too intimidated by the idea that I was using Big Complicated Google Things from the Big Fancy Google Library to just dive in and start learning how it worked for myself. This delayed me in figuring out what Scopes were really doing (until a colleague did it for me), how injected Providers worked, and probably other things too. Second, despite some of my griping, I actually like using Guice more now. The result of this refactoring clearly improved our codebase, and it’s really different reading about the virtues of dependency injection versus seeing it in action on a real problem.

Finally, I’m trying to “be the change I want to see” by writing all this down. I was helped immensely by every clue or bit of context I could scrape from forum threads, old documentation, StackOverflow answers (and questions), and other sources online. Hopefully this post can give the same boost to the next person interested in a relatively obscure usage of Guice.

Sources & Further Reading

https://github.com/google/guice/wiki/Scopes

https://github.com/google/guice/blob/master/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java

https://stackoverflow.com/a/41237632/1631803

https://stackoverflow.com/questions/17303134/is-there-a-way-to-check-if-im-inside-a-servlet-request-with-guice/17411033#17411033

https://stackoverflow.com/questions/42185668/the-difference-between-a-scoping-annotation-and-scope-instances-in-guice

https://stackoverflow.com/questions/28202882/guice-provides-methods-vs-provider-classes

https://stackoverflow.com/questions/3112853/how-do-i-make-an-optional-binding-in-guice

https://stackoverflow.com/questions/27510508/guice-how-to-provide-the-same-key-differently-in-different-scopes

https://stackoverflow.com/questions/27336407/using-the-provider-from-two-different-scopes/41237510#41237510

Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
olivia
olivia
3 months ago

really love the content you’ve provided, looking forward for more information like this, keep us posted!

Rub md
Rub md
2 months ago

Appreciate your thoughts and very helpful for the reader, thanks for sharing.