Skip to content

Instantly share code, notes, and snippets.

@dblevins
Created June 6, 2012 05:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dblevins/2880038 to your computer and use it in GitHub Desktop.
Save dblevins/2880038 to your computer and use it in GitHub Desktop.

The following is an overview of the annotation scanning code used in Apache TomEE cleaned up and as a potential commons project and independently reusable library.

Source code here: https://svn.apache.org/repos/asf/commons/sandbox/classscan/branches/commons-finder/

AnnotationFinder

 public AnnotationFinder(Archive archive)

Basic constructor. The Parser or some ParserFactory should be added.

 public AnnotationFinder enableMetaAnnotations()
 public AnnotationFinder enableFindImplementations()
 public AnnotationFinder enableFindSubclasses()

Methods to enable some heavier lifting. Each has a cost and none are essential to basic annotation scanning.

 public List<Package> findAnnotatedPackages(Class<? extends Annotation> annotation)
 public List<Class<?>> findAnnotatedClasses(Class<? extends Annotation> annotation)
 public List<Method> findAnnotatedMethods(Class<? extends Annotation> annotation)
 public List<Constructor> findAnnotatedConstructors(Class<? extends Annotation> annotation)
 public List<Field> findAnnotatedFields(Class<? extends Annotation> annotation)

Fairly self explanatory.

 public List<Annotated<Class<?>>> findMetaAnnotatedClasses(Class<? extends Annotation> annotation)
 public List<Annotated<Method>> findMetaAnnotatedMethods(Class<? extends Annotation> annotation)
 public List<Annotated<Field>> findMetaAnnotatedFields(Class<? extends Annotation> annotation)

Meta-annotation versions of the above. Just noticed a couple missing :)

 public boolean isAnnotationPresent(Class<? extends Annotation> annotation)
 public List<Class<?>> findClassesInPackage(String packageName, boolean recursive)

Some trivial utility methods.

 public <T> List<Class<? extends T>> findSubclasses(Class<T> clazz)
 public <T> List<Class<? extends T>> findImplementations(Class<T> clazz)

These are quite heavy and not recommended for large sets of classes such as the entire JVM classpath. Works great for a handful of jars. Beyond that is not recommended unless you have heaps of Heap.

 public AnnotationFinder select(Class<?>... clazz)
 public AnnotationFinder select(String... clazz)
 public AnnotationFinder select(Iterable<String> clazz)

Newer methods that allow you to narrow down the scope of the AnnotationFinder. Say you have a finder for an entire .war file and you want to get the data for just a jar or specific class or list of classes. Likely there could be 'join' methods to mirror these. As well there could be a 'select' method that could easily take a Filter as an arg.

is a composable system. You create your finder and feed it an archive, like so:

Archive archive = new JarArchive(classloader, jarURL);
AnnotationFinder finder = new AnnotationFinder( archive );
List<Class<?>> plugins = finder.findAnnotatedClasses(PluginAnnotation.class)

If you want some filtering, you add that in:

Archive archive = new JarArchive(classloader, jarURL);

archive = new FilteredArchive(archive, new Filter {

    @Override
    public boolean accept(String name) {
        return name.startsWith("org.foo.");
    }
});

AnnotationFinder finder = new AnnotationFinder( archive );
List<Class<?>> plugins = finder.findAnnotatedClasses(PluginAnnotation.class)

Several archives can be composed together via CompositeArchive

Archive archive = new CompositeArchive(
    new JarArchive(classloader, jarURL),
    new FileArchive(classloader, new File("target/classes/")),
    new ClassesArchive(Foo.class, Bar.class)
    );

Sky is the limit.

We have the following Archive implementations

  • ClassesArchive(Class<?>... classes)
  • ClassesArchive(Iterable<Class<?>> classes)
  • FileArchive(ClassLoader loader, URL url)
  • FileArchive(ClassLoader loader, File dir)
  • JarArchive(ClassLoader loader, URL url)

For creating combinations of the above we have:

  • CompositeArchive(Archive... archives)
  • CompositeArchive(Iterable archives)

For filtering classes out of archvies:

  • FilteredArchive(Archive archive, Filter filter)

And a convenience class to quickly get an Archive from a set of urls

  • ClasspathArchive(ClassLoader loader, URL... urls)
  • ClasspathArchive(ClassLoader loader, Iterable urls)

