简体   繁体   中英

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?

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.

The class Module is loaded by your main classloader of the App class. The Weather class is loaded by the JarClassLoader . By doing this the parent class Module is also (again) loaded by the JarClassLoader because the parent classloder is not used. So you end up in two similar but not equal class instances Module . As each class has a reference to its classloader they are different and therefore not compatible.

The main problem is you load all classes even those loaded by another classloader before. Try to cache only the classData in cacheClasses() and call defineClass() only if there is no findLoadedClass() in the parent classloader.

But this will not help completely as you have completly doubled dependencies in your classloaders. The behaviour will depend on the order the classes are loaded. To make this work you have to split your weather module.

The issue has to do with loading a duplicate class (or interface, in this case) from a JAR file. The Module classes are not compatible, being loaded from two different locations. In general, you shouldn't mix manually loading classes and import ing/automatically loading them within a single package.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM