Aller au contenu

Mejores Prácticas para Desarrollo de Plugins

Esta guía exhaustiva cubre las mejores prácticas para desarrollar plugins robustos, eficientes y mantenibles para Hytale. Incluye patrones recomendados, antipatrones a evitar, y estrategias de optimización.

Principios Fundamentales

1. KISS (Keep It Simple, Stupid)

Mantén tu código simple y fácil de entender.

// ❌ MAL - Demasiado complejo
public class ComplexTransformer implements ClassTransformer {
    private Map<String, List<Tuple<MethodVisitor, Function<byte[], byte[]>>>> transformers;

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        return transformers.getOrDefault(className, Collections.emptyList())
            .stream()
            .reduce(bytecode, (acc, tuple) -> tuple.getSecond().apply(acc), (a, b) -> b);
    }
}

// ✅ BIEN - Simple y claro
public class SimpleTransformer implements ClassTransformer {

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        if (!shouldTransform(className)) {
            return null;
        }

        try {
            return doTransform(bytecode);
        } catch (Exception e) {
            logError("Error transforming " + className, e);
            return null;
        }
    }

    private boolean shouldTransform(String className) {
        return className.equals("com.hypixel.hytale.server.core.HytaleServer");
    }

    private byte[] doTransform(byte[] bytecode) {
        // Lógica clara de transformación
        ClassReader reader = new ClassReader(bytecode);
        ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
        ClassVisitor visitor = new MyVisitor(Opcodes.ASM9, writer);
        reader.accept(visitor, 0);
        return writer.toByteArray();
    }
}

2. DRY (Don't Repeat Yourself)

Evita duplicar código.

// ❌ MAL - Código duplicado
public class DuplicatedCodeTransformer extends MethodVisitor {

    @Override
    public void visitCode() {
        super.visitCode();

        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("Start of method");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }

    @Override
    public void visitInsn(int opcode) {
        if (opcode == Opcodes.RETURN) {
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("End of method");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        super.visitInsn(opcode);
    }
}

// ✅ BIEN - Código reutilizable
public class CleanCodeTransformer extends MethodVisitor {

    @Override
    public void visitCode() {
        super.visitCode();
        injectPrintln("Start of method");
    }

    @Override
    public void visitInsn(int opcode) {
        if (opcode == Opcodes.RETURN) {
            injectPrintln("End of method");
        }
        super.visitInsn(opcode);
    }

    /**
     * Helper para inyectar System.out.println
     */
    private void injectPrintln(String message) {
        mv.visitFieldInsn(
            Opcodes.GETSTATIC,
            "java/lang/System",
            "out",
            "Ljava/io/PrintStream;"
        );
        mv.visitLdcInsn(message);
        mv.visitMethodInsn(
            Opcodes.INVOKEVIRTUAL,
            "java/io/PrintStream",
            "println",
            "(Ljava/lang/String;)V",
            false
        );
    }
}

3. Fail Fast

Detecta y reporta errores tempranamente.

// ✅ BIEN - Validación temprana
public class ValidatingTransformer implements ClassTransformer {

    private final String targetClass;
    private final String targetMethod;

    public ValidatingTransformer(String targetClass, String targetMethod) {
        // Validar en el constructor
        if (targetClass == null || targetClass.isEmpty()) {
            throw new IllegalArgumentException("targetClass cannot be null or empty");
        }
        if (targetMethod == null || targetMethod.isEmpty()) {
            throw new IllegalArgumentException("targetMethod cannot be null or empty");
        }

        this.targetClass = targetClass;
        this.targetMethod = targetMethod;
    }

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        // Validar parámetros
        if (className == null || bytecode == null) {
            throw new IllegalArgumentException("className and bytecode must not be null");
        }

        if (!className.equals(targetClass)) {
            return null;
        }

        return doTransform(bytecode);
    }
}

Rendimiento y Optimización

1. Minimizar Transformaciones

Solo transforma lo necesario.

// ✅ BIEN - Filtrado eficiente
public class OptimizedTransformer implements ClassTransformer {

    private static final Set<String> TARGET_CLASSES = Set.of(
        "com.hypixel.hytale.server.core.HytaleServer",
        "com.hypixel.hytale.server.core.entity.entities.Player"
    );

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        // Retornar temprano si no es una clase objetivo
        if (!TARGET_CLASSES.contains(className)) {
            return null;
        }

        // Transformar solo clases específicas
        return doTransform(className, bytecode);
    }
}

