Search code examples
javawarreflections

Build classpath from JAR inside a WAR file


I am trying to dynamically extract configuration from a given WAR file. My goal is to find all class that implement some interface (Parameter).

The war file is not on the classpath, so I create a temporary classloader to inspect its classes.

URL webClasses = new URL("jar","","file:" + new File(path).getAbsolutePath() + "!/WEB-INF/classes/");
URLClassLoader cl = URLClassLoader.newInstance(new URL[] { webClasses });
Reflections reflections = new Reflections(ClasspathHelper.forClassLoader(cl), new SubTypesScanner(), cl);
Set<Class<? extends Parameter>> params = reflections.getSubTypesOf(Parameter.class);

This works fine. I find all the implementation of Parameter in my webapp.

Now, I would like to do the same for each jar present in WEB-INF/lib. Unfortunatelly I have not found a way to build the classloader in that case. The classloader seems to ignore jar URLs (jar:<url>!/WEB-INF/lib/{entry}.jar).

When I run the code below, there is no class found by Reflections:

Set<URL> libs = findLibJars(warUrl));
// The URLs are in the following format : {myWar}.war!/WEB-INF/lib/{myLib}.jar
URLClassLoader cl = URLClassLoader.newInstance(urls.toArray(new URL[urls.size()]));
cl.loadClass("my.company.config.AppParameter");

If possible, I would like to avoid having to extract WEB-INF/lib/*.jar from the WAR file and analyze them separately.

I am missing something here ? (other way to do it, other way to create the classloader, ... any lead would help). Maybe this can be done without using the Reflections library ?


Solution

  • It is certainly possible. Here is an ugly example of doing it:

    String warName = "wlaj.war";
    ClassLoader loader = new ClassLoader() {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // This probably needs fixing:
            String fileName = name.replace('.', '/') + ".class";
            try {
                try (ZipFile zf = new ZipFile(warName)) {
                    ZipEntry jar = zf.getEntry("WEB-INF/lib/jlaj.jar");
                    if (jar == null)
                        throw new ClassNotFoundException("No jlaj.jar");
                    try (ZipInputStream jarInput = new ZipInputStream(zf.getInputStream(jar))) {
                        for (ZipEntry cl; (cl = jarInput.getNextEntry()) != null; ) {
                            if (fileName.equals(cl.getName())) {
                                ByteArrayOutputStream data = new ByteArrayOutputStream();
                                byte[] buffer = new byte[4096];
                                for (int len; (len = jarInput.read(buffer)) != -1; ) {
                                    data.write(buffer, 0, len);
                                }
                                buffer = data.toByteArray();
                                return defineClass(name, buffer, 0, buffer.length);
                            }
                        }
                    }
                }
                throw new ClassNotFoundException();
            } catch (IOException ex) {
                throw new ClassNotFoundException("Error opening class file", ex);
            }
        }
    };
    loader.loadClass("jlaj.NewJFrame");
    

    The problems:

    1. Need an elegant foolproof way to convert a class name into the file path inside the JAR. In my case "jlaj.NewJFrame" -> "jlaj/NewJFrame.class", but it gets very ugly when trying to load inner classes and maybe in some other situations I can't even think about.
    2. Reading the class data should probably belong to some utility readAll method because it looks very messy.
    3. Obviously this needs a non-anonymous class with both WAR name and inner JAR name passed to the constructor. Or ZipFile and ZipEntry, if you want to iterate over those externally.

    On the positive side, it works.