Adam Warski

25 Apr 2013

MacWire 0.2: Scopes are simple!

dependency injection
metaprogramming
macwire
scala

MacWire generates new instance creation code of given classes, using values in the enclosing type for constructor parameters, with the help of Scala Macros. DI container replacement.

Version 0.2 has just landed!

First things first …

First, some bad news. Due to limitations of the current macros implementation in Scala (for more details see this discussion) in order to avoid occasional compilation errors it is necessary to add type ascriptions to the dependencies. This is a way of helping the type-checker that is invoked by the macro to figure out the types of the values which can be wired.

In practice it is good to always write the type ascription. For example:

trait MyModule {
   ...
   lazy val theUserFinder: UserFinder = wire[UserFinder]
   ...
}

This is a major inconvenience, but hopefully will get resolved some day. See the “Limitations” section in the README for more details.

Update 28/04/2013 As noted in the comments below, the recommendation for adding type ascriptions may be in fact relaxed by adding special-handling for declarations that are simple wire[X] expressions. So stay tuned for future releases for improvements :)

Also, it is worth noting that the type ascription may be a subtype of the wired type. This can be useful if you want to expose e.g. a trait that the wired class extends, instead of the full implementation.

… to the point

Now, to the good news. Especially when developing web applications, it is very useful to scope some of the dependencies, so that there’s a new instance for each request (but for each dependency usage, it is the same instance in one request), or that there’s a single instance per http session.

So far MacWire had two “built-in” scopes: singleton (declare the dependency as lazy val) and dependent (def). Now it is possible to create custom scopes, by implementing the Scope trait.

Scopes often seem “magical”; in traditional frameworks it is usually necessary to add an annotation to the class and then the dependency is automagically turned into a scoped one by the container.

In fact implementing a scoped bean is quite simple. We need two things. Firstly, instead of using instances directly, we need to use a proxy which delegates method calls to the “current instance”. What “current instance” means is scope-specific.

Secondly, we need a way to read a value from the scope, and store a value in the scope, if there’s no value yet. The two methods in the Scope trait correspond directly to the two cases outlined above.

Using a scope is quite straightforward; the scope of a dependency is declared in the wiring code, in the module. For example:

trait WebModule {
   lazy val loggedInUser: LoggedInUser = session(wire[LoggedInUser])

   def session: Scope
}

Note that the scope here is abstract (only the name suggests that it is a session scope). This has a couple of advantages. For testing, we can substitute a NoOpScope. Also, we can move the framework-integration code to the integration layer, or even provide a couple of implementations depending on the framework/container used.

MacWire comes with a Scope implementation targeted at synchronous web frameworks, ThreadLocalScope. The scope needs to be associated and disassociated with a scope storage. To implement a:

  • request scope, we need a new empty storage for every request (associateWithEmptyStorage method)
  • session scope, the storage (a Map) should be stored in the HttpSession (associate(Map) method)

This association can be done, for example, in a servlet filter:

class ScopeFilter(sessionScope: ThreadLocalScope) extends Filter {
  def init(filterConfig: FilterConfig) {}

  def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
    val httpRequest = request.asInstanceOf[HttpServletRequest]

    val sessionScopeStorage = new ScopeStorage {
      def get(key: String) = Option(httpRequest.getSession.getAttribute(key))
      def set(key: String, value: Any) {
        httpRequest.getSession.setAttribute(key, value)
      }
    }

    sessionScope.withStorage(sessionScopeStorage) {
      chain.doFilter(request, response)
    }
  }

  def destroy() {}
}

Note also that it is trivial to use a scoped value e.g. in an asynchronous process, where no web request is available: simply associate the scope with any storage, can be a temporary map. In traditional web frameworks this usually requires some amount of hacking.

To sum up, to use a scope in MacWire you need to:

  • declare a Scope in your module, use if for scoped dependencies
  • provide an implementation of the scope, e.g. ThreadLocalScope
  • associate the scope with a storage, e.g. in a servlet filter

You can see how this works in practice by browsing & running a Scalatra+MacWire example application. To run, clone MacWire and use this command: sbt examples-scalatra/run. To browse the code, simply head to GitHub.

2013-04-25_1649

The example contains a request-scoped dependency (SubmittedData), session-scoped dependency (LoggedInUser) and three main services (Service1, Service2, Service3). Each class has some dependencies (declared in the constructor), and is wired using wire[]. The code is organised in two modules: logic and servlet. The services have methods to return a string-status, containing the current request and session-scoped values, so that it is possible to verify that indeed the values are shared properly. The servlet module contains a servlet which shows the service status, plus the scopes implementations.

Note that the scopes subproject is completely independent, and can be used stand-alone with manual wiring or any other library/framework. Its only dependency is javassist.

Have fun!
Adam

comments powered by Disqus

Any questions?

Can’t find the answer you’re looking for?