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

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

我有一个工作代码可以动态加载具有不同 Class 名称的不同 Class 实现。 class 文件被加载到 内存数据库 Apache Derby Db),并且 classloader 检索.class 文件来自 BLOB 列。

我想要做的是,插入 .class 文件作为带有版本列和 IS_ENABLED 标志的二进制 BLOB,然后 classloader 将在 运行 时间加载不同版本的 class。将有相同数量的已编译 class 版本的数据库条目,并且只有一个 class 且 IS_ENABLED 标志设置为 TRUE.

因为我尝试使用自定义 classloader 加载相同的 class 名称,所以出现以下异常;

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)

有两个不同的 .class 文件 (Greeter.class.v1, Greeter.class.v2)(在代码开头插入)相同的接口 (Greeter.java)

在测试代码的开头,class 文件从 lib/classes/ 文件夹中检索并作为 blob 二进制数据插入到内存数据库中,之后,.class 文件由数据库顺序检索并加载。加载具有相同名称的 class 时,发生异常。

我该如何解决这个问题?有没有办法卸载 class,或者重新加载同名的 class?

项目树

DerbyServerClassLoader.java - Class 装载机

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 - 欢迎界面

package com.levent.greeter;

public interface Greet {

    public String getGreetMessage();

}

DbSingleton.java - 实用程序 Class 连接 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 - 测试代码

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();
            }
        }
    }

}

输出

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)

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

没有办法强制1一个class卸载。除非卸载旧的 class,否则无法将新版本的 class 加载到同一个 class 加载器中。 (这是因为 class 的真实身份是一个元组,包含 class 的完全限定名称和 class 加载程序身份。)

解决方案是在新的 classloader 中加载新版本的 class。

尚不清楚这对您是否实用,但不幸的是,这是唯一可用的选项。 JVM 以一种您无法破坏它的方式执行 "duplicate class definition" 检查。该检查具有安全性和 JVM 稳定性影响。


1 - 未被任何可达对象引用的 class 最终将被 GC 卸载(取模 JVM 命令行选项、版本等)。但是,消除对 class 的所有引用可能很棘手。此外,反复强制 GC 运行 现在 对整体性能不利。

我认为问题可能出自您正在使用的 ClassLoader 父级。您没有重载 loadClass 方法,所以您是在父级 class 中委派,具体是在 ClassLoader.getSystemClassLoader().

正如 javadoc 在 https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html#getSystemClassLoader()

中所说

This method is first invoked early in the runtime's startup sequence, at which point it creates the system class loader and sets it as the context class loader of the invoking Thread.

您想加载更改后的 classes 但您将操作委托给 Thread ClassLoader,这有点令人困惑。

你可以做这样的事情,使用你自己的 Class ClassLoader

package a;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;

public class ReloadTest {

    public static void main(String[] args) throws ClassNotFoundException, IOException {

        final Class<?> clazz = ReloadTest.class;

        System.out.println("Class: " +  clazz.hashCode());

        final URL[] urls = new URL[1];

        urls[0] =  clazz.getProtectionDomain().getCodeSource().getLocation();
        final ClassLoader delegateParent = clazz.getClassLoader().getParent();

        try (final URLClassLoader cl = new URLClassLoader(urls, delegateParent)) {

            final Class<?> reloadedClazz = cl.loadClass(clazz.getName());
            System.out.println("Class reloaded: " + reloadedClazz.hashCode());
            System.out.println("Are the same: " + (clazz != reloadedClazz) );
        }
    }
}

希望有所帮助!

P.D: 这个link和同样的问题有关,可能对你也有帮助Reload used classes at runtime Java