2. Cachear Resultados

Cachea operaciones costosas.

public class CachingTransformer implements ClassTransformer {

    private final Map<String, Pattern> patternCache = new ConcurrentHashMap<>();
    private final Map<String, Boolean> classCheckCache = new ConcurrentHashMap<>();

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        // Cachear verificación de clase
        Boolean shouldTransform = classCheckCache.computeIfAbsent(
            className,
            this::shouldTransformClass
        );

        if (!shouldTransform) {
            return null;
        }

        return doTransform(bytecode);
    }

    private boolean shouldTransformClass(String className) {
        // Operación costosa, cachear resultado
        Pattern pattern = patternCache.computeIfAbsent(
            "server-classes",
            k -> Pattern.compile("com\\.hypixel\\.hytale\\.server\\..*")
        );

        return pattern.matcher(className).matches();
    }
}

3. Usar COMPUTE_FRAMES Inteligentemente

// ✅ BIEN - Usar COMPUTE_FRAMES solo cuando sea necesario
public class SmartFramesTransformer implements ClassTransformer {

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        ClassReader reader = new ClassReader(bytecode);

        // Si solo modificamos código existente sin cambiar flujo de control,
        // COMPUTE_MAXS es suficiente y más rápido
        int flags = needsFrameComputation(className)
            ? ClassWriter.COMPUTE_FRAMES
            : ClassWriter.COMPUTE_MAXS;

        ClassWriter writer = new ClassWriter(reader, flags);
        ClassVisitor visitor = new MyVisitor(Opcodes.ASM9, writer);
        reader.accept(visitor, 0);
        return writer.toByteArray();
    }

    private boolean needsFrameComputation(String className) {
        // Solo necesitamos COMPUTE_FRAMES si modificamos el flujo de control
        // (agregamos try-catch, loops, condicionales, etc.)
        return className.equals("com.hypixel.hytale.server.core.ComplexClass");
    }
}

4. Evitar Operaciones en el Hot Path

// ❌ MAL - Operaciones costosas en cada llamada
public class SlowTransformer extends MethodVisitor {

    @Override
    public void visitMethodInsn(int opcode, String owner, String name,
                               String descriptor, boolean isInterface) {
        // Compilar regex en cada invocación (MUY LENTO)
        if (owner.matches("com\\.hypixel\\.hytale\\..*")) {
            // ...
        }
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }
}

// ✅ BIEN - Precalcular y cachear
public class FastTransformer extends MethodVisitor {

    private static final Pattern HYTALE_PATTERN =
        Pattern.compile("com\\.hypixel\\.hytale\\..*");

    @Override
    public void visitMethodInsn(int opcode, String owner, String name,
                               String descriptor, boolean isInterface) {
        if (HYTALE_PATTERN.matcher(owner).matches()) {
            // ...
        }
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }
}

Seguridad y Validación

1. Validar Entrada del Usuario

public class SecureConfigManager {

    public void loadConfig(Path configPath) {
        // Validar path
        if (!isSecurePath(configPath)) {
            throw new SecurityException("Invalid config path: " + configPath);
        }

        // Validar que existe
        if (!Files.exists(configPath)) {
            createDefaultConfig(configPath);
            return;
        }

        // Validar permisos
        if (!Files.isReadable(configPath)) {
            throw new SecurityException("Cannot read config file: " + configPath);
        }

        // Cargar config de forma segura
        try (InputStream in = Files.newInputStream(configPath)) {
            Properties props = new Properties();
            props.load(in);
            validateProperties(props);
            applyConfig(props);
        } catch (IOException e) {
            throw new RuntimeException("Error loading config", e);
        }
    }

