Blog-Archiv

Sonntag, 18. April 2021

Java Class Loaders and Enum Values

Enum values are singletons just in the scope of their class-loader. A class-loader holds a set of classes that can work together. But an instance of class A, created by class-loader X, can not work together with an instance of class A created by class-loader Y, although both may have been compiled from exactly the same source and thus are identical.

This Blog introduces a custom class-loader and a test application that compares enum values belonging to different class loaders, showing that they are unique only in the scope of their class-loader.

JVMs, Applications, Class Loaders, Classes

A Java web- or application-server, which normally runs in one JVM (Java Virtual Machine), uses class-loaders to keep its loaded applications apart.

JVM
Application/ClassLoader X
Class A
Class B
Application/ClassLoader Y
Class A
Class B
JVM
Application/ClassLoader Z
Class A
Class B

That means application X can use the same class A as application Y, but at runtime, class A in application X will be different from class A in application Y. There will be a runtime exception when you try to cast an instance of class A created by X to an instance of class A created by Y. Mind that the class-loader instance makes the difference, not the class-loader type.

So, at runtime, this is reality:

JVM_1
Application/ClassLoader X_1
Class A_X_1
Class B_X_1
Application/ClassLoader Y_1
Class A_Y_1
Class B_Y_1
JVM_2
Application/ClassLoader Z_2
Class A_Z_2
Class B_Z_2

That sounds complicated, and it is. How can we try this out?

Custom Class Loader

The Java ClassLoader is abstract, so we need to derive it to have a loader that we can construct in our test. Follwing can be used for loading classes from resource-streams:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.*;

/**
 * Prefers to load classes as resources instead of delegating to parent loader.
 */
public class ResourceClassLoader extends ClassLoader
{
    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    	if (className.startsWith("java."))	// this gets called recursively for super-classes,
            return super.loadClass(className, resolve);	// and loading "java." classes is prohibited
    	
        final byte[] bytes = loadClassAsResource(className);
        return defineClass(className, bytes, 0, bytes.length);
    }

    private byte[] loadClassAsResource(String className)  {
    	final String filePath = className.replace('.', '/') + ".class";
    	
    	final InputStream inputStream = getResourceAsStream(filePath);
    	final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            int nextByte = 0;
            while ( (nextByte = inputStream.read()) != -1 ) {
                outputStream.write(nextByte);
            }
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
        return outputStream.toByteArray();
    }
}

This class-loader will try to load every class from its parent's resource stream. This is sufficient for our test.

The overridden loadClass() method on line 9 will be called recursively for every super-class of the class to be loaded, which is at least java.lang.Object because implicitly every class derives Object. This is the reason for the super.loadClass() call on line 11 that delegates to the parent loader.

Any other call will be delegated to loadClassAsResource() on line 17 that tries to read bytes from a resource stream given by the fully qualified class name. Theses bytes have then to go through defineClass() on line 14 which builds a Java class from them.

Let's try out if this works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    public static void main(String[] args) throws Exception {
        final ResourceClassLoader classLoader = new ResourceClassLoader();
        System.out.println(
                "Initial  "+ResourceClassLoader.class.getSimpleName()+
                " identityHashCode = "+System.identityHashCode(ResourceClassLoader.class));
        
        final Class<?> clazz = classLoader.loadClass(ResourceClassLoader.class.getName());
        System.out.println(
                "Reloaded "+clazz.getSimpleName()+
                " identityHashCode = "+System.identityHashCode(clazz));
        
        final ResourceClassLoader reloadedInstance = 
                (ResourceClassLoader) clazz.getDeclaredConstructor().newInstance();
    }

The method System.identityHashCode() can be used to display identities of classes and objects being in memory. It demonstrates that the original class and the reloaded class are not identical.

Anyway, the last statement on line 12 makes this application fail. Output is:

Initial  ResourceClassLoader identityHashCode = 237852351
Reloaded ResourceClassLoader identityHashCode = 1807837413

Exception in thread "main" java.lang.ClassCastException: 
  class fri.classloader.ResourceClassLoader cannot be cast to 
  class fri.classloader.ResourceClassLoader 
  (fri.classloader.ResourceClassLoader is in unnamed module of loader
     fri.classloader.ResourceClassLoader @3b22cdd0; 
   fri.classloader.ResourceClassLoader is in unnamed module of loader
     'app')

That means, the reloaded class is not compatible to the originally loaded class. Although their fully qualified class-names are the same, they belong to different class loaders.

Reloading an Enum Class

Here is an example enum that we want to reload:

public enum Seasons
{
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER,
    ;
}

Following test code compares the enum value SUMMER to a reloaded one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.lang.reflect.Field;

public class ClassLoaderTest
{
    public static void main(String[] args) throws Exception {
        System.out.println(
            "Original "+Seasons.class.getSimpleName()+
            " identityHashCode = "+System.identityHashCode(Seasons.class));
        
        final ClassLoader customClassLoader = new ResourceClassLoader();
        final Class<?> classLoadedEnum = 
            customClassLoader.loadClass(Seasons.class.getName());
        
        System.out.println(
            "Reloaded "+classLoadedEnum.getSimpleName()+
            " identityHashCode = "+System.identityHashCode(classLoadedEnum));
        
        Object summerReloaded = null;
        for (final Field field : classLoadedEnum.getFields())
            if (field.getName().equals(Seasons.SUMMER.name()))
                summerReloaded = field.get(classLoadedEnum);
        
        System.out.println(
            "(Original "+Seasons.SUMMER+" == reloaded "+summerReloaded+") is: "+
            (Seasons.SUMMER == summerReloaded));
    }
}

First we print the identity of the original SUMMER enum class on line 6.

On line 10, a new class-loader is created. It is used to reload the Seasons enum class, as done on line 11. The identity of the reloaded class is printed on line 14.

We can not cast the reloaded class to Seasons, this would yield an exception, see the ResourceClassLoader example above. So we have to use reflection to read the SUMMER field of the reloaded enum, as done from line 18 to 21.

The print statement on line 23 shows the following:

Original Seasons identityHashCode = 992136656
Reloaded Seasons identityHashCode = 48612937
(Original SUMMER == reloaded SUMMER) is: false

That means, not only the two enum classes are different, also their SUMMER values are not the same and can not be compared using the == identity operator.

Conclusion

It is not easy to understand theoretical statements about class loading and singleton behaviors when not having source code to try out how it works. It hope the provided examples are helpful.




Keine Kommentare: