Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Classloading Using Different Versions Of The Same Class : java.lang.LinkageError : attempted duplicate class definition for name

I have a working code that loads dynamically different Class Implementations with different Class Names. The class files are loaded into the in-memory database (Apache Derby Db), and classloader retrieve the .class file from the BLOB columns.

What I want to do is, inserting the .class files as binary BLOB with version column and IS_ENABLED flags, then classloader will load the class for different versions on run-time. There will be db entries same amount of the compiled class versions and there will only one class with IS_ENABLED flag set to TRUE.

Because that I try to load the same class name with custom classloader, I get the following Exception;

Exception in thread "main" java.lang.LinkageError: loader (instance of  com/levent/classloader/DerbyServerClassLoader): attempted  duplicate class definition for name: "com/levent/greeter/Greeter"
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at com.levent.classloader.DerbyServerClassLoader.findClass(DerbyServerClassLoader.java:38)
    at com.levent.example.ClientClassLoaderDBVersionDemo.main(ClientClassLoaderDBVersionDemo.java:43)

There are two different .class files (Greeter.class.v1, Greeter.class.v2) (inserted at the begining of the code) for the same Interface (Greeter.java)

In the beginning of test code, class files are retrieved from the lib/classes/ folder and inserted as blob binary data to the in-memory db, after then, .class files are sequentially retrieved by the database and loaded. While loading the class with same name, Exception occurs.

How can I solve this problem? Is there any way to unload a class, or else, anyway to reload a class with same name?

Project Tree

enter image description here

DerbyServerClassLoader.java - Class Loader

package com.levent.classloader;

import java.sql.Blob;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DerbyServerClassLoader extends ClassLoader {

    private ClassLoader parent;
    private String connectionString;

    public DerbyServerClassLoader(String connectionString) {
        this(ClassLoader.getSystemClassLoader(), connectionString);
    }

    public DerbyServerClassLoader(ClassLoader parent, String connectionString) {
        super(parent);
        this.parent = parent;
        this.connectionString = connectionString;
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        Class cls = null;

        try {
            cls = parent.loadClass(name);           // Delegate to the parent Class Loader
        } catch (ClassNotFoundException clnfE) {    // If parent fails, try to locate and load the class
            byte[] bytes = new byte[0];
            try {
                bytes = loadClassFromDatabase(name);
            } catch (SQLException sqlE) {
                throw new ClassNotFoundException("Unable to load class", sqlE);
            }
            return defineClass(name, bytes, 0, bytes.length);
        }

        return cls;
    }

    private byte[] loadClassFromDatabase(String name) throws SQLException {
        PreparedStatement pstmt = null;
        Connection connection = null;

        try {
            connection = DriverManager.getConnection(connectionString);

            String sql = "SELECT CLASS FROM CLASSES WHERE CLASS_NAME = ? AND IS_ENABLED = ?";
            pstmt = connection.prepareStatement(sql);
            pstmt.setString(1, name);
            pstmt.setBoolean(2, true);
            ResultSet rs = pstmt.executeQuery();

            if (rs.next()) {
                Blob blob = rs.getBlob(1);
                byte[] data = blob.getBytes(1, (int) blob.length());
                return data;
            }
        } catch (SQLException e) {
            System.out.println("Unexpected exception: " + e.toString());
        } catch (Exception e) {
            System.out.println("Unexpected exception: " + e.toString());
        } finally {
            if (pstmt != null) {
                pstmt.close();
            }

            if(connection != null) {
                connection.close();
            }
        }

        return null;
    }

}

Greet.java - Greeter Interface

package com.levent.greeter;

public interface Greet {

    public String getGreetMessage();

}

DbSingleton.java - Utility Class to connect DerbyDb

