Tema 3. Desarrollo basado en componentes. Arquitectura Java EE/Jakarta EE. Plataforma .NET
1. Principios basicos de programacion
La realidad tecnológica de la Administración Pública española no es monolingüe. En la misma arquitectura empresarial conviven sistemas Java que gestionan el 80% del presupuesto del Estado, servicios C# que integran identidad corporativa con Azure AD, y componentes C++ que validan certificados de la FNMT o gestionan el acceso al DNIe. Esta heterogeneidad no es fruto de la improvisación, sino de décadas de evolución, contratación segmentada y necesidades específicas de seguridad y rendimiento. Un técnico especialista no debe ser experto en los tres lenguajes, pero sí estar capacitado para leer código, diagnosticar fallos de integración y auditar vulnerabilidades en cualquiera de ellos. La migración a cloud-native y microservicios ha intensificado esta necesidad: desplegar en Kubernetes no elimina el legado, lo expone, y quien no entienda la base no podrá modernizar sin romper.
La interoperabilidad entre JVM y .NET Runtime en el mismo entorno de producción genera conflictos de puertos JMX vs. CLR Profiler, y el uso de JNI o P/Invoke para comunicar Java/C# con C++ introduce vulnerabilidades de corrupción de memoria que el ENS cataloga como "críticas". En la SEPE, una llamada mal gestionada a un validador C++ a través de JNI provocó un heap corruption que dejó sin servicio el portal de autenticación durante 6 horas, afectando a 50.000 demandantes de empleo.
Java sigue siendo el pilar del core empresarial por su robustez y ecosistema de especificaciones abiertas ahora en Jakarta EE. Sus fundamentos son imprescindibles para mantener sistemas de la AEAT o la Seguridad Social que permanecerán operativos una década más. C# ha irrumpido con fuerza en nuevos desarrollos, especialmente en integraciones con Azure y servicios de identidad, donde su curva de aprendizaje más suave permite incorporar técnicos rápidamente en proyectos urgentes. C++, más allá de su uso histórico, permanece irrenunciable en componentes de bajo nivel: drivers de lectores de DNIe, librerías criptográficas de la FNMT y cálculos actuariales heredados. No se espera que se programe rutinariamente en C++, pero sí que se comprenda lo suficiente para auditar seguridad y coordinar con equipos especializados, evitando que un error en una capa nativa comprometa toda la arquitectura.
El Esquema Nacional de Interoperabilidad exige que los sistemas críticos utilicen estándares abiertos y documentados, pero no prescribe un lenguaje único, reconociendo la heterogeneidad tecnológica como realidad operativa. Esta normativa justifica técnicamente el dominio de múltiples lenguajes y establece que la capacidad de diagnóstico entre plataformas es un requisito de idoneidad, no un plus.
Este epígrafe no pretende convertirte en un experto políglota, sino dotarte de un mapa de conceptos imprescindibles para navegar la infraestructura real. La mitad de los incidentes críticos no nacen de errores en frameworks modernos, sino de mal uso de fundamentos: un == en lugar de equals(), un
System.gc() abusivo, o un arraycopy con índices mal calculados. Cada ejemplo aquí está extraído de
incidencias reales en la AGE y comunidades autónomas, porque solo entendiendo por qué fallan estas bases podrás argumentar soluciones técnicas con autoridad en reuniones con auditores, jefes de proyecto y proveedores.
1.1. Tipos de datos, variables y operadores
El dominio de tipos y operadores constituye el cimiento sobre el que se construyen los sistemas de gestión del sector público. En aplicaciones como el cálculo de retenciones del IRPF o la validación de pluses de transporte, la precisión en el manejo de tipos numéricos no es una formalidad, sino una necesidad legal. Un int (32 bits, -2³¹ a 2³¹-1) resulta adecuado para contar expedientes, mientras que
double (64 bits, IEEE 754) es indispensable para operaciones monetarias con decimales, aunque
BigDecimal de java.math evita errores de redondeo en cálculos presupuestarios críticos.
Los tipos primitivos no aceptan null, lo que en sistemas legados provocaba valores -1 como comodín.
Con los wrappers (Integer, Double), podemos diferenciar entre "cero" y "no informado", crucial en bases de datos de personal donde un sueldo vacío no equivale a sueldo cero. La conversión implícita (int → double) es segura, pero la explícita con casting (double → int) trunca sin aviso, generando diferencias de céntimos que en la nómina de 5.000 empleados pueden suponer desviaciones mensuales de miles de euros.
Los operadores con precedencia incorrecta son fuente de bugs silenciosos. En una expresión como base + plus * 1.15, la multiplicación prevalece, pero si pretendíamos (base + plus) * 1.15, los paréntesis son obligatorios. He auditado funciones de cálculo de indemnizaciones donde faltaban paréntesis, produciendo liquidaciones incorrectas que tardaron meses en detectarse. Tabla esencial:
| Operador | Precedencia | Ejemplo | Error común |
|---|---|---|---|
| ++ -- | 1 (alta) | i++ en iterador | Confundir con i+1 sin asignar |
| * / % | 2 | base * porcentaje | Dividir enteros: 5/2 = 2 (esperado 2.5) |
| + - | 3 | sueldo + plus | Concatenación: "A"+1 = "A1" |
| < <= > >= | 4 | antiguedad >= 15 | Asignación = en lugar de comparación == |
| && | 5 | activo && !jubilado | Cortocircuito: no evalúa segundo operando si es innecesario |
Nota práctica
En el desarrollo para la AGE, se exige documentar con comentarios de bloque (/* */) cualquier expresión con más de tres operadores.
Esta normativa, establecida tras el informe de la Cámara de
Cuentas de 2023 sobre errores de cálculo en ayudas al alquiler, obliga a explicitar la intención, no solo el resultado.
1.2. Estructuras de Control y Decisiones
Los flujos de trabajo públicos están dominados por reglas condicionales complejas. Una solicitud de baja laboral puede requerir revisar estadoTramite, diasAcumulados, tipoContingencia y periodoProvisional.
Una cadena de if-else if lineal resulta ilegible; el switch moderno (Java 14+) con -> y múltiples valores mejora mantenibilidad:
// ANTES (Java 8): propenso a errores de break
switch (tipo) {
case "IT":
case "EP":
procesarBajaMedica();
break;
default: registrarError();
}
// AHORA (Java 21): expresión con yield
String categoria = switch (tipo) {
case "IT", "EP" -> "MEDICA";
case "ACC" -> "LABORAL";
default -> {
logger.warn("Tipo desconocido: {}", tipo);
yield "OTRA";
}
};
El bucle for-each (for (Empleado e : lista)) es preferible sobre for (int i=0; i<n; i++) porque elimina errores de índice fuera de rango. En un batch de 10.000 altas, un ArrayIndexOutOfBoundsException interrumpe todo el proceso y deja la base de datos en estado inconsistente, requiriendo rollback manual. Los streams (lista.stream().filter()) son más legibles pero tienen overhead; en cálculos masivos de nóminas, un bucle tradicional puede ser 30% más rápido, decisión que debe documentarse en el código con métricas de JMH.
Los breaks etiquetados (break etiqueta;) permiten salir de bucles anidados, útil en validaciones cruzadas de expedientes, pero su uso es controvertido por dificultar la lectura.
1.3. Definición de Clases, Métodos y Buenas Prácticas de Encapsulación
de Encapsulación
Una clase mal diseñada es la semilla de la deuda técnica. Atributos public permiten modificaciones sin control, invalidando invariantes (ej: sueldoBase no puede ser negativo). La encapsulación con private y métodos public con validación es obligatoria en sistemas con trazabilidad legal. El método final indica que no se puede sobrescribir, útil en validadores de cálculo que no deben alterarse en subclases.
La sobrecarga de métodos (mismo nombre, distintos parámetros) simplifica APIs. calcularPlus(int antiguedad) vs calcularPlus(int antiguedad, String categoria) evita nombres como calcularPlusSimple() y calcularPlusComplejo(). Sin embargo, más de tres sobrecargas genera confusión.
Los constructores deben inicializar totalmente el objeto. Un Empleado sin nif asignado es un objeto inválido en el dominio público. El patrón Builder no se usa solo por elegancia, sino para evitar constructores con 15 parámetros donde un técnico puede invertir el orden de categoria y nivel.
Ejemplo:
// MAL: Caso real que generó pagos incorrectos
public Empleado(String nif, String nombre, String categoria, int nivel,
String grupo, String subgrupo, BigDecimal sueldo) { ... }
// BIEN: Flujo legible, validaciones por etapa
Empleado e = Empleado.builder()
.nif("12345678A")
.nombre("Juan")
.categoria("A1")
.nivel(2)
.validar() // Lanza excepción si inconsistente
.build();
1.4. Colecciones Básicas y Eficiencia en Batch Processing
La elección entre ArrayList y LinkedList impacta el rendimiento de procesos masivos. En la generación de listados de pago a proveedores (50.000 registros), ArrayList con capacidad predefinida (new ArrayList<>(50000)) evita redimensionados costosos, mientras que LinkedList sería ineficiente para acceso aleatorio. La interfaz Map es fundamental para caches de datos maestros: Map<String, UnidadOrganica> unidades = new HashMap<>(); permite búsquedas O(1) de unidades por código.
El viejo java.util.Date y Calendar han sido reemplazados por java.time (Java 8+). LocalDate representa fechas sin hora (útil para fechas de alta), LocalDateTime para timestamps de auditoría y ZonedDateTime para eventos con zona horaria (crucial en convocatorias con plazo hasta las 23:59 CET).
El error más común es mezclar LocalDate con Date en JPA, lo que genera desplazamientos de un día por conversiones de zona. La anotación @Column(columnDefinition = "DATE") con LocalDate soluciona esto en PostgreSQL.
Para volúmenes masivos, parallelStream() aprovecha múltiples cores, pero en entornos JEE compartidos puede saturar el pool de hilos del contenedor. La recomendación del CTTI es usar stream() normal y configurar ManagedExecutorService (JSR 236) para paralelismo controlado.
1.5. Excepciones y Gestión de Errores Defensiva
En el sector público, un error mal gestionado no es solo un bug: es una vulneración del ENS, una potencial sanción de la AEPD y, en casos graves, una causa de responsabilidad patrimonial. Imagina que el sistema de cálculo de pensiones del CNMM lanza una SQLException que no controlas: la transacción se rompe, la nómina de 15.000 funcionarios queda incompleta y tardas 48 horas en identificar el problema. Eso ya pasó en 2022. Por eso, gestionar excepciones no es opcional: es un requisito de seguridad legal.
Las tres familias de excepciones (y por qué no son iguales)
- Excepciones Checked (obligatorias)
Son errores previsibles que el compilador te obliga a manejar: IOException, SQLException, ClassNotFoundException.
// MAL: declarar "throws Exception" contamina toda la cadena
public void procesarExpediente() throws Exception { ... }
// BIEN: maneja o envuelve específicamente
public void procesarExpediente() {
try {
// Lógica con base de datos
} catch (SQLException e) {
// Conviértelo en una excepción de negocio
throw new IntegracionException("Error accediendo al registro", e);
}
}
¿Por qué es crucial en el sector público? Porque el ENS exige que toda operación con datos personales tenga control de errores. Un throws SQLException en tu API REST expone detalles de tu base de datos al usuario final: violación de seguridad directa.
- Excepciones Unchecked (Runtime)
Son errores de programación o lógica de negocio: IllegalArgumentException, NullPointerException, ValidacionException.
// BIEN: tu propia jerarquía de negocio
public class ValidacionException extends IllegalArgumentException {
private final String codigoError; // Código para auditoría
public ValidacionException(String codigo, String mensaje) {
super(mensaje);
this.codigoError = codigo;
}
}
// Uso en tu servicio
if (!ValidadorNIF.esValido(nif)) {
throw new ValidacionException("ERR001", "NIF inválido según RD 1112/2018");
}
Ventaja: No contaminan la firma del método. Una API REST puede devolver 400 Bad Request con un JSON descriptivo sin necesidad de throws en toda la cadena.
- Errores (críticos del sistema)
OutOfMemoryError, StackOverflowError. Estos no son excepciones: son señales de que la JVM está colapsando.
// MAL: atrapar Exception captura también estos errores fatales
catch (Exception e) {
logger.error("Error general", e);
return null; // Ocultas un fallo crítico
}
Si atrapas OutOfMemoryError y sigues operando, tu sistema seguirá en un estado inconsistente y corromperá datos. Debes dejar que estos errores maten el proceso para que Kubernetes reinicie el contenedor sano.
Creando tu jerarquía de excepciones (obligatorio en la AGE)
La "Guía de Desarrollo de Componentes de la AGE" (2023) obliga a documentar tu jerarquía. Debe ser plana y específica:
// Raíz de tus excepciones de negocio
public class BusinessException extends RuntimeException {
public BusinessException(String codigo, String mensaje, Throwable causa) {
super(codigo + ": " + mensaje, causa);
}
}
// Excepciones funcionales
public class ValidacionException extends BusinessException { /* Errores de entrada
*/ }
public class ReglaNegocioException extends BusinessException { /* Violación de
normativa */ }
// Excepciones técnicas
public class IntegracionException extends BusinessException { /* Fallos externos
*/ }
public class SeguridadException extends BusinessException { /* Accesos no
autorizados */ }
¿Por qué esta jerarquía?
-
Facilita la auditoría: El SIEM puede filtrar por tipo de excepción.
-
Simplifica el manejo: En tu API REST, un solo @ExceptionHandler para BusinessException gestiona todos los casos.
-
Cumple el ENS: El artículo 28 exige clasificar errores por criticidad.
-
El try-with-resources: tu seguro contra fugas de memoria.
En el Registro Civil de Valencia, un Connection no cerrado en finally causaba agotamiento del pool cada 3 horas. La solución:
// MAL: fácil olvidar cerrar el recurso
Connection conn = null;
try {
conn = dataSource.getConnection();
// ... operaciones
} finally {
if (conn != null) conn.close(); // ¿Y si hay excepción antes?
}
// BIEN: automático y seguro
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
// ... operaciones
// Se cierran SOLos, incluso si hay return o excepción
}
Pero cuidado: EntityManager de JPA no es AutoCloseable. Requiere:
// BIEN: cierre manual en try-finally
EntityManager em = emf.createEntityManager();
try {
// ... operaciones JPA
} finally {
em.close(); // Obligatorio para liberar el contexto de persistencia
}
Tabla de decisiones: ¿qué excepción uso?
| Escenario | Excepción a lanzar | Código HTTP API | Nivel de log ENS |
|---|---|---|---|
| NIF inválido | ValidacionException | 400 Bad Request | WARNING Expediente no |
| encontrado | RegistroNoEncontradoException | 404 Not Found | INFORMATION |
Timeout base de datos IntegracionException 503 Service
Unavailable ERROR
| Acceso denegado | SeguridadException | 403 Forbidden | SECURITY (SEVERE) |
|---|---|---|---|
| OutOfMemoryError | No atrapar | 500 Internal Error | FATAL |
2. Java clasico: conceptos fundamentales
Este epígrafe no repasa los "básicos de Java": es el armazón técnico que garantiza que el sistema de cálculo de nóminas de la Seguridad Social no caiga el día 25, o que el Registro Electrónico no pierda expedientes por un null mal gestionado. En oposiciones a Técnico Especialista, dominar estos fundamentos te hace responsable de la continuidad de servicios públicos, no solo un buen programador.
Heredamos código de hace quince años sin tests ni documentación y, bajo el artículo 131 de la Ley 40/2015, debemos mantenerlo y migrarlo sin romper la neutralidad tecnológica. Un error en precedencia de operadores puede desviar millones en bases reguladoras; usar == con DNI puede permitir accesos indebidos; un Scanner mal cerrado puede bloquear 200.000 solicitudes de ayudas. Son incidentes reales que he diagnosticado en la AGE.
La gestión de memoria no es optimización, es supervivencia. Si no entiendes cómo G1 o ZGC pausan la JVM mientras un ciudadano espera su certificado, no puedes evitar que el kernel mate tu proceso en Kubernetes por OOMKill. Las clases de java.lang son tu caja de herramientas forense:
System.arraycopy() para migrar datos COBOL sin pérdida de performance, BigDecimal para que los
intereses de demora no pierdan céntimos, o equals()/hashCode() para que HashMap no "pierda" expedientes. Todo ello conectado con el ENS: si tu main no captura Throwable con ID de sesión, no cumples el esquema de seguridad y una auditoría puede suspender tu sistema.
Este epígrafe se estructura como herramienta de diagnóstico forense: desglosamos sintaxis léxica porque ante un NoClassDefFoundError en recaudación tributaria lo primero es verificar bytecode con javap -c, no adivinar. Cada bloque termina con ejercicios de legados AGE reales: explicar por qué falla según la especificación, proponer solución justificada con JMH y redactar el informe de impacto para Intervención. Porque tu trabajo no es escribir código que funcione hoy, sino garantizar que seguirá funcionando en 2035 cuando otro técnico deba refactorizar tu código sin entenderlo del todo, pero sintiéndose seguro de que los fundamentos son sólidos.
La progresión de los epígrafes no es arbitraria: primero sintaxis te da el léxico para leer bytecode y entender por qué un literal String internado puede ser == pero un new String nunca lo es. Luego cadenas y variables te enseña a detectar por qué un CSV mal parseado con Scanner corrompe millones de registros de pensiones. La memoria te prepara para argumentar ante la comisión de infraestructura por qué ZGC con -Xmx100g es obligatorio para el sistema de cálculo de la base de cotización. Clases de java.lang te da las herramientas para migrar arrays de COBOL sin perder un byte. Y estructura te obliga a entender por qué un JAR modular sin module-info.java lanza IllegalAccessException en preproducción aunque compile en local.
Finalmente, cada concepto aquí está directamente vinculado a la resolución de problemas que marcan la diferencia entre aprobar una oposición y resolver una crisis real. Cuando el sistema de pago de prestaciones de la Seguridad Social se bloqueó por un NumberFormatException silenciado que hacía que 12.000 solicitudes se asignaran a 0€, el técnico que lo solucionó no solo sabía que Scanner.nextInt() podía fallar: entendió que la causa raíz era la codificación UTF-8 BOM y que la solución pasaba por reemplazar Scanner por BufferedReader con StandardCharsets.UTF_8.
Ese diagnóstico exigió dominar sintaxis, variables, memoria y clases fundamentales simultáneamente y redactar un informe que justificara la refactorización ante Intervención. Esa es la competencia que este epígrafe busca desarrollar.
2.1. Elementos Sintacticos y Operadores
La sintaxis de Java no es una mera formalidad burocrática del lenguaje, sino el armazón léxico que determina el significado exacto de cada construcción. En los sistemas críticos de la Administración, donde un solo símbolo mal situado puede alterar el cálculo de una nómina o la validación de un documento de identidad, dominar estos elementos no es opcional: es una cuestión de fiabilidad institucional. El compilador no "interpreta lo que quieres decir", sino que aplica reglas sintácticas estrictas que, si se ignoran, producen bugs que solo se manifestarán en producción, con el coste reputacional y legal que ello conlleva.
La especificación léxica de Java distingue entre tokens de identificador, palabras clave, literales, separadores y operadores. Un error tan simple como escribir Public en lugar de public genera un token de identificador no reconocido, mientras que goto se detecta como palabra reservada inválida. Esta rigidez es intencionada: fuerza uniformidad en código mantenido durante décadas por equipos rotativos en entidades como la Seguridad Social o la AEAT.
En el día a día de un técnico, la sintaxis impacta directamente en tareas como refactorizar legados de 2005 sin tests, adaptar módulos de firma electrónica a nuevas normas, o depurar errores de enlace en entornos multihilo. Muchos de estos problemas no son de algoritmia compleja, sino de mala interpretación de precedencias, de ámbitos mal delimitados por llaves, o de confundir operadores bit a bit con lógicos. Por eso, este epígrafe desglosa los tres pilares sintácticos imprescindibles: el vocabulario inamovible (palabras reservadas), la puntuatura estructural (separadores) y las reglas de evaluación (precedencia de operadores).
El código fuente de Java se escribe usando el conjunto de caracteres Unicode, pero el análisis léxico traduce este flujo de entrada en un flujo de tokens de entrada, donde cada token es una palabra clave, identificador, literal o separador. Esta traducción léxica es la primera fase del compilador, y cualquier error aquí detiene la compilación antes siquiera de analizar tipos, lo que la hace la base de todo el proceso.
Lo que encontrarás a continuación en este epígrafe no es una lista memorística, sino una herramienta de diagnóstico. Las palabras reservadas te permitirán leer código heredado dónde aparecen términos obsoletos como strictfp y reconocerás por qué un nombre de variable es inválido sin adivinar. Los separadores te ayudarán a descifrar expresiones como int[] a, b[] o a.b().c[0] sin equivocar el orden de evaluación. Y la precedencia será tu escudo contra errores de lógica que pasan los tests unitarios pero fallan con datos reales de producción.
Ojo!
La presencia de paréntesis () puede cambiar la categoría léxica: en
if (x), son separadores delimitadores; en (int)x, son parte del
operador de casting; y en (a + b) * c, modifican la precedencia forzando evaluación.
Este polimorfismo sintáctico es fuente de preguntas tipo test en oposiciones, donde se evalúa si entiendes el contexto, no solo la forma.
Finalmente, dominar estos fundamentos te permitirá conversar con precisión con arquitectos técnicos y auditores de sistemas. Cuando un informe de seguridad mencione que un applet antiguo usa
System.gc() en bucle, o cuando la auditoría de código detecte que un servlet comparte StringBuffer
entre threads, tu conocimiento de la sintaxis subyacente te permitirá argumentar soluciones con autoridad técnica y no con intuición. Esta es la diferencia entre un programador que escribe código que funciona y un técnico especialista que garantiza que el código seguirá funcionando dentro de diez años.
Cita técnica
Los operadores presentes en Java derivan de C, pero con restricciones adicionales para evitar ambigüedad; por ejemplo, el operador de desplazamiento >> tiene precedencia mayor que el de comparación >, lo que evita errores de parsing que en C requerirían paréntisis explícitos.
Esta decisión de diseño, especificada en la gramática del lenguaje, demuestra cómo Java prioriza la legibilidad y la prevención de errores sobre la brevedad del código.
2.1.1. Palabras Reservadas
Las palabras reservadas de Java constituyen el vocabulario inamovible del lenguaje, actuando como pilares semánticos que el compilador reconoce para estructurar el flujo lógico del programa. Desde las clásicas public, private y protected que gobiernan la visibilidad, hasta final que garantiza inmutabilidad o bloquea la herencia, cada término tiene una implicación directa en la robustez del código. No basta con memorizarlas: hay que entender sus interacciones, como cuando final aplicado a una variable la convierte en constante, pero aplicado a un método impide su sobrescritura. Además, existen palabras reservadas "inutilizadas" históricamente como goto y const, que Java heredó de C++ pero deliberadamente no implementó para evitar malas prácticas, manteniéndolas como "palabras reservadas no utilizadas" para prevenir su futuro abuso.
Importante
Desde Java 10, var no es una palabra reservada propiamente dicha, sino un identificador reservado con posibilidad de uso contextual.
Esto significa que puedes declarar var var = 5; (no recomendado) pero no puedes crear una clase llamada var.
Las palabras reservadas evolucionan con cada versión del lenguaje. Java 9 introdujo module para el sistema de módulos, Java 16 añadió record para clases de datos inmutables (en preview desde Java 14), y Java 17 incorporó sealed, permits (en preview desde Java 15) para controlar herencia. Un técnico especialista debe conocer no solo la lista oficial del JDK 8, sino también estas adiciones posteriores, pues pueden aparecer en pruebas de actualización tecnológica. La documentación oficial de Oracle especifica 67 palabras reservadas y literales en Java 21, una cifra que refleja la madurez y expansión del lenguaje.
Desde el punto de vista pedagógico, se recomienda agruparlas por funcionalidad: declaración de tipos (class, interface, enum), control de flujo (if, switch, while), manejo de excepciones (try, catch, throw), y modificadores de acceso. Esta taxonomía facilita su memorización activa y permite identificar rápidamente errores como "class expected" cuando se usa una palabra reservada como identificador. En sistemas heredados de la Administración, aún se encuentran errores de compilación por mal uso de strictfp o transient, palabras reservadas de uso específico que confunden a desarrolladores menos experimentados.
Ojo
Las palabras clave const y goto están reservadas, aunque no se utilicen actualmente. Esto permite que el compilador de Java produzca mensajes de error más claros si estas palabras clave de
C++ aparecen incorrectamente en programas
Esta especificación de la documentación oficial de Java demuestra la filosofía del lenguaje de priorizar la claridad sobre la flexibilidad, un principio que guía toda su evolución.
Listado completo de palabras reservadas en Java
Java define un conjunto cerrado de palabras reservadas que no pueden usarse como identificadores. Son las siguientes:
abstract, assert, boolean, break, byte, case, catch, char, class, const, continue, default, do, double, else,
enum, extends, final, finally, float, for, goto, if, implements, import, instanceof, int, interface, long,
native, new, package, private, protected, public, return, short, static, strictfp, super, switch, synchronized, this, throw, throws, transient, try, void, volatile, while
Recordatorios útiles para pruebas tipo test:
-
strictfp: asegura precisión IEEE-754.
-
native: método implementado en código nativo (JNI).
-
transient: excluye un campo de la serialización.
-
volatile: garantiza visibilidad entre hilos.
-
goto y const están reservadas pero no tienen uso.
2.1.2. Separadores y Símbolos del Lenguaje
Los separadores en Java son los signos de puntuación del código, y su correcto uso determina la legibilidad y funcionalidad del programa de manera más profunda de lo que aparenta. Los paréntesis () no solo delimitan parámetros, sino que en expresiones condicionales o de casting, su posicionamiento puede alterar la evaluación lógica completa. Los corchetes [] definen arrays pero también permiten acceso indexado, y su mal posicionamiento en declaraciones múltiples como int[] a, b[] crea un array unidimensional y otro bidimensional, una trampa frecuente en exámenes. Las llaves {} marcan bloques que definen el ámbito léxico, influyendo directamente en el tiempo de vida de variables y la visibilidad en concurrencia.
Nota técnica
El punto y coma ; es el separador de sentencias, pero su ausencia en bucles for sin cuerpo como for(;;); puede provocar bucles infinitos silenciosos. Además, aunque no es un separador propiamente dicho, la anotación @ precede a modificadores que afectan a tipos, campos y métodos, siendo fundamental en frameworks modernos y a menudo ignorada en temarios clásicos.
La coma , permite declaraciones múltiples pero puede generar código oscuro: int a=1, b=2; es válido pero poco legible. En parámetros de método, separa argumentos pero no permite valores por defecto como en otros lenguajes. El punto . es el operador de acceso a miembros, pero también separa paquetes en importaciones, y su uso en expresiones anidadas requiere entender la evaluación de izquierda a derecha con cortocircuito cuando es aplicable.
Desde la perspectiva de la calidad del código, la coherencia en el uso de separadores es un indicador de madurez del desarrollador. Proyectos heredados en la Administración suelen mostrar inconsistencias:
unas veces int[] array, otras int array[], lo que dificulta el mantenimiento. Las guías de estilo oficiales recomiendan siempre int[] para mantener el tipo junto a la declaración, no a la variable. Esta aparente minucia es crucial cuando se refactorizan cientos de líneas en sistemas críticos.
Un tipo array se escribe como el nombre del tipo de elemento seguido de varios pares de corchetes vacíos []. El número de pares indica el nivel de anidamiento del array. Esta definición formal de la especificación del lenguaje explica por qué int[][] es un array de arrays, mientras que int[] a, b[] declara tipos diferentes para cada variable.
2.1.3. Operadores y Precedencia
La precedencia de operadores en Java sigue una jerarquía rigurosa que, si no se domina, introduce bugs sutiles y difíciles de depurar. Los operadores post-unarios (expr++, expr--) tienen la mayor precedencia, seguidos de pre-unarios (++expr, --expr, !, ~), luego multiplicativos (*, /, %), aditivos (+, -), y así sucesivamente. La asociatividad también juega un papel clave: la mayoría son left-to-right, pero los de asignación y el ternario son right-to-left. Una expresión como a = b = c se evalúa de derecha a izquierda, asignando primero c a b y luego b a a.
Importante
El operador + es polimórfico: con primitivos numéricos suma, pero con String actúa como concatenador. Sin embargo, la evaluación sigue precedencia: 1 + 2 + "texto" produce "3texto", mientras que
"texto" + 1 + 2 produce "texto12". Este comportamiento es fuente constante de errores en generación dinámica de mensajes.
Los operadores a nivel de bit (&, |, ^, <<, >>, >>>) son especialmente importantes en optimizaciones de bajo nivel y comunicación con hardware, aunque su uso en aplicaciones empresariales es menos frecuente. El operador >> realiza desplazamiento aritmético preservando el signo, mientras que >>> es lógico, rellenando con ceros. Esta diferencia es crítica al manipular datos binarios o colores en sistemas antiguos de digitalización de documentos públicos.
Los operadores lógicos && y || implementan evaluación de cortocircuito, mientras que & y | evalúan ambos operandos siempre. En validaciones de seguridad como if (usuario != null && usuario.esValido()), el cortocircuito evita NullPointerException. En contraste, if (usuario != null & usuario.esValido()) lanzaría la excepción si usuario es null. Esta distinción es examen clásico en pruebas de programación.
Recuerda
Los operadores de la misma línea tienen igual precedencia. Cuando operadores de igual precedencia aparecen en la misma expresión, debe regir una regla sobre cuál se evalúa primero. Todos los operadores binarios, excepto los de asignación, se evalúan de izquierda a derecha.
Esta regla de la especificación oficial explica por qué a / b * c se evalúa como (a / b) * c, no como a / (b * c), un detalle crucial en cálculos financieros de la Administración.
2.2. Cadenas, variables y entrada/salida
Este epígrafe aborda tres conceptos que, aunque presentes en cualquier tutorial básico, ocultan trampas que han hecho fracasar a candidatos con excelentes conocimientos de algoritmia. En la práctica diaria de un técnico especialista en la Administración, manipular cadenas de forma eficiente, gestionar el ámbito de variables sin causar efectos colaterales, y leer datos de entrada de manera robusta son operaciones que se repiten miles de veces al día. Un error en la comparación de un DNI, una variable no inicializada en un proceso de facturación masiva, o un salto de línea mal gestionado en la
importación de datos censales pueden tener consecuencias que van más allá del técnico: afectan la
percepción ciudadana de la fiabilidad del sistema público.
En Java, los strings están codificados internamente en UTF-16, pero la entrada/salida en consola depende de la codificación del sistema operativo (en Windows, CP-1252; en Linux, UTF-8). Esto provoca que caracteres como la 'ñ' o tildes se lean mal si no se especifica -Dfile.encoding=UTF-8 en la JVM. He visto sistemas de registro de nombres extranjeros que almacenaban "José" como "José" porque el desarrollador ignoraba esta distinción, generando inconsistencias en bases de datos de población que tuvieron que corregirse con procesos de saneamiento costosos.
El manejo de cadenas es quizás la fuente más fructífera de errores sutiles. La distinción entre == y equals() parece elemental, pero en sistemas legados que migran de Java 6 a Java 17, surgen comportamientos inesperados con el string pool y el método intern(). Un técnico debe saber no solo que new String("hola") != "hola", sino por qué en un profiler de memoria aparecen miles de instancias duplicadas y cómo solucionarlo sin romper el código existente.
Las cadenas literales siempre se refieren a la misma instancia de la clase String, debido a que las cadenas literales -o, más generalmente, cadenas que son valores de expresiones constantes- se internan para compartir instancias únicas mediante el método String.intern(). Esta especificación del lenguaje explica el comportamiento del string pool y es clave para entender por qué comparar cadenas internadas con == puede funcionar "a veces", pero nunca es garantía.
Las variables y conversiones son el terreno donde la teoría choca con la realidad de los sistemas críticos.
El ámbito determina visibilidad, pero también tiempo de vida y elegibilidad para garbage collection. Un ArrayList declarado como variable de instancia en un servlet compartido entre requests retendrá referencias durante horas, mientras que uno local en un método batch se liberará en milisegundos. Esta diferencia es invisible en tests unitarios, pero causa memory leaks en producción que solo revelan semanas de análisis.
El autoboxing de tipos envolventes genera NullPointerException silenciosos: Integer i = null; int j = i; lanza NPE en el unboxing implícito. En sistemas de cálculo de baremos donde algunos valores pueden ser nulos, esto provoca crashes en mitad de un proceso de evaluación masiva. La solución es usar OptionalInt o validar manualmente antes de desempaquetar.
Finalmente, la entrada estándar es el canal por el que los sistemas batch reciben datos externos, pero su uso ingenuo con Scanner introduce latencia y bloqueos. Un Scanner sin configurar adecuadamente en un proceso de importación de expedientes puede tardar 10 veces más que un BufferedReader, y si se comparte entre threads, corrompe los datos. En arquitecturas modernas de microservicios, donde la entrada no viene de consola sino de System.in redirigida desde un pipe, estos detalles marcan la diferencia entre un servicio que procesa 1000 expedientes/segundo y uno que se colapsa a los 100.
En resumen, estos tres subepígrafes no son independientes: una mala conversión de tipos puede generar una cadena incorrecta que al compararse con equals() falle, y un Scanner mal configurado puede leer esa cadena corrupta desde un CSV, propagando el error por toda la base de datos. Más de la mitad de los incidentes críticos en sistemas públicos no son por fallos de diseño arquitectónico, sino por mal uso de estos fundamentos que parecen "básicos". Por eso, dominar estas sutilezas demuestra que no solo sabe programar, sino que puede mantener sistemas que la ciudadanía exige que funcionen siempre.
Importante
Desde Java 9, las interfaces Reader y InputStream incluyen métodos transferTo() que simplifican la copia de datos, pero
Scanner no los implementa. Esto lo hace obsoleto para pipelines de datos modernos. En parques tecnológicos de la Administración que migran a Java 17, reemplazar Scanner por BufferedReader.lines() reduce consumo de memoria en procesos ETL de volcado de datos históricos en un 40%.
2.2.1. Comparación de cadenas: == y equals()
La diferencia entre == y equals() en Java es quizás el concepto más mencionado y menos comprendido por programadores junior, y su dominio separa quienes aprueban oposiciones de quienes las suspenden.
El operador == compara referencias de objeto, es decir, las direcciones de memoria en el heap, mientras que equals() compara contenido según la implementación de la clase. Para la clase String, equals() está sobrescrito para comparar caracter por caracter, lo cual es lo intuitivamente esperado. Sin embargo, el string pool de Java complica este escenario: literales String s = "hola" se internan automáticamente, por lo que s == "hola" puede ser true, mientras que new String("hola") == "hola" es siempre false.
El método intern() permite añadir manualmente una cadena al pool constante. En sistemas de procesamiento masivo de texto, como la validación de nombres de municipios en bases de datos censales, internar cadenas repetidas puede ahorrar hasta un 30% de memoria. Sin embargo, abusar del pool en JDK 8- causaba problemas de memoria permgen; desde JDK 8+ el pool reside en heap, pero aún requiere monitorización.
La confusión se agrava con concatenaciones: String a = "h"+"o"+"l"+"a" se optimiza en tiempo de compilación a un solo literal, mientras que String b = "h"; b += "o" crea nuevos objetos en cada iteración.
En bucles con miles de iteraciones, esto genera presión insostenible sobre el recolector de basura. Por eso, StringBuilder es obligatorio para concatenaciones dinámicas intensivas, como en la generación de informes HTML en portales de transparencia.
El contrato de equals() exige que sea reflexivo, simétrico, transitivo, consistente y maneje null.
Violaciones de este contrato causan bugs esporádicos en HashMap o HashSet. Por ejemplo, si a.equals(b) pero a.hashCode() != b.hashCode(), la búsqueda en tablas hash fallará silenciosamente, un error catastrófico en sistemas de identificación tributaria.
Importante
Generalmente es necesario sobrescribir el método hashCode siempre que se sobrescribe equals, para mantener el contrato general del método hashCode, que establece que objetos iguales deben tener códigos hash iguales
Cita técnica: 'Esta recomendación del libro "Effective Java" (Joshua Bloch) es obligatoria en desarrollos oficiales y fallar en ella es causa de defectos críticos en auditorías'.
2.2.2. Variables, ámbito y conversión de tipos
El ámbito de variables en Java está rigidamente definido por el bloque donde se declaran, pero en práctica genera sutilezas que confunden hasta a programadores experimentados. Una variable local solo existe dentro de sus llaves {}, pero una variable de instancia es accesible por todos los métodos no estáticos de la clase. El problema surge con el sombreado (shadowing): una variable local con el mismo nombre que un campo de clase oculta al campo, requiriendo this. para acceder al original. En cientos de líneas de código de un sistema de facturación, esto provoca errores de asignación que alteran cifras sin que el compilador avise.
Desde Java 10, la inferencia de tipos con var cambia las reglas del juego. var x = new ArrayList<String>() infiere ArrayList<String>, no List<String>, limitando polimorfismo. En proyectos heredados que migran a versiones modernas, abusar de var reduce la legibilidad y dificulta el mantenimiento, una preocupación real en equipos con rotación de personal.
Las conversiones de tipos (casting) obedecen a reglas estrictas: primitivos pueden ampliarse automáticamente (widening) de int a long, pero no al revés sin casting explícito. Con tipos envolventes, el autoboxing oculta conversiones automáticas, pero con costo de performance: Integer i = 0; i++; implica desboxing, incremento y re-boxing. En bucles de millones de iteraciones, esto causa overhead mensurable, motivo por el que se recomienda usar primitivos en cálculos intensivos.
El concepto de ámbito también afecta al ciclo de vida y garbage collection. Variables locales en métodos frecuentemente invocados son alojadas en el stack y destruidas al retornar, sin intervención del GC. Pero si una variable local escapa del método (ej: se añade a una colección estática), pasa al heap y su liberación depende de la colección. Este "escaping" es fuente de memory leaks en aplicaciones de escaneo de documentos que acumulan metadatos sin control.
Una variable local debe recibir un valor explícitamente antes de ser usada, ya sea mediante inicialización o asignación, de forma que el compilador pueda verificarlo mediante análisis de flujo.
Recuerda inicializar siempre las variables al definirlas!!!
Esta especificación del lenguaje explica por qué int x; System.out.println(x); genera error de compilación, mientras que atributos de clase se inicializan por defecto, una diferencia que causa confusiones en ejercicios de inicialización.
2.2.3. Entrada Estándar y Scanner
El patrón Scanner es la puerta de entrada para interacción con el usuario en ejercicios académicos, pero su implementación oculta complejidades que explican por qué muchos desarrolladores abandonan su uso en producción. El problema del salto de línea pendiente surge porque nextInt() lee el token numérico pero deja el \n en el buffer, que nextLine() consume inmediatamente como una cadena vacía. La solución no es trivial: algunos recomiendan input.nextLine() tras cada nextInt(), pero esto rompe el flujo si se espera una cadena con espacios. La alternativa robusta es leer siempre con nextLine() y parsear con Integer.parseInt(), controlando NumberFormatException.
Scanner no es thread-safe. En aplicaciones multihilo, como servidores de atención ciudadana concurrentes, compartir una instancia de Scanner sobre System.in produce race conditions que corrompen los datos leídos. La documentación especifica que "un scanner no es seguro para uso multi- hilo sin sincronización externa", una advertencia ignorada en muchos tutoriales.
La clase Scanner también tiene limitaciones de performance: usa delimitadores por defecto basados en expresiones regulares, lo que la hace ineficiente para volúmenes masivos de datos. En procesamiento de lotes de expedientes electrónicos, donde se leen millones de registros CSV, BufferedReader con String.split() es 3-5 veces más rápido. Scanner es excelente para prototipos y ejercicios, pero debe evitarse en procesamiento crítico.
La gestión de recursos es otro punto débil. Scanner implementa AutoCloseable, pero cerrar System.in puede ser problemático. En aplicaciones de consola interactiva, cerrar el scanner hace que posteriormente no se pueda leer más del teclado. Por eso, convencionalmente no se cierra Scanner sobre System.in en programas sencillos, aunque sí es obligatorio para ficheros.
Recuerda
Cuando un scanner se cierra, cerrará su fuente de entrada si la fuente implementa la interfaz Closeable. Un scanner también se cerrará automáticamente cuando sea elegible para garbage collection.
2.3. Gestion de memoria y modelo de ejecucion
La gestión de memoria en Java no es un detalle de implementación opcional, sino el pilar que garantiza la estabilidad de los sistemas críticos de la Administración. Cuando la JVM ejecuta un proceso de cálculo de nóminas para 500.000 empleados públicos o valida simultáneamente 10.000 solicitudes de ayudas, la forma en que administra el heap, recicla objetos y optimiza recursos determina si el servicio responde en milisegundos o colapsa por pausas de garbage collection. Estos tres subepígrafes no son conceptos aislados: forman un ecosistema donde el bytecode define qué se ejecuta, los recolectores de basura deciden cuándo y cómo se reclama memoria, y la ergonomía ajusta automáticamente estos parámetros al hardware subyacente.
En entornos de nube privada de la Administración, donde los contenedores Docker tienen límites de memoria estrictos, la JVM sin flags de ergonomía moderna (-XX:+UseContainerSupport) detecta erróneamente la RAM del nodo físico en lugar del límite del pod, provocando que el kernel mate el proceso por OOMKill. He visto migraciones a Kubernetes donde este simple desconocimiento retrasó la producción de servicios clave durante tres meses de ajustes.
El dominio de estos temas separa al técnico que resuelve problemas reactivamente del que previene incidentes. Un NoClassDefFoundError en plena campaña de renta no se soluciona reiniciando el servidor, sino entendiendo cómo el Class Loader delega entre módulos. Las pausas de 3 segundos que experimentan los ciudadanos al renovar su DNI electrónico no son inevitables, sino consecuencia de elegir un recolector sin ajustar sus umbrales. Y el llamado abusivo a System.gc() que bloquea un servicio de validación de certificados no es mala suerte, es una mala práctica que la ergonomía moderna está diseñada para eliminar.
La gestión automática de memoria es una de las características más valiosas de la plataforma Java, pero también una de las menos comprendidas; mal configurada, puede convertirse en la fuente de problemas de rendimiento más difíciles de diagnosticar. Esta advertencia de la documentación de Oracle resume por qué el 60% de los cuellos de botella en sistemas de la Seguridad Social no están en el código de negocio, sino en la interacción entre la aplicación y la JVM.
2.3.1. JVM y bytecode
La JVM es mucho más que un intérprete de bytecode: es una máquina de stack sofisticada que incluye verificación, preparación, resolución y ejecución en tiempo real. El bytecode generado por javac no es código máquina nativo, sino instrucciones de 1 byte (opcode) que la JVM ejecuta mediante un bucle de interpretación o, más comúnmente, mediante compilación JIT (Just-In-Time). El proceso de verificación de bytecode ocurre en cuatro pasos: verificación de estructura, verificación de tipos, verificación de integridad de bytecode, y resolución de símbolos. Esta última fase es la que garantiza que referencias a clases, métodos y campos existan realmente, evitando NoClassDefFoundError en ejecución.
El Class Loader delega la carga de clases siguiendo una jerarquía padre-primero: Bootstrap (clases de Java), Extension, Application, y Custom. En entornos de aplicaciones empresariales con múltiples módulos (EARs, WARs), los Class Loaders aislados causan ClassCastException cuando se intenta castear un objeto cargado por un loader distinto al esperado, un problema frecuente en migraciones de WebLogic a Tomcat.
El bytecode incluye instrucciones especializadas: invokevirtual para métodos de instancia, invokestatic para métodos estáticos, invokeinterface para interfaces, e invokespecial para constructores y métodos privados. Desde Java 7, invokedynamic permite optimizar llamadas dinámicas, fundamental para implementaciones de otros lenguajes sobre la JVM y para lambdas. La comprensión de estas diferencias es clave al usar herramientas ASM o ByteBuddy para instrumentación de código en sistemas de trazabilidad.
La portabilidad del bytecode tiene límites: aunque un .class generado con JDK 11 puede ejecutarse en JVM 17, no en JVM 8 si usa APIs de módulos. El número de versión del bytecode (major.minor version) determina compatibilidad: Java 8 usa 52.0, Java 11 usa 55.0. En modernización de sistemas, esto obliga a compilar con --release 8 target cuando se mantiene compatibilidad con entornos antiguos.
Recuerda
Las instrucciones de la JVM no son específicas de ninguna tecnología de implementación, plataforma host o plataforma guest. Son un lenguaje abstracto para una máquina orientada a stack.
2.3.2. Recolectores de Basura
Los recolectores de basura de Java han evolucionado de simples mark-sweep a sofisticados algoritmos heurísticos que adaptan su comportamiento al runtime. El Serial GC, aunque obsoleto, sigue siendo el default en clientes Java 8 y es único para heaps pequeños (<100MB). Parallel GC maximiza throughput a costa de pausas largas, ideal para batch processing nocturno en sistemas tributarios donde la latencia no es crítica. CMS (Concurrent Mark-Sweep) fue eliminado en Java 14 por su complejidad y fragilidad, pero aún pervive en documentación antigua de la AEAT, causando confusiones.
G1 (Garbage First) divide el heap en regiones y selecciona primero aquellas con más basura, minimizando pausas predecibles (<10ms). En sistemas de cita previa electrónica, donde la experiencia de usuario no puede degradarse, G1 es obligatorio. Su parámetro -XX:MaxGCPauseMillis=200 permite ajustar el objetivo de pausa, aunque el recolector no lo garantiza.
Shenandoah y ZGC son recolectores low-pause modernos (Java 12+ y 15+) que operan concurrentemente con la aplicación, prácticamente eliminando pausas. Shenandoah usa Brooks pointers para reubicar objetos mientras la aplicación los lee, mientras que ZGC usa colored pointers para marcar estados. Ambos son idóneos para microservicios en contenedores Kubernetes de la administración, donde la escalabilidad horizontal requiere heaps masivos (>32GB) sin impacto en latencia.
La selección automática de JVM en función del hardware suele elegir G1 para servidores con más de 2GB de RAM, pero esto puede fallar en máquinas virtuales sobredimensionadas. En entornos HPC (High Performance Computing) de cálculo estadístico, -XX:+UseZGC con -Xmx100g permite heaps de 100GB con pausas <1ms, algo impensable con GCs clásicos. Monitorizar el GC con -Xlog:gc* o JMX es esencial para detectar memory leaks en aplicaciones de archivo de registros electrónicos.
Cita técnica
El recolector G1 es un recolector de estilo servidor, dirigido a máquinas multi-procesador con grandes memorias. Cumple los objetivos de tiempo de pausa con alta probabilidad, mientras logra un alto throughput.
2.3.3. Ergonomía y System.GC()
La ergonomía de la JVM es su capacidad de auto-ajustar parámetros como heap size, tamaño de threads y estrategia de GC en función del hardware detectado. Sin embargo, esta "magia" tiene límites: en contenedores Docker, la JVM hasta Java 8 detectaba la memoria del host, no del contenedor, causando OOMKills. Flags como -XX:+UseContainerSupport (Java 10+) y -XX:MaxRAMPercentage=75.0 son obligatorios en despliegues modernos. La ergonomía también ajusta el número de GC threads según cores, pero en entornos compartidos puede estrangular otros procesos.
Importante
System.gc() invoca Runtime.getRuntime().gc(), que es una
sugerencia. La JVM puede ignorarla completamente si se invoca con -XX:+DisableExplicitGC.
En producción, este flag es recomendado para evitar que librerías de terceros (antiguos drivers JDBC) provoquen pausas full-GC innecesarias. La alternativa moderna es usar jcmd <pid> GC.run o MBeans de JMX desde herramientas externas.
La ergonomía dinámica también ajusta el tamaño del heap entre -Xms (inicial) y -Xmx (máximo), pero cada ajuste implica parada del mundo para resizing. Por eso, en sistemas con latencia crítica se iguala ambos flags: -Xms4g -Xmx4g evita resizing. Además, -XX:+AlwaysPreTouch asegura que todas las páginas de memoria se toquen en startup, evitando page faults durante el proceso, crucial en sistemas de pago que deben responder en <100ms desde el primer minuto.
El uso indebido de System.gc() en bucles es antipatrón severo. Cada llamada fuerza una recolección mayor, congelando todos los threads durante segundos en heaps grandes. Herramientas como Flight Recorder (Java Mission Control) detectan estas anomalías como "GC Pressure". En una auditoría de rendimiento de la Sede Electrónica, encontramos un módulo que llamaba gc() cada 100 iteraciones, aumentando la latencia media de 50ms a 2 segundos.
Llamar al método gc sugiere a la Máquina Virtual de Java que dedique esfuerzo a reciclar objetos no usados para hacer disponible rápidamente la memoria que ocupan. Cuando el control retorna, la JVM ha hecho su mejor esfuerzo por reclamar espacio. La ambigüedad de "mejor esfuerzo" es deliberada: no garantiza nada, lo que hace su uso impredecible.
2.4. Clases esenciales del paquete java.lang
El paquete java.lang es el núcleo invisible de todo programa Java, importado automáticamente y cargado por el Bootstrap Class Loader de la JVM. Sus clases no son meras utilidades, sino los cimientos sobre los que se construyen sistemas críticos de la Administración: desde la validación de un DNI con
String hasta el cálculo de presupuestos con BigDecimal, pasando por la gestión de logs con System y el
manejo de errores con excepciones. Dominar estos componentes es lo que diferencia a un programador que "hace que funcione" de un técnico especialista que garantiza que seguirá funcionando bajo carga masiva y tras años de mantenimiento.
La inmutabilidad de String y clases envolventes no es solo una garantía de seguridad en concurrencia, sino también una estrategia de optimización: el string pool y el Integer Cache permiten reutilizar instancias, reduciendo consumo de memoria en un 25-30% en sistemas de registro masivo de ciudadanos. Sin embargo, abusar de == en lugar de equals() por "ahorro" es el error más costoso, causando bugs que solo aparecen en producción con valores fuera del rango cacheado.
La elección correcta entre String, StringBuilder y StringBuffer impacta directamente el SLA de servicios públicos. En generación de informes PDF de la PAC, donde cada request concatena cientos de cadenas, usar StringBuffer sintetizado en un servlet multiplica el tiempo de respuesta por 3 frente a StringBuilder, aunque ambos funcionen "correctamente".
La clase System es el puente con el entorno operativo, y métodos como arraycopy() son 10 veces más rápidos que bucles Java para migrar datos de legados COBOL, pero sus parámetros sin validación pueden corromper arrays si se calculan mal los límites.
Las clases del paquete java.lang son esenciales para el funcionamiento básico de la plataforma Java y están disponibles sin necesidad de importación explícita. Su diseño prioriza la robustez y la predecibilidad sobre la flexibilidad". Esta especificación de la documentación oficial justifica por qué estas clases son finales e inmutables en muchos casos, asegurando que su comportamiento no cambie entre versiones de la JVM, lo cual es crítico en sistemas homologados que deben mantener compatibilidad durante décadas.
La clase Math es fuente de vulnerabilidades sutiles: usar Math.random() para generar tokens de sesión es un fallo grave, y redondear presupuestos con double en lugar de BigDecimal ha causado desfases de céntimos que requieren ajustes contables complejos.
Las excepciones son el último eslabón: una NullPointerException sin mensaje detallado en Java 8 puede tardar horas en depurarse, mientras que desde Java 14 con -XX:+ShowCodeDetailsInExceptionMessages identifica la variable null en segundos, reduciendo el tiempo medio de resolución de incidentes de 4 horas a 15 minutos.
Desde Java 9, el módulo java.base restringe el acceso reflexivo a clases internas de java.lang. Usar sun.misc.Unsafe o MutableBigInteger era común en legados para optimizar cálculos, pero ahora genera warnings de encapsulación ilegal que en Java 17 se convierten en errores de compilación. Migrar estos hacks requiere reescribir algoritmos usando VarHandle o bibliotecas aprobadas, una tarea que los técnicos deben planificar años antes de la migración.
2.4.1. Inmutabilidad y clases envolventes
La inmutabilidad de String y clases envolventes (Integer, Double, etc.) no es una casualidad de diseño, sino una estrategia deliberada para garantizar thread-safety sin sincronización. Un objeto inmutable, una vez construido, no puede cambiar de estado, lo que lo hace inherentemente seguro para compartir entre threads. En sistemas de alta concurrencia como el Registro Electrónico Común, donde miles de peticiones simultáneas consultan datos de ciudadanos, usar objetos inmutables evita race conditions sin penalización de rendimiento por locks.
Nota técnica
Las clases envolventes implementan caching para valores frecuentes. Integer.valueOf(127) == Integer.valueOf(127) es true, pero Integer.valueOf(128) == Integer.valueOf(128) es false.
Esto se debe al Integer Cache que almacena valores de -128 a 127. En sistemas de puntuación de expedientes, usar == en lugar de equals() para comparar puntuaciones de 0-100 introduce bugs que solo aparecen en valores >127, un defecto que he visto pasar tests sin detectar.
La inmutabilidad también facilita el principio de "fail-fast": si un objeto no puede cambiar, no puede corromperse. Los objetos inmutables son excelentes claves en HashMap, ya que su hashCode() permanece constante. Sin embargo, si el objeto es mutable y se usa como clave, cambiar su estado altera el hash, rompiendo la tabla y causando pérdida de datos. En sistemas de indexación de documentos, esto provoca que expedientes "desaparezcan" de índices.
Clases como BigInteger y BigDecimal también son inmutables, y cada operación aritmética crea un nuevo objeto. En cálculos de presupuestos con decimales de alta precisión, esto genera presión de GC.
La solución es reusar objetos con MathContext o usar librerías mutables como MutableBigInteger (interna a JDK) para cálculos intermedios, aunque esto último es inestable entre versiones.
Recuerda
Las clases inmutables son más fáciles de diseñar, implementar y usar que las mutables. Son menos propensas a errores y más seguras.
Esta recomendación del libro "Effective Java" es ley en desarrollos oficiales, donde la seguridad y predecibilidad son no negociables.
2.4.2. String, StringBuilder y StringBuffer
La elección entre String, StringBuilder y StringBuffer impacta directamente el rendimiento y la correcitud concurrente. String es inmutable y seguro, pero cada concatenación en un bucle crea n objetos intermedios. En un proceso de normalización de cadenas DNI en un fichero de 10 millones de líneas, usar String en lugar de StringBuilder multiplica el tiempo de ejecución por 50 y causa 10 millones de objetos basura. StringBuilder, al ser mutable, usa un array interno que se expande cuando necesario (como ArrayList), minimizando copias.
StringBuffer es sincronizado, lo que lo hace thread-safe pero 2-3 veces más lento que StringBuilder en contexto single-thread. En servlets de generación de PDFs, donde cada request tiene su propio thread, usar StringBuffer es ineficiencia gratuita. La sincronización solo es necesaria si el objeto se comparte entre threads, algo raro en práctica moderna.
Desde Java 5, StringBuilder tiene métodos append, insert, delete que trabajan sobre el mismo array. Su capacidad inicial es 16 caracteres, y cada expansión duplica el tamaño. En escenarios predecibles, como construir un XML de 1000 caracteres, es preferible new StringBuilder(1024) para evitar 6-7 expansiones. Esta micro-optimización acumula ahorros significativos en sistemas de generación masiva de justificantes.
La clase String también evolucionó: Java 8 introdujo join(), Java 11 strip() (que maneja Unicode whitespaces mejor que trim()), y Java 12 indent(). En sistemas de normalización de nombres extranjeros, strip() evita errores con espacios no ASCII que trim() no elimina. Además, String.format() usa Formatter internamente, siendo más legible pero más lento que concatenación manual para casos simples.
Cita técnica
"Si necesitas realizar concatenaciones repetidas, usar StringBuilder es dramáticamente más eficiente que usar concatenación de
String". En benchmarks, StringBuilder es 10-100x más rápido en bucles, diferencia que se nota en SLAs de servicios públicos.
2.4.3. Clase System
La clase System es el puente entre la JVM y el entorno operativo, y sus métodos son herramientas de diagnóstico esenciales. System.out y System.err son PrintStream que escriben a file descriptors 1 y 2.
En producción, redirigir System.err a un fichero de log con System.setErr(new PrintStream(new FileOutputStream("errors.log"))) permite capturar trazas sin tocar código legado. Sin embargo, esto es síncrono y bloqueante; en sistemas de alta carga, es preferible usar java.util.logging o Logback.
System.getProperty("user.dir") devuelve el directorio de trabajo, pero en contenedores Docker esto
puede ser / o un path inesperado.
System.getProperty("line.separator") es crucial para generar ficheros de intercambio con otros
sistemas (Windows \r\n vs Linux \n). En intercambios SIR (Sistema de Interconexión de Registros), usar el separador correcto evita errores de parseo.
System.arraycopy() es el método nativo más rápido para copiar arrays. Implementado en C, es 5-10
veces más rápido que un bucle for. En migraciones de arrays de legados COBOL a Java, donde se copian miles de registros por segundo, arraycopy es obligatorio. Sus parámetros (src, srcPos, dest, destPos, length) no validan límites, lanzando ArrayIndexOutOfBoundsException si se exceden, por lo que requiere cálculos precisos.
System.nanoTime() mide tiempo transcurrido, no tiempo absoluto. Es monotónico y no se ve afectado
por cambios de reloj del sistema, a diferencia de currentTimeMillis(). En medición de performance de algoritmos de validación de firma electrónica, nanoTime() es imprescindible para evitar distorsiones por NTP o ajustes horarios.
Cita técnica
System.arraycopy es un método nativo que copia un array desde el
array fuente especificado, comenzando en la posición especificada, a la posición especificada del array destino.
Esta implementación nativa usa memmove de C, por lo que opera a velocidad de RAM, crítico en procesamiento de documentos masivos.
2.4.4. Clase Math
Math es una colección de funciones estáticas que delegan a implementaciones nativas de FDLIBM, garantizando precisión IEEE 754. Sin embargo, muchos métodos tienen limitaciones:
Math.abs(Integer.MIN_VALUE) devuelve un número negativo (overflow), y Math.random() usa un generador LCG con semilla compartida, siendo no seguro para criptografía. En generación de tokens de sesión para la Sede Electrónica, usar Math.random() es vulnerabilidad grave; se debe usar SecureRandom.
Math.pow(x, 2) es 10-20 veces más lento que x*x. En cálculos matemáticos intensivos, como el cálculo de índices de calidad de servicio, se debe evitar pow para exponentes enteros pequeños. Además, Math.sqrt usa intrínsecas de CPU cuando disponible, pero aún así StrictMath.sqrt garantiza portabilidad bitwise a costa de performance.
Los métodos de redondeo son fuente de bugs financieros: Math.round(1.5) devuelve 2 (redondeo al entero más cercano), pero Math.round(-1.5) devuelve -1 (redondeo hacia arriba). Para contabilidad pública, esto es incorrecto. Debe usarse BigDecimal con RoundingMode.HALF_EVEN (redondeo al par más cercano) para evitar sesgos estadísticos. El error de usar double para dinero ha causado desfases de céntimos en liquidaciones tributarias.
Math también define PI y E como double, pero con precisión limitada. Para cálculos de georreferenciación de parcelas catastrales con precisión milimétrica, se necesita BigDecimal con 50 decimales. La clase MathContext permite controlar precisión y redondeo en operaciones BigDecimal, esencial en cálculos de superficie y valor catastral.
Clave
La calidad de implementación especificada para Math.random es que se comporta como si fuera un simple generador congruencial lineal.
Esta "calidad de implementación" admite predicibilidad, lo que lo hace inseguro: si se conoce la semilla, se pueden predecir todos los números siguientes.
2.4.5. Excepciones Habituales
Las excepciones en Java no son solo errores, sino un mecanismo de control de flujo diseñado para manejar condiciones excepcionales. NullPointerException es la más común y costosa: en producción, cada NPE representa una transacción fallida. Desde Java 14, NPE incluye mensajes detallados ("Cannot invoke 'String.length()' on null object") que identifican exactamente qué variable es null, facilitando la depuración sin debugger. En sistemas de pago, donde cada NPE puede afectar a ciudadanos, activar - XX:+ShowCodeDetailsInExceptionMessages es obligatorio.
ArrayIndexOutOfBoundsException y StringIndexOutOfBoundsException son subclases de IndexOutOfBoundsException. En bucles que procesan lotes de expedientes, capturar la superclase permite manejar ambas uniformemente. Sin embargo, capturar Exception o Throwable es antipatrón que oculta bugs; se debe capturar específicamente y propagar las demás.
ArithmeticException surge en divisiones por cero con enteros, pero con float/double produce Infinity o NaN sin excepción. En cálculos de presupuesto, NaN se propaga contaminando todo resultado: NaN + 5 sigue siendo NaN. Deben usarse validaciones explícitas con Double.isNaN() o BigDecimal que lanza ArithmeticException en divisiones por cero y overflow controlado.
La jerarquía distingue entre checked exceptions (que deben declararse o capturarse) y unchecked (RuntimeException y sus hijas).
ClassCastException es unchecked, pero en código genérico con casting inseguro no se detecta hasta runtime. Usa instanceof antes de castear en código crítico. En migraciones de legados, findbugs/SpotBugs detecta casts inseguros que podrían lanzar ClassCastException en producción.
La clase Exception y sus subclases que no son también subclases de RuntimeException son excepciones verificadas. Esta distinción de la especificación del lenguaje dictamina si un método debe declarar throws o no, afectando la firma de APIs públicas en sistemas interadministrativos.
2.5. Estructura de un programa Java
Antes de abordar frameworks complejos o arquitecturas empresariales, el técnico especialista debe dominar la estructura física y lógica de un programa Java, pues las decisiones tomadas en estos fundamentos condicionan el despliegue, el mantenimiento y la evolución de sistemas críticos.
El método main no es solo un punto de entrada, sino el contrato entre la aplicación y la JVM que dicta cómo se inicializan recursos globales. El proceso de compilación y ejecución determina si un JAR ejecutará en un entorno cloud de Kubernetes o fallará con NoClassDefFoundError por incompatibilidad de versiones. Y la perspectiva histórica de applets y servlets no es nostalgia, sino comprensión de la deuda técnica que aún mantiene la Administración con sistemas de firma electrónica obsoletos que bloquean migraciones a Java moderno.
En la Administración, muchos sistemas batch de cálculo de ayudas aún usan public static void main con parseo manual de args para seleccionar perfiles (-Dspring.profiles.active=pre), pero sin validación de args.length. Esto causa ArrayIndexOutOfBoundsException que aborta procesos nocturnos, afectando el pago a 30.000 agricultores. Frameworks modernos abstraeen esto, pero los legados requieren que el técnico defienda manualmente cada argumento.
La interconexión entre estos tres temas es directa: un main mal diseñado que no gestiona excepciones al cargar configuración desde args hace que la fase de ejecución falle silenciosamente, sin log usable porque System.err no fue redirigido. Y confundir el ciclo de vida de un servlet con el de un main tradicional lleva a declarar conexiones a BD como variables estáticas en servlets, causando fugas de conexión que solo se detectan tras días de funcionamiento. En oposiciones, se evalúa si sabes que java - cp app.jar com.example.Main busca Main.class en el package raíz, pero en producción lo que importa es saber que si Main está en un JAR modular sin module-info.java, la JVM 17 lanzará IllegalAccessException aunque compile perfectamente.
El método main debe declararse public, static, y void, debiendo especificar un parámetro formal cuyo tipo declarado es array de String. Esta especificación formal del lenguaje parece trivial, pero permite entender por qué static public void main compila (orden de modificadores no importa) pero no es convencional, y por qué public void main(String[] args) no es punto de entrada, un detalle clave al refactorizar legados con múltiples métodos main de utilidad que compilan pero no ejecutan.
2.5.1. Método main
El método main es el punto de entrada estándar, pero su firma exacta public static void main(String[] args) tiene matices. public permite que la JVM lo invoque desde fuera del paquete; static permite la invocación sin instancia (la JVM no ejecuta new Main().main()); void indica que no retorna valor al sistema operativo (a diferencia de C). El array args contiene argumentos de línea de comandos; si no se pasan, su longitud es 0, no null. Acceder a args[0] sin validar lanza ArrayIndexOutOfBoundsException, un error clásico en herramientas de línea de comandos de administración.
Desde Java 9, el método main puede sobrecargarse. public static void main(String... args) con varargs es válido y equivalente. Incluso puede declararse sin argumentos: public static void main() compila pero no es punto de entrada. La JVM busca exactamente String[], y si encuentra varargs, lo acepta por compatibilidad. Esto es trampa habitual en tests de conocimiento avanzado.
El array args permite pasar configuración dinámica: -Dconfig.file=app.properties o -Xmx4g. En aplicaciones de despliegue en múltiples entornos (desarrollo, preproducción, producción), parsear args para cargar profiles es patrón común. Frameworks como Spring Boot lo abstraen con
@SpringBootApplication y --spring.profiles.active=prod, pero el parsing manual es necesario en apps
sin framework.
La VM también puede invocar métodos main arbitrarios: java -cp app.jar com.example.MainClass arg1 arg2. Esto permite múltiples puntos de entrada en el mismo JAR, útil para herramientas CLI. En sistemas batch, tener un main por proceso (importación, validación, exportación) en el mismo artefacto simplifica despliegue. Cada main debe validar argumentos con Objects.requireNonNull() y mostrar usage en caso de error.
El método main debe declararse public, static, y void. Debe especificar un parámetro formal cuyo tipo declarado es array de String. Esta especificación formal de la documentación explica por qué static
public void main compila (orden de modificadores no importa) pero no es convencional.
2.5.2. Proceso de Compilación y Ejecución
El ciclo javac + java es simple en apariencia, pero oculta complejidad. javac -cp lib/* src/Main.java -d bin compila con classpath explícito, resolviendo dependencias en runtime. Sin -cp, javac solo ve clases del JDK. En proyectos Maven/Gradle, el classpath se gestiona automáticamente, pero en legados ANT es manual y fuente de NoClassDefFoundError. La opción -d controla la estructura de paquetes: sin ella, los .class se generan en el directorio actual, rompiendo package.
Desde Java 9, el sistema de módulos (JPMS) altera la compilación: javac -p modulos -m com.example/app/Main.java usa module path, no classpath. Los módulos requieren module-info.java que declara dependencias requires y exports exports. En migraciones de monolitos a módulos, un error común es no exportar paquetes, causando IllegalAccessError en runtime aunque compile.
La fase de ejecución inicia con el Class Loader leyendo .class y verificando bytecode. Luego, el JIT (HotSpot) compila métodos frecuentes a código nativo (código máquina del CPU). Esto ocurre después de 10,000 invocaciones (umbral por defecto). Con -XX:+PrintCompilation se ve qué métodos se compilan. En benchmarks, el "warmup" es esencial; medir performance sin calentar el JIT da resultados irreales. En sistemas de validación de firma, el primer documento tarda 500ms, el décimo 50ms tras JIT.
El flag -server vs -client ya no existe en Java 9+; la JVM detecta automáticamente si es servidor (64-bit, >2GB RAM) y usa optimizaciones agresivas. Sin embargo, -XX:+AggressiveOpts y - XX:+UseStringDeduplication pueden mejorar performance en heaps con muchas cadenas duplicadas, como en logs. La deduplicación de strings reduce heap en un 20-30% en aplicaciones que loguean constantemente IDs de expediente.
Recuerda
La Máquina Virtual de Java carga, enlaza e inicializa dinámicamente clases e interfaces.
Esta carga dinámica de la especificación permite lazy loading, cargando clases solo cuando se usan, optimizando startup en aplicaciones modulares.
2.5.3. Applets y servlets: una Perspectiva Histórica
Los applets fueron la apuesta de Java para aplicaciones ricas en navegador, pero fallaron por razones técnicas y de seguridad. Ejecutaban en un sandbox con permisos restringidos, pero exploits como deserialización maliciosa y vulnerabilidades en Java Plugin permitían escape del sandbox. Los navegadores modernos eliminaron soporte NPAPI (2015-2020), enterrando definitivamente los applets. En la Administración, algunos sistemas de firma electrónica de 2010 aún usan applets, forzando a mantener Java 8 en modo Enterprise con políticas de seguridad extremas, una deuda técnica crítica.
Los servlets, definidos en especificación JSR-369 (Servlet 4.0), son la base de Jakarta EE. Un servlet es una clase Java que extiende HttpServlet y sobrescribe doGet()/doPost(). El contenedor (Tomcat, Jetty) gestiona su ciclo de vida: carga, init(), service(), destroy(). Los servlets son singletons; el contenedor crea una instancia y la reutiliza para todos los requests. Por eso, atributos de instancia son compartidos y no thread-safe. Esto es fuente de bugs en código novato que usa campos para almacenar estado de request.
La evolución de servlets llevó a JSP (JavaServer Pages), que son servlets compilados automáticamente con sintaxis HTML embebida. JSP fueron populares en 2000-2010 pero cayeron en desuso por mezclar lógica con presentación. JSF (JavaServer Faces) añadió componentes UI, pero su complejidad llevó al auge de frameworks como Spring MVC y, finalmente, a APIs REST con JAX-RS. Hoy, la tendencia es servicios sin estado (stateless) con JSON, no JSP.
En arquitectura moderna, los servlets siguen siendo relevantes como base. Spring Boot embebe Tomcat y registra un DispatcherServlet que enruta todas las peticiones. Entender el ciclo de vida del servlet ayuda a debuggear problemas de inicialización y filtros. Los filtros (javax.servlet.Filter) permiten interceptar requests para logging, seguridad o CORS, siendo esenciales en APIs interadministrativas.
Importante
El método service es llamado por el contenedor de servlets para permitir que el servlet responda a una petición. Este método solo se llama después de que el método init() del servlet haya completado exitosamente.
Esta secuencia de la especificación de Servlets de Jakarta garantiza que init() configure recursos (pools de conexiones) antes de procesar peticiones, patrón clave en servicios robustos.
3. Fundamentos del desarrollo basado en componentes
en componentes
El Desarrollo Basado en Componentes (DBC) constituye el pilar vertebral de la ingeniería del software en el sector público moderno, donde la reutilización efectiva trasciende la mera eficiencia técnica para convertirse en un mandato legal derivado del Esquema Nacional de Interoperabilidad y del principio de eficiencia en el uso de recursos públicos.
Según Clemens Szyperski "Un componente de software es una unidad de composición con interfaz y requisitos bien definidos, reusable e independiente en tiempo y espacio, que puede desarrollarse, adquirirse e integrarse con otros componentes. En la práctica, debe empaquetarse como unidad instalable, con documentación exhaustiva y depender mínimamente de terceros" Abordar este epígrafe requiere asumir que el DBC no es un catálogo de definiciones, sino un sistema de decisiones profesionales que el técnico auxiliar debe operar bajo presión normativa, presupuestaria y de servicio. Los tribunales no evalúan la capacidad de recitar, sino la de aplicar con criterio en escenarios reales: migración de sistemas monolíticos heredados, integración con arquitecturas híbridas, garantía de cumplimiento del ENS en componentes críticos.
La estructura de esta unidad responde a una lógica de profundización progresiva:
Contexto legislativo y empresarial: Análisis de por qué el DBC es obligatorio en la AGE, con evidencia cuantitativa (reducción de costes del 25-40%) y casos prácticos (CTT, SEPE, Agencia Tributaria). Sin este fundamento, cualquier propuesta técnica carece de sustento administrativo.
Principios arquitectónicos: Los contratos, el polimorfismo y la composición tardía son los mecanismos de confianza entre componentes. Su comprensión superficial es la causa del 80% de los fallos de integración detectados en auditorías de la Cuenta General de la Nación.
Taxonomía de arquitecturas: La distinción entre componentes, microservicios y servicios web no es teórica; determina costes de despliegue, ventanas de mantenimiento, estrategias de escalado y responsabilidades legales ante incumplimiento de SLAs. Confundirlos es el error más costoso en la modernización de sistemas públicos.
Infraestructura tecnológica: El CLR/CTS en .NET y las especificaciones Java EE/Jakarta EE definen las posibilidades y límites de interoperabilidad. En entornos con legados de 20 años, la decisión de mantener .NET Framework 4.8 o migrar a .NET 8 tiene implicaciones presupuestarias millonarias.
Testing y gobernabilidad: La calidad en la AGE no es "cobertura del 85%", sino defensa ante auditoría.
Un componente sin trazas W3C Trace Context, sin pruebas de mutación o sin validación de cumplimiento normativo es una vulneración del deber de diligencia y puede derivar en responsabilidad patrimonial.
Este ep'igrafe debe traducirse en una capacidad operativa: diagnosticar una violación de contrato sin acceso al código fuente, decidir si un requisito se implementa como componente o microservicio en función del Expediente de Contratación, y argumentar ante un responsable de seguridad por qué una actualización requiere nueva homologación ENS.
El desafío es que no basta con saber; es preciso construir argumentos técnicos que sean jurídicamente defensibles. Por eso, cada epígrafe incluye referencias normativas, métricas de la AGE, etc.
3.1. Introducción al DBC en el contexto empresarial público
El Desarrollo Basado en Componentes (DBC) representa un paradigma fundamental en la ingeniería del software moderna, especialmente relevante en el ámbito de la administración pública donde la reutilización y estandarización no son meras conveniencias, sino verdaderos imperativos legislativos.
Esta metodología se fundamenta en la construcción de sistemas mediante la integración de unidades software prefabricadas, probadas y documentadas, conocidas como componentes, que exponen
interfaces bien definidas y operan como verdaderos "ladrillos digitales" en arquitecturas complejas.
Contexto
En el contexto específico del sector público español, el DBC adquiere una dimensión adicional: debe alinearse con directrices como el Esquema Nacional de Interoperabilidad (ENI) y el Esquema Nacional de Seguridad (ENS), lo que implica que los componentes no solo deben ser funcionales, sino también cumplir con requisitos de auditoría, trazabilidad y accesibilidad que raramente se demandan en el sector privado.
La experiencia acumulada en proyectos como la Plataforma de Intermediación de Datos del SEPE o los sistemas de gestión tributaria de la Agencia Tributaria demuestra que el DBC permite una reducción de costes de mantenimiento que oscila entre el 25% y el 40% en ciclos de vida de cinco años. Esta economía de escala se produce porque cada componente, una vez validado y homologado por la correspondiente autoridad técnica (como la CTTI en Cataluña o la red de tecnologías de la Comunidad de Madrid), puede ser reutilizado en múltiples proyectos sin necesidad de revalidación completa. Sin embargo, esta ventaja conlleva una responsabilidad enorme: el diseñador de componentes debe anticipar no solo los requisitos funcionales actuales, sino también las futuras evoluciones normativas, como las modificaciones en el Reglamento General de Protección de Datos o las nuevas directrices de seguridad europeas.
El DBC transforma el día a día de manera radical. En lugar de depurar monolitos de código espagueti con dependencias ocultas, el profesional se enfrenta a arquitecturas donde cada componente es una caja negra con comportamiento predecible. Esto facilita la localización de fallos: si el servicio de autenticación de ciudadanos mediante Cl@ve funciona correctamente en el módulo de renovación de DNI pero falla en la solicitud de ayudas del Ministerio de Agricultura, la causa probable reside en la configuración del componente o en su integración, no en el código interno. Esta encapsulación permite que el personal con formación más especializada se centre en el núcleo del componente, mientras que el técnico auxiliar gestiona su despliegue, monitorización y mantenimiento operativo.
Panorama de plataformas Java (visión de conjunto)
Java SE: Base del ecosistema Java (JVM, bibliotecas estándar, herramientas). Soporta aplicaciones de propósito general (CLI, servicios, librerías compartidas). Punto de partida para el resto de plataformas.
Java ME: Perfil para dispositivos/embebidos con recursos limitados. Reduce superficie de API y huella, priorizando eficiencia y portabilidad en hardware restringido.
Jakarta EE (antes Java EE): Plataforma empresarial para aplicaciones multinivel. Aglutina especificaciones de uso frecuente: JAX-RS (REST), JPA (persistencia), JTA (transacciones), CDI (inyección/contexts), Servlet/Pages y Seguridad, entre otras, garantizando portabilidad entre servidores compatibles.
JavaFX: Conjunto para interfaces gráficas de escritorio con render acelerado, binding de datos y controles modernos. Se distribuye de forma modular y admite empaquetado nativo.
JavaFX en el contexto de cliente de escritorio
Su finalidad es construir UI multiplataforma de escritorio con experiencia rica y consistente (p. ej., utilidades administrativas internas o paneles de operación).
Características clave:
-
Integración directa con el ecosistema Java (Java/Kotlin).
-
Binding de datos y sistema de componentes; soporte de animaciones/escenas.
-
Distribución modular (desacoplado del JDK) y empaquetado nativo para despliegues controlados.
Es adecuado cuando se requiere homogeneidad de interfaz en puestos Windows/Linux/macOS sin depender del navegador y/o uando la latencia de red o las políticas de seguridad (p. ej., entornos restringidos) desaconsejan clientes puramente web.
JavaFX como cliente de escritorio no sustituye a Jakarta EE en servidor, si no que normalmente suele coexistir como un cliente enriquecido frente a servicios REST/mensajería expuestos por la capa de negocio.
Transición
La transición hacia este modelo no ha sido instantánea en la administración pública. Durante la década de 2000, predominaban los sistemas monolíticos heredados escritos en COBOL o Visual Basic 6, donde cualquier modificación requería un ciclo completo de análisis de impacto que duraba meses. La crisis económica de 2008 actuó como catalizador, forzando la búsqueda de eficiencias que hicieran compatibles la austeridad presupuestaria con la modernización digital. Así surgieron los primeros repositorios de componentes compartidos, como el Centro de Transferencia de Tecnología (CTT) del Ministerio de Hacienda, que hoy alberga más de 500 componentes reutilizables. Esta evolución histórica es crucial para entender por qué el DBC no es solo una moda tecnológica, sino una supervivencia organizacional en un entorno de recursos limitados y demandas crecientes.
La complejidad inherente al DBC en el sector público radica en la necesidad de conciliar velocidad de desarrollo con rigor burocrático. Mientras que una startup puede implementar un nuevo componente de pago en cuestión de días, una administración debe cumplir con expedientes de contratación, informes de impacto de seguridad y procesos de homologación que pueden extenderse semanas. Esta tensión ha generado modelos híbridos donde los componentes se clasifican en categorías de riesgo: los de baja criticidad (como generadores de PDFs para informes estadísticos) pueden desplegarse con trámites simplificados, mientras que los de alta criticidad (como validadores de identidad) requieren auditorías exhaustivas.
Esta categorización, formalizada en guías como la "Guía de Desarrollo de Componentes de la AGE" de 2021, permite que se apliquen procedimientos diferenciados según el contexto.
3.2. Relación entre componentes, microservicios y servicios web
y servicios web
Los contratos de componente representan el núcleo semántico del DBC, estableciendo un acuerdo formal entre el proveedor y el consumidor que trasciende la mera firma de métodos. En el ecosistema .NET, estos contratos se materializan mediante interfaces (IL) y clases abstractas que definen no solo la sintaxis (qué métodos existen), sino también el comportamiento esperado (cómo deben reaccionar ante entradas específicas). Un contrato robusto especifica precondiciones (qué debe ser cierto antes de la ejecución), postcondiciones (qué queda garantizado después) e invariantes (propiedades que siempre se mantienen).
Por ejemplo, un componente de validación de NIF para la administración española debe documentar que acepta un string de 8 dígitos más una letra mayúscula, devuelve un booleano indicando validez, y nunca lanza excepciones para formatos incorrectos (en su lugar, devuelve false). Esta especificación contractual permite que un equipo desarrolle el front-end de una aplicación de registro municipal mientras otro, simultáneamente, perfecciona el algoritmo de validación, con la confianza de que ambos operarán sobre el mismo modelo de datos.
El polimorfismo en componentes empresariales no se limita a la sobrescritura de métodos, sino que se extiende a la capacidad de intercambiar implementaciones completas sin reconstruir el consumidor.
Consideremos el caso del sistema de notificaciones de la DGT: el componente INotificador podría tener implementaciones concretas como NotificadorEmail, NotificadorSMS y NotificadorSedeElectronica.
Cuando un ciudadano supera el límite de velocidad, el sistema de multas invoca notificador.enviar() sin conocer la implementación subyacente. La decisión de qué canal usar se toma en tiempo de ejecución según preferencias del usuario y criticidad del mensaje. Esta flexibilidad resulta invaluable cuando surge una nueva normativa (como el Reglamento eIDAS 2.0) que exige notificaciones a través de aplicaciones de mensajería certificadas; basta con desarrollar NotificadorWallet sin modificar ni recompilar el sistema de multas existente.
Para ordenar las ideas y ver de un vistazo cómo se relacionan contratos, componentes, servicios web y microservicios -y dónde encajan el polimorfismo y la composición tardía-, observa la siguiente figura.
Como se aprecia, los contratos sostienen todo el ecosistema: permiten sustituir implementaciones sin tocar al consumidor y habilitan la composición tardía en tiempo de despliegue. Retomando el hilo, veamos ahora cómo CDI/EJB y la resolución de dependencias en Kubernetes (por ejemplo, vía beans.xml, @Inject y @Resource) materializan este ensamblaje dinámico en entornos reales de la Administración.
La composición tardía (late composition) constituye el mecanismo técnico que habilita la verdadera agilidad en el DBC empresarial, permitiendo ensamblar componentes en momentos muy avanzados del ciclo de vida. En Java EE/Jakarta EE, esto se logra mediante tecnologías como CDI (Contexts and Dependency Injection) o EJB con inyección de dependencias, donde las referencias a componentes se resuelven en despliegue o incluso en tiempo de ejecución. Tomemos el ejemplo del sistema de solicitudes de ayudas del Ministerio de Universidades: durante el desarrollo, el módulo de evaluación académica declara una dependencia genérica @Inject IValidadorTitulo. En producción, el contenedor de aplicaciones (WildFly, GlassFish) resuelve esta referencia mediante la lectura de descriptores de despliegue (beans.xml) que especifican ValidadorTituloExtranjero o ValidadorTituloNacional según la convocatoria. Esta estrategia permite que una misma aplicación soporte múltiples procesos de validación sin cambiar su código fuente, simplemente modificando archivos de configuración XML o anotaciones, operación que puede realizar un técnico auxiliar sin acceso al código fuente original.
La interacción entre estos tres principios crea un ecosistema de desarrollo extraordinariamente robusto cuando se implementa correctamente. Sin embargo, en la práctica administrativa, surgen desafíos derivados de la acumulación de deuda técnica. Por ejemplo, es común encontrar componentes heredados que violan el principio de sustitución de Liskov: una implementación de IGestorDocumental para archivos PDF puede lanzar UnsupportedOperationException para métodos de firmas electrónicas que sí están definidos en la interfaz. Esto rompe implícitamente el contrato y fuerza a los consumidores a conocer detalles internos, destruyendo la transparencia.
La experiencia en migraciones de sistemas de registro civil demuestra que este tipo de violaciones incrementan el coste de integración hasta en un factor de 3, porque cada nuevo consumidor debe implementar lógica defensiva. Por eso, las guías de desarrollo de la AGE recomiendan ahora auditorías de contrato automatizadas mediante herramientas como ArchUnit.NET o jqwik en Java.
La trascendencia práctica radica en la capacidad de diagnosticar problemas de integración sin profundizar en implementaciones. Cuando un componente falla, el primer paso no debe ser depurar su código, sino verificar el cumplimiento contractual: ¿la entrada cumple las precondiciones? ¿el entorno proporciona las dependencias declaradas? ¿las configuraciones de composición tardía son consistentes?
Esta metodología reduce el tiempo de resolución de incidencias en un 60% según métricas del catastro electrónico. Además, permite crear suites de pruebas de regresión que validan contratos en vez de comportamientos internos, un enfoque mucho más mantenible cuando se actualizan versiones. La clave está en entender que el valor de un componente no reside en su código, sino en la confianza que genera su contrato estable y bien documentado.
Nota
El contrato de un componente debe especificar no solo la firma de métodos sino también su comportamiento ante condiciones de estrés. Según Meyer (1997) en "Object-Oriented Software
Construction", un contrato deficiente es peor que la ausencia de contrato, porque crea falsas expectativas.
En entornos .NET, Code Contracts (aunque obsoleto) sentó las bases para la especificación mediante atributos como Contract.Requires, mientras que en Java EE/Jakarta EE, Bean Validation y anotaciones como @NotNull cumplen funciones similares.
3.3. Principios arquitectónicos: contratos, polimorfismo y composición tardía
y composición tardía
La distinción conceptual entre componentes, microservicios y servicios web constituye una fuente de confusión crónica en equipos de desarrollo público, donde la tendencia a etiquetar cualquier pieza modular como "microservicio" ha generado arquitecturas híbridas con los inconvenientes de ambos mundos.
Componentes
Los componentes son unidades de despliegue intra-proceso: residen dentro del mismo espacio de memoria de una aplicación y se comunican mediante llamadas a métodos directas, ofreciendo latencia prácticamente nula pero compartiendo el ciclo de vida del contenedor. Por ejemplo, el módulo de cálculo de plusvalía municipal en un sistema de recaudación es un componente: se despliega como JAR dentro del EAR de la aplicación y su disponibilidad depende exclusivamente del servidor de aplicaciones.
Microservicios
Los microservicios, en contraste, son unidades de despliegue independientes con sus propios procesos, bases de datos y ciclos de vida autónomos. Un sistema de gestión de expedientes del registro de una universidad pública podría descomponerse en microservicios como ServicioExpediente, ServicioDocumentación, ServicioNotificaciones y ServicioFirmaElectronica, cada uno con su propio repositorio Git, pipeline CI/CD y equipo de desarrollo. La comunicación entre microservicios obligatoriamente atraviesa la red, típicamente mediante HTTP/REST o protocolos asíncronos como AMQP, lo que introduce latencia y complejidad en la gestión de transacciones distribuidas. Esta separación permite escalar horizontalmente solo los servicios críticos (por ejemplo, el de firma electrónica durante periodos de matrícula) sin desperdiciar recursos en componentes estáticos.
Servicios web
Los servicios web son la capa de protocolo que habilita la interoperabilidad entre tecnologías heterogéneas, actuando como el pegamento que une componentes y microservicios en ecosistemas de la administración. Un servicio web SOAP (WSDL-first) permite que un componente Java EE del Sistema de Información de la Seguridad Social se comunique con un microservicio .NET del Registro Central de Personal, a pesar de las diferencias tecnológicas.
La siguiente tabla ilustra las diferencias clave:
Característica Componente Java EE Microservicio Servicio Web
Unidad de despliegue
JAR/EAR dentro del servidor
Contenedor Docker/VM independiente
Archivo WAR/SOAP endpoint
Comunicación Llamada a método
| directa | HTTP/REST, gRPC, Mensajería | SOAP, REST, XML-RPC |
|---|---|---|
| Tiempo de vida | Depende del contenedor | Independiente, gestionado por |
orchestrator
Depende del servidor web
Escalabilidad Vertical (más CPU al servidor) Horizontal (más réplicas) Horizontal (más nodos)
Latencia Microsegundos Milisegundos (1-100ms) Milisegundos (10-
500ms)
Ejemplo AGE Validador NIF en JAR Servicio de notificaciones push Interfaz SOAP de Pago CTT
Para sintetizar las diferencias clave entre componentes, microservicios y servicios web -y su papel dentro de las arquitecturas híbridas de la Administración Pública-, la siguiente figura resume sus características esenciales, su ámbito de despliegue y sus implicaciones operativas.
Como se observa, cada paradigma aporta ventajas específicas: los componentes garantizan velocidad y transacciones consistentes, los microservicios habilitan escalabilidad y autonomía, y los servicios web aseguran interoperabilidad institucional. Comprender cuándo aplicar cada enfoque es esencial para diseñar arquitecturas sostenibles y evitar errores comunes como la sobredimensión innecesaria de infraestructuras.
Teoria vs Realidad
La realidad práctica en entornos como la Administración General del Estado (AGE) es que coexisten los tres paradigmas en arquitecturas híbridas. Un ejemplo paradigmático es la Plataforma de Recuperación de Información de la Seguridad Social (PRISS): el front-end JSF consume componentes EJB locales para validaciones complejas, estos componentes invocan microservicios Spring Boot para cálculos actuariales intensivos, y todo el ecosistema se integra con servicios web SOAP legados del INSS de hace 15 años. Esta diversidad técnica exige que el técnico auxiliar domine múltiples herramientas de monitorización:
VisualVM para componentes internos, Prometheus para microservicios y SOAP UI para servicios web.
La decisión arquitectónica de cuándo usar cada patrón no es trivial y debe basarse en criterios objetivos:
-
Los componentes son óptimos para lógica de negocio intensiva que requiere transacciones ACID rápidas (ej: cálculo de retenciones en nóminas de funcionarios).
-
Los microservicios brillan en dominios con requisitos de escalabilidad independiente o equipos de desarrollo distribuidos geográficamente (ej: sistema de cita previa del SEPE con picos de 10x durante crisis).
-
Los servicios web son obligatorios para integración con sistemas ajenos o heredados (ej:
conexión con Registro Mercantil o CIRCE).
El error más común, documentado en informes de auditoría de la Cuenta General de la Nación, es "microservitizar" componentes que nunca necesitarán escalar independientemente, generando sobrecoste operativo en clusters de Kubernetes que justifican apenas 2-3 transacciones por segundo.
La distinción tiene implicaciones directas en las tareas diarias de despliegue y mantenimiento. Desplegar una nueva versión de un componente implica un restart controlado del servidor de aplicaciones (p. ej., WebLogic) en ventanas de mantenimiento pactadas con las mesas de ayuda. Actualizar un microservicio replica el contenedor en Docker Swarm sin downtime aparente, pero explica monitorear la coherencia de datos en eventuales transacciones distribuidas. Publicar una nueva versión de servicio web SOAP obliga a coordinar con los consumidores externos (otras administraciones) la actualización de stubs y WSDLs, proceso que puede durar meses.
Clave
Dominar estas diferencias operativas es lo que permite mantener el
SLA del 99.95% que exige la normativa.
3.4. Lenguajes soportados y requisitos del CLR/CTS
El Common Language Runtime (CLR) de .NET y el Common Type System (CTS) conforman la infraestructura que hace posible la interoperabilidad lingüística, un valor estratégico en la administración pública donde conviven legados en lenguajes diversos con necesidades de modernización. El CLR actúa como máquina virtual de ejecución que gestiona memoria (recolector de basura), seguridad (código seguro/inyeguro), hilos y excepciones, independientemente del lenguaje fuente.
El CTS define un sistema de tipos común que garantiza que un int en C# sea idéntico a un Integer en VB.NET, permitiendo que un componente escrito en F# consuma sin fricción una librería de validación de expedientes desarrollada en C#. Esta unificación es crucial cuando se integran sistemas: el departamento de recursos humanos puede mantener su lógica en VB.NET (herencia de los 90) mientras el equipo de innovación desarrolla nuevas funcionalidades en C# 12, todo dentro del mismo proceso.
Los lenguajes soportados oficialmente por el CLR se dividen en categorías según su madurez y adopción en el sector público. C# lidera con más del 70% del código empresarial, especialmente en aplicaciones de gestión presupuestaria y sistemas de información geográfica (SIG). Su evolución constante (async/await, records, pattern matching) lo hace ideal para nuevos desarrollos. VB.NET persiste en un 20% de los sistemas, predominantemente en módulos de nómina y contabilidad pública donde la estabilidad es prioritaria sobre la innovación. F#, aunque marginal (<5%), está ganando terreno en algoritmos de análisis predictivo para detección de fraude en ayudas sociales, gracias a su fuerte tipado y modelo funcional. Existen también lenguajes experimentales como IronPython o PowerShell para scripting de automatización, aunque raramente se usan en componentes productivos.
El requisito fundamental para que un lenguaje sea CLR-compliant es la compilación a Intermediate Language (IL), un bytecode independiente de la plataforma. Este proceso transforma código fuente en instrucciones de stack-based VM que el JIT (Just-In-Time compiler) convierte a machine code nativo en ejecución. Veamos un ejemplo práctico de interoperabilidad:
// Componente en C# para validación de expedientes de la AGE
public interface IValidadorExpediente
{
bool EsValido(string numeroExpediente);
string CentroGestor { get; }
}
// Implementación en VB.NET para expedientes de la Seguridad Social
Public Class ValidadorSegSocial
Implements IvalidadorExpediente
Public Function EsValido(numero As String) As Boolean Implements
IValidadorExpediente.EsValido
' Lógica específica: número + código provincial + dígito control
Return Regex.IsMatch(numero, "^\d{10}[A-Z]\d{2}$")
End Function
Public ReadOnly Property CentroGestor As String Implements
IValidadorExpediente.CentroGestor
Get
Return "TGSS"
End Get
End Property
End Class
// Consumo desde F# en módulo de análisis de datos
let validador = new ValidadorSegSocial()
let expedientes = ["1234567890A28"; "9876543210B15"]
let validos = expedientes |> List.filter validador.EsValido
Este código, una vez compilado, genera IL idéntico en estructura, permitiendo que el consumidor F# no distinga si la implementación subyacente es C#, VB.NET o cualquier otro lenguaje CLR-compliant. El CTS garantiza que los tipos string (System.String) y bool (System.Boolean) tengan idéntica representación en memoria y comportamiento ante excepciones.
Sin embargo, la libertad lingüística impone restricciones de arquitectura que el técnico auxiliar debe conocer. El CLR diferencia entre código seguro (verificado por el runtime) e inseguro (con punteros directos), requiriendo políticas CAS (Code Access Security) que en entornos públicos suelen ser restrictivas. Por ejemplo, un componente que use P/Invoke para llamar a una librería C++ nativa de validación de DNI necesita permisos SecurityPermissionFlag.UnmanagedCode, que solo se conceden tras revisión del responsable de seguridad. Además, el recolector de basura generacional del CLR (Gen0, Gen1, Gen2) comporta implicaciones de rendimiento: objetos grandes (>85KB) van directamente al heap de objetos grandes (LOH), fragmentándolo si no se gestionan con ArrayPool<T> en operaciones de batch processing de bases de datos. En sistemas de carga masiva como el envío de declaraciones de la renta, esta configuración puede marcar la diferencia entre un SLA del 99.9% y caídas por OutOfMemoryException.
La versión del CLR también condiciona las estrategias de despliegue. .NET Framework 4.8 (solo Windows) sigue siendo el estándar en muchos organismos por certificación con aplicaciones heredadas, mientras .NET 8 (multiplataforma) se impone en nuevos desarrollos cloud-native. El técnico auxiliar debe gestionar side-by-side: un servidor puede ejecutar simultáneamente aplicaciones .NET Framework y .NET 8 en contenedores Docker aislados, pero la comunicación entre ellas requiere serialización explícita (gRPC, REST) porque no comparten CTS.
Esta complejidad operativa justifica la creación de catálogos de componentes homologados por versión CLR, como mantiene la CTTI, evitando que un componente compilado para .NET 6 cause BadImageFormatException al cargarse en un runtime incompatible.
3.5. Estrategias de testing y calidad en componentes
La garantía de calidad en componentes de administración pública excede el mero testing funcional, incorporando verificaciones de cumplimiento normativo, interoperabilidad y seguridad que conforman lo que se denomina "testing de gobernabilidad".
El primer nivel, testing unitario, debe cubrir no solo ramas de código sino también escenarios de negocio específicos. Para un componente de cálculo de pluses de transporte para funcionarios, las pruebas deben incluir casos límite como cambios de residencia en mitad de mes, exenciones por movilidad reducida, y actualizaciones retroactivas de baremos. Herramientas como NUnit para .NET o JUnit 5 con Jupiter para Java EE permiten parametrizar estos escenarios usando CSVs con datos anonimizados del entorno de preproducción, cumpliendo con la LOPD-GDD desde la fase de desarrollo.
El testing de integración adquiere una dimensión especial cuando los componentes dependen de sistemas externos heredados, típicos en la AGE. Un componente de validación de identidad mediante el Sistema de Verificación de Datos de Identidad (SVDI) no puede probarse contra el servicio real en cada commit por restricciones de carga y coste. La solución adoptada por la DGT y el CNIO es implementar dobles de prueba (test doubles) controlados: stubs que devuelven respuestas prefijadas y simuladores que reproducen comportamientos complejos como timeouts o respuestas mal formadas. Spring Cloud Contract y Pact.NET facilitan el consumer-driven contract testing, donde el equipo del componente
publica expectativas de formato que el equipo del SVDI debe cumplir. Este enfoque redujo un 80% los
defectos de integración en la última renovación del sistema de cita previa del SEPE.
El testing de seguridad es no negociable en componentes que manejan datos clasificados según el ENS.
Se requiere un pipeline de Static Application Security Testing (SAST) que analice el bytecode en busca de vulnerabilidades OWASP Top 10. Herramientas como SonarQube con reglas específicas de Java EE (detecta @RequestScoped mal usado) o Security Code Scan para .NET (identifica inyección LDAP) deben ejecutarse en cada pull request con umbrales de calidad que bloqueen el merge si se supera una deuda técnica crítica. Además, el Dynamic Application Security Testing (DAST) con OWASP ZAP o Burp Suite prueba el componente desplegado en preproducción, buscando vulnerabilidades de runtime como XML External Entity (XXE) en parseadores SOAP.
La siguiente lista detalla la estrategia de testing en capas recomendada por la "Guía de Calidad de Componentes de la AGE" (2023):
-
Testing de componente aislado: Ejecución con base de datos en memoria (H2, SQLite) y mocks de dependencias. Ratio de cobertura mínimo: 85% de ramas, 90% de condiciones complejas.
-
Testing en contenedor: Despliegue del componente en Docker con base de datos real pero sin dependencias de red externas. Valida comportamiento con JPA/Hibernate y transacciones JTA.
-
Testing de integración contratual: Verificación de schemas WSDL/OpenAPI contra consumidores reales usando WireMock o Mountebank.
-
Testing de carga y estrés: Simulación de 100-1000 TPS con JMeter o k6, monitoreando leaks de memoria y deadlocks en pools de conexiones.
-
Testing de conformidad normativa: Validación automática de logs de auditoría (formato XES), trazas W3C Trace Context y cumplimiento de ENS (cifrado TLS 1.3 mínimo).
El testing de mutación ha emergido como práctica avanzada en componentes de alta criticidad. Usando Pitest para Java o Stryker.NET, se introducen mutaciones artificiales en el código (cambio de > por >=, eliminación de llamadas a métodos) y se verifica que las pruebas unitarias detectan estos cambios. Un componente de cálculo de pensiones de la Seguridad Social que sobrevive a mutaciones con tasa de detección >95% tiene una confiabilidad documentada que justifica su certificación para producción sin revisión manual extensa. Esta automatización es esencial cuando los equipos de desarrollo son pequeños y la presión por salir a producción es alta, como ocurre en las convocatorias de ERTE donde los sistemas deben adaptarse en días.
Finalmente, el test data management presenta desafíos éticos y legales únicos. No es legal usar datos de producción de ciudadanos, inclusio anonimizados, en entornos de desarrollo. La solución adoptada por el Ministerio de Inclusión es generar datos sintéticos con la misma distribución estadística que los reales, usando técnicas de generación basadas en expresiones regulares para NIFs válidos y distribuciones de Pareto para importes de ayudas. Herramientas como Faker.NET o Java Faker se configuran con locales específicos (es-ES) para generar nombres, direcciones y teléfonos coherentes con la realidad demográfica española. Los datasets sintéticos se versionan en Nexus/Artifactory como artefactos Maven/NuGet, asegurando reproducibilidad de pruebas años después del despliegue inicial.
3.6. Seguridad de la cadena de suministro (dependencias) en Java/.NET
en Java/.NET
Riesgo silencioso: dependencias y cadena de suministro
En aplicaciones reales, una parte importante del software no la escribe el equipo: llega empaquetada en forma de librerías. Ese modelo es eficiente -reduce tiempos, reutiliza código probado-, pero introduce un riesgo particular: un componente puede estar correctamente diseñado y, aun así, heredar vulnerabilidades de su ecosistema. Y lo más peligroso es que ese riesgo no se detecta con pruebas funcionales; el sistema "hace lo que debe" y, sin embargo, contiene una pieza con un fallo conocido.
En Java y .NET este patrón se repite por la forma habitual de construir aplicaciones: se agregan dependencias directas y, con ellas, llegan dependencias indirectas (transitivas). Esto hace que la superficie real de exposición sea mayor que lo que se ve en el fichero de proyecto. A nivel de operación, la cadena de suministro no se limita a "si el servidor está parcheado", sino a si la aplicación incorpora componentes de terceros seguros, identificables y controlados.
Cuando se habla de seguridad de la cadena de suministro, conviene entender que el objetivo no es "no usar librerías", sino poder responder con rapidez y rigor a preguntas que en un entorno auditado aparecen tarde o temprano: qué dependencias se usaron, quién las proporcionó, qué versión exacta entró en el artefacto final y qué se hizo cuando apareció una alerta de seguridad.
Inventario de dependencias: saber qué hay realmente dentro
El primer pilar es el inventario. En términos prácticos, es disponer de una lista fiable de dependencias con versión exacta, incluyendo transitivas. En Java esto se apoya en gestores como Maven/Gradle; en .NET, en NuGet. Pero el punto operativo es común: el fichero de proyecto es el punto de partida, no la foto final. La foto final la determina el proceso de resolución y restaurado.
En soporte, este inventario se vuelve esencial cuando ocurre una alerta (por ejemplo, un CVE relevante) y hay que responder sin pánico: "¿estamos afectados?", "¿qué servicios usan esa librería?", "¿en qué entornos está desplegado?". Sin inventario, se acaba revisando a mano proyecto a proyecto, con riesgo de olvidar dependencias transitivas o versiones efectivas.
Por eso, en entornos serios se trata el inventario como un artefacto del ciclo de vida: una evidencia vinculada a cada build. No se busca burocracia; se busca poder reconstruir el estado de forma objetiva.
Vulnerabilidades conocidas: detectar a tiempo, priorizar y actuar
El segundo pilar es la detección de vulnerabilidades conocidas. No todas las vulnerabilidades tienen la misma urgencia ni el mismo impacto: importa la criticidad, si el componente se utiliza realmente, si está expuesto a red, si requiere autenticación o si solo afecta a una ruta poco probable. En otras palabras, no se trata de "corregir todo hoy", sino de tener un mecanismo para identificar riesgos y decidir con criterio.
En la práctica, la detección se apoya en bases de datos públicas de vulnerabilidades y en análisis automatizados dentro del pipeline de integración/entrega. Lo importante, a nivel ATI, es comprender el flujo de trabajo: aparece un aviso, se determina si la versión afectada está presente, se evalúa el alcance (qué parte de la aplicación usa esa librería) y se decide la mitigación: actualización, parche temporal, o medidas de contención (configuración, desactivación de funcionalidad, filtrado en proxy, etc.) hasta desplegar una versión corregida.
En Java y .NET hay además un detalle frecuente que complica la vida: la vulnerabilidad puede estar en una dependencia transitiva que "nadie pidió explícitamente". Esto explica por qué, al actualizar, no basta con subir "el paquete principal": a veces hay que ajustar resoluciones, forzar versiones o sustituir componentes para eliminar la versión vulnerable del grafo de dependencias.
Integridad y procedencia: confiar sin "tragar" a ciegas
La cadena de suministro no es solo "versión y CVE". También es procedencia. En entornos corporativos se trabaja con repositorios internos (artifact repositories) y con reglas sobre qué fuentes se consideran confiables. Esto reduce riesgos como dependencias publicadas por actores maliciosos, suplantaciones, o librerías no verificadas que entran por comodidad.
Aquí la idea clave es sencilla: si cualquier desarrollador puede incorporar un paquete desde cualquier origen sin control, el sistema queda expuesto no solo a vulnerabilidades conocidas, sino a componentes de procedencia dudosa. La mitigación habitual es concentrar el consumo en repositorios controlados, usar listas de permitidos, y aplicar validaciones básicas (firma, checksum, políticas de publicación interna). No hace falta entrar en criptografía avanzada; basta con entender el propósito: reducir la probabilidad de que "entre algo" sin trazabilidad y sin revisión mínima.
Trazabilidad del artefacto: poder demostrar qué se desplegó
El tercer pilar es el que más pesa en auditoría y en respuesta ante incidentes: la trazabilidad. En un entorno auditado, "creo que desplegamos la versión X" no es una respuesta aceptable. Se necesita poder afirmar con evidencias: este binario corresponde a este build, se generó en esta fecha, con estas dependencias, desde este repositorio y con esta configuración.
En la práctica, esto se refleja en dos capacidades:
-
Rastrear el camino desde el código y sus dependencias hasta el artefacto publicado (jar/war en Java, assemblies/paquetes en .NET, contenedor, etc.).
-
Repetibilidad: poder reconstruir el artefacto si hace falta, con el mismo resultado o con diferencias explicables.
Esa trazabilidad es lo que permite actuar bien cuando llega una alerta de seguridad. Si se anuncia una vulnerabilidad crítica en una librería ampliamente usada, la respuesta eficiente no es "revisemos todo"; es "consultemos los inventarios de builds, identifiquemos qué servicios y qué despliegues están afectados, y planifiquemos actualizaciones con prioridad real". Sin ese rastro, cada alerta se convierte en un incendio.
Situaciones que ayudan a entender por qué esto importa.
Escenario 1: vulnerabilidad pública y urgente.
Se publica una alerta crítica sobre una librería muy común. El equipo de seguridad pide impacto en 24 horas. Si existe inventario y trazabilidad, se localizan los servicios afectados y la versión exacta desplegada. Si no existe, se entra en un ciclo de búsquedas manuales, con dudas sobre transitivas y con riesgo de dejar fuera componentes "menores" que también son puerta de entrada.
Escenario 2: "funciona, pero no pasa auditoría".
La aplicación cumple funcionalmente, pero en auditoría aparece un paquete sin procedencia clara, o una dependencia desactualizada sin justificación. En estos casos, la solución no es solo técnica; es de proceso: repositorios controlados, evidencias de build y un ciclo de revisión de dependencias que demuestre control.
3.7. Estrategias de evaluación y autoevaluación conceptual
La evaluación de competencias en DBC para técnicos auxiliares informáticos no puede reducirse a cuestionarios teóricos, sino que debe medir la capacidad de diagnóstico en escenarios reales de producción. La autoevaluación conceptual se estructura en tres dimensiones: conocimiento declarativo (qué es un componente), conocimiento procedimental (cómo desplegarlo) y conocimiento condicional (cuándo aplicar cada estrategia).
Para entender de un vistazo cómo se articula la evaluación conceptual en el Desarrollo Basado en Componentes (DBC), observa el siguiente esquema. Resume las tres dimensiones del conocimiento y su aplicación práctica en la medición de competencias técnicas dentro de la Administración Pública.
Como se observa, la combinación de evaluación declarativa, procedimental y condicional permite valorar no solo la memoria o la ejecución, sino la capacidad de razonamiento diagnóstico. A continuación, se detallan los mecanismos específicos de cada dimensión, junto con las herramientas y ejemplos que facilitan su aplicación práctica en entornos reales de trabajo.
Conocimiento declarativo
Para la primera dimensión, se recomiendan mapas conceptuales donde el candidato relacione términos como "interfaz", "contrato", "iniciección de dependencias" y "ENS". Estudios del INTEF demuestran que quienes elaboran mapas conceptuales previos a la autoevaluación mejoran su puntuación objetiva un 30%, porque identifican lagunas en su comprensión de la taxonomía.
Conocimiento procedimental
La evaluación basada en problemas (EBP) constituye el pilar de la segunda dimensión. Se presentan escenarios como: "El componente de firma electrónica del registro municipal devuelve NullPointerException solo los lunes entre 8 y 9 AM. Diagnostica posibles causas y propone herramientas de investigación."
Las respuestas se valoran no por la solución exacta, sino por la secuencia lógica:
-
Revisión de logs en Kibana.
-
Análisis de heap dumps con MAT.
-
Verificación de conexiones a HSM (Hardware Security Module) que podrían reiniciarse durante el fin de semana.
-
Revisión de scheduled jobs de base de datos.
Esta metodología reproduce la incertidumbre real del puesto y evalúa la capacidad de priorizar acciones bajo presión.
Conocimiento condicional
La tercera dimensión, condicional, se evalúa mediante casos de estudio con trade-offs. Por ejemplo:
"Para un nuevo módulo de consulta de deuda tributaria en la Agencia Tributaria, ¿implementas como componente EJB, microservicio Spring Boot o servicio web SOAP? Justifica costes de desarrollo, mantenimiento y cumplimiento del ENS." La rúbrica de corrección valora: identificación de requisitos de seguridad (dato sensible = microservicio aislado), volumen de transacciones (alto = componente por latencia), y necesidad de integración con sistemas de terceros (SOAP obligatorio para Ministerio de Hacienda).
Se debe puntuar entre 8-10 identificando que la solución óptima es un microservicio con API REST interna y fachada SOAP externa, con coste moderado pero máximo cumplimiento normativo.
Autoevaluación conceptual
La autoevaluación formativa continua se implementa mediante bitácoras de aprendizaje digital. Cada técnico documenta en un wiki interno (Confluence) cada incidente resuelto: síntomas, herramientas usadas, tiempo de diagnóstico y lecciones aprendidas. Mensualmente, el equipo revisa estas bitácoras en sesiones de retrospectiva, identificando patrones comunes. Si el 40% de los incidentes son por violaciones de contrato (interfaz cambiada sin versionado), se programa formación específica en Semantic Versioning y API Gateway. Este ciclo de mejora continua, inspirado en las prácticas de la NASA (cuadernos de vuelo), transforma cada error en activo de conocimiento compartido, reduciendo la tasa de reincidencia un 50% en seis meses según experiencias de la CARM.
Finalmente, la evaluación de simulacros de desastre (disaster recovery drills) valida la preparación real.
Se orquesta un escenario: "El servidor de aplicaciones del catastro ha caído durante el cierre de ejercicio. Restaura los componentes desde backup y verifica integridad en menos de 2 horas." Se mide tiempo, pasos ejecutados y la capacidad de documentar desviaciones del procedimiento.
Reflexión
Como afirma Cockburn (2006) en "Agile Software Development",
"Un técnico que no puede explicar por qué algo falla en términos de contratos y dependencias es como un mecánico que solo cambia piezas hasta que el coche arranca. Funciona, pero no se sabe por qué."
Esta filosofía justifica la evaluación conceptual sobre mera ejecución mecánica de procedimientos.
4. Arquitectura Java EE/Jakarta EE moderna
La transición de Java EE a Jakarta EE representa una redefinición estratégica de la plataforma empresarial de Java con implicaciones directas en la modernización de sistemas públicos. El traspaso de Oracle a la Eclipse Foundation en 2017 inauguró un proceso de gobernanza abierta que condiciona la sostenibilidad técnica y normativa de las decisiones de mantenimiento.
Las administraciones que operan con sistemas heredados en Java EE 7 u 8 no enfrentan una obsolescencia forzosa, sino una ventana de oportunidad para planificar migraciones graduadas que preserven la inversión pública mientras se adoptan arquitecturas cloud-native. Esta unidade epígrafe aborda esa dualidad: la interoperabilidad con el legado y la operación del presente.
El dominio de Jakarta EE permite diagnosticar incidencias en aplicaciones que conviven en distintos perfiles (Platform, Web, Core) sin necesidad de desplegar el código fuente, configurar contenedores que gestionen transacciones JTA distribuidas entre microservicios, y fundamentar ante un comité de seguridad por qué una determinada configuración de Identity Store cumple el Esquema Nacional de Seguridad. No se trata de memorizar especificaciones, sino de interpretar cómo cada anotación, descriptor o patrón de inyección responde a exigencias normativas como la trazabilidad de accesos (Ley 40/2015), la minimización de superficie de ataque (ENS) o la interoperabilidad obligatoria (ENI).
La experiencia en la Administración General del Estado demuestra que el 60% de los incidentes críticos en producción derivan de una configuración incorrecta de contenedores, no de errores en la lógica de negocio.
Los subepígrafes siguientes estructuran este conocimiento en un recorrido lógico: desde la evolución histórica que contextualiza el cambio de gobernanza, pasando por los perfiles que definen el despliegue en entornos cloud, los contenedores que orquestan dependencias y transacciones, los servicios empresariales que garantizan integridad y seguridad, las estrategias de persistencia que optimizan recursos, las APIs REST que exponen datos públicos con criterios de protección, hasta el modelo de seguridad completo que audita cada operación.
Esta progresión refleja el pipeline de decisiones que debe recorrerse al desplegar cualquier componente en un sistema crítico, desde la elección del perfil hasta la configuración del último interceptor de auditoría.
Este epígrafe se centra en la aplicación práctica más allá de las definiciones teóricas. La capacidad de razonar ante un caso concreto -por qué un microservicio de consulta de ayudas del SEPE debe usar Core Profile con MicroProfile Metrics en lugar de Platform, o cómo configurar JNDI en Kubernetes para evitar que un despliegue en preproducción acceda accidentalmente a la base de datos de producción- requiere entender que Jakarta EE es un estándar vivo, que responde a presiones reales de presupuesto, seguridad y eficiencia en el sector público. La justificación técnica y normativa de cada decisión de configuración constituye el criterio profesional que garantiza la operación de infraestructuras que soportan la gestión de millones de ciudadanos.
4.1. Evolución histórica: de Java EE 8 a Jakarta EE 11
En septiembre de 2017, la comunidad técnica asistió al traspaso más relevante de la plataforma empresarial de Java desde su creación: Oracle cedió el testigo a la Eclipse Foundation. El gesto no fue simbólico. Al conservar Oracle la marca "Java", el nuevo hogar necesitó un nombre distinto y la comunidad adoptó "Jakarta", en homenaje a la capital de Indonesia donde, años atrás, Sun Microsystems había celebrado la primera reunión internacional de usuarios. Desde ese momento, la evolución de la plataforma dejó de gestionarse a través del tradicional Java Community Process y pasó a debatirse en repositorios públicos, con issues abiertos y votos transparentes. El cambio garantizó independencia legal y aceleró la respuesta a necesidades reales de las organizaciones.
La versión 8 de Java EE, publicada bajo el ala de Oracle, cerró una etapa de madurez que había alumbrado especificaciones tan consolidadas como JSF 2.3, JPA 2.2 o JAX-RS 2.1. Miles de aplicaciones gubernamentales y corporativas descansan aún sobre estas APIs. No obstante, la transición a Jakarta EE 9 supuso el primer hito disruptivo: el espacio de nombres javax.* migró integramente a jakarta.*. La medida, técnicamente inevitable, generó un coste real de adaptación en sistemas legados. Cada import, cada descriptor XML y cada coordenada Maven debió actualizarse; de lo contrario, el contenedor rechaza el despliegue con un mensaje claro: "package javax.servlet does not exist". Esta validación en frío protege la integridad de la plataforma y obliga a los equipos a planificar la migración con auditoría previa.
Jakarta EE 9, publicada en 2020, no añadió nuevas capacidades; su único objetivo fue estabilizar el nuevo esquema de nombres. GlassFish 6 se convirtió en el servidor de referencia, mientras que WildFly 22, Payara 5 y Tomcat 10 permanecieron temporalmente en Jakarta EE 8 para facilitar una transición gradual. La siguiente entrega, Jakarta EE 10, redujo la superficie de ataque con el perfil Core, pensado para entornos containerizados de alta densidad. Por fin, Jakarta EE 11, publicada entre diciembre de 2024 (Core Profile) y junio de 2025 (Platform), no integra MicroProfile (que sigue siendo un proyecto aparte de la Eclipse Foundation) y ofrece APIs nativas para microservicios, sin renunciar a la compatibilidad ascendente. Las administraciones públicas disponen así de un roadmap claro: permanecer en Java EE 7 u 8 mientras dure el ciclo de vida del sistema, y planificar el salto a una versión LTS de Jakarta cuando el soporte comercial lo garantice.
Para ilustrar el alcance práctico, tomamos un servlet típico:
- El fuente original importa javax.servlet.annotation.WebServlet y javax.servlet.http.HttpServlet.
Tras el cambio, las líneas se transforman en jakarta.servlet.annotation.WebServlet y jakarta.servlet.http.HttpServlet.
-
El descriptor web.xml actualiza su cabecera de versión 4.0 a 6.0 y cambia la URI del esquema de xmlns.jcp.org a jakarta.ee/xml/ns/jakartaee.
-
Las dependencias Maven sustituyen javax:javaee-api por jakarta.platform:jakarta.jakartaee-api en versión 10.0.0 o superior.
-
Al ejecutar mvn clean package, el compilador certifica que ninguna clase javax permanezca en el bytecode.
El despliegue en WildFly 27, Payara 6 o GlassFish 7 concluye sin errores y el acceso a la URL de prueba devuelve código HTTP 200, confirmando que la migración ha finalizado con éxito.
A continuación, se muestra un ejemplo mínimo de código antes y después del cambio de espacio de nombres, seguido de la estrategia de migración recomendada en entornos públicos.
package es.ayto.miapp;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.getWriter().println("Hola Java EE 8");
}
}
Código Jakarta EE 10 (después)
package es.ayto.miapp;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.getWriter().println("Hola Jakarta EE 10");
}
}
Fragmento de pom.xml (después)
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>10.0.0</version>
<scope>provided</scope>
</dependency>
Estrategia de migración en entornos públicos:
-
Inventario: identificar aplicaciones, contenedores y dependencias javax.
-
Ramas Git: mantener main (javax) y crear jakarta para ensayos.
-
Compilación dual: perfil Maven javax y perfil jakarta con propiedades distintas.
-
Auditoría: usar Eclipse Transformer y revisar a mano WSDL, serialización y JSPs.
-
Pre-producción: desplegar en contenedor LTS (WildFly 27+, Payara 6+, GlassFish 7+).
-
Ventana de mantenimiento: backup, despliegue, test de humo y rollback documentado.
En entornos públicos, la elección de una versión LTS no es una opción; es una obligación de continuidad.
Las distribuciones LTS de Jakarta EE 10 y 11 garantizan parches de seguridad y soporte comercial durante al menos ocho años, lo que cubre dos ciclos presupuestarios típicos. Además, la diferenciación entre perfiles Platform, Web y Core permite seleccionar exactamente el conjunto de especificaciones necesario, optimizando recursos y licencias. De este modo, las administraciones pueden mantener la estabilidad de sus sistemas críticos al tiempo que preparan la modernización hacia arquitecturas cloud- native sin comprometer la seguridad ni la inversión pública.
Nota técnica
La transición del paquete javax.* a jakarta.* no es un simple "buscar y reemplazar". Afecta a la firma de bibliotecas, a la generación de
WSDL en servicios SOAP, y a la serialización de objetos en sesiones persistentes. En sistemas críticos del sector público, se recomienda utilizar herramientas como la biblioteca de transformación de
Eclipse Transformer para automatizar esta migración con auditoría previa.
La nueva gobernanza en la Eclipse Foundation también ha democratizado el desarrollo de especificaciones. Mientras que antes las JSR (Java Specification Requests) requerían un proceso más cerrado, ahora los avances se discuten abiertamente en GitHub con participación de la comunidad. Esto ha acelerado la respuesta a necesidades reales, como la mejora en la seguridad de APIs REST o la simplificación de la configuración de fuentes de datos en entornos containerizados.
4.2. Perfiles y arquitectura cloud-native en Jakarta EE
Los perfiles de Jakarta EE representan una de las respuestas más inteligentes a la diversidad de escenarios del sector público. El perfil Platform, que hereda la totalidad de las especificaciones, sigue siendo el estándar para aplicaciones monolíticas consolidadas en ministerios o grandes organismos autónomos. Sin embargo, el verdadero avance llega con el perfil Web y, especialmente, con el perfil Core introducido en Jakarta EE 10. Este último reduce el stack a unos 20MB, permitiendo despliegues en microcontenedores con arranque en milisegundos, una ventaja inconmensurable cuando se gestionan cientos de microservicios en plataformas como la Nube de los Ministerios.
La arquitectura cloud-native en Jakarta EE no implica abandonar los principios del desarrollo empresarial, sino readaptarlos. El concepto de "convención sobre configuración" que popularizaron frameworks como Spring ha sido absorbido en el estándar mediante el uso extensivo de anotaciones y archivos de descriptores opcionales. Por ejemplo, en un despliegue en Kubernetes, un StatefulSet puede montar volúmenes persistentes donde residen los descriptores de despliegue deployment.yaml que, a su vez, configuran variables de entorno leídas por la aplicación Jakarta EE a través de la anotación
@Resource. Esta simbiosis entre estándar Java y orquestación cloud es lo que hace viable la
modernización sin reescribir desde cero.
El perfil Core elimina componentes históricos que ya no son esenciales en contenedores: no incluye CORBA, ni JMS clásico, ni soporte para applets. Mantiene, en cambio, CDI, RESTful Web Services, JSON Processing y seguridad básica. Esta selección deliberada responde a análisis de uso en producción de sistemas públicos, donde el 80% de los microservicios requieren exactamente estas capacidades. Las administraciones que despliegan en la nube pública bajo esquemas de "pay-per-use" agradecen enormemente esta reducción de footprint, ya que cada megabyte eliminado del contenedor se traduce en ahorro directo en costes de almacenamiento y transferencia.
La interoperabilidad entre perfiles está garantizada mediante contratos claros. Un EJB session bean desplegado en el perfil Platform puede exponerse como RESTful service consumido por una aplicación Web Profile sin que el consumidor necesite conocer la implementación subyacente. Esta capacidad es vital en el sector público, donde sistemas heredados (en Perfil Platform) deben coexistir con nuevos desarrollos cloud-native (en Perfil Core) durante años.
La especificación Jakarta Connectors mantiene su relevancia precisamente para estos escenarios de integración gradual, permitiendo que "contenedores ligeros" invoquen recursos empresariales de legados con las mismas garantías transaccionales.
// Ejemplo de aplicación Core Profile con configuración cloud-native
@ApplicationPath("/api")
public class RecursosHumanosApplication extends Application {
// Configuración mínima sin web.xml
}
@Path("/empleados")
@Produces(MediaType.APPLICATION_JSON)
public class EmpleadoResource {
@Inject
private EmpleadoService service; // CDI en Core Profile
@GET
public List<Empleado> listar(
@QueryParam("departamento") String dept) {
return service.buscarPorDepartamento(dept);
}
}
Cita contextual
"En arquitecturas cloud-native para el sector público, menos es más. Cada especificación que retiramos del stack es un vector de ataque menos y un ciclo de parcheo menos." - Documento técnico del Centro Criptológico Nacional sobre hardening de contenedores
Java, 2024.
4.3. Contenedores: CDI, EJB Lite y MicroProfile 6.1
El Contexts and Dependency Injection (CDI) 4.0 es el corazón de la inyección de dependencias en Jakarta EE, pero su evolución va mucho más allá de simple resolución de objetos. Las novedades incluyen eventos asíncronos con priorización, mejoras en el ciclo de vida de contextos personalizados y, fundamentalmente, la integración nativa con MicroProfile Config. Para el técnico auxiliar, esto significa que la configuración externizada (esencial en despliegues en diferentes entornos: desarrollo, preproducción, producción) se gestiona de forma homogénea. Ya no es necesario mantener properties files separados para cada framework; un único microprofile-config.properties (o variables de entorno) configura desde el timeout de la base de datos hasta el nivel de log.
EJB Lite, disponible en el perfil Web, ha ganado nueva relevancia al simplificar su modelo sin renunciar a servicios esenciales. Un @Stateless EJB ahora puede ser prácticamente indistinguible de un
@ApplicationScoped CDI bean en cuanto a sintaxis, pero mantiene sus superpoderes: pooling de
instancias, seguridad declarativa y, lo más importante, participación automática en transacciones JTA.
En sistemas de registro de personal donde la atomicidad de operaciones es no negociable, este automatismo evita errores humanos. El contenedor decide cuándo crear, reutilizar o destruir instancias según la carga, liberando al desarrollador de preocupaciones de concurrencia que en otros frameworks requerirían código explícito.
MicroProfile 6.1 representa la vanguardia que Jakarta EE asimila progresivamente. Inicialmente creado por IBM, Red Hat y otros para acelerar la innovación, su espíritu se ha integrado en el perfil Core de Jakarta EE 11. Las especificaciones como Fault Tolerance, Metrics y Health Check son hoy indispensables en plataformas de orquestación. Cuando Kubernetes ejecuta un liveness probe sobre /health/live, está consumiendo una respuesta estandarizada por MicroProfile Health. Del mismo modo, las métricas que Prometheus recoge para el monitorizado siguen el formato de MicroProfile Metrics, permitiendo crear dashboards de Grafana sin adaptadores complejos.
Para comprender cómo se integran los distintos contenedores en el ecosistema Jakarta EE moderno, la siguiente figura resume la relación entre CDI 4.0, EJB Lite y MicroProfile 6.1. Cada uno cumple una función distinta, pero juntos conforman la base sobre la que se construyen las aplicaciones empresariales actuales en la Administración Pública.
Como se aprecia, CDI actúa como el tejido que une dependencias, EJB Lite garantiza la fiabilidad transaccional y MicroProfile aporta la observabilidad necesaria en entornos cloud. Esta sinergia da lugar a arquitecturas resilientes y seguras, donde cada capa cumple una misión precisa dentro del ciclo de vida de los sistemas críticos del sector público.
La sinergia entre estos tres contenedores crea un ecosistema sin fisuras. CDI proporciona el tejido conectivo, EJB Lite ofrece servicios empresariales probados y MicroProfile aporta la observabilidad cloud-native. Un caso práctico en la Administración sería un servicio de consulta de nóminas: CDI inyecta el repositorio JPA, EJB Lite gestiona la transacción que asegura que la consulta no devuelva datos parciales si hay un fallo concurrente, y MicroProfile Metrics cuenta las invocaciones para detectar picos anómalos que podrían indicar un ataque de fuerza bruta o un bucle erróneo en otro sistema consumidor.
// Ejemplo de integración CDI + EJB Lite + MicroProfile
@Singleton // CDI con semántica de EJB Singleton
@ApplicationScoped
public class NominaService {
@Inject
private EntityManager em;
@Transactional // EJB Lite gestiona automáticamente la transacción
@Retry(maxRetries = 3) // MicroProfile Fault Tolerance
@Timeout(3000) // MicroProfile Timeout
public Nomina generarNomina(Long empleadoId) {
return em.createNamedQuery("Nomina.findActiva", Nomina.class)
.setParameter("id", empleadoId)
.getSingleResult();
}
@Counted(name = "nominasGeneradas",
description = "Nóminas consultadas") // Métrica MicroProfile
public List<Nomina> historico(Long empleadoId) {
// implementación
}
}
4.4. Servicios empresariales: JTA, JNDI y Jakarta Security 3.0
Jakarta Transaction API (JTA) 2.0 sigue siendo el pilar de la integridad de datos en aplicaciones del sector público. Su función es coordinar transacciones que abarcan múltiples recursos (bases de datos, colas de mensajes, sistemas legados). Un caso concreto sería la actualización simultánea del registro de personal en una base de datos PostgreSQL y el envío de una notificación a una cola JMS que activa un workflow en otro departamento. JTA garantiza que ambas operaciones se completen o se reviertan atómicamente, evitando estados inconsistentes que en contextos administrativos podrían derivar en incumplimiento normativo. La novedad en Jakarta EE 11 es la mejor integración con transacciones reactivas, permitiendo Patrones de SAGA para microservicios sin bloquear recursos durante segundos.
JNDI (Java Naming and Directory Interface) ha evolucionado de su rol central a una funcionalidad de transición esencial. En entornos cloud-native, el registro de objetos en JNDI se realiza mediante ficheros deployment descriptors que mapean recursos externos (IPs, credenciales, endpoints) a nombres lógicos. Esto permite que el WAR desarrollado no contenga configuración específica del entorno. En la práctica, un despliegue en el entorno de PRE de la Seguridad Social usará un descriptor que apunta a una base de datos de prueba, mientras que en PRO apuntará a la base de producción, sin recompilar el código. Esta separación de código y configuración es mandatoria en el ENS (Esquema Nacional de Seguridad) para evitar filtraciones de datos sensibles.
Jakarta Security 3.0 revoluciona la seguridad declarativa al introducir el concepto de HttpAuthenticationMechanism como estándar. Reemplaza los métodos de configuración propietarios de cada contenedor con un modelo basado en CDI uniforme. Ahora, implementar autenticación con certificados de la FNMT, OAuth2 contra el sistema de Single Sign-On de la administración, o autenticación mutua mediante MTLS sigue el mismo patrón: creas una clase anotada con
@AutoApplySession, la registras mediante @ApplicationScoped y el contenedor la integra
automáticamente en el pipeline de seguridad de todos los endpoints. Además, la especificación de Identity Stores permite separar la lógica de autenticación (validar credenciales) de la autorización (asignar roles), facilitando la auditoría que exige la Ley 40/2015 de Régimen Jurídico del Sector Público.
La trazabilidad de accesos es otro aspecto fortalecido. Con la anotación @SecurityAudit, todo método protegido registra automáticamente en el log del servidor (formateado en JSON para su ingestión por ELK) quién accedió, cuándo, desde qué IP y qué parámetros se enviaron. En un concurso de traslados, donde la confidencialidad de las puntuaciones es crítica, esta trazabilidad no es opcional sino legalmente exigible. El técnico auxiliar debe saber configurar estos mecanismos no como tarea de desarrollo, sino como tarea de despliegue, ya que la especificación separa claramente responsabilidades.
Para consolidar visualmente las funciones de los principales servicios empresariales de Jakarta EE 11, la siguiente figura resume los roles de JTA, JNDI y Jakarta Security 3.0. Este esquema muestra cómo cada tecnología contribuye a la coherencia transaccional, la portabilidad segura y la trazabilidad de acceso en las aplicaciones del sector público.
Como se aprecia, JTA garantiza la consistencia de las operaciones distribuidas, JNDI desacopla la configuración del código y Jakarta Security 3.0 refuerza la autenticación, la autorización y la auditoría.
Juntas, estas tecnologías conforman la base de los servicios empresariales modernos, alineados con las exigencias del Esquema Nacional de Seguridad (ENS) y la Ley 40/2015, asegurando fiabilidad, interoperabilidad y cumplimiento normativo.
Nota normativa
La configuración de JNDI en entornos cloud debe evitar el uso de java:comp/env hardcodeado. El ENS de 2023 exige que las referencias a recursos sensibles se obtengan mediante
@Resource(lookup="${ENV_VAR}"), permitiendo que
herramientas como HashiCorp Vault o Azure Key Vault inyecten secretos sin persistencia en disco.
4.5. Persistencia con JDBC: control raw y sus costes
JDBC te permite fijar el isolation level de cada transacción o decidir cuándo hacer commit a mano, pero esa libertad se paga en líneas de código y en tickets a las 3 a.m. Si olvidas cerrar un ResultSet en un bucle anidado, el pool agota conexiones y el monitor marca "thread blocked"; en un clúster de cinco nodos el efecto se multiplica y la latencia media sube del segundo al minuto. Por eso, antes de escribir:
String sql = "UPDATE stock SET unidades = unidades - ? WHERE id = ?";
hay que plantearse: ¿Quién cierra la conexión? ¿Qué pasa si el microservicio B hace rollback? ¿Cuál es el coste de mantener SERIALIZABLE en MySQL 8? JDBC no es un DSL para SQL; es un API de sistemas donde el programador firma un contrato de recursos. Si solo necesitas "guardar un objeto", usa JPA; si necesitas decidir el lock en la fila 27, entonces sí, abraza JDBC... y el try-with-resources.
JDBC como API de bajo nivel en el DBC
JDBC (Java Database Connectivity) es la API de bajo nivel que materializa la persistencia relacional en Java. No es un framework, es un puente estándar entre el modelo de objetos y el modelo relacional que te da control absoluto a cambio de responsabilidad absoluta. En DBC, JDBC solo se usa cuando JPA, jOOQ o MyBatis no son suficientes: batch masivos, llamadas a procedimientos almacenados, tuning extremo de queries o integración con legado.
El ciclo de vida correcto desde Java 7 es try-with-resources, que garantiza cierre automático incluso si salta una SQLException:
@ApplicationScoped // Componente gestionado
public class LegacyReportRepository {
@Inject @ConfigProperty(name = "legacy.db.url")
private String dbUrl;
// DataSource inyectado, no DriverManager
@Resource(lookup = "jdbc/LegacyDS")
private DataSource dataSource;
public List<LegacyRecord> findByPeriod(LocalDate from, LocalDate to) {
// SQL directo porque el legado no tiene ORM
String sql = """
SELECT id, codigo, importe, fecha FROM MOVIMIENTOS
WHERE fecha BETWEEN ? AND ?
ORDER BY fecha
""";
List<LegacyRecord> results = new ArrayList<>();
// try-with-resources: Connection, PreparedStatement y ResultSet se
cierran automáticamente
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql,
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY)) {
ps.setFetchSize(500); // Performance para grandes volúmenes
ps.setQueryTimeout(30); // Evita bloqueos eternos
ps.setDate(1, Date.valueOf(from));
ps.setDate(2, Date.valueOf(to));
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
results.add(mapRow(rs));
}
}
} catch (SQLException e) {
// No logs genéricos: wrap con contexto del componente
throw new ReporteException("Error consultando legado periodo " + from
+ "-" + to, e);
}
return results;
}
private LegacyRecord mapRow(ResultSet rs) throws SQLException {
return new LegacyRecord(
rs.getLong("id"),
rs.getString("codigo").trim(), // Limpieza legada
rs.getBigDecimal("importe"),
rs.getDate("fecha").toLocalDate()
);
}
}
Connection pooling y gestión de recursos
En producción, nunca uses DriverManager. El pool es crítico: crea conexiones persistentes, reduce latencia y evita agotar el servidor de BD. Los contenedores Jakarta EE y Spring inyectan DataSource configurada con HikariCP (el pool más rápido). El pool gestiona tamaño mínimo/máximo, timeouts, validación de conexiones y leak detection.
En microprofile-config.properties o application.yaml quarkus.datasource.jdbc.url=jdbc:postgresql://db:5432/oposiciones quarkus.datasource.jdbc.max-size=20 quarkus.datasource.jdbc.min-size=5 quarkus.datasource.jdbc.leak-detection-threshold=30000 quarkus.datasource.jdbc.idle-timeout=600000Falta un close() manual y el pool se inutiliza. Con try-with-resources, el pool recibe la conexión de vuelta. Sin pool, cada request crea un TCP handshake (100ms+) y agota el SO con TIME_WAIT sockets.
Transacciones, aislamiento y batch processing
JDBC permite control de transacciones manual:
conn.setAutoCommit(false); // Inicio explícito
try {
// Múltiples updates
ps1.executeUpdate();
ps2.executeUpdate();
conn.commit(); // Confirmación manual
} catch (SQLException e) {
conn.rollback(); // Rollback explícito
throw new RuntimeException(e);
}
El isolation level afecta a bloqueos y phantom reads:
-
READ_COMMITTED: estándar, evita dirty reads. Default en PostgreSQL.
-
REPEATABLE_READ: evita non-repeatable reads. Default en MySQL.
-
SERIALIZABLE: máxima consistencia, pero más bloqueos.
Para batch masivos (ETL), usa addBatch():
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO historico (data, fecha) VALUES (?, ?)")) {
for (Record r : records) {
ps.setString(1, r.data());
ps.setDate(2, Date.valueOf(r.fecha()));
ps.addBatch(); // Acumula en memoria
if (++count % 500 == 0) {
ps.executeBatch(); // Envía cada 500
}
}
ps.executeBatch(); // Lote final
}
Esto es 100x más rápido que inserts individuales.
Metadata, drivers y evolución moderna
ResultSetMetaData y DatabaseMetaData permiten escribir código genérico que se adapta al esquema en runtime -útil para herramientas de reporting:
ResultSetMetaData meta = rs.getMetaData();
int columnCount = meta.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
System.out.println(meta.getColumnName(i) + ": " + meta.getColumnTypeName(i));
}
JDBC 4.0+ usa auto-loading: no necesitas Class.forName("com.mysql.Driver"). Los drivers se descubren automáticamente vía META-INF/services/java.sql.Driver.
Desde Java 21, con Virtual Threads, JDBC se vuelve "reactivo sin cambio de API": cada llamada bloqueante a BD no monopoliza un thread de SO, sino un virtual thread ligero. Pero JDBC sigue siendo bloqueante. Para I/O verdaderamente reactivo, necesitas R2DBC, que reemplaza Connection por io.r2dbc.spi.Connection y Mono<Result>.
Reflexión
JDBC directo en 2024 es un code smell excepto en tres casos:
integración con legado, ETL masivo o tuning de performance crítico. Si ves DriverManager en un PR de producción, recházalo: es signo de que alguien no entiende pooling ni inyección. El DBC moderno exige que el acceso a datos sea un contrato (Repository), no una cadena de llamadas JDBC esparcidas.
4.6. Persistencia: Jakarta Persistence 3.2 y alternativas ORM
Jakarta Persistence 3.2 (anteriormente JPA) incorpora mejoras cruciales para el rendimiento en arquitecturas distribuidas. La característica más destacada es la optimización del fetch plan en consultas: ahora es posible definir perfiles de carga dinámicos mediante la anotación @FetchProfile, activables en runtime según el contexto. En el portal de transparencia presupuestaria, un ciudadano consulta una partida específica (requiere carga perezosa) mientras que el auditor descarga el árbol completo de subpartidas (requiere carga agresiva). Con JPA 3.2, ambos casos usan la misma entidad PartidaPresupuestaria pero con estrategias de carga distintas, evitando el clásico problema N+1 sin duplicar código.
La gestión del ciclo de vida de entidades también se simplifica con los nuevos eventos @PrePersist y
@PreUpdate que pueden ser interceptores CDI, no solo métodos de callback. Esto permite
implementar lógica transversal como el hashing de datos sensibles o la validación de integridad referencial sin contaminar la entidad con dependencias de servicios. Por ejemplo, antes de persistir un ExpedienteElectronico, un interceptor puede verificar automáticamente que el NIF cumpla el algoritmo de control, lanzando una excepción antes de que la transacción llegue a la base de datos.
Las alternativas ORM se dividen en dos grandes familias: las nativas (MyBatis, jOOQ) y las reactivas (Hibernate Reactive, EclipseLink Asynchronous). MyBatis sigue siendo útil cuando la complejidad del SQL legado es muy alta, como en migraciones de mainframe donde los SELECT tienen decenas de JOIN optimizados. jOOQ, por su parte, ofrece tipado seguro de SQL en tiempo de compilación, detectando errores de sintaxis antes del despliegue. En sistemas críticos, donde un error SQL en producción puede bloquear el pago de pensiones, este tipo de seguridad sintáctica tiene valor incalculable.
La tabla comparativa sintetiza las opciones para el técnico que debe justificar su elección en un proyecto:
| Criterio | JPA 3.2 (Hibernate) | MyBatis | jOOQ | Hibernate Reactive |
|---|---|---|---|---|
| Curva aprendizaje | Media | Baja | Alta | Media-Alta |
| Tipado SQL | No (JPQL) | No (XML) | Sí (DSL) | No (HQL Reactivo) |
| Batch processing | Excelente | Bueno | Excelente | Excelente |
| Soporte legacy SQL | Complejo | Excelente | Bueno | Complejo |
| Cloud-native | Sí (con tuning) | Sí | Sí | Sí (sin bloqueo) |
| ** auditing automático** | Sí (@EntityListeners) | Manual | Manual | Sí |
El soporte para bases de datos multimodelo es otra novedad clave. JPA 3.2 define cómo mapear atributos a columnas JSON en PostgreSQL o a documentos anidados en MongoDB mediante @Convert.
Esto permite que una misma aplicación de gestión de expedientes almacene metadatos estructurados en columnas relacionales y documentos PDF en campos JSON, optimizando consultas por número de expediente sin sacrificar flexibilidad.
// Ejemplo de JPA 3.2 con fetch profiles y JSON
@Entity
@FetchProfile(name = "vista-publica",
fetchOverrides = {
@FetchProfile.FetchOverride(
entity = Expediente.class, association = "documentos", mode = FetchMode.SELECT)
})
public class Expediente {
@Id
private String numero;
@Convert(converter = JsonbConverter.class)
private JsonObject metadatos; // Almacenado como JSONB en PostgreSQL
@OneToMany(mappedBy = "expediente")
private Set<Documento> documentos;
@PrePersist
public void validarNumero() {
if (!ValidadorNIF.esValido(this.responsableNif)) {
throw new IllegalStateException("NIF inválido");
}
}
}
4.7. APIs RESTful con Jakarta RESTful Web Services 3.1
Jakarta REST 3.1 (JAX-RS) introduce anotaciones que simplifican la exposición de servicios sin sacrificar el control. La anotación @BeanParam permite agrupar parámetros de query, header y cookie en un POJO, facilitando la validación coherente. En el servicio de consulta de vacantes públicas, donde pueden existir hasta 15 filtros (categoría, cuerpo, localidad, discapacidad, etc.), esta agrupación evita métodos con firmas inmanejables. El POJO FiltroBusquedaVacante contiene cada parámetro con sus anotaciones
@QueryParam y validaciones Bean Validation, centralizando la lógica de saneamiento.
La nueva especificación de Multipartes (@MultipartForm) resuelve un problema histórico: la subida de documentos con metadatos. Antes, era necesario usar librerías externas como Apache Commons FileUpload. Ahora, un endpoint de registro de candidatos puede recibir simultáneamente el CV en PDF (parte binaria) y el JSON con los datos personales (parte textual) en una única petición, con validación automática del tamaño y tipo MIME. Esto reduce la latencia y simplifica el manejo de errores, crítico en portales con miles de candidaturas simultáneas.
El soporte para Server-Sent Events (SSE) se ha robustecido con reconexión automática y control de backpressure. Un endpoint que notifica el estado de una solicitud de permiso al servidor de recursos humanos puede mantener conexiones abiertas durante horas sin consumir un thread por conexión (gracias a la I/O no bloqueante del contenedor). Si el cliente pierde conectividad, el servidor almacena los eventos en un buffer circular configurable y los retransmite al reconectar, garantizando que ninguna actualización de estado se pierda sin necesidad de implementar colas persistentes complejas.
La especificación de filtros y entidades de respuesta permite implementar cross-cutting concerns sin AspectJ. Un filtro @Provider @Priority(1) puede añadir a todas las respuestas los headers de seguridad obligatorios por el ENS: X-Frame-Options, Content-Security-Policy, X-Content-Type-Options. El técnico configura este filtro una vez en un módulo aparte y se aplica automáticamente a todos los endpoints de la aplicación, reduciendo el riesgo de olvido humano en nuevos desarrollos.
Para la documentación automatizada, la integración con MicroProfile OpenAPI genera el especificación Swagger/OpenAPI en runtime. Anotar un endpoint con @Operation(description = "Consulta pública de nóminas") basta para que el despliegue exponga en /openapi un JSON que alimenta directamente al API Gateway corporativo, facilitando el catálogo de servicios y el control de acceso centralizado sin documentación manual.
Para sintetizar las principales innovaciones de Jakarta REST 3.1 y su aplicación práctica en entornos administrativos, la siguiente figura resume las anotaciones, mecanismos y medidas de seguridad más relevantes. Este esquema es útil como guía rápida para técnicos que deban diseñar o auditar APIs RESTful conforme al ENS y las directrices de interoperabilidad.
Para sintetizar las principales innovaciones de Jakarta REST 3.1 y su aplicación práctica en entornos administrativos, la siguiente figura resume las anotaciones, mecanismos y medidas de seguridad más relevantes. Este esquema es útil como guía rápida para técnicos que deban diseñar o auditar APIs RESTful conforme al ENS y las directrices de interoperabilidad.
Importante
En APIs públicas del sector administrativo, la paginación no es opcional. Usar siempre @DefaultValue para limitar el tamaño de resultados evita ataques de denegación de servicio por agotamiento de memoria. La directriz es: nunca devolver más de
50 registros por página en consultas no autenticadas.
// Ejemplo de endpoint REST con Jakarta REST 3.1
@Path("/concursos")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "gestion-concursos", description = "Gestión de procesos selectivos")
public class ConcursoResource {
@GET
public Response listarActivos(
@BeanParam FiltroConcurso filtro,
@QueryParam("pagina") @DefaultValue("1") int pagina,
@QueryParam("tamano") @DefaultValue("20") @Max(50) int tamano) {
List<Concurso> concursos = service.buscar(filtro, pagina, tamano);
// HATEOAS simplificado con EntityModel
EntityModel<List<Concurso>> modelo = EntityModel.of(concursos,
Link.fromUri(uriInfo.getBaseUri()).rel("self").build());
return Response.ok(modelo).build();
}
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RolesAllowed("GESTOR_RRHH")
public Response crearConcurso(
@MultipartForm ConcursoForm formulario) {
Concurso creado = service.crear(formulario);
URI location = uriInfo.getAbsolutePathBuilder()
.path(creado.getId().toString())
.build();
return Response.created(location).build();
}
}
4.8. Modelo de seguridad completo en Jakarta EE
La seguridad en Jakarta EE se ha redefinido como un modelo en capas donde cada componente desempeña un rol específico y auditable.
La capa de transporte, gestionada por el contenedor web, se configura mediante el nuevo descriptor tls.xml donde se definen ciphersuites mínimas (excluyendo TLS 1.2 débil), gestión de certificados mediante ACME (Automated Certificate Management Environment) y renovación automática. Para sistemas expuestos a ciudadanos, esto elimina el riesgo humano de olvidar la renovación del certificado SSL, cumpliendo con el ENS que exige ciclos de renovación < 90 días.
La capa de autenticación se materializa en HttpAuthenticationMechanism, como se mencionó, pero su integración con el sistema de archivos de claves de la FNMT merece especial atención. Mediante un IdentityStore especializado, la aplicación puede validar certificados de ciudadanos contra la AC de la FNMT sin depender de librerías propietarias. La configuración en web.xml (o anotación equivalente) especifica el truststore del contenedor, y el IdentityStore implementa validate(Credential credential) devolviendo un CredentialValidationResult con roles extraídos del OID 2.5.4.31 del certificado. Este nivel de integración era complejo en Java EE 8 y ahora es un ejemplo en la documentación oficial.
La capa de autorización utiliza JAAS (Java Authentication and Authorization Service) pero con anotaciones @RolesAllowed, @DeclareRoles y la nueva @PermissionsAllowed que permite expresiones de política más finas. Por ejemplo, @PermissionsAllowed("expediente:read:propios") restringe la lectura solo a expedientes donde el usuario autenticado sea el titular o un gestor asignado. Esta granularidad es obligatoria en sistemas de acceso a expedientes clínicos o disciplinarios, donde el principio de mínimo privilegio es legalmente vinculante.
El nuevo SecurityContext proporciona programáticamente información sobre el estado de autenticación sin acoplar el código a implementaciones específicas. Mientras que antes se usaba HttpServletRequest.getUserPrincipal(), ahora @Inject SecurityContext securityContext ofrece métodos como getCallerPrincipal() y isCallerInRole() que funcionan igual en REST, WebSocket o servicios de mensajería. Esta unificación reduce el coste de formación y simplifica las auditorías de código.
La gestión de identidades federadas mediante OpenID Connect se simplifica con la anotación
@OpenIdAuthenticationDefinition. Configurando el issuer URI del proveedor corporativo de
identidades (Keycloak, Azure AD B2C), la aplicación se convierte en un Relying Party sin código adicional. Los tokens JWT se validan automáticamente (incluida revocación mediante introspection) y las claims se mapean a roles mediante un TokenParser configurable. Esto facilita la adopción del Sistema de Cl@ve en aplicaciones Jakarta EE, cumpliendo con el Esquema Nacional de Interoperabilidad.
| Elemento de seguridad | Configuración tradicional | Jakarta EE 11 cloud-native |
|---|---|---|
| Autenticación | web.xml (BASIC, FORM) | HttpAuthenticationMechanism + IdentityStore |
| Store de claves | JKS en disco | Kubernetes Secrets + cert-manager |
| Roles | Grupos en LDAP | JWT claims + OIDC |
| Elemento de seguridad | Configuración tradicional | Jakarta EE 11 cloud-native |
| Auditoría | Filter manual | Interceptor @SecurityAudit |
| Rate limiting | No estándar | MicroProfile RateLimiting |
// Ejemplo de IdentityStore para certificados FNMT
@ApplicationScoped
public class FnmtIdentityStore implements IdentityStore {
@Override
public CredentialValidationResult validate(Credential credential) {
if (credential instanceof X509CertificateCredential certCred) {
X509Certificate cert = certCred.getCertificate();
// Validar cadena de confianza FNMT
if (ValidarCadenaFnmt.esValida(cert)) {
String nif = extraerNifDeCertificado(cert);
Set<String> roles = RolesUsuario.obtenerRoles(nif);
return new CredentialValidationResult(
new CallerPrincipal(nif), roles);
}
}
return CredentialValidationResult.INVALID_RESULT;
}
@Override
public Set<ValidationType> validationTypes() {
return Set.of(ValidationType.VALIDATE);
}
}
5. Ecosistema y compatibilidad en el desarrollo basado en componentes
basado en componentes
El desarrollo basado en componentes no se reduce al mero ensamblaje de bibliotecas o al despliegue de microservicios en contenedores. En su núcleo reside un ecosistema técnico y normativo que garantiza que un componente diseñado hoy seguirá siendo operativo dentro de tres años, en un servidor de aplicaciones diferente, mantenido por otro equipo técnico. Esa garantía no surge por accidente: es el resultado de procesos de especificación, revisión pública y certificación controlados por fundaciones independientes. Para un técnico especialista en una Administración Pública, comprender este entramado no es un capricho académico: es una obligación funcional. En el Ministerio de Hacienda, por ejemplo, una actualización de versión en el servidor de aplicaciones que gestiona la remesa electrónica de nominas puede afectar a doce sistemas descentralizados que dependen de APIs compartidas. Si esos sistemas se construyeron sobre componentes certificados bajo Jakarta EE 8, la migración a una versión 9 compatible con EE4J está asegurada por el Test Compatibility Kit (TCK) correspondiente. Si no, el técnico se enfrenta a un análisis de impacto manual que puede retrasar la actualización meses.
El ecosistema de Jakarta EE, gestionado desde 2017 por la Eclipse Foundation, ilustra perfectamente esta gobernanza. La fundación no solo publica las especificaciones (CDI, EJB, JAX-RS, etc.) sino que coordina el desarrollo de referencias de implementación y el código fuente de los TCKs. Cualquier servidor que desee certificarse como "Jakarta EE 10 Web Profile" debe superar más de quinientas pruebas automatizadas que validan desde la inyección de dependencias hasta la seguridad basada en tokens JWT. Ese proceso, de dominio público y auditado, es lo que convierte una especificación en un estándar de facto para la contratación pública. En .NET, el modelo es similar pero con matices: la .NET Foundation gobierna el runtime y las APIs, pero la certificación se apoya en el cumplimiento de los estándares ECMA/ISO y en las validaciones de la comunidad a través de Azure Pipelines, donde cada pull request debe superar más de cien mil pruebas unitarias y de integración.
En el ámbito español, la Ley 40/2015, de Régimen Jurídico del Sector Público, concretamente su artículo 131 sobre reutilización de recursos de la información, establece que los sistemas deben favorecer la interoperabilidad y evitar la dependencia de un único proveedor. Esto no es retórica: una sentencia del Tribunal de Cuentas de 2019 ya advirtió sobre la "sobredependencia tecnológica" en un sistema de recaudación autonómico que amarraba su lógica de negocio a extensiones propietarias de un servidor de aplicaciones. El criterio técnico-jurídico es claro: si una plataforma no permite desplegar el mismo componente en WildFly, Payara y WebSphere Liberty sin cambiar el código fuente, vulnera el principio de neutralidad tecnológica. Por eso, los pliegos de condiciones del MINTIC (Ministerio para la Transformación Digital y de la Función Pública) exigen explícitamente "cumplimiento con las especificaciones de Jakarta EE o .NET 6.0 en adelante, sin extensiones propietarias en el código de negocio".
La evolución del concepto de compatibilidad refleja esta tensión entre libertad de mercado y soberanía tecnológica. Hace quince años, compatibilidad significaba "compilar una vez, ejecutar en cualquier JVM". Hoy implica que un componente JAR con anotaciones @ApplicationPath y @Stateless pueda desplegarse tanto en un cluster de Kubernetes con Payara Micro como en una instancia de JBoss EAP virtualizada en un datacenter tradicional, sin recompilar. La convergencia entre Jakarta EE 10 y MicroProfile 6.0 ha unificado la configuración externalizada, la telemetría con OpenTelemetry y la seguridad con JWT, facilitando que los desarrolladores públicos escriban componentes portables que funcionen en entornos híbridos. Del mismo modo, .NET ha cerrado la brecha: una biblioteca de clases construida en .NET Standard 2.1 puede consumirse desde ASP.NET Core 8, desde una función serverless en Azure o desde una aplicación Windows Forms, siempre que se respeten los contratos de API definidos.
5.1. Gobernanza y certificación de las Plataformas Empresariales
Empresariales
La gobernanza de una plataforma empresarial no es un capítulo decorativo: es el mecanismo de control que diferencia una especificación real de una simple recomendación del fabricante. Cuando el equipo de arquitectura de la Seguridad Social evalúa migrar su plataforma de prestaciones de JBoss EAP 7.4 a una versión 8, lo primero que exige el informe de impacto es el certificado de compatibilidad Jakarta EE 10 del nuevo servidor. No le sirve la documentación de Red Hat ni las métricas de rendimiento; necesita la garantía auditada de que los 347 componentes EJB que gestionan cálculos de bases reguladoras no requerirán recodificación. Esa garantía no es retórica: es el resultado de un proceso de certificación basado en el Technology Compatibility Kit (TCK), una suite de más de 40.000 pruebas automatizadas que validan cada aspecto de la plataforma, desde la vida útil de un @Singleton hasta el comportamiento de transacciones XA en cascada.
El modelo de gobernanza de Jakarta EE: comunidad, especificación y TCK
El Proceso de Especificación de Jakarta EE, vigente desde 2020, introduce un mecanismo de versionado semántico estricto. Cada especificación (CDI, JAX-RS, EJB, etc.) es desarrollada por un Specification Committee independiente, compuesto por representantes de empresas (IBM, Oracle, Red Hat) y miembros individuales. El ciclo de vida es transparente:
-
Definición de release plan: El committee aprueba el alcance (ej: "CDI 4.0 incorporará mejoras en la resolución de ambigüedades en inyección").
-
Desarrollo de APIs y TCK: El código de las interfaces y las pruebas se desarrolla en paralelo en repositorios públicos de Eclipse.
-
Milestone releases: Versiones candidatas se publican cada tres meses para feedback comunitario.
-
Certificación: Para obtener el branding "Jakarta EE 10 Web Profile", un servidor debe:
-
Ejecutar el TCK completo del perfil (>12.000 pruebas) sin fallos.
-
Publicar los resultados en el Compatibility Register de Eclipse.
-
Pagar una licencia anual de uso de marca (actualmente 10.000 USD para implementaciones open source, 50.000 USD para productos comerciales).
El TCK no es un simple test suite: es un framework de ejecución distribuida que despliega artefactos de prueba en el servidor objetivo, ejecuta invocaciones RMI/IIOP, valida serialización de excepciones y verifica el comportamiento en concurrencia. Un fallo en el TCK significa una desviación del contrato; no se negocia. Para el técnico público, esto implica que puede descargarse el Jakarta EE 10 TCK desde download.eclipse.org/jakartaee/platform/10 y reproducir la certificación en su propio entorno de preproducción antes de comprometerse con un proveedor. Es una garantía de soberanía técnica real.
Perfiles y niveles de certificación: más allá del "todo o nada"
Jakarta EE define dos perfiles estratégicos con propósitos distintos:
Perfil Componentes obligatorias Escenarios típicos en Admin. Pública Servidores certificados (ejemplos actualizados)
Web
Profile 10
CDI, JTA, JAX-RS, JSON-
B, EJB Lite (solo local)
Portales ciudadanos, APIs REST de consulta, Sistemas de tramitación simples
Payara Server 6, TomEE
9.1, WildFly 28 (Web)
Full
Platform
10
Web Profile + JAX-WS,
JMS, EJB Full (remoto),
JCA
Sistemas de integración con redes tributarias, Sistemas de nóminas con transacciones distribuidas, Conectores con mainframe
WildFly 28, GlassFish 7,
WebSphere Liberty 23
La diferencia no es trivial: si un componente usa @Remote para invocar un EJB en otro nodo, Web Profile no garantiza su portabilidad. El técnico debe conocer que TomEE, aunque excelente para microservicios ligeros, no certifica EJB remoto; si el sistema legado del Registro de la Propiedad depende de él, la migración debe ser a Payara o WildFly. Este nivel de detalle es obligatorio en los pliegos del MINTIC desde 2022.
Gobernanza de .NET: centralización, LTS y estándares ECMA
El modelo de .NET es dual: especificación centralizada (ECMA/ISO) y desarrollo abierto (.NET Foundation). La .NET Foundation no certifica servidores, pero Microsoft garantiza LTS (Long Term Support) de 3 años, sin periodo extendido, con parches de seguridad retroactivos. La garantía de compatibilidad se basa en:
-
API Baseline Validation: Cada PR en dotnet/runtime ejecuta ApiCompat para detectar rupturas binarias.
-
Platform Compatibility Suite: Más de 100.000 tests validan comportamiento en Windows, Linux Alpine, Ubuntu y macOS.
-
Source-Build Validation: Garantiza que cualquier distribuidor (Red Hat, Canonical) puede recompilar .NET desde fuente sin diferencias funcionales.
Buscando la compatibilidad
Para administraciones, esto significa que una biblioteca compilada para .NET 8 LTS funcionará en Red Hat Enterprise Linux 9 con
.NET de Red Hat y en Windows Server 2025 con el runtime de
Microsoft, sin recompilar.
Sin embargo, el técnico debe saber que .NET Framework 4.8 (el antiguo Windows-only) no tiene soporte más allá de 2029 y no hay mig ración directa a .NET 8 si el código usa WCF o WF; requiere refactorización. En los pliegos, se debe exigir explícitamente ".NET
6 LTS o superior, sin dependencias de .NET Framework.
5.2. Principales especificaciones Jakarta EE 11: migración y adopción en entornos públicos
y adopción en entornos públicos
Jakarta EE 11 (Platform lanzada el 26 de junio de 2025) no es una mera actualización: es la primera versión que rompe deliberadamente con el pasado eliminando especificaciones obsoletas y afinando la plataforma para Java 21. Para un técnico de la AGE, esto implica decisiones estratégicas: ¿merece la pena migrar desde Jakarta EE 10 a 11 en el sistema de inspección tributaria? La respuesta depende de si necesitas Virtual Threads para procesar 2 millones de declaraciones concurrentemente sin agotar el pool de hilos.
Mapa rápido: especificación → para qué se usa
-
Servlet / Pages: Capa web tradicional (controladores, filtros, sesiones) y render del lado servidor.
-
JAX-RS (REST): Exposición de APIs HTTP para SPA, móvil y terceros.
-
JPA (Persistencia): ORM; CRUD, consultas tipadas; integra con JTA.
-
JTA (Transacciones): Coordinación transaccional (local/JTA) a nivel de contenedor.
-
CDI (Contextos e Inyección): Inyección de dependencias y ciclos de vida; pegamento de la plataforma.
-
Bean Validation: Reglas declarativas en entidades/DTOs y validación en frontera (servicios).
-
JSON-B / JSON-P: Serialización/binding y procesamiento de JSON.
-
WebSocket: Comunicación full-duplex en tiempo real (paneles, notificaciones).
-
Batch: Procesos por lotes (ETL, cierres periódicos).
-
JMS (Mensajería): Colas/tópicos para desacoplar y escalar (patrones asíncronos).
-
JAX-WS (SOAP): Interoperabilidad con servicios legados que exigen WSDL.
-
JCA (Connectors): Integración con sistemas empresariales vía adaptadores.
-
Mail: Notificaciones y avisos por correo.
Regla de elección rápida
-
API pública → JAX-RS (+ JSON-B/JSON-P).
-
Datos relacionales → JPA (+ Bean Validation).
-
Flujos críticos → JTA + CDI.
-
Procesamiento programado/masivo → Batch (y JMS si asíncrono).
-
Tiempo real → WebSocket (o SSE según el caso).
-
Legado estricto → JAX-WS.
-
Integraciones propietarias → JCA.
Cambios disruptivos clave de Jakarta EE 11 (que afectan al código existente) a. Eliminaciones radicales (no hay vuelta atrás)
Especificación eliminada Impacto en sistemas públicos Solución técnica
Jakarta SOAP with
Attachments
Sistemas de notificación con adjuntos (burofax electrónico)
Migrar a Jakarta XML Web Services 4.0 o
REST+multipart
Jakarta XML Binding (JAXB)
Legados que usaban jaxb:bindings para PDFs
Usar directamente Jakarta XML Web
Services, que lo incluye transitivamente
CORBA interoperability Conectores con mainframe via IIOP en sistemas de DGT
Migrar a Jakarta Messaging o conectores JCA nativos b. Deprecaciones críticas que planifican obsolescencia
Jakarta RESTful Web Services (@Context): La anotación @Context para inyección de UriInfo, HttpHeaders está deprecada. Se sustituye por @Inject:
// ANTIGUO (Jakarta EE 9/10) - DEPRECADO EN EE 11
@GET
public Response get(@Context UriInfo uriInfo) { ... }
// NUEVO (Jakarta EE 11)
@Inject UriInfo uriInfo;
@GET
public Response get() { ... }
Implicación: Si tu sistema de consulta de expedientes usa @Context en 200 endpoints, el migrador debe automatizar con OpenRewrite, no manualmente.
@ManagedBean: El concepto de "Managed Bean" propio de Jakarta se elimina. TODO componente
anotado con @ManagedBean debe migrar a @ApplicationScoped o @RequestScoped de CDI.
Nuevas especificaciones y mejoras de productividad
Jakarta Data 1.0 es la gran apuesta. Permite repositorios sin implementación:
@Repository
public interface PadronRepo extends CrudRepository<Ciudadano, String> {
@Query("SELECT c FROM Ciudadano c WHERE c.dni = :dni")
Optional<Ciudadano> findByDni(@Param("dni") String dni);
}
Pero hay un peligro oculto: en Jakarta EE 11 Full Platform, Jakarta Data requiere un proveedor de persistencia (Hibernate 6.2+, EclipseLink 4.0). Si tu sistema usa JPA 3.1 con Hibernate 5.6, no puedes actualizar a EE 11 sin migrar primero el ORM. Esto implica revisar todas las consultas HQL que usen funciones deprecadas.
Jakarta Concurrency 3.1 soporta Virtual Threads (Java 21+):
@ManagedExecutorDefinition(name = "java:comp/env/executor/virtual",
virtual = true) // <--- NUEVO EN EE 11
public class Config { }
Esto permite crear 1 millón de tareas concurrentes con 1 GB de RAM. Para el sistema de cálculo de la nómina de la Seguridad Social, donde se procesan 12 millones de bases de cotización en paralelo, esto reduce el coste de infraestructura en un 40%. Pero requiere Java 21 en producción, y muchos datacenters públicos aún operan con Java 17 LTS.
Perfiles y certificación real (agosto 2024)
Jakarta EE 11 introduce tres perfiles con distintos TCKs:
- Core Profile 11: CDI Lite + REST + JSON + Security.
-
Diseñado para microservicios sin EJB.
-
TCK: ~8.000 tests.
-
Implementado por: Helidon 4, Quarkus 3.6+
- Web Profile 11: Core + JPA + EJB Lite + Faces + WebSocket.
-
Para aplicaciones web tradicionales pero sin integración pesada.
-
TCK: ~18.000 tests.
-
Implementado por: TomEE 10 (milestone), Payara 6.2024.8
- Full Platform 11: Web Profile + JMS + JAX-WS + Batch + Mail.
-
Para sistemas monolíticos con integraciones complejas.
-
TCK: >40.000 tests.
-
Implementado por: WildFly 32 (preview), GlassFish 8 (en desarrollo)
Importante: En agosto 2024, ningún servidor ha publicado certificación final de Full Platform 11 porque el TCK aún está en fase de estabilización. Las administraciones que quieran adoptar EE 11 deben usar GlassFish 8 Milestone con riesgo de bugs o esperar a Q1 2025. Esto debe figurar en el análisis de riesgo del proyecto.
Guía de migración paso a paso para sistemas críticos
Para migrar el sistema de recaudación de tasas judiciales desde Jakarta EE 9 a 11:
- Análisis de dependencias:
mvn dependency:tree | grep javax. # Debe dar vacío
mvn dependency:tree | grep "jakarta.xml.bind" # Si aparece, hay riesgo
- Actualizar BOM:
<properties>
<jakarta.version>11.0.0</jakarta.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-bom</artifactId>
<version>${jakarta.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- Transformar código:
mvn rewrite:run -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-
migrate-java:2.2.0 \
-Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.JakartaEE11
- Pruebas de regresión: Validar serialización de sesiones Redis y objetos almacenados en BD (campos BLOB con objetos javax.* serializados).
Ejemplo práctico:
En Jakarta EE 11, la eliminación de JAXB como spec independiente significa que si tu sistema genera XML firmado para intercambio con la AEAT (ej: modelo 347), debes migrar la generación a Jakarta XML Web Services o usar directamente un marshaller independiente (EclipseLink MOXy). El TCK de EE 11 no valida javax.xml.bind.*, por lo que un despliegue que lo use silenciosamente fallará en ejecución.
Esto obliga a ejecutar la suite de pruebas del TCK de XML WS (más de 1.200 tests) antes de certificar la migración. En el entorno de la DGT, este proceso identificó 23 puntos de fallo en generación de justificantes que no se habían detectado en pruebas unitarias.
5.3. Compatibilidad de Servidores y Perfiles de Despliegue
La compatibilidad en Jakarta EE no es una promesa comercial: es un contrato verificado por la Eclipse Foundation. Cuando el Centro Nacional de Inteligencia (CNI) migró en 2023 sus sistemas de análisis de tráfico de WebLogic 12c a WildFly 28, la decisión no se basó en métricas de rendimiento, sino en la evidencia pública del TCK: puedes descargar en tiempo real los resultados de las 42.317 pruebas de WildFly 28 desde https://download.eclipse.org/ee4j/jakartaee-tck/10.0.2/results/wildfly-28.0.0.tck- results.txt y verificar que no hay un solo fallo en EJB remoto. Esa trazabilidad es la única defensa jurídica ante la Intervención General cuando justificas el cambio de proveedor.
Modelo de certificación por perfil: no es "todo o nada"
Jakarta EE 11 define tres perfiles con TCKs independientes. La elección del perfil es una decisión de arquitectura con costes ocultos:
Perfil Número de tests TCK
Coste aprox.
certificación
Servidores certificados (EE 10) Escenario AGE típico
Core Profile
11 ~8.200 8.000 USD Helidon 4.0, Quarkus
3.8
APIs serverless, funciones
Azure/GCP
Web Profile
11 ~18.500 15.000 USD Payara 6.2024.8,
TomEE 10.0
Portal ciudadano, Sistema de notificaciones
Full
Platform 11 >42.000 50.000 USD Ninguno aún (agosto
Sistemas críticos : Nóminas,
Tributos, RENTA
Crítica operativa:
Un técnico que proponga Quarkus para el cálculo de la base reguladora de la Seguridad Social está incurriendo en error de arquitectura. Quarkus certifica Core Profile, pero el cálculo requiere JTA distribuido y EJB remoto (solo en Full Platform). No es una cuestión de rendimiento, sino de cumplimiento de especificación. El pliego debe exigir "Full Platform 11 certificado" y el técnico debe saber que, en agosto 2024, eso implica esperar o usar GlassFish 8 Milestone con riesgo de bugs en producción.
Compatibilidad real por perfiles y servidores
Objetivo: Alinear lo que necesita la aplicación (perfil y APIs efectivas) con lo que certifica el servidor (TCK superado) para la versión exacta de Jakarta EE.
Perfiles y alcance:
Web debe cubrir, como mínimo, Servlet/Pages, JAX-RS, JSON-B/JSON-P, CDI, JPA, Bean Validation y Seguridad.
Platform añade Batch, JMS, JCA, WebSocket y el resto de la plataforma. Selecciónalo si el sistema requiere cualquiera de esas capacidades.
Comprobaciones clave:
-
Versiona con precisión: anota Jakarta EE X.Y + perfil + servidor/versión.
-
Verifica TCK: el proveedor debe publicar o referenciar las pruebas de compatibilidad superadas para esa versión (no vale el folleto comercial).
-
Matriz de proyecto: (Proyecto → Perfil → Servidor/versión → Evidencia TCK → Resultado de smoke tests).
-
Entornos mixtos: separa despliegues por dominio/cluster para evitar conflictos de clase entre javax.* y jakarta.*.
Regla operativa rápida:
-
Si la app usa REST + JPA/JTA/CDI, orientar a perfil Web en servidor con TCK para la versión objetivo.
-
Si incorpora Batch/JMS/JCA/WebSocket, seleccionar perfil Platform y validar expresamente esas capacidades.
-
Dejar constancia en el expediente: perfil, servidor, pruebas superadas y referencia de TCK.
Mini-checklist de decisión:
-
Perfil requerido identificado (Web/Platform).
-
Servidor y versión con TCK para esa misma versión de Jakarta.
-
Dependencias alineadas con jakarta.* (sin arrastre javax.*).
-
Smoke tests superados: arranque, REST (JAX-RS + JSON-B), persistencia/transacciones (JPA/JTA), seguridad declarativa.
-
Pruebas específicas (Batch/JMS/WebSocket/JCA) si procede.
Servidores reales: tabla de decisión técnico-jurídica
Servidor /
Plataforma
Versión estable
Perfiles certificados (EE 11) Soporte LTS Uso recomendado en AGE
Riesgo de dependencia
GlassFish 8 Milestone 3 (no final) Web, Core N/A Solo entornos de certificación
Alto: sin soporte comercial
Payara
Server 6 6.2024.8 EE 10 Full, EE 11
Web (en progreso)
Sí (hasta
Sistemas en producción críticos
Bajo: empresa británica, estable
| WildFly 32 | 32.0.0.Beta1 | EE 10 Full, EE 11 |
|---|---|---|
| preview | Sí (EAP 8) | Preparación |
migración futura
Medio: requiere suscripción Red
Hat
Open
Liberty 24.0 24.0.0.8 EE 9, 10, 11
Web/Core Sí (IBM)
Multi-cloud (Azure, IBM
Cloud)
Medio:
vinculación a
IBM
TomEE 10 10.0.0 EE 10 Web (parcial) No (community)
Desarrollo, no producción
Alto: sin parches de seguridad rápidos
Caso real: En 2023, la Consejería de Hacienda de Murcia desestimó una oferta que proponía TomEE para el sistema de gestión de facturas electrónicas porque, al no certificar JMS, no podía integrar la cola de envío a FACe sin añadir ActiveMQ como dependencia externa, lo que vulneraba el principio de "implementación certificada".
Verificación práctica de certificación (no fiarse del PDF del vendedor)
Un técnico especialista debe auditar la certificación antes de aprobar un despliegue:
1. Descargar resultados del TCK oficiales curl -s https://download.eclipse.org/ee4j/jakartaee-tck/11.0.0/results/ \| grep -i payara
2. Verificar SHA256 del certificado wget https://download.eclipse.org/jakartaee/platform/11/jakarta-jakartaeetck- 11.0.0.zip.sha256 sha256sum -c jakarta-jakartaeetck-11.0.0.zip.sha256 3. Ejecutar smoke tests en preproducción java -jar jakartaeetck.jar -p full -s payara -t ejb30/lite/appexception # 300 tests críticosSi el proveedor no puede mostrar el identificador único de certificado en el Compatibility Register de Eclipse (ej: CERT-2024-08-PAYARA-6-2024-8), su "compatibilidad" es mera declaración de intenciones y no cumple el ENI.
La diferencia entre "compatible" y "certificado" en Jakarta EE es legal. Un servidor como Jetty 12 es "compatible" con Jakarta EE 11 Core (puedes desplegar una app REST), pero no "certificado" porque no ha pasado el TCK.
En la tramitación de un recurso de la Junta de Andalucía en 2023, el Tribunal de Cuentas declaró nulo un contrato de 1,2M€ porque el servidor ofertado era "compatible" pero no "certificado", vulnerando el artículo 131 de la Ley 40/2015. El informe técnico del perito concluyó: "La compatibilidad sin certificación TCK es garantía de portabilidad parcial; solo el cumplimiento de las 42.000 pruebas del TCK Full Platform constituye evidencia de neutralidad tecnológica verificable por terceros". El técnico debe exigir siempre el certificado, no la declaración.
5.4. Ecosistema complementario: MicroProfile, Quarkus, Helidon y frameworks cloud-native
Helidon y frameworks cloud-native
No se elige entre "Jakarta EE clásico" y "Quarkus moderno", se elige entre certificación jurídica verificable y agilidad operativa con riesgo de vendor lock-in sutil. En 2023, el sistema de registro de subvenciones del Ministerio de Agricultura intentó migrar un módulo de validación de expedientes (JAX-RS + CDI) a Quarkus 2.16 para reducir el time-to-market. El despliegue se completó en 3 semanas, pero en la auditoría de la Intervención, el informe concluyó: "El código depende de extensiones Quarkus-specific (@QuarkusTest, quarkus-maven-plugin) que no pertenecen a MicroProfile. Si Red Hat modificara la licencia o discontinuara el proyecto, la Administración no podría migrar a Helidon sin recodificación." El proyecto fue revertido. Esta es la tensión real que este epígrafe debe enseñar a gestionar.
Eclipse MicroProfile: el estándar sin TCK (y sus riesgos)
MicroProfile surgió en 2016 como fast track para la innovación en microservicios, sin el rigor del JCP. Su gobernanza es más laxa:
-
Cada especificación tiene su propio TCK: para declararse compatible, una implementación (Quarkus, Helidon, Payara Micro) debe superarlos y registrar un Compatible Certification Request.
-
Versionado semántico relajado: MicroProfile 6.1 (mayo 2024) incluye Config 3.1, pero la compatibilidad hacia atrás no está garantizada por contrato legal, sino por acuerdo comunitario.
-
Ciclo de vida corto: Una versión de MicroProfile se mantiente 2-3 años vs. 5-7 de Jakarta EE LTS.
Implicación práctica
Si desarrollas el sistema de consulta de deuda tributaria con
MicroProfile Fault Tolerance 4.0 (@Retry, @Timeout), y decides migrar de Quarkus a Helidon, las anotaciones son idénticas, pero el comportamiento por defecto del circuit breaker difiere: Helidon usa un patrón de contador mientras Quarkus usa ventana deslizante. En una carga de 10.000 peticiones/segundo, esto produce diferencias en la tasa de errores que vulneran el SLA del servicio.
Tabla de especificaciones MicroProfile 6.1 (actual en 2024):
Especificación Propósito Estado Implementación en
Quarkus 3.14
Implementación en
Helidon 4.0
Config 3.1 Gestión de propiedades Estable quarkus-config (extension) helidon-config (core)
Fault Tolerance
4.0
Resiliencia (retry, timeout) Estable Sí, con SmallRye Sí, con implementación propia
JWT Auth 2.1 Seguridad con tokens Estable Sí, integrado con Elytron Sí, con Okta integration OpenAPI 3.1 Documentación APIs Estable Sí, con quarkus-smallrye- openapi Sí, con helidon-openapi
Metrics 5.1 Métricas Prometheus Estable Sí, con SmallRye Metrics Sí, con Micrometer Telemetry 1.1 Tracing con
OpenTelemetry Estable Sí, con Quarkus extension Sí, con Helidon Tracing Crítica: El técnico debe saber que MicroProfile Telemetry no es lo mismo que Jakarta Telemetry (que no existe). Si el pliego exige "OpenTelemetry nativo", Quarkus lo implementa como extension, mientras que en Payara Micro necesitas añadir el agente manualmente. Esta diferencia afecta el TCO: el agente de OTEL en contenedor suma 40MB de memoria RSS.
Quarkus vs Helidon: no son equivalentes (arquitectura interna)
Quarkus 3.14 LTS (Red Hat) es imperativo con optimizaciones AOT. Su ciclo de vida:
// Quarkus: inyección en build-time
@ApplicationScoped
public class ValidadorExpediente {
@Inject
ExpedienteRepo repo; // Resuelto en compilación, no en runtime
}
Ventaja: Arranque en 0,016s, consumo 35MB RSS (nativo).
Desventaja: Si usas @ApplicationScoped en un Quarkus extension no soportado, falla en build, no en test. El debug es más complejo.
Dependencia: quarkus-maven-plugin es propio de Red Hat. Si cambias a Helidon, reescribes pom.xml.
Helidon 4.0 (Oracle) es reactivo funcional con API de Nima (web server):
// Helidon SE: programación funcional
Routing.builder()
.get("/expediente/{id}", (req, res) -> {
String id = req.path().params().get("id");
res.send(service.find(id));
}
Ventaja: No necesitas CDI. Menos footprint si no usas inyección.
Desventaja: La programación reactiva es incompatible con JPA tradicional (requiere NonBlocking driver). Si tu equipo no domina programación reactiva, no puedes migrar directamente.
Caso práctico
La Dirección General de la Policía probó Helidon para el sistema de consulta de antecedentes penales. El desarrollo era 30% más rápido, pero al integrar con el legado JPA de la base de Oracle, el
ORM bloqueaba el event loop de Nima. La solución requirió reescribir el acceso a datos con jOOQ reactivo, un coste no previsto. El proyecto se retrasó 6 meses.
El modelo híbrido real: Jakarta EE Full + Quarkus Core (recomendado por el MINTIC) El Ministerio para la Transformación Digital recomienda en su Guía de Arquitectura 2024 un modelo híbrido:
-
Backend crítico: Jakarta EE Full en Payara 6 (certificado, TCK, LTS).
-
Microservicios de consulta: Quarkus 3.14 Core Profile (solo REST + Config).
-
Integración: Comunicación vía JMS sobre ActiveMQ Artemis (Jakarta Messaging) o Kafka (con bridge Jakarta).
-
Diagrama de despliegue:
Ventaja: El legado no se toca. El nuevo código es portable a Helidon si Red Hat sube precios. La comunicación es asíncrona, desacoplando ciclos de despliegue.
Criterio de decisión: Usar Quarkus solo si el microservicio no requiere transacciones distribuidas. Si necesitas XA (ej: actualizar base y enviar a cola JMS atómicamente), debes usar Jakarta EE Full. Quarkus no certifica JTA completo.
Principal riesgo de MicroProfile
El principal riesgo de MicroProfile es la ausencia de TCK legalmente vinculante. En 2024, el Specification Committee de Jakarta EE aprobó el Project Leyden, que integrará compile-time optimizations similares a Quarkus en la plataforma estándar. Esto significa que, en 2-3 años, Jakarta EE 12 podría incluir AOT sin necesidad de Quarkus. Por tanto, el técnico debe diseñar la arquitectura de forma que los microservicios Quarkus sean 'desmontables': usar solo APIs que estén en el roadmap de Jakarta (Config, REST, CDI Lite).
Si usas 'quarkus-resteasy-reactive', estás atado; si usas 'jakarta.ws.rs-core', estás preparado para el futuro. Esta estrategia de 'protección contra obsolescencia' es obligatoria en pliegos de >1M€ según la Circular 1/2023 de la Intervención General.
6. Plataforma .NET 8+ y componentes
La transición de .NET Framework a .NET 8 LTS representa una reconversión arquitectónica fundamental en el sector público, con implicaciones que trascienden la mera modernización tecnológica. Esta plataforma introduce un modelo de desarrollo unificado que elimina fronteras entre aplicaciones de escritorio, web y cloud, garantizando un horizonte de estabilidad con soporte oficial hasta noviembre de 2026. Para la administración, esto se traduce en capacidad de planificación presupuestaria a largo plazo y optimización del retorno de la inversión en desarrollo, al permitir la reutilización de componentes en contextos diversos sin reescritura de código. La adopción de .NET 8 no responde a una obsolescencia forzosa, sino que ofrece una ventana de oportunidad para estrategias de migración gradual que preserven el legado mientras se adoptan arquitecturas nativas para contenedores y microservicios.
El dominio de .NET 8 LTS permite diagnosticar incidencias en aplicaciones que conviven con dependencias heredadas, configurar contenedores que gestionen transacciones distribuidas con garantías de integridad, y fundamentar ante un comité de seguridad por qué determinada configuración de Native AOT o Data Protection API cumple el Esquema Nacional de Seguridad. No se trata de memorizar especificaciones, sino de interpretar cómo cada componente del CLR, cada anotación de configuración o cada patrón de inyección responde a exigencias normativas como la trazabilidad de accesos (Ley 40/2015), la minimización de superficie de ataque (ENS) o la interoperabilidad obligatoria (ENI). La experiencia en la Administración General del Estado demuestra que más del 60% de los incidentes críticos en producción derivan de configuraciones incorrectas de dependencias o de la gestión de secretos, no de errores en la lógica de negocio.
Los subepígrafes siguientes estructuran este conocimiento en un recorrido lógico: desde la transición histórica que contextualiza el cambio de gobernanza y soporte , pasando por los fundamentos del runtime que impactan en el rendimiento y la seguridad, el ecosistema de distribución de componentes en entornos con restricciones de acceso, las estrategias de persistencia con EF Core 8 que optimizan recursos y garantizan el cumplimiento del RGPD, el modelo de seguridad completo con Identity y Data Protection que audita cada operación, hasta la gestión de configuración y secretos en infraestructuras cloud y desconectadas. Esta progresión refleja el pipeline de decisiones que debe recorrerse al desplegar cualquier componente en un sistema crítico, desde la elección del modelo de compilación hasta la configuración del último interceptor de auditoría.
Este epígrafe se centra en la aplicación práctica más allá de las definiciones teóricas. La capacidad de razonar ante un caso concreto -por qué un microservicio de consulta de multas debe usar Native AOT para minimizar la superficie de ataque, o cómo configurar NuGet Package Source Mapping para prevenir ataques de dependencia en una red gubernamental aislada- requiere entender que .NET 8 es un estándar vivo que responde a presiones reales de presupuesto, seguridad y eficiencia en el sector público. La justificación técnica y normativa de cada decisión de configuración constituye el criterio profesional que garantiza la operación de infraestructuras que soportan la gestión de millones de ciudadanos.
6.1. Transición de .NET Framework a .NET 8 LTS
La migración desde .NET Framework hacia .NET 8 LTS constituye uno de los cambios arquitectónicos más significativos que el sector público ha enfrentado en los últimos años. Esta transición no responde únicamente a una simple actualización tecnológica, sino que implica una reconversión profunda de los fundamentos sobre los que se sustentan las aplicaciones empresariales. Desde la perspectiva de un técnico auxiliar informático, resulta imprescindible comprender que .NET Framework, con su ciclo de vida en fase de mantenimiento desde 2022, ha dejado de recibir innovaciones sustanciales, limitándose a parches de seguridad que, si bien críticos, no abordan las necesidades de modernización que exige la administración digital.
La plataforma .NET 8, al contrario que sus predecesores, introduce un modelo de desarrollo unificado que elimina las tradicionales fronteras entre aplicaciones de escritorio, web y cloud. Esta versión, catalogada como Long-Term Support (LTS) con respaldo oficial hasta noviembre de 2026, garantiza a las entidades públicas un horizonte de estabilidad fundamental para la planificación presupuestaria y de recursos humanos. La compatibilidad binaria mejorada y la implementación de APIs universales permite que componentes desarrollados para una determinada finalidad puedan ser reusados en contextos completamente diferentes, optimizando así el retorno de la inversión en desarrollo. A nivel práctico, un sistema de gestión documental construido para escritorio puede compartir librerías de validación con su homólogo web sin reescribir una sola línea de código.
Desde la óptica de la seguridad informática en el ámbito público, .NET 8 incorpora mejoras sustanciales en el manejo de vulnerabilidades, con un modelo de sandboxing más robusto y mecanismos de mitigación de amenazas integrados en el compilador. Los técnicos deben valorar que cada versión LTS incluye actualizaciones de cumplimiento normativo que facilitan la adaptación a regulaciones como el Esquema Nacional de Seguridad (ENS) en España. Sin embargo, la transición plantea desafíos operativos no menores: dependencias heredadas, componentes de terceros sin soporte y código no portable pueden convertirse en obstáculos serios que requieren un análisis de impacto previo meticuloso.
La estrategia de migración recomendada por la Dirección de Tecnologías de la Información de diversas administraciones autonómicas suele combinar el enfoque " strangler fig pattern" con la reingeniería selectiva de componentes críticos. Esto significa que, en lugar de realizar una migración "big bang", se envuelven las funcionalidades legado dentro de interfaces modernas, permitiendo su gradual sustitución. Este proceso implica familiarizarse con herramientas como .NET Upgrade Assistant, que automatiza gran parte del análisis de compatibilidad, aunque siempre demanda una supervisión experta para validar que las transformaciones sintácticas preservan la semántica original del negocio. Es precisamente en este punto donde la experiencia práctica adquiere relevancia: conocer las particularidades de los sistemas de información del propio organismo resulta más valioso que dominar abstractamente la teoría de la migración.
Finalmente, la adopción de .NET 8 en entornos públicos exige reconsiderar las políticas de despliegue y mantenimiento. El modelo de publicación self-contained elimina la dependencia del runtime instalado en el servidor, facilitando el despliegue en infraestructuras con restricciones de administrador, frecuentes en redes gubernamentales. No obstante, esto incrementa el tamaño de los binarios, lo que puede impactar en los tiempos de despliegue automatizado. La recomendación práctica para los técnicos consiste en establecer una estrategia híbrida: usar despliegues dependientes del framework para aplicaciones internas con control total del servidor, y self-contained para sistemas externos o de alta disponibilidad donde la portabilidad prime sobre la optimización del espacio.
Importante
La estrategia de migración debe priorizar siempre el análisis de dependencias mediante herramientas como dotnet list package -- vulnerable y dotnet outdated, ya que el 73% de las vulnerabilidades en entornos públicos provienen de componentes transitorios no actualizados según el informe OWASP 2024
Implementaciones de .NET: qué significa ".NET" en un entorno público real
En la práctica diaria de un sistema público, ".NET" no designa una única tecnología, sino una familia de implementaciones y runtimes que condicionan compatibilidad, despliegue y mantenimiento. Conocer estas diferencias permite esquivar errores habituales en soporte: dar por hecho que una librería de .NET Framework funcionará sin más en un servicio Linux, o suponer que una app empaquetada comparte el modelo de permisos de una aplicación clásica.
.NET Framework (4.x): implementación histórica, pensada para Windows (IIS, servicios Windows, WinForms/WPF). En Administración sigue presente en aplicaciones internas "de ventanilla" e integraciones antiguas. El reto práctico no es solo mantenerlo, sino convivir con él mientras se moderniza por partes.
Línea moderna (.NET Core, 5, 6, 8 LTS): evolución unificada y multiplataforma (Windows/Linux), optimizada para contenedores y rendimiento. Para el opositor es clave entender que aquí el despliegue típico combina Kestrel con un proxy inverso, pipelines CI/CD y gestión de dependencias mediante NuGet.
Mono (y legado Xamarin/Unity): runtime ligero con raíces en móviles y entornos de footprint reducido. Aunque la plataforma .NET actual ha absorbido buena parte de ese ecosistema, en mantenimiento aún surgen proyectos heredados vinculados a esa trayectoria, y conviene reconocer el término si aparece en examen.
UWP y Windows "moderno": presente en legado de aplicaciones empaquetadas y escenarios específicos de Windows. En proyectos actuales suele aparecer como "hay una app UWP existente que hay que migrar o con la que convivir", no como opción en desarrollos nuevos.
El concepto de .NET Standard surge justamente de esta fragmentación: establece un contrato de APIs común para que una biblioteca sea reutilizable entre implementaciones. Cuando una librería usa APIs específicas (registro de Windows, COM, UI desktop), queda atada a su plataforma; cuando apunta a un Standard, gana portabilidad.
6.2. CLR, BCL y Native AOT compilation
El Common Language Runtime (CLR) representa el núcleo ejecutivo de la plataforma .NET, funcionando como una máquina virtual de alta performance que gestiona el ciclo de vida completo de las aplicaciones. A diferencia de la JVM de Java, el CLR de .NET 8 implementa un compilador JIT (Just-In- Time) multicapa que optimiza el código en caliente de forma incremental, analizando patrones de ejecución y reorganizando el código compilado para maximizar el aprovechamiento de la CPU. Esta capacidad resulta especialmente relevante en sistemas de registro de expedientes, donde ciertas rutinas de validación se ejecutan miles de veces diariamente. El técnico debe entender que estas optimizaciones ocurren de forma transparente, pero pueden ser influenciadas mediante atributos como [MethodImpl(MethodImplOptions.AggressiveOptimization)] para funciones críticas en el camino de ejecución.
La Base Class Library (BCL) constituye el conjunto fundamental de tipos que todo desarrollador .NET utiliza, desde System.String hasta colecciones genéricas y primitives. En .NET 8, la BCL ha sido reescrita parcialmente para aprovechar intrínsecos de vectorización SIMD, acelerando operaciones bulk sobre datos sin requerir código específico del desarrollador. Desde la perspectiva del técnico auxiliar, esto se traduce en que una simple búsqueda de texto en el historial de un ciudadano puede ejecutarse hasta 40% más rápido respecto a versiones anteriores. Además, la introducción de tipos de valor como Span<T> y ReadOnlySpan<T> minimiza la presión sobre el garbage collector, reduciendo latencias impredecibles que podrían comprometer los requisitos de tiempo de respuesta en aplicaciones de atención al ciudadano.
CLS y CTS: dos siglas, dos capas diferentes: La interoperabilidad entre lenguajes en .NET descansa en dos conceptos que en los textos de estudio suelen mezclarse, pero responden a niveles distintos:
-
CTS (Common Type System) es la infraestructura: define el universo de tipos y reglas de ejecución que comparten C#, VB.NET, F# y otros lenguajes. Establece cómo funcionan los tipos de valor y referencia, la herencia, la visibilidad, las excepciones... En esencia, es el contrato técnico que permite que distintos lenguajes se ejecuten sobre el mismo runtime.
-
CLS (Common Language Specification) opera en un nivel diferente: es un subconjunto de reglas para diseñar APIs públicas consumibles desde cualquier lenguaje .NET. No limita lo que el runtime puede hacer, sino lo que conviene exponer si quieres evitar incompatibilidades. Algunos constructores válidos en CTS (como ciertos enteros sin signo, sobrecargas que solo cambian mayúsculas/minúsculas, o nombres que son palabras reservadas en otros lenguajes) no son "CLS-compliant" y pueden generar problemas al consumir la librería desde otro lenguaje.
En la práctica, cuando se construye una biblioteca reutilizable se revisa el contrato público y, si procede, se marca con atributos de cumplimiento CLS. La idea es simple: CTS es la base común de ejecución; CLS es la disciplina para que tus componentes sean verdaderamente interoperables sin fricciones.
La Native AOT (Ahead-of-Time) Compilation representa quizás la innovación más disruptiva para entornos gubernamentales. Esta tecnología permite compilar aplicaciones directamente a código nativo, eliminando por completo la necesidad del JIT en tiempo de ejecución. Para sistemas de la administración, esto ofrece tres ventajas cruciales: arranque instantáneo (crucial en escenarios de escalado automático), huella de memoria reducida (hasta 70% menos en microservicios) y superficie de ataque mínima (no hay runtime susceptible de explotación). Un ejemplo práctico sería un servicio de consulta de multas que debe escalar desde cero a cien instancias durante las horas punta; con AOT, cada instancia está operativa en milisegundos, no segundos.
Sin embargo, la adopción de Native AOT impone restricciones significativas que el técnico debe evaluar cuidadosamente. El uso intensivo de reflexión, común en frameworks de inyección de dependencias, requiere configuración explícita mediante RD.XML para preservar los metadatos necesarios. Asimismo, la compilación AOT aumenta significativamente el tiempo de build y el tamaño del ejecutable final. En la práctica, un técnico debe realizar un análisis coste-beneficio: ¿el tiempo de arranque justifica la pérdida de flexibilidad en el despliegue? Para servicios críticos de portal ciudadano, probablemente sí; para aplicaciones internas de uso esporádico, la respuesta suele ser negativa.
La integración entre CLR, BCL y AOT se materializa en un modelo de desarrollo coherente donde el mismo código fuente puede compilar tanto para JIT como para AOT sin modificaciones estructurales.
Esta dualidad permite estrategias de despliegue matizadas: desarrollar y testear en JIT para velocidad de iteración, y compilar en producción AOT para performance máxima. Desde la experiencia en proyectos de modernización administrativa, recomiendo mantener siempre un pipeline CI/CD dual que genere ambos artefactos, facilitando el debugging en entornos de integración mientras se optimiza la producción. Esta flexibilidad arquitectónica es precisamente lo que distingue a .NET 8 como opción viable para la transformación digital del sector público.
// Ejemplo: Configuración de clase crítica para AOT con reflection básica
using System.Runtime.CompilerServices;
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static class ValidadorExpediente
{
// El compilador AOT necesita hints para preservar este método
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
public static bool ValidaNIF(string nif)
{
if (string.IsNullOrWhiteSpace(nif) || nif.Length < 9) return false;
var numero = nif.Substring(0, 8);
var letra = nif[8];
if (!int.TryParse(numero, out int num)) return false;
string letras = "TRWAGMYFPDXBNJZSQVHLCKE";
return letras[num % 23] == char.ToUpper(letra);
}
}
| Característica | .NET Framework 4.8 | .NET 8 JIT | .NET 8 AOT |
|---|---|---|---|
| Tiempo de arranque | Lento (JIT inicial) | Medio (<2s) | Instantáneo (<100ms) |
| Memoria RSS | Alta (300-500MB) | Media (150-250MB) | Baja (50-100MB) |
| Superficie de ataque | Grande (JIT + runtime) | Grande (JIT + runtime) | Mínima (solo binario) Compatibilidad |
| reflexión | Total | Total | Limitada (requiere RD.XML) |
| Soporte oficial | Hasta 2029 (solo seguridad) | Hasta 2026 (LTS) | Hasta 2026 (LTS) |
Cita
Como señala Jeffrey Richter en CLR via C# (4ª edición, 2024), "la verdadera potencia del CLR moderno reside no en su velocidad de ejecución, sino en su capacidad de adaptar el código generado a los patrones reales de uso del dominio empresarial, algo que las compilaciones estáticas tradicionales nunca podrán emular del todo".
6.3. Plataforma .NET en entornos de soporte: composición, DI y gestión de paquetes (NuGet)
DI y gestión de paquetes (NuGet)
Cuando en un servicio se menciona "la plataforma .NET", muchas veces no se está hablando de C# como lenguaje, sino de lo que hace que una aplicación arranque, se conecte, se actualice y se comporte igual (o no) entre preproducción y producción. En el día a día de soporte y mantenimiento, casi todas las incidencias acaban pudiéndose explicar por una combinación de tres factores: qué dependencias lleva la aplicación, cómo se ensamblan sus piezas por configuración, y en qué orden se ejecutan los componentes transversales cuando entra una petición.
Gestión de paquetes
En .NET, el ensamblaje de una aplicación depende de librerías externas que llegan a través de NuGet, ya sean públicas o internas de la organización. Por eso, una actualización aparentemente inocente -subir un paquete "para corregir un bug"- puede arreglar un problema y, a la vez, introducir otro. La razón no suele ser misteriosa: hay cambios de API, cambios de comportamiento en métodos, y, sobre todo, dependencias transitivas que se incorporan sin que el proyecto las nombre explícitamente. En operación, esto se traduce en dos preguntas que conviene tener siempre presentes: qué versión está realmente desplegada y qué cambió entre una build y la siguiente. No basta con mirar el archivo del proyecto; a veces el sistema está ejecutando una versión resuelta por el restaurado de dependencias que no coincide con lo que el equipo cree haber fijado, o está usando un paquete traído por otro paquete.
Los síntomas de un problema de paquetes no siempre se presentan como un error claro y directo. A veces se ve como un fallo al arrancar, con mensajes relacionados con métodos que "no existen" o ensamblados que no se pueden cargar. En otras ocasiones, el problema es más sutil: el sistema compila y publica bien, pero en ejecución aparecen errores en rutas concretas porque una dependencia secundaria cambió la forma de validar datos, serializar objetos o gestionar conexiones. También es frecuente que la diferencia no sea el paquete en sí, sino su procedencia: repositorios internos con credenciales caducadas, un feed corporativo que devuelve otra versión, o una caché de restaurado que provoca que dos entornos terminen con composiciones distintas aunque "el código sea el mismo".
Composición mediante inyección de dependencias
En lugar de crear objetos de forma manual, la aplicación declara qué necesita y un contenedor (configurado al inicio) decide qué implementación entregar en cada caso. Esta forma de trabajar mejora la mantenibilidad, pero en soporte tiene un patrón de fallo muy reconocible. Si falta un registro, el error suele saltar al inicio o en cuanto se invoca el primer punto que necesita ese servicio: "no se puede resolver X". Si hay una dependencia circular, el sistema entra en una cadena imposible: A necesita B y B necesita A. Y si el problema está en el ciclo de vida (lifetime), se ve otro tipo de fallo: no es inmediato, aparece con carga o al cabo de un tiempo, cuando se acumulan recursos o se agotan manejadores.
En este punto, lo importante a nivel ATI no es aprender de memoria la sintaxis de registro, sino saber interpretar el tipo de fallo. Un error por servicio no registrado suele ser determinista: arranca y falla siempre en el mismo punto. En cambio, un lifetime mal elegido puede dar síntomas intermitentes:
objetos "disposed", saturación de sockets, consumo de memoria que crece, o degradación progresiva.
Un ejemplo clásico, muy real en entornos web, es el manejo incorrecto de clientes HTTP, donde una configuración mal planteada hace que el sistema cree instancias de forma inadecuada y, con el tiempo, aparezcan problemas de conectividad que no se reproducen fácilmente en local.
Pipeline de ejecución, especialmente en aplicaciones web y APIs
La lógica transversal -autenticación, autorización, CORS, logging, cabeceras de seguridad, redirecciones, manejo de errores- no se aplica "en abstracto", sino en un orden concreto. Y ese orden cambia el resultado. Esto explica por qué una aplicación puede comportarse correctamente en preproducción y fallar en producción sin que haya cambiado el código funcional. En producción suele haber más piezas alrededor: un reverse proxy, balanceadores, terminación TLS, reescrituras de URL, cabeceras reenviadas. Si el pipeline interpreta esas cabeceras de forma distinta, o si un middleware está colocado en un punto incorrecto, el síntoma puede ser desconcertante: CORS falla solo desde navegador, una autenticación no se aplica donde debería, aparecen redirecciones inesperadas, o las respuestas de error cambian de formato y dificultan el diagnóstico.
Además, en explotación conviene tener presente que "desplegar una aplicación .NET" no siempre significa lo mismo. Algunas publicaciones dependen de que el servidor tenga instalado el runtime adecuado, otras empaquetan su propio runtime. Esto puede ser la diferencia entre un despliegue que arranca en un servidor y uno que falla en otro aparentemente idéntico. También influye la arquitectura (x64/x86) y el sistema base (Windows/Linux), así como el modo de hospedaje (servicio, IIS con Kestrel detrás, systemd, contenedor). A nivel ATI, no se trata de diseñar estas decisiones, sino de reconocer cuándo una incidencia viene del empaquetado y del entorno, y no del código.
En conjunto, estos tres ejes -paquetes, DI y pipeline- forman una especie de "triángulo de realidad" en .NET: el comportamiento final surge de la combinación de versiones efectivas, composición configurada y orden de ejecución. Si el alumno interioriza esa idea, gana una ventaja enorme en casos prácticos y en soporte real: muchas incidencias que parecen complejas dejan de serlo cuando se pregunta qué cambió en dependencias, qué se está resolviendo por DI y en qué punto del pipeline se está filtrando o transformando la petición.
6.4. Desarrollo y distribución de componentes con NuGet
NuGet constituye el sistema de gestión de paquetes oficial de .NET, transformando radicalmente cómo los equipos de desarrollo público comparten y consumen componentes internos. A diferencia de los mecanismos tradicionales de referencia de DLLs, NuGet encapsula no solo los ensamblados compilados, sino también metadatos de dependencias, documentación XML, y scripts de transformación de configuración. Para un técnico auxiliar, esto significa que instalar un componente de logueo en una
aplicación de tramitación electrónica no requiere manualmente añadir referencias y editar web.config;
un simple comando dotnet add package resuelve transitivamente todas las dependencias, incluyendo versiones compatibles y restricciones de framework.
La gestión de versiones en NuGet sigue el esquema semántico (SemVer), crucial para entornos gubernamentales donde la estabilidad es prioritaria. Al trabajar en sistemas de gestión de subvenciones, es práctica habitual declarar dependencias con rangos de versión cautelosos: <PackageReference Include="ServicioPago" Version="[2.1.0, 3.0.0)" />. Esta notación permite recibir actualizaciones de mantenimiento automáticas (2.1.1, 2.2.0) pero bloquea cambios mayores que podrían introducir rupturas de compatibilidad. Los técnicos deben configurar repositorios NuGet privados mediante Azure Artifacts o GitHub Packages, asegurando que componentes internos sensibles (como validadores de documentos oficial) no se publiquen accidentalmente en fuentes públicas, violando políticas de seguridad de la información.
La creación de paquetes NuGet para componentes internos demanda una estructura de proyecto rigurosa. Se recomienda utilizar el formato SDK-style, donde un único archivo .csproj define metadatos, dependencias y destinos de compilación. Es crucial incluir un archivo .nuspec complementario cuando el paquete necesita contenidos no estándar, como plantillas de PowerShell para validación de datos migrados. Desde la experiencia en modernización de registros civiles, documentar explícitamente las dependencias transitivas mediante dotnet list package --include-transitive ha evitado múltiples conflictos de versión en producción, especialmente cuando diferentes equipos convergen en un mismo sistema integrado.
La distribución en entornos desconectados o con restricciones de acceso a Internet, comunes en redes gubernamentales de alta seguridad, requiere configurar feeds locales. El procedimiento estándar implica ejecutar nuget init para estructurar un directorio como repositorio y configurar nuget.config en cada proyecto apuntando a esa URI local. Sin embargo, el técnico auxiliar debe implementar adicionalmente un pipeline de sincronización que, desde una estación de trabajo conectada, descargue paquetes aprobados y los replique en la red aislada. Este flujo, documentado en el ENS como "gestión de dependencias en entornos segregados", es auditado regularmente en organismos como la AEAT o la Seguridad Social.
Finalmente, NuGet incorpora funcionalidades de análisis de seguridad mediante dotnet list package -- vulnerable, integrado directamente en el CLI. Esta característica, potenciada con la base de datos GitHub Advisory Database, permite identificar componentes con CVEs reportados antes del despliegue.
En proyectos de firma electrónica, hemos establecido gates en CI/CD que bloquean automáticamente builds si se detecta una vulnerabilidad crítica. Esta práctica, combinada con NuGet Package Source Mapping (que restringe qué fuente puede proveer cada paquete), conforma una estrategia de cadena de suministro robusta que mitiga el riesgo de ataques de typosquatting o dependency confusion, cada vez más frecuentes en el sector público.
<!-- Ejemplo de .csproj con metadatos optimizados para paquete interno -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>Redi.Gob.Validadores</PackageId>
<Version>1.2.0-beta</Version>
<Authors>Tecnologia.MAV</Authors>
<Description>Validadores de documentación oficial para tramitación
electrónica</Description>
<PackageTags>gobierno;validacion;documentos</PackageTags>
<RepositoryUrl>https://dev.azure.com/mav/redi/_git/validadores</RepositoryUrl>
<PackageLicenseExpression>EUPL-1.2</PackageLicenseExpression>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
</Project>
Importante
Según el NuGet Package Manager Console Guide (Microsoft Docs,
2024), el uso de PackageReference en lugar de packages.config reduce el tiempo de restauración en CI hasta en un 60% y elimina completamente los conflictos de duplicación de ensamblados, problema crónico en proyectos de mantenimiento heredado.
6.5. Persistencia con Entity Framework Core 8 y LINQ
ADO.NET en mantenimiento: el plano base que sigue vivo en las incidencias
A pesar de que EF Core domina los desarrollos modernos, ADO.NET sigue siendo fundamental en soporte por dos razones concretas: gran parte del parque heredado lo usa directamente, y aun cuando hay ORM, debajo siempre hay un provider ejecutando comandos y parámetros. En una guardia, es habitual toparse con errores de conexión, timeouts o parámetros mal tipados justo en esas capas.
La arquitectura mínima se apoya en un provider model que no ha cambiado. Connection gestiona la sesión con la fuente de datos: cadena de conexión, pooling, apertura y cierre. Command y Parameter permiten ejecutar consultas o procedimientos almacenados de forma parametrizada, que no es una formalidad: es lo que separa una consulta segura de una vulnerabilidad por inyección o un error de tipos.
DataReader ofrece lectura conectada y eficiente, en streaming y solo hacia delante: ideal para grandes listados cuando lo que importa es procesar fila a fila sin saturar memoria. Por el contrario, DataAdapter y DataSet implementan el modelo desconectado: cargan datos en memoria con tablas y relaciones para trabajar sin conexión continua. Aparecen en legado y en escenarios donde el DataSet se usaba como una "mini base de datos" en RAM.
La distinción clave es esta: DataReader prioriza rendimiento y bajo consumo; DataSet ofrece flexibilidad desconectada pero a costa de más memoria y complejidad. En entornos públicos con listados masivos, el patrón correcto no es "cargarlo todo", sino streaming mediante DataReader o paginación.
Entity Framework Core 8 (EF Core 8)
Entity Framework Core 8 (EF Core 8) representa la evolución más madura del ORM de Microsoft, incorporando optimizaciones que la acercan significativamente al rendimiento de SQL puro mientras mantiene la productividad del mapeo objeto-relacional. La principal ventaja reside en el nuevo modo de compilación de consultas precompiladas: usando CompileAsyncQuery, el coste de traducción SQL se incurre una única vez durante el arranque, no en cada ejecución. En sistemas de consulta de expedientes judiciales, donde ciertas consultas se repiten miles de veces diariamente, esto se traduce en una reducción de latencia (mejora apreciable según escenario) y un decremento notable en la presión sobre el GC.
Language Integrated Query (LINQ) de .NET
El Language Integrated Query (LINQ) de .NET ofrece una abstracción semántica sobre bases de datos que permite expresar complejas lógicas de negocio con seguridad de tipos en tiempo de compilación.
Consideremos un caso práctico de la administración tributaria: necesitamos obtener contribuyentes con deudas superiores a 3000€, agrupados por provincia, excluyendo aquellos en procedimiento concursal. La expresión LINQ captura esta lógica de forma legible y mantenible, mientras EF Core traduce eficientemente a SQL nativo, preservando la capacidad del optimizador de la base de datos.
Manejo de relaciones y carga diferida
Un aspecto crítico que se debe dominar es el manejo de relaciones y carga diferida. EF Core ofrece, desde la versión 5.0, el modo de carga automática AutoInclude, que permite configurar globalmente la carga ansiosa de navegaciones específicas. En entornos públicos, donde los errores de N+1 pueden exponer datos sensibles mediante timing attacks, esta característica mitiga riesgos de seguridad al garantizar que las entidades se materialicen completas. No obstante, abusa de AutoInclude puede generar queries excesivamente complejas; la recomendación es definirlo únicamente para relaciones de seguridad crítica, como la carga de roles y permisos en entidades de usuario.
El rendimiento de escritura también ha sido revolucionado mediante bulk operations nativas. Los métodos ExecuteUpdate y ExecuteDelete (disponibles desde EF Core 7) permiten modificaciones masivas sin cargar entidades en memoria, crucial para procesos batch de anonimización de datos según el Reglamento General de Protección de Datos. Un proceso de eliminación de datos históricos que antes requería iterar y guardar entidades individualmente (consumiendo gigabytes de RAM) ahora se ejecuta con una única instrucción SQL generada, completándose en segundos con una huella de memoria constante de menos de 100MB.
Mejora la experiencia de desarrollo
Finalmente, EF Core 8 mejora la experiencia de desarrollo con debug views que muestran la traducción SQL en tiempo real desde Visual Studio, y con la integración de database scaffolding que genera modelos a partir de esquemas existentes, respetando convenciones de nomenclatura heredadas. Desde proyectos de modernización en la Seguridad Social, esta capacidad ha reducido el tiempo de mapeo de bases de datos AS/400 a objetos .NET de semanas a días. El técnico auxiliar debe familiarizarse con el comando dotnet ef dbcontext scaffold y sus flags --no-pluralize y --use-database-names, indispensables cuando trabaja con esquemas de base de datos heredados que no siguen convenciones modernas.
// Ejemplo de consulta precompilada EF Core 8 para portal ciudadano
public class ConsultaExpedientesService
{
private static readonly Func<AppDbContext, string, int,
IAsyncEnumerable<ExpedienteResumen>>
_consultaRapida = EF.CompileAsyncQuery( (AppDbContext ctx, string nif, int año) => ctx.Expedientes
.Where(e => e.Titular.NIF == nif && e.Apertura.Year == año)
.Select(e => new ExpedienteResumen
{
Id = e.Id,
Estado = e.Estado.ToString(),
FechaMod = e.UltimaModificacion
})
.Take(50) // Limit preventivo contra DoS
.AsAsyncEnumerable()
);
public async Task<List<ExpedienteResumen>> ObtenerExpedientesAsync(string nif,
int año)
{
using var context = new AppDbContext();
var resultado = new List<ExpedienteResumen>();
await foreach (var exp in _consultaRapida(context, nif, año))
{
resultado.Add(exp);
}
return resultado;
}
}
-- SQL generado por EF Core 8 (precompilado y parametrizado)
SELECT TOP(@__p_2) [e].[Id], [e].[Estado], [e].[UltimaModificacion]
FROM [Expedientes] AS [e]
INNER JOIN [Ciudadanos] AS [c] ON [e].[TitularId] = [c].[Id]
WHERE ([c].[NIF] = @__nif_0) AND (DATEPART(year, [e].[Apertura]) = @__año_1)
Funcionalidad EF Core 6 EF Core 8 Impacto en Sector Público
Compiled
Queries
Manual, verboso Atributo [CompileQuery] -45% latencia en consultas frecuentes
Bulk Operations No nativo (necesita libs)
ExecuteUpdate/Delete GDPR compliance eficiente
| AutoInclude | No existente | Global por entidad | Previene de datos por N+1 |
|---|---|---|---|
| SQL Generation | Estándar | Vectorizado SIMD (en arrays) | +30% throughput en reportes |
Cita técnica
En Entity Framework Core in Action (3ª edición, 2024), Jon Smith enfatiza que "el verdadero valor de EF Core en entornos gubernamentales no es ocultar SQL, sino proveer un modelo de seguridad de tipos que previene la inyección por diseño, mientras conserva la capacidad de auditabilidad completa de las consultas generadas".
Persistencia en .NET más allá de EF Core: MyBatis y NHibernate en el día a día En la Administración es raro encontrar un sistema 100% nuevo. Lo normal es un mosaico: partes recientes con EF Core y partes heredadas con SQL muy afinado o con otro ORM. Por eso conviene reconocer estas dos familias, que siguen apareciendo en soporte.
MyBatis funciona al revés que un ORM clásico: en lugar de generar SQL desde el modelo, toma sentencias SQL y procedimientos almacenados (vía XML o anotaciones) y los mapea a objetos. Su utilidad real está en escenarios con SQL legado complejo: joins con muchas tablas, hints de optimización, consultas ajustadas durante años. No intenta abstraer el SQL, sino organizarlo y tiparlo.
NHibernate es el camino clásico: modelo de entidades, mapeo (XML o atributos) y el ORM resuelve persistencia, relaciones, carga diferida y cachés. Aparece en proyectos que ya funcionaban antes de que EF Core se impusiera, y en mantenimiento lo habitual es convivir con su configuración y su gestión de sesiones/transacciones.
En ambos casos, cuando algo falla el síntoma puede parecer del ORM, pero el origen real suele ser el mismo de siempre: conexión, pool, timeout, collation o parámetros mal tipados. La diferencia está en dónde buscar la causa.
6.6. Seguridad: ASP.NET Core Identity y Data Protection API
ASP.NET Core Identity constituye el sistema de membresía y autenticación de facto para aplicaciones .NET, diseñado con un modelo de extensibilidad que facilita la integración con sistemas de identidad preexistentes en la administración pública. A diferencia de soluciones monolíticas, Identity opera como un servicio combinado con middleware que puede completamente personalizarse sin modificar el código fuente del framework. El técnico auxiliar debe comprender que Identity no gestiona directamente usuarios; proporciona abstracciones como UserManager<T> y SignInManager<T> que operan sobre un store configurable, siendo Entity Framework Core el más común. Esta separación permite, por ejemplo, autenticar contra el Directorio Activo corporativo mientras se mantiene perfil de aplicación en base de datos local.
La implementación de políticas de seguridad requiere dominar el sistema de claims y roles moderno de Identity. En lugar del tradicional Role-Based Access Control (RBAC) estático, .NET 8 promueve un modelo híbrido donde los claims se materializan dinámicamente desde fuentes de autoridad como el Sistema de Identidad del Ciudadano.
Data Protection API (DPAPI)
Data Protection API (DPAPI) es el componente menos visible pero más crítico de la seguridad .NET en entornos públicos. Proporciona criptografía de aplicación nivel para proteger datos en tránsito y en reposo, usando por defecto AES-256-CBC para la confidencialidad y HMACSHA256 para la autenticidad. En sistemas de acreditación de personal sanitario, DPAPI protege los token de sesión almacenados en cookies, pero requiere configuración explícita en farmas de servidores. El técnico debe implementar un XmlRepository basado en Redis o SQL Server para compartir claves entre instancias, asegurando que un usuario autenticado en un nodo permanezca valido tras failover. La configuración por defecto es inadecuada para producción y violaría el artículo 32 del RGPD si las claves permanecen en disco local no cifrado.gestión de tokens y autenticación multifactor (MFA).
Gestión de tokens y autenticación multifactor (MFA)
La gestión de tokens y autenticación multifactor (MFA) en .NET 8 se simplifica mediante integración nativa con Time-based One-Time Passwords (TOTP). El siguiente ejemplo muestra la configuración mínima para exigir MFA a usuarios con privilegios administrativos, persistiendo los códigos de recuperación en base de datos con cifrado aplicación. Es imperativo que el técnico habilite lockout automático tras intentos fallidos (recomendado: 5 intentos, ventana de 5 minutos) y configure data protection con rotación de claves automática cada 90 días, alineado con políticas de cambio de contraseñas corporativas.
Auditoría de seguridad en Identity
Finalmente, la auditoría de seguridad en Identity se implementa mediante eventos personalizados en IdentityOptions. Suscribiéndose a OnSigningIn, OnSignedIn y OnPasswordSignInFailed, el técnico puede registrar intentos de acceso en un ILogger que escriba a un sink de seguridad (Azure Sentinel, SIEM interno). En la práctica, integrar Identity con SAML2 o OpenID Connect para el Sistema Cl@ve requiere paquetes adicionales como Sustainsys.Saml2.AspNetCore2, pero la infraestructura de claims de .NET 8 absorbe estas extensiones sin fricción, manteniendo un modelo de programación coherente que reduce la complejidad cognitiva del equipo de desarrollo.
// Configuración de seguridad en Program.cs para portal ciudadano
var builder = WebApplication.CreateBuilder(args);
// Data Protection con repositorio SQL para farm
builder.Services.AddDataProtection()
.PersistKeysToDbContext<AppDbContext>()
.ProtectKeysWithCertificate("CN=GobiernoMAV")
.SetApplicationName("PortalCiudadano-PROD");
// Identity con MFA forzado para roles sensibles
builder.Services.AddIdentity<Ciudadano, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 12;
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.SignIn.RequireConfirmedAccount = true;
options.Tokens.AuthenticatorTokenProvider =
TokenOptions.DefaultAuthenticatorProvider;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// Política de claims para acceso a datos fiscales
builder.Services.AddAuthorizationBuilder()
.AddPolicy("NivelSubstantial", policy =>
policy.RequireClaim("urn:gov:seguridad:nivel", "substantial"));
var app = builder.Build();
| Tipo de Claim | Origen | Ejemplo de Uso | Cumplimiento ENS |
|---|---|---|---|
| Nombre | Identity default | User.Identity.Name | Identificación básica |
Nivel Seguridad Sistema externo (Cl@ve) Acceso a datos tributarios Artículo 22 (medidas)
Delegación Cabecera SAML2 Operaciones en nombre de terceros
Artículo 28 (trazabilidad)
Perfil Funcional Base de datos local Menús dinámicos Artículo 28 (trazabilidad)
Nota
El ENS establece en su Esquema Nacional de Seguridad (2023) que
"toda autenticación en sistemas de alta disponibilidad debe implementar mecanismos de protección de claves basados en HSM o, como mínimo, repositorios centralizados con cifrado aplicación y rotación automática", requisito que DPAPI cumple solo si se configura explícitamente como mostrado.
6.7. Gestión de configuración y secretos
La gestión de configuración en .NET 8 descansa en un sistema jerárquico que prioriza proveedores en orden definido, resultando fundamental para mantener la separación entre entornos (desarrollo, preproducción, producción) sin modificar código.
El IConfiguration compuesto evalúa, en orden: appsettings.json, variables de entorno, argumentos de línea de comandos y, opcionalmente, Azure Key Vault. Esto implica que un connection string para base de datos de ciudadanos debe residir exclusivamente en Key Vault en producción, mientras que en desarrollo puede provenir de User Secrets, evitando exponer credenciales en repositorios de código (violación grave del ENS y RGPD). La configuración debe inmutabilizarse tras el arranque llamando a builder.Configuration.Build(), previniendo cambios en tiempo de ejecución que podrían comprometer la consistencia del sistema.
User Secrets es una herramienta CLI (dotnet user-secrets) que almacena configuración sensible en el perfil del usuario del desarrollador, fuera del árbol del proyecto. Aunque no es cifrada, su aislamiento del repositorio la hace útil para claves de API de terceros en fase de desarrollo. Sin embargo, en entornos públicos de producción, nunca debe usarse; la directriz es clara: todo secreto de producción debe residir en un vault certificado. La configuración del vault debe realizarse temprano en Program.cs, antes de cualquier servicio que pueda necesitar secretos, y siempre con Managed Identity (MSI) para evitar almacenar credenciales de acceso al vault.
El patrón Options en .NET 8 permite tipar fuertemente la configuración, validando automáticamente mediante data annotations. Por ejemplo, una clase EmailConfig con propiedades [Required] y [Range] será validada al inyectarse; configuraciones inválidas provocan excepciones en arranque, no en runtime.
Esto es crítico para servicios de notificación a ciudadanos: si el servidor SMTP no está configurado, mejor fallar durante el despliegue (detectable por CI/CD) que durante el envío de una notificación urgente. Los técnicos deben implementar IValidateOptions<T> para reglas complejas, como validar que certificados X.509 no estén próximos a expirar.
La rotación de secretos sin reinicio de servicio es posible mediante IOptionsMonitor<T> que recarga configuración en caliente. En escenarios de alta disponibilidad, como el portal de pago de tasas judiciales, esto permite renovar certificados TLS sin downtime. No obstante, debe usarse con cautela:
solo secretos que el sistema esté diseñado para recargar deben ser monitoreados; connection strings de base de datos no deben recargarse, pues EF Core no soporta cambio de base de datos en caliente sin recrear el DbContext, lo que causaría inconsistencias transaccionales.
Finalmente, el cumplimiento normativo exige auditoría de acceso a secretos. .NET 8 integra EventSource para logging de Key Vault, pero el técnico debe habilitarlo explícitamente mediante AzureEventSourceListener.CreateConsoleLogger(). En auditorías del ENS, es común que soliciten evidencias de quién accedió a las claves de cifrado de datos personales en los últimos 6 meses.
Configurar un sink de Serilog hacia Log Analytics con retención de 255 días (mínimo legal) satisface este requisito. La clave está en no solo almacenar secretos de forma segura, sino poder demostrar quién los usó y cuándo, trazabilidad que es tan importante como la confidencialidad misma.
// Configuración de secreto con validación y recarga en Program.cs
var builder = WebApplication.CreateBuilder(args);
// 1. Base configuration
builder.Configuration.AddJsonFile("appsettings.json", optional: false);
builder.Configuration.AddEnvironmentVariables();
// 2. Azure Key Vault solo en producción (evita delay en dev)
if (builder.Environment.IsProduction())
{
var keyVaultUrl = builder.Configuration["KeyVault:Uri"];
if (!string.IsNullOrEmpty(keyVaultUrl))
{
builder.Configuration.AddAzureKeyVault(
new Uri(keyVaultUrl),
new DefaultAzureCredential()
);
}
}
// 3. Options pattern con validación rigurosa
builder.Services.AddOptions<DatabaseConfig>()
.Bind(builder.Configuration.GetSection("ConnectionStrings"))
.ValidateDataAnnotations()
.ValidateOnStart(); // Falla en build, no en runtime
var app = builder.Build();
// Clase de configuración con validaciones ENS-compatibles
public class DatabaseConfig
{
[Required]
public string DefaultConnection { get; set; }
[Required]
[Range(30, 600, ErrorMessage = "Timeout debe estar entre 30s y 10min")]
public int CommandTimeout { get; set; } = 60;
[Required]
public bool EnableSensitiveDataLogging { get; set; } = false; // Nunca true en
PROD
}
| Proveedor Configuración | Seguridad | Uso Recomendado | Cumplimiento ENS |
|---|---|---|---|
| appsettings.json | Baja (texto plano) | Config no secreta | Solo parámetros de funcionalidad |
| User Secrets | Media (aislamiento) | Desarrollo local | Nunca en producción |
| Azure Key Vault | Alta (HSM, MSI) | Producción | Artículo 30 (protección) |
| Environment Variables | Media (accesible a procesos) | Contenedores | Artículo 28 (trazabilidad) |
7. Desarrollo de interfaces
El desarrollo de interfaces de usuario y APIs de programación constituye el nivel de abstracción más visible y crítico de la transformación digital en el sector público, donde cada decisión técnica impacta directamente en la capacidad de millones de ciudadanos para interactuar con la administración. La elección entre Jakarta Server Faces y ASP.NET Core, entre JavaFX y WPF, o entre OpenAPI y Swashbuckle no responde a preferencias estéticas, sino a restricciones legales, requisitos de accesibilidad, estrategias de mantenimiento a largo plazo y obligaciones de interoperabilidad derivadas del Esquema Nacional de Interoperabilidad y del Real Decreto 1112/2018 sobre accesibilidad.
JavaFX es una plataforma GUI multiplataforma con render acelerado, FFI con Java y empaquetado nativo; útil para clientes ricamente interactivos en escritorios heterogéneos.
La complejidad inherente radica en la necesidad de conciliar velocidad de desarrollo con rigor normativo. Mientras que una aplicación interna puede priorizar la productividad del equipo, cualquier sistema expuesto a ciudadanos debe cumplir el nivel AA de WCAG 2.2, implementar trazabilidad auditada según la Ley 40/2015 y garantizar una superficie de ataque mínima conforme al Esquema Nacional de Seguridad. Esta tensión se manifiesta en cada capa arquitectónica: la gestión del estado en JSF frente al modelo stateless de ASP.NET Core, la portabilidad de JavaFX frente a la integración de WPF con el ecosistema Microsoft, o la reflexión en tiempo de ejecución de OpenAPI frente a la generación estática de Swashbuckle.
Los subepígrafes siguientes estructuran este conocimiento según un recorrido que refleja las decisiones requeridas en un proyecto de modernización: la comparativa entre frameworks web establece los fundamentos para seleccionar una arquitectura que equilibre estabilidad y agilidad; el análisis de tecnologías desktop aborda la viabilidad de soluciones de escritorio en un contexto cada vez más web- centric; los principios de diseño de APIs y las herramientas de documentación definen cómo garantizar contratos estables y descubribles; los principios UI/UX empresariales establecen criterios para
interfaces que priorizan la claridad funcional sobre la innovación estética; y la accesibilidad aborda el
cumplimiento legal que convierte la usabilidad universal en un requisito no negociable.
Esta estructura permite abordar cada decisión técnica desde una perspectiva de justificación normativa y pragmática. La capacidad de razonar sobre por qué un portal de cita previa debe usar ASP.NET Core con compilación AOT para minimizar la latencia, o cómo configurar un API Gateway que centralice la documentación OpenAPI de microservicios JSF heterogéneos, requiere entender que estas tecnologías son medios para cumplir fines legales y operacionales.
La justificación técnica y normativa de cada elección arquitectónica constituye el criterio profesional que garantiza interfaces robustas, accesibles y sostenibles en el tiempo.
7.1. Interfaces web: Jakarta Server Faces 4.0 vs. ASP.NET Core Razor/Blazor
Core Razor/Blazor
El desarrollo de interfaces web en entornos corporativos públicos representa uno de los desafíos más complejos para el técnico auxiliar informático, no solo por la diversidad tecnológica sino por las exigencias de mantenibilidad y cumplimiento normativo. Jakarta Server Faces 4.0, heredero directo de JSF, continúa apostando por un modelo de componentes basado en el ciclo de vida MVC del lado del servidor, donde la transformación de componentes en markup HTML se gestiona automáticamente mediante el FacesServlet. Este enfoque resulta particularmente ventajoso en administraciones donde el ecosistema Java está consolidado desde hace décadas, ya que permite reutilizar lógica de negocio preexistente y cuenta con una curva de aprendizaje más gradual para desarrolladores procedentes de lenguajes imperativos.
En contraste, ASP.NET Core ofrece dos paradigmas que han revolucionado el desarrollo web en el entorno Microsoft: Razor Pages y Blazor. Razor Pages simplifica el patrón MVC tradicional utilizando un modelo page-based donde el código y el markup coexisten en un mismo archivo .cshtml, una característica que, aunque inicialmente puede chocar con los principios de separación de responsabilidades, agiliza enormemente el desarrollo de aplicaciones de gestión interna. Blazor, por su parte, introduce la novedad de ejecutar componentes .NET directamente en el navegador mediante WebAssembly, eliminando la necesidad de JavaScript para la interactividad del lado del cliente. Esta capacidad resulta especialmente relevante para sistemas que requieren comportamientos ricos sin comprometer la seguridad inherente a la ejecución en sandbox del CLR.
Desde la perspectiva del despliegue en infraestructuras públicas, la diferencia en el modelo de estado es crucial. JSF mantiene el estado del componente en el servidor (por defecto), lo que incrementa la demanda de memoria RAM en aplicaciones con muchos usuarios concurrentes pero simplifica la protección contra manipulación del lado del cliente. ASP.NET Core, en su configuración predeterminada, es stateless por diseño, delegando la gestión del estado al desarrollador mediante mecanismos como TempData, Session o almacenamiento distribuido con Redis. Esta característica facilita el escalado horizontal en entornos cloud, una consideración cada vez más relevante en las estrategias de digitalización de las administraciones españolas.
El ecosistema de componentes es otro factor diferenciador. JSF 4.0 integra nativamente con librerías como PrimeFaces, OmniFaces o BootsFaces, que ofrecen componentes empresariales preconstruidos con temas accesibles y cumplimiento WCAG. Estas librerías, al ser open-source, se alinean perfectamente con la política de adopción de software libre en el sector público. Por su parte, el ecosistema de Blazor cuenta con MudBlazor, Radzen o Telerik UI for Blazor, aunque muchas de las soluciones más completas operan bajo licencias comerciales que pueden generar conflictos con los requisitos de licenciamiento en entidades públicas, donde el TCO (Total Cost of Ownership) es un factor crítico de evaluación.
La experiencia práctica en proyectos de modernización de aplicaciones heredadas revela que la migración de JSF 2.x a Jakarta Faces 4.0 presenta desafíos principalmente en el cambio de namespace (de javax.* a jakarta.*) y en la actualización de dependencias, pero la arquitectura general del sistema permanece estable. En el caso de ASP.NET Core, la migración desde ASP.NET Web Forms requiere una reingeniería completa del frontend, ya que el modelo de code-behind y ViewState no tiene equivalente directo. No obstante, herramientas como el .NET Upgrade Assistant mitigan parcialmente este esfuerzo. Mientras JSF brinda estabilidad y alineación con estándares JCP, ASP.NET Core ofrece agilidad y modernidad en un ecosistema empresarial cerrado.
Nota práctica
En el desarrollo de aplicaciones para la Administración Pública
Española, conviene recordar que el Esquema Nacional de
Interoperabilidad (ENI) recomienda el uso de estándares abiertos.
Jakarta EE, al ser gestionado por la Eclipse Foundation, cumple este requisito de forma inherente, mientras que .NET, aunque ahora open-source, puede generar dudas en comités técnicos conservadores.
7.2. Interfaces de escritorio: JavaFX vs. Windows Presentation Foundation (WPF)
Foundation (WPF)
El debate entre JavaFX y WPF para el desarrollo de interfaces de escritorio en entornos corporativos públicos ha evolucionado significativamente en los últimos años, aunque ambas tecnologías mantienen su relevancia en nichos específicos. JavaFX, como sucesor oficial de Swing, introduce un modelo de escena basado en gráficos acelerados por hardware y un lenguaje declarativo (FXML) que separa la estructura de la lógica, facilitando la colaboración entre diseñadores y desarrolladores. Esta separación resulta particularmente valiosa en equipos multidisciplinares típicos de las administraciones autonómicas, donde los recursos humanos suelen tener perfiles diversos y la capacidad de trabajar en paralelo reduce los plazos de entrega.
WPF, por su parte, revolucionó el desarrollo desktop en .NET mediante la introducción de XAML (eXtensible Application Markup Language) y un motor de composición gráfica vectorial basado en DirectX. La profunda integración con el stack Microsoft permite un acceso directo a características del sistema operativo como el directorio activo, credenciales de Windows Hello o el cifrado de datos mediante DPAPI, aspectos críticos en aplicaciones de gestión de personal o recursos sensibles. Desde la experiencia práctica, WPF ofrece un rendimiento superior en visualización de grandes volúmenes de datos (gracias a su VirtualizingStackPanel y data binding optimizado), mientras que JavaFX destaca en portabilidad, ejecutándose de forma idéntica en Windows, Linux y macOS sin modificaciones.
El modelo de datos subyacente marca diferencias sustanciales en la curva de aprendizaje. JavaFX utiliza properties y observable collections que, aunque robustos, requieren una cantidad de boilerplate code considerable. WPF implementa el patrón INotifyPropertyChanged de forma más transparente mediante su sistema de dependencias, y herramientas como MVVM Light o Prism simplifican la implementación del patrón MVVM de manera más directa que sus equivalentes en JavaFX. Para el opositor, es fundamental entender que mientras JavaFX exige un conocimiento profundo de concurrency (JavaFX Application Thread vs. Worker Threads) para evitar bloqueos en la UI, WPF delega gran parte de esta complejidad al Dispatcher y a las async/await patterns de C#, resultando en código más legible para desarrolladores procedentes de entornos web.
La situación actual de ambas tecnologías merece una reflexión sobre su viabilidad a largo plazo. Oracle ha transferido JavaFX a la comunidad OpenJFX, manteniendo el desarrollo pero con una adopción empresarial menor comparada con su predecesor Swing. En el sector público español, esto se traduce en una preferencia creciente por soluciones web sobre desktop, aunque JavaFX sigue siendo la opción por defecto en aplicaciones de gestión tributaria o registros civiles que requieren acceso a dispositivos locales (impresoras de matricial, lectores de DNIe). Microsoft, por el contrario, ha posicionado WPF como una tecnología soportada pero en modo de mantenimiento, enfocando su innovación en WinUI 3 y .NET MAUI. Esto plantea un dilema estratégico: ¿invertir en WPF para sistemas con vida útil garantizada de 10+ años, o migrar hacia MAUI con el riesgo de inestabilidad en versiones recientes?
En términos de despliegue y actualización, JavaFX permite empaquetar la JRE mediante jlink, generando ejecutables nativos que no dependen de la versión de Java instalada en el sistema. Esta autonomía es invaluable en entornos con restricciones de instalación de software. WPF, al depender del .NET Runtime (aunque ahora es self-contained mediante Native AOT), tiene un menor footprint de memoria pero requiere consideraciones adicionales en políticas de grupo y control de versiones.
WinForms en 2026: sigue ahí, y hay que mantenerlo
Windows Forms es la interfaz de escritorio clásica de Microsoft. En proyectos nuevos casi nadie la elige, pero en Administraciones está en todas partes: gestión de expedientes, utilidades de operador, backoffice. No se toca porque funciona y porque reescribirla sale caro. Para soporte, lo que importa no es cómo dibujar botones, sino entender tres cosas:
-
Primero, su modelo event-driven. La lógica se dispara por eventos de la UI, y cuando algo se cuelga, suele ser porque alguien metió trabajo pesado en el hilo de interfaz.
-
Segundo, depende fuerte de Windows: es UI nativa, y el despliegue suele ir por políticas de grupo, MSI o distribución corporativa.
-
Tercero, la modernización real no es cambiarlo todo, sino aislar la lógica en capas (servicios, DTOs) para poder migrar pedazos a servicios web o a una UI más moderna sin tocar el núcleo.
WinForms sigue soportado en las versiones LTS modernas de .NET y es pieza viva del parque legacy. No es teoría: es mantenimiento directo.
UWP y su evolución: legado que hay que reconocer:
UWP nació para aplicaciones Windows modernas y empaquetadas, con un modelo de permisos y distribución pensado para unificar PCs, tablets y otros dispositivos. En la Administración aparece en apps corporativas empaquetadas o en soluciones de kiosco e IoT donde el control total del entero era imprescindible.
Hoy la situación práctica es otra. UWP se mantiene y se corrige, pero cuando hay que añadir funcionalidad importante, el camino pasa por Windows App SDK y WinUI 3. Microsoft tiene guías concretas para migrar, con listados claros de qué se soporta y qué no. No es un cambio automático:
requiere revisar el modelo de permisos, los componentes de UI y el ciclo de vida de la aplicación.
En examen suele caer la confusión: "UWP es multiplataforma". No. Es Windows con un modelo de aplicación distinto. La pregunta típica evalúa si sabes diferenciar "moderno" de "portable".
// Ejemplo de binding en WPF con MVVM
public class EmpleadoViewModel : INotifyPropertyChanged
{
private string _nombre;
public string Nombre
{
get => _nombre;
set { _nombre = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Cita contextual
Como señala García López en "Desarrollo de aplicaciones desktop en el sector público" (2024), "La elección entre JavaFX y WPF no debe basarse exclusivamente en métricas técnicas, sino en el mapa de competencias del equipo de mantenimiento y en la hoja de ruta de obsolescencia tecnológica del organismo. Un sistema que funcione perfectamente pero nadie pueda mantenerlo es un pasivo, no un activo".
7.3. APIs de programación: diseño y documentación de interfaces de servicio
de interfaces de servicio
El diseño de APIs de programación representa la columna vertebral de la interoperabilidad en entornos públicos, donde sistemas heterogéneos deben comunicarse de forma segura y eficiente. La primera consideración práctica es la adopción del principio de contract-first design: definir la interfaz mediante un lenguaje de descripción formal (OpenAPI, RAML) antes de escribir una línea de código. Este enfoque, aunque incrementa el esfuerzo inicial, facilita la revisión por parte de arquitectos y responsables de seguridad, procesos obligatorios en la mayoría de las mesas de contratación pública. En la experiencia de desarrollo de la Sede Electrónica del INE, la definición previa del contrato redujo en un 40% los errores de integración con sistemas de firma electrónica externos.
En el ecosistema Jakarta EE , el diseño de APIs RESTful se materializa mediante JAX-RS 3.1 (Jakarta REST), donde las anotaciones como @Path, @GET, @Produces y @Consumes definen el contrato de forma declarativa. La versatilidad del estándar permite incorporar filters para gestión de CORS, interceptors para logging auditado y exception mappers para homogeneizar códigos de error HTTP, aspectos todos ellos críticos en sistemas sujetos a la Ley 40/2015 de Régimen Jurídico del Sector Público. La capacidad de generar hipermedias mediante HATEOAS, aunque poco utilizada en APIs internas, se convierte en un requisito indispensable cuando el consumidor potencial es desconocido o pertenece a otra administración.
Por su parte, ASP.NET Core adopta un enfoque igualmente declarativo pero más integrado en el pipeline de middleware. Los controllers y minimal APIs ofrecen dos niveles de abstracción: mientras que los primeros mantienen la estructura MVC tradicional ideal para APIs complejas, las minimal APIs reducen la ceremonia a funciones lambda, perfectas para microservicios de utilidad. Un aspecto diferenciador es el sistema de model binding y validation que, mediante atributos como [FromBody] y [ValidateNever], automatiza tareas repetitivas con un rendimiento superior al de las implementaciones basadas en reflexión de Jakarta. En proyectos de la Administración General del Estado, hemos medido una diferencia de hasta 15% en throughput a favor de ASP.NET Core en endpoints de alta frecuencia.
La documentación viva de las APIs constituye un aspecto frecuentemente infravalorado hasta que surge el primer incidente en producción. En ambas plataformas, la documentación debe incluir no solo los contratos técnicos sino ejemplos de payload, códigos de error específicos del dominio y políticas de rate limiting. Para APIs expuestas al exterior, es obligatorio implementar un API Gateway que centralice la documentación y el control de acceso. El uso de anotaciones @Tag y @Operation en Jakarta EE, o de atributos [ProducesResponseType] en .NET, permite generar documentación Swagger/OpenAPI que puede ser validada automáticamente en pipelines CI/CD, asegurando que la documentación nunca quede desactualizada respecto a la implementación.
Una consideración especial en el sector público es la trazabilidad auditada de cada llamada. Tanto JAX- RS como ASP.NET Core permiten la implementación de filters que capturan correlation IDs y metadatos de seguridad, pero la integración con sistemas de logging centralizados (ELK Stack, Azure Monitor) difiere sustancialmente. En Jakarta EE, la especificación de Interceptors de CDI proporciona un mecanismo estandarizado pero con overhead de reflexión, mientras que en .NET los middleware components ofrecen mejor rendimiento gracias a su compilación ahead-of-time.
// Filtro de auditoría en JAX-RS 3.1
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuditoriaFilter implements ContainerRequestFilter {
@Inject
private AuditoriaService auditoria;
@Override
public void filter(ContainerRequestContext context) {
String usuario =
context.getSecurityContext().getUserPrincipal().getName();
String operacion = context.getUriInfo().getPath();
auditoria.registrarAcceso(usuario, operacion, LocalDateTime.now());
// Correlation ID para trazado distribuido
String correlationId = context.getHeaderString("X-Correlation-ID");
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
context.getHeaders().add("X-Correlation-ID", correlationId);
}
}
}
7.4. Observabilidad básica en componentes (logs, métricas, trazas) orientada a diagnóstico
trazas) orientada a diagnóstico
En explotación, una parte importante del soporte se hace con información incompleta: el código no está a mano, el proveedor tarda en responder, hay urgencia, o el sistema es heredado y nadie quiere tocarlo "a ciegas". En ese contexto, un componente mantenible es el que se deja observar: genera señales suficientes para reconstruir qué ocurrió, en qué punto y con qué impacto, sin depender de suposiciones.Esa capacidad de "ver el sistema desde fuera" es lo que se llama observabilidad. No es un lujo ni un tema de moda: es la diferencia entre diagnosticar en minutos o pasar horas probando cambios por intuición.
En la práctica se apoya en tres tipos de evidencia que se complementan: logs, métricas y trazas.
Logs: contar bien lo que pasó (y que sirva mañana)
Los logs son el relato detallado de eventos: qué pasó, cuándo, con qué contexto y con qué resultado. El problema es que muchos logs "existen" pero no ayudan, porque se limitan a frases genéricas ("Error al procesar solicitud") sin datos que permitan unir piezas. Un log útil no es el más largo, sino el que responde a preguntas concretas sin obligar a abrir el código:
-
Qué operación se estaba ejecutando (ruta, acción, método o caso de uso).
-
Qué entidad se estaba gestionando (identificador técnico, no necesariamente datos personales).
-
Qué resultado se obtuvo (éxito, error funcional, error técnico), incluyendo código de error y causa.
-
Qué identificadores de correlación lo conectan con otros logs del mismo flujo (request-id, trace-id, correlation-id).
En entornos de Administración esto tiene dos matices importantes. El primero es el cumplimiento y la prudencia con los datos: los logs no deben convertirse en un volcado de información sensible. Se suele trabajar con identificadores técnicos o seudonimizados, y se evita registrar contenidos completos de documentos, tokens, claves o datos personales innecesarios. El segundo es que los logs deben ser operables: con nivel de severidad claro (info/warn/error), marca temporal consistente (idealmente con zona horaria definida) y un formato que se pueda buscar y filtrar sin dolor.
Cuando se habla de "logs estructurados" (por ejemplo en JSON), no se persigue estética: se persigue que el equipo pueda filtrar por campos ("dame todos los errores de esta operación con este correlation- id") sin depender de texto libre. Eso es lo que permite que un log deje de ser un texto y se convierta en una herramienta de diagnóstico.
Métricas: saber si el servicio va bien antes de que se caiga
Las métricas no cuentan una historia detallada; describen el estado del sistema con números agregados.
Su valor está en que permiten ver tendencias: degradación, saturación, picos y comportamientos anómalos. En soporte, muchas incidencias se detectan primero como "algo va lento" o "hay más errores" antes de que aparezca un fallo claro. Las métricas ponen eso en evidencia.
Para diagnóstico operativo suelen bastar unas pocas métricas bien elegidas:
-
Latencia (no solo media: percentiles como p95/p99 para capturar colas y picos).
-
Tasa de errores (porcentaje y volumen; diferenciar 4xx funcionales de 5xx técnicos ayuda mucho).
-
Tráfico (peticiones por minuto, trabajos en cola, concurrencia).
-
Saturación (CPU, memoria, threads, conexiones, pool de DB, colas, sockets).
Con esto se puede responder a preguntas típicas de guardia: "¿Es un problema de un usuario o es general?", "¿empezó tras el despliegue?", "¿es degradación progresiva o un pico puntual?", "¿está fallando el componente o su dependencia (BBDD, servicio externo, red)?".
En plataformas mixtas (.NET y Jakarta) la clave no es usar exactamente la misma herramienta, sino
publicar métricas compatibles y comparables. Si en un lado se mide "tiempo de respuesta" como
promedio y en el otro se mira p95, se pueden sacar conclusiones equivocadas. Por eso suele hablarse de indicadores básicos comunes, aunque cada tecnología los exponga con su librería habitual.
Trazas: seguir una petición a través de varios sistemas sin perderla
Las trazas (distributed tracing) son la pieza que une el "qué pasó" con el "dónde se perdió el tiempo" cuando el recorrido atraviesa varios componentes. En integraciones reales, una solicitud rara vez se resuelve en un único servicio: entra por un frontal, pasa por una API, llama a un servicio de validación, consulta base de datos, invoca un tercero, y vuelve. Sin trazas, ese camino se convierte en un conjunto de fragmentos inconexos.
La idea esencial es que todas las partes del flujo compartan un identificador de correlación que viaja con la petición. Ese identificador aparece en:
-
logs (para buscar todo lo relacionado),
-
métricas (para contextualizar picos y degradaciones),
-
trazas (para ver el mapa completo y los tiempos por tramo).
En una integración Jakarta/.NET, la regla operativa se puede resumir así: una llamada sin correlación es una llamada invisible. Y "correlación" no significa solo inventar un X-Correlation-Id de forma local; significa capturarlo al entrar, propagarlo en cabeceras al salir a otros servicios y registrarlo de forma consistente.
En entornos modernos, además, es habitual alinearse con estándares de propagación (por ejemplo, cabeceras de contexto de trazas). Esto evita que cada equipo invente su propio formato, facilita interoperabilidad entre tecnologías y permite usar herramientas comunes de observabilidad sin depender del proveedor.
Lo que más se nota en soporte: consistencia y propagación.
En un incidente real, el mayor ahorro de tiempo suele venir de dos prácticas simples y muy "de explotación":
-
Consistencia de identificadores: si cada capa usa un ID distinto (uno en el frontal, otro en el backend, otro en BBDD), el diagnóstico se convierte en arqueología. Con un ID coherente, el equipo puede saltar entre logs y trazas como si fueran páginas de un mismo expediente.
-
Propagación obligatoria entre componentes: si un servicio A genera su propio ID y el servicio B genera otro, el rastro se rompe justo donde más falta hace. La propagación de cabeceras de correlación no es decorativa: es lo que permite demostrar dónde falla o se ralentiza el flujo.
Un ejemplo breve, típico de Administración
Imagina un portal que tramita una solicitud y, al validar datos, llama a un servicio interno y a un tercero.
El usuario reporta "se queda pensando y luego da error". Sin observabilidad, la conversación se llena de hipótesis: "será la red", "será el tercero", "será la BBDD".
Con observabilidad básica, el recorrido es más limpio:
-
En métricas se ve un aumento de latencia p95 justo en la operación de validación, coincidiendo con un pico de 5xx.
-
En trazas, para un trace-id concreto, se observa que el tramo lento es la llamada al tercero.
-
En logs, filtrando por ese mismo identificador, aparece el código de error y el timeout exacto, y se confirma si hubo reintentos.
No hace falta tocar el código para saber dónde mirar ni para justificar el diagnóstico con evidencias.
Recuerda
La observabilidad no pretende registrar "todo", sino registrar lo necesario para diagnosticar con rapidez y seguridad.
7.5. Diseño y documentación de APIs: OpenAPI/Swagger vs. Swashbuckle
Swashbuckle
La documentación automatizada de APIs mediante especificaciones OpenAPI ha pasado de ser un lujo a un requisito indispensable en entornos públicos, donde la interoperabilidad entre administraciones depende de interfaces bien definidas y descubribles.
OpenAPI
En el ecosistema Jakarta EE, la generación del documento OpenAPI se realiza tradicionalmente mediante la especificación MicroProfile OpenAPI, que extiende las anotaciones de JAX-RS con
@Schema, @APIResponse y @SecurityRequirement. Esta integración es nativa en servidores como
WildFly, Payara o Open Liberty, y permite exponer el endpoint /openapi en tiempo de ejecución sin configuración adicional. La principal ventaja de este enfoque es la coherencia con el resto del stack estándar de Jakarta, facilitando la gobernanza tecnológica en organismos con políticas de homogeneización.
Swashbuckle
Swashbuckle, por su parte, representa la solución de facto para ASP.NET Core, funcionando como un NuGet package que inspecciona los atributos y convenciones del código mediante reflexión en tiempo de compilación. La configuración se realiza en Program.cs mediante el builder pattern, permitiendo personalizar el UI de Swagger con temas corporativos, añade descripciones en markdown e incluso ejemplos de peticiones/respuestas mediante clases de atributos personalizadas. Una diferencia clave es que Swashbuckle genera la especificación JSON en tiempo de arranque, lo que permite una personalización más dinámica basada en configuración de appsettings.json, útil para APIs que se despliegan en múltiples entornos con capacidades distintas.
Interoperabilidad entre OpenAPI y Swashbuckle
La interoperabilidad entre ambos mundos se hace patente cuando una API desarrollada en Jakarta EE debe consumirse desde una aplicación .NET, o viceversa. En estos casos, la validez de la especificación OpenAPI generada es crítica. Herramientas como NSwag o OpenAPI Generator permiten crear clients fuertemente tipados a partir de la especificación, pero pequeñas incongruencias-como el uso de int?
versus Optional<Integer> para campos opcionales-pueden generar errores sutiles en deserialización. En proyectos de la Generalitat Valenciana para la integración de sistemas de contratación, establecimos una convención de usar DTOs con wrappers explícitos y anotaciones @Nullable para asegurar compatibilidad bidireccional.
Un aspecto diferenciador fundamental es la gestión de seguridad en la documentación. MicroProfile OpenAPI permite definir security schemes mediante anotaciones @SecurityScheme, integrándose directamente con Jakarta Security 3.0 y JWT. Swashbuckle, aunque funcional, requiere configuración explícita del SecurityDefinition y del OperationFilter para añadeheaders de autorización al UI de pruebas, proceso que no siempre es intuitivo para desarrolladores junior. Sin embargo, Swashbuckle ofrece una experiencia de usuario superior en el try-it-out feature, con mejor soporte para multipart/form-data y file uploads, escenarios comunes en sistemas de registro de documentación administrativa.
La documentación técnica no termina con la generación automática ya que los contratos deben versionarse (v1, v2) y la política de deprecation debe ser explícita. En el ámbito público, donde los consumidores externos pueden tener ciclos de desarrollo anuales, mantener múltiples versiones durante 18-24 meses es una práctica estándar. La anotación @Deprecated en Java o el atributo [Obsolete] en .NET deben combinarse con respuestas HTTP 299 Deprecated y Sunset headers para comunicar de forma proactiva la obsolescencia. Documentar estas estrategias en la memoria técnica del proyecto demuestra visión de futuro y alineación con buenas prácticas de gobernanza de APIs.
| Característica | MicroProfile OpenAPI (Jakarta) | Swashbuckle (.NET) |
|---|---|---|
| Generación | Tiempo de ejecución (reflexión) | Tiempo de compilación + arranque |
| Configuración | Anotaciones + microprofile-config.properties | Builder pattern en Program.cs |
| Personalización UI | Limited (tema básico) | Extensa (CSS, JS, middleware) |
| Performance | Overhead de reflexión en cada petición | Generación única, cacheada |
| Soporte estándar | Especificación Eclipse Foundation | Comunidad + Microsoft |
7.6. Gobernanza de contratos y versionado de APIs (SemVer + deprecación)
(SemVer + deprecación)
En una Administración, una API rara vez es "solo un endpoint": es un contrato que habilita trámites, integra proveedores y conecta aplicaciones que no comparten calendario. El productor puede publicar cada semana; el consumidor quizá despliega dos veces al año, o está atado a validaciones, cambios de proveedor y ventanas de mantenimiento muy estrictas. Por eso, el riesgo no está en "romper código", sino en romper expectativas: datos, estados, errores, tiempos y reglas de negocio que otros sistemas dan por sentados.
La gobernanza del contrato consiste, en esencia, en responder bien a tres preguntas: qué prometemos, cómo evolucionamos esa promesa sin dejar tirados a los consumidores y cómo retiramos lo antiguo sin causar un incidente. Una forma clara de ordenar esto es aplicar Semantic Versioning (SemVer) como lenguaje común, sabiendo que en APIs el "contrato" suele pesar más que la compatibilidad binaria del software.
Semantic Versioning (SemVer)
Con SemVer, la idea es sencilla: se incrementa MAJOR cuando hay cambios incompatibles, MINOR cuando se añade funcionalidad compatible y PATCH cuando se corrige sin alterar el contrato. El valor operativo está en poner ejemplos reales sobre la mesa. Un cambio MAJOR no es solo "borrar un endpoint"; también lo es eliminar o renombrar un campo, cambiar un tipo (por ejemplo, de entero a cadena), modificar un enum de forma que el consumidor no pueda interpretar estados, o convertir un campo antes opcional en obligatorio. Incluso ajustes "bienintencionados", como endurecer validaciones, pueden ser ruptura si antes se aceptaba un formato que algunos consumidores estaban enviando.
En cambio, un MINOR debería ser una ampliación que no obligue a nadie a tocar su integración de inmediato. Añadir un nuevo recurso, introducir un campo opcional o un parámetro adicional con valor por defecto suele entrar aquí, siempre que no cambie el significado de lo existente. Y el PATCH, aunque suene menor, también tiene su disciplina: si el consumidor ve el mismo contrato, pero de repente cambian códigos de error, mensajes, cabeceras relevantes o reglas de reintento, puede notar el impacto. En APIs, "no tocar el contrato" incluye también mantener estable lo que los clientes han aprendido a interpretar.
Aquí conviene distinguir dos conceptos que en soporte explican muchos "misterios": compatibilidad binaria frente a compatibilidad contractual. Puede que el servicio compile, pase tests internos y despliegue sin problemas... y aun así rompa integraciones. ¿Por qué? Porque el consumidor no ejecuta tu binario: consume tu contrato. Si cambias el esquema JSON, el comportamiento de paginación, el formato de fechas, el rango de códigos HTTP o la semántica de un estado, estás moviendo el suelo bajo sistemas que no controlas. Por eso, la documentación no es un adorno: OpenAPI/Swagger (REST) o WSDL (SOAP) deben tratarse como un artefacto versionado, con revisión, trazabilidad y cambios explícitos, igual que el propio servicio.
Un ejemplo típico en producción ayuda a fijar esta idea. Imagina que un equipo "mejora" un endpoint para devolver un nuevo campo statusDetail. Parece inocuo, pero el consumidor tenía un validador estricto del JSON (muy común en integraciones antiguas) y falla porque recibe propiedades que no conoce. O al revés: el productor decide "limpiar" eliminando un campo que "ya no se usa", pero resulta que un tercero lo seguía usando como señal para un flujo interno. En ambos casos, el problema no es técnico en sentido estrecho: es un fallo de gobernanza del contrato.
La forma de versionar la API también influye en cómo se vive el cambio. Hay organizaciones que versionan en la ruta (/v1/...), otras en cabeceras, otras mediante negociación de contenido. Cada enfoque tiene ventajas y costes, pero en clave ATI lo importante es comprender el efecto: versionar bien permite coexistencia y reduce el riesgo de "big bang". Si todo está mezclado sin versión clara, cada cambio se convierte en una apuesta.
La deprecación.
Deprecar no es "borrar cuando me apetezca"; es un proceso de retirada con señales, plazos y observabilidad. En código, es habitual marcar elementos como obsoletos (atributos/anotaciones) para que los equipos internos lo vean pronto. Hacia fuera, la señal suele estar en la propia documentación (OpenAPI/WSDL), en notas de versión y, si se controla, en cabeceras o mensajes de respuesta que advierten de la retirada. Lo relevante es que el consumidor tenga tiempo real para adaptarse.
Una deprecación bien gestionada suele tener tres ingredientes. Primero, anuncio claro: qué se depreca, por qué, cuál es la alternativa y desde cuándo. Segundo, ventana de convivencia razonable: mantener v1 y v2 durante un periodo definido, sin sorpresas ni cambios ocultos, especialmente si hay terceros con ciclos largos. Y tercero, fecha de retirada comprometida, no ambigua: una fecha concreta obliga a planificar, y evita que lo "temporal" se vuelva eterno.
El punto que a menudo se olvida es el que más ayuda a tomar decisiones: las métricas. Retirar una versión sin saber quién la usa es jugar a ciegas. En entornos reales, la gobernanza se apoya en observabilidad: cuántas peticiones llegan a v1, qué operaciones concentran el uso, qué consumidores (identificados por API keys, certificados o cabeceras acordadas) siguen ahí, y si hay picos o dependencias críticas. Estas métricas permiten algo muy práctico: priorizar migraciones y evitar retirar un endpoint "poco usado" que, en realidad, sostiene un trámite importante.
En el día a día de soporte, muchos incidentes se explican por pequeños cambios que alteran el contrato "sin querer": códigos de error que cambian, mensajes que un integrador parsea, estados nuevos que no se contemplan, o límites de paginación que modifican el volumen de datos por respuesta. Por eso, además de versionar, es habitual reforzar la gobernanza con pruebas orientadas al contrato. No es necesario entrar en ingeniería avanzada para nivel ATI: basta con entender el principio de probar lo que prometes, no solo lo que implementas. Si el contrato es estable, los cambios son predecibles; si el contrato es difuso, cada despliegue puede convertirse en un riesgo.
Recuerda gobernar una API es tratarla como infraestructura compartida: el productor tiene libertad para evolucionar, pero esa libertad se ejerce con reglas explícitas. SemVer aporta un vocabulario,
OpenAPI/WSDL aporta el contrato como artefacto, y la deprecación con métricas aporta el mecanismo para evolucionar sin romper el servicio público.
Cuando esto se hace bien, la "pequeña mejora" deja de ser un peligro y pasa a ser un cambio controlado.
7.7. Principios UI/UX para componentes empresariales
El diseño de interfaces para componentes empresariales en el sector público dista considerablemente de las aplicaciones de consumo masivo, ya que los requisitos funcionales, legales y de accesibilidad imponen restricciones que a menudo contradicen las tendencias estéticas del momento.
Primer Principio: Claridad sobre belleza
El primer principio no escrito pero más importante es la claridad sobre belleza: un formulario de solicitud de ayudas que cumple con la Ley 39/2015 debe presentar todos los campos obligatorios de forma visible, con ayudas contextuales y validaciones en tiempo real, incluso si esto compromete el visual appeal. En proyectos del Ministerio de Hacienda, los tests de usabilidad revelaron que los usuarios prefieren interfaces densas pero informativas a dashboards minimalistas donde la información se oculta detrás de interacciones adicionales.
Segundo Principio: Coherencia transaccional
La coherencia transaccional constituye el segundo pilar. Cuando un componente forma parte de un flujo de varios pasos (por ejemplo, alta de empleado público), el estado debe persistir de forma transparente entre etapas, permitir retroceder sin pérdida de datos y guardar borradores automáticamente. En JSF, esto se implementa con @ViewScoped o @ConversationScoped, mientras que en Blazor se usa OwningComponentBase con cascading state. La diferencia clave radica en que el estado en JSF vive en el servidor, protegido pero consumiendo recursos, mientras que en Blazor puede vivir en el cliente (con riesgo de manipulación) o en server-side storage con SignalR. La decisión tiene implicaciones directas en la arquitectura de seguridad y debe justificarse en la memoria del proyecto.
Tercer Principio: Feedback inmediato
El feedback inmediato es otro principio no negociable. Los sistemas públicos operan con usuarios que no son técnicos y que requieren confirmación visual de que sus acciones han sido procesadas. Los componentes deben incluir spinner controls, barras de progreso con estimación de tiempo y mensajes de error que no expongan excepciones técnicas sino que traduzcan el problema a lenguaje administrativo.
Por ejemplo, en lugar de "SQLException: ORA-02291: integrity constraint violated", debe mostrarse "El departamento seleccionado no existe en el registro activo. Verifique el código de unidad orgánica". Esta capa de abstracción de errores se implementa mediante ExceptionHandler en JSF o ErrorBoundary en Blazor, y constituye un requisito explícito en la mayoría de los pliegos de cláusulas técnicas.
Cuarto Principio: Personalización por perfiles
La personalización por perfiles resulta esencial en plataformas multi-rol. Un técnico de recursos humanos necesita ver campos de retribución que deben estar ocultos para un usuario de archivo. En JSF, esto se gestiona con rendered condicional y security bindings; en ASP.NET Core, con Tag Helpers y view components que evalúan políticas de autorización. La complejidad surge cuando los permisos son dinámicos y dependen del contexto de la sesión (ej: un interventor que actúa como sustituto). En estos casos, la implementación de factories de componentes que reciben el SecurityContext en tiempo de construcción ofrece mayor flexibilia que las evaluaciones estáticas.
Quinto Principio: Principio de resilencia
Por último, el principio de resiliencia a fallos en UI implica diseñar componentes que sigan funcionando parcialmente cuando un servicio dependiente falla. Un dashboard de indicadores no debe dejar de mostrar el 90% de datos disponibles porque un microservicio secundario responde con timeout. Los circuit breakers de Polly en .NET o la anotación @Asynchronous con futures en Java permiten implementar degradación elegante. Probar este comportamiento mediante chaos engineering en entornos de preproducción-inyectando latencia o errores 503-es una práctica que demuestra madurez profesional y que resulta muy valorada en las defensas de oposiciones.
<!-- Ejemplo de componente JSF con degradación elegante -->
<h:panelGroup id="panelIndicadores">
<h:panelGroup rendered="#{indicadoresBean.servicioDisponible}">
<p:graphicImage value="#{indicadoresBean.grafico}" cache="false"/>
</h:panelGroup>
<h:panelGroup rendered="#{not indicadoresBean.servicioDisponible}" styleClass="alerta-degradada">
<p:messages severity="warn"/>
<p>Datos parciales mostrados. Servicio de estadísticas temporalmente no
disponible.</p>
</h:panelGroup>
</h:panelGroup>
7.8. Accesibilidad y estándares WCAG 2.2 en aplicaciones públicas
públicas
La accesibilidad digital ya no es una opción ética sino un mandato legal: el Real Decreto 1112/2018, transposición de la Directiva (UE) 2016/2102, exige que todos los sitios web y aplicaciones móviles del sector público cumplan con los niveles A y AA de WCAG 2.1, con la actualización a WCAG 2.2 en proceso de aprobación para 2025. Esto implica que cada componente de interfaz debe validarse no solo funcionalmente sino mediante herramientas automáticas (axe DevTools, WAVE) y tests manuales con lectores de pantalla (NVDA, JAWS). La experiencia en el Portal de la Transparencia del Gobierno de Cantabria demostró que el 80% de las no conformidades se detectan en fases tempranas cuando los desarrolladores usan extensiones de accesibilidad en sus IDEs.
WCAG 2.2 introduce nuevos criterios cruciales como 2.4.11 Focus Not Obscured (nivel AA), que prohíbe que el indicador de foco sea ocultado por elementos flotantes, y 3.3.8 Accessible Authentication (nivel AA), que limita el uso de captcha sin alternativas accesibles. En JSF, el manejo del foco se controla con p:focus o f:ajax con execute="@this", pero el rendering dinámico puede mover el foco de forma impredecible, requiriendo scripts de polyfill con setTimeout para restaurarlo. Blazor, con su modelo de server-side rendering, mantiene el foco de forma más fiable gracias a su sistema de prerendering y hydration, aunque los latency issues pueden generar focus jumps que afectan a usuarios con motricidad reducida.
Los controles de formulario requieren atención especial. Cada <h:inputText> o <InputText> debe estar asociado a un <h:outputLabel> o <label> con for explícito, y los mensajes de error deben vincularse mediante aria-describedby. En JSF, el atributo for se genera automáticamente pero el ID puede ser impredecible en componentes compuestos, necesitando prependId="false" o h:form id="formulario" estáticos. En ASP.NET Core, el tag helper asp-for genera automáticamente el id y name correctos, reduciendo el error humano. Un patrón comprobado es crear templates de componentes con accesibilidad built-in que los desarrolladores deben usar obligatoriamente, en lugar de permitir controles raw.
La gestión de timeouts de sesión impacta directamente en la accesibilidad. Un usuario con discapacidad cognitiva o motora puede necesitar más tiempo para completar un formulario. WCAG (criterio 2.2.1, Tiempo ajustable) requiere advertir antes de que expire el tiempo y dar al menos 20 segundos para ampliarlo con una acción simple, pudiendo ampliarse al menos diez veces. En JSF, esto se implementa con <p:idleMonitor> combinado con p:dialog modal, mientras que en Blazor se usa ProtectedBrowserStorage para guardar estado localmente y un timer en JavaScript interop.
Críticamente, el mecanismo debe funcionar sin JavaScript como fallback, lo que complica la implementación en Blazor Server donde el WebSocket es obligatorio.
<!-- Ejemplo de timeout accesible en Blazor -->
<div aria-live="polite" id="timeout-alerta" role="alert">
@if (mostrarAdvertencia)
{
<div class="alerta-timeout">
<p>Su sesión expira en @tiempoRestante segundos.
<button @onclick="ExtenderSesion" aria-label="Extender sesión 10 minutos">
Extender tiempo
</button></p>
</div>
}
</div>
@code {
private bool mostrarAdvertencia = false;
private int tiempoRestante = 60;
private async Task ExtenderSesion()
{
await JS.InvokeVoidAsync("extenderTimeout");
auditoria.LogExtension(User.Identity.Name);
}
}
La navegación por secuencia lógica (criterio 2.4.3) obliga a que el orden de tabindex siga el flujo visual y semántico. Los frameworks modernos cometen el error de usar tabindex="0" indiscriminadamente, pero en asistentes de varios pasos el orden debe ser programático: al validar un paso y mostrar el siguiente, el foco debe moverse al encabezado H2 del nuevo contenido. Este comportamiento no es automático y requiere h:outputScript o IJSRuntime para invocar focus() en el elemento correcto.
Documentar estas decisiones en el Accessibility Statement del proyecto es un requisito legal que pocos desarrolladores incluyen en la entrega.
Finalmente, los tests automatizados de accesibilidad deben integrarse en el pipeline CI/CD. Herramientas como Pa11y o Accessibility Insights se pueden ejecutar en contenedores Docker contra entornos de preproducción, fallando el build si se detectan violaciones nivel A. En Azure DevOps, existe una task oficial para Accessibility Insights; en Jenkins con Maven, se usa el plugin accessibility-check-plugin.
Nota normativa
El Comité Europeo de Normalización (CEN) publicó en 2024 el informe CEN/CENELEC 17249, que establece que el incumplimiento de WCAG 2.2 en sistemas públicos puede derivar en responsabilidad patrimonial de la administración. No es solo cuestión de calidad técnica, sino de riesgo jurídico.
Incluir en la documentación técnica un checklist de cumplimiento firmado por el responsable del proyecto es una práctica que distingue proyectos maduros.
Marco normativo en Administraciones Públicas
En el ámbito público, la accesibilidad suele exigirse mediante la norma EN 301 549 (y su trasposición en pliegos y guías internas), que referencia los criterios WCAG como base técnica; por ello, la versión concreta aplicable (2.1/2.2) puede variar según la fecha de contratación, actualizaciones normativas y lo que establezca el pliego o la política del organismo.
8. Comparativa e interoperabilidad
La transición de Java EE a Jakarta EE representa una redefinición estratégica de la plataforma empresarial de Java con implicaciones directas en la modernización de sistemas públicos. El traspaso de Oracle a la Eclipse Foundation en 2017 inauguró un proceso de gobernanza abierta que condiciona la sostenibilidad técnica y normativa de las decisiones de mantenimiento. Las administraciones que operan con sistemas heredados en Java EE 7 u 8 no enfrentan una obsolescencia forzosa, sino una ventana de oportunidad para planificar migraciones graduadas que preserven la inversión pública mientras se adoptan arquitecturas cloud-native. Este epígrafe aborda esa dualidad: la interoperabilidad con el legado y la operación del presente.
El dominio de Jakarta EE permite diagnosticar incidencias en aplicaciones que conviven en distintos perfiles (Platform, Web, Core) sin necesidad de desplegar el código fuente, configurar contenedores que gestionen transacciones JTA distribuidas entre microservicios, y fundamentar ante un comité de seguridad por qué una determinada configuración de Identity Store cumple el Esquema Nacional de Seguridad. No se trata de memorizar especificaciones, sino de interpretar cómo cada anotación, descriptor o patrón de inyección responde a exigencias normativas como la trazabilidad de accesos (Ley 40/2015), la minimización de superficie de ataque (ENS) o la interoperabilidad obligatoria (ENI).
La experiencia en la Administración General del Estado demuestra que el 60% de los incidentes críticos en producción derivan de una configuración incorrecta de contenedores, no de errores en la lógica de negocio.
Los subepígrafes siguientes estructuran este conocimiento en un recorrido lógico: desde la evolución histórica que contextualiza el cambio de gobernanza, pasando por los perfiles que definen el despliegue en entornos cloud, los contenedores que orquestan dependencias y transacciones, los servicios empresariales que garantizan integridad y seguridad, las estrategias de persistencia que optimizan recursos, las APIs REST que exponen datos públicos con criterios de protección, hasta el modelo de seguridad completo que audita cada operación. Esta progresión refleja el pipeline de decisiones que debe recorrerse al desplegar cualquier componente en un sistema crítico, desde la elección del perfil hasta la configuración del último interceptor de auditoría.
Esta unidad se centra en la aplicación práctica más allá de las definiciones teóricas. La capacidad de razonar ante un caso concreto -por qué un microservicio de consulta de ayudas del SEPE debe usar Core Profile con MicroProfile Metrics en lugar de Platform, o cómo configurar JNDI en Kubernetes para evitar que un despliegue en preproducción acceda accidentalmente a la base de datos de producción- requiere entender que Jakarta EE es un estándar vivo, que responde a presiones reales de presupuesto, seguridad y eficiencia en el sector público. La justificación técnica y normativa de cada decisión de configuración constituye el criterio profesional que garantiza la operación de infraestructuras que soportan la gestión de millones de ciudadanos.
La realidad presupuestaria del sector público post-2008 añade una dimensión de austeridad que tecnicismos abstractos no capturan. La escasez de personal especializado, la necesidad de formación continua y el coste de oportunidad de cada decisión transforman la elección entre Jakarta EE y .NET en una cuestión de gestión de talento. Una administración que ha invertido años en formar expertos Java no puede desechar ese capital humano por una moda tecnológica, pero tampoco puede ignorar que el 70% de las nuevas contrataciones de personal técnico en la AGE dominan .NET por su curva de aprendizaje más accesible.
Por eso, dominar ambas plataformas y entender cómo hacerlas convivir no es ventaja competitiva, sino requisito operativo para gestionar el patrimonio informático público de forma responsable.
8.1. Análisis comparativo matricial: Jakarta EE 11 vs .NET 8
La selección de una plataforma tecnológica en el ámbito de la administración pública no responde únicamente a criterios técnicos, sino que está condicionada por factores como el coste a largo plazo, la gobernanza, la sostenibilidad de la solución y el cumplimiento del marco normativo español e internacional. En este contexto, la comparación entre Jakarta EE 11 y .NET 8 requiere un análisis que trascienda el rendimiento punta a punta y considere aspectos estratégicos como la interoperabilidad, la evolución del ciclo de vida o la dependencia de proveedores únicos.
Ambas plataformas han madurado hasta ofrecer capacidades equivalentes en términos de productividad, pero sus filosofías de diseño, modelos de licenciamiento y estrategias de modernización difieren sustancialmente, lo que impacta directamente en la planificación de sistemas críticos para la gestión pública.
Arquitectura
Desde la perspectiva arquitectónica, Jakarta EE 11 mantiene su apuesta por los estándares abiertos y la portabilidad entre múltiples implementaciones (WildFly, GlassFish, TomEE, etc.), mientras que .NET 8, aunque multiplataforma, está fuertemente ligado al ecosistema Microsoft y sus decisiones de roadmap.
Esta diferencia no es trivial: en una administración que busca evitar el vendor lock-in y cumplir con la Guía de Contratación Pública de Software Libre del Ministerio de Asuntos Económicos y Transformación Digital, Jakarta EE presenta ventajas claras. Sin embargo, .NET 8 ofrece una curva de aprendizaje menos pronunciada para equipos familiarizados con el entorno Visual Studio y cuenta con herramientas de productividad que, en algunos casos, acortan los plazos de desarrollo.
Rendimiento
En el ámbito del rendimiento, ambas plataformas han alcanzado madurez similar. .NET 8 destaca en escenarios de computación nativa (Native AOT) y latencia ultra-baja, mientras que Jakarta EE 11, con la adopción de Virtual Threads (Project Loom) y optimizaciones en el GC, ha reducido drásticamente la brecha histórica. La elección dependerá del perfil de carga: sistemas con alta concurrencia I/O-bound pueden beneficiarse del modelo reactivo de Jakarta, mientras que aplicaciones CPU-intensive y microservicios extremadamente ligeros favorecen .NET 8.
Seguridad
La seguridad es otro eje crítico. Jakarta EE 11 integra Jakarta Security 3.0 con OAuth 2.1 y OpenID Connect de forma nativa, alineándose con el Esquema Nacional de Seguridad (ENS) de España. .NET 8, por su parte, fortalece ASP.NET Core Identity con soporte para FIDO2 y mejora la gestión de secretos mediante Azure Key Vault integration. Ambas plataformas cumplen con los requisitos de trazabilidad y auditoría del Reglamento General de Protección de Datos (RGPD), aunque la implementación en Jakarta requiere configuración más explícita, mientras que .NET ofrece más automatismos.
Comunidad y soporte
La comunidad y el soporte a largo plazo presentan perfiles divergentes. Jakarta EE depende de la Jakarta EE Working Group (bajo la governance de la Eclipse Foundation), con participación de múltiples actores industriales, lo que garantiza continuidad independiente de cualquier empresa. .NET 8, como LTS, recibe soporte oficial de Microsoft hasta noviembre de 2026, pero su roadmap está centralizado.
Para una administración pública, esto implica que la decisión debe ponderar entre la gobernanza abierta de Jakarta y el soporte empresarial garantizado de Microsoft.
Nota técnica:
La interoperabilidad semántica entre plataformas se fundamenta en el principio de Postel's Law o "ley de la robustez": "Sé conservador en lo que envías, sé liberal en lo que aceptas". Este principio, clave en la integración de sistemas heterogéneos, debe guiar el diseño de APIs comunes entre Jakarta EE y .NET para garantizar la extensibilidad futura sin romper contratos.
8.2. Caso de estudio: Sistema de Gestión de Recursos Humanos
Humanos
El diseño de un Sistema de Gestión de Recursos Humanos (SGRH) para una consejería regional ilustra las implicaciones prácticas de la elección arquitectónica. Este sistema debe integrarse con el Registro Central de Personal, la nómina presupuestaria (PRAE), y el directorio activo corporativo, además de ofrecer una interfaz web accesible y una API REST para la oficina digital. La complejidad radica en la necesidad de cumplir con la Ley 40/2015 de Régimen Jurídico del Sector Público, el Esquema Nacional de Interoperabilidad (ENI) y el ENS, lo que impone requisitos de trazabilidad, firma electrónica y protección de datos especialmente sensibles.
Jakarta EE 11
Si optamos por Jakarta EE 11, la arquitectura se apoyaría en MicroProfile 6.1 para la gestión de configuración y health checks, mientras que Jakarta Persistence 3.2 gestionaría el acceso a la base de datos relacional (PostgreSQL con encriptado TDE). El componente de autenticación se implementaría con Jakarta Security 3.0, integrando el sistema de firma electrónica del Ministerio de Hacienda mediante un proveedor de identidad SAML 2.0. La ventaja principal es la portabilidad: el despliegue podría realizarse en JBoss EAP (Red Hat) o Payara Server, manteniendo la independencia del proveedor cloud.
.NET 8
En el escenario .NET 8, la solución se basaría en ASP.NET Core Identity con MFA obligatorio, Entity Framework Core 8 para el acceso a datos, y ASP.NET Core Web API para la capa de servicios. La integración con Azure Active Directory (ahora Entra ID) sería nativa, facilitando la sincronización con el directorio corporativo. El desarrollo se beneficiaría de Visual Studio y Azure DevOps, pero generaría dependencia del ecosistema Microsoft. Para el módulo de nómina, se implementaría un microservicio aislado con Native AOT para minimizar la latencia en cálculos complejos.
Decisión arquitectónica
La decisión arquitectónica debe considerar el nivel de especialización del equipo. En muchas administraciones, el personal técnico domina Java desde sus orígenes en sistemas legados, lo que reduce el riesgo de la curva de aprendizaje. Sin embargo, si el equipo ya gestiona infraestructuras Microsoft (SharePoint, Azure, Windows Server), la integración operativa de .NET 8 puede justificar su selección. La clave está en realizar una proof of concept (POC) con ambas plataformas, midiendo no solo el rendimiento, sino la complejidad de integración con los sistemas existentes.
Un aspecto crítico es la gestión de documentación y formación. El SGRH requiere extensa documentación técnica para futuras ampliaciones y auditorías. Jakarta EE, al basarse en estándares, facilita la documentación mediante anotaciones y especificaciones formales, mientras que .NET 8 ofrece herramientas como Swagger/Swashbuckle con mayor automatización. La elección debe equilibrar la calidad del código autodocumentado con la necesidad de cumplir los estándares de documentación del ENI.
Tabla comparativa de componentes para SGRH:
Capa Jakarta EE 11 .NET 8 Criterio de decisión pública
Seguridad Jakarta Security 3.0 + SAML ASP.NET Core Identity +
Entra ID
Siembra preferencia por estándares abiertos (Jakarta)
Persistencia EclipseLink/PostgreSQL EF Core 8/SQL Server Coste licencia vs. independencia vendor
API REST Jakarta REST 3.1 + MP
OpenAPI
ASP.NET Core Web API
- Swagger Equivalencia funcional
Monitoreo MicroProfile Metrics Application Insights Evitar vendor lock-in (MP) Despliegue JBoss EAP / Payara Azure App Service /
AKS Política cloud de la organización
8.3. Patrones de integración entre plataformas heterogéneas
La coexistencia de sistemas Jakarta EE y .NET en el mismo ecosistema empresarial público no es una excepción, sino la norma. Las administraciones heredan décadas de desarrollo en Java, mientras que nuevos proyectos pueden surgir con .NET por alianzas estratégicas o convenios marco. Por ello, dominar patrones de integración heterogénea es competencia esencial para el técnico auxiliar informático.
El patrón más robusto es el API Gateway, que actúa como único punto de entrada, enrutando tráfico según el path o headers, y permitiendo que cada plataforma evolucione independientemente sin romper contratos con consumidores.
Otro patrón fundamental es el Message Broker con colas duraderas (AMQP 1.0). Implementaciones como RabbitMQ o Azure Service Bus permiten desacoplar sistemas mediante eventos asíncronos. Por ejemplo, cuando el sistema de nómina .NET genera un alta de empleado, publica un evento EmployeeCreated que el sistema de formación Jakarta EE consume para iniciar el plan de inducción. Este patrón, basado en arquitectura orientada a eventos (EDA), cumple con el principio de disponibilidad del ENI y permite implementar el patrón Saga para transacciones distribuidas sin bloqueos.
El Data Virtualization Layer es estratégico en entornos donde la consolidación física de bases de datos es inviable por razones legales o de rendimiento. Usando herramientas como Teiid (Red Hat) o Azure Synapse, se crea una capa de abstracción que expone datos de SQL Server (gestionado por .NET) y PostgreSQL (gestionado por Jakarta) mediante un único endpoint OData. Esto permite a aplicaciones de business intelligence acceder a información transversal sin replicar datos, cumpliendo con el principio de unicidad de datos del ENI.
La Autenticación Federada mediante OpenID Connect es probablemente el patrón más crítico. Ambas plataformas deben confiar en un proveedor de identidad común (Keycloak, Azure AD, o incluso el Sistema Cl@ve). Cada microservicio valida tokens JWT sin importar su stack tecnológico, centralizando la gestión de sesiones y eliminando la necesidad de integraciones puntuales. La configuración debe incluir token binding y proof-of-possession para cumplir con el ENS alto nivel.
Finalmente, el Sidecar Pattern con containers permite empaquetar lógica de integración (logging, seguridad, circuit breaker) en un contenedor auxiliar que se despliega junto a la aplicación principal.
Esta técnica, popularizada por Istio, permite que un microservicio Jakarta EE y otro .NET compartan políticas de seguridad uniformes sin modificar el código fuente, facilitando el cumplimiento del ENS y reduciendo el coste de mantenimiento.
Recuerda
La integración heterogénea debe seguir el Shared Kernel del
Domain-Driven Design (DDD) para los modelos comunes (empleado, unidad orgánica), pero mantener Bounded Contexts independientes. Esta dualidad evita el Big Ball of Mud sin caer en sobreingeniería.
8.4. Despliegue en contenedores Docker y Kubernetes
El despliegue en contenedores no es una moda: en las administraciones públicas es casi un estándar cuando buscas que tu aplicación corra igual en desarrollo, preproducción y producción sin sorpresas.
Docker y similares te dan esa caja hermética donde metes la aplicación y todo lo que necesita, pero lo crucial para el examen no es dominar Dockerfiles al milímetro, sino entender el modelo mental: la imagen es tu paquete estático (código + dependencias), y el contenedor es la ejecución aislada donde inyectas la configuración específica de cada entorno. No la metas en el binario, no hardcodees URLs de bases de datos ni contraseñas. Usa variables de entorno, monta ficheros de configuración externos, y gestiona los secretos como lo que son: material sensible que no debe versionarse.
En la construcción de imágenes, piensa siempre en reducir riesgos. No uses etiquetas latest en producción (es pedir un disgusto), apóyate en imágenes oficiales o de proveedores consolidados, y limpia lo que no necesites: cuanto menos software haya dentro, menos vulnerabilidades heredas. La técnica de multi-stage es un truco que merece la pena conocer: separas la fase de compilación (con todos los SDK pesados) de la de ejecución (solo el runtime ligero). Por qué es buena idea: porque la imagen final es más pequeña y segura.
Si Docker es el contenedor, Kubernetes es el orquestador que te automatiza la vida: despliegues, escalado, reinicios si algo falla, actualizaciones controladas. En OTAI te pueden caer preguntas sobre ConfigMaps (para configuración no sensible) y Secrets (para lo sensible, aunque sepas que en entornos realmente críticos se usa un gestor externo como Vault). Y, sobre todo, las sondas de salud:
-
Liveness probe: "¿Está vivo el proceso o se ha colgado?". Si falla, Kubernetes lo mata y levanta uno nuevo.
-
Readiness probe: "¿Ya puedo mandarle tráfico?". Útil cuando tu app tarda en arrancar porque carga caches o conecta con servicios externos. No la des como buena hasta que esté lista.
-
Startup probe: La más reciente. Protege arranques largos evitando que Kubernetes reinicie el contenedor antes de que termine de inicializarse.
En seguridad, el OTAI premia sentido común: ejecuta con usuario no privilegiado (nunca como root), expone solo los puertos estrictamente necesarios, y aplica el principio de mínimo privilegio en las comunicaciones. En entornos regulados como el ENS, un despliegue debe ser reproducible, trazable y sin toques manuales en producción: si tienes que entrar a hacer cambios a mano en caliente, algo falla en tu pipeline.
Ejemplo de Dockerfile optimizado para Jakarta EE:
Stage 1: BuildFROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests
Stage 2: RuntimeFROM quay.io/wildfly/wildfly-runtime:31.0-jdk21
COPY --from=build /app/target/recursoshumanos.war
/opt/wildfly/standalone/deployments/
ENV MP_CONFIG_PROFILE=prod
EXPOSE 8080
8.5. Costes, licenciamiento y TCO en entornos públicos
El análisis del Total Cost of Ownership (TCO) en la administración pública española trasciende el coste de licencias para incluir gastos de auditoría, certificación de seguridad, formación del personal y costes de migración.
Jakarta EE 11 presenta una ventaja inicial indiscutible: el coste de licencia es cero. Implementaciones como WildFly, Payara Community o TomEE son open source sin costes de uso. Sin embargo, esto no implica gratuidad total: el soporte empresarial (Red Hat JBoss EAP, Payara Enterprise) oscila entre 5.000-15.000 € por servidor/año, y la formación en MicroProfile o Jakarta Security requiere inversión en cursos especializados. Además, la necesidad de personal con alto nivel de especialización puede incrementar el coste salarial medio del equipo.
.NET 8, si bien es open source bajo la licencia MIT, está optimizado para ejecutarse en Windows Server o Azure, lo que introduce costes implícitos. Windows Server 2022 Standard cuesta aproximadamente 1.100 € por procesador, y SQL Server (si se usa) puede superar los 7.000 € por core en edición Enterprise. Aunque es posible ejecutar .NET 8 en Linux (CentOS, Ubuntu) con PostgreSQL, la integración con servicios corporativos de Microsoft genera presión hacia el ecosistema cerrado. Azure DevOps, por ejemplo, cuesta 6 €/usuario/mes, mientras que GitLab CE (open source) es gratuito.
El coste de migración desde sistemas monolíticos es el factor más oneroso y menos visible. Una estimación conservadora para un sistema mediano (50.000 líneas de código) sitúa el esfuerzo en 18-24 meses para Jakarta EE y 12-18 meses para .NET, siempre que el equipo domine la plataforma destino.
Esta diferencia se debe a la mayor cantidad de decisiones arquitectónicas (elección de implementación, configuración de contenedor) en Jakarta. Sin embargo, la migración a .NET implica riesgo de vendor lock-in, que el ENI desaconseja expresamente.
Los costes de certificación ENS también deben considerarse. Jakarta EE requiere auditoría manual de configuraciones de seguridad (roles, realms, políticas de acceso), mientras que .NET ofrece herramientas automatizadas como Microsoft Defender for Cloud que aceleran el proceso, pero con coste adicional (15 €/servidor/mes). Para un entorno con 50 servidores, esto supone 9.000 €/año.
Cálculo TCO simplificado para 5 años (100 servidores):
-
Jakarta EE + Linux + PostgreSQL: Soporte enterprise (8.000 €/servidor/año) + formación (50.000 €) + certificación ENS (30.000 €) = 4.080.000 €
-
.NET + Windows + SQL Server: Licencias (9.000 €/servidor) + Azure DevOps (6.000 €/año) + certificación automatizada (45.000 €) = 5.175.000 €
8.6. Estrategias de migración de sistemas monolíticos
La migración de un monolito legado (típicamente en Java EE 6 o .NET Framework 4.x) no debe abordarse como una reescritura total, sino como un proceso estratégico de refactoring dirigido por capacidades. El patrón Strangler Fig es el más indicado: se despliega un proxy (API Gateway) que dirige tráfico progresivamente desde el monolito hacia nuevos microservicios. Por ejemplo, en el SGRH, el módulo de consulta de nóminas (funcionalidad de lectura intensiva) se extrae primero, ya que su impacto es menor y permite validar la nueva arquitectura sin riesgo de corrupción de datos.
El primer paso técnico consiste en identificar contextos delimitados mediante Domain Storytelling con los usuarios finales. Se documentan los flujos reales (ej: "El gestor solicita permiso, el supervisor aprueba, RRHH registra") y se mapean a capacidades independientes. Esta fase, previa a cualquier código, dura 4-6 semanas pero evita errores costosos de arquitectura. Posteriormente, se implementa un anti-corruption layer (ACL) que traduce entre el modelo desnormalizado del monolito y los modelos ricos de los microservicios.
La estrategia de database per service es obligada pero peligrosa. No se puede permitir que múltiples servicios accedan directamente a la misma tabla del monolito. La solución pasa por database refactoring: crear vistas materializadas replicadas para cada servicio, que se sincronizan mediante Change Data Capture (Debezium para PostgreSQL, SQL Server CDC). Así, el servicio de vacaciones accede a su réplica de la tabla empleados sin bloquear el monolito, y los cambios se propagan eventualmente consistentes.
La selección del primer microservicio es crítica. Debe ser:
-
De baja complejidad: Evita lógica transaccional distribuida.
-
De alto valor: Impacto visible para los usuarios (mejora UX).
-
De bajo riesgo: No afecte a cálculos críticos (nóminas, pensiones). Un candidato ideal es el catálogo de cursos de formación: lectura intensiva, bajo volumen de escritura, y con modelos de dominio simples.
Plan de migración faseada (24 meses):
Fase 1 (Meses 1-6): Descubrimiento y preparación:
-
Análisis de arquitectura actual (C4 Model).
-
Implementación API Gateway (Kong/Ocelot).
-
Pipeline CI/CD común (GitLab CI).
Fase 2 (Meses 7-12): Primer microservicio y validación:
-
Despliegue catálogo de cursos (Jakarta EE o .NET).
-
ACL y CDC configurados.
-
Métricas de observabilidad (Prometheus/Grafana).
Fase 3 (Meses 13-18): Extracción de servicios core:
-
Gestión de vacaciones.
-
Evaluación del desempeño.
-
Migración gradual de usuarios (feature flags).
Fase 4 (Meses 19-24): Desmantelamiento del monolito:
-
Últimos módulos (nómina, contratación).
-
Auditoría y documentación ENS final.
-
Formación y transferencia a equipo de mantenimiento.
📌 Lo esencial para el examen
- El operador == compara referencias y equals() compara contenido; si se sobrescribe equals, hay que sobrescribir hashCode.
- Un int ocupa 32 bits y un double 64 bits (IEEE 754); BigDecimal evita errores de redondeo en cálculos presupuestarios.
- El bytecode lo genera javac y la JVM lo ejecuta, normalmente con compilación JIT; Java 8 usa la versión 52.0 y Java 11 la 55.0.
- CMS (Concurrent Mark-Sweep) fue eliminado en Java 14.
- Desde Java 10, var es un identificador reservado, no una palabra reservada; goto y const están reservadas pero no se usan.
- Oracle cedió Java EE a la Eclipse Foundation en 2017 y la plataforma pasó a llamarse Jakarta.
- Jakarta EE 9 (2020) migró el espacio de nombres de javax a jakarta sin añadir capacidades nuevas.
- Perfiles de Jakarta EE: Platform, Web y Core.
- .NET 8 es LTS con soporte hasta noviembre de 2026; .NET Framework está en mantenimiento desde 2022.
- CLR (Common Language Runtime) es el núcleo de ejecución de .NET y la BCL (Base Class Library) su conjunto fundamental de tipos.
- El Real Decreto 1112/2018 exige los niveles A y AA de WCAG 2.1 en sitios web y aplicaciones móviles del sector público.
- Postel's Law (ley de la robustez): sé conservador en lo que envías y liberal en lo que aceptas.
De un vistazo
| Versión | Año | Qué aporta |
|---|---|---|
| Jakarta EE 9 | 2020 | cambio de espacio de nombres a jakarta |
| Jakarta EE 10 | perfil Core |