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?
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;
}
}
package com.levent.greeter;
public interface Greet {
public String getGreetMessage();
}
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;
}
}
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?
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.