Ya se encuentran abiertas las inscripciones a los cursos de Gugler.
Inscripciones : mi.gugler.com.ar
Información: www.gugler.com.ar
https://www.instagram.com/p/DccUvw-NTex/?igsi=MTN2azV3emV4MjkzYg==
Inscripciones : mi.gugler.com.ar
Información: www.gugler.com.ar
https://www.instagram.com/p/DccUvw-NTex/?igsi=MTN2azV3emV4MjkzYg==
String json = "{\n" +
" \"name\": \"Emanuel\",\n" +
" \"language\": \"Java\"\n" +
"}";
A partir de Java 15, esto cambió con JEP 378: Text Blocks.
Ahora podemos escribir:
String json = """
{
"name": "Emanuel",
"language": "Java"
}
""";
Mucho más parecido al texto que realmente queremos representar.
Pero no son simplemente strings multilínea
Lo interesante de los Text Blocks está en cómo Java decidió tratar la indentación y los espacios.
Por ejemplo:
String text = """
Hello
World
""";
El resultado es:
Hello
World
La indentación que utilizamos para mantener prolijo nuestro código Java no pasa automáticamente al String.
Java distingue entre el whitespace incidental, necesario para organizar el código fuente, y el whitespace que realmente forma parte del contenido.
Esto permite escribir código legible sin terminar con espacios innecesarios dentro del texto.
¿Y si realmente necesito espacios? Podemos utilizar `\s`.
String text = """
Hello\s
World
""";
Ese \s representa un espacio que queremos conservar explícitamente.
También existe una particularidad interesante con \:
String text = """
Hello \
World
""";
Aquí el \ evita que el salto de línea del código fuente se convierta en un salto de línea del String.
El resultado es:
Hello World
Es decir, los Text Blocks no solamente agregan una nueva sintaxis: agregan herramientas para controlar exactamente cómo se transforma el código fuente en el contenido del String.
Uno de los casos donde más se nota la diferencia es al trabajar con formatos que naturalmente ocupan varias líneas.
String json = """
{
"name": "Emanuel",
"language": "Java",
"version": 25
}
""";
String sql = """
SELECT id, name
FROM users
WHERE active = true
ORDER BY name
""";
String html = """
<html>
<body>
<h1>Hello Java</h1>
</body>
</html>
""";
El código se parece mucho más al documento que estamos representando.
Y podemos combinarlos con String.formatted():
String message = """
Hello %s!
Welcome to %s.
""".formatted("Emanuel", "Java");
Esto resulta especialmente cómodo para generar pequeños fragmentos de texto estructurado.
Text Blocks fueron introducidos como preview en Java 13, tuvieron una segunda preview en Java 14 y finalmente se convirtieron en una feature estándar en Java 15, mediante JEP 378.
Y hay algo interesante detrás de esta evolución.
La feature parece pequeña, pero obligó a Java a resolver cuestiones bastante delicadas:
Por eso, los Text Blocks son un buen ejemplo de algo que parece ser simplemente syntax sugar, pero que en realidad requiere bastante diseño del lenguaje.
Los Text Blocks permiten escribir strings multilínea de una manera mucho más natural:
String text = """
Este es un texto
de varias líneas
escrito directamente
en Java.
""";
Pero lo importante de JEP 378 no es solamente el """.
Es la decisión de que la representación visual del texto en el código fuente y el contenido real del String no tienen por qué ser exactamente lo mismo.
Y ahí está probablemente la parte más interesante de esta feature: Java agregó una sintaxis para escribir texto como texto, sin abandonar el control preciso sobre whitespace y terminadores de línea.
Durante muchos años, CDI (Contexts and Dependency Injection) fue una de las tecnologías que definieron el desarrollo empresarial en Java. Frameworks como Jakarta EE, Quarkus y Helidon la adoptaron como base para construir aplicaciones modernas.
Sin embargo, el ecosistema siempre tuvo una tensión difícil de resolver: la flexibilidad de CDI frente al costo de su modelo de ejecución.
Gran parte de las implementaciones tradicionales dependen de escaneo del classpath, reflexión y generación dinámica de proxies. Ese enfoque ofrece una enorme potencia, pero también incrementa el tiempo de arranque, el consumo de memoria y complica escenarios como GraalVM y las aplicaciones cloud-native.
Con el lanzamiento de Micronaut 5.1, aparece una iniciativa que puede cambiar este panorama: Open DI (ODI).
Open DI es un proyecto de Eclipse EE4J que implementa Jakarta CDI Lite, pero utilizando la infraestructura de inyección de dependencias compile-time desarrollada por Micronaut.
En otras palabras, la API que utiliza el desarrollador es la de CDI, mientras que el motor que genera los beans y los proxies trabaja durante la compilación, aprovechando el sistema de annotation processors de Micronaut.
Esto significa que una aplicación puede escribir código utilizando las anotaciones estándar de CDI como:
@Inject
@Singleton
@ApplicationScoped
@Produces
pero obtener las ventajas de una resolución de dependencias prácticamente libre de reflexión en tiempo de ejecución.
Desde sus primeras versiones, Micronaut apostó por una idea muy clara: Todo lo que pueda resolverse durante la compilación no debería hacerse durante la ejecución.
Ese principio permitió conseguir tiempos de inicio extremadamente bajos y un consumo reducido de memoria, dos características muy valoradas en microservicios y funciones serverless.
Hasta ahora, esa infraestructura era exclusiva del ecosistema Micronaut.
Con Open DI, esa tecnología comienza a exponerse mediante una implementación estándar de CDI Lite.
Lo interesante no es solamente que exista otra implementación de CDI.
Lo realmente novedoso es que el modelo tradicional de CDI comienza a separarse de la idea de un contenedor basado en reflexión.
Open DI demuestra que es posible implementar la especificación utilizando generación de código durante la compilación.
Eso abre la puerta a aplicaciones que conservan la portabilidad del estándar pero con características mucho más atractivas para entornos cloud-native.
¿Y ... CDI Lite y no CDI Full? Sí. Open DI implementa CDI Lite, no la especificación completa.
Eso implica que algunas capacidades avanzadas de CDI Full, como ciertas extensiones portables, decorators o passivation, quedan fuera del alcance del proyecto.
Pero probablemente esa sea una decisión deliberada.
La mayoría de las aplicaciones modernas utilizan solamente una parte relativamente pequeña del enorme conjunto de funcionalidades que ofrece CDI Full. Apostar por CDI Lite permite simplificar la implementación y mantener el foco en el rendimiento.
El anuncio de Micronaut 5.1 prácticamente esconde una de sus novedades más importantes. Micronaut 5.1 hace posible el lanzamiento de Open DI 1.0.
Para lograrlo, el framework incorporó nuevos puntos de integración ("CDI integration hooks") dentro de su infraestructura de inyección de dependencias.
No se trata simplemente de un proyecto externo utilizando Micronaut, sino de una colaboración donde la propia arquitectura del framework evoluciona para permitir una implementación estándar de CDI.
Es demasiado pronto para saber si Open DI tendrá una adopción masiva. Pero conceptualmente resulta muy interesante.
Durante años parecía que había que elegir entre dos caminos:
Open DI intenta combinar ambos mundos.
Si el proyecto madura y mantiene compatibilidad con la especificación, podríamos comenzar a ver aplicaciones que escriben código estándar de Jakarta CDI mientras aprovechan una infraestructura de ejecución mucho más eficiente.
No sería la primera vez que una innovación nacida en un framework termina influyendo sobre todo el ecosistema Java. Quizás Open DI sea otro ejemplo de ello.
Más allá de si termina convirtiéndose o no en una implementación ampliamente adoptada, Open DI representa algo muy valioso: demuestra que los estándares también pueden evolucionar.
Durante mucho tiempo asociamos CDI con reflexión, escaneo del classpath y contenedores pesados. Open DI desafía esa idea y propone una implementación basada en generación de código en tiempo de compilación.
Si esa visión prospera, el futuro de la inyección de dependencias en Java podría parecerse mucho más a Micronaut de lo que imaginábamos.
Dejo links:
https://github.com/eclipse-ee4j/odi#open-di
https://micronaut.io/2026/07/27/micronaut-framework-5-1-0-release/
Ya hablamos de LangChain4j
Creamos un proyecto básico de Spring Boot con módulos como Web y Configuration y luego agregamos la dependencia con maven:
<dependency>
<groupId>io.github.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.2.0</version>
</dependency>
Configura tu proveedor de modelo, como OpenAI:
import io.github.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LangChainConfig {
@Bean
public OpenAiChatModel openAiChatModel() {
return OpenAiChatModel.builder()
.apiKey("TU_CLAVE_API")
.temperature(0.7)
.maxTokens(500)
.build();
}
}
Implemente un controlador para manejar las solicitudes de los usuarios.
import io.github.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final OpenAiChatModel chatModel;
public ChatController(OpenAiChatModel chatModel) {
this.chatModel = chatModel;
}
@PostMapping
public String chat(@RequestBody String userInput) {
return chatModel.chat(userInput).getContent();
}
}
Ejecuta la aplicación Spring Boot y envía una solicitud POST a `/api/chat`:
curl -X POST http://localhost:8080/api/chat LangChain es un marco diseñado para trabajar con modelos de lenguaje para crear aplicaciones avanzadas de IA.
Para agregar memoria a las conversaciones:
import io.github.langchain4j.memory.ChatMemory;
import io.github.langchain4j.memory.InMemoryChatMemory;
import org.springframework.context.annotation.Bean;
@Configuration
public class LangChainConfig {
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
}
En el controlador, utiliza el ChatMemory para mantener el contexto entre solicitudes:
import io.github.langchain4j.memory.ChatMemory;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final OpenAiChatModel chatModel;
private final ChatMemory chatMemory;
public ChatController(OpenAiChatModel chatModel, ChatMemory chatMemory) {
this.chatModel = chatModel;
this.chatMemory = chatMemory;
}
@PostMapping
public String chat(@RequestBody String userInput) {
chatMemory.addUserMessage(userInput);
String response = chatModel.chat(userInput).getContent();
chatMemory.addAiMessage(response);
return response;
}
}
LangChain4j es una herramienta poderosa para desarrollar aplicaciones inteligentes en Java. Combinado con Spring, puedes crear servicios escalables que aprovechen lo mejor de la IA generativa. Desde aplicaciones conversacionales hasta sistemas de soporte, las posibilidades son infinitas.
No solamente cómo construir compiladores, sino también cómo tomar decisiones sobre la sintaxis, el sistema de tipos y la experiencia de desarrollo.
Por eso decidí comenzar un proyecto experimental: MateScript.
¿Qué es MateScript?
MateScript es un lenguaje inspirado en JavaScript, pero con tipado estático y compilación a bytecode JVM.
JavaScript es uno de los lenguajes más populares del mundo, pero también arrastra varios problemas derivados de sus decisiones históricas.
Por ejemplo:
let age = 42;
age = "hola";
Esto es perfectamente válido.
MateScript buscará detectar estos problemas durante la compilación.
let age = 42;
age = "hola";
Resultado:
Type mismatch: expected Int found String
MateScript intentará mantener las características más atractivas de JavaScript:
Pero agregando:
¿Por qué no usar TypeScript?
TypeScript es una excelente herramienta.
Sin embargo, sigue dependiendo del ecosistema JavaScript.
El objetivo de MateScript es diferente:
Primeras decisiones de diseño
Variables inmutables por defecto
let name = "Emanuel";
Una variable declarada con let no podrá modificarse.
Para variables mutables utilizaremos:
var counter = 0;
Inferencia de tipos
No será necesario declarar tipos explícitamente.
let age = 42;
El compilador inferirá:
Int
Funciones
La sintaxis será muy similar a JavaScript.
function greet(name: String): String {
return "Hola " + name;
}
Lambdas
let square = x => x * x;
Null Safety
Los tipos no aceptarán valores nulos por defecto.
let name: String = null;
Error de compilación.
Para permitir nulos:
let name: String?;
Y acá podemos hacer algo bastante elegante, podemos hacer que los tipos tengan alias por lo tanto :
String? va a ser -> Optional<String>
Ahora bien, como hago Optinal<T> , acá podemos utilizar el camino que usa Typescript union de tipos : T|Null y podemos hacer que Null sea un object como Scala. Y no vamos a tener nulos en nuestro lenguaje.
¿Por qué la JVM?
La JVM es una de las plataformas más maduras de la industria.
En lugar de construir una máquina virtual propia, MateScript aprovechará toda esa infraestructura.
¿Qué sigue?
En los próximos artículos comenzaremos la implementación de MateScript.
El primer paso será construir una gramática utilizando ANTLR capaz de reconocer programas simples como:
let name = "Emanuel";
print(name);
A partir de allí iremos avanzando gradualmente hacia un compilador completo capaz de generar bytecode JVM.
La meta no es crear el próximo Kotlin. La meta es aprender cómo se construyen realmente los lenguajes de programación.
Ahora vamos a construir nuestro primer parser desde cero.
No te preocupes si nunca usaste ANTLR. Vamos paso por paso.
Paso 1: Crear un proyecto Maven
Creamos un proyecto vacío:
mvn archetype:generate \
-DgroupId=com.assembly \
-DartifactId=mate \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
La estructura debería quedar así:
mate
│
├── pom.xml
│
└── src
├── main
│ └── java
└── test
Paso 2: Agregar ANTLR
Abrimos el pom.xml y agregamos:
<dependencies>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
Paso 3: Instalar el plugin de ANTLR
Ahora agregamos:
<build>
<plugins>
<plugin>
<groupId>org.antlr</groupId>
<artifactId>antlr4-maven-plugin</artifactId>
<version>4.13.2</version>
<executions>
<execution>
<goals>
<goal>antlr4</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Este plugin será el encargado de generar código Java a partir de nuestras gramáticas.
Paso 4: Crear la carpeta de gramáticas
Creamos:
src/main/antlr4
Quedando:
src
└── main
├── antlr4
└── java
Paso 5: Crear nuestra primera gramática
Creamos:
src/main/antlr4/Mate.g4
Contenido:
grammar Mate;
program
: 'hola' EOF
;
Nuestro lenguaje, por ahora, solamente acepta una palabra:
hola
Nada más.
Es el lenguaje más inútil de la historia.
Pero funciona.
Paso 6: Generar el parser
Ejecutamos:
mvn generate-sources
Si todo salió bien veremos algo parecido a:
Generating grammar...
Y aparecerá una carpeta nueva:
target/generated-sources/antlr4
Dentro encontraremos:
MateLexer.java
MateParser.java
MateListener.java
MateBaseListener.java
Estas clases fueron generadas automáticamente por ANTLR.
Nosotros no escribimos una sola línea de ellas.
Paso 7: Crear una clase de prueba
Creamos:
package com.mate;
import org.antlr.v4.runtime.*;
public class Main {
public static void main(String[] args) {
String source = "hola";
var lexer =
new MateLexer(
CharStreams.fromString(source));
var tokens =
new CommonTokenStream(lexer);
var parser =
new MateParser(tokens);
parser.program();
System.out.println("Programa válido");
}
}
Paso 8: Ejecutar
Corremos:
mvn compile exec:java
Resultado:
Programa válido
¿Qué pasa si el programa es inválido?
Probemos:
String source = "chau";
Ahora obtenemos:
line 1:0 mismatched input 'chau' expecting 'hola'
ANTLR detectó correctamente que nuestro programa no cumple la gramática.
¿Qué acaba de pasar?
Definimos una regla:
program
: 'hola' EOF
;
ANTLR generó automáticamente:
Y nuestro programa pudo validar si un texto respetaba o no esa regla.
int result = 10 + 20;
Sin embargo, antes de que la JVM pueda ejecutar ese programa, alguien tuvo que diseñar un compilador capaz de entender ese texto y transformarlo en bytecode.
En esta serie vamos a construir nuestro propio lenguaje para la JVM. No pretendemos competir con Kotlin o Scala, pero sí recorrer muchos de los mismos conceptos que utilizan estos lenguajes internamente.
Y el primer paso es aprender una herramienta fundamental: ANTLR.
¿Qué es ANTLR?
ANTLR (ANother Tool for Language Recognition) es una herramienta que permite generar analizadores léxicos y sintácticos a partir de una gramática.
En lugar de escribir manualmente todo el código necesario para interpretar un lenguaje, simplemente describimos las reglas del mismo y ANTLR genera automáticamente gran parte de la infraestructura necesaria.
Por ejemplo, podemos definir una regla como:
expression
: expression '+' expression
| NUMBER
;
A partir de ella ANTLR podrá reconocer expresiones como:
1 + 2 + 3
Un compilador suele dividirse en varias etapas:
Código Fuente
│
▼
Lexer
│
▼
Tokens
│
▼
Parser
│
▼
AST
│
▼
Análisis Semántico
│
▼
Generación de Código
ANTLR participa principalmente en las dos primeras:
¿Qué hace el Lexer?
El Lexer toma una secuencia de caracteres y la divide en tokens.
Por ejemplo:
let age = 42
se transforma en:
LET
IDENTIFIER(age)
ASSIGN
NUMBER(42)
El Lexer no entiende el significado del programa. Solo identifica patrones.
¿Qué hace el Parser?
El Parser toma los tokens generados por el Lexer y verifica que respeten las reglas definidas por la gramática.
Por ejemplo:
LET IDENTIFIER ASSIGN NUMBER
podría convertirse en:
Assignment
├── Variable(age)
└── Number(42)
Esta estructura representa la forma del programa y servirá como base para las siguientes etapas.
¿Qué es una gramática?
Una gramática define qué construcciones son válidas dentro de un lenguaje.
Por ejemplo:
variableDeclaration
: 'let' IDENTIFIER '=' expression
;
Esta regla indica que una declaración válida debe contener:
¿ANTLR genera compiladores?
No.
ANTLR resuelve principalmente:
Todavía debemos implementar:
Por eso ANTLR es una herramienta muy importante, pero representa solo una parte del compilador completo.
Supongamos que tenemos un objeto costoso de crear:
public class Config {
private static final ExpensiveObject INSTANCE =
new ExpensiveObject();
}
El objeto se crea cuando la clase es cargada, aunque nunca llegue a utilizarse.
Una solución clásica es utilizar el patrón Initialization-on-demand holder:
public class Config {
private static class Holder {
static final ExpensiveObject INSTANCE =
new ExpensiveObject();
}
public static ExpensiveObject instance() {
return Holder.INSTANCE;
}
}
Funciona, pero es un patrón poco evidente y algo verboso.
Con Java 26 podemos declarar un valor que será calculado únicamente la primera vez que se necesite:
private static final LazyConstant<ExpensiveObject> INSTANCE =
LazyConstant.of(ExpensiveObject::new);
y obtenerlo mediante:
ExpensiveObject obj = INSTANCE.get();
La instancia se crea una sola vez y es segura para múltiples hilos.
Veamos un ejemplo:
class DatabaseConnection {
private static final LazyConstant<ConnectionPool> POOL =
LazyConstant.of(ConnectionPool::new);
static ConnectionPool pool() {
return POOL.get();
}
}
La conexión se abrirá únicamente cuando alguien invoque:
DatabaseConnection.pool();
¿Qué ventajas tiene?
LazyConstant está pensado para recursos costosos o poco utilizados, donde tiene sentido retrasar su creación.
Durante años, Java obligó a utilizar patrones como el Holder idiom o double-checked locking para conseguir inicialización perezosa segura. Java 26 introduce LazyConstant para convertir ese patrón en una característica del lenguaje y la biblioteca estándar, haciendo el código más simple, legible y menos propenso a errores.
Pero hay una diferencia fundamental en cómo están construidos.
Scala y Kotlin generan bytecode JVM de manera bastante directa. Clojure, en cambio, adopta una filosofía distinta: es un hosted language, es decir, un lenguaje "hospedado" sobre la plataforma Java.
¿Y qué significa eso? ¿Tiene ventajas? ¿Tiene costos?
Lenguajes como Java, Scala o Kotlin siguen aproximadamente este esquema:
Código fuente
↓
Compilador
↓
Bytecode JVM (.class)
↓
JVM
Muchas características del lenguaje se traducen directamente en clases, métodos y bytecode especializado.
Por ejemplo, una case class de Scala:
case class Person(name: String)
genera automáticamente métodos como:
Todo esto queda representado directamente en bytecode.
Clojure también produce bytecode JVM, pero gran parte del comportamiento del lenguaje vive en su runtime:
Código Clojure
↓
Compilador
↓
Bytecode + Runtime de Clojure
↓
JVM
Por eso Rich Hickey describe a Clojure como un hosted language.
La plataforma Java no es solamente un destino de compilación: es el ecosistema sobre el cual está construido el lenguaje.
Ventaja: Aprovecha toda la plataforma Java
Desde Clojure podemos usar directamente cualquier clase Java:
(import java.time.LocalDate)
(LocalDate/now)
No hay puentes especiales ni adaptadores.
Clojure reutiliza:
En vez de reinventar la rueda, se apoya en ella.
Ventaja: Un compilador relativamente pequeño
Muchas características importantes de Clojure viven en bibliotecas y no en el compilador:
Esto hace que el compilador sea considerablemente más sencillo que los de Scala o Kotlin.
Ventaja: Desarrollo interactivo y REPL
Una de las mayores fortalezas de Clojure es su modelo de desarrollo interactivo.
Podemos definir una función:
(defn cuadrado [x]
(* x x))
y cargarla inmediatamente en la REPL, sin recompilar todo el proyecto.
Este enfoque favorece:
Ventaja: Evolución mediante librerías
Muchas características avanzadas de Clojure se agregaron sin modificar demasiado el lenguaje:
Al depender más del runtime y menos del compilador, la evolución del ecosistema suele ser más flexible.
Las desventajas
Menos optimizaciones
Scala y Kotlin generan bytecode más especializado.
En Clojure, muchas operaciones pasan por componentes del runtime como:
Esto introduce niveles adicionales de indirección y, en ciertos casos, puede afectar el rendimiento.
Más reflexión
Si escribimos:
(defn largo [s]
(.length s))
el compilador puede recurrir a reflexión.
Podemos ayudarlo con type hints:
(defn largo [^String s]
(.length s))
pero esto requiere intervención del programador.
Menos comprobaciones en tiempo de compilación
Scala y Kotlin verifican muchas cosas antes de ejecutar:
Clojure, al ser dinámico, detecta muchos errores recién en tiempo de ejecución.
Interoperabilidad asimétrica
Desde Clojure usar Java es muy fácil.
Pero desde Java consumir código Clojure no es tan natural:
IFn plus = Clojure.var("myns", "plus");
Mientras que una clase escrita en Scala o Kotlin suele verse como una clase Java convencional.
Dos filosofías distintas
No es que una aproximación sea mejor que la otra.
Scala y Kotlin intentan enriquecer la JVM mediante compiladores sofisticados y bytecode especializado.
Clojure adopta otra filosofía: construir un lenguaje pequeño y dinámico que reutilice al máximo la plataforma existente.
Al final, la diferencia más interesante no es a qué compilan, porque todos terminan ejecutándose en la JVM.
La verdadera diferencia es: ¿Cuánto del lenguaje vive en el compilador y cuánto vive en el runtime?
Y en esa decisión de diseño está gran parte de la personalidad de Clojure.
Pero existe una opción intermedia muy interesante: Jdbi.
JDBI busca mantener la simplicidad y el control de SQL, pero eliminando gran parte del código repetitivo de JDBC.
¿Qué es JDBI?
JDBI es una librería que se construye sobre JDBC y agrega:
La idea es poder escribir SQL real sin tener que pelear con ResultSet, PreparedStatement y toneladas de try/catch.
Dependencia Maven:
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-core</artifactId>
<version>3.49.6</version>
</dependency>
Si usamos una base de datos específica, agregamos el driver correspondiente.
Por ejemplo para MySQL:
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
Crear una conexión:
Jdbi jdbi = Jdbi.create(
"jdbc:mysql://localhost:3306/test",
"root",
"password"
);
Ejecutar una consulta
Supongamos esta tabla:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
age INT
);
Y esta clase:
public class User {
private Long id;
private String name;
private Integer age;
// getters y setters
}
Ahora podemos consultar así:
List<User> users = jdbi.withHandle(handle ->
handle.createQuery("SELECT * FROM users")
.mapToBean(User.class)
.list()
);
Fijate que desaparece todo el manejo manual de ResultSet.
Insertar datos:
jdbi.useHandle(handle ->
handle.createUpdate("""
INSERT INTO users(id, name, age)
VALUES(:id, :name, :age)
""")
.bind("id", 1L)
.bind("name", "Emanuel")
.bind("age", 30)
.execute()
);
Parámetros nombrados
Una de las mejores cosas de JDBI es evitar los clásicos:
statement.setString(1, ...);
statement.setInt(2, ...);
En su lugar:
.bind("name", "Juan")
Mucho más legible.
JDBI también permite definir interfaces tipo DAO.
public interface UserDao {
@SqlQuery("SELECT * FROM users WHERE id = :id")
User findById(@Bind("id") Long id);
@SqlUpdate("""
INSERT INTO users(id, name, age)
VALUES(:id, :name, :age)
""")
void insert(
@Bind("id") Long id,
@Bind("name") String name,
@Bind("age") Integer age
);
}
Y luego:
UserDao dao = jdbi.onDemand(UserDao.class);
User user = dao.findById(1L);
¿Por qué mucha gente lo elige?
Porque queda en un punto muy cómodo. No es tan automático como JPA y permite hacer optimizaciones fácilmente, pero no es tan trabajoso como JDBC.
JDBI se integra muy bien con Spring Framework.
Dependencia:
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-spring5</artifactId>
<version>3.49.6</version>
</dependency>
Configuración:
@Configuration
public class JdbiConfig {
@Bean
public Jdbi jdbi(DataSource dataSource) {
Jdbi jdbi = Jdbi.create(dataSource);
jdbi.installPlugin(new SqlObjectPlugin());
return jdbi;
}
}
Crear el DAO como Bean
@Configuration
public class DaoConfig {
@Bean
public UserDao userDao(Jdbi jdbi) {
return jdbi.onDemand(UserDao.class);
}
}
Usarlo desde un Service
@Service
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public User getUser(Long id) {
return userDao.findById(id);
}
}
¿Cuándo conviene usar JDBI?
JDBI suele ser una muy buena opción cuando:
Especialmente en microservicios o aplicaciones donde el modelo relacional es importante, JDBI puede resultar mucho más simple y transparente que un ORM completo.
Dejo link: https://jdbi.org
Java vs C#
🇯🇵 Java sale con Spring Boot.
🇺🇸 C# responde con ASP.NET Core.
Ambos tienen experiencia internacional, planteles maduros y una enorme cantidad de títulos.
Resultado: Empate 1-1.
> Si tenés que hacer una API corporativa, cualquiera de los dos te lleva a octavos.
🏟️ Partido 2: Concurrencia
Elixir vs Go
🇧🇷 Elixir apuesta por la posesión y el juego asociado con actores y supervisores.
🇩🇪 Go juega más directo con goroutines y channels.
Elixir domina el mediocampo cuando aparecen millones de procesos concurrentes.
Resultado: Elixir 2 - Go 1.
La BEAM sigue siendo una de las ligas más difíciles de enfrentar.
🏟️ Partido 3: Aplicaciones Standalone
C++ vs Rust
🇮🇹 C++ es el veterano con ocho mundiales encima.
🇳🇱 Rust llega con una generación joven y una defensa impecable.
Rust reduce errores, pero C++ todavía tiene una experiencia enorme.
Resultado: C++ 1 - Rust 1.
La leyenda sigue vigente, pero la promesa pide pista.
🏟️ Partido 4: Cloud
Java vs Node.js
🇦🇷 Java viene con GraalVM y Spring Boot.
🇪🇸 Node.js juega rápido y con poco peso.
En microservicios y aplicaciones cloud modernas, ambos se sienten cómodos.
Resultado: Empate.
El VAR determina que la elección depende más del equipo que del lenguaje.
🏟️ Partido 5: Inteligencia Artificial
Python vs el resto del mundo
🇫🇷 Python entra a la cancha.
Resultado: Python 5 - 0.
Hubo partido durante cinco minutos.
🏟️ Partido 6: Programación Funcional
Scala vs Haskell
🇺🇾 Scala combina técnica y pragmatismo.
🇭🇷 Haskell juega un fútbol hermoso que pocos entienden.
Resultado: Scala 2 - Haskell 1.
La posesión fue 90% para Haskell, pero Scala convirtió las que tuvo.
🏟️ Partido 7: Frontend
TypeScript vs JavaScript
🇵🇹 JavaScript es el capitán histórico.
🇵🇹 TypeScript es el mismo equipo, pero con preparador físico.
Resultado: TypeScript 3 - JavaScript 1.
Los errores en tiempo de compilación fueron la figura del partido.
🏟️ Partido 8: Bases de Datos
PostgreSQL vs MySQL
🇨🇴 PostgreSQL juega con mucha técnica.
🇲🇽 MySQL apuesta por la experiencia.
Resultado: PostgreSQL 2 - MySQL 1.
PostgreSQL ganó por posesión y calidad de juego.
Y listo, no hice mucho, ni dice mucho pero es divertido :P
Su objetivo es permitir que una aplicación Java pueda:
La FFM API forma parte del proyecto Project Panama.
Pero demos un paso para atras, ¿Por qué existía JNI?
Durante años, si una aplicación Java necesitaba:
la única opción era usar JNI (Java Native Interface)
El problema es que JNI:
Además, obliga a escribir:
Puff si no habre puteado con JNI.
¿Qué propone la FFM API?
La FFM API permite hacer esto:
printf("Hola desde C!\n");
...directamente desde Java.
Y además:
Todo usando una API moderna.
La FFM API tiene dos pilares:
1. Foreign Function
Permite invocar funciones nativas.
Por ejemplo:
2. Foreign Memory
Permite manejar memoria fuera del heap de Java.
Esto es importante porque:
La FFM API permite trabajar con esa memoria de manera segura.
Primer ejemplo: llamar a printf
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import static java.lang.foreign.ValueLayout.*;
public class Main {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MemorySegment printfAddress =
stdlib.find("printf")
.orElseThrow();
FunctionDescriptor printfSignature =
FunctionDescriptor.of(JAVA_INT, ADDRESS);
MethodHandle printf = linker.downcallHandle(
printfAddress,
printfSignature
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment text =
arena.allocateFrom("Hola desde C!\n");
printf.invoke(text);
}
}
}
¿Qué está pasando acá?
Linker linker = Linker.nativeLinker();
Obtiene un linker capaz de interactuar con funciones nativas.
SymbolLookup stdlib = linker.defaultLookup();
Busca símbolos exportados por librerías nativas.
stdlib.find("printf")
Obtiene la dirección de memoria de la función.
Muy parecido a: void* ptr = dlsym(...)
FunctionDescriptor.of(JAVA_INT, ADDRESS)
Describe la firma de la función.
En este caso: int printf(char*)
linker.downcallHandle(...)
Crea un MethodHandle que permite invocar la función nativa.
Manejo de memoria con Arena
Uno de los conceptos más importantes es:
Un Arena administra memoria nativa.
try (Arena arena = Arena.ofConfined()) {
}
Cuando el bloque termina:
La memoria nativa se representa con: MemorySegment
Es básicamente:
Y para reservar memoria hacemos:
MemorySegment segment = arena.allocate(4);
Por ejemplo escribir y leer un entero:
segment.set(JAVA_INT, 0, 42);
int value = segment.get(JAVA_INT, 0);
Supongamos esta estructura:
struct Point {
int x;
int y;
};
Podemos modelarla en Java.
StructLayout POINT = MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
);
Y para reservar memoria hacemos:
MemorySegment point = arena.allocate(POINT);
Escribir los campos:
point.set(JAVA_INT, 0, 10);
point.set(JAVA_INT, 4, 20);
¿Qué ventajas tiene?
Menos complejidad
No hace falta:
¿Puede FFM API reemplaza completamente JNI?
Todavía no en todos los casos.
Hay escenarios avanzados donde JNI sigue siendo necesario.
Pero para muchísimos casos:
la FFM API es mucho mejor.
users.stream()
.filter(u -> u.getAge() > 50)
.toList();
users.Where(u => u.Age > 50).ToList();
Pero internamente… son dos mundos completamente distintos.
Y eso cambia todo.
En Java, esto:
Predicate<User> p = u -> u.getAge() > 50;
Se compila a bytecode usando invokedynamic
Es equivalente a:
class Lambda implements Predicate<User> {
public boolean test(User u) {
return u.getAge() > 50;
}
}
No hay forma de saber:
Solo podés ejecutarla, no analizarla
Por lo tanto, siempre ocurre en memoria. Nunca puede transformarse en sql o en nada
En .NET LINQ, esto:
users.Where(u => u.Age > 50)
Puede ser interpretado como:
Expression<Func<User, bool>>
Esto NO es una función
Es un árbol de expresión (AST)
¿Cómo se ve ese AST?
Conceptualmente:
GreaterThan
├── MemberAccess (Age)
└── Constant (50)
O en código:
BinaryExpression(
left: MemberExpression("Age"),
operator: GreaterThan,
right: Constant(50)
)
Ahora sí podés:
¿Por qué Java eligió este camino?
Java priorizó:
Las lambdas fueron diseñadas como: “syntactic sugar” sobre interfaces funcionales
No como estructuras analizables.
¿Se podría implementar LINQ en Java?
Sí… pero requeriría:
Streams y LINQ no son equivalentes.
La clave es entender qué significa realmente “expresión” al estilo LINQ.
Primero: ¿qué es una “expresión” en LINQ?
En .NET LINQ no estás pasando una función común.
x => x.Age > 50
Eso puede ser dos cosas distintas en C#:
Func<User, bool> // código ejecutable
Expression<Func<User, bool>> // árbol de expresión (AST)
En el segundo caso, el compilador no genera bytecode directo, sino una estructura tipo:
GreaterThan
├── MemberAccess (Age)
└── Constant (50)
En Java eso NO existe
x -> x.getAge() > 50
Siempre es: Predicate<User>
Y eso es:
Entonces… ¿por qué Java no puede simplemente agregarlo?
Puede… pero implicaría cambios profundos
1. Cambiar cómo funcionan las lambdas
Hoy en Java:
Predicate<User> p = x -> x.getAge() > 50;
Se compila usando invokedynamic
Es básicamente una función
Para soportar expresiones, Java necesitaría algo como:
Expression<User, Boolean> expr = x -> x.getAge() > 50;
Eso implica que el compilador:
2. Falta un AST estándar en el lenguaje
Java no tiene algo como:
expr.getBody(); // árbol de nodos
Tendrías que definir:
Básicamente: meter un mini compilador dentro del lenguaje
3. Problema de ambigüedad (esto es clave)
¿Qué pasa con esto?
x -> x.getAge() > calcularEdadMinima()
¿Eso es SQL?
❌ No siempre
❌ Puede depender de lógica Java
❌ Puede tener efectos secundarios
LINQ tiene reglas estrictas para esto… Java no.
4. Compatibilidad hacia atrás (el verdadero monstruo)
Java es ultra conservador.
Si agregás expresiones:
C# lo resolvió desde el diseño… Java llegó tarde (lambdas en Java 8).
5. Filosofía del lenguaje
Java prioriza:
LINQ introduce:
Eso no encaja del todo con el ADN de Java
Entonces… ¿es imposible?
No. Para nada.
De hecho, Java ya hace algo parecido, pero explícito:
Todos construyen un AST… pero a mano
La posta (la diferencia real)
No es que Java “no pueda”.
Es que hoy:
Conclusión
Java podría implementar algo tipo LINQ si:
Pero eso implicaría:
cambiar una de las decisiones fundamentales de cómo Java entiende las lambdas
repoPersona
.filter(p -> p.getEdad() > 50)
.toList();
Y que mágicamente se transforme en:
SELECT * FROM persona WHERE edad > 50;
Sería hermoso.
Pero… ¿no son lazy los streams?
Sí, los streams en Java son lazy:
users.stream()
.filter(u -> u.getAge() > 50)
.toList();
No se ejecuta hasta el final
Peeeero...
Lazy no es lo mismo que interpretable
En Java, esto:
u -> u.getAge() > 50
Se compila a bytecode
No queda como estructura accesible
Es decir:
¿Por qué en C# sí se puede?
En .NET LINQ pasa algo distinto:
repo.Where(x => x.Age > 50)
Eso no es una función normal
Es: Expression<Func<T, bool>>
El código es datos (un AST)
Por esta razón necesitamos de cosas como Specification de jpa para tipar nuestras querys :(
Java Streams no pueden ser LINQ porque: Java no puede “leer” lambdas como datos
Y eso obliga a usar soluciones como Specifications.
Con Jakarta Persistence 4.0 eso cambia.
Esta nueva versión incorpora características largamente esperadas, mejora la seguridad de tipos y prepara el terreno para trabajar junto a Jakarta Data. Veamos qué trae de nuevo.
¿Qué es Jakarta Persistence?
Jakarta Persistence es la especificación estándar para el mapeo objeto-relacional (ORM) en Java.
Permite representar tablas como objetos Java y realizar operaciones CRUD sin escribir SQL para cada interacción.
@Entity
public class Cliente {
@Id
private Long id;
private String nombre;
}
Implementaciones populares incluyen:
Novedades más importantes de Jakarta Persistence 4.0:
1. EntityAgent: trabajar sin Persistence Context
Una de las novedades más llamativas es la incorporación de EntityAgent.
Hasta ahora, casi todas las operaciones pasaban por un EntityManager y un Persistence Context.
entityManager.persist(cliente);
Con EntityAgent se pueden realizar operaciones sobre entidades desacopladas del contexto de persistencia, simplificando ciertos escenarios y reduciendo overhead.
Conceptualmente:
entityAgent.insert(cliente);
Esto acerca la API a modelos más ligeros y modernos.
2. Carga masiva por ID
Un problema clásico:
for(Long id : ids) {
entityManager.find(Cliente.class, id);
}
Esto puede generar el famoso problema N+1.
Jakarta Persistence 4.0 incorpora soporte para obtener múltiples entidades por identificador en una sola operación.
La ventaja:
3. Consultas más seguras con Static Query
Una crítica frecuente a JPA era que las consultas JPQL eran simples Strings.
@Query("select c from Cliente c")
Si el nombre de una propiedad cambiaba, el error aparecía recién en tiempo de ejecución.
Persistence 4.0 incorpora una API de Static Queries que permite mayor verificación en compilación.
Beneficios:
4. Result Set Mapping programático
Hasta ahora era habitual definir mapeos complejos mediante anotaciones.
@SqlResultSetMapping(...)
La nueva versión agrega una API programática para definir estos mappings.
Ventajas:
5. Nuevas capacidades para Entity Graphs
Los Entity Graphs fueron introducidos para controlar qué relaciones se cargan.
@EntityGraph(attributePaths = {
"pedidos",
"direccion"
})
Persistence 4.0 mejora significativamente esta funcionalidad:
Nuevas anotaciones.
Mejor integración con operaciones existentes.
Uso de Entity Graphs en refresh().
6. Soporte para entidades Read-Only
En muchos escenarios solo queremos leer datos.
Antes:
Cliente cliente =
entityManager.find(Cliente.class, id);
La entidad quedaba administrada por el contexto.
Persistence 4.0 agrega soporte explícito para carga en modo solo lectura.
Beneficios:
7. @PreMerge
Hasta ahora existían callbacks como:
@PrePersist
@PostPersist
@PreUpdate
@PostUpdate
Ahora aparece:
@PreMerge
public void beforeMerge() {
System.out.println("Fusionando entidad");
}
Esto permite interceptar el proceso de merge antes de que ocurra.
8. Excluir campos del Optimistic Locking
Hasta ahora cualquier modificación podía afectar el control de versiones.
Persistence 4.0 incorpora una anotación para excluir ciertos atributos del mecanismo de optimistic locking.
Ideal para:
9. select new implícito
Actualmente:
select new ClienteDTO(
c.id,
c.nombre
)
from Cliente c
Persistence 4.0 simplifica este escenario permitiendo inferir automáticamente el DTO cuando se especifica el tipo de resultado.
10. Preparándose para Jakarta Data
Quizás el cambio más importante no sea una característica puntual.
Muchas de las nuevas APIs fueron diseñadas para trabajar mejor con Jakarta Data
La idea es ofrecer una experiencia similar a:
pero de forma estandarizada dentro del ecosistema Jakarta EE.
¿Vale la pena actualizar?
la respuesta es sí.
Las mejoras apuntan a problemas reales que los desarrolladores vienen sufriendo desde hace años:
Jakarta Persistence 4.0 representa probablemente la evolución más importante de JPA desde la llegada de los Entity Graphs y los Stored Procedures.
La incorporación de EntityAgent, Static Queries, Result Set Mapping programático, mejoras en Entity Graphs y nuevas capacidades de carga hacen que la especificación se vea mucho más moderna y preparada para competir con las soluciones de persistencia actuales.
Para quienes pensaban que JPA había quedado estancado, Jakarta Persistence 4.0 demuestra exactamente lo contrario.
Dejo link:
https://jakarta.ee/specifications/persistence/4.0/
CLI + correlación de datos
1. Encontrar el proceso
jps -l
o directamente:
ps aux | grep java
guardate el PID
2. Confirmar que el problema es CPU
top -p <pid>
o mejor:
top -H -p <pid>
Esto muestra threads individuales
Acá está la clave:
3. Convertir TID a HEX (clave para Java)
Java muestra threads en hexadecimal, Linux en decimal.
printf "%x\n" <tid>
Ejemplo:
12345 → 3039
4.Sacar thread dump
jstack <pid> > dump.txt
5. Buscar el thread problemático
Dentro del dump:
grep -i 3039 dump.txt
Ese es el thread que está consumiendo CPU
¿Qué vas a encontrar? Generalmente algo así:
"id=123 nid=0x3039 runnable"
Y abajo el stack:
at com.miapp.CalculoPesado.procesar(CalculoPesado.java:42)
at com.miapp.Service.loop(Service.java:88)
¡encontraste el problema!
¿Dónde entra jstat en todo esto?
Acá es donde sumás contexto.
Ejecutá:
jstat -gcutil <pid> 1000
Qué mirar para CPU alto
Caso 1: GC excesivo
CPU alto por Garbage Collector
Caso 2: Full GC frecuentes
pausas pesadas → CPU + latencia
Caso 3: GC normal
Entonces el problema es código, no GC
Pero en entornos reales (servidores, contenedores, CI), necesitás herramientas sin GUI. Ahí aparece jstat.
Una herramienta liviana, incluida en la JVM, que te permite ver qué está pasando con la memoria y el Garbage Collector en tiempo real.
jstat (Java Virtual Machine Statistics Monitoring Tool) es una utilidad de consola que permite consultar métricas internas de la JVM.
Primero necesitás el PID del proceso:
jps -l
Después ejecutás:
jstat -gc <pid> 1000
Esto imprime estadísticas cada 1 segundo.
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
1024.0 1024.0 0.0 512.0 8192.0 4096.0 16384.0 8000.0 5120.0 4800.0 1024.0 900.0 10 0.25 2 0.50 0.75
Ahora lo importante: entender esto 👇
Generación joven (Young Gen)
S0C / S1C → tamaño de Survivor 0 y 1
S0U / S1U → uso actual
EC → tamaño de Eden
EU → uso de Eden
Acá viven los objetos nuevos.
Generación vieja (Old Gen)
OC → capacidad
OU → uso
👉 Si esto crece mucho y no baja → posible problema de memoria.
Metaspace
MC / MU → capacidad y uso
CCSC / CCSU → Compressed Class Space
👉 Relacionado con clases cargadas.
YGC → cantidad de GC jóvenes
YGCT → tiempo total en GC joven
FGC → cantidad de Full GC
FGCT → tiempo total en Full GC
GCT → tiempo total de GC
Veamos un caso saludable
👉 Todo OK.
Posible memory leak
👉 Algo está reteniendo objetos.
🟠 Problema de GC
👉 La app está gastando demasiado tiempo en GC.
⚙️ Otros modos útiles
-gcutil (más simple y legible)
jstat -gcutil <pid> 1000
Salida en porcentajes:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 50.00 60.00 48.00 93.75 87.89 10 0.25 2 0.50 0.75
👉 Ideal para monitoreo rápido.
Ejecutar N veces
jstat -gc <pid> 1000 10
👉 Ejecuta 10 veces y termina.
jstat es una herramienta simple pero extremadamente poderosa.
✔ Ideal para servidores sin GUI
✔ Perfecta para debugging en vivo
✔ Fundamental para entender el GC