    private boolean isSecurePath(Path path) {
        // Verificar que no use path traversal
        String normalized = path.normalize().toString();
        return !normalized.contains("..") &&
               normalized.startsWith("config/");
    }

    private void validateProperties(Properties props) {
        // Validar valores
        String maxPlayers = props.getProperty("max-players");
        if (maxPlayers != null) {
            try {
                int value = Integer.parseInt(maxPlayers);
                if (value < 1 || value > 1000) {
                    throw new IllegalArgumentException(
                        "max-players must be between 1 and 1000"
                    );
                }
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException(
                    "max-players must be a number"
                );
            }
        }
    }
}

2. Sandboxing de Transformaciones

public class SandboxedTransformer implements ClassTransformer {

    private static final Set<String> FORBIDDEN_PACKAGES = Set.of(
        "java.lang.System",
        "java.lang.Runtime",
        "java.lang.Process",
        "java.io.File",
        "java.nio.file.Files"
    );

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        // No transformar clases del sistema
        if (className.startsWith("java.") ||
            className.startsWith("javax.") ||
            className.startsWith("sun.")) {
            return null;
        }

        return doTransform(bytecode);
    }

    /**
     * Visitor que previene acceso a clases peligrosas.
     */
    private static class SecurityCheckVisitor extends MethodVisitor {

        public SecurityCheckVisitor(int api, MethodVisitor mv) {
            super(api, mv);
        }

        @Override
        public void visitMethodInsn(int opcode, String owner, String name,
                                   String descriptor, boolean isInterface) {
            // Bloquear llamadas a métodos peligrosos
            String ownerClass = owner.replace('/', '.');

            if (FORBIDDEN_PACKAGES.contains(ownerClass)) {
                throw new SecurityException(
                    "Attempted to call forbidden method: " +
                    ownerClass + "." + name
                );
            }

            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
        }
    }
}

Compatibilidad Entre Versiones

1. Versionado Semántico

public class VersionedPlugin implements ClassTransformer {

    public static final String VERSION = "2.1.0";
    public static final int MAJOR_VERSION = 2;
    public static final int MINOR_VERSION = 1;
    public static final int PATCH_VERSION = 0;

    // API compatible con versión mínima
    private static final String MIN_HYTALE_VERSION = "1.0.0";

    @Override
    public int priority() {
        return 50;
    }

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        // Verificar compatibilidad
        if (!isCompatible()) {
            System.err.println(
                "[Plugin] Incompatible Hytale version. " +
                "Required: " + MIN_HYTALE_VERSION
            );
            return null;
        }

        return doTransform(className, bytecode);
    }

    private boolean isCompatible() {
        // Verificar versión de Hytale
        String hytaleVersion = getHytaleVersion();
        return compareVersions(hytaleVersion, MIN_HYTALE_VERSION) >= 0;
    }

    private String getHytaleVersion() {
        // Obtener versión del servidor
        // Implementación dependiente de cómo Hytale expone su versión
        return "1.0.0";
    }

    private int compareVersions(String v1, String v2) {
        String[] parts1 = v1.split("\\.");
        String[] parts2 = v2.split("\\.");

        for (int i = 0; i < Math.min(parts1.length, parts2.length); i++) {
            int num1 = Integer.parseInt(parts1[i]);
            int num2 = Integer.parseInt(parts2[i]);

            if (num1 != num2) {
                return Integer.compare(num1, num2);
            }
        }

        return Integer.compare(parts1.length, parts2.length);
    }
}

2. Feature Toggles

public class FeatureToggledPlugin implements ClassTransformer {

    private final ConfigManager config;

    public FeatureToggledPlugin() {
        this.config = new ConfigManager("config/plugin.properties");
    }

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        // Verificar features habilitadas
        if (!config.getBoolean("features.transformation.enabled", true)) {
            return null;
        }

        if (className.equals("com.hypixel.hytale.server.core.HytaleServer")) {
            if (config.getBoolean("features.server-hooks.enabled", true)) {
                return transformServer(bytecode);
            }
        }

        if (className.equals("com.hypixel.hytale.server.core.entity.entities.Player")) {
            if (config.getBoolean("features.player-hooks.enabled", true)) {
                return transformPlayer(bytecode);
            }
        }

        return null;
    }
}

Testing y Debugging

1. Unit Testing

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

public class TransformerTest {

    private MyTransformer transformer;

    @BeforeEach
    public void setUp() {
        transformer = new MyTransformer();
    }

    @Test
    public void testTransformTargetClass() {
        byte[] original = loadClassBytes("com.example.TargetClass");
        byte[] transformed = transformer.transform(
            "com.example.TargetClass",
            "com/example/TargetClass.class",
            original
        );

        assertNotNull(transformed, "Should transform target class");
        assertNotEquals(original, transformed, "Should modify bytecode");

        // Verificar que el bytecode es válido
        assertTrue(isValidBytecode(transformed), "Bytecode should be valid");
    }

    @Test
    public void testIgnoreNonTargetClass() {
        byte[] original = loadClassBytes("com.example.OtherClass");
        byte[] result = transformer.transform(
            "com.example.OtherClass",
            "com/example/OtherClass.class",
            original
        );

        assertNull(result, "Should not transform non-target class");
    }

    @Test
    public void testHandleInvalidBytecode() {
        byte[] invalid = new byte[] { 0x00, 0x01, 0x02 };

        assertThrows(Exception.class, () -> {
            transformer.transform("Invalid", "Invalid.class", invalid);
        });
    }

