Search code examples
javaeclipsereflectioneclipse-pluginclassloader

Reflections library not working when used in an Eclipse plug-in


I have developed an application using the Reflections library for querying all the classes having a particular annotation. Everything was working like a charm until I decided to create an Eclipse plug-in from my application. Then Reflections stop working.

Given that my application is working fine when not part of an Eclipse plug-in, I think it should be a class-loader problem. So I added to my Reflections class the classloaders of the plug-in activator class, the context class loader, and all other class loaders I could imagine, without any success. This is a simplified version of my code:

ConfigurationBuilder config = new ConfigurationBuilder();
config.addClassLoaders(thePluginActivatorClassLoader);
config.addClassLoaders(ClasspathHelper.getContextClassLoader());
config.addClassLoaders("all the classloaders I could imagine");
config.filterInputsBy(new FilterBuilder().include("package I want to analyze"));

Reflections reflections = new Reflections(config);
Set<Class<?>> classes = reflections.getTypesAnnotatedWith(MyAnnotation.class); //this Set is empty

I also tried adding URLs of the classes I want to load to the ConfigurationBuilder class, but it did not help.

Could someone tell me if there is a way to make Reflections work as part of an Eclipse plug-in ?, or should I better look for another alternative ?. Thanks a lot, I am really puzzled about it.


Solution

  • I assume you already know how to create bundles (otherwise, check this).

    After some debuging and exploration of Reflections API I have realised that the problem is that Reflections simply fails to read OSGi URLs (bundleresource://...) resulting in an exception:

    org.reflections.ReflectionsException: could not create Vfs.Dir from url, 
    no matching UrlType was found [bundleresource://1009.fwk651584550/]
    

    and this suggestion:

    either use fromURL(final URL url, final List<UrlType> urlTypes) 
    or use the static setDefaultURLTypes(final List<UrlType> urlTypes) 
    or addDefaultURLTypes(UrlType urlType) with your specialized UrlType.
    

    So I believe implementing a UrlType for OSGi (e.g. class BundleUrlType implements UrlType {...}) and registering it like this:

    Vfs.addDefaultURLTypes(new BundleUrlType());
    

    should make Reflections API usable from inside a bundle. Reflections dependencies should be added to the Eclipse Plugin project as described here.

    This is how my sample MANIFEST.MF looked like after adding needed jars:

    Manifest-Version: 1.0
    Bundle-ManifestVersion: 2
    Bundle-Name: ReflectivePlugin
    Bundle-SymbolicName: ReflectivePlugin
    Bundle-Version: 1.0.0.qualifier
    Bundle-Activator: reflectiveplugin.Activator
    Bundle-ActivationPolicy: lazy
    Bundle-RequiredExecutionEnvironment: JavaSE-1.6
    Import-Package: javax.annotation;version="1.0.0",
     org.osgi.framework;version="1.3.0",
     org.osgi.service.log;version="1.3",
     org.osgi.util.tracker;version="1.3.1"
    Bundle-ClassPath: .,
     lib/dom4j-1.6.1.jar,
     lib/guava-r08.jar,
     lib/javassist-3.12.1.GA.jar,
     lib/reflections-0.9.5.jar,
     lib/slf4j-api-1.6.1.jar,
     lib/xml-apis-1.0.b2.jar
    Export-Package: reflectiveplugin, 
     reflectiveplugin.data
    

    Note: Used Reflections v. 0.9.5

    Here's a sample UrlType implementation:

    package reflectiveplugin;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URL;
    import java.util.Enumeration;
    import java.util.Iterator;
    
    import org.osgi.framework.Bundle;
    import org.reflections.vfs.Vfs;
    import org.reflections.vfs.Vfs.Dir;
    import org.reflections.vfs.Vfs.File;
    import org.reflections.vfs.Vfs.UrlType;
    
    import com.google.common.collect.AbstractIterator;
    
    public class BundleUrlType implements UrlType {
    
        public static final String BUNDLE_PROTOCOL = "bundleresource";
    
        private final Bundle bundle;
    
        public BundleUrlType(Bundle bundle) {
            this.bundle = bundle;
        }
    
        @Override
        public boolean matches(URL url) {
            return BUNDLE_PROTOCOL.equals(url.getProtocol());
        }
    
        @Override
        public Dir createDir(URL url) {
            return new BundleDir(bundle, url);
        }
    
        public class BundleDir implements Dir {
    
            private String path;
            private final Bundle bundle;
    
            public BundleDir(Bundle bundle, URL url) {
                this(bundle, url.getPath());
            }
    
            public BundleDir(Bundle bundle, String p) {
                this.bundle = bundle;
                this.path = p;
                if (path.startsWith(BUNDLE_PROTOCOL + ":")) { 
                    path = path.substring((BUNDLE_PROTOCOL + ":").length()); 
                }
            }
    
            @Override
            public String getPath() {
                return path;
            }
    
            @Override
            public Iterable<File> getFiles() {
                return new Iterable<Vfs.File>() {
                    public Iterator<Vfs.File> iterator() {
                        return new AbstractIterator<Vfs.File>() {
                            final Enumeration<URL> entries = bundle.findEntries(path, "*.class", true);
    
                            protected Vfs.File computeNext() {
                                return entries.hasMoreElements() ? new BundleFile(BundleDir.this, entries.nextElement()) : endOfData();
                            }
                        };
                    }
                };
            }
    
            @Override
            public void close() { }
        }
    
        public class BundleFile implements File {
    
            private final BundleDir dir;
            private final String name;
            private final URL url;
    
            public BundleFile(BundleDir dir, URL url) {
                this.dir = dir;
                this.url = url;
                String path = url.getFile();
                this.name = path.substring(path.lastIndexOf("/") + 1);
            }
    
            @Override
            public String getName() {
                return name;
            }
    
            @Override
            public String getRelativePath() {
                return getFullPath().substring(dir.getPath().length());
            }
    
            @Override
            public String getFullPath() {
                return url.getFile();
            }
    
            @Override
            public InputStream openInputStream() throws IOException {
                return url.openStream();
            }
        }
    }
    

    And this is how I create reflections in the Activator class:

    private Reflections createReflections(Bundle bundle) {
        Vfs.addDefaultURLTypes(new BundleUrlType(bundle));
        Reflections reflections = new Reflections(new Object[] { "reflectiveplugin.data" });
        return reflections;
    }
    

    The last bit is very confusing, but still important: if you run your plugin inside of Eclipse (Run As / OSGi Framework) you have to add also your classes output directory to the Reflections path patterns (i.e. "bin" or "target/classes"). Although, it's not needed for a released plugin (to build a plugin/bundle do "Export"->"Deployable plug-ins and fragments").