การโหลดคลาสโดยใช้เวอร์ชันที่แตกต่างกันของคลาสเดียวกัน: java.lang.LinkageError: พยายามนิยามคลาสที่ซ้ำกันสำหรับชื่อ

ฉันมีรหัสการทำงานที่โหลดการใช้งานคลาสที่แตกต่างกันแบบไดนามิกด้วยชื่อคลาสที่แตกต่างกัน ไฟล์คลาสถูกโหลดลงใน ฐานข้อมูลในหน่วยความจำ (Apache Derby Db) และ classloader จะดึงไฟล์ .class จาก คอลัมน์ BLOB

สิ่งที่ฉันต้องการทำคือการแทรกไฟล์ .class เป็น BLOB ไบนารี่ที่มีคอลัมน์เวอร์ชันและแฟล็ก IS_ENABLED จากนั้น classloader จะโหลดคลาสสำหรับเวอร์ชันต่างๆ ในขณะรันไทม์ จะมีรายการ db ในจำนวนเท่ากันของเวอร์ชันคลาสที่คอมไพล์แล้ว และจะมีเพียงคลาสเดียวเท่านั้นที่ตั้งค่าสถานะ IS_ENABLED เป็น TRUE

เนื่องจากฉันพยายามโหลดชื่อคลาสเดียวกันกับ classloader แบบกำหนดเอง ฉันจึงได้รับข้อยกเว้นต่อไปนี้

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)

ในช่วงเริ่มต้นของโค้ดทดสอบ ไฟล์คลาสจะถูกดึงมาจากโฟลเดอร์ lib/classes/ และแทรกเป็นข้อมูลไบนารี blob ไปยัง db ในหน่วยความจำ หลังจากนั้นไฟล์ .class จะถูกดึงตามลำดับโดยฐานข้อมูลและโหลด ขณะโหลดคลาสด้วยชื่อเดียวกัน จะมีข้อยกเว้นเกิดขึ้น

ฉันจะแก้ไขปัญหานี้ได้อย่างไร? มีวิธีใดที่จะยกเลิกการโหลดคลาสหรือโหลดคลาสที่มีชื่อเดียวกันซ้ำหรือไม่?

ต้นไม้โครงการ

ป้อนคำอธิบายรูปภาพที่นี่

DerbyServerClassLoader.java - ตัวโหลดคลาส

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

package com.levent.greeter;

public interface Greet {

    public String getGreetMessage();

}

DbSingleton.java - คลาสยูทิลิตี้สำหรับเชื่อมต่อ 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)

person Levent Divilioglu    schedule 22.12.2015    source แหล่งที่มา
comment
คุณได้ดูที่ docs.oracle.com/javase/6/docs/api/java/net/   -  person Sheetal Mohan Sharma    schedule 22.12.2015


คำตอบ (2)


ฉันคิดว่าปัญหาน่าจะมาจากผู้ปกครอง ClassLoader ที่คุณใช้อยู่ คุณไม่ได้ใช้งานเมธอด loadClass มากเกินไป ดังนั้นคุณจึงมอบหมายในคลาสพาเรนต์อย่างเป็นรูปธรรมใน ClassLoader.getSystemClassLoader()

ดังที่ javadoc พูดไว้ที่ https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html#getSystemClassLoader()

วิธีการนี้ถูกเรียกใช้ครั้งแรกในช่วงต้นของลำดับการเริ่มต้นของรันไทม์ ซึ่ง ณ จุดนี้จะสร้างตัวโหลดคลาสระบบและตั้งค่าเป็นตัวโหลดคลาสบริบทของเธรดที่เรียกใช้

คุณต้องการโหลดคลาสที่เปลี่ยนแปลง แต่คุณกำลังมอบหมายให้กับการดำเนินการ Thread ClassLoader ทำให้เกิดความสับสนเล็กน้อย

คุณสามารถทำสิ่งนี้ได้โดยใช้คลาสของคุณเอง 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: ลิงก์นี้เกี่ยวข้องกับปัญหาเดียวกัน อาจช่วยได้เช่นกัน โหลดคลาสที่ใช้ซ้ำเมื่อรันไทม์ ชวา

person Francisco Hernandez    schedule 22.12.2015

ฉันจะแก้ไขปัญหานี้ได้อย่างไร? มีวิธีใดที่จะยกเลิกการโหลดคลาสหรือโหลดคลาสที่มีชื่อเดียวกันซ้ำหรือไม่?

ไม่มีวิธีบังคับให้1 คลาสยกเลิกการโหลด และเว้นแต่คลาสเก่าจะถูกยกเลิกการโหลด จะไม่มีทางโหลดคลาสเวอร์ชันใหม่ลงในคลาสโหลดเดอร์เดียวกันได้ (นั่นเป็นเพราะตัวตนที่แท้จริงของคลาสคือสิ่งทูเพิลที่ประกอบด้วยชื่อที่ผ่านการรับรองโดยสมบูรณ์ของคลาสและข้อมูลระบุตัวตนของตัวโหลดคลาส)

วิธีแก้ไขคือโหลดเวอร์ชันใหม่ของคลาสใน classloader ใหม่

ยังไม่ชัดเจนว่าสิ่งนี้จะเป็นประโยชน์สำหรับคุณหรือไม่ แต่น่าเสียดายที่มันเป็นเพียงตัวเลือกเดียวเท่านั้น JVM ทำการตรวจสอบ "คำจำกัดความคลาสที่ซ้ำกัน" ในลักษณะที่คุณไม่สามารถล้มล้างได้ การตรวจสอบมีผลกระทบด้านความปลอดภัยและความเสถียรของ JVM


1 - คลาสที่ไม่ได้อ้างอิงโดยออบเจ็กต์ที่เข้าถึงได้ใดๆ จะถูกยกเลิกการโหลดโดย GC ในที่สุด (ตัวเลือกบรรทัดคำสั่ง modulo JVM เวอร์ชัน ฯลฯ) อย่างไรก็ตาม การลบการอ้างอิงถึงคลาสทั้งหมดออกอาจเป็นเรื่องยุ่งยาก นอกจากนี้ การบังคับให้ GC ทำงานตอนนี้ซ้ำๆ ส่งผลเสียต่อประสิทธิภาพโดยรวม

person Stephen C    schedule 22.12.2015