'. '

CompileTimeCache

From APIDesign

Revision as of 13:30, 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

NetBeans configuration mechanism is based on concept of layers of virtual filesystems written in XML. It is quite flexible, yet slightly errorprone. When one makes typo, it is quite hard to find where a problem is. And this is where annotations help quite a lot. Annotations are natural Java language elements. They have associated Javadoc, code completion works when you use them in an IDE, one can see for their usage, etc. All the Java comfort is available for free.

Then, with use of layer generating AnnotationProcessors we can convert them into the flexible NetBeansLayer format. No typos, no inconsistencies in the XML definitions. Everything done automatically and even with compile time verification that the registration is sane (e.g. classes are public, methods are static, etc.). We have a builder like API to generate the XML quite easily:

Code from WordsProcessor.java:
See the whole file.

File f = layer(e).file(
    "Services/" + teName.replace('.', '-') + ".instance"
);
f.methodvalue(
    "instanceCreate",
    "org.apidesign.anagram.impl.annotations.WordsImpl",
    "create"
);
f.stringvalue(
    "instanceClass",
    "org.apidesign.anagram.impl.annotations.WordsImpl"
);
f.stringvalue(
    "instanceOf",
    "org.apidesign.anagram.api.WordLibrary"
);
f.methodvalue(
    "words",
    teName,
    e.getSimpleName().toString()
);
f.write();
 
Just a single @Words annotation and its processor can hide so much XML boiler plate text! NetBeans started to use this approach heavily in its 6.7 release and that was also one of the reasons why we declared the year 2009 TheYearOfAnnotations2009 (listen to or download the podcast). Compile time annotations are really useful!

Lookup Integration

Personal tools