Загрузка классов с использованием разных версий одного и того же класса: java.lang.LinkageError: попытка дублирования определения класса для имени
У меня есть рабочий код, который загружает динамически разные реализации классов с разными именами классов. Файлы классов загружаются в базу данных в памяти (Apache Derby Db), а загрузчик классов извлекает файл .class из столбцов BLOB.
Что я хочу сделать, это вставить файлы.class в виде двоичного BLOB с колонкой версии и IS_ENABLED
flags, тогда classloader загрузит класс для разных версий во время выполнения. Будет количество записей в базе данных того же количества версий скомпилированного класса, и будет только один класс с IS_ENABLED
флаг установлен в ИСТИНА.
Поскольку я пытаюсь загрузить то же имя класса с помощью пользовательского загрузчика классов, я получаю следующее исключение;
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-объектов в БД в памяти, после чего файлы.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 - интерфейс приветствия
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)
2 ответа
Думаю проблема станет от родителя ClassLoader
ты используешь. Вы не перегружаете метод loadClass, поэтому вы делегируете в родительский класс, конкретно в ClassLoader.getSystemClassLoader()
,
Как говорит Javadoc на https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html
Этот метод сначала вызывается в начале последовательности запуска среды выполнения, после чего он создает загрузчик класса системы и устанавливает его как загрузчик класса контекста вызывающего потока.
Вы хотите загрузить измененные классы, но делегируете 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) );
}
}
}
Надежда помогает!
PD: Эта ссылка связана с теми же проблемами, может ли она помочь. Перезагрузите используемые классы во время выполнения Java.
Как я могу решить эту проблему? Есть ли способ выгрузить класс или, в любом случае, перезагрузить класс с тем же именем?
Невозможно заставить 1 класс выгружаться. И если старый класс не выгружен, невозможно загрузить новую версию класса в тот же загрузчик классов. (Это потому, что истинная идентичность класса - это кортеж, содержащий полностью определенное имя класса и идентификатор загрузчика классов.)
Решение состоит в том, чтобы загрузить новую версию класса в новый загрузчик классов.
Не ясно, будет ли это практичным для вас, но, к сожалению, это единственный доступный вариант. JVM выполняет проверки "дублированного определения класса" таким образом, что вы не можете его подорвать. Проверка имеет последствия для безопасности и стабильности JVM.
1 - класс, на который не ссылается ни один достижимый объект, в конечном итоге будет выгружен GC (параметры командной строки JVM по модулю, версии и т. Д.). Однако может быть сложно исключить все ссылки на класс. И, кроме того, многократное принудительное выполнение GC сейчас плохо сказывается на общей производительности.