Ja tens totes les peces tècniques: saps llegir una fallada, capturar-la, llançar-la, donar-li nom propi, garantir la neteja i tancar recursos sense perdre informació. El que falta és la part que no s'aprèn llegint l'especificació del llenguatge: decidir on va cada cosa.
Perquè un sistema amb excepcions perfectament dissenyades pot continuar sent inmanejable si cada capa captura el que no li toca, si l'usuari rep un stack trace a la cara, si una fallada apareix cinc vegades al registre o —el cas de BiblioTech fins ara— si el registre no existeix i tot el diagnòstic són System.out.println que es perden en tancar el terminal.
Aquesta lliçó tanca el mòdul amb dues meitats. La primera és estratègia: on es captura, què fa cada capa, com es construeix la frontera d'errors del main, quina informació arriba a l'usuari i quina només al registre, i quan convé validar en comptes de llançar. La segona és logging: per què System.out.println no és una opció en producció, com es fa servir java.util.logging de debò, què registrar a cada nivell, què no registrar mai, i com és l'ecosistema real que veuràs a 11-07.
En acabar, BiblioTech no tindrà ni un sol System.out.println de diagnòstic, el seu main tindrà una frontera que impedeix que res escapi sense registrar-se, i els avisos seran registres amb marca de temps, nivell i context, filtrables i arxivables. És a dir: programari que es pot posar en producció.
Contingut
- On es captura una excepció
- Les capes de BiblioTech i què fa cadascuna amb els errors
- La frontera d'errors del
main - El gestor global d'excepcions no capturades
- Què veu l'usuari i què veu el registre
- Errors recuperables i irrecuperables: degradació elegant
- Reintents amb espera i el seu límit
- Validació prèvia enfront d'excepció
- El patró "objecte resultat" i
Optional - Idempotència i estat coherent
- Per què
System.out.printlnno serveix en producció - Els nivells de log i què registrar a cadascun
java.util.logginga la pràcticaHandler,Formatterilogging.properties- Registrar una excepció correctament
- Què NO registrar mai
- Missatges de log útils i correlació
- L'ecosistema real: SLF4J, Logback i Log4j2
- BiblioTech: la refactorització final
- Errors Comuns i Consells
- Exercicis
- On es captura una excepció
La regla que resumeix tota la resta:
Captura una excepció on puguis prendre una decisió sobre ella, no on es produeix.
Sona obvi i tanmateix la major part del codi mal escrit la incompleix. El reflex d'envoltar amb try-catch qualsevol línia que pugui fallar produeix això:
// MALAMENT: captura on es produeix, sense poder decidir res
public Material cercar(String referencia) {
try {
return cataleg.obtenirPerReferencia(referencia);
} catch (MaterialNoTrobatException e) {
e.printStackTrace(); // no es una decisio
return null; // i ara retornem el null que tant va costar eliminar
}
}Aquell catch no decideix res: no recupera, no reintenta, no tradueix i no aporta context. L'única cosa que fa és degradar una excepció amb nom i dades a un null mut, desfent tota la feina de 06-03 i 06-04.
La versió correcta és no capturar:
// BE: no es captura. Puja a qui pugui decidir.
public Material cercar(String referencia) {
return cataleg.obtenirPerReferencia(referencia);
}Les tres preguntes que decideixen si has de capturar aquí:
- Puc fer alguna cosa diferent de propagar? Recuperar amb un valor per defecte, reintentar, degradar el servei. Si no, no capturis.
- Tinc context per afegir? Si puc embolcallar aportant informació que la capa inferior no coneixia, captura i tradueix conservant la causa (06-03).
- Soc la frontera? Si per damunt de mi només hi ha la JVM o l'usuari, he de capturar sí o sí, perquè ningú més no ho farà.
Si la resposta a les tres és no, deixa que pugi. No capturar és una decisió activa i moltes vegades la correcta.
- Les capes de BiblioTech i què fa cadascuna amb els errors
Una aplicació real s'organitza en capes, i cadascuna té una política d'errors diferent:
flowchart TB
U["USUARI<br/>Marta, Diego, Nuria"]
subgraph P["PRESENTACIO - MenuBiblioTech, RebutConsola"]
P1["CAPTURA tot el que l'usuari pugui entendre<br/>Tradueix a missatges clars<br/>Registra el que es inesperat<br/>MAI no mostra un stack trace"]
end
subgraph S["SERVEI - GestorPrestecs, Cataleg, CuaReserves"]
S1["Aplica regles de negoci i LLANCA excepcions del domini<br/>Tradueix les de persistencia conservant la causa<br/>Garanteix estat coherent amb finally<br/>Gairebe mai no captura"]
end
subgraph D["DOMINI - Material, Llibre, Empleat, Prestec"]
D1["Valida invariants i LLANCA<br/>Fail-fast en constructors i setters<br/>MAI no captura ni registra"]
end
subgraph PE["PERSISTENCIA - modul 7"]
PE1["Llanca IOException i similars<br/>El servei les tradueix"]
end
U -->|"introdueix dades"| P1
P1 -->|"invoca"| S1
S1 -->|"usa"| D1
S1 -->|"llegeix i escriu"| PE1
D1 -.->|"BiblioTechException"| S1
PE1 -.->|"IOException"| S1
S1 -.->|"BiblioTechException<br/>amb la causa conservada"| P1
P1 -.->|"missatge clar,<br/>sense detalls interns"| U
En taula:
| Capa | Captura? | Llança? | Registra? | Què mostra? |
|---|---|---|---|---|
| Domini | Gairebé mai | Sí, sempre que es violi un invariant | No | Res: no coneix la interfície |
| Servei | Només per traduir o compensar | Sí, del vocabulari del domini | Només el rellevant per al negoci | Res |
| Persistència | Només per traduir | Sí, d'E/S | Detalls tècnics | Res |
| Presentació | Sí, tot el que és esperable | Poques vegades | Tot el que és inesperat | Missatges a l'usuari |
Les dues regles que se'n deriven i que convé gravar:
El domini mai no registra ni imprimeix. Un Llibre no sap si la seva aplicació és de consola, web o mòbil, ni si hi ha algú mirant. La seva única manera de comunicar un problema és llançar. És l'abstracció de 03-08 aplicada a la gestió d'errors: cada capa parla el vocabulari del seu nivell i no pressuposa res de les de dalt.
La presentació és el filtre. És l'única capa que sap qui hi ha a l'altre costat i què pot entendre, i per tant l'única que pot decidir quin missatge mostrar. També és l'única que hauria de registrar les fallades inesperades amb tot el seu detall.
- La frontera d'errors del
main
mainPer molt ben repartides que estiguin les responsabilitats, sempre pot escapar alguna cosa. La frontera d'errors és el try-catch més extern de l'aplicació, i la seva missió és que res no arribi mai a la JVM sense registrar-se i sense un missatge digne.
package com.nexussoftware.bibliotech.presentacio;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.BiblioTechException;
/**
* Punt d'entrada de BiblioTech amb frontera d'errors.
*
* Estructura de la frontera, del mes especific al mes general:
* 1. BiblioTechException : fallada esperada del domini -> missatge net, sortida 1
* 2. RuntimeException : bug -> missatge generic amb incidencia, registre complet, sortida 2
* 3. Error : fallada de la JVM -> registre minim i sortida immediata, sortida 3
*/
public class BiblioTechApp {
private static final Logger LOG = Logger.getLogger(BiblioTechApp.class.getName());
public static void main(String[] args) {
ConfiguracioLog.inicialitzar();
installarGestorGlobal();
LOG.info("BiblioTech arrencant");
try {
new MenuBiblioTech().arrencar();
LOG.info("BiblioTech finalitzat correctament");
} catch (BiblioTechException e) {
// Fallada ESPERADA del domini que ningu no ha gestionat. Es rara, pero llegible.
System.err.println();
System.err.println("L'operacio no s'ha pogut completar: " + e.getMessage());
LOG.log(Level.WARNING, "Excepcio de domini no gestionada [" + e.getCodi() + "]", e);
System.exit(1);
} catch (RuntimeException e) {
// Fallada INESPERADA: es un bug. L'usuari no ha de veure els detalls.
String incidencia = generarIdIncidencia();
System.err.println();
System.err.println("S'ha produit un error intern.");
System.err.println("Incidencia: " + incidencia);
System.err.println("Comuniqueu aquest codi a l'equip de suport de Nexus Software.");
LOG.log(Level.SEVERE, "Error no controlat. Incidencia " + incidencia, e);
System.exit(2);
} catch (Error e) {
// Fallada de la JVM: no es pot recuperar. Registrar el minim i sortir.
// Es captura AQUI i nomes aqui, per deixar constancia abans de morir.
System.err.println();
System.err.println("Error irrecuperable de la maquina virtual: "
+ e.getClass().getSimpleName());
try {
LOG.log(Level.SEVERE, "Error irrecuperable", e);
} catch (Throwable ignorat) {
// Si ni tan sols es pot registrar (p. ex. OutOfMemoryError),
// no hi ha res mes a fer. No es propaga per no amagar l'Error.
}
System.exit(3);
}
}
/** Identificador curt que l'usuari pot comunicar a suport. */
private static String generarIdIncidencia() {
return "INC-" + Long.toHexString(System.nanoTime()).toUpperCase().substring(0, 8);
}
private static void installarGestorGlobal() {
Thread.setDefaultUncaughtExceptionHandler((fil, fallada) ->
LOG.log(Level.SEVERE, "Excepcio no capturada al fil '"
+ fil.getName() + "'", fallada));
}
}Les decisions que fan bona aquesta frontera:
| Element | Per què |
|---|---|
Tres catch ordenats |
Del específic al general (06-02). BiblioTechException primer, perquè és el que produeix un missatge útil |
| Distingir domini de bug | Una fallada de negoci mereix el seu missatge; un bug mereix una incidència i silenci sobre els detalls |
| Identificador d'incidència | L'usuari comunica INC-A3F91C0B, i al registre es troba el stack trace exacte. Sense exposar res |
| Codis de sortida diferents | 1 negoci, 2 bug, 3 fatal. Un script o un sistema d'integració contínua pot reaccionar de manera diferent |
Capturar Error només aquí |
És l'única excepció a la regla de 06-01: es captura per registrar abans de morir, mai per continuar |
El registre de l'Error, protegit |
Amb un OutOfMemoryError, fins i tot escriure al log pot fallar |
Aquest és el motiu pel qual es diu que catch (Exception e) només és legítim a la frontera: aquí no hi ha ningú per damunt, així que capturar ample no amaga res de ningú.
- El gestor global d'excepcions no capturades
La frontera del main cobreix el fil principal. Però tan bon punt hi hagi més fils —mòdul 8—, una excepció que escapi d'un fil secundari mata aquell fil en silenci i el programa continua com si res. És una font clàssica de fallades invisibles.
Thread.setDefaultUncaughtExceptionHandler instal·la una xarxa de seguretat per a tots els fils:
package com.nexussoftware.bibliotech.presentacio;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class GestorGlobal {
private static final Logger LOG = Logger.getLogger(GestorGlobal.class.getName());
private GestorGlobal() { }
public static void installar() {
Thread.setDefaultUncaughtExceptionHandler((fil, fallada) -> {
// Aquest codi s'executa quan una excepcio escapa de QUALSEVOL fil
LOG.log(Level.SEVERE,
"Excepcio no capturada al fil '" + fil.getName()
+ "' (id " + fil.threadId() + ")", fallada);
if (fallada instanceof Error) {
LOG.severe("Error irrecuperable: es sollicita l'aturada de l'aplicacio");
System.exit(3);
}
});
// Hook d'aturada: ultima oportunitat de neteja (06-05)
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
LOG.info("BiblioTech aturant-se: consolidant estat");
// Al modul 7, aqui es persistira el cataleg
}, "aturada-bibliotech"));
}
public static void main(String[] args) {
installar();
Thread filSecundari = new Thread(() -> {
throw new IllegalStateException("Fallada a la tasca de manteniment");
}, "manteniment");
filSecundari.start();
try { filSecundari.join(); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("El fil principal segueix viu, pero la fallada HA QUEDAT REGISTRADA");
}
}Sense aquell gestor, el fil manteniment moriria imprimint un stack trace a System.err que en producció probablement ningú no llegiria, i l'aplicació continuaria funcionant sense la tasca de manteniment i sense que ningú ho sabés.
Un detall de precedència: si un fil concret té el seu propi gestor (fil.setUncaughtExceptionHandler(...)), aquell té prioritat sobre el global.
- Què veu l'usuari i què veu el registre
Una separació que s'incompleix constantment i amb conseqüències reals:
| Usuari | Registre | |
|---|---|---|
| Missatge en el seu idioma i vocabulari | Sí | No necessàriament |
| Què fer a continuació | Sí | No |
| Identificador d'incidència | Sí | Sí (per correlacionar) |
| Classe de l'excepció | No | Sí |
| Stack trace | MAI | Sí, complet |
| Camins de fitxers, noms de taules, consultes | MAI | Sí |
| Dades internes del sistema | No | Sí |
| Marca de temps i fil | No | Sí |
Un stack trace a la cara de l'usuari és un problema per tres motius, en ordre de gravetat:
- És un risc de seguretat. Revela paquets, classes, versions de llibreries, camins del sistema de fitxers i de vegades fragments de consultes. És informació de reconeixement gratuïta per a qui vulgui atacar el sistema.
- No li serveix de res. Marta Ruiz no pot fer res amb
NullPointerException at Prestec.java:97. - Destrueix la confiança. Un mur de text vermell comunica "això està trencat i ningú no ho controla".
La versió correcta, aplicada:
// El que veu l'usuari
System.err.println("No s'ha pogut completar el prestec.");
System.err.println("Incidencia: INC-A3F91C0B");
System.err.println("Comuniqueu aquest codi a l'equip de suport.");
// El que va al registre
LOG.log(Level.SEVERE, "Fallada en prestar LIB-0001 a EMP-001 el dia 12. Incidencia INC-A3F91C0B", e);L'identificador d'incidència és la peça que uneix tots dos mons: l'usuari el comunica, suport el busca al registre, i apareix el stack trace complet amb tot el context. Sense exposar res.
I una regla addicional, més subtil: els missatges de les excepcions del domini sí que es poden mostrar a l'usuari, perquè estan escrits en el seu vocabulari —"El material LIB-0001 està prestat i s'espera la seva devolució el dia 27"—. Els missatges de les excepcions tècniques, no. És exactament la distinció que permet el mètode esCorregiblePerUsuari() que vas posar a BiblioTechException a 06-04.
- Errors recuperables i irrecuperables: degradació elegant
| Recuperable | Irrecuperable | |
|---|---|---|
| Definició | Existeix una alternativa acceptable | No hi ha manera sensata de continuar |
| Resposta | Degradar el servei i continuar | Registrar i acabar de manera ordenada |
| Exemples a BiblioTech | El fitxer de catàleg no existeix → arrencar buit | La configuració obligatòria falta → no arrencar |
| La cua d'avisos no respon → apuntar i reintentar més tard | OutOfMemoryError |
|
| L'exportador falla → informar i continuar operant | Dades corruptes a l'índex principal |
La degradació elegant consisteix a continuar donant el servei que es pugui donar, informant del que no està disponible:
package com.nexussoftware.bibliotech.servei;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Arrencada de BiblioTech amb degradacio elegant: l'aplicacio funciona
* encara que faltin components opcionals, i nomes avorta si falta alguna cosa essencial.
*/
public class ArrencadaBiblioTech {
private static final Logger LOG = Logger.getLogger(ArrencadaBiblioTech.class.getName());
private boolean avisosDisponibles = true;
private boolean exportacioDisponible = true;
public Cataleg arrencar(String camiCataleg) {
Cataleg cataleg = new Cataleg();
// --- Component OPCIONAL 1: cataleg previ ---
try {
carregarCataleg(cataleg, camiCataleg);
LOG.info(() -> "Cataleg carregat: " + cataleg.mida() + " materials");
} catch (CatalegNoAccessibleException e) {
// DEGRADACIO: sense cataleg previ es pot treballar; es comenca buit.
LOG.log(Level.WARNING,
"No s'ha pogut carregar el cataleg des de '" + e.getCami()
+ "'. S'arrenca amb cataleg buit.", e);
System.out.println("Avis: no hi havia cataleg previ. Es comenca de zero.");
}
// --- Component OPCIONAL 2: servei d'avisos ---
try {
connectarServeiAvisos();
LOG.info("Servei d'avisos disponible");
} catch (RuntimeException e) {
avisosDisponibles = false;
LOG.log(Level.WARNING, "Servei d'avisos no disponible; s'acumularan en cua", e);
System.out.println("Avis: els avisos de venciment no s'enviaran en aquesta sessio.");
}
// --- Component ESSENCIAL: configuracio ---
// Si aixo falla, NO es degrada: s'avorta. Continuar amb una tarifa
// desconeguda produiria multes incorrectes, que es pitjor que no arrencar.
int diesPrestec = llegirConfiguracioObligatoria("dies.prestec");
LOG.config(() -> "Configuracio: dies.prestec=" + diesPrestec);
return cataleg;
}
public boolean potEnviarAvisos() { return avisosDisponibles; }
public boolean potExportar() { return exportacioDisponible; }
private void carregarCataleg(Cataleg cataleg, String cami) throws CatalegNoAccessibleException {
throw new CatalegNoAccessibleException(cami,
new java.io.FileNotFoundException(cami + " (No such file or directory)"));
}
private void connectarServeiAvisos() {
throw new IllegalStateException("El servei d'avisos no respon al port 8080");
}
private int llegirConfiguracioObligatoria(String clau) {
return 15; // al modul 7 vindra d'un fitxer .properties
}
}El criteri per decidir entre degradar i avortar:
Degrada quan el servei reduït continuï sent correcte. Avorta quan continuar produiria resultats incorrectes.
Arrencar sense catàleg previ dona un servei reduït però correcte: la biblioteca està buida i això és cert. Arrencar sense conèixer la tarifa diària produiria multes incorrectes, que és molt pitjor que no arrencar: un sistema que dona números equivocats amb tota naturalitat és més perillós que un que no arrenca.
- Reintents amb espera i el seu límit
Ja vas veure el patró a 06-02. Aquí van les regles de decisió, que és el que s'aprèn amb l'experiència:
Què reintentar i què no:
| Reintentar | No reintentar |
|---|---|
| Fallada de xarxa o de connexió | IllegalArgumentException: la dada continuarà sent invàlida |
| Fitxer temporalment bloquejat | MaterialNoTrobatException: continuarà sense existir |
| Servei que retorna "no disponible" | LimitPrestecsExceditException: la regla no canvia sola |
| Bloqueig de base de dades | Qualsevol error de programació |
La regla: reintenta el que depèn de l'entorn; mai el que depèn de les dades o de les regles.
Les quatre condicions d'un reintent correcte:
- Límit d'intents. Sense ell, un servei caigut converteix la teva aplicació en un bucle infinit.
- Espera creixent (backoff exponencial): 200, 400, 800, 1600 ms. Reintentar en ràfega agreuja la caiguda del servei remot.
- Fallada definitiva en esgotar-los, conservant l'últim error com a causa. Mai retornar
nullfingint èxit. - Idempotència de l'operació. Aquesta és la que més s'oblida: si el préstec es va registrar i només va fallar la confirmació, reintentar crearà dos préstecs. Només es reintenta el que es pot repetir sense efectes secundaris.
Un refinament que veuràs en sistemes reals: el tallacircuits (circuit breaker). Si un servei falla repetidament, es deixa de cridar durant un temps en comptes de reintentar a cada operació. És un patró d'arquitectura que queda fora de l'abast d'aquest curs, però convé conèixer-ne el nom.
- Validació prèvia enfront d'excepció
Moltes vegades pots triar entre preguntar abans o intentar i capturar. La taula de criteris:
| Criteri | Validació prèvia (if) |
Excepció |
|---|---|---|
| Freqüència de la fallada | Alta: és un cas normal | Baixa: és excepcional |
| Cost de comprovar | Barat (isEmpty, containsKey) |
Car o impossible sense intentar-ho |
| Condició de cursa | Pot canviar entre la comprovació i l'ús | Immune: és atòmic |
| Llegibilitat | Millor si són una o dues condicions | Millor si són moltes o estan disperses |
| Rendiment | ~70 vegades més ràpid (06-02) | Irrellevant si és rar |
Exemples aplicats:
// VALIDACIO PREVIA: barata, el cas es frequent i no hi ha cursa
if (cataleg.existeix(referencia)) {
Material m = cataleg.obtenirPerReferencia(referencia);
// ...
}
// Millor encara amb el metode que retorna Optional
cataleg.cercarPerReferencia(referencia)
.ifPresent(m -> System.out.println(m.getTitol()));
// EXCEPCIO: la conversio no es pot validar sense fer-la
try {
int dies = Integer.parseInt(entrada);
} catch (NumberFormatException e) {
// Escriure un validador d'enters correcte (signe, rang, desbordament)
// es mes propens a errors que capturar
}
// EXCEPCIO OBLIGATORIA: condicio de cursa
// Amb fitxers o recursos compartits, "comprovar i despres usar" no es atomic:
// entre l'exists() i l'open() un altre proces pot haver esborrat el fitxer.
try (BufferedReader r = new BufferedReader(new FileReader(cami))) {
// ...
} catch (FileNotFoundException e) {
// L'unica forma fiable
}Aquell últim cas té nom propi, TOCTOU (time-of-check to time-of-use), i és una font coneguda de fallades i de vulnerabilitats. Sempre que el recurs sigui compartit —fitxers, base de dades, memòria entre fils—, la comprovació prèvia no garanteix res i cal gestionar l'excepció de totes maneres.
La regla pràctica final: valida el que sigui barat i estable de comprovar; captura el que no es pugui saber sense intentar-ho.
- El patró "objecte resultat" i
Optional
OptionalNo tota fallada ha de ser una excepció. Hi ha dues alternatives legítimes.
Optional, per a l'absència de valor:
// Ja el fas servir a BiblioTech des de 06-03
public Optional<Material> cercarPerReferencia(String referencia) {
return Optional.ofNullable(indexPerReferencia.get(referencia));
}Comunica "potser no hi ha res, i això és normal" d'una manera que el compilador ajuda a no ignorar. Es desenvolupa a 10-04.
L'objecte resultat, quan necessites transportar el motiu de la fallada sense llançar. És especialment útil en validacions en lot, on vols recollir tots els errors i no només el primer:
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.List;
/**
* Resultat d'una operacio que pot fallar, SENSE llancar.
*
* Util quan vols acumular DIVERSOS errors en comptes d'avortar al primer:
* validar un formulari, importar un fitxer, comprovar regles de negoci.
*/
public class Resultat {
private final boolean exit;
private final List<String> errors;
private Resultat(boolean exit, List<String> errors) {
this.exit = exit;
this.errors = List.copyOf(errors);
}
public static Resultat ok() {
return new Resultat(true, List.of());
}
public static Resultat ambErrors(List<String> errors) {
return new Resultat(false, errors);
}
public boolean esExit() { return exit; }
public List<String> getErrors() { return errors; }
public String descriure() {
if (exit) { return "OK"; }
return "Rebutjat (" + errors.size() + " problemes):\n - "
+ String.join("\n - ", errors);
}
// ------------------------------------------------------------------
/** Exemple: validar una alta acumulant TOTS els problemes. */
public static Resultat validarAlta(String referencia, String titol, int any, String isbn) {
List<String> errors = new ArrayList<>();
if (referencia == null || !referencia.matches("LIB-\\d{4}")) {
errors.add("La referencia ha de tenir el format LIB-NNNN, i era: " + referencia);
}
if (titol == null || titol.isBlank()) {
errors.add("El titol no pot estar buit");
}
if (any < 1450 || any > 2100) {
errors.add("L'any ha d'estar entre 1450 i 2100, i era: " + any);
}
if (isbn == null || !isbn.matches("97[89]-\\d{10}")) {
errors.add("L'ISBN ha de tenir el format 978-NNNNNNNNNN, i era: " + isbn);
}
return errors.isEmpty() ? ok() : ambErrors(errors);
}
public static void main(String[] args) {
System.out.println(validarAlta("LIB-0001", "Java Eficac", 2018, "978-0000000001")
.descriure());
System.out.println();
System.out.println(validarAlta("L-1", "", 1200, "12345").descriure());
}
}Sortida:
OK Rebutjat (4 problemes): - La referencia ha de tenir el format LIB-NNNN, i era: L-1 - El titol no pot estar buit - L'any ha d'estar entre 1450 i 2100, i era: 1200 - L'ISBN ha de tenir el format 978-NNNNNNNNNN, i era: 12345
Amb excepcions, l'usuari hauria corregit la referència, reenviat, descobert el del títol, corregit, reenviat... quatre voltes. Amb l'objecte resultat, veu els quatre problemes de cop.
Quan usar cada mecanisme:
| Mecanisme | Quan |
|---|---|
| Excepció | La fallada és excepcional i qui crida normalment no pot continuar |
Optional |
L'absència de valor és un resultat normal |
| Objecte resultat | Hi ha diversos errors a comunicar alhora, o la fallada és tan freqüent que forma part del flux |
boolean |
Només quan "no" és una resposta legítima i única (Set.add) |
- Idempotència i estat coherent
Dues propietats que fan que un sistema sobrevisqui a les fallades.
Idempotent significa que repetir l'operació produeix el mateix resultat que fer-la una vegada.
// NO idempotent: dues crides, dos prestecs
gestor.prestar("LIB-0001", "EMP-001", 12);
// Idempotent: la segona crida no fa res
sessio.close();
cuaReserves.cancellar("LIB-0001", "EMP-001"); // si no n'hi havia, no passa resPer què importa aquí: una operació idempotent es pot reintentar sense por. Si no ho és, un reintent després d'una fallada de xarxa pot duplicar l'efecte. És la quarta condició de l'apartat 7.
La tècnica habitual per fer idempotent una cosa que no ho és de manera natural: un identificador d'operació que permeti detectar el duplicat.
public Prestec prestar(String idOperacio, String referencia, String idEmpleat, int dia) {
// Si aquesta operacio ja s'ha processat, es retorna el resultat anterior
Prestec jaFet = operacionsProcessades.get(idOperacio);
if (jaFet != null) {
LOG.info(() -> "Operacio " + idOperacio + " ja processada; es retorna el resultat previ");
return jaFet;
}
// ... proces normal ...
}Estat coherent és el que vas resoldre a 06-05 amb el patró de marcadors i compensació al finally. La regla que ho resumeix:
Una operació ha de deixar el sistema en un estat vàlid, tant si acaba com si falla. Mai a mitges.
I l'ordre que ho facilita, aplicant el de 06-03:
- Validar-ho tot (no modifica res).
- Preparar els objectes nous (encara no visibles per a ningú).
- Aplicar els canvis, com més tard i com més junts millor.
- Compensar al
finallysi no s'ha arribat al final.
- Per què
System.out.println no serveix en producció
System.out.println no serveix en produccióComença la segona meitat. Tot el diagnòstic de BiblioTech fins ara ha estat System.out.println, i cal substituir-lo. Aquests són els set motius:
| Problema | Conseqüència real |
|---|---|
| No es pot filtrar | O ho veus tot o no veus res. Amb mil línies per minut, trobar la rellevant és impossible |
| No es pot desactivar | El diagnòstic de depuració continua imprimint-se en producció, amb el seu cost |
| No té marca de temps | "Quan va passar això? Abans o després d'allò?" Sense resposta |
| No té nivell | Un avís rutinari i una fallada greu es veuen exactament igual |
| No diu d'on ve | Ni classe ni mètode ni fil. En un programa multifil, és il·legible |
| No s'arxiva ni rota | Es perd en tancar el terminal. I si redirigeixes a fitxer, creix sense límit fins a omplir el disc |
| És síncron i bloquejant | Escriure a consola bloqueja el fil. En un bucle calent, enfonsa el rendiment |
Compara la mateixa informació:
// Logger
LOG.warning("Alta rebutjada: referencia LIB-0001 duplicada, ja la fa servir 'Java Eficac'");2026-08-05 11:42:07.312 [main] WARNING com.nexussoftware.bibliotech.servei.Cataleg registrar Alta rebutjada: referencia LIB-0001 duplicada, ja la fa servir 'Java Eficac'
El segon es pot filtrar per nivell, per classe o per fil; es pot arxivar i rotar; es pot desactivar sense tocar el codi; i diu quan, on i què.
I una regla que confon molta gent:
System.out.printlncontinua sent correcte per a la SORTIDA del programa, és a dir, per al que l'usuari ha demanat veure: el menú, el llistat del catàleg, el rebut. El que cal substituir és el diagnòstic: els avisos, les traces, els errors.
La prova per distingir-los: això ho ha demanat l'usuari? Si sí, és sortida. Si no, és log.
- Els nivells de log i què registrar a cadascun
java.util.logging defineix set nivells. Aquests són, amb la seva equivalència a SLF4J/Logback, que és el que veuràs a l'ecosistema real:
java.util.logging |
SLF4J/Logback | Quan usar-lo | Exemple a BiblioTech |
|---|---|---|---|
SEVERE |
ERROR |
El sistema no pot fer la seva feina. Requereix intervenció | Índex del catàleg corrupte; error no controlat |
WARNING |
WARN |
Alguna cosa va malament però s'ha pogut continuar | No hi havia catàleg previ; el servei d'avisos no respon |
INFO |
INFO |
Fites del funcionament normal | Arrencada, aturada, préstec completat, importació acabada |
CONFIG |
DEBUG |
Paràmetres de configuració en arrencar | dies.prestec=15, tarifa.diaria=0.25 |
FINE |
DEBUG |
Detall útil per depurar | Entrada i sortida dels mètodes de servei |
FINER |
TRACE |
Més detall encara | Cada iteració d'un bucle d'importació |
FINEST |
TRACE |
Màxim detall | Bolcat d'estructures internes |
La guia pràctica, que és el que de debò cal interioritzar:
SEVERE/ERROR — Algú ha de mirar això.
Si registres a aquest nivell alguna cosa que no requereix intervenció, estàs entrenant l'equip a ignorar els errors. És la fallada més comuna dels sistemes de log mal configurats: quan tot és un error, res no ho és.
WARNING/WARN — Ha anat malament però continuem.
Degradacions, reintents, configuracions sospitoses, ús de valors per defecte perquè faltava un paràmetre.
INFO — Les fites del negoci.
Ha de poder llegir-se com la història del que ha fet l'aplicació. Si té tant soroll que no es pot llegir, hi ha coses que haurien de ser FINE.
CONFIG/FINE/FINER/FINEST — Desactivats en producció per defecte.
S'activen quan cal investigar alguna cosa concreta. Per això han de ser barats quan estan desactivats, que és de què tracta l'apartat següent.
I un error de criteri molt freqüent: una excepció capturada i gestionada correctament no és un SEVERE. Si el catàleg no existia i vas arrencar buit, això és un WARNING: el sistema va fer el previst. SEVERE és per al que ningú no ha previst.
java.util.logging a la pràctica
java.util.logging a la pràcticajava.util.logging (JUL) ve al JDK, sense dependències. És l'elecció per a aquest curs precisament per això: pots executar-ho tot sense descarregar res. A 11-07 veuràs l'estàndard real de l'ecosistema.
Obtenir un Logger:
package com.nexussoftware.bibliotech.servei;
import java.util.logging.Logger;
public class Cataleg {
/**
* Un Logger per classe, estatic i final, anomenat amb el nom complet
* de la classe. Es la convencio universal, i permet filtrar per paquet:
* activar el nivell FINE per a com.nexussoftware.bibliotech.servei i deixar
* la resta a INFO.
*/
private static final Logger LOG = Logger.getLogger(Cataleg.class.getName());
// ...
}Per què static final: perquè el Logger no depèn de la instància, obtenir-lo és relativament car i així es fa una sola vegada per classe.
Registrar missatges:
LOG.severe("Missatge greu");
LOG.warning("Avis");
LOG.info("Informacio");
LOG.config("Configuracio");
LOG.fine("Detall de depuracio");
// Forma general, amb el nivell explicit
LOG.log(Level.INFO, "Missatge");
// AMB EXCEPCIO: la forma correcta de registrar una fallada
LOG.log(Level.SEVERE, "Missatge descriptiu", excepcio);La forma mandrosa amb Supplier (Java 8+), que és important:
// MALAMENT: la concatenacio es fa SEMPRE, encara que FINE estigui desactivat
LOG.fine("Cercant " + referencia + " en un cataleg de " + materials.size()
+ " materials, index amb " + index.size() + " entrades");
// BE: la lambda nomes s'avalua si el nivell FINE esta actiu
LOG.fine(() -> "Cercant " + referencia + " en un cataleg de " + materials.size()
+ " materials, index amb " + index.size() + " entrades");És el mateix principi d'avaluació mandrosa de les lambdes de 04-05 i de l'Objects.requireNonNull amb Supplier de 06-03. En un mètode molt cridat, la diferència entre construir aquella cadena un milió de vegades i no construir-la cap és perfectament mesurable.
L'alternativa clàssica, abans de les lambdes, era comprovar abans:
Continua sent vàlida i de vegades necessària —quan el càlcul del missatge és car de debò—, però la forma amb Supplier és més neta.
La jerarquia de loggers. Els Logger s'organitzen pel seu nom, separat per punts, formant un arbre:
flowchart TB
R["'' (arrel)<br/>nivell INFO, ConsoleHandler"]
R --> C["com"]
C --> N["com.nexussoftware"]
N --> B["com.nexussoftware.bibliotech"]
B --> D["...bibliotech.domini"]
B --> S["...bibliotech.servei<br/>nivell FINE"]
B --> P["...bibliotech.presentacio"]
S --> S1["...servei.Cataleg"]
S --> S2["...servei.GestorPrestecs"]
Un Logger sense nivell propi hereta el del seu pare, i els seus missatges es propaguen als Handler de tots els seus ancestres. Això és el que permet configurar amb precisió:
// Activar el detall NOMES a la capa de servei
Logger.getLogger("com.nexussoftware.bibliotech.servei").setLevel(Level.FINE);
// La resta continua a INFO
Handler, Formatter i logging.properties
Handler, Formatter i logging.propertiesTres peces que cal distingir:
| Peça | Què fa |
|---|---|
Logger |
Rep els missatges i decideix si passen el filtre de nivell |
Handler |
Els envia a una destinació: consola, fitxer, socket, memòria |
Formatter |
Converteix el registre en text |
Filter |
Criteri addicional més fi que el nivell |
Els Handler inclosos al JDK:
Handler |
Destinació |
|---|---|
ConsoleHandler |
System.err |
FileHandler |
Un fitxer, amb rotació per mida i nombre |
StreamHandler |
Un OutputStream qualsevol |
SocketHandler |
Un host i port remots |
MemoryHandler |
Una memòria intermèdia circular en memòria |
Configuració programàtica completa per a BiblioTech:
package com.nexussoftware.bibliotech.presentacio;
import java.io.IOException;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
/**
* Configuracio del sistema de registre de BiblioTech.
*
* Dues destinacions:
* - CONSOLA: nomes INFO i superiors, en format compacte d'una linia.
* - FITXER: tot des de FINE, amb rotacio, per a diagnostic posterior.
*/
public final class ConfiguracioLog {
private static final String CAMI_LOG = "bibliotech-%g.log"; // %g = numero de rotacio
private static final int MIDA_MAXIMA = 1_000_000; // 1 MB per fitxer
private static final int FITXERS_ROTACIO = 5; // 5 fitxers com a maxim
private static boolean inicialitzat = false;
private ConfiguracioLog() { }
public static synchronized void inicialitzar() {
if (inicialitzat) {
return; // idempotent (06-06)
}
inicialitzat = true;
Logger arrel = Logger.getLogger("");
// 1. Treure els handlers per defecte: els substituim pels nostres
for (Handler h : arrel.getHandlers()) {
arrel.removeHandler(h);
}
// 2. El logger arrel accepta tot; cada handler filtra pel seu compte
arrel.setLevel(Level.ALL);
// 3. CONSOLA: nomes el que es important, format compacte
ConsoleHandler consola = new ConsoleHandler();
consola.setLevel(Level.INFO);
consola.setFormatter(new FormatCompacte());
arrel.addHandler(consola);
// 4. FITXER: tot el detall, amb rotacio
try {
FileHandler fitxer = new FileHandler(CAMI_LOG, MIDA_MAXIMA, FITXERS_ROTACIO, true);
fitxer.setLevel(Level.FINE);
fitxer.setFormatter(new FormatDetallat());
arrel.addHandler(fitxer);
} catch (IOException e) {
// Si no es pot crear el fitxer, l'aplicacio NO ha de caure:
// es degrada a nomes consola i s'avisa. Degradacio elegant.
arrel.log(Level.WARNING,
"No s'ha pogut crear el fitxer de log; nomes hi haura registre en consola", e);
}
// 5. Nivell especific per paquet
Logger.getLogger("com.nexussoftware.bibliotech.servei").setLevel(Level.FINE);
Logger.getLogger("com.nexussoftware.bibliotech.domini").setLevel(Level.INFO);
Logger.getLogger(ConfiguracioLog.class.getName()).config("Sistema de registre inicialitzat");
}
/** Una linia per esdeveniment: nivell, classe curta i missatge. */
private static class FormatCompacte extends Formatter {
@Override
public String format(LogRecord registre) {
String classe = registre.getSourceClassName();
String curta = (classe == null) ? "?" : classe.substring(classe.lastIndexOf('.') + 1);
StringBuilder sb = new StringBuilder();
sb.append(String.format("[%-7s] %-18s %s%n",
registre.getLevel().getName(), curta, formatMessage(registre)));
if (registre.getThrown() != null) {
sb.append(" causa: ").append(registre.getThrown()).append('\n');
}
return sb.toString();
}
}
/** Format complet per al fitxer, amb marca de temps, fil i stack trace. */
private static class FormatDetallat extends Formatter {
@Override
public String format(LogRecord registre) {
StringBuilder sb = new StringBuilder();
// Marca de temps. S'usa l'instant en millisegons i es formata
// sense java.time, que es materia de 10-05.
sb.append(String.format("%tF %<tT.%<tL", registre.getMillis()));
sb.append(String.format(" [%-6d] %-7s ",
registre.getLongThreadID(), registre.getLevel().getName()));
sb.append(registre.getSourceClassName()).append('.')
.append(registre.getSourceMethodName()).append(" - ");
sb.append(formatMessage(registre)).append('\n');
// El stack trace complet, sagnat
Throwable t = registre.getThrown();
if (t != null) {
sb.append(" ").append(t).append('\n');
for (StackTraceElement marc : t.getStackTrace()) {
sb.append(" at ").append(marc).append('\n');
}
// Causes i suprimides (06-01, 06-06)
Throwable causa = t.getCause();
int nivell = 1;
while (causa != null && nivell <= 5) {
sb.append(" Caused by: ").append(causa).append('\n');
causa = causa.getCause();
nivell++;
}
for (Throwable suprimida : t.getSuppressed()) {
sb.append(" Suppressed: ").append(suprimida).append('\n');
}
}
return sb.toString();
}
}
}Configuració per fitxer. L'alternativa a configurar en codi és un fitxer logging.properties, que s'activa en arrencar la JVM:
# logging.properties de BiblioTech
# Handlers globals
handlers = java.util.logging.ConsoleHandler, java.util.logging.FileHandler
# Nivell del logger arrel: deixa passar tot, els handlers filtren
.level = ALL
# --- Consola: nomes INFO i superiors ---
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# --- Fitxer: tot des de FINE, amb rotacio ---
java.util.logging.FileHandler.pattern = bibliotech-%g.log
java.util.logging.FileHandler.limit = 1000000
java.util.logging.FileHandler.count = 5
java.util.logging.FileHandler.append = true
java.util.logging.FileHandler.level = FINE
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# Format de SimpleFormatter: data, nivell, classe.metode, missatge, excepcio
java.util.logging.SimpleFormatter.format = %1$tF %1$tT.%1$tL %4$-7s [%2$s] %5$s%6$s%n
# --- Nivells per paquet ---
com.nexussoftware.bibliotech.level = INFO
com.nexussoftware.bibliotech.servei.level = FINE
com.nexussoftware.bibliotech.domini.level = INFOI s'arrenca així:
java -Djava.util.logging.config.file=logging.properties \
-cp . com.nexussoftware.bibliotech.presentacio.BiblioTechAppL'avantatge decisiu de la configuració per fitxer: es pot canviar el nivell de log sense recompilar ni tocar el codi. En producció, quan cal investigar un problema, es puja el nivell a FINE al paquet afectat, es reprodueix, i es torna a baixar.
- Registrar una excepció correctament
La forma correcta i les incorrectes, l'una al costat de l'altra:
try {
gestor.prestar(referencia, idEmpleat, dia);
} catch (BiblioTechException e) {
// ==================== CORRECTE ====================
// El Throwable va com a TERCER argument: el Formatter imprimeix el stack
// trace complet, amb les seves causes i suprimides, al mateix esdeveniment de log.
LOG.log(Level.WARNING, "No s'ha pogut prestar " + referencia + " a " + idEmpleat, e);
// ==================== INCORRECTE ====================
// 1. printStackTrace: va a System.err, sense nivell, sense data, sense filtre
e.printStackTrace();
// 2. Nomes el missatge: perd el stack trace i totes les causes
LOG.warning("Error: " + e.getMessage());
// 3. Concatenar l'excepcio: dona nomes toString(), sense pila
LOG.warning("Error: " + e);
// 4. Registrar I rellancar: la mateixa fallada apareixera dues vegades
LOG.log(Level.SEVERE, "Error", e);
throw e;
}Sobre el quart cas, que mereix la seva pròpia regla:
Registra on gestiones. Si vas a rellançar, no registris el stack trace complet: la capa que finalment gestioni l'error ho farà, i amb més context. Com a molt, afegeix una línia de context a nivell
FINE.
Si cada capa registra i rellança, una sola fallada produeix cinc esdeveniments SEVERE amb cinc stack traces gairebé idèntics, i trobar el primer es converteix en arqueologia.
I una advertència sobre el nivell: una excepció capturada i gestionada correctament poques vegades és SEVERE. El MaterialNoTrobatException que es produeix perquè l'usuari va escriure malament una referència és INFO o FINE, no un error del sistema. Reserva SEVERE per al que requereix que algú intervingui.
- Què NO registrar mai
Un log és un fitxer que es copia, s'envia per correu, s'agrega a un sistema centralitzat i es guarda mesos. Tot el que hi escriguis surt del control de la teva aplicació.
Mai registris:
| Categoria | Exemples |
|---|---|
| Credencials | Contrasenyes, fins i tot xifrades o "només els primers caràcters" |
| Tokens i claus | Tokens de sessió, claus d'API, galetes d'autenticació |
| Dades financeres | Números de targeta, CVV, comptes bancaris |
| Dades personals sensibles | DNI, adreça, salut, dades biomètriques |
| Contingut complet de peticions | Pot dur qualsevol dels anteriors a dins |
| Bolcats d'objectes sencers | LOG.fine("Usuari: " + usuari) imprimirà el que hi hagi al seu toString(), avui i d'aquí a dos anys |
Aquell últim punt és el més traïdor, perquè la fallada apareix després:
// Avui: Empleat.toString() retorna "EMP-001 (Marta Ruiz)". Sembla inofensiu.
LOG.fine(() -> "Processant " + empleat);
// D'aqui a un any algu afegeix el DNI i el telefon al toString()...
// i de sobte estas registrant dades personals a tots els fitxers de log,
// sense que ningu hagi tocat aquesta linia.La defensa: registra camps concrets, no objectes.
Quan necessitis registrar alguna cosa parcialment identificable, emmascara-la:
/** Deixa visibles els ultims 4 caracters: 978-0000000001 -> ***0001 */
private static String emmascarar(String valor) {
if (valor == null || valor.length() <= 4) {
return "****";
}
return "***" + valor.substring(valor.length() - 4);
}Advertència formal: en un sistema real, quines dades personals es poden registrar, durant quant de temps i amb quines mesures de protecció no és una decisió tècnica. Està regulat —a la Unió Europea, pel RGPD— i ho ha de revisar el responsable de protecció de dades o el departament de compliment normatiu de la teva organització. Davant del dubte, no ho registris i pregunta. Un log amb dades personals mal gestionades és un incident de seguretat i pot comportar sancions.
A BiblioTech, EMP-001 és un identificador intern perfectament registrable; el nom complet de la persona ja és una dada personal, i en un sistema real caldria consultar-ho abans de posar-lo al log.
- Missatges de log útils i correlació
Un missatge de log respon a cinc preguntes: què ha passat, amb quines dades, en quina operació, amb quin resultat i —si ha fallat— per què.
| Missatge | Valoració |
|---|---|
"Error" |
Inútil |
"Error en prestar" |
Falta tot el context |
"Error en prestar LIB-0001" |
Falta qui i quan |
"Prestec rebutjat: LIB-0001 a EMP-001 el dia 12. Motiu: material no disponible fins al dia 27" |
Complet |
Els identificadors que sempre han d'aparèixer a BiblioTech:
- Referència del material (
LIB-0001) i referència del préstec (PR-0007). - Identificador de l'empleat (
EMP-001), no el seu nom. - Dia de l'operació.
- Identificador d'operació, per correlacionar diverses línies del mateix flux.
Sobre aquest últim punt: en un sistema amb moltes operacions concurrents, les línies de log de fluxos diferents s'entrellacen i no hi ha manera de saber quines pertanyen al mateix. La solució és un identificador de correlació que es propagui per totes les capes i aparegui a cada missatge:
Amb ell, filtrar per INC-A3F91C0B al fitxer retorna la història completa d'aquella operació, de principi a fi, encara que n'hi hagués cent més en paral·lel. Els sistemes moderns ho automatitzen amb el MDC de SLF4J o amb traçabilitat distribuïda (OpenTelemetry), i el log estructurat —esdeveniments en JSON amb camps anomenats en comptes de text lliure— permet a més consultar-los com si fossin una base de dades. Tots dos temes es recuperen a 11-07 i a 12-07.
- L'ecosistema real: SLF4J, Logback i Log4j2
java.util.logging és el que has fet servir aquí perquè ve al JDK i no requereix instal·lar res. Però en un projecte professional et trobaràs una altra cosa.
| Component | Què és |
|---|---|
| SLF4J | Una façana: una API de logging sense implementació. El teu codi depèn només d'ella |
| Logback | La implementació més usada, del mateix autor que SLF4J. La que porta Spring Boot per defecte |
| Log4j2 | L'altra gran implementació, amb molt bon rendiment gràcies al seu registre asíncron |
java.util.logging |
La del JDK. Menys flexible, però sense dependències |
Per què existeix la façana: el teu codi escriu contra SLF4J, i en el moment del desplegament es tria la implementació. Si demà canvies de Logback a Log4j2, no toques ni una línia del teu codi: canvies una dependència. És exactament el principi de programar contra interfícies de 04-01, aplicat a les llibreries.
Així es veu el mateix codi amb SLF4J:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Cataleg {
private static final Logger LOG = LoggerFactory.getLogger(Cataleg.class);
public void registrar(Material material) {
// Sintaxi de marcadors {}: sense concatenacio i sense lambda.
// El missatge nomes es compon si el nivell esta actiu.
LOG.debug("Registrant {} en un cataleg de {} materials",
material.getReferencia(), materials.size());
// ...
}
}Els avantatges enfront de JUL: la sintaxi de marcadors {} resol el problema de l'avaluació mandrosa sense lambdes, la configuració en XML o YAML és molt més expressiva, hi ha appenders per a totes les destinacions imaginables, i el suport de log estructurat en JSON ve de sèrie.
Tot el que has après en aquesta lliçó es trasllada tal qual: els nivells, un logger per classe, registrar l'excepció com a argument i no concatenar-la, no registrar dades sensibles, registrar on gestiones. Només canvia l'API. Les llibreries essencials de l'ecosistema, inclosa aquesta, es veuen a 11-07.
- BiblioTech: la refactorització final
Ara tot junt. Primer, el Cataleg amb logging en comptes de println:
package com.nexussoftware.bibliotech.servei;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.Llibre;
import com.nexussoftware.bibliotech.domini.Material;
import com.nexussoftware.bibliotech.domini.MaterialNoTrobatException;
import com.nexussoftware.bibliotech.domini.ReferenciaDuplicadaException;
/**
* Cataleg de BiblioTech, versio final del modul 6.
*
* Canvis respecte a 06-04:
* - Zero System.out.println: tot el diagnostic va al Logger.
* - Els missatges de log porten identificadors, no noms de persones.
* - Els missatges cars usen Supplier per no construir-se si el nivell esta desactivat.
*/
public class Cataleg {
private static final Logger LOG = Logger.getLogger(Cataleg.class.getName());
private final List<Material> materials = new ArrayList<>();
private final Map<String, Material> indexPerReferencia = new HashMap<>();
private final Map<String, String> titolsPerIsbn = new HashMap<>();
private String propietariDelBloqueig = null;
public void registrar(Material material) {
Objects.requireNonNull(material, "El material a registrar no pot ser nul");
String referencia = material.getReferencia();
// FINE: detall de depuracio, desactivat en produccio.
// Amb Supplier, la concatenacio no s'executa si FINE esta apagat.
LOG.fine(() -> "Registrant " + referencia + " (cataleg actual: "
+ materials.size() + " materials)");
Material existent = indexPerReferencia.get(referencia);
if (existent != null) {
// Es registra a nivell FINE i es LLANCA. No es registra a nivell superior
// perque qui capturi decidira la seva gravetat: en una importacio massiva
// un duplicat es rutina, en una alta manual es un avis.
LOG.fine(() -> "Alta rebutjada: referencia " + referencia + " duplicada");
throw new ReferenciaDuplicadaException(
ReferenciaDuplicadaException.Tipus.REFERENCIA, referencia,
existent.getTitol());
}
if (material instanceof Llibre llibre) {
String titolExistent = titolsPerIsbn.get(llibre.getIsbn());
if (titolExistent != null) {
LOG.fine(() -> "Alta rebutjada: ISBN duplicat a " + referencia);
throw new ReferenciaDuplicadaException(
ReferenciaDuplicadaException.Tipus.ISBN, llibre.getIsbn(), titolExistent);
}
titolsPerIsbn.put(llibre.getIsbn(), llibre.getTitol());
}
materials.add(material);
indexPerReferencia.put(referencia, material);
// INFO: fita de negoci. Es llegeix com la historia del que fa l'aplicacio.
LOG.info(() -> "Material registrat: " + referencia
+ " (total " + materials.size() + ")");
}
public Material obtenirPerReferencia(String referencia) {
Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
Material trobat = indexPerReferencia.get(referencia);
if (trobat == null) {
LOG.fine(() -> "Referencia no trobada: " + referencia);
throw new MaterialNoTrobatException(referencia, materials.size());
}
return trobat;
}
public Optional<Material> cercarPerReferencia(String referencia) {
if (referencia == null) { return Optional.empty(); }
return Optional.ofNullable(indexPerReferencia.get(referencia));
}
public void bloquejar(String idPropietari) {
if (propietariDelBloqueig != null && !propietariDelBloqueig.equals(idPropietari)) {
LOG.warning(() -> "Bloqueig denegat a " + idPropietari
+ ": el te " + propietariDelBloqueig);
throw new IllegalStateException(
"El cataleg esta bloquejat per " + propietariDelBloqueig);
}
propietariDelBloqueig = idPropietari;
LOG.fine(() -> "Bloqueig del cataleg adquirit per " + idPropietari);
}
public void desbloquejar(String idPropietari) {
if (!Objects.equals(propietariDelBloqueig, idPropietari)) {
// WARNING: passa alguna cosa rara, pero no impedeix continuar
LOG.warning(() -> "Intent de desbloqueig per " + idPropietari
+ " sense ser el propietari (" + propietariDelBloqueig + ")");
return;
}
propietariDelBloqueig = null;
LOG.fine(() -> "Bloqueig del cataleg alliberat per " + idPropietari);
}
public boolean estaBloquejat() { return propietariDelBloqueig != null; }
public boolean existeix(String r) { return r != null && indexPerReferencia.containsKey(r); }
public int mida() { return materials.size(); }
public List<Material> llistar() { return List.copyOf(materials); }
}Ara GestorPrestecs, amb logging, identificador d'operació i compensació:
package com.nexussoftware.bibliotech.servei;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.*;
/** Orquestra prestecs amb registre complet i estat coherent. */
public class GestorPrestecs {
private static final Logger LOG = Logger.getLogger(GestorPrestecs.class.getName());
private final Cataleg cataleg;
private final Map<String, Empleat> empleats = new HashMap<>();
private final Map<String, Prestec> prestecs = new HashMap<>();
private int comptador = 0;
public GestorPrestecs(Cataleg cataleg) {
this.cataleg = Objects.requireNonNull(cataleg, "El cataleg no pot ser nul");
}
public void donarAlta(Empleat empleat) {
Objects.requireNonNull(empleat, "L'empleat no pot ser nul");
empleats.put(empleat.getIdentificador(), empleat);
// Es registra l'IDENTIFICADOR, no el nom: el nom es una dada personal
LOG.info(() -> "Empleat donat d'alta: " + empleat.getIdentificador());
}
public Prestec prestar(String idOperacio, String referencia, String idEmpleat, int dia) {
Objects.requireNonNull(idOperacio, "L'identificador d'operacio no pot ser nul");
Objects.requireNonNull(referencia, "La referencia no pot ser nulla");
Objects.requireNonNull(idEmpleat, "L'identificador d'empleat no pot ser nul");
if (dia < 1) {
throw new IllegalArgumentException("El dia ha de ser 1 o posterior, i era: " + dia);
}
LOG.fine(() -> "[" + idOperacio + "] Iniciant prestec de " + referencia
+ " a " + idEmpleat + " el dia " + dia);
// --- Fase 1: lectura i validacio (no modifica res) ---
Material material = cataleg.obtenirPerReferencia(referencia);
Empleat empleat = empleats.get(idEmpleat);
if (empleat == null) {
throw new MaterialNoTrobatException(idEmpleat, empleats.size());
}
if (!material.estaDisponible()) {
throw new MaterialNoDisponibleException(referencia, material.getTitol(),
"un altre empleat", dia + Prestec.DIES_PRESTEC);
}
if (!empleat.potPrendrePrestat()) {
throw new LimitPrestecsExceditException(idEmpleat, empleat.getNom(),
empleat.getPrestecsAcumulats(), Empleat.MAX_PRESTECS_SIMULTANIS);
}
// --- Fase 2: modificacio amb compensacio (06-05) ---
boolean materialMarcat = false;
boolean quotaConsumida = false;
boolean completat = false;
String referenciaPrestec = seguentReferencia();
try {
material.prestar();
materialMarcat = true;
empleat.registrarPrestec();
quotaConsumida = true;
Prestec prestec = new Prestec(referenciaPrestec, material, empleat, dia);
prestecs.put(referenciaPrestec, prestec);
completat = true;
LOG.info(() -> "[" + idOperacio + "] Prestec " + referenciaPrestec
+ " registrat: " + referencia + " -> " + idEmpleat + " (dia " + dia + ")");
return prestec;
} finally {
if (!completat) {
// WARNING, no SEVERE: la compensacio es el comportament previst
LOG.warning(() -> "[" + idOperacio + "] Prestec incomplet de " + referencia
+ "; compensant");
try {
if (quotaConsumida) { empleat.registrarDevolucio(); }
if (materialMarcat) { material.retornar(); }
LOG.fine(() -> "[" + idOperacio + "] Estat restaurat");
} catch (RuntimeException fallada) {
// SEVERE de debo: la compensacio ha fallat i l'estat pot
// haver quedat incoherent. Aixo SI que requereix intervencio.
LOG.log(Level.SEVERE, "[" + idOperacio
+ "] LA COMPENSACIO HA FALLAT. Estat possiblement incoherent a "
+ referencia, fallada);
}
}
}
}
public void retornar(String idOperacio, String referenciaPrestec, int dia) {
Prestec prestec = prestecs.get(referenciaPrestec);
if (prestec == null) {
throw new MaterialNoTrobatException(referenciaPrestec, prestecs.size());
}
if (prestec.estaRetornat()) {
throw new PrestecJaRetornatException(referenciaPrestec, prestec.getDiaInici());
}
prestec.registrarDevolucio(dia);
prestec.getTitular().registrarDevolucio();
int retard = prestec.calcularDiesRetard(dia);
if (retard > 0) {
LOG.info(() -> "[" + idOperacio + "] Devolucio de " + referenciaPrestec
+ " amb " + retard + " dies de retard");
} else {
LOG.info(() -> "[" + idOperacio + "] Devolucio de " + referenciaPrestec + " en termini");
}
}
private String seguentReferencia() {
comptador++;
return String.format("PR-%04d", comptador);
}
public int prestecsRegistrats() { return prestecs.size(); }
}I la capa de presentació, que és on es captura i es tradueix:
package com.nexussoftware.bibliotech.presentacio;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.Cataleg;
import com.nexussoftware.bibliotech.servei.GestorPrestecs;
/**
* Capa de presentacio: la UNICA que captura per a l'usuari, l'unica que
* imprimeix per consola el que l'usuari ha demanat veure, i l'unica que decideix
* quin missatge mostrar en cada cas.
*/
public class OperacionsConsola {
private static final Logger LOG = Logger.getLogger(OperacionsConsola.class.getName());
private final GestorPrestecs gestor;
private int comptadorOperacions = 0;
public OperacionsConsola(GestorPrestecs gestor) {
this.gestor = gestor;
}
/**
* Executa un prestec traduint cada fallada a un missatge comprensible.
*
* Aqui SI que es captura: es la frontera amb l'usuari i hi ha decisions a prendre.
*/
public void prestar(String referencia, String idEmpleat, int dia) {
String idOperacio = seguentIdOperacio();
try {
gestor.prestar(idOperacio, referencia, idEmpleat, dia);
// SORTIDA del programa, no log: es el que l'usuari ha demanat veure
System.out.println("Prestat " + referencia + " a " + idEmpleat
+ ". Devolucio prevista: dia " + (dia + Prestec.DIES_PRESTEC));
} catch (MaterialNoTrobatException e) {
// Fallada esperada i corregible: missatge amable, log de baix nivell
System.out.println("No existeix el material '" + e.getReferenciaCercada() + "'.");
if (e.catalegBuit()) {
System.out.println("El cataleg esta buit: carregueu primer els materials.");
}
LOG.fine(() -> "[" + idOperacio + "] Referencia inexistent: "
+ e.getReferenciaCercada());
} catch (MaterialNoDisponibleException e) {
System.out.println("'" + e.getTitol() + "' esta prestat.");
System.out.println("Disponible previsiblement en " + e.diesEspera(dia) + " dies.");
System.out.println("El podeu reservar amb l'opcio 5.");
LOG.fine(() -> "[" + idOperacio + "] Material no disponible: " + e.getReferencia());
} catch (LimitPrestecsExceditException e) {
System.out.printf("Heu assolit el limit de %d prestecs simultanis.%n",
e.getLimit());
System.out.printf("Heu de retornar %d material(s) abans de prendre'n un altre.%n",
e.devolucionsNecessaries());
LOG.fine(() -> "[" + idOperacio + "] Limit excedit per " + e.getIdEmpleat());
} catch (BiblioTechException e) {
// Xarxa de seguretat del domini: capturable gracies a l'arrel comuna (06-04)
System.out.println("No s'ha pogut completar l'operacio: " + e.getMessage());
LOG.log(Level.WARNING, "[" + idOperacio + "] Fallada de domini no prevista ["
+ e.getCodi() + "]", e);
} catch (RuntimeException e) {
// BUG: l'usuari no veu els detalls, el registre els veu tots
System.out.println("S'ha produit un error intern.");
System.out.println("Incidencia: " + idOperacio);
LOG.log(Level.SEVERE, "[" + idOperacio + "] Error no controlat en prestar "
+ referencia + " a " + idEmpleat, e);
}
}
private String seguentIdOperacio() {
comptadorOperacions++;
return String.format("OP-%05d", comptadorOperacions);
}
// ------------------------------------------------------------------
public static void main(String[] args) {
ConfiguracioLog.inicialitzar();
Cataleg cataleg = new Cataleg();
cataleg.registrar(new Llibre("LIB-0001", "Java Eficac", "Bloch", 2018, "978-0000000001"));
cataleg.registrar(new Llibre("LIB-0002", "Patrons de Disseny", "GoF", 1994, "978-0000000002"));
cataleg.registrar(new Llibre("LIB-0003", "Refactoritzacio", "Fowler", 1999, "978-0000000003"));
GestorPrestecs gestor = new GestorPrestecs(cataleg);
gestor.donarAlta(new Empleat("Marta Ruiz", "EMP-001"));
gestor.donarAlta(new Empleat("Diego Alonso", "EMP-002"));
OperacionsConsola consola = new OperacionsConsola(gestor);
System.out.println("\n----- Operacions -----");
consola.prestar("LIB-0001", "EMP-001", 10);
consola.prestar("LIB-0001", "EMP-002", 11); // no disponible
consola.prestar("LIB-9999", "EMP-001", 11); // no trobat
consola.prestar("LIB-0002", "EMP-001", 11);
consola.prestar("LIB-0003", "EMP-001", 11);
consola.prestar("LIB-0002", "EMP-001", 12); // no disponible (ja el te)
}
}Sortida en consola (nivell INFO i superiors del log, més la sortida del programa):
[INFO ] Cataleg Material registrat: LIB-0001 (total 1) [INFO ] Cataleg Material registrat: LIB-0002 (total 2) [INFO ] Cataleg Material registrat: LIB-0003 (total 3) [INFO ] GestorPrestecs Empleat donat d'alta: EMP-001 [INFO ] GestorPrestecs Empleat donat d'alta: EMP-002 ----- Operacions ----- [INFO ] GestorPrestecs [OP-00001] Prestec PR-0001 registrat: LIB-0001 -> EMP-001 (dia 10) Prestat LIB-0001 a EMP-001. Devolucio prevista: dia 25 'Java Eficac' esta prestat. Disponible previsiblement en 14 dies. El podeu reservar amb l'opcio 5. No existeix el material 'LIB-9999'. [INFO ] GestorPrestecs [OP-00004] Prestec PR-0002 registrat: LIB-0002 -> EMP-001 (dia 11) Prestat LIB-0002 a EMP-001. Devolucio prevista: dia 26 [INFO ] GestorPrestecs [OP-00005] Prestec PR-0003 registrat: LIB-0003 -> EMP-001 (dia 11) Prestat LIB-0003 a EMP-001. Devolucio prevista: dia 26 'Patrons de Disseny' esta prestat. Disponible previsiblement en 14 dies. El podeu reservar amb l'opcio 5.
I a bibliotech-0.log, amb tot el detall:
2026-08-05 11:42:07.104 [1 ] CONFIG ...ConfiguracioLog.inicialitzar - Sistema de registre inicialitzat 2026-08-05 11:42:07.118 [1 ] FINE ...Cataleg.registrar - Registrant LIB-0001 (cataleg actual: 0 materials) 2026-08-05 11:42:07.119 [1 ] INFO ...Cataleg.registrar - Material registrat: LIB-0001 (total 1) ... 2026-08-05 11:42:07.201 [1 ] FINE ...GestorPrestecs.prestar - [OP-00002] Iniciant prestec de LIB-0001 a EMP-002 el dia 11 2026-08-05 11:42:07.202 [1 ] FINE ...OperacionsConsola.prestar - [OP-00002] Material no disponible: LIB-0001 2026-08-05 11:42:07.205 [1 ] FINE ...Cataleg.obtenirPerReferencia - Referencia no trobada: LIB-9999 2026-08-05 11:42:07.206 [1 ] FINE ...OperacionsConsola.prestar - [OP-00003] Referencia inexistent: LIB-9999
Compara la consola i el fitxer. La consola mostra el que l'usuari necessita: missatges clars i les fites importants. El fitxer conté la història completa, amb marca de temps, fil, classe, mètode i identificador d'operació, filtrable per qualsevol d'aquells camps. I no hi ha un sol System.out.println de diagnòstic a tot el projecte.
Errors Comuns i Consells
Capturar on es produeix en comptes d'on es decideix. Un catch que només imprimeix i retorna null degrada una excepció amb nom i dades al null mut que va costar dues lliçons eliminar. Si no pots decidir, no capturis.
Mostrar un stack trace a l'usuari. És un risc de seguretat —revela paquets, versions i camins—, no li serveix de res i destrueix la confiança. Missatge clar més identificador d'incidència; el trace, al registre.
Registrar i rellançar a cada capa. Una fallada apareix cinc vegades amb cinc traces gairebé idèntiques. Registra on gestiones.
Registrar-ho tot com a SEVERE. Quan tot és un error, ningú no mira els errors. SEVERE és només per al que requereix que algú intervingui.
Concatenar a les crides de log de nivell baix. LOG.fine("... " + x + " ...") construeix la cadena encara que FINE estigui desactivat. Fes servir la forma amb Supplier: LOG.fine(() -> "...").
Registrar objectes complets. LOG.fine("Empleat: " + empleat) imprimirà el que retorni toString() avui i d'aquí a dos anys, quan algú hi afegeixi el DNI. Registra camps concrets.
Registrar credencials, tokens o dades personals. Els logs es copien, s'envien i s'arxiven durant mesos. És un incident de seguretat, i en un sistema real la política la marca el responsable de protecció de dades, no tu.
printStackTrace() en producció. Va a System.err sense nivell, sense data, sense filtre i sense context. Fes servir LOG.log(Level.X, "missatge", e).
Reintentar operacions no idempotents. Si el préstec es va registrar i només va fallar la confirmació, el reintent crea un segon préstec. Només es reintenta el que es pot repetir sense efectes secundaris.
Confiar en la validació prèvia amb recursos compartits. Entre l'exists() i l'open() el fitxer pot desaparèixer. És TOCTOU, i l'excepció cal gestionar-la de totes maneres.
Un Logger no estàtic o creat a cada crida. Obtenir-lo té cost. private static final Logger LOG = Logger.getLogger(LaMevaClasse.class.getName());, una vegada per classe.
Consell: fes servir un identificador d'operació a tots els missatges d'un mateix flux. Converteix un log il·legible en una història que es pot seguir amb un sol filtre.
Consell: llegeix el teu propi log com si fossis el de guàrdia a les tres de la matinada. Si no pots reconstruir què ha passat, falten dades. Si hi ha tant soroll que no trobes res, sobren línies a INFO.
Consell: configura per fitxer, no en codi. Poder pujar el nivell a FINE en un paquet concret sense recompilar és el que salva una investigació en producció.
Consell: en passar a SLF4J a 11-07, tot això es trasllada tal qual. Només canvia l'API; els criteris són els mateixos.
Exercicis
Exercici 1: la política d'errors de cada capa
Per a cadascun dels vuit escenaris, respon: (a) en quina capa es detecta, (b) en quina capa s'ha de capturar, (c) quin nivell de log correspon, (d) quin missatge exacte veu l'usuari i (e) quin missatge exacte va al registre. Justifica cada decisió.
- La Marta escriu
"dotze"on es demanava el nombre de dies. LlibrerepanyPublicacio = 1200al seu constructor.- El fitxer
cataleg.txtno existeix en arrencar. - Un
NullPointerExceptionaPrestec.descripcioTitularperquè el titular ésnull. - El Diego intenta el seu quart préstec simultani.
- El servei d'avisos no respon en notificar un venciment.
- El disc s'omple mentre s'exporta el catàleg.
OutOfMemoryErrordurant una importació massiva.
Després, escriu la classe PoliticaErrors amb un mètode String decidir(Throwable t) que, donat un Throwable, retorni una recomanació amb el nivell de log, si el missatge és apte per a l'usuari i l'acció suggerida. Recolza't en BiblioTechException.esCorregiblePerUsuari().
Exercici 2: RegistreOperacions amb java.util.logging
Escriu RegistreOperacions a com.nexussoftware.bibliotech.servei, que embolcalli qualsevol operació de BiblioTech aportant registre complet i mesurament.
Requisits:
<T> T executar(String nomOperacio, String idEmpleat, Supplier<T> operacio):- Genera un identificador d'operació
OP-NNNNN. - Registra a
FINEl'inici amb l'identificador, el nom i l'empleat. - Mesura la durada amb
System.nanoTime(). - Si té èxit: registra a
INFOel final amb la durada en mil·lisegons. - Si llança una
BiblioTechException: la registra aWARNINGamb l'excepció com a tercer argument i la rellança. - Si llança qualsevol altra
RuntimeException: la registra aSEVEREamb l'identificador com a incidència i l'embolcalla en unaIllegalStateExceptionel missatge de la qual inclogui la incidència, conservant la causa. - Fes servir
try-finallyperquè la durada es registri també quan falla.
- Genera un identificador d'operació
- Un mètode
void bolcarEstadistiques()que mostri, per nom d'operació: nombre d'execucions, èxits, fallades i durada mitjana. - Tots els missatges han de fer servir la forma mandrosa amb
Supplier. - No ha de registrar el nom de cap empleat, només el seu identificador.
Al main, configura el logging amb dos handlers (consola a INFO, fitxer a FINE), executa almenys sis operacions —correctes, amb fallada de domini i amb bug— i mostra les estadístiques.
Exercici 3: auditoria de gestió d'errors
Aquest és el MenuBiblioTech que ha sobreviscut a tot el mòdul sense revisar-se. Té deu problemes de gestió d'errors i de logging. Troba'ls, explica el mal de cadascun i reescriu la classe aplicant tot el que has après al mòdul.
public class MenuBiblioTech {
public void arrencar() {
while (true) {
try {
System.out.print("Opcio: ");
int opcio = Integer.parseInt(new Scanner(System.in).nextLine());
switch (opcio) {
case 1: prestar(); break;
case 2: retornar(); break;
case 0: System.exit(0);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
private void prestar() throws Exception {
Scanner sc = new Scanner(System.in);
System.out.print("Referencia: ");
String ref = sc.nextLine();
System.out.print("Empleat: ");
String emp = sc.nextLine();
try {
gestor.prestar(ref, emp, 1);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
System.out.println(e.getStackTrace()[0]);
throw e;
} finally {
sc.close();
}
}
private void retornar() {
try {
gestor.retornar(llegirReferencia(), 1);
} catch (Exception e) {
}
System.out.println("Devolucio processada");
}
}Pista sobre dos d'ells que són fàcils de passar per alt: un té a veure amb on es crea el Scanner i un altre amb el que l'últim mètode afirma a l'usuari.
Solucions
Solució 1
Taula de decisions:
| # | Escenari | (a) Es detecta | (b) Es captura | (c) Nivell | (d) Usuari | (e) Registre |
|---|---|---|---|---|---|---|
| 1 | "dotze" com a nombre de dies |
Presentació (parseInt) |
Presentació | FINE |
"'dotze' no es un numero. Escriviu un enter entre 1 i 30." |
[OP-00012] Entrada no numerica a dies: 'dotze' |
| 2 | anyPublicacio = 1200 |
Domini (constructor) | Presentació o importador | WARNING en lot, FINE en alta manual |
"L'any ha d'estar entre 1450 i 2100. Heu escrit 1200." |
Alta rebutjada de LIB-0007: any 1200 fora de rang |
| 3 | No existeix cataleg.txt |
Persistència | main/arrencada |
WARNING |
"No hi havia cataleg previ. Es comenca de zero." |
No s'ha pogut carregar el cataleg des de 'cataleg.txt' + stack trace |
| 4 | NullPointerException per titular nul |
Domini | Frontera | SEVERE |
"Error intern. Incidencia INC-A3F91C0B." |
[INC-A3F91C0B] Error no controlat en emetre rebut + trace complet |
| 5 | Quart préstec del Diego | Domini (Empleat) |
Presentació | FINE |
"Heu assolit el limit de 3 prestecs. Retorneu 1 material." |
[OP-00018] Limit excedit per EMP-002 (3/3) |
| 6 | Servei d'avisos no respon | Servei | Servei (degradació) | WARNING |
(res, o "els avisos s'enviaran més tard") | Servei d'avisos no disponible; 3 avisos en cua + trace |
| 7 | Disc ple en exportar | Persistència (IOException) |
Presentació | SEVERE |
"No s'ha pogut exportar el cataleg. Comproveu l'espai al disc." |
Fallada d'E/S en exportar a 'cataleg-export.txt' + trace |
| 8 | OutOfMemoryError |
JVM | Només la frontera | SEVERE |
"Error irrecuperable. L'aplicacio es tancara." |
Error irrecuperable + classe i trace, amb el registre protegit |
Justificacions dels casos menys evidents:
Cas 2: el nivell depèn del context, no del tipus. En una importació massiva, un any invàlid en una línia de mil és rutina i mereix WARNING a l'informe agregat; en una alta manual és un error de l'usuari i n'hi ha prou amb FINE. És la mateixa excepció amb dues polítiques, com a l'exercici 3 de 06-02.
Cas 4: un NullPointerException és sempre un bug, mai una cosa que l'usuari pugui corregir. D'aquí SEVERE i el missatge genèric amb incidència: l'usuari no ha de veure Prestec.java:97 ni saber que existeix una classe Prestec.
Cas 6: es captura a servei, no a presentació, perquè és allà on hi ha el coneixement per degradar: acumular els avisos en cua i continuar. La presentació no té per què assabentar-se que existeix un servei d'avisos.
Cas 7: SEVERE encara que l'usuari pugui actuar, perquè un disc ple afecta tot el sistema i algú d'operacions se n'ha d'assabentar.
Cas 8: l'únic cas en què es captura un Error, i només a la frontera, només per deixar constància abans de morir, i amb el propi registre protegit perquè escriure el log també pot fallar sense memòria.
package com.nexussoftware.bibliotech.servei;
import java.io.IOException;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import com.nexussoftware.bibliotech.domini.BiblioTechException;
/**
* Classifica un Throwable i recomana com tractar-lo.
* Materialitza la taula de decisions d'aquesta llico.
*/
public final class PoliticaErrors {
/** Recomanacio completa per a una fallada concreta (record, 04-07). */
public record Recomanacio(Level nivell, boolean missatgeAptePerUsuari,
boolean requereixIncidencia, String accio) {
@Override
public String toString() {
return String.format("nivell=%-7s | usuari=%-5s | incidencia=%-5s | %s",
nivell.getName(), missatgeAptePerUsuari, requereixIncidencia, accio);
}
}
private PoliticaErrors() { }
public static Recomanacio decidir(Throwable t) {
if (t == null) {
return new Recomanacio(Level.INFO, false, false, "Res a fer");
}
// 1. Error de la JVM: irrecuperable
if (t instanceof Error) {
return new Recomanacio(Level.SEVERE, false, true,
"Registrar (protegit) i acabar de manera ordenada. NO continuar.");
}
// 2. Excepcio del domini: fallada esperada, missatge ja escrit per a l'usuari
if (t instanceof BiblioTechException bte) {
if (bte.esCorregiblePerUsuari()) {
return new Recomanacio(Level.FINE, true, false,
"Mostrar el missatge tal qual i permetre reintentar.");
}
return new Recomanacio(Level.WARNING, true, false,
"Mostrar el missatge i indicar que no depen de l'usuari.");
}
// 3. Fallades d'E/S: condicio de l'entorn
if (t instanceof IOException) {
return new Recomanacio(Level.SEVERE, false, true,
"Degradar si hi ha alternativa; si no, avortar l'operacio. Avisar operacions.");
}
// 4. Validacions tecniques frequents a la frontera amb l'usuari
if (t instanceof NumberFormatException) {
return new Recomanacio(Level.FINE, true, false,
"Demanar la dada de nou indicant el format esperat.");
}
if (t instanceof NoSuchElementException) {
return new Recomanacio(Level.FINE, true, false,
"Informar que no s'ha trobat i oferir alternatives.");
}
// 5. Fallades de programacio: l'usuari no ha de veure detalls
if (t instanceof NullPointerException
|| t instanceof IndexOutOfBoundsException
|| t instanceof ClassCastException) {
return new Recomanacio(Level.SEVERE, false, true,
"BUG: missatge generic amb incidencia, stack trace complet al registre.");
}
// 6. Resta de no comprovades: probablement tambe un bug
if (t instanceof RuntimeException) {
return new Recomanacio(Level.SEVERE, false, true,
"Tractar com a bug fins que es demostri el contrari.");
}
// 7. Comprovades no contemplades
return new Recomanacio(Level.WARNING, false, true,
"Condicio externa: registrar amb detall i decidir a la capa superior.");
}
public static void main(String[] args) {
Throwable[] casos = {
new NumberFormatException("For input string: \"dotze\""),
new IllegalArgumentException("L'any ha d'estar entre 1450 i 2100, i era: 1200"),
new IOException("cataleg.txt (No such file or directory)"),
new NullPointerException("Cannot invoke \"Empleat.getNom()\""),
new OutOfMemoryError("Java heap space"),
new NoSuchElementException("No line found")
};
System.out.printf("%-32s %s%n", "EXCEPCIO", "RECOMANACIO");
System.out.println("-".repeat(120));
for (Throwable t : casos) {
System.out.printf("%-32s %s%n", t.getClass().getSimpleName(), decidir(t));
}
}
}Sortida:
EXCEPCIO RECOMANACIO ------------------------------------------------------------------------------------------------------------------------ NumberFormatException nivell=FINE | usuari=true | incidencia=false | Demanar la dada de nou indicant el format esperat. IllegalArgumentException nivell=SEVERE | usuari=false | incidencia=true | Tractar com a bug fins que es demostri el contrari. IOException nivell=SEVERE | usuari=false | incidencia=true | Degradar si hi ha alternativa; si no, avortar l'operacio. Avisar operacions. NullPointerException nivell=SEVERE | usuari=false | incidencia=true | BUG: missatge generic amb incidencia, stack trace complet al registre. OutOfMemoryError nivell=SEVERE | usuari=false | incidencia=true | Registrar (protegit) i acabar de manera ordenada. NO continuar. NoSuchElementException nivell=FINE | usuari=true | incidencia=false | Informar que no s'ha trobat i oferir alternatives.
Una troballa interessant del mateix exercici: l'IllegalArgumentException amb el missatge de l'any invàlid cau a la categoria "bug", quan en realitat era una validació de negoci perfectament esperada. És exactament l'argument a favor de les excepcions pròpies de 06-04: si aquella validació llancés AnyPublicacioInvalidException extends CatalegException, la política la classificaria bé automàticament. Amb excepcions estàndard, la classificació automàtica és impossible.
Solució 2
package com.nexussoftware.bibliotech.servei;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.BiblioTechException;
/**
* Embolcalla operacions de BiblioTech aportant registre, mesurament i
* politica uniforme d'errors.
*
* Es un exemple del patro "decorador de serveis" que al modul 11
* fan els frameworks amb aspectes i anotacions.
*/
public class RegistreOperacions {
private static final Logger LOG = Logger.getLogger(RegistreOperacions.class.getName());
/** Estadistiques acumulades d'una operacio (mutable a proposit). */
private static class Estadistica {
int execucions;
int exits;
int fallades;
long nanosTotals;
double mitjanaMs() {
return (execucions == 0) ? 0.0 : (nanosTotals / 1_000_000.0) / execucions;
}
}
private final Map<String, Estadistica> estadistiques = new HashMap<>();
private int comptador = 0;
/**
* Executa una operacio registrant inici, final, durada i fallades.
*
* @param nomOperacio nom logic (PRESTAR, RETORNAR, IMPORTAR...)
* @param idEmpleat IDENTIFICADOR de l'empleat, mai el seu nom
* @param operacio l'operacio a executar
*/
public <T> T executar(String nomOperacio, String idEmpleat, Supplier<T> operacio) {
String idOperacio = seguentId();
long inici = System.nanoTime();
boolean exit = false;
// Forma mandrosa: si FINE esta desactivat, la cadena no es construeix
LOG.fine(() -> "[" + idOperacio + "] INICI " + nomOperacio
+ " (empleat " + idEmpleat + ")");
try {
T resultat = operacio.get();
exit = true;
return resultat;
} catch (BiblioTechException e) {
// Fallada ESPERADA del domini: WARNING amb l'excepcio com a
// tercer argument, i es RELLANCA perque decideixi qui cridi.
LOG.log(Level.WARNING, "[" + idOperacio + "] " + nomOperacio
+ " rebutjada [" + e.getCodi() + "]", e);
throw e;
} catch (RuntimeException e) {
// Fallada INESPERADA: es un bug. SEVERE amb l'identificador com a
// incidencia, i s'EMBOLCALLA conservant la causa (06-03).
LOG.log(Level.SEVERE, "[" + idOperacio + "] Error no controlat a "
+ nomOperacio, e);
throw new IllegalStateException(
"Error intern a " + nomOperacio + ". Incidencia: " + idOperacio, e);
} finally {
// La durada es registra SEMPRE, amb exit i amb fallada (06-05)
long duracio = System.nanoTime() - inici;
acumular(nomOperacio, exit, duracio);
final boolean hiHaHagutExit = exit;
final double ms = duracio / 1_000_000.0;
if (hiHaHagutExit) {
LOG.info(() -> String.format("[%s] FI %s OK (%.2f ms)",
idOperacio, nomOperacio, ms));
} else {
LOG.fine(() -> String.format("[%s] FI %s FALLIDA (%.2f ms)",
idOperacio, nomOperacio, ms));
}
}
}
/** Variant sense resultat. */
public void executarSenseResultat(String nomOperacio, String idEmpleat, Runnable operacio) {
executar(nomOperacio, idEmpleat, () -> {
operacio.run();
return null;
});
}
private synchronized void acumular(String nom, boolean exit, long nanos) {
Estadistica e = estadistiques.computeIfAbsent(nom, n -> new Estadistica());
e.execucions++;
e.nanosTotals += nanos;
if (exit) { e.exits++; } else { e.fallades++; }
}
private synchronized String seguentId() {
comptador++;
return String.format("OP-%05d", comptador);
}
public void bolcarEstadistiques() {
System.out.println();
System.out.printf("%-14s %-12s %-9s %-9s %s%n",
"OPERACIO", "EXECUCIONS", "EXITS", "FALLADES", "MITJANA (ms)");
System.out.println("-".repeat(62));
estadistiques.forEach((nom, e) ->
System.out.printf("%-14s %-12d %-9d %-9d %.2f%n",
nom, e.execucions, e.exits, e.fallades, e.mitjanaMs()));
}
// ------------------------------------------------------------------
public static void main(String[] args) {
com.nexussoftware.bibliotech.presentacio.ConfiguracioLog.inicialitzar();
Cataleg cataleg = new Cataleg();
cataleg.registrar(new com.nexussoftware.bibliotech.domini.Llibre(
"LIB-0001", "Java Eficac", "Bloch", 2018, "978-0000000001"));
cataleg.registrar(new com.nexussoftware.bibliotech.domini.Llibre(
"LIB-0002", "Patrons de Disseny", "GoF", 1994, "978-0000000002"));
RegistreOperacions registre = new RegistreOperacions();
System.out.println("\n----- Operacions -----");
// 1 i 2: correctes
registre.executar("CONSULTAR", "EMP-001",
() -> cataleg.obtenirPerReferencia("LIB-0001").getTitol());
registre.executar("CONSULTAR", "EMP-002",
() -> cataleg.obtenirPerReferencia("LIB-0002").getTitol());
// 3: fallada de domini esperada, es rellanca tal qual
try {
registre.executar("CONSULTAR", "EMP-001",
() -> cataleg.obtenirPerReferencia("LIB-9999").getTitol());
} catch (BiblioTechException e) {
System.out.println("Rebutjada: " + e.getMessage());
}
// 4: alta correcta
registre.executarSenseResultat("ALTA", "EMP-003", () ->
cataleg.registrar(new com.nexussoftware.bibliotech.domini.Llibre(
"LIB-0003", "Refactoritzacio", "Fowler", 1999, "978-0000000003")));
// 5: alta duplicada, fallada de domini
try {
registre.executarSenseResultat("ALTA", "EMP-003", () ->
cataleg.registrar(new com.nexussoftware.bibliotech.domini.Llibre(
"LIB-0001", "Copia", "X", 2020, "978-0000000009")));
} catch (BiblioTechException e) {
System.out.println("Rebutjada: " + e.getMessage());
}
// 6: BUG simulat, s'embolcalla amb incidencia
try {
registre.executar("INFORME", "EMP-001", () -> {
String nul = null;
return nul.length(); // NullPointerException
});
} catch (IllegalStateException e) {
System.out.println("Error intern: " + e.getMessage());
System.out.println(" causa registrada: " + e.getCause().getClass().getSimpleName());
}
registre.bolcarEstadistiques();
}
}Sortida (consola, nivell INFO):
----- Operacions -----
[INFO ] RegistreOperacions [OP-00001] FI CONSULTAR OK (0,08 ms)
[INFO ] RegistreOperacions [OP-00002] FI CONSULTAR OK (0,01 ms)
[WARNING] RegistreOperacions [OP-00003] CONSULTAR rebutjada [MATERIALNOTROBAT]
causa: com.nexussoftware.bibliotech.domini.MaterialNoTrobatException: No existeix...
Rebutjada: No existeix cap material amb la referencia 'LIB-9999' (el cataleg te 2 materials)
[INFO ] Cataleg Material registrat: LIB-0003 (total 3)
[INFO ] RegistreOperacions [OP-00004] FI ALTA OK (0,15 ms)
[WARNING] RegistreOperacions [OP-00005] ALTA rebutjada [REFERENCIADUPLICADA]
Rebutjada: Ja existeix un material amb la referencia LIB-0001: 'Java Eficac'
[SEVERE ] RegistreOperacions [OP-00006] Error no controlat a INFORME
Error intern: Error intern a INFORME. Incidencia: OP-00006
causa registrada: NullPointerException
OPERACIO EXECUCIONS EXITS FALLADES MITJANA (ms)
--------------------------------------------------------------
CONSULTAR 3 2 1 0.04
ALTA 2 1 1 0.09
INFORME 1 0 1 0.05Les tres decisions de disseny de l'exercici:
- Les fallades de domini es rellancen tal qual; els bugs s'embolcallen. Les primeres ja tenen un missatge apte per a l'usuari i un tipus que la presentació pot distingir; els segons necessiten una incidència i amagar els detalls.
- El nivell reflecteix qui ho ha de mirar.
WARNINGper a un rebuig de negoci —algú podria voler saber quants n'hi ha— iSEVEREper a un bug, que requereix intervenció de desenvolupament. - La durada es mesura al
finally, així que es registra també a les fallades. Saber que una operació ha trigat 4 segons abans de fallar és informació valuosa: suggereix un temps d'espera esgotat i no un rebuig immediat.
Solució 3
Els deu problemes:
| # | Problema | Mal |
|---|---|---|
| 1 | catch (Throwable t) al bucle |
Atrapa OutOfMemoryError i StackOverflowError i continua el bucle, amb la JVM ja inservible |
| 2 | t.printStackTrace() com a gestió |
Bolca un trace cru a la cara de l'usuari: risc de seguretat, inútil per a ell i sense registre |
| 3 | new Scanner(System.in) dins del bucle |
Crea un Scanner nou a cada volta. Amb el sc.close() de prestar() es tanca System.in i totes les lectures posteriors llancen NoSuchElementException |
| 4 | sc.close() al finally tancant System.in |
L'error de 06-06: tanca l'entrada estàndard de tot el procés |
| 5 | System.exit(0) dins del try |
Salta els finally (06-05) i qualsevol neteja pendent |
| 6 | switch sense default |
Una opció invàlida no produeix cap efecte ni missatge: l'usuari creu que no ha funcionat el teclat |
| 7 | catch (Exception e) + throw e a prestar |
Captura massa ample i a sobre rellança després d'imprimir: registre duplicat i el throws Exception contamina la signatura |
| 8 | System.out.println(e.getStackTrace()[0]) |
Filtra la classe i la línia internes a l'usuari. I llança ArrayIndexOutOfBoundsException si el trace està buit (el fast throw de 06-01) |
| 9 | catch (Exception e) { } buit a retornar |
El pitjor error del mòdul: la fallada desapareix sense deixar rastre |
| 10 | "Devolucio processada" fora del try |
S'imprimeix encara que la devolució hagi fallat. L'aplicació menteix a l'usuari |
Hi ha un onzè problema estructural: no existeix cap Logger. Tot el diagnòstic és printStackTrace i println, així que no hi ha res per filtrar, arxivar ni analitzar.
Versió corregida:
package com.nexussoftware.bibliotech.presentacio;
import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.nexussoftware.bibliotech.domini.*;
import com.nexussoftware.bibliotech.servei.GestorPrestecs;
/**
* Menu principal de BiblioTech, versio final del modul 6.
*
* Politica d'errors:
* - Un unic Scanner d'instancia sobre System.in, que MAI no es tanca.
* - Es captura per TIPUS, del mes especific al mes general.
* - Les fallades de domini produeixen missatges per a l'usuari i log FINE.
* - Els bugs produeixen un missatge generic amb incidencia i log SEVERE.
* - No es captura Throwable: els Error pugen a la frontera del main.
* - Zero System.out.println de diagnostic.
*/
public class MenuBiblioTech {
private static final Logger LOG = Logger.getLogger(MenuBiblioTech.class.getName());
private static final int DIES_PRESTEC = 15;
/** UN SOL Scanner, creat una vegada i mai tancat (06-06). */
private final Scanner scanner = new Scanner(System.in);
private final GestorPrestecs gestor;
private int comptadorOperacions = 0;
private boolean sortir = false;
public MenuBiblioTech(GestorPrestecs gestor) {
this.gestor = gestor;
}
public void arrencar() {
LOG.info("Menu de BiblioTech iniciat");
while (!sortir) {
mostrarMenu();
int opcio = llegirOpcio(0, 3);
// Cada opcio gestiona ELS SEUS PROPIS errors: aqui no hi ha xarxa que
// capturi de tot, perque una xarxa aixi amagaria bugs.
switch (opcio) {
case 1 -> prestar();
case 2 -> retornar();
case 3 -> llistar();
case 0 -> sortir();
default -> System.out.println("Opcio no reconeguda: " + opcio);
}
}
LOG.info("Menu de BiblioTech finalitzat");
}
private void mostrarMenu() {
System.out.println("""
===== BiblioTech - Nexus Software =====
1. Prestar material
2. Retornar material
3. Llistar cataleg
0. Sortir
=======================================""");
}
/** Reintent indefinit: hi ha una persona davant que pot corregir (06-02). */
private int llegirOpcio(int min, int max) {
while (true) {
System.out.print("Opcio: ");
String linia = scanner.nextLine().trim();
try {
int valor = Integer.parseInt(linia);
if (valor < min || valor > max) {
System.out.printf("Ha d'estar entre %d i %d.%n", min, max);
continue;
}
return valor;
} catch (NumberFormatException e) {
System.out.println("'" + linia + "' no es un numero. Escriviu un digit de "
+ min + " a " + max + ".");
LOG.fine(() -> "Entrada no numerica al menu: '" + linia + "'");
}
}
}
private void prestar() {
String idOperacio = seguentIdOperacio();
System.out.print("Referencia del material: ");
String referencia = scanner.nextLine().trim();
System.out.print("Identificador de l'empleat: ");
String idEmpleat = scanner.nextLine().trim();
int dia = llegirEnterAmbLimit("Dia del prestec (1-365)", 1, 365);
if (dia == -1) {
System.out.println("Operacio cancellada.");
return;
}
try {
gestor.prestar(idOperacio, referencia, idEmpleat, dia);
// SORTIDA del programa (el que l'usuari ha demanat veure), no log
System.out.println("Prestat " + referencia + ". Devolucio prevista: dia "
+ (dia + DIES_PRESTEC));
} catch (MaterialNoTrobatException e) {
System.out.println("No existeix el material '" + e.getReferenciaCercada() + "'.");
LOG.fine(() -> "[" + idOperacio + "] Referencia inexistent: "
+ e.getReferenciaCercada());
} catch (MaterialNoDisponibleException e) {
System.out.println("'" + e.getTitol() + "' esta prestat.");
System.out.println("Disponible d'aqui a uns " + e.diesEspera(dia) + " dies.");
LOG.fine(() -> "[" + idOperacio + "] Material no disponible: " + e.getReferencia());
} catch (LimitPrestecsExceditException e) {
System.out.printf("Limit de %d prestecs assolit. Retorneu %d material(s).%n",
e.getLimit(), e.devolucionsNecessaries());
LOG.fine(() -> "[" + idOperacio + "] Limit excedit per " + e.getIdEmpleat());
} catch (BiblioTechException e) {
System.out.println("No s'ha pogut completar el prestec: " + e.getMessage());
LOG.log(Level.WARNING, "[" + idOperacio + "] Fallada de domini no prevista ["
+ e.getCodi() + "]", e);
} catch (RuntimeException e) {
// BUG. L'usuari veu una incidencia; el registre, tot el detall.
System.out.println("Error intern. Incidencia: " + idOperacio);
LOG.log(Level.SEVERE, "[" + idOperacio + "] Error no controlat en prestar "
+ referencia + " a " + idEmpleat, e);
}
// NO es captura Throwable: un Error ha de pujar a la frontera del main.
}
private void retornar() {
String idOperacio = seguentIdOperacio();
System.out.print("Referencia del prestec (PR-NNNN): ");
String referenciaPrestec = scanner.nextLine().trim();
int dia = llegirEnterAmbLimit("Dia de la devolucio (1-365)", 1, 365);
if (dia == -1) {
System.out.println("Operacio cancellada.");
return;
}
try {
gestor.retornar(idOperacio, referenciaPrestec, dia);
// DINS del try: nomes s'afirma l'exit si de debo n'hi ha hagut
System.out.println("Devolucio de " + referenciaPrestec + " processada.");
} catch (PrestecJaRetornatException e) {
System.out.println("Aquell prestec ja es va retornar el dia "
+ e.getDiaDevolucioOriginal() + ".");
LOG.fine(() -> "[" + idOperacio + "] Devolucio duplicada de "
+ e.getReferenciaPrestec());
} catch (MaterialNoTrobatException e) {
System.out.println("No existeix el prestec '" + e.getReferenciaCercada() + "'.");
LOG.fine(() -> "[" + idOperacio + "] Prestec inexistent: "
+ e.getReferenciaCercada());
} catch (BiblioTechException e) {
System.out.println("No s'ha pogut completar la devolucio: " + e.getMessage());
LOG.log(Level.WARNING, "[" + idOperacio + "] Fallada de domini a la devolucio", e);
} catch (RuntimeException e) {
System.out.println("Error intern. Incidencia: " + idOperacio);
LOG.log(Level.SEVERE, "[" + idOperacio + "] Error no controlat en retornar "
+ referenciaPrestec, e);
}
}
private void llistar() {
System.out.println("(llistat del cataleg)");
}
/**
* Sortida ORDENADA: es marca el marcador i el bucle acaba sol.
* Res de System.exit aqui: aixo saltaria els finally i la neteja (06-05).
*/
private void sortir() {
sortir = true;
System.out.println("Fins aviat.");
}
private int llegirEnterAmbLimit(String peticio, int min, int max) {
for (int intent = 1; intent <= 3; intent++) {
System.out.print(peticio + ": ");
String linia;
try {
linia = scanner.nextLine().trim();
} catch (NoSuchElementException e) {
// Passa si System.in es tanca o arriba al final (entrada redirigida)
LOG.log(Level.WARNING, "Entrada estandard esgotada; es cancella l'operacio", e);
return -1;
}
try {
int valor = Integer.parseInt(linia);
if (valor < min || valor > max) {
System.out.printf("Fora de rang [%d..%d]. Intent %d de 3.%n",
min, max, intent);
continue;
}
return valor;
} catch (NumberFormatException e) {
System.out.printf("'%s' no es un numero. Intent %d de 3.%n", linia, intent);
}
}
return -1;
}
private String seguentIdOperacio() {
comptadorOperacions++;
return String.format("OP-%05d", comptadorOperacions);
}
}Els onze problemes resolts, un a un:
- Es captura per tipus, mai
Throwable. ElsErrorpugen a la frontera delmain. - Ni un sol
printStackTrace: tot va alLoggeramb el seu nivell. - Un sol
Scannerd'instància, creat al camp. - Mai no es tanca
System.in. System.exitsubstituït per un marcador i una sortida ordenada del bucle.switchambdefaultque informa.- Captures específiques, sense rellançar, i
prestar()ja no declarathrows. - Zero informació interna en pantalla: identificador d'incidència i res més.
- Cap
catchbuit. - La confirmació d'èxit està dins del
try. - Existeix un
Loggerper classe, amb nivells distingits i missatges mandrosos.
Conclusió
Has tancat el mòdul amb la part que converteix el coneixement tècnic en programari de producció.
En estratègia, tens la regla que governa tota la resta: captura on puguis decidir, no on es produeix, amb les seves tres preguntes —puc fer alguna cosa diferent de propagar?, tinc context per afegir?, soc la frontera?— i la conseqüència que no capturar és una decisió activa i moltes vegades la correcta. Coneixes la política d'errors de cada capa de BiblioTech: el domini valida, llança i mai no registra ni imprimeix, perquè no sap qui hi ha a l'altre costat; el servei aplica regles, tradueix conservant la causa i garanteix estat coherent; la presentació és el filtre que captura tot el que és esperable i decideix quin missatge mostrar.
Saps construir la frontera d'errors del main, amb els seus tres catch ordenats que distingeixen una fallada de domini (missatge net) d'un bug (incidència i silenci sobre els detalls) i d'un Error (registre protegit i sortida immediata) —l'única excepció legítima a la regla de no capturar Error—, amb codis de sortida diferents perquè un script pugui reaccionar. I saps instal·lar el gestor global amb Thread.setDefaultUncaughtExceptionHandler, que impedeix que una excepció escapada d'un fil secundari mati aquell fil en silenci, juntament amb el hook d'aturada de 06-05.
Tens clara la separació entre el que veu l'usuari —missatge en el seu vocabulari, què fer, identificador d'incidència— i el que va al registre —classe, stack trace, camins, context—, amb el motiu de fons: un stack trace en pantalla és un risc de seguretat abans que una molèstia. I l'identificador d'incidència com a peça que uneix tots dos mons sense exposar res.
Distingeixes recuperable d'irrecuperable amb el criteri que decideix: degrada quan el servei reduït continuï sent correcte; avorta quan continuar produiria resultats incorrectes —arrencar sense catàleg és correcte, calcular multes amb una tarifa desconeguda no—. Coneixes les quatre condicions d'un reintent sensat, inclosa la que més s'oblida: la idempotència de l'operació. Saps quan validar prèviament i quan capturar, amb l'advertència de TOCTOU que fa obligatòria l'excepció quan el recurs és compartit. I coneixes les alternatives a llançar: Optional per a l'absència normal de valor i l'objecte resultat per acumular diversos errors alhora en comptes d'avortar al primer.
En logging, tens els set motius pels quals System.out.println no serveix en producció —no es filtra, no es desactiva, no porta data ni nivell ni origen, no s'arxiva i bloqueja—, amb la distinció que evita l'excés contrari: System.out continua sent correcte per a la sortida que l'usuari ha demanat; el que se substitueix és el diagnòstic. Coneixes la taula de nivells i què mereix cadascun, amb l'advertència que quan tot és SEVERE, ningú no mira els errors i que una excepció gestionada correctament poques vegades ho és.
Domines java.util.logging a la pràctica: un Logger static final per classe anomenat amb la classe completa, la jerarquia per paquets que permet activar FINE només a la capa de servei, la forma mandrosa amb Supplier que evita construir missatges que ningú no llegirà, els Handler de consola i de fitxer amb rotació, els Formatter propis, i la configuració per logging.properties que permet pujar el nivell en producció sense recompilar. Saps registrar una excepció correctament —LOG.log(Level.X, "missatge", e), amb el Throwable com a tercer argument— i per què les quatre alternatives són pitjors, inclosa la de registrar i rellançar a cada capa. Saps què no registrar mai, amb el parany del toString() que avui és inofensiu i d'aquí a dos anys inclou un DNI, la tècnica d'emmascarar, i l'advertència formal que en un sistema real això ho revisa el responsable de protecció de dades, no tu. I saps escriure missatges útils amb identificadors en comptes de noms, i correlacionar-los amb un identificador d'operació. Amb el mapa de l'ecosistema real: SLF4J com a façana, Logback i Log4j2 com a implementacions, i la certesa que tot l'après aquí es trasllada tal qual —només canvia l'API—, que és el que veuràs a 11-07.
BiblioTech, en tancar el mòdul 6, és un altre sistema. La seva jerarquia d'excepcions —BiblioTechException abstracta com a arrel, CatalegException i PrestecException com a branques, i cinc fulles amb dades estructurades— anomena cada fallada en el llenguatge del negoci i transporta el que qui captura necessita per decidir. Els seus constructors rebutgen les dades invàlides en comptes de corregir-les amb avisos, així que ja no existeixen préstecs amb referència PR-0000 ni llibres publicats l'any 0. Cataleg ha eliminat el null com a valor de retorn: obtenirPerReferencia llança i cercarPerReferencia retorna Optional. GestorPrestecs valida abans de modificar, marca cada pas amb un marcador i compensa al finally, de manera que una fallada a mitges deixa el material disponible, la quota restituïda i cap préstec fantasma, amb l'excepció arribant intacta. SessioBiblioteca i ExportadorCataleg són AutoCloseable, es tanquen passi el que passi i no perden cap excepció pel camí. MenuBiblioTech captura per tipus, tradueix cada fallada a un missatge comprensible, no mostra mai un detall intern i fa servir un únic Scanner que mai no es tanca. I main té una frontera d'errors amb incidències, codis de sortida i gestor global. No queda un sol System.out.println de diagnòstic a tot el projecte: tot el registre passa per Logger, amb marca de temps, nivell, classe, fil i identificador d'operació, en consola per al que és important i en fitxer rotat per a tot el detall.
De les cinc fragilitats que vas declarar en tancar el mòdul 5, quatre estan resoltes: les dades invàlides ja no avorten el programa, els errors no es comuniquen amb null ni false muts, els avisos no van per System.out.println, i les operacions no deixen l'estat incoherent en fallar a mitges.
Queda la cinquena, i és la més simple d'enunciar: res no es guarda en sortir. El catàleg, els préstecs, les multes, les reserves i l'historial viuen íntegrament en memòria. Tanca BiblioTech i desapareixen tres mòduls de feina. La propera execució comença amb un catàleg buit, com si mai no hagués existit. I hi ha una segona mancança, més discreta: l'aplicació no pot llegir res de l'exterior més enllà del que es teclegi, així que carregar un inventari de mil materials significa teclejar-los un a un.
Al mòdul 7, Entrada/Sortida d'arxius, es resol. Veuràs la lectura i l'escriptura de fitxers amb l'API que aquest mòdul només ha fregat —FileReader, FileWriter i el tancament garantit que ja domines—; els fluxos de bytes i de caràcters i per què són dues jerarquies diferents; BufferedReader i BufferedWriter, i què guanya exactament la memòria intermèdia; la serialització d'objectes, que recuperarà el serialVersionUID que vas declarar a les teves excepcions; l'API NIO.2 amb Path i Files, la forma moderna de treballar amb el sistema de fitxers; i els formats d'intercanvi CSV i Properties, amb els quals BiblioTech per fi carregarà la seva configuració —aquelles constants DIES_PRESTEC, TARIFA_DIARIA, MULTA_MAXIMA i LLINDAR_LLEU que porten sis mòduls escrites a foc al codi— des d'un fitxer extern. En acabar-lo, BiblioTech recordarà. I tot el que has après en aquest mòdul —les excepcions comprovades d'E/S, el try-with-resources, la traducció entre capes, la degradació elegant quan un fitxer no existeix i el registre del que passa— serà exactament l'eina que necessitaràs des de la primera línia.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
