====== 05 - Control de Errores y Configuración ====== ===== 1. Gestión de Excepciones y Control de Flujo ===== Las excepciones son eventos anómalos que ocurren durante la ejecución de un programa y rompen el flujo normal de las instrucciones. Java proporciona un mecanismo orientado a objetos para capturar y gestionar estos errores de forma controlada. ==== 1.1 Jerarquía de Excepciones ==== Todas las excepciones derivan de la clase base ''java.lang.Throwable'', que se divide en dos ramas principales: @startuml class Object class Throwable class Error class Exception class RuntimeException Object <|-- Throwable Throwable <|-- Error Throwable <|-- Exception Exception <|-- RuntimeException @enduml * ''Error'': Fallos graves e irrecuperables a nivel de entorno o Máquina Virtual (ej. ''OutOfMemoryError'', ''StackOverflowError''). La aplicación no debe intentar capturarlos.\\ \\ * ''Exception'': Errores que la aplicación puede (y debe) gestionar:\\ \\ * **Excepciones Verificadas** (''Checked Exceptions''): Se verifican en tiempo de compilación. El compilador **obliga a gestionarlas** explícitamente mediante ''try-catch'' o a declararlas en la firma del método con ''throws'' (ej. ''IOException'', ''FileNotFoundException''). **Heredan de ''Exception''**\\ \\ * **Excepciones No Verificadas** (''Unchecked'' / ''RuntimeExceptions''): Ocurren en tiempo de ejecución y **heredan de ''RuntimeException''**. Suelen indicar errores de programación o lógica (ej. ''NullPointerException'', ''IllegalArgumentException''). ==== 1.2 Tratamiento de Excepciones ==== La estrategia para gestionar una excepción varía fundamentalmente según si se trata de una excepción verificada (''Checked'') o no verificada (''Unchecked''). === Tratamiento de Checked Exceptions: La regla "Catch or Declare" === Dado que el compilador verifica la presencia de estas excepciones, la sintaxis de Java obliga a tomar una de las siguientes dos decisiones ante cualquier instrucción que pueda lanzarlas: * **Capturar la excepción** (''try-catch''): El método asume la responsabilidad de gestionar el fallo localmente en el momento en que ocurre.\\ \\ * **Propagar la excepción** (''throws''): El método delega la responsabilidad al nivel superior en la cadena de llamadas, indicando en su firma que puede lanzar dicha excepción. public class GestorConfiguracion { // OPCIÓN A: Captura local mediante try-catch public void cargarDesdeFicheroLocal() { try { // FileReader lanza FileNotFoundException (Checked Exception) FileReader reader = new FileReader("config.json"); } catch (FileNotFoundException e) { IO.println("Fichero no encontrado. Cargando valores por defecto..."); } } // OPCIÓN B: Propagación hacia el llamador mediante 'throws' public void cargarDesdeFicheroDelegado() throws FileNotFoundException { // Se traslada la obligación de tratamiento al método que invoque a 'cargarDesdeFicheroDelegado' FileReader reader = new FileReader("config.json"); } } === Tratamiento de Unchecked Exceptions: Programación Defensiva === Al derivar de ''RuntimeException'', el compilador no exige el uso de ''try-catch'' ni la declaración con ''throws''. Aunque la sintaxis permite capturarlas, la buena práctica establece que las Unchecked Exceptions no deben enmascararse con ''try-catch'', sino prevenirse mediante validaciones previas (programación defensiva). // MALA PRÁCTICA: Usar try-catch para tapar un error de lógica del programador public void mostrarLongitudIncorrecto(String texto) { try { IO.println("Longitud: " + texto.length()); // Si texto es null, lanza NullPointerException } catch (NullPointerException e) { IO.println("Error: La cadena recibida era nula."); } } // BUENA PRÁCTICA: Programación defensiva (validación explícita de condiciones) public void mostrarLongitudCorrecto(String texto) { if (texto != null) { IO.println("Longitud: " + texto.length()); } else { IO.println("Aviso: No se puede procesar una cadena nula."); } } === Resumen de Estrategias de Tratamiento === ^ Tipo de Excepción ^ Control del Compilador ^ Estrategia Recomendada ^ | Checked Exception | Obligatorio | Usar ''try-catch'' para recuperarse localmente o ''throws'' para delegar la gestión a capas superiores. | | Unchecked Exception | Opcional | Evitar su aparición mediante validaciones preliminares (''if''). Evitar el abuso de ''try-catch''. | En el desarrollo de backend moderno, la mayoría de los frameworks (como Spring Boot) disponen de manejadores globales de excepciones (por ejemplo, mediante la anotación ''@ControllerAdvice'').\\ \\ Por este motivo, la arquitectura moderna favorece crear excepciones personalizadas que hereden de ''RuntimeException'' (Unchecked). De este modo, evitamos llenar las clases de negocio y servicios con bloques ''try-catch'' repetitivos o firmas de métodos sobrecargadas con ''throws''. La excepción se interrumpe de forma limpia, //vuela// hasta el manejador global, y este se encarga automáticamente de capturarla, registrar el log y devolver la respuesta HTTP adecuada (por ejemplo, un código //400 Bad Request// o //404 Not Found// en formato JSON) al cliente web. ==== 1.3 Sintaxis y Estructura: try, catch, finally y try-with-resources ==== Para capturar y gestionar las excepciones en tiempo de ejecución, Java proporciona una estructura de bloques bien definida. === El bloque clásico: try - catch - finally === * ''try'': Delimita el bloque de código que contiene operaciones susceptibles de lanzar una excepción. Si ocurre un fallo, la ejecución del bloque se interrumpe inmediatamente en la línea del error.\\ \\ * ''catch'': Captura la excepción producida en el bloque ''try''. Se pueden encadenar múltiples bloques ''catch'' para gestionar distintos tipos de excepciones de forma específica. Regla de orden: deben colocarse siempre desde la excepción más específica (hija) hasta la más genérica (padre).\\ \\ * ''finally'': Bloque de ejecución garantizada. El código dentro de ''finally'' se ejecutará siempre, independientemente de si se lanzó una excepción, si esta fue capturada o si el bloque ''try'' ejecutó un ''return''. Históricamente se utilizaba para liberar recursos críticos (cerrar ficheros, desconectar bases de datos o liberar sockets). public class GestionTradicional { public void leerFicheroClasico() { FileReader reader = null; try { reader = new FileReader("datos.txt"); int caracter = reader.read(); IO.println("Primer carácter: " + (char) caracter); } catch (IOException e) { // Captura de errores de Entrada/Salida IO.println("Error al procesar el archivo: " + e.getMessage()); } finally { // Limpieza manual obligatoria: Garantizamos el cierre del recurso if (reader != null) { try { reader.close(); } catch (IOException e) { IO.println("Error al cerrar el lector."); } } IO.println("Operación finalizada (bloque finally ejecutado)."); } } } === Simplificación moderna: try-with-resources (Java 7+) === La gestión manual de cierre de recursos en el bloque ''finally'' genera un código redundante, propenso a errores y difícil de leer. Para solucionarlo, Java introduce el bloque ''try-with-resources''. Cualquier clase que implemente la interfaz ''java.lang.AutoCloseable'' (o ''java.io.Closeable'') puede instanciarse dentro de los paréntesis del ''try''. Java se encargará de invocar automáticamente su método ''close()'' al finalizar el bloque, se haya producido una excepción o no. ==== 1.4 Lanzamiento de Excepciones y Excepciones Personalizadas ==== Además de capturar las excepciones lanzadas por el propio API de Java, podemos provocar nuestras propias excepciones o crear tipos de error específicos para nuestro dominio de negocio. === Las palabras clave throw y throws === * ''throw'' (Acción): Se utiliza dentro del cuerpo de un método para lanzar explícitamente una instancia de excepción (''throw new MiExcepcion("Detalle...");''). Detiene inmediatamente la ejecución normal del método.\\ \\ * ''throws'' (Declaración): Se añade en la firma del método para avisar a los invocadores de que este método puede propagar una ''Checked Exception'' que no ha sido capturada internamente. === Excepciones Personalizadas === Crear clases de excepción propias permite dar nombres significativos a los fallos de nuestra aplicación (ej. ''UsuarioNoEncontradoException'', ''SaldoInsuficienteException''). Para crearlas, basta con extender de ''Exception'' (si queremos que sea verificada) o de ''RuntimeException'' (si queremos que sea no verificada, opción recomendada en arquitectura moderna). public class ConfigurationException extends RuntimeException { // Constructor básico con mensaje public ConfigurationException(String mensaje) { super(mensaje); } // Constructor con mensaje y causa (Exception Chaining) // Permite envolver una excepción de bajo nivel dentro de una de nuestro dominio public ConfigurationException(String mensaje, Throwable causa) { super(mensaje, causa); } } public void validarClave(String clave) { if (clave == null || clave.isEmpty()) { // Lanzamiento explícito mediante 'throw' throw new ConfigurationException("La clave de configuración no puede estar vacía."); } } public void procesarFichero(String ruta) { try { FileReader fr = new FileReader(ruta); } catch (IOException e) { // Encadenamiento de excepciones (Exception Chaining): // Capturamos una IOException de bajo nivel y lanzamos nuestra excepción de dominio preservando la causa 'e' throw new ConfigurationException("Error al cargar la configuración desde " + ruta, e); } } Al capturar un error técnico (como un fallo de base de datos o de archivo) y relanzarlo como una excepción propia de negocio, pásale siempre la excepción original como segundo argumento al constructor (''super(mensaje, causa);''). De este modo, no se perderá la traza original del problema cuando se registre en el archivo de log. ===== 2. Formatos Estructurados y Archivos de Configuración ===== En las aplicaciones web modernas y arquitecturas de microservicios, la lectura de ficheros se centra en la carga de **archivos de configuración** y el intercambio de datos. Para ello se utilizan formatos estructurados estandarizados: **JSON**, **YAML** y **XML**. ==== 2.1 Comparativa de Formatos ==== ^ Formato ^ Sintaxis Base ^ Casos de Uso Principales ^ Ventajas ^ | JSON | Clave-Valor, Arrays {} [] | APIS REST, intercambio de datos web, configs simples | Ligero, nativo en la web, fácil parseo | | YAML | Indentación por espacios | Configuración de aplicaciones (Spring Boot, Docker, K8s) | Altamente legible por humanos, admite comentarios | | XML | Etiquetas anidadas | Configuración empresarial (Log4j2, Maven pom.xml), SOAP | Validación estricta con esquemas (XSD), estándares robustos | ==== 2.2 Proceso de Parseo y Deserialización ==== El proceso de convertir un fichero en cualquiera de estos formatos a objetos Java de nuestro dominio se denomina **deserialización** (y la operación inversa, **serialización**). Durante este proceso, es donde ocurren la mayoría de las excepciones de entrada/salida y parseo que la aplicación debe controlar: * **''FileNotFoundException'' / ''IOException''**: El archivo no existe en la ruta indicada, el nombre está mal escrito o el proceso no tiene permisos de lectura.\\ \\ * **Excepciones de Formato/Sintaxis**: El archivo contiene caracteres inválidos, etiquetas sin cerrar (XML) o claves mal formadas.\\ \\ * **Excepciones de Conversión de Tipos (''NumberFormatException'')**: Ocurre cuando la aplicación espera leer un parámetro numérico (ej. un puerto o número de conexiones) pero en el fichero se ha escrito un texto no válido. Para leer archivos de configuración sin necesidad de librerías externas ni mapeadores complejos, Java proporciona la clase nativa ''java.util.Properties''. Podemos diseñar una clase de utilidad para realizar esta lectura de forma limpia: public class ConfigurationLoader { /** * Carga un archivo de propiedades y valida sus valores clave. * @param filePath Ruta relativa o absoluta del archivo .properties * @return Objeto Properties cargado, o null si ocurrió un error crítico */ public Properties loadSettings(String filePath) { Properties config = new Properties(); // Uso de try-with-resources para garantizar el cierre del fichero try (FileInputStream file = new FileInputStream(filePath)) { // Carga el contenido del fichero en el objeto Properties config.load(file); // Lectura y conversión explícita con control de errores de tipo String url = config.getProperty("db.url"); int maxConnections = Integer.parseInt(config.getProperty("db.max_connections")); IO.println("Configuration loaded successfully. Max connections: " + maxConnections); return config; } catch (FileNotFoundException e) { IO.println("Critical error: Configuration file not found at " + filePath); } catch (IOException e) { IO.println("I/O error while reading configuration file: " + e.getMessage()); } catch (NumberFormatException e) { IO.println("Format error: db.max_connections must be a valid integer."); } return null; // Si ocurre cualquier excepción, devolvemos null } } Esta aproximación facilita la sustitución de componentes y la realización de pruebas unitarias en el futuro: static void main() { // Instanciación del servicio de configuración ConfigurationLoader configLoader = new ConfigurationLoader(); // Carga de ajustes desde el fichero Properties config = configLoader.loadSettings("config.properties"); if (config != null) { // Uso de los valores leídos String dbUrl = config.getProperty("db.url"); IO.println("Connecting to database at: " + dbUrl); } else { IO.println("Application failed to start due to invalid configuration."); } } En los temas posteriores aprenderemos a mapear automáticamente archivos JSON y YAML a objetos Java complejos. ===== Ejercicios ===== === Ejercicio 1 === Crea una excepción no verificada llamada ''InvalidConfigurationException''. Modifica el método ''loadSettings'' de la clase ''ConfigurationLoader'' para que: * Si el archivo no existe (''FileNotFoundException'') o hay un error de lectura (''IOException''), relance una ''InvalidConfigurationException'' envolviendo la excepción original (Exception Chaining).\\ \\ * Si la propiedad ''db.max_connections'' no es un número entero válido o su valor es menor o igual a cero, lance explícitamente una ''InvalidConfigurationException'' con un mensaje descriptivo. === Ejercicio 2 === Implementa un programa principal en la clase ''Main'' que instancie ''ConfigurationLoader'' y trate de cargar el archivo ''config.properties''. * Captura la excepción ''InvalidConfigurationException'' mediante un bloque ''try-catch''.\\ \\ * En el bloque ''catch'', muestra por pantalla el mensaje de error de la excepción y, si existe una causa raíz (root cause), imprime también el mensaje de la excepción original.\\ \\ * Asegúrate de incluir un bloque de ejecución garantizada o una verificación lógica para mostrar el estado final del intento de inicio de la aplicación.