Search code examples
javamavenclassloaderdynamic-class-loaders

ClassCastException on custom class loading from jar


I'm trying to implement custom class loader for educational purposes.

I have the module "Weather" in jar file which I want load from App class by JarClassLoader.

Classloader from here (it loads all classes from the specified jar):

package com.example.classloading;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class JarClassLoader extends ClassLoader {
    private HashMap<String, Class<?>> cache = new HashMap<String, Class<?>>();
    private String jarFileName;
    private String packageName;
    private static String WARNING = "Warning : No jar file found. Packet unmarshalling won't be possible. Please verify your classpath";

    public JarClassLoader(String jarFileName, String packageName) {
        this.jarFileName = jarFileName;
        this.packageName = packageName;

        cacheClasses();
    }

    private void cacheClasses() {
        try {
            JarFile jarFile = new JarFile(jarFileName);
            Enumeration entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) entries.nextElement();
                // simple class validation based on package name
                if (match(normalize(jarEntry.getName()), packageName)) {
                    byte[] classData = loadClassData(jarFile, jarEntry);
                    if (classData != null) {
                        Class<?> clazz = defineClass(stripClassName(normalize(jarEntry.getName())), classData, 0, classData.length);
                        cache.put(clazz.getName(), clazz);
                        System.out.println("== class " + clazz.getName() + " loaded in cache");
                    }
                }
            }
        }
        catch (IOException IOE) {
            System.out.println(WARNING);
        }
    }

    public synchronized Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> result = cache.get(name);
        if (result == null)
            result = cache.get(packageName + "." + name);
        if (result == null)
            result = super.findSystemClass(name);    
        System.out.println("== loadClass(" + name + ")");    
        return result;
    }

    private String stripClassName(String className) {
        return className.substring(0, className.length() - 6);
    }

    private String normalize(String className) {
        return className.replace('/', '.');
    }

    private boolean match(String className, String packageName) {
        return className.startsWith(packageName) && className.endsWith(".class");
    }

    private byte[] loadClassData(JarFile jarFile, JarEntry jarEntry) throws IOException {
        long size = jarEntry.getSize();
        if (size == -1 || size == 0)
            return null;

        byte[] data = new byte[(int)size];
        InputStream in = jarFile.getInputStream(jarEntry);
        in.read(data);

        return data;
    }
}

Interface and implementation (just template without any specific logic):

package com.example.classloading;   

public interface Module {
    public void demo(String str);
}    


package com.example.classloading;   

public class Weather implements Module {
    public void demo(String str) {
        System.out.println("hello from weather module");
    }
}

App class:

import com.example.classloading.JarClassLoader;
import com.example.classloading.Module;

public class App {
    public static void main(String[] args) {
        JarClassLoader jarClassLoader = new JarClassLoader("classloading/weather-module/target/weather-module-1.0-SNAPSHOT.jar", "com.example.classloading");
        try {
            Class<?> clas = jarClassLoader.loadClass("com.example.classloading.Weather");
            Module sample = (Module) clas.newInstance();
            sample.demo("1");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

    }
}

Problem: When I run the main method I get the following output:

== loadClass(java.lang.Object)
== class com.example.classloading.Module loaded in cache
== class com.example.classloading.Weather loaded in cache
== loadClass(com.example.classloading.Weather)
Exception in thread "main" java.lang.ClassCastException: com.example.classloading.Weather cannot be cast to com.example.classloading.Module
    at App.main(App.java:12)

Is there a problem in a logic or syntax? Module doesn't loaded by application class loader?


File tree (slightly simplified) :

├───classloading
│   │   pom.xml
│   │
│   ├───menu-module
│   │   │   pom.xml
│   │   │
│   │   ├───src
│   │   │   ├───main
│   │   │   │   ├───java
│   │   │   │   │   │   App.java
│   │   │   │   │   │
│   │   │   │   │   └───com
│   │   │   │   │       └───example
│   │   │   │   │           └───classloading
│   │   │   │   │                   JarClassLoader.java
│   │   │   │   │                   Module.java
│   │   │   │   │
│   │   │   │   └───resources
│   │   │   └───test
│   │   │       └───java
│   │   └───target
│   │       ├───classes
│   │       │   │   App.class
│   │       │   │
│   │       │   └───com
│   │       │       └───example
│   │       │           └───classloading
│   │       │                   JarClassLoader.class
│   │       │                   Module.class
│   │       │
│   │       └───generated-sources
│   │           └───annotations
│   └───weather-module
│       │   pom.xml
│       │   
│       ├───src
│       │   ├───main
│       │   │   ├───java
│       │   │   │   └───com
│       │   │   │       └───example
│       │   │   │           └───classloading
│       │   │   │                   Module.java
│       │   │   │                   Weather.java
│       │   │   │
│       │   │   └───resources
│       │   └───test
│       │       └───java
│       └───target
│           │   weather-module-1.0-SNAPSHOT.jar
│           │
│           ├───classes
│           │   │   Module.class
│           │   │   Weather.class
│           │   │
│           │   └───com
│           │       └───example
│           │           └───classloading
│           │                   Module.class
│           │                   Weather.class
│           │
│           ├───maven-archiver
│           │       pom.properties
│           │
│           └───maven-status
│               └───maven-compiler-plugin
│                   ├───compile
│                   │   └───default-compile
│                   │           createdFiles.lst
│                   │           inputFiles.lst
│                   │
│                   └───testCompile
│                       └───default-testCompile
│                               inputFiles.lst
│
└───

Update: I made the change in JarClassLoader cacheClasses()

if (match(normalize(jarEntry.getName()), packageName))

to

if (match(normalize(jarEntry.getName()), packageName) 
&& !normalize(jarEntry.getName()).contains("Module"))

It is workaround. How to do it in a right way?

Update: as I understand it is possible to delete Module interface from module Weather then "declare the "menu" module as a dependency for the weather module" @Costi Ciudatu.

Now I have following pom.xml files:

menu-module

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>classloading</artifactId>
        <groupId>java-tasks</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>menu</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.8.1</version>
        </dependency>
    </dependencies>

</project>

weather-module

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>java-tasks</groupId>
    <artifactId>weather-module</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>java-tasks</groupId>
            <artifactId>menu</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

classloading

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>java-tasks</groupId>
    <artifactId>classloading</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>weather-module</module>
        <module>menu-module</module>
    </modules>

</project>

Problem: I tried to package weather-module and got error:

[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building weather-module 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[WARNING] The POM for java-tasks:menu:jar:1.0-SNAPSHOT is missing, no dependency information available
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.471 s
[INFO] Finished at: 2017-04-07T09:15:38+03:00
[INFO] Final Memory: 8M/245M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project weather-module: Could not resolve dependencies for project java-tasks:weather-module:jar:1.0-SNAPSHOT: Could not find artifact java-tasks:menu:jar:1.0-SNAPSHOT -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException

How I should configure maven pom.xml files for correct working?


Solution

  • Your weather module should not contain a copy of the Module class. Otherwise, you end up with two copies of that class, which is the root cause of the ClassCastException.

    Make the weather module depend on the menu module or extract the Module class in a separate one. Bottomline, you should make sure you end up with a single version of Module in your classpath.