How-to: avoid method or getter to be called several times by caching result
jkronegg Feb 5, 2009 2:05 PMIn a .xhtml page, JSF may call your method or getters several times. This may cause performance issues if the method/getter takes a lot of time to complete (e.g. isRendered(), see JBSEAM-3008). Another problem is that the returned result can change between calls, leading to incoherences in the JSF page.
While the forum provide some kind of solution, the provided solutions are not fully described and compared. This post aims to correct that fact. Feel free to comment and maybe add other methods.
I will first compare the methods on overall, then describe each technique individually.
Overall comparison
The table below presents the caching methods. The Result computation
column can contain the following values:
-
call
: the result is computed every call -
creation
: the result is computed at the component creation time -
lazy
: the result is computed at the first call
Method name | Description | Result computation | Advantages | Disadvantages | Reference |
Default | Do nothing and let the Seam framework call methods when required | call |
|
| - |
@Create/@Factory | Annotate the property initializer method and it will be called after the instanciation+injection process | creation |
|
| Method called several times |
In-method cache | Cache the result in the method itself by using a if (x==null) {x=...}guard | lazy |
|
| Why does JSF call my getter hundreds of times? |
Interceptor cache | Build a Seam Interceptor which will be used to cache the value. | lazy |
|
| - |
Default method
The default method
is used to define the following baseline code:
@Name("defaultExample") @Scope(ScopeType.CONVERSATION) public class DefaultExampleAction implements DefaultExample { public Person getPerson() { Person person = ... // e,g. database lookup return person; } }
When using such code, the database request will be done every time the getPerson() method is called, which is often the case in a JSF page.
@Create and @Factory method
This method is described in the Method called several times
post. The @Factory annotation is used for injected components, while the @Create is used for standard Java properties.
The typical code for the @Factory annotation is the following :
@Name("factoryExample") @Scope(ScopeType.CONVERSATION) public class FactoryExampleAction implements FactoryExample { @In private Person person; public Person getPerson() { return person; } @Factory("person") public void personInitializer() { person = ... // e,g. database lookup } }
The typical code for the @Create annotation is the following :
@Name("createExample") @Scope(ScopeType.CONVERSATION) public class CreateExampleAction implements CreateExample { private Person person; public Person getPerson() { return person; } @Create public void personInitializer() { person = ... // e,g. database lookup } }
For both annotations, the Seam component life cycle is the following:
- component instanciation
- injection and calls to @Factory annotated methods to create the injected components
- call of @Create annotated methods
- JSF calls the getter getPerson() several times
These annotations have the advantage to be called at the component creation time (this ensures that all the required fields are initialized when the component is really used). On the other side, the component instanciation duration will be longer, the property initializer method cannot use dynamic data not available at creation time.
As a side comment, some programmers complains about @Factory or @Create annotations are not working on the following code (see example in this post):
@Name("createBadExample") @Scope(ScopeType.CONVERSATION) public class CreateBadExampleAction implements CreateExample { private Person person; @Create // called once at creation and at every call => bad => annotate the initializer, not the getter! public Person getPerson() { person = ... // e,g. database lookup return person; } }
It should remembered that the @Factory or @Create annotations must be added on the initializer method and not on the getter itself!
In-method cache method
This method is described in the Seam FAQ :
@Name("inMethodCacheExample") @Scope(ScopeType.CONVERSATION) public class InMethodCacheExampleAction implements InMethodCacheExample { private Person person = null; public Person getPerson() { if (person==null) { // not yet cached => cache it person = ... // e,g. database lookup } return person; } }
The getPerson() method now uses lazy-loading which improves the component instanciation performance. The disdvantage is that it requires the person property and that the cache scope is the same as the component. Moreover, the getter now contains code which is related to the caller habits and not to the business job of getting the Person (I do not think this is the general Seam philosophy).
A variant can be to use the Seam page/session/etc. context, so the code can be the following:
@Name("inMethodCacheExample") @Scope(ScopeType.CONVERSATION) public class InMethodCacheExampleAction implements InMethodCacheExample { public Person getPerson() { Context cacheContext = Contexts.getPageContext(); Person person = cacheContext.get("myPerson"); if (person==null) { // not yet cached => cache it person = ... // e,g. database lookup cacheContext.set("myPerson", person); } return person; } }
This improve the initial version: the person property is removed and the cache scope can be different than the component scope. The drawback is that there is even more business-unrelated code.
It should be noticed that the In-method cache
method is very flexible in the sense that you can implement your own caching strategy (LRU, LFU, ...).
Interceptor cache method
AFAIK, this approach has not been described in the Seam forum. The idea is to annotate the method whose the result need to be cached. This lead to the following code:
@Name("interceptorCacheExample") @Scope(ScopeType.CONVERSATION) @CachedMethods public class InterceptorCacheExampleAction implements InterceptorCacheExample { @CachedMethodResult(ScopeType.PAGE) public Person getPerson() { Person person = ... // e,g. database lookup return person; } }
The @CachedMethods class annotation tells Seam that the component is processed by a specific Interceptor. The @CachedMethodResult annotation tells the Interceptor that the method result is cached. The cache scope can be defined to the standard Seam ScopeType scopes, the cache default scope being ScopeType.PAGE.
This solution produces clean code and lazy initializing, but it has the drawback on the logging/profiling side: if the result is present in cache, the getPerson() method will not be called. Consequently, if you log something at the beginning/end of the method, your logs will contain only entries for the first call and if you profile, the profiler will tell that the method has been called only one time.
The magic behind the @CachedMethods and @CachedMethodResult annotations is in the following three files (and also because the InterceptorCacheExampleAction component is in fact a proxy):
Annotation CachedMethods.java:
import org.jboss.seam.annotations.intercept.Interceptors; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Interceptors(CachedMethodResultInterceptor.class) public @interface CachedMethods {}
Annotation CachedMethodResult.java:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CachedMethodResult { ScopeType value() default ScopeType.PAGE; }
Class CachedMethodResultInterceptor.java:
import org.jboss.seam.annotations.intercept.Interceptor; @Interceptor(stateless=true,around={JavaBeanInterceptor.class}) public class CachedMethodResultInterceptor implements OptimizedInterceptor { @AroundInvoke public Object aroundInvoke(InvocationContext ic) throw Exception { Method m = ic.getMethod(); CachedMethodResult cmr = m.getAnnotation(CachedMethodResult.class); if (cmr!=null) { // the method result is cached => get it from the cache (or cache it if absent) Context c = cmr.value().getContext(); String key = CachedMethodResultInterceptor.class.getName()+"#"+m.getDeclaringClass().getName()+"/"+m.getName(); // Note: the key can be whatever unique value composed by the Interceptor and Method. The above key could be improved Object result = c.get(key); if (result==null) { // result not yet in cache => cache it result = ic.proceed(); c.set(key, result); } return result; } else { // the method is not cached => delegate call to the InvocationContext return ic.proceed(); } } }
When a JSF page calls the getPerson() method, the call is not made directly, but via the Seam interceptors. As the component class is annotated with the @CachedMethods, the CachedMethodResultInterceptor will be inserted in the Seam interceptor chain (because of the around
annotation property, the CachedMethodResultInterceptor will be called before the JavaBeanInterceptor). Thus, as the getPerson() method is annotated by @CachedMethodResult, the CachedMethodResultInterceptor will look into the cache if the result is already computed. If cached, the result will be returned from the cache (i.e. no getPerson() method call!). Else, the result will be obtained from the InvocationContext (which will delegate the method call to the JavaBeanInterceptor), cached and returned.
From the performance/speed perspective, the Invocator cache
method is probably a bit slower than the In-method cache
method because of the added CachedMethodResultInterceptor and of the cache lookup. However, the code simplicity and cache scope functionality are a great improvement which may worth the little performance drawback (if any).
You may have been noticed that the InterceptorCacheExampleAction class structure is very similar to the default
DefaultExampleAction class structure: only the @CachedMethods and @CachedMethodResult annotations have been added. This means that you can add method result caching at a very low cost.
Conclusion
Having reviewed the (some of the) existing method result caching methods, we cannot tell that one is better than another: it depends on the usage. If you are looking for...
Method name | Clean code | High component creation performance | High call performance | Custom cache strategie |
Default | yes | yes | - | - |
@Create/@Factory | - | - | yes | - |
In-method cache | - | yes | yes | yes |
Interceptor cache | yes | yes | yes | - |
I hope it helped...