Sistema de ClassLoader¶
El sistema de ClassLoader de Hytale es fundamental para el funcionamiento de los early plugins, permitiendo la transformación de bytecode en tiempo de carga de clases.
¿Qué es un ClassLoader?¶
Un ClassLoader es un componente de la JVM (Java Virtual Machine) responsable de:
- Localizar archivos de clases (.class)
- Leer el bytecode de las clases
- Verificar que el bytecode es válido
- Definir la clase en la JVM
- Resolver referencias a otras clases
Jerarquía de ClassLoaders en Hytale¶
graph TD
A[Bootstrap ClassLoader] --> B[Platform ClassLoader]
B --> C[Application ClassLoader]
C --> D[URLClassLoader Plugin]
C --> E[TransformingClassLoader]
style A fill:#f9f,stroke:#333
style E fill:#9f9,stroke:#333
style D fill:#99f,stroke:#333
1. Bootstrap ClassLoader¶
- Carga clases del core de Java (
java.lang.*,java.util.*, etc.) - Implementado en código nativo (C/C++)
- No puede ser modificado
2. Platform ClassLoader¶
- Carga clases de plataforma Java
- Extensiones y módulos del JDK
3. Application ClassLoader¶
- Carga clases del classpath de la aplicación
- Incluye
hytale-server.jary dependencias
4. URLClassLoader (Plugins)¶
Ubicación: EarlyPluginLoader.java:42
pluginClassLoader = new URLClassLoader(
urls.toArray(new URL[0]),
EarlyPluginLoader.class.getClassLoader()
);
- Carga JARs de plugins desde
earlyplugins/ - Permite aislar clases de plugins
- Padre: Application ClassLoader
5. TransformingClassLoader¶
El ClassLoader personalizado que aplica transformaciones.
public class TransformingClassLoader extends ClassLoader {
private final ClassLoader parent;
private final List<ClassTransformer> transformers;
public TransformingClassLoader(ClassLoader parent,
List<ClassTransformer> transformers) {
super(parent);
this.parent = parent;
this.transformers = transformers;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. Verificar si ya está cargada
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2. Delegar a clases del sistema
if (shouldDelegateToParent(name)) {
return parent.loadClass(name);
}
// 3. Cargar y transformar
try {
byte[] bytecode = readClassBytes(name);
bytecode = applyTransformers(name, bytecode);
c = defineClass(name, bytecode, 0, bytecode.length);
if (resolve) {
resolveClass(c);
}
return c;
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
private boolean shouldDelegateToParent(String name) {
// Delegar clases del sistema
return name.startsWith("java.") ||
name.startsWith("javax.") ||
name.startsWith("sun.") ||
name.startsWith("jdk.");
}
private byte[] applyTransformers(String name, byte[] bytecode) {
String classPath = name.replace('.', '/');
for (ClassTransformer transformer : transformers) {
byte[] transformed = transformer.transform(
name,
classPath,
bytecode
);
if (transformed != null) {
bytecode = transformed;
}
}
return bytecode;
}
}
Proceso de Carga de Clases¶
sequenceDiagram
participant App as Aplicación
participant TCL as TransformingClassLoader
participant Cache as Class Cache
participant Parent as Parent ClassLoader
participant FS as FileSystem
participant T1 as Transformer 1
participant T2 as Transformer 2
participant JVM as JVM
App->>TCL: loadClass("com.hypixel.hytale.Server")
TCL->>Cache: findLoadedClass()
alt Clase ya cargada
Cache-->>TCL: Clase existente
TCL-->>App: Retornar clase
else Clase no cargada
TCL->>TCL: shouldDelegateToParent()?
alt Es clase del sistema
TCL->>Parent: loadClass()
Parent-->>TCL: Clase del sistema
TCL-->>App: Retornar clase
else Es clase transformable
TCL->>FS: readClassBytes()
FS-->>TCL: bytecode original
TCL->>T1: transform(name, bytecode)
T1-->>TCL: bytecode transformado (o null)
TCL->>T2: transform(name, bytecode)
T2-->>TCL: bytecode transformado (o null)
TCL->>JVM: defineClass()
JVM->>JVM: Verificar bytecode
JVM-->>TCL: Clase definida
TCL->>Cache: Guardar clase
TCL-->>App: Retornar clase
end
end
Delegación de ClassLoader¶
Modelo de Delegación Padre-Primero¶
Java usa un modelo de delegación padre-primero por defecto:
- Verificar si la clase ya está cargada
- Delegar al padre para cargar
- Si el padre no puede, cargar localmente
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. Verificar cache
Class<?> c = findLoadedClass(name);
if (c != null) return c;
// 2. Delegar al padre (PADRE-PRIMERO)
try {
c = parent.loadClass(name);
return c;
} catch (ClassNotFoundException e) {
// Continuar...
}
// 3. Cargar localmente
c = findClass(name);
return c;
}
Modelo Hijo-Primero (Personalizado)¶
Para plugins, a veces queremos hijo-primero:
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. Verificar cache
Class<?> c = findLoadedClass(name);
if (c != null) return c;
// 2. Cargar localmente primero (HIJO-PRIMERO)
if (shouldLoadLocally(name)) {
try {
c = findClass(name);
return c;
} catch (ClassNotFoundException e) {
// Continuar...
}
}
// 3. Delegar al padre
c = parent.loadClass(name);
return c;
}
Aislamiento de Clases¶
El aislamiento de clases previene conflictos entre versiones de bibliotecas.
Problema: Conflicto de Versiones¶
Sin aislamiento, solo una versión se carga (la primera).
Solución: ClassLoader Separados¶
Application ClassLoader
├── hytale-server.jar
├── LibraryA-1.0.jar
└── URLClassLoader (Plugin)
├── plugin.jar
└── LibraryA-2.0.jar
Cada ClassLoader mantiene su propia versión.
Implementación¶
public class IsolatedClassLoader extends URLClassLoader {
private final Set<String> isolatedPackages;
public IsolatedClassLoader(URL[] urls,
ClassLoader parent,
Set<String> isolatedPackages) {
super(urls, parent);
this.isolatedPackages = isolatedPackages;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// Verificar si ya está cargada
Class<?> c = findLoadedClass(name);
if (c != null) return c;
// Si está en paquetes aislados, cargar localmente
for (String pkg : isolatedPackages) {
if (name.startsWith(pkg)) {
c = findClass(name);
if (resolve) resolveClass(c);
return c;
}
}
// De lo contrario, delegar al padre
return super.loadClass(name, resolve);
}
}
}
Carga Dinámica de Clases¶
Cargar Clase por Nombre¶
// Obtener el ClassLoader actual
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// Cargar clase
Class<?> clazz = cl.loadClass("com.ejemplo.MiClase");
// Crear instancia
Object instance = clazz.getDeclaredConstructor().newInstance();
Cargar desde JAR¶
// Crear URLClassLoader para el JAR
URL jarUrl = new File("plugin.jar").toURI().toURL();
URLClassLoader loader = new URLClassLoader(new URL[]{jarUrl});
// Cargar clase del JAR
Class<?> clazz = loader.loadClass("com.plugin.Main");
Usar con ServiceLoader¶
// Cargar servicios del ClassLoader de plugins
URLClassLoader pluginLoader = EarlyPluginLoader.getPluginClassLoader();
ServiceLoader<ClassTransformer> services = ServiceLoader.load(
ClassTransformer.class,
pluginLoader
);
for (ClassTransformer transformer : services) {
System.out.println("Encontrado: " + transformer.getClass());
}
Seguridad del ClassLoader¶
Verificación de Bytecode¶
La JVM verifica el bytecode antes de ejecutarlo:
private Class<?> defineClassSafe(String name, byte[] bytecode) {
try {
// defineClass() verifica automáticamente
return defineClass(name, bytecode, 0, bytecode.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(
"Bytecode inválido para: " + name,
e
);
} catch (LinkageError e) {
throw new IllegalStateException(
"Error enlazando: " + name,
e
);
}
}
Verificación Manual con ASM¶
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.util.CheckClassAdapter;
public void verifyBytecode(byte[] bytecode) throws Exception {
ClassReader reader = new ClassReader(bytecode);
// CheckClassAdapter lanza excepción si el bytecode es inválido
CheckClassAdapter.verify(reader, false, new PrintWriter(System.err));
}
Restricciones de Seguridad¶
public class SecureClassLoader extends ClassLoader {
private final Set<String> allowedPackages;
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// Verificar si el paquete está permitido
if (!isAllowed(name)) {
throw new SecurityException(
"No se permite cargar: " + name
);
}
return super.loadClass(name, resolve);
}
private boolean isAllowed(String className) {
for (String pkg : allowedPackages) {
if (className.startsWith(pkg)) {
return true;
}
}
return false;
}
}
Problemas Comunes y Soluciones¶
ClassNotFoundException¶
Causa: La clase no se encuentra en el classpath
Solución:
try {
Class<?> clazz = classLoader.loadClass("com.ejemplo.MiClase");
} catch (ClassNotFoundException e) {
System.err.println("Clase no encontrada: " + e.getMessage());
// Verificar que el JAR está en el classpath
}
NoClassDefFoundError¶
Causa: La clase fue encontrada en tiempo de compilación pero no en tiempo de ejecución
Solución:
// Asegurar que todas las dependencias están disponibles
// Incluir dependencias transitivas en el JAR del plugin
ClassCastException¶
Causa: La misma clase cargada por diferentes ClassLoaders
Solución:
// Usar el mismo ClassLoader para clases relacionadas
ClassLoader cl = MyClass.class.getClassLoader();
Class<?> related = cl.loadClass("com.ejemplo.Related");
LinkageError¶
Causa: Clase cargada múltiples veces por diferentes ClassLoaders
Solución:
// Verificar antes de definir
Class<?> existing = findLoadedClass(name);
if (existing != null) {
return existing;
}
// Solo entonces definir la nueva clase
return defineClass(name, bytecode, 0, bytecode.length);
Depuración¶
Registrar Cargas de Clases¶
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
System.out.println("[ClassLoader] Cargando: " + name);
System.out.println("[ClassLoader] Desde: " + this.getClass().getName());
Class<?> c = super.loadClass(name, resolve);
System.out.println("[ClassLoader] Cargada: " + c.getName());
System.out.println("[ClassLoader] Por: " + c.getClassLoader());
return c;
}
Inspeccionar Jerarquía¶
public static void printClassLoaderHierarchy(ClassLoader cl) {
System.out.println("Jerarquía de ClassLoaders:");
int level = 0;
while (cl != null) {
System.out.println(" ".repeat(level) + "↳ " + cl.getClass().getName());
cl = cl.getParent();
level++;
}
System.out.println(" ".repeat(level) + "↳ Bootstrap ClassLoader");
}
Listar Clases Cargadas¶
public static void listLoadedClasses(ClassLoader cl) {
try {
Field f = ClassLoader.class.getDeclaredField("classes");
f.setAccessible(true);
@SuppressWarnings("unchecked")
Vector<Class<?>> classes = (Vector<Class<?>>) f.get(cl);
System.out.println("Clases cargadas: " + classes.size());
for (Class<?> c : classes) {
System.out.println(" - " + c.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
Mejores Prácticas¶
✅ Hacer¶
-
Sincronizar carga de clases
-
Verificar cache primero
-
Cerrar ClassLoaders
-
Manejar excepciones
❌ No Hacer¶
- No cargar múltiples veces
- No ignorar excepciones
- No mezclar ClassLoaders
- No modificar bytecode sin verificar
Rendimiento¶
Optimizar Carga de Clases¶
// Cache de bytecode transformado
private final Map<String, byte[]> bytecodeCache =
new ConcurrentHashMap<>();
private byte[] getTransformedBytecode(String name) {
return bytecodeCache.computeIfAbsent(name, k -> {
byte[] original = readOriginalBytecode(k);
return applyTransformations(k, original);
});
}
Carga Lazy¶
// Cargar solo cuando sea necesario
private volatile Class<?> cachedClass;
public Class<?> getMyClass() {
if (cachedClass == null) {
synchronized (this) {
if (cachedClass == null) {
cachedClass = loadClass("com.ejemplo.MiClase");
}
}
}
return cachedClass;
}
Siguiente¶
- Plugin System: Sistema completo de plugins
- Early Plugins: Plugins tempranos
- API Plugin Overview: Referencia de API
¿Necesitas ayuda? Consulta FAQ o Troubleshooting