6

I have an annotation @MyAnnotation and I can annotate any type (class) with it. Then I have a class called AnnotatedClassRegister and I would like it to register all classes annotated with @MyAnnotation so I can access them later. And I'd like to register these classes automatically upon creation of the AnnotatedClassRegister if possible, and most importantly before the annotated classes are instantiated.

I have AspectJ and Guice at my disposal. The only solution I came up with so far is to use Guice to inject a singleton instance of the AnnotatedClassRegister to an aspect, which searches for all classes annotated with @MyAnnotation and it adds the code needed to register such class in its constructor. The downside of this solution is that I need to instantiate every annotated class in order for the code added by AOP to be actually run, therefore I cannot utilize lazy instantiation of these classes.

Simplified pseudo-code example of my solution:

// This is the class where annotated types are registered
public class AnnotatedClassRegister {
    public void registerClass(Class<?> clz) {
        ...
    }
}

// This is the aspect which adds registration code to constructors of annotated
// classes
public aspect AutomaticRegistrationAspect {

    @Inject
    AnnotatedClassRegister register;

    pointcutWhichPicksConstructorsOfAnnotatedClasses(Object annotatedType) : 
            execution(/* Pointcut definition */) && args(this)

    after(Object annotatedType) :
            pointcutWhichPicksConstructorsOfAnnotatedClasses(annotatedType) {

        // registering the class of object whose constructor was picked 
        // by the pointcut
        register.registerClass(annotatedType.getClass())
    }
}

What approach should I use to address this problem? Is there any simple way to get all such annotated classes in classpath via reflection so I wouldn't need to use AOP at all? Or any other solution?

Any ideas are much appreciated, thanks!

5
  • Does it need to be runtime or can you make the list at compile time? Commented May 4, 2011 at 21:24
  • It does have to be at runtime because it's a framework feature. Users of the framework will use the annotation to annotate their classes and the framework should do all the dirty work to register them when the application is started. Commented May 5, 2011 at 6:07
  • Okay, but the assembly could still be done at compile time, as long as the framework knows how to find it at runtime, right? Commented May 5, 2011 at 7:20
  • Well, JPA implementations do it at runtime on annotated classes with @Entity and such. Do they use a common standard way to retrieve all annotated classes or do they check the entire classloader for them? Commented Feb 15, 2016 at 17:55
  • eventual alternative (for some future reader) not necessarily for this exact use case: create a service, see ServiceLoader (standard JDK); or a agent, see java.lang.instrument Commented Apr 20 at 8:14

6 Answers 6

7

It's possible:

  1. Get all paths in a classpath. Parse System.getProperties().getProperty("java.class.path", null) to get all paths.

  2. Use ClassLoader.getResources(path) to get all resources and check for classes. See the following code, originally found at DZone, archived at Archive.org:

/**
 * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
 *
 * @param packageName The base package
 * @return The classes
 * @throws ClassNotFoundException
 * @throws IOException
 */
private static Class[] getClasses(String packageName)
        throws ClassNotFoundException, IOException {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    assert classLoader != null;
    String path = packageName.replace('.', '/');
    Enumeration<URL> resources = classLoader.getResources(path);
    List<File> dirs = new ArrayList<File>();
    while (resources.hasMoreElements()) {
        URL resource = resources.nextElement();
        dirs.add(new File(resource.getFile()));
    }
    ArrayList<Class> classes = new ArrayList<Class>();
    for (File directory : dirs) {
        classes.addAll(findClasses(directory, packageName));
    }
    return classes.toArray(new Class[classes.size()]);
}

/**
 * Recursive method used to find all classes in a given directory and subdirs.
 *
 * @param directory   The base directory
 * @param packageName The package name for classes found inside the base directory
 * @return The classes
 * @throws ClassNotFoundException
 */
private static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
    List<Class> classes = new ArrayList<Class>();
    if (!directory.exists()) {
        return classes;
    }
    File[] files = directory.listFiles();
    for (File file : files) {
        if (file.isDirectory()) {
            assert !file.getName().contains(".");
            classes.addAll(findClasses(file, packageName + "." + file.getName()));
        } else if (file.getName().endsWith(".class")) {
            classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
        }
    }
    return classes;
}
Sign up to request clarification or add additional context in comments.

3 Comments

The code snippet you posted helped me and it seems like what I've been looking for! Thanks!
Broken link (May/2017)
Classes are available in the archived copy
4

It isn't simple that much is sure, but I'd do it in a Pure Java way:

  • Get your application's Jar location from the classpath
  • Create a JarFile object with this location, iterate over the entries
  • for every entry that ends with .class do a Class.forName() to get the Class object
  • read the annotation by reflection. If it's present, store the class in a List or Set

Aspects won't help you there, because aspects only work on code that's actually executed.

But annotation processing may be an Option, create a Processor that records all annotated classes and creates a class that provides a List of these classes

5 Comments

Thanks for Your answer! But what if I don't have a jar file? It's a web application running on Tomcat in exploded war directory. I'll look into annotation processing meanwhile..
@eQui in that case use Peter's answer, which is a slight variation of mine that will also work with exploded war files (or use annotation processing)
Annotation Processing looks like it would do what I want but it needs me to specify which classes to process and that I don't know until runtime.. I have found this nice article which explains how the processing can be run programatically from java code, but how can I process all classes in the web application's classpath?
@eQui but it needs me to specify which classes to process Nope. The only thing you need to know is which annotation to look for. You can't run annotation processing from the web container. apt works on sources, not compiled classes, it's part of the build process.
+1 for broadening my horizons because I didn't know that such a thing as annotation processing even existed. I would give another +1 for your explanations but I can't :) Anyway, I got it working nicely thanks to the code snippet @Peter has posted. Thank you both!
1

