More use cases in scoping
6.1 Defining a custom scope
6.1.4 A custom scope in Spring
The same principles of context, instance longevity, and thread-safety apply to all injec- tors, and Spring is no different. The particular interfaces to expose for scoping, how- ever, are slightly different. Spring also has a Scope interface that your scope implementation must expose. But it looks a bit different from Guice’s Scope. It is shown in listing 6.3.
package org.springframework.beans.factory.config; public interface Scope {
Object get(String key, ObjectFactory unscopedProvider); Object remove(String key);
String getConversationId();
void registerDestructionCallback(String key, Runnable destructionCallback);
}
Let’s take a look at this interface and see how it relates to what we saw in the previous section. First, the scoping provider method:
Object get(String key, ObjectFactory unscoped);
This is almost identical to Guice’s scope() method, except that instead of returning a scoped provider, get() returns the scoped instance itself. Instead of taking a combina- torial key, it takes a string key. You can think of get() as being the provider itself. The second argument, ObjectFactory, is an implementation of the Provider pattern (sim- ilar to Guice’s unscoped Provider). It provides no-scoped instances of the given key.
Next, the remove() method: Object remove(String key);
remove evicts any instance stored for the given key in the scope’s internal cache (the hash table in TransactionScope), and then another method retrieves the scope’s identity: String getConversationId();
This is an unusually named method. What it essentially points to is the unique identity of the particular scope context. If a transaction were active for the current thread, this method would return a string that uniquely identified this transaction. If we were implementing a session scope, we would return a session ID, unique to a user.
Finally, there’s a lifecycle support method:
void registerDestructionCallback(String key, Runnable
➥ destructionCallback);
registerDestructionCallback() is intended to support lifecycle for specific keys when their instances go out of scope. We won’t delve much into this now since lifecy- cle is coming up in the next chapter.
So what might TransactionScope look like with Spring? Listing 6.4 takes a stab at it.
package my.custom;
public class TransactionScope implements Scope {
private final ThreadLocal<Map<String, Object>> instances = new ThreadLocal<Map<String, Object>>();
public Object get(String key, ObjectFactory unscoped) { Map<String, Object> map = instances.get();
if (null == map)
throw new IllegalStateException("no transaction is active"); if (!map.containsKey(key)) {
map.put(key, unscoped.getObject()); }
return map.get(key); }
public void beginScope() { instances.set(new HashMap<String, Object>()); }
public void endScope() { instances.remove(); }
public Object remove(String key) { if (null == instances.get())
throw new IllegalStateException("no transaction is active"); return instances.get().remove(key);
}
public String getConversationId() { if (null == instances.get())
throw new IllegalStateException("no transaction is active"); return instances.get().toString();
}
public void registerDestructionCallback(String key, Runnable destructionCallback) {
... } }
Listing 6.4’s TransactionScope is very similar to Guice’s TransactionScope. The major difference is the three additional methods that Spring requires. For the conver- sation ID, I return the hash table itself (in String form). This is a simple hack for
Listing 6.4 Transaction scope implemented as a custom scope in Spring
Scoped instances provider method Starts scope context Disposes current scope context
Disposes a specific key from current context
A unique string identifying the hash table (context)
identifying a context. Depending on the kind of Map, this may or may not have good results. You will want to generate a more appropriate identifier in a production ver- sion. I also perform a sanity check every time scoping methods are called:
if (null == instances.get())
throw new IllegalStateException("no transaction is active"); This is important because it fails fast with a clear and easily identifiable IllegalState- Exception. Countless hours have been wasted because failing code does not indicate exactly what was wrong and where.
Registering this scope with the injector is straightforward, and it involves a bit of simple cut-and-paste code in your injector configuration:
<beans ...> <bean ➥ class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="transaction"> <bean class="my.custom.TransactionScope"/> </entry> </map> </property> </bean> ... </beans>
Then you are free to use it just like any other scope when declaring keys: <bean id="gauge" class="nuclear.Gauge" scope="transaction"> ...
</bean>
Now your new scoped objects can be injected and used as needed. The next section switches around and looks at problems you can encounter when creating your own scopes. It is essential to understand these before you go off and create your own scopes.
6.2
Pitfalls and corner cases in scoping
Letting the injector manage the state of your objects is a wonderful thing when applied properly, but it also attracts danger. Scopes are extremely powerful because they invert the responsibility of managing state, and the injector takes care of passing it around to dependents. It keeps objects free of such concerns and makes them more focused and testable. However, this same power can lead to very grave pitfalls.
Many of these have to do with thread-safety. Some are about construction of object graphs that are striped with objects of different scopes. These pitfalls can lead to a range of unrelated problems that are not immediately apparent, for example, mem- ory leaks, poor performance, or even erratic behavior. The counter to this latent peril is a thorough understanding of problem cases. With that said, let’s start with scoping and thread-safety.