Razor
Razor

Reputation: 190

Class<?>.isAnnotationPresent returns false for annotated class

I have my own annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
}

And class:

package com.ltp.analog.test;

import com.ltp.analog.core.annotation.Component;

@Component
public class TestOne {

    public void test(){
        System.out.println("TEST ONE");
    }

}

Also implemented custom ClassLoader:


package com.ltp.analog.reflection;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class AnalogClassLoader extends ClassLoader{

    @Override
    public Class findClass(String name) {
        Class cl = findLoadedClass(name);

        if(cl != null){
            return cl;
        }

        byte[] b = loadClassFromFile(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassFromFile(String fileName)  {
        InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(
                fileName.replaceAll("[.]", "/") + ".class");
        byte[] buffer;
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        int nextValue = 0;
        try {
            while ( (nextValue = inputStream.read()) != -1 ) {
                byteStream.write(nextValue);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        buffer = byteStream.toByteArray();
        return buffer;
    }

}

And ReflectionUtils class which have to load all the classes in package recursively:

package com.ltp.analog.reflection;

import com.ltp.analog.Testing;
import com.ltp.analog.reflection.qualifier.ClassQualifier;

import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

public class ReflectionUtils {

    private static final AnalogClassLoader cl = new AnalogClassLoader();

    public static List<Class> getClassesInPackageRecursively(String packName){
        List<String> packs = getSubpackagesRecursively(packName);

        List<Class> result = new LinkedList<>();

        packs.forEach(pack -> result.addAll(getClassesInPackage(pack)));

        return result.stream().distinct().collect(Collectors.toList());
    }

    public static List<Class> getClassesInPackage(String packName){
        if(packName == null || packName.isEmpty()){
            return List.of();
        }

        URL url = ClassLoader.getSystemClassLoader().getResource(packName.replaceAll("[.]", "/"));

        if(url == null){
            return List.of();
        }

        File pack = new File(url.getPath());

        if(!pack.isDirectory() || pack.listFiles() == null){
            return List.of();
        }

        return Arrays.stream(pack.listFiles())
                .filter(File::isFile)
                .filter(f -> f.getName().endsWith(".class"))
                .map(f -> cl.findClass(packName + "." + f.getName().substring(0, f.getName().indexOf('.'))))
                .collect(Collectors.toList());
    }

    public static List<String> getSubpackagesRecursively(String packName){
        List<String> result = new LinkedList<>();

        for(String pack : getSubpackages(packName)){
            List<String> subPacks = getSubpackagesRecursively(pack);
            result.addAll(subPacks);
        }

        result.add(packName);

        return result.stream().distinct().collect(Collectors.toList());
    }

    public static List<String> getSubpackages(String packName){
        if(packName == null || packName.isEmpty()){
            return List.of();
        }

        URL url = ClassLoader.getSystemClassLoader().getResource(packName.replaceAll("[.]", "/"));

        if(url == null){
            return List.of();
        }

        File pack = new File(url.getPath());

        if(!pack.isDirectory() || pack.listFiles() == null){
            return List.of();
        }

        return Arrays.stream(pack.listFiles())
                .filter(File::isDirectory)
                .map(f -> packName + "." + f.getName())
                .collect(Collectors.toList());
    }

    private ReflectionUtils(){}

}

The problem is that after loading all the classes in the passed package I'm trying to filter them and get only annotated by @Component, but the result is weird:

someClass.isAnnotationPresent(Component.class) returns false, even when there is a @Component annotation in someClass.getDeclaredAnnotations()

Sample:

List<Class> componentClasses = new LinkedList<>();
        scans.forEach(s -> componentClasses.addAll(ReflectionUtils.getClassesInPackageRecursively(s)));
        System.out.printf("Classes: %d", componentClasses.size());
        componentClasses.forEach(c -> {
            System.out.println("-".repeat(50));
            System.out.println(Arrays.stream(c.getAnnotations()).map(Annotation::toString).collect(Collectors.joining(", ")));
            System.out.printf("%s -> %s\n", c.getName(), c.isAnnotationPresent(Component.class));
        });

The output:

...
@com.ltp.analog.core.annotation.Component()
com.ltp.analog.test.TestOne -> false
...

Upvotes: 0

Views: 133

Answers (1)

rzwitserloot
rzwitserloot

Reputation: 103244

The problem is classloader shenanigans, no doubt.

You have 2 separate classes coincidentally both named com.ltp.analog.core.annotation.Component. They aren't the same thing even though they have the same name.

Come again? Yes, really.

Imagine this code:

java.lang.String x = (java.lang.String) obj;

and when you run that code, you get this error:

Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.String

You'd go: Whaaaa? My JVM is broken!

But no, there is an explanation. And the same thing is happening to you: There are 2 different classes that are nevertheless both identically named: They are both named com.ltp.analog.core.annotation.Component. When you call c.getAnnotations(), you get an instance back that is from the one 'take' on this class. Its toString still gives you com.ltp.analog.core.annotation.Component. When you call isAnnotationPresent, your Component.class variable is the other 'take'. Thus, the answer is no, for the same reason .isAnnotationPresent(Override.class) returns false: Not the same class.

You get here when you use classloaders and you override loadClass (you should never be able to get into this situation if you only override findClass).

Any class is actually defined by both of these:

  • Its fully qualified name
  • Its loader

Its loader is defined by what thatClassInstance.getClassLoader() returns, and it is whichever classloader actually invoked .defineClass(byteArrayWithByteCodeInIt).

Thus, you loaded Component once in one loader and then again in another; the one that you get with c.getAnnotations() is loaded by the one loader, and the class that the code you pasted is in loaded another.

The solution is more general: You need to be really really careful when you override loadClass. You can't use any type 'across loaders' unless that type was loaded by a common loader.

Thus, the fix:

Fix AnalogClassLoader - it needs to properly ask its parent loader to load Component and not do it itself.

YOu didn't paste the code of AnalogClassLOader, so I can't give more tips on how to accomplish this. More generally, don't write your own classloaders unless you fully understand them, they are very tricky beasts.

Upvotes: 3

Related Questions