Well, if your AnnotatedClassRegister.registerClass() doesn't have to be called immediately at AnnotatedClassRegister creation time, but it could wait until a class is first instantiated, then I would consider using a Guice TypeListener, registered with a Matcher that checks if a class is annotated with @MyAnnotation.

That way, you don't need to search for all those classes, they will be registered just before being used. Note that this will work only for classes that get instantiated by Guice.

Comments

0

I would use the staticinitialization() pointcut in AspectJ and amend classes to your register as they are loaded, like so:

after() : staticinitialization(@MyAnnotation *) {
    register.registerClass(thisJoinPointStaticPart.getSignature().getDeclaringType());
}

Piece of cake, very simple and elegant.

Comments

0

You can use the ClassGraph package like so:

Java:

try (ScanResult scanResult = new ClassGraph().enableAnnotationInfo().scan()) {
  for (ClassInfo classInfo = scanResult.getClassesWithAnnotation(classOf[MyAnnotation].getName()) {
    System.out.println(String.format("classInfo = %s", classInfo.getName()));
  }
}

Scala:

Using(new ClassGraph().enableAnnotationInfo.scan) { scanResult =>
  for (classInfo <- scanResult.getClassesWithAnnotation(classOf[MyAnnotation].getName).asScala) {
    println(s"classInfo = ${classInfo.getName}")
  }
}

Comments

0

Essence of the Approach

My approach includes three steps:

Sample Classes with Annotations

Sample classes to demonstrate the approach

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Author(String name) {
}

import java.time.LocalDateTime;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Note(String title, Author author,
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "UTC") //
        @JsonSerialize(using = LocalDateTimeSerializer.class) //
        @JsonDeserialize(using = LocalDateTimeDeserializer.class) //
        LocalDateTime publicationDate, List<NoteTag> tags) {
}

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonFormat(shape = JsonFormat.Shape.STRING)
public enum NoteTag {
    @JsonProperty("ELASTICSEARCH")
    ELASTICSEARCH,
    @JsonProperty("JAVA")
    JAVA,
    @JsonProperty("JSON")
    JSON,
    @JsonProperty("REST")
    REST;
}

Methods Implementing the Approach

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

import com.fasterxml.jackson.databind.annotation.JsonNaming;

public class Runner {

    public static Package[] getDeclaredPackages() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        return classLoader.getDefinedPackages();
    }
    
    public static Collection<Class<?>> getPackageClasses(final String packageName)
            throws Exception {
        final StandardJavaFileManager fileManager = ToolProvider
                .getSystemJavaCompiler()
                .getStandardFileManager(null, null, null);
        return StreamSupport
                .stream(fileManager
                        .list(StandardLocation.CLASS_PATH, packageName,
                                Collections.singleton(
                                        JavaFileObject.Kind.CLASS),
                                false)
                        .spliterator(), false)
                .map(javaFileObject -> {
                    try {
                        final String canonicalClassName = javaFileObject
                                .getName().replaceAll("[\\\\/]", ".")
                                .replaceAll(
                                        ".*(" + packageName + ".*)\\.class.*",
                                        "$1");
                        return Class.forName(canonicalClassName);
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                }).collect(Collectors.toCollection(ArrayList::new));
    }
    
    public static void main(String[] args) throws Exception {
        Field[] fields = Note.class.getDeclaredFields();
        for (Field field : fields) {
            Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
            System.out.println(Arrays.toString(fieldAnnotations));
        }
        
        Package[] packages = getDeclaredPackages();
        Collection<Class<?>> classes = new LinkedList<>();
        for (Package packaje : packages) {
            Collection<Class<?>> packageClasses = getPackageClasses(packaje.getName());
            System.out.printf("Package '%s': %d class(es)\n", packaje.getName(),
                    packageClasses.size());
            classes.addAll(packageClasses);
        }
        System.out.println();
        classes.stream().filter(c -> (c.isAnnotationPresent(JsonNaming.class))).forEach(System.out::println);
    }
}

Printing field annotations in the first lines of the main method is only for using annotations in program. Note is a sample class (listed above)

Loop for (Package packaje : packages) counts classes in every package and add them to the collection

Project uses libraries:

jackson-annotations-2.16.0.jar
jackson-core-2.16.0.jar
jackson-databind-2.16.0.jar
jackson-datatype-jsr310-2.12.3.jar

Output at the Console

Package 'com.fasterxml.jackson.annotation': 73 class(es)
Package 'com.fasterxml.jackson.databind.deser': 46 class(es)
Package 'com.fasterxml.jackson.datatype.jsr310.deser': 15 class(es)
Package 'com.fasterxml.jackson.databind.jsonFormatVisitors': 27 class(es)
Package 'g0l0s.annotatedclassretriever.entities': 3 class(es)
Package 'com.fasterxml.jackson.databind.util': 59 class(es)
Package 'com.fasterxml.jackson.databind.ser.std': 76 class(es)
Package 'com.fasterxml.jackson.databind.ser': 25 class(es)
Package 'com.fasterxml.jackson.databind.jsonschema': 4 class(es)
Package 'com.fasterxml.jackson.datatype.jsr310.ser': 17 class(es)
Package 'g0l0s.annotatedclassretriever': 1 class(es)
Package 'com.fasterxml.jackson.databind.annotation': 17 class(es)
Package 'com.fasterxml.jackson.databind': 73 class(es)
Package 'com.fasterxml.jackson.databind.deser.std': 90 class(es)

Classes with an annotation:
class g0l0s.annotatedclassretriever.entities.Author
class g0l0s.annotatedclassretriever.entities.Note

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.