    private byte[] loadClassBytes(String className) {
        // Cargar bytes de clase de prueba
        try (InputStream in = getClass().getResourceAsStream(
                "/" + className.replace('.', '/') + ".class")) {
            return in.readAllBytes();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean isValidBytecode(byte[] bytecode) {
        try {
            ClassReader reader = new ClassReader(bytecode);
            ClassWriter writer = new ClassWriter(0);
            CheckClassAdapter checker = new CheckClassAdapter(writer, true);
            reader.accept(checker, 0);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

2. Logging Estructurado

public class StructuredLoggingTransformer implements ClassTransformer {

    private static final Logger LOGGER = LoggerFactory.getLogger(
        StructuredLoggingTransformer.class
    );

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        long startTime = System.nanoTime();

        try {
            if (!shouldTransform(className)) {
                LOGGER.debug("Skipping class: {}", className);
                return null;
            }

            LOGGER.info("Transforming class: {}", className);
            byte[] result = doTransform(bytecode);

            long duration = System.nanoTime() - startTime;
            LOGGER.info("Transformed {} in {}ms",
                className,
                duration / 1_000_000.0
            );

            return result;

        } catch (Exception e) {
            LOGGER.error("Error transforming class: {}", className, e);
            return null;
        }
    }
}

3. Debug Mode

public class DebuggableTransformer implements ClassTransformer {

    private static final boolean DEBUG = Boolean.getBoolean("plugin.debug");
    private static final Path DEBUG_OUTPUT = Paths.get("debug", "transformed");

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        try {
            byte[] result = doTransform(bytecode);

            if (DEBUG && result != null) {
                saveDebugInfo(className, bytecode, result);
            }

            return result;

        } catch (Exception e) {
            if (DEBUG) {
                e.printStackTrace();
            }
            return null;
        }
    }

    private void saveDebugInfo(String className, byte[] original, byte[] transformed) {
        try {
            // Guardar bytecode original
            Path originalPath = DEBUG_OUTPUT.resolve("original")
                .resolve(className.replace('.', '/') + ".class");
            Files.createDirectories(originalPath.getParent());
            Files.write(originalPath, original);

            // Guardar bytecode transformado
            Path transformedPath = DEBUG_OUTPUT.resolve("transformed")
                .resolve(className.replace('.', '/') + ".class");
            Files.createDirectories(transformedPath.getParent());
            Files.write(transformedPath, transformed);

            // Guardar comparación textual
            Path diffPath = DEBUG_OUTPUT.resolve("diff")
                .resolve(className.replace('.', '/') + ".txt");
            Files.createDirectories(diffPath.getParent());

            String diff = generateDiff(original, transformed);
            Files.writeString(diffPath, diff);

            System.out.println("[Debug] Saved debug info for " + className);

        } catch (IOException e) {
            System.err.println("[Debug] Error saving debug info: " + e.getMessage());
        }
    }

    private String generateDiff(byte[] original, byte[] transformed) {
        StringBuilder sb = new StringBuilder();

        sb.append("=== ORIGINAL ===\n");
        sb.append(disassemble(original));

        sb.append("\n=== TRANSFORMED ===\n");
        sb.append(disassemble(transformed));

        return sb.toString();
    }

    private String disassemble(byte[] bytecode) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);

        ClassReader reader = new ClassReader(bytecode);
        TraceClassVisitor tracer = new TraceClassVisitor(pw);
        reader.accept(tracer, 0);

        return sw.toString();
    }
}

Documentación y Mantenimiento

1. Documentar Transformaciones

/**
 * Transforma la clase Player para agregar sistema de estadísticas personalizadas.
 *
 * <h3>Modificaciones:</h3>
 * <ul>
 *   <li>Agrega campo: {@code private CustomStats customStats}</li>
 *   <li>Agrega método: {@code public CustomStats getCustomStats()}</li>
 *   <li>Modifica {@code takeDamage()} para actualizar estadísticas</li>
 * </ul>
 *
 * <h3>Compatibilidad:</h3>
 * <ul>
 *   <li>Versión mínima de Hytale: 1.0.0</li>
 *   <li>Requiere: CustomStatsPlugin v2.0+</li>
 * </ul>
 *
 * <h3>Configuración:</h3>
 * <pre>
 * # config/player-stats.properties
 * enabled=true
 * track-damage=true
 * track-deaths=true
 * </pre>
 *
 * @author Tu Nombre
 * @version 2.0.0
 * @since 1.0.0
 */
public class PlayerStatsTransformer implements ClassTransformer {
    // ...
}

2. Changelog

Mantén un registro de cambios.

/**
 * Player Statistics Transformer
 *
 * @version 2.1.0
 *
 * <h3>Changelog:</h3>
 * <ul>
 *   <li><b>2.1.0</b> (2026-01-15)
 *     <ul>
 *       <li>Added: Support for async stat updates</li>
 *       <li>Fixed: Race condition in stat calculation</li>
 *       <li>Changed: Improved performance by 30%</li>
 *     </ul>
 *   </li>
 *   <li><b>2.0.0</b> (2026-01-01)
 *     <ul>
 *       <li>Breaking: Changed stats API</li>
 *       <li>Added: Death tracking</li>
 *       <li>Removed: Deprecated methods</li>
 *     </ul>
 *   </li>
 *   <li><b>1.0.0</b> (2025-12-15)
 *     <ul>
 *       <li>Initial release</li>
 *     </ul>
 *   </li>
 * </ul>
 */
public class PlayerStatsTransformer implements ClassTransformer {
    // ...
}

3. README y Ejemplos

Proporciona documentación clara y ejemplos de uso.

# Player Stats Plugin

## Instalación

1. Descarga `player-stats-2.1.0.jar`
2. Coloca en `earlyplugins/`
3. Inicia servidor con `--accept-early-plugins`

## Configuración

```properties
# config/player-stats.properties
enabled=true
track-damage=true
track-deaths=true
save-interval=300

Uso

// Obtener stats de un jugador
Player player = ...;
CustomStats stats = player.getCustomStats();

System.out.println("Daño total: " + stats.getTotalDamage());
System.out.println("Muertes: " + stats.getDeaths());

API

Ver API Documentation

## Patrones de Diseño Recomendados

### 1. Strategy Pattern

```java
public interface TransformationStrategy {
    byte[] transform(byte[] bytecode);
}

public class LoggingStrategy implements TransformationStrategy {
    @Override
    public byte[] transform(byte[] bytecode) {
        // Agregar logging
        return bytecode;
    }
}

public class ProfilingStrategy implements TransformationStrategy {
    @Override
    public byte[] transform(byte[] bytecode) {
        // Agregar profiling
        return bytecode;
    }
}

public class StrategyBasedTransformer implements ClassTransformer {

