CompileTimeCache

From APIDesign

Revision as of 12:58, 25 July 2009 by JaroslavTulach (Talk | contribs)
Jump to: navigation, search

Caches are usually created during installation time or during first execution/computation. Just like the CacheForModularity, they provide another store for information available elsewhere. This store is easily accessible, faster to read and use which then results in more optimal execution.

However caches can also be created during compilation. With JDK6 and the help of AnnotationProcessors, it is easier than ever. This article shows general principles and describes few tools that can make the task even simpler. Let's describe yet another variant of Component Injection, by rewriting the Anagram Game as introduced in Chapter 7.

Contents

Define the Annotation

Although not necessary, it is good to define an annotation in your API. At the end AnnotationProcessors are optimized to process annotations. To allow API users to easily define list of words to feed into the Anagram game, let's introduce @Words annotation:

Code from Words.java:
See the whole file.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Words {
}
 

Any user will be allowed to annotation its own static method with this annotation instructing the API to call this method whenever somebody needs a list of words to play the anagram game. Here is a sample use:

Code from WordsFactory.java:
See the whole file.

public class WordsFactory {
    @Words
    public static String[] myWords() {
        return new String[] {
            "microprocessor",
            "navigation",
            "optimization",
            "parameter",
            "patrick",
        };
    }
}
 

Please note that although this example is quite simple (and there are usually more complicated annotations), it already simplifies the use of the API compared to original ServiceLoader based alternative - one does not need to implement any factory interface, just provide a method and annotate it.

Compiler Plugin

The @Words annotation has RetentionPolicy.SOURCE. This means that it shall not propagate to resulting bytecode or be present in runtime (via reflection API). Instead it needs to be processed during compile time via AnnotationProcessor. Is it complex to define one? Not at all, just write:

Code from WordsProcessor.java:
See the whole file.

@ServiceProvider(service=Processor.class)
@SupportedAnnotationTypes("org.apidesign.anagram.api.annotations.Words")
@SupportedSourceVersion(SourceVersion.RELEASE_5)
public class WordsProcessor extends LayerGeneratingProcessor {
 
    @Override
    protected boolean handleProcess(
        Set<? extends TypeElement> set, RoundEnvironment env
    ) throws LayerGenerationException {
        Elements elements = processingEnv.getElementUtils();
 
        for (Element e : env.getElementsAnnotatedWith(Words.class)) {
            Words w = e.getAnnotation(Words.class);
 
            TypeElement te = (TypeElement)e.getEnclosingElement();
            String teName = elements.getBinaryName(te).toString();
}
}
}
 

The processor needs to be present in the same JAR as the @Words annotation and it has to be registered via java extension mechanism. The code above is slightly NetBeans specific (use of @ServiceProvider instead of plain META-INF/service/ registration and subclassing of LayerGeneratingProcessor), yet it still shows the common properties of every processor. Each has a method to process its annotations which is called by the compiler as soon as these annotations are used in the compiled code. Then one needs to iterate over all those annotations and somehow process them.

The good thing is that since JDK6, the javac compiler invokes these processors by default. All that is needed is to include a library which contains their registrations on the compile classpath. Processors will be discovered and used during compilation. Natural, zero configuration needed.

Generating Caches

Annotations have gained quite a lot of popularity since Java 5. However people usually use RetentionPolicy.RUNTIME or at most RetentionPolicy.CLASS and then scan classes during execution. This is an approach used by Jersey or Spring. It is not really effective. Why? Well, imagine that the first thing your framework does is to open all JARs on classpath, seek what classes their contain and inspect some of them. If they contain the desired annotation, do something, however as majority of the does not use your annotation, just skip them (but leaving them uselessly loaded in memory). Not really effective.

Is there a better way? Indeed: Generate caches during compilation and use them to effectively load just the needed classes instead of scanning whole classpath. All that is needed is to register an AnnotationProcessor for important annotations and after compilation generate META-INF/jersey.classes or META-INF/spring.classes with list of class names to inspect. Then just use

for (Enumeration<URL> en = getClassLoader().getResources("META-INF/jersey.classes"); en.hasNextElement(); ) {
  URL config = en.nextElement();
  // read and process classes named in config
}

Even this simple optimization can dramatically boost start up performance of your framework.

Namespace

Personal tools
buy