I have a project that is generating a .war
file as a build artifact.
I want to load this .war
into the JVM so that I can parse the runtime annotations it contains, but I am having trouble with loading the classes.
I have the following snippet of code to fetch class names from a JarFile
:
public static Set<String> getClassNamesFromJarFile(JarFile jarFile) throws IOException {
Set<String> classNames = new HashSet<>();
Enumeration<JarEntry> e = jarFile.entries();
while (e.hasMoreElements()) {
JarEntry jarEntry = e.nextElement();
if (jarEntry.getName().endsWith(".class")) {
String className = jarEntry.getName()
.replace("/", ".");
className = className.substring(0, className.length() - 6);
classNames.add(className);
}
}
return classNames;
}
And I use this snippet for getting the JarFile
:
protected static JarFile getJarFile(String jarFileUrl) throws IOException {
try {
return new JarFile(new URI(jarFileUrl.replaceAll(" ", " ")).getSchemeSpecificPart());
} catch (URISyntaxException ex) {
return new JarFile(jarFileUrl.substring("file:".length()));
}
}
Then, on my main, I have the following code:
public static void main(String[] args) throws IOException {
String warpath = "C:\\User\\Me\\Desktop\\someWarFile.war";
// Fetch JarFile
JarFile jar = getJarFile("file:" warpath);
// Load .war into the JVM
URL url = new File(warpath).toURI().toURL();
URLClassLoader child = new URLClassLoader(
new URL[]{url},
Object.class.getClassLoader()
);
// Get class names in the .war
Set<String> classes = getClassNamesFromJarFile(jar);
// Try to get classes from name
for (String className : classes) {
try {
System.out.println("CLASS FOUND: " className);
Class<?> actualClass= Class.forName(className, true, child);
} catch(Error | Exception e) {
System.out.println("ERROR: " e);
}
}
}
For every class I get the output:
...
CLASS FOUND: WEB-INF.classes.com.some.company.project.state.ProjectStateProvider$1
ERROR: java.lang.NoClassDefFoundError: com/some/company/project/state/ProjectStateProvider$1 (wrong name: WEB-INF/classes/com/some/company/project/state/ProjectStateProvider$1)
CLASS FOUND: WEB-INF.classes.com.some.company.configuration.rest.JsonProvider
ERROR: java.lang.NoClassDefFoundError: com/some/company/configuration/rest/JsonProvider (wrong name: WEB-INF/classes/com/some/company/configuration/rest/JsonProvider)
...
I have tried removing the WEB-INF.
and the WEB-INF.classes.
prefixes from the className, I have also tried adding .class
to the className and every combination of the previous solutions, but I just get a java.lang.ClassNotFoundException
.
How do I reference classes loaded from the .war
like the WEB-INF.classes.com.some.company.project.state.ProjectStateProvider$1
class for example?
CodePudding user response:
I'm using this code, and it works fine:
private void reloadJar(final File pJarFile) throws InstantiationException, IllegalAccessException, MalformedURLException, IOException {
try (final URLClassLoader classLoader = new URLClassLoader(new URL[] { pJarFile.toURI().toURL() }); //
final JarFile file = new JarFile(pJarFile);) {
final Plugin plugin = new Plugin(pJarFile, classLoader);
final Enumeration<JarEntry> entries = file.entries();
while (entries.hasMoreElements()) {
final JarEntry jarEntry = entries.nextElement();
if (!isClassFile(jarEntry)) {
plugin.addNonClassfile(jarEntry);
continue;
}
try {
final String id = getClassName(jarEntry);
final Class<?> cls = classLoader.loadClass(id);
if (JcUClass.isSubclassOf(cls, KittenPluginIf.class)) {
final KittenPluginIf inst = (KittenPluginIf) cls.newInstance();
plugin.setPlugin(inst);
} else if (JcUClass.isSubclassOf(cls, KittenPageIf.class)) {
final KittenPageIf inst = (KittenPageIf) cls.newInstance();
plugin.addPage(inst);
} else {
plugin.addUtilityClass(jarEntry);
}
} catch (final ClassNotFoundException e2) {
plugin.addClassNotFoundEntry(jarEntry);
}
}
if (plugin.isValid()) mPlugins.add(plugin);
}
}
static private boolean isClassFile(final ZipEntry entry) {
return !entry.isDirectory() && entry.getName().endsWith(".class");
}
static private String getClassName(final ZipEntry entry) {
final String className = entry.getName().replace('/', '.'); // including ".class"
return className.substring(0, className.length() - ".class".length());
}
You have to remove some code that contains some custom classes of mine, but this outlines the procedure easily.
The other thing that you have to be aware of: those classes in the .jar or .war file have to be normal javac compiled classes, or any other builder that outputs default java class binaries, like EclipseBuild.
CodePudding user response:
OK. I've changed the getClassNamesFromJarFile
method slightly:
public static Set<String> getClassNamesFromJarFile(
JarFile file, String pfx, String sfx) {
Set<String> result = new HashSet<>();
Enumeration<JarEntry> enm = file.entries();
while (enm.hasMoreElements()) {
JarEntry e = enm.nextElement();
String path = e.getName();
if (path.startsWith(pfx) && path.endsWith(sfx)) {
path = path.substring(pfx.length(), path.length()-sfx.length());
path = path.replace('/', '.');
result.add(path);
}
}
return result;
}
Then:
public static void main(String[] args) throws IOException {
String warpath = "C:\\User\\Me\\Desktop\\someWarFile.war";
// Fetch JarFile
JarFile jar = new JarFile(warpath);
// Load .war into the JVM
URL url = new URL("jar:file:" warpath "!/WEB-INF/classes/");
URLClassLoader child = new URLClassLoader(
new URL[]{url},
Object.class.getClassLoader()
);
// Get class names in the .war
Set<String> classes = getClassNamesFromJarFile(jar, "WEB-INF/classes/", ".class");
// Try to get classes from name
for (String className : classes) {
try {
System.out.println("CLASS FOUND: " className);
Class<?> actualClass= Class.forName(className, true, child);
} catch(Error | Exception e) {
System.out.println("ERROR: " e);
}
}
}
UPDATE:
Now the classes will not load if they depend on other classes not in the classpath. For instance, if the war contains jar files in WEB-INF/lib, you need to add them to the URLClassLoader
. You should also make available the jar of the servlet API.