package com.levent.derbyutility;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DbSingleton {

    private static DbSingleton instance = null;

    private Connection conn = null;

    private DbSingleton() {
        try{
            DriverManager.registerDriver(new org.apache.derby.jdbc.EmbeddedDriver());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static DbSingleton getInstance() {
        if(instance == null) {
            synchronized(DbSingleton.class) {
                if(instance == null) {
                    instance = new DbSingleton();
                }
            }
        }

        return instance;
    }

    public Connection getConnection() throws SQLException {
        if(conn == null || conn.isClosed()) {
            synchronized (DbSingleton.class) {
                if(conn == null || conn.isClosed()) {
                    try{
                        //String dbUrl = "jdbc:derby://localhost:1527/myDB;create=true;user=me;password=mine";
                        String dbUrl = "jdbc:derby://localhost:1527/memory:myDB;create=true";

                        conn = DriverManager.getConnection(dbUrl);
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return conn;
    }

}

ClientClassLoaderDBVersionDemo.java - Test Code

package com.levent.example;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;

import com.levent.classloader.DerbyServerClassLoader;
import com.levent.derbyutility.DbSingleton;
import com.levent.greeter.Greet;

public class ClientClassLoaderDBVersionDemo {

    // apache derby in-memory db
    private static final String connectionString = "jdbc:derby://localhost:1527/memory:myDB;create=true";
    private static final String classFileName1 = "Greeter.class.v1";
    private static final String classFileName2 = "Greeter.class.v2";
    private static final String className = "com.levent.greeter.Greeter";

    public static void main(String[] args) {
        prepareClass();

        try {
            Greet greet = null;

            DerbyServerClassLoader cl = new DerbyServerClassLoader(connectionString);

            updateVersion(className, "v1");
            Class clazz1 = cl.findClass(className);
            greet = (Greet) clazz1.newInstance();
            System.out.println("Version 1 Greet.getGreetMessage() : " + greet.getGreetMessage());

            updateVersion(className, "v2");
            Class clazz2 = cl.findClass(className);
            greet = (Greet) clazz2.newInstance();
            System.out.println("Version 2 Greet.getGreetMessage() : " + greet.getGreetMessage());           
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private static void prepareClass() {
        DbSingleton instance = DbSingleton.getInstance();

        Connection conn = null;

        try {
            conn = instance.getConnection();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }

        Statement sta;

        if (conn != null) {
            try {
                sta = conn.createStatement();
                int count = sta
                        .executeUpdate("CREATE TABLE CLASSES (CLASS_NAME VARCHAR(50), CLASS BLOB, IS_ENABLED BOOLEAN, VERSION VARCHAR(10) )");
                System.out.println("CLASSES Table created");
                sta.close();

                sta = conn.createStatement();

                PreparedStatement psta = conn.prepareStatement("INSERT INTO CLASSES (CLASS_NAME, CLASS, IS_ENABLED, VERSION) values (?, ?, ?, ?)");
                byte[] bytes = null;
                InputStream blobObject = null;

                psta.setString(1, className);
                bytes = readJarFileAsByteArray(classFileName1);
                blobObject = new ByteArrayInputStream(bytes); 
                psta.setBlob(2, blobObject, bytes.length);
                psta.setBoolean(3, false);
                psta.setString(4, "v1");
                count = psta.executeUpdate();

                psta.setString(1, className);
                bytes = readJarFileAsByteArray(classFileName2);
                blobObject = new ByteArrayInputStream(bytes); 
                psta.setBlob(2, blobObject, bytes.length);
                psta.setBoolean(3, false);
                psta.setString(4, "v2");
                count += psta.executeUpdate();

                System.out.println(count + " record(s) created.");
                sta.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    private static byte[] readJarFileAsByteArray(String classFileName) {
        Path currentRelativePath = Paths.get("");
        String s = currentRelativePath.toAbsolutePath().toString();

        File file = new File(s + "/lib/classes/" + classFileName);
        byte[] fileData = new byte[(int) file.length()];
        DataInputStream dis;
        try {
            dis = new DataInputStream(new FileInputStream(file));
            dis.readFully(fileData);
            dis.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return fileData;
    }

    private static void updateVersion(String className, String version) {
        DbSingleton instance = DbSingleton.getInstance();

        Connection conn = null;

        try {
            conn = instance.getConnection();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }

        Statement sta;

        if (conn != null) {
            try {
                int count = 0;
                sta = conn.createStatement();
                PreparedStatement psta = conn.prepareStatement("UPDATE CLASSES SET IS_ENABLED = ? WHERE CLASS_NAME = ?");
                psta.setBoolean(1, false);
                psta.setString(2, className);
                count = psta.executeUpdate();
                System.out.println(count + " record(s) updated.");

                psta = conn.prepareStatement("UPDATE CLASSES SET IS_ENABLED = ? WHERE CLASS_NAME = ? AND VERSION = ?");

                psta.setBoolean(1, true);
                psta.setString(2, className);
                psta.setString(3, version);

                count = psta.executeUpdate();

                System.out.println(count + " record(s) updated.");
                sta.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

Output

CLASSES Table created
2 record(s) created.
2 record(s) updated.
1 record(s) updated.
Version 1 Greet.getGreetMessage() : Hail to the King Baby!
2 record(s) updated.
1 record(s) updated.
Exception in thread "main" java.lang.LinkageError: loader (instance of  com/levent/classloader/DerbyServerClassLoader): attempted  duplicate class definition for name: "com/levent/greeter/Greeter"
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at com.levent.classloader.DerbyServerClassLoader.findClass(DerbyServerClassLoader.java:38)
    at com.levent.example.ClientClassLoaderDBVersionDemo.main(ClientClassLoaderDBVersionDemo.java:43)
like image 406
Levent Divilioglu Avatar asked Dec 22 '15 11:12

Levent Divilioglu


1 Answers

How can I solve this problem? Is there any way to unload a class, or else, anyway to reload a class with same name?

There is no way to force1 a class to unload. And unless the old class is unloaded, there is no way to load a new version of a class into the same classloader. (That is because the true identity of a class is a tuple comprising the fully qualified name of the class and the class loader identity.)

The solution is to load the new version of the class in a new classloader.

It is not clear if this will be practical for you, but unfortunately it is the only option available. The JVM does the "duplicate class definition" checks in a way that you can't subvert it. The check has security and JVM stability implications.


1 - A class that is not referenced by any reachable object will eventually be unloaded by the GC (modulo JVM command line options, versions, etc). However, it can be tricky to eliminate all references to a class. And besides, repeatedly forcing the GC to run now is bad for overall performance.

like image 184
Stephen C Avatar answered Sep 20 '22 07:09

Stephen C