Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
Guía práctica: ether-di

ether-di provee los bloques de construcción para inyección de dependencias explícita en Java 21+. No hay reflexión, no hay anotaciones, no hay contenedor IoC. El grafo de dependencias se construye en Java puro y es visible, depurable y compatible con GraalVM native-image.

Instalación

<dependency>
<groupId>dev.rafex.ether.di</groupId>
<artifactId>ether-di</artifactId>
<version>1.0.0</version>
</dependency>

Las tres clases

Clase Propósito
Lazy<T> Inicialización perezosa thread-safe; el supplier se ejecuta exactamente una vez
Closer Cierre de recursos en orden LIFO con acumulación de excepciones suprimidas
Bootstrap Arranque de aplicación: factory → warmup → shutdown hook

Lazy<T> — inicialización perezosa

El supplier NO se ejecuta al construir el Lazy. Se ejecuta en el primer get() y nunca más.

var config = new Lazy<>(AppConfig::load);
var database = new Lazy<>(() -> DataSource.create(config.get()));
var cache = new Lazy<>(() -> new RedisCache(config.get().redisUrl()));
// Solo en este punto se inicializa AppConfig
AppConfig cfg = config.get();
// Llamadas posteriores devuelven el mismo objeto
AppConfig same = config.get(); // mismo objeto, supplier no se vuelve a llamar
boolean ready = config.isInitialized(); // true

Thread-safety

Lazy usa doble verificación de bloqueo (DCL). Múltiples hilos llamando get() simultáneamente producen exactamente una inicialización — el resto espera y reciben el mismo objeto.

// Seguro para 16 hilos en paralelo
var lazy = new Lazy<>(() -> new HeavyResource());
// lazy.get() llamado desde N hilos → una sola instancia creada

Uso en un contenedor de dependencias

public class AppContainer {
private final Lazy<AppConfig> config = new Lazy<>(AppConfig::load);
private final Lazy<DataSource> dataSource = new Lazy<>(() ->
DataSourceFactory.create(config.get()));
private final Lazy<UserRepository> users = new Lazy<>(() ->
new JdbcUserRepository(dataSource.get()));
private final Lazy<UserService> service = new Lazy<>(() ->
new UserServiceImpl(users.get()));
public UserService userService() { return service.get(); }
public DataSource dataSource() { return dataSource.get(); }
}

Closer — gestión de recursos en orden LIFO

Closer implementa AutoCloseable. Registra recursos y los cierra en orden inverso. Si varios recursos lanzan excepción, propaga la primera y adjunta las demás como suppressed.

var closer = new Closer();
var pool = closer.register(DataSourceFactory.create(config)); // registrado 1°
var cache = closer.register(new CacheManager()); // registrado 2°
var server = closer.register(HttpServer.start(8080)); // registrado 3°
// Al cerrar: server → cache → pool (LIFO)
try (closer) {
runApplication(pool, cache, server);
}

Flujo de cierre

graph LR
    C[closer.close] --> S[server.close]
    S --> CH[cache.close]
    CH --> P[pool.close]

register() devuelve el mismo objeto

var server = closer.register(HttpServer.start(8080));
// 'server' es el mismo objeto — sin variable temporal extra

Idempotencia

closer.close();
closer.close(); // no-op
closer.isClosed(); // true

Manejo de múltiples excepciones

var closer = new Closer();
closer.register(() -> { throw new IllegalStateException("error en A"); });
closer.register(() -> { throw new IllegalStateException("error en B"); }); // cierra primero
try {
closer.close();
} catch (RuntimeException e) {
// e.getMessage() == "error en B" (primera excepción, LIFO)
// e.getSuppressed()[0] == "error en A" (adjunta como suprimida)
}

Bootstrap — arranque con ciclo de vida completo

Bootstrap une factory → warmup → shutdown hook en una sola llamada.

var runtime = Bootstrap.start(
AppContainer::new, // 1. Crea el contenedor
AppContainer::warmup, // 2. Inicializa eager (fail-fast en arranque)
c -> c.shutdown() // 3. Callback antes de cerrar recursos (Ctrl+C)
);

Bootstrap.Runtime

El objeto devuelto es AutoCloseable:

Bootstrap.Runtime<AppContainer> runtime = Bootstrap.start(AppContainer::new);
AppContainer container = runtime.container();
Closer closer = runtime.closer();
runtime.close(); // cierra closer → todos los recursos en LIFO

Sobrecargas

// Solo factory
Bootstrap.start(AppContainer::new);
// Factory + warmup
Bootstrap.start(AppContainer::new, AppContainer::warmup);
// Factory + warmup + onShutdown
Bootstrap.start(AppContainer::new, AppContainer::warmup, c -> LOG.info("bye"));

Ciclo de vida completo

sequenceDiagram
    participant M as main()
    participant B as Bootstrap
    participant C as Container
    participant CL as Closer

    M->>B: start(factory, warmup, onShutdown)
    B->>CL: new Closer()
    B->>C: factory.get()
    Note over B,C: si C implementa AutoCloseable,<br/>se registra en Closer
    B->>C: warmup(container)
    B->>B: registra JVM shutdown hook
    B-->>M: Runtime(container, closer)
    Note over M: la app corre...
    M->>B: JVM shutdown (Ctrl+C / SIGTERM)
    B->>C: onShutdown(container)
    B->>CL: closer.close()
    CL->>C: container.close() + resources LIFO

Patrón completo recomendado

Combina los tres componentes para un arranque seguro con shutdown graceful:

// AppContainer.java
public class AppContainer implements AutoCloseable {
private final Closer closer = new Closer();
// Dependencias lazy — se inicializan solo cuando se necesitan
private final Lazy<AppConfig> config = new Lazy<>(AppConfig::load);
private final Lazy<DataSource> db = new Lazy<>(() ->
closer.register(DataSourceFactory.create(config.get())));
private final Lazy<UserRepository> users = new Lazy<>(() ->
new JdbcUserRepository(db.get()));
private final Lazy<UserService> service = new Lazy<>(() ->
new UserServiceImpl(users.get()));
/** Warmup: inicializa toda la cadena eager para detectar errores en arranque. */
public void warmup() {
service.get(); // dispara config → db → users → service
}
public UserService userService() { return service.get(); }
@Override
public void close() {
closer.close(); // cierra DataSource y demás recursos en LIFO
}
}
// Main.java
public class Main {
public static void main(String[] args) {
var runtime = Bootstrap.start(
AppContainer::new,
AppContainer::warmup // falla rápido si hay error de config o DB
);
// JVM shutdown hook ya registrado — Ctrl+C activa cierre limpio
JettyServer.start(runtime.container());
}
}

Uso en tests con try-with-resources

@Test
void integrationTest() throws Exception {
try (var runtime = Bootstrap.start(TestContainer::new)) {
var service = runtime.container().userService();
assertNotNull(service.findById(1L));
}
// Closer cierra todos los recursos al salir del bloque
}

Compatibilidad con GraalVM native-image

ether-di no usa reflexión, generación de proxies ni class-loading dinámico. Compila a native-image sin ninguna configuración adicional:

<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<!-- No se necesita reflect-config.json ni resource-config.json para ether-di -->
</plugin>

Comparación con alternativas

ether-di Spring DI CDI Guice
Reflexión No Sí Sí Sí
Anotaciones No Sí Sí Sí
GraalVM native Sin config Requiere hints Requiere hints Requiere hints
Grafo visible en código Sí No No Parcial
Tamaño del jar ~10 KB ~1 MB+ ~500 KB ~700 KB
Errores detectados En compilación En runtime En runtime En runtime

Más información