    private final Map<String, TransformationStrategy> strategies = new HashMap<>();

    public StrategyBasedTransformer() {
        strategies.put("logging", new LoggingStrategy());
        strategies.put("profiling", new ProfilingStrategy());
    }

    @Override
    public byte[] transform(String className, String classPath, byte[] bytecode) {
        String strategy = getStrategyForClass(className);

        if (strategy != null && strategies.containsKey(strategy)) {
            return strategies.get(strategy).transform(bytecode);
        }

        return null;
    }
}

2. Builder Pattern

public class TransformerBuilder {

    private final List<String> targetClasses = new ArrayList<>();
    private final List<MethodTransformation> transformations = new ArrayList<>();
    private int priority = 50;
    private boolean debug = false;

    public TransformerBuilder targetClass(String className) {
        this.targetClasses.add(className);
        return this;
    }

    public TransformerBuilder addMethodTransformation(MethodTransformation transformation) {
        this.transformations.add(transformation);
        return this;
    }

    public TransformerBuilder withPriority(int priority) {
        this.priority = priority;
        return this;
    }

    public TransformerBuilder withDebug(boolean debug) {
        this.debug = debug;
        return this;
    }

    public ClassTransformer build() {
        return new ConfiguredTransformer(
            targetClasses,
            transformations,
            priority,
            debug
        );
    }
}

// Uso:
ClassTransformer transformer = new TransformerBuilder()
    .targetClass("com.hypixel.hytale.server.core.HytaleServer")
    .addMethodTransformation(new LoggingTransformation())
    .withPriority(100)
    .withDebug(true)
    .build();

Antipatrones a Evitar

❌ God Class

// MAL - Clase que hace demasiado
public class GodTransformer implements ClassTransformer {
    // Transforma todo
    // Maneja configuración
    // Hace logging
    // Gestiona estadísticas
    // Maneja persistencia
    // etc...
}

✅ Separación de Responsabilidades

// BIEN - Clases especializadas
public class ServerTransformer implements ClassTransformer { }
public class PlayerTransformer implements ClassTransformer { }
public class ConfigManager { }
public class StatisticsCollector { }
public class PersistenceManager { }

❌ Acoplamiento Fuerte

// MAL - Dependencias hardcodeadas
public class CoupledTransformer implements ClassTransformer {
    private final SpecificLogger logger = new SpecificLogger();
    private final SpecificConfig config = new SpecificConfig();
}

✅ Inyección de Dependencias

// BIEN - Dependencias inyectadas
public class DecoupledTransformer implements ClassTransformer {
    private final Logger logger;
    private final ConfigManager config;

    public DecoupledTransformer(Logger logger, ConfigManager config) {
        this.logger = logger;
        this.config = config;
    }
}

Checklist de Calidad

Antes de liberar tu plugin, verifica:

  • Funcionalidad
  • Todas las transformaciones funcionan correctamente
  • Maneja casos edge correctamente
  • No rompe funcionalidad existente

  • Rendimiento

  • Transformaciones son eficientes
  • No hay memory leaks
  • Impacto en startup time es mínimo

  • Seguridad

  • Valida todas las entradas
  • No expone información sensible
  • No permite ejecución arbitraria de código

  • Compatibilidad

  • Especifica versiones compatibles
  • Maneja versiones antiguas gracefully
  • No rompe otros plugins

  • Testing

  • Unit tests pasando
  • Integration tests pasando
  • Probado en servidor real

  • Documentación

  • README completo
  • Javadoc en clases públicas
  • Ejemplos de uso
  • Changelog actualizado

  • Código

  • Sigue convenciones de Java
  • No hay warnings del compilador
  • Pasa linters (Checkstyle, SpotBugs)

Recursos Adicionales


¿Problemas? Consulta Troubleshooting