• No results found

A custom scope in Guice

More use cases in scoping

6.1 Defining a custom scope

6.1.3 A custom scope in Guice

A scope in Guice is represented by the Scope interface. Scope is as follows: public interface Scope {

<T> Provider<T> scope(Key<T> key, Provider<T> unscoped); } PowerStation Chamber << transaction-scoped >> - start() - fire() Gauge - measure() Figure 6.3 A PowerStation’s Chamber depends on a transaction-scoped measuring Gauge.

A custom scope is required to implement only one method, scope(), which returns a scoped instance Provider. Recall the Provider pattern from chapter 4. This is the same, except that instead of being used against the reinjection problem, here it’s used for providing scoped instances. The two arguments to scope() manage the particular object in its context:

■ Key<T> key—The combinatorial key to be scoped

■ Provider<T> unscoped—A provider that creates new instances of the bound key (in other words, it is a no-scope provider)

Use provider unscoped when a new context comes to life and new instances need to be provided. When the scope context completes, you simply discard all scoped instances, obtaining new ones from provider unscoped instead (see figure 6.4).

Listing 6.2 shows how this is done with the transaction scope.

public class TransactionScope implements Scope {

private final ThreadLocal<Map<Key<?>, Object>> instances = new ThreadLocal<Map<Key<?>, Object>>(); public <T> Provider<T> scope(

final Key<T> key, final Provider<T> unscoped) { return new Provider<T>() {

public T get() {

Map<Key<?>, Object> map = instances.get(); if (!map.containsKey(key)) { map.put(key, unscoped.get()) } return (T) map.get(key); } }; }

public void beginScope() { instances.set(new HashMap<Key<?>, Object>()); }

public void endScope() { instances.remove(); }

}

Listing 6.2 TransactionScope implemented in Guice

Scope’s provider Scope’s (Dependent) Injector injects No scope provider obtain No scope (as necessary)

Figure 6.4 The injector obtains instances from a Scope provider, which itself may use the no-scope provider.

Returns a scoped provider

Returns cached or new instance if unavailable

Sets up a new context

In listing 6.2, class TransactionScope exposes the Guice interface Scope. The imple- mented method scope() returns a scoped provider that acts as a bridge between the injector and its default no-scope provider. The no-scope provider unscoped is used to create new instances as required.

A scoped provider is returned for each key requested in method scope(). The pro- vider checks a thread-local hash table, returning cached instances for the current thread:

public T get() {

Map<Key<?>, Object> map = instances.get(); if (!map.containsKey(key)) {

map.put(key, unscoped.get()); }

return (T) map.get(key); }

If an instance is not already present in the hash table, it is created and cached: if (!map.containsKey(key)) {

map.put(key, unscoped.get()); }

Otherwise, the instance mapped to the given key is returned normally: return (T) map.get(key);

This ensures objects that don’t yet exist in the current context are created when needed. Method beginScope() sets up a context for the current transaction by creat- ing a new hash table to cache objects for the life of the scope (transaction):

public void beginScope() {

instances.set(new HashMap<Key<?>, Object>()); }

The hash table maps Guice Keys to their scoped instances. Its complement, method endScope(), is called by the transaction framework when a transaction completes (either by a successful commit or an abortive rollback), disposing of the cached instances:

public void endScope() { instances.remove(); }

The entire context of the transaction, its scope, and scoped instances are confined to a single thread by using the ThreadLocal construct:

public class TransactionScope implements Scope {

private final ThreadLocal<Map<Key<?>, Object>> instances = new ThreadLocal<Map<Key<?>, Object>>(); ...

}

ThreadLocals are utilities that allow stored objects to be available only to the storing thread and none other. Because a transaction runs entirely in a single thread, we can be

sure that its context will only ever need to be accessed by that thread, and simultaneous transactions are separated from one another by confinement to their respective threads.

Now when a transaction-scoped object (such as Gauge from the previous section) is wired to any other object, it is done so inside an active transaction in the current thread. When a transaction completes, this instance is lost and any new transactions will see a new instance created. This also applies to concurrent transactions; they are kept walled apart by thread confinement.

Cool! What about when no transaction is active and a key is sought? Well, this is a serious problem, and it likely represents an error on the programmer’s part. Either she did not start the transaction where it was meant to start or she configured the injector incorrectly. You should add a safety check that reports the error quickly and clearly in such cases:

public <T> Provider<T> scope(

final Key<T> key, final Provider<T> unscoped) { return new Provider<T>() {

public T get() {

Map<Key<?>, Object> map = instances.get();

if (null == map) { throw new

OutOfScopeException("no transaction was active"); } if (!map.containsKey(key)) { map.put(key, unscoped.get()); } return (T) map.get(key); } }; }

This is our sanity check that ensures a proper exception is raised (OutOfScopeExcep- tion) if a transaction-scoped object is sought outside a transaction.

Registering a custom scope in Guice is simple:

public class TransactionModule extends AbstractModule { @Override

protected void configure() {

bindScope(TransactionScoped.class, new TransactionScope());

... } }

The scoping annotation @TransactionScoped is declared as follows: @Retention(RetentionPolicy.RUNTIME)

@ScopeAnnotation

public @interface TransactionScoped { }

This is a standard bit of boilerplate that needs to be used when declaring any Guice scope annotation. Notice that @ScopeAnnotation is a meta-annotation (an annotation

on an annotation), which tells the injector that this is a scoping annotation. Now say that three times fast!