atteo / classindex

Index classes, do not scan them!
Apache License 2.0
263 stars 42 forks source link

Build Status Maven Central Apache 2 Javadocs

About

ClassIndex is an index of classes which you can query for:

ClassIndex:

How to use it?

  1. Add dependency

    1. Maven
      <dependency>
          <groupId>org.atteo.classindex</groupId>
          <artifactId>classindex</artifactId>
          <version>3.4</version>
      </dependency>
    2. Gradle
      compile('org.atteo.classindex:classindex:3.4')
      annotationProcessor('org.atteo.classindex:classindex:3.4') // for gradle 5.0+
    3. Scala
      libraryDependencies ++= Seq(
          "org.atteo.classindex" % "classindex" % "3.4"
      )
    4. For other see: Maven Central
  2. Annotate your annotation with @IndexAnnotated

       @IndexAnnotated
       public @interface Entity {}
  3. Retrieve a list of annotated classes at run-time

       Iterable<Class<?>> klasses = ClassIndex.getAnnotated(Entity.class);

How it works?

Indexer

ClassIndex indexes your classes at compile time by providing the implementation of the standard annotation processor. Adding ClassIndex to your compile classpath is sufficient to trigger indexing process thanks to automatic discovery of annotation processors by 'javac'.

Run-time API

ClassIndex provides a convenient API to read the indexes generated by the annotation processor.

Why ClassIndex?

Speed

Traditional classpath scanning is a very slow process. Replacing it with compile-time indexing speeds Java applications bootstrap considerably.

Here are the results of the benchmark comparing ClassIndex with various scanning solutions.

Library Application startup time
None - hardcoded list 0:00.18
Scannotation 0:05.11
Reflections 0:05.37
Reflections Maven plugin 0:00.52
Corn 0:24.60
ClassIndex 0:00.18

Notes: benchmark was performed on Intel i5-2520M CPU @ 2.50GHz, classpath size was set to 121MB.

Compatibility

The traditional approach to retrieve a list of classes annotated with a given annotation is a process called classpath scanning. This involves:

  1. finding out the application run-time classpath and
  2. analysing the bytecode of the classes located there.

The first part is not really possible in a general case, because ClassLoader API does not provide a method to retrieve its classpath. Well, the ClassLoader can even generate the classes on the fly. Usually, though, the specific classloader implementation used is a URLClassLoader from which the classpath can be retrieved using its [getURLs()](https://docs.oracle.com/javase/10/docs/api/java/net/URLClassLoader.html#getURLs()) method.

ClassIndex, on the other hand, uses annotation processing to generate the index of classes at compile-time ant put it with the rest of the compiled .class files. This does not slow the compilation process in any measurable way. The index is then available at run-time using the getResource() method which must be implemented by all classloaders.

Usage

Class Indexing

There are two annotations which trigger compile-time indexing:

Here is an example:

@IndexAnnotated
public @interface Entity {
}

@Entity
public class Car {
}

...

for (Class<?> klass : ClassIndex.getAnnotated(Entity.class)) {
    System.out.println(klass.getName());
}

You can also find the complete example as a Maven project on Github: https://github.com/atteo/classindex-maven-example

Service Loader

For subclasses of the given class the index file name and format is compatible with what ServiceLoader expects.

This means that if you annotate your service interface with @IndexSubclasses, the files in META-INF/services/ will be generated automatically for you.

Keep in mind that ServiceLoader also requires for the classes to have a zero-argument default constructor.

jaxb.index

For classes inside given package the index file is named "jaxb.index", it is located inside the package folder and it's format is compatible with what JAXBContext.newInstance(String) expects.

This means that if you annotate your package in package-info.java file with @IndexSubclasses, jaxb.index file will be generated automatically for you.

Javadoc storage

From version 2.0 @IndexAnnotated and @IndexSubclasses allow to specify storeJavadoc attribute. When set to true Javadoc comment for the indexed classes will be stored. You can retrieve first sentence of the Javadoc using ClassIndex.getClassSummary().

@IndexAnnotated(storeJavadoc = true)
public @interface Entity {
}

/**
 * This is car.
 * Detailed car description follows.
 */
@Entity
public class Car {
}

...

assertEquals("This is car", ClassIndex.getClassSummary(Car.class));

Class filtering

Filtering allows you to select only classes with desired characteristics. Here are some basic samples:

ClassFilter.only()
    .topLevel()
    .from(ClassIndex.getAnnotated(SomeAnnotation.class));
ClassFilter.only()
    .topLevel()
    .withModifiers(Modifier.PUBLIC)
    .from(ClassIndex.getAnnotated(SomeAnnotation.class));
ClassFilter.any(
    ClassFilter.only().topLevel(),
    ClassFilter.only().enclosedIn(WithInnerClassesInside.class)
).from(ClassIndex.getAnnotated(SomeAnnotation.class);

Indexing when annotations cannot be used

Sometimes you cannot easily use annotations to trigger compile time indexing because you don't control the source code of the classes which should be annotated. For instance you cannot add @IndexAnnotated meta-annotation to @Entity annotation. Although not so straightforward, it is still possible to use ClassIndex in this case.

There are two steps necessary:

First create a custom annotation processor by extending ClassIndexProcessor

public class MyImportantClassIndexProcessor extends ClassIndexProcessor {
    public MyImportantClassIndexProcessor() {
        indexAnnotations(Entity.class);
    }
}

In the constructor specify what indexes should be created by calling appropriate methods:

Finally register the processor by creating the file 'META-INF/services/javax.annotation.processing.Processor' in your classpath with the full class name of your processor, see the example here

Important note: you also need to ensure that your custom processor is always available on the classpath when compiling indexed classes. When that is not the case there will not be any error - those classes will be missing in the index.

Making shaded jar

During compilation ClassIndex writes index files. When creating a shaded jar those index files get overwritten by default. To not lost any indexed classes ClassIndex provides special transformer for Maven which merges the index files instead of overwriting them. To use it add the configuration below to your POM file:

<build>
    <plugins>
        <plugin>
            <artifactId>maven-shade-plugin</artifactId>
            <version>@maven-shade-plugin.version@</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <transformers>
                            <transformer implementation="org.atteo.classindex.ClassIndexTransformer"/>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
            <dependencies>
                <dependency>
                    <groupId>org.atteo.classindex</groupId>
                    <artifactId>classindex-transformer</artifactId>
                    <version>@class index version@</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

Eclipse

Eclipse uses its own Java compiler which is not strictly standard compliant and requires extra configuration. In Java Compiler -> Annotation Processing -> Factory Path you need to add ClassIndex jar file. See the screenshot.

License

ClassIndex is available under Apache License 2.0.

Download

You can download the library from here or use the following Maven dependency: