3

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 ?

Olivier.Roger
  • 4,241
  • 5
  • 40
  • 68
  • Usually a servlet based web server would unzip the war file and then you have them all in some subfolder. Wouldn't it be easier to work with the extracted files? – Claude Martin Dec 11 '15 at 15:24
  • I admit this is mostly curiosity that pushes me to search for such a solution. Extracting the files is indeed a way I know would work but since the purpose is purely to inspect the war and discover some configuration data. I would like this action to be fast (by filtering by package for instance since only package starting with my.company will potentially contains the classes I am looking for). In case the War file contains many third party dependencies, it would be a waste to extract, then delete them – Olivier.Roger Dec 11 '15 at 22:54
  • but isn't it already deployed? Then the files should be there. A "war" is just a zip file. – Claude Martin Dec 12 '15 at 22:41
  • Unfortunately my inspection process takes places before deployment. That is why the WAR file is not even in the classpath. I create a separate classloader to inpect it. I know a war file is zipped but the JAR URL is a java specificity. – Olivier.Roger Dec 12 '15 at 23:00

1 Answers1

2

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.

Sergei Tachenov
  • 24,345
  • 8
  • 57
  • 73
  • It works indeed, unfortunately the Reflections library apparently relies on URL classloaders only, so I had to write the search method myself which is probably less efficient but perfectly fits my need for now. Thank you – Olivier.Roger Dec 14 '15 at 12:56
  • I am not sure that your solution is realy very ugluy becose a found the same code in URLClassLoader class: String path = name.replace('.', '/').concat(".class"); : ) – usk-dima Feb 28 '20 at 11:47