The above currently only supports jar: and file: urls

Filters

Several built in filters exist for convenience

  • ClassFilter(String name)
  • ContainsFilter(String token)
  • PackageFilter(String packageName)
  • PatternFilter(String expression)
  • PatternFilter(Pattern pattern)
  • PrefixFilter(String prefix)
  • SuffixFilter(String suffix)

As well as some filter implementations that allow all of the above to be composed together

  • ExcludeIncludeFilter(Filter include, Filter exclude)
  • FilterList(Filter... filters)
  • FilterList(Iterable filters)
  • IncludeExcludeFilter(Filter include, Filter exclude)

And the following convenience class for quickly creating any of the above

public class Filters {
    public static Filter packages(String... packages) {
    public static Filter classes(String... classes) {
    public static Filter prefixes(String... prefixes) {
    public static Filter tokens(String... tokens) {
    public static Filter suffixes(String... suffixes) {
    public static Filter patterns(String... patterns) {
    public static Filter optimize(Filter... filters) {
    public static Filter optimize(List<Filter>... filterss) {
    public static Filter invert(Filter filter) {
}

Meta-Annotations

Meta-Annotations are an experiment in annotation inheritance, abstraction and encapsulation with a Java SE mindset

A meta-annotation is any annotation class annotated with @Metatype. The other annotations used on the meta-annotation become part of its definition. If any of those annotations happen to also be meta-annotations, they are unrolled as well and their annotations become part of the definition.

@Metatype

The recursion that is the meta-annotation concept only happens when an annotation is marked as a @javax.annotation.Metatype.

When @Metatype is seen the basic contract is "carry the surrounding annotations forward". When a class, method or other target uses an annotation annotated with @Metatype the annotations on that annotation are "unrolled" or carried forward and effectively placed on that class, method or other target as if they were explicitly declared.

If any of the annotations that are carried forward also are annotated with @Metatype the recursion continues. The result is a simple algorithm or design pattern that provides inheritance or reuse in a way that is not specific to any domain, API, or specification.

APIs and specifications can choose to formally adopt annotation reuse in this fashion, but the core concept and implementations of @Metatype do not need to be expanded to support these APIs or specifications.

The simple elegance of this not being domain specific is that it could be used to combine several annotations from different specifications into one reusable annotation. Say JAX-RS @PathParam("id") with Bean Validation @NotNull to create a new annotation called @ValidId.

Creating Meta-Annotations

If the annotation in question can be applied to ElementType.ANNOTATION_TYPE or ElementType.TYPE, creating a meta-annotation version of it is quite easy.

@TransactionManagement(TransactionManagementType.CONTAINER)
@Metatype
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ContainerManagedTransactions {
}

When the annotation in question cannot be applied to ElementType.ANNOTATION_TYPE or ElementType.TYPE, things get interesting. This is where meta-annotations depart from things like @Stereotype. The goal of meta-annotations is to be completely generic and not specific to any one domain or API. A such, you cannot really require all existing APIs change to allow for meta-annotations. The goal is that meta-annotations can be used generically and do not need to be "designed" into an API.

To allow annotations that apply to FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, or PACKAGE, as well as any other location where annotations may be applied in the future a compromise is made.

import javax.ejb.Schedule;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Metatype
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

public @interface Daily {
    public static class $ {

        @Daily
        @Schedule(second = "0", minute = "0", hour = "0", month = "*", dayOfWeek = "*", year = "*")
        public void method() {
        }
    }
}

An inner class named $. This is enough to bind together the @Daily and @Schedule in the context to which they both apply.

Ugly but effective. Alternate proposals welcome.

The above is considered the public API portion of the meta-annotation concept.

The concept itself is born out of standards based systems like EJB and CDI where annotation processing is invisible to the application itself. In those settings the above is enough and no additional APIs would be needed to support meta-annotations in standard APIs.

Under the covers

The "guts" of this particular implementation is designed to look and feel as much like the reflection API as possible. Obviously, with VM level control, you could do much better. A clean Java SE API might be just what is needed and its very possible that meta-annotations should really be a Java SE concept.

Here's a glimpse as to how things can look under the covers:

final java.lang.reflect.AnnotatedElement annotated = new org.apache.commons.classscan.filter.meta.MetaAnnotatedClass(Triangle.class);
assertNotNull(annotated);

assertTrue(annotated.isAnnotationPresent(Color.class));
assertTrue(annotated.getAnnotation(Color.class) != null);
assertTrue(!contains(Color.class, annotated.getDeclaredAnnotations()));
assertTrue(contains(Color.class, annotated.getAnnotations()));
assertEquals("red", annotated.getAnnotation(Color.class).value());

assertTrue(annotated.isAnnotationPresent(Red.class));
assertTrue(annotated.getAnnotation(Red.class) != null);
assertTrue(!contains(Red.class, annotated.getDeclaredAnnotations()));
assertTrue(contains(Red.class, annotated.getAnnotations()));

assertTrue(annotated.isAnnotationPresent(Crimson.class));
assertTrue(annotated.getAnnotation(Crimson.class) != null);
assertTrue(contains(Crimson.class, annotated.getDeclaredAnnotations()));
assertTrue(contains(Crimson.class, annotated.getAnnotations()));

The application classes would look like so:

@Crimson
// -> @Red -> @Color
public static class Triangle {

}

@Metatype
@Color("red")
// one level deep
@Target(value = {TYPE})
@Retention(value = RUNTIME)
public static @interface Red {
}

@Metatype
@Red
// two levels deep
@Target(value = {TYPE})
@Retention(value = RUNTIME)
public static @interface Crimson {
}

Best Practices

It is recommended to have an api package or some other package where "approved' annotations are defined and to prohibit usage of the non-meta versions of those annotations. All the real configuration will then be centralized in the api package and changes to the values of those annotations will be localized to that package and automatically be reflected throughout the application.

An interesting side-effect of this approach is that if the api package where the meta-annotation definitions exist is kept in a separate jar as well, then one can effectively change the configuration of an entire application by simply replacing the api jar.

Future concepts

XML Overriding

The unrolling of meta-annotations happens under the covers. In that same vein, so could the concept of overriding.

The above @Red annotation might theoretically be overridden via xml as follows:

<org.superbiz.api.Red>
  <org.superbiz.api.Color value="dark red"/>
</org.superbiz.api.Red>

Or take more complex meta-annotation definition like the following:

package org.superbiz.corn.meta.api;

import javax.ejb.Schedule;
import javax.ejb.Schedules;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Metatype
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

public @interface PlantingTime {
    public static interface $ {

        @PlantingTime
        @Schedules({
                @Schedule(month = "5", dayOfMonth = "20-Last", minute = "0", hour = "8"),
                @Schedule(month = "6", dayOfMonth = "1-10", minute = "0", hour = "8")
        })
        public void method();
    }
}

This might theoretically be overridden as:

<org.superbiz.corn.meta.api.PlantingTime>
  <javax.ejb.Schedules>
    <value>
      <javax.ejb.Schedule month="5" dayOfMonth="15-Last" minute="30" hour="5"/>
      <javax.ejb.Schedule month="6" dayOfMonth="1-15" minute="30" hour="5"/>
    </value>
  </javax.ejb.Schedules>
</org.superbiz.corn.meta.api.PlantingTime>

Merging or Aggregating definitions

Certain annotations take lists and are designed to be multiples. In the current definition of meta-annotations, the following is illegal.

@RolesAllowed({"Administrator", "SuperUser"})
@Metatype
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Admins {
}

@RolesAllowed({"Employee", "User"})
@Metatype
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Users {
}


public static class MyBean {

    @Admin
    @User
    public void doSomething() {
        // ...
    }
}

Here the @Admin and @User annotation each resolve to @RolesAllowed. Since only one @RolesAllowed annotation is allowed on the method per the Java language specification, this results in an error.

The intention is clear however and aggregating metadata together in this way is natural.

A theoretical way to support something like this is with an annotation to describe that this aggregation is intended and desired. Note the addition of the theoretical @Merge annotation.

@RolesAllowed({"Administrator", "SuperUser"})
@Metatype
@Merge
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Admins {
}

@RolesAllowed({"Employee", "User"})
@Metatype
@Merge
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Users {
}


public static class MyBean {

    @Admin
    @User
    public void doSomething() {
        // ...
    }
}

A new @RolesAllowed annotation would be created containing the list {"Administrator", "SuperUser", "Employee", "User"} and that would represent the final @RolesAllowed usage for the doSomething() method.

Parameters

Currently, meta-annotations to not allow parameters. It could, however, be desirable to allow this and provide some way for parameters of the encapsulated annotations be overridden in the meta-annotation.

Ideas on how this could be well designed are welcome.

Meta-Annotation Case Study: EJB

A view of how Meta-Annotations might be applied to a technology such as EJB is here:

Meta-Annotation Presentations

Source Structure Overview

Here's an overview of the code (somewhat redundant with the above information):

org.apache.commons.classscan.finder

  • AnnotationFinder
  • IAnnotationFinder
  • ResourceFinder

AnnotationFinder is essentially the "db" and has methods to query.

ResourceFinder is for basic file location such as "META-INF/persistence.xml" and can also be used as part of a ServiceLoader replacement. See the ResourceFinder.mdtext for details

org.apache.commons.classscan.finder.archive

  • Archive
  • ArchiveIterator
  • ClassesArchive
  • ClasspathArchive
  • CompositeArchive
  • FileArchive
  • FilteredArchive
  • JarArchive

The Archive interface allows for sources of class files to be abstracted. The JarArchive, ClassesArchive and FileArchive are concrete implementations. CompositeArchive is simply a collection of Archive implementations. FilteredArchive allows for the archive itself to be filtered in various ways.

org.apache.commons.classscan.finder.filter

  • ClassFilter
  • ContainsFilter
  • ExcludeIncludeFilter
  • Filter
  • FilterList
  • Filters
  • IncludeExcludeFilter
  • PackageFilter
  • PatternFilter
  • PrefixFilter
  • SuffixFilter

The Filter interface simply allows you to give a yay or nay to a String. This is of course useful for greatly limiting the classes actually scanned when combined with an Archive via wrapping it with a FilteredArchive and supplying a Filter.

FilterList is a Filter implementation that combines one or more filters. Both "yes" and "no" filters can be used via IncludeExcludeFilter or ExcludeIncludeFilter which are modeled after HTTPd allowed host pattern.

PackageFilter, ClassFilter, SuffixFilter, PrefixFilter, ContainsFilter and PatternFilter are concrete implementations of Filter that each have a simple approach to testing the string to give a yay or nay.

The Filters class is a convenience class that makes it easy to construct the various filters with little syntax as well as performs unwrapping when necessary if it sees things like a FilterList with only one filter or a FilterList containing another FilterList.

org.apache.commons.classscan.finder.meta

  • Annotated
  • AnnotatedMember
  • AnnotatedMethod
  • MetaAnnotated
  • MetaAnnotatedClass
  • MetaAnnotatedConstructor
  • MetaAnnotatedElement
  • MetaAnnotatedField
  • MetaAnnotatedMethod
  • MetaAnnotation

This package provides basic support for meta-annotations which are simply annotation reuse or inheritance. CDI, Bean Validation and JAX-RS all have similar concepts of inheritance. This code does a more generic approach to reuse that isn't tied to any one particular specification. Fun to play with, but not critical to the core concept of annotation scanning.

org.apache.commons.classscan.finder.model

  • Annotatable
  • AnnotationInfo
  • ClassInfo
  • FieldInfo
  • Info
  • InfoHandler
  • MethodInfo
  • PackageInfo

The basic objects to model class metadata along with a listener.

There could easily be a InfoHandlerList which would be an implementation of InfoHandler that allowed for a collection of InfoHandlers.

org.apache.commons.classscan.finder.parse

  • AsmParser
  • Parser

Abstracts out the actual bytecode parsing such as ASM. The parser need only read from the Archive and produce the model of Info objects and invoke the InfoHandler.

org.apache.commons.classscan.finder.util

  • Classes
  • SingleLinkedList
  • UriSet
  • UrlSet

Just some utilities. UrlSet and UriSet are essentially searchable collections of URL or URI to allow for something as large as a classpath of URLs to be easily narrowed. Was written before the Filter API and only partially supports it. Would be nice to have it updated. SingleLinkedList is a minimally functional list designed to save memory in the model Info objects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment