Lezione 14 · Canale 1 · giovedì 29 ottobre 2026
Logging in Java: livelli, handler e formatter
Progettazione del Software
Lezione 14 · Canale 1 · giovedì 29 ottobre 2026
Progettazione del Software
Per capire se un programma funziona si stampano i passaggi: Hello World dice almeno che il programma è partito, una stampa in un ciclo dice che il ciclo gira. Un ascoltatore conviene che cominci stampando e.getActionCommand(): si vede subito se è stato invocato e con quale comando.
Le println però si mettono quando serve, si tolgono quando il problema è risolto e si rimettono al problema successivo. Il logging registra messaggi con un livello di importanza, e il programma sceglie quali livelli registrare e dove mandarli. I messaggi restano nel codice e si accendono o si spengono cambiando il livello: durante il test si registra tutto, nel programma installato solo i problemi importanti.
Serve soprattutto con l'interfaccia grafica, dove non si vede che cosa succede dentro. Un errore tipico è passare all'ascoltatore un dato sbagliato: l'ascoltatore lancia un'eccezione, e il log del dato ricevuto mostra la causa.
Un logger è l'oggetto che riceve i messaggi. Si ottiene con Logger.getLogger(nome), del pacchetto java.util.logging, non con un costruttore: getLogger è un metodo factory, cioè un metodo che restituisce un oggetto nuovo creato al suo interno. Il nome è libero.
I messaggi si inviano con un metodo per livello, dal meno al più importante:
log.info(s): un'informazione;log.warning(s): un avviso, è successo qualcosa che forse non è corretto;log.severe(s): un problema, qualcosa non funziona.log.setLevel(Level.WARNING) fissa il livello minimo: da lì in poi si registrano solo i messaggi di quel livello o più importanti. Il livello si può cambiare in qualunque momento dell'esecuzione.
import java.util.logging.Level;
import java.util.logging.Logger;
public class Main {
public static void main(String[] args) {
Logger log = Logger.getLogger("ABCD");
log.info("una stringa");
log.warning("altra stringa");
log.severe("altra ancora");
log.setLevel(Level.WARNING);
log.info("una stringa");
log.warning("altra stringa");
log.severe("altra ancora");
}
}
Prima di setLevel escono tre messaggi, dopo solo WARNING e SEVERE; con Level.SEVERE resterebbe solo l'ultimo. Ogni messaggio occupa due righe, con data, ora, classe e metodo da cui viene:
ott 30, 2026 7:01:12 PM Main main
INFO: una stringa
Rispetto a una println il messaggio dice già da dove viene. Il logger però scrive su System.err e println su System.out: sono due flussi separati, e sulla console le loro righe possono uscire in un ordine diverso da quello delle istruzioni.
Per convenzione ogni classe ha il suo logger, in un campo statico, con il nome della classe. Il campo statico è uno solo per classe, quindi tutti i metodi usano lo stesso logger; classi diverse hanno logger diversi, ciascuno con il suo livello. Si alza il livello di una classe che funziona e si tengono le informazioni di una che si sta controllando. La convenzione vale anche con una classe sola.
import java.util.logging.Logger;
public class Multipli {
static Logger log = Logger.getLogger("Multipli");
public static void main(String[] args) {
log.info("altra stringa");
Altra.metodo();
}
}
class Altra {
static Logger log = Logger.getLogger("Altra");
static void metodo() {
log.info("una stringa");
}
}
Il primo messaggio arriva da Multipli main, il secondo da Altra metodo.
Un handler decide che cosa fare di un messaggio. Ogni logger ha una lista di handler e manda ogni messaggio a tutti; log.addHandler(h) aggiunge h alla lista. Un handler è un oggetto funtore, come un ascoltatore: il logger lo riceve per chiamarne i metodi.
ConsoleHandler: stampa sulla console;FileHandler: scrive in un file;SocketHandler: manda il messaggio a un server, dati indirizzo IP e porta.La console non sempre resta: un programma con interfaccia grafica si lancia dall'ambiente grafico e la sua console si perde. Un file conserva i messaggi, e con data e ora si ritrova che cosa è successo quando l'utente ha visto un errore. Con un SocketHandler un server raccoglie i log di un programma installato su un altro computer.
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
public class Main {
static Logger log = Logger.getLogger("Main");
public static void main(String[] args) throws IOException {
FileHandler h = new FileHandler("log.txt");
h.setFormatter(new SimpleFormatter());
log.addHandler(h);
log.setUseParentHandlers(false);
log.info("una stringa");
}
}
Il costruttore di FileHandler apre il file e può lanciare IOException. Il file nasce nella cartella in cui si esegue il programma: quella corrente da terminale, quella del progetto in Eclipse. Le due righe in mezzo correggono due difetti:
setFormatter il file è in XML, un formato per programmi; SimpleFormatter scrive le stesse due righe della console;addHandler aggiunge senza togliere, quindi il messaggio uscirebbe ancora anche sulla console; setUseParentHandlers(false) spegne l'handler di sistema che stampa sulla console.Un formatter trasforma un messaggio in una stringa, e ogni handler ha il suo, impostato con h.setFormatter(f). Dopo log.info(s):
Le classi del logging
Un formatter proprio estende la classe astratta Formatter e definisce public String format(LogRecord r), che riceve il messaggio come LogRecord e restituisce la stringa da pubblicare. Un primo formatter ignora il messaggio:
class F extends Formatter {
public String format(LogRecord r) {
return "ABCD\n";
}
}
Il formatter si dà a un handler, e l'handler al logger:
ConsoleHandler h = new ConsoleHandler();
h.setFormatter(new F());
log.addHandler(h);
log.setUseParentHandlers(false);
log.info("una stringa");
Il programma stampa ABCD, non una stringa: l'handler pubblica la stringa che riceve dal formatter, qualunque sia. Senza setUseParentHandlers(false) compaiono sia ABCD sia il messaggio in due righe, perché resta anche l'handler di sistema.
Il record contiene i dati del messaggio, ciascuno con il suo metodo: getMessage() dà la stringa passata a log.info, getLevel() il livello, getSourceClassName() e getSourceMethodName() la classe e il metodo da cui viene. Contiene anche l'istante e l'identificativo del thread, un numero e non un nome come main.
class F extends Formatter {
public String format(LogRecord r) {
return r.getSourceClassName() + " " + r.getSourceMethodName()
+ " === " + r.getMessage() + " ===\n";
}
}
Con questo F l'esempio stampa Main main === una stringa ===.
Quando nessun handler di Java fa ciò che si vuole, per esempio mandare i log a un server web o spedirli per email alla chiusura, si estende la classe astratta Handler con tre metodi:
publish(LogRecord r): fa qualcosa del messaggio, di solito dopo averlo formattato;flush(): completa l'invio dei dati rimasti in sospeso;close(): chiude le risorse usate, come un file o una connessione.class H extends Handler {
public void publish(LogRecord r) {
String s = getFormatter().format(r);
System.out.print(s);
}
public void flush() {
}
public void close() {
}
}
publish deve chiamare il formatter da sé: un publish che non pubblica niente non produce nessuna uscita, anche se il logger gli passa ogni messaggio. Un H nuovo non ha formatter, quindi prima di aggiungerlo gli si dà con mio.setFormatter(new F()).
format e publish non li chiama il programma: il programma chiama log.info, il logger chiama publish e publish chiama format. È lo schema degli ascoltatori: si scrive una classe che altre classi useranno.
Se il SimpleFormatter va bene e cambia solo la disposizione, conviene cambiarne la stringa di formato invece di scrivere un formatter. È una stringa come quella di printf, e va impostata prima del primo messaggio:
System.setProperty("java.util.logging.SimpleFormatter.format",
"%2$s: %5$s%n");
Ogni segnaposto inserisce come stringa un argomento numerato:
%2$s: la sorgente, classe e metodo, per esempio Main main;%3$s: il nome del logger;%4$s: il livello;%5$s: il messaggio;%n: il ritorno a capo.Con questo formato log.info("una stringa") stampa una riga sola, senza data: Main main: una stringa. La stringa passata a log.info resta il messaggio, e il formatter la mette dove sta %5$s. A differenza di printf, che usa tutti gli argomenti, il formato sceglie solo quelli che servono; con un formato vuoto non si stampa niente.
Logger
Logger.getLogger(nome):
metodo factorystatic,
con il nome della classeinfo, warning, severesetLevel(Level.WARNING):
solo da WARNING in suHandler
addHandler(h): aggiunge
senza togliereConsoleHandler,
FileHandler,
SocketHandlersetUseParentHandlers(false):
niente console di sistemaFormatter
format(LogRecord r)
restituisce la stringah.setFormatter(f)FileHandler senza
formatter: XMLHandler proprio
publish, flush, closegetFormatter().format(r)Formato
%2$s sorgente%3$s logger%4$s livello%5$s messaggio