Saltar a contenido

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:

  1. Localizar archivos de clases (.class)
  2. Leer el bytecode de las clases
  3. Verificar que el bytecode es válido
  4. Definir la clase en la JVM
  5. 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.jar y 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:

  1. Verificar si la clase ya está cargada
  2. Delegar al padre para cargar
  3. 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

Servidor usa: LibraryA v1.0
Plugin usa: LibraryA v2.0

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

  1. Sincronizar carga de clases

    synchronized (getClassLoadingLock(name)) {
        // Cargar clase
    }
    

  2. Verificar cache primero

    Class<?> c = findLoadedClass(name);
    if (c != null) return c;
    

  3. Cerrar ClassLoaders

    if (classLoader instanceof URLClassLoader) {
        ((URLClassLoader) classLoader).close();
    }
    

  4. Manejar excepciones

    try {
        return loadClass(name);
    } catch (ClassNotFoundException e) {
        // Log y manejar
    }
    

❌ No Hacer

  1. No cargar múltiples veces
  2. No ignorar excepciones
  3. No mezclar ClassLoaders
  4. 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


¿Necesitas ayuda? Consulta FAQ o Troubleshooting