Java – Autowiring di un Logger con Spring Framework/Boot

Nelle applicazioni Java di un certo livello, specialmente nelle web application, è tipico utilizzare il logging per tracciare le attività della applicazione. In questo articolo descriverò una tecnica molto semplice per “iniettare” il Logger di una libreria di logging all’interno di un bean gestito da Spring (Framework o Boot).

Introduzione

Per Java esistono diverse librerie di logging ed esistono anche librerie di facade (“facciata”) che servono come livello di “astrazione” verso altre librerie di logging. In questo articolo mi baserò principalmente sulla libreria SLF4J (una ben nota libreria di facade per il logging) ma voglio chiarire subito che quanto è descritto nell’articolo può valere allo stesso modo anche per una qualunque altra libreria di logging.

Nelle applicazioni con Spring Framework/Book è tipico ad esempio fare un controller che usa il logging nel seguente modo:

package xyz.myapp.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// .... altri import

@Controller         // oppure @RestController
public class HomeController {
    private static final Logger logger = LoggerFactory.getLogger(HomeController.class);

    // ... resto del codice
}

Dal momento che i controller (come altri bean Spring) normalmente sono dei “singleton” (ovvero esiste una sola ed unica istanza) si potrebbe semplificare e abbreviare un pochino la riga che inizializza il Logger nel seguente modo: innanzitutto, invece che un campo static si può usare un campo “di istanza” (non static) e poi si può usare getClass() invece che specificare il class literal della classe.

public class HomeController {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    // .....
}

In generale si tratta comunque di ripetere in tutti i controller (e i service, i DAO, ecc...) una cosa del genere. Una soluzione alternativa potrebbe essere quella di creare una classe “base” (es. BaseController) in cui mettere quel campo come protected per renderlo accessibile dalle sotto-classi. È una soluzione tecnicamente fattibile e funzionante ma potrebbe non essere “accettata” in certi progetti.

Sarebbe invece molto bello (e pratico!) se il Logger si potesse ottenere in questo modo:

public class HomeController {
    @Autowired private Logger logger;

    // .....
}

Oppure volendo “allentare” un po’ il livello di accesso si potrebbe anche togliere il private.

public class HomeController {
    @Autowired Logger logger;

    // .....
}

E tutto questo senza inizializzare esplicitamente “a mano” il campo logger come si vede nei due esempi iniziali. Sembra impossibile? E invece è assolutamente realizzabile!

Soluzione

Come si vede c’è un @Autowired, questo vuol dire che da qualche parte Spring dovrà creare un oggetto Logger (e attenzione, un Logger specifico per HomeController) e poi iniettarlo nel campo logger. Questo in generale dovrebbe valere anche per qualunque altro controller, service, ecc... Quindi ad esempio in un MailingService il Logger iniettato deve essere specifico per la classe MailingService.

Per poter realizzare questo autowiring un po’ particolare, è necessario sfruttare una funzionalità che è disponibile solo a partire da Spring Framework 4.3. Questa cosa è anche sfruttabile in Spring Boot a partire dalla versione 1.4 (che si basa appunto su Spring Framework 4.3).

Già a partire da Spring Framework 3 esiste la possibilità di definire dei metodi annotati con l’annotation @Bean che rappresenta in Java l’equivalente (similare ma non perfettamente uguale) del tag <bean> che si poteva usare nella tradizionale XML configuration di Spring per definire i bean.

Un metodo @Bean si mette tipicamente in una classe annotata @Configuration ma è anche possibile usarlo in un altro tipo di bean, ad esempio in una classe annotata @Component. Quando @Bean si trova in una classe non @Configuration funziona nel cosiddetto lite mode che ha alcuni limiti rispetto all’utilizzo in una classe @Configuration. Questa differenza non è molto rilevante ai fini dell’articolo ma si può vedere la documentazione ufficiale: Using the @Bean Annotation.

La documentazione di Spring dice che dalla versione 4.3 il metodo @Bean può avere come parametro un oggetto di classe InjectionPoint oppure la sotto-classe DependencyDescriptor. Questi due tipi servono per rappresentare e descrivere il punto di “iniettamento” in cui Spring andrà ad iniettare un oggetto. Questo in sostanza permette al metodo factory @Bean di ottenere informazioni dettagliate su quel punto e quindi fare eventualmente qualcosa di più particolare.

La soluzione concreta per mettere in pratica questo autowiring del Logger consiste in una semplice classe @Configuration che contiene un metodo @Bean.

package xyz.myapp.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

@Configuration
public class LoggingConfig {

    @Bean
    @Scope("prototype")
    public Logger slf4jLogger(InjectionPoint injectionPoint) {
        Class<?> targetClass = injectionPoint.getMember().getDeclaringClass();
        Logger logger = LoggerFactory.getLogger(targetClass);
        return logger;
    }
}

La classe LoggingConfig diventa a tutti gli effetti un bean Spring ma un bean particolare perché il suo scopo principale è quello di definire e creare ulteriori bean.

Il funzionamento è il seguente: quando Spring deve creare la istanza di HomeController, deve anche inizializzare il campo logger che è @Autowired. Per far questo deduce che è possibile ottenere l’oggetto Logger dal metodo slf4jLogger di LoggingConfig poiché il tipo di ritorno è appropriato per il tipo del campo.

Il parametro di tipo InjectionPoint fornisce delle informazioni sul punto di iniettamento. InjectionPoint è in grado di fornire alcune informazioni basilari (sufficienti per l’esempio) mentre la sotto-classe DependencyDescriptor fornisce informazioni aggiuntive più specifiche.

Dall’oggetto InjectionPoint si possono ottenere varie informazioni, in particolare:

  • injectionPoint.getDeclaredType() restituisce il Class del tipo richiesto nel punto di iniettamento (nell’esempio: un oggetto java.lang.Class che descrive la classe org.slf4j.Logger)

  • injectionPoint.getMember() restituisce il java.lang.reflect.Member che descrive il membro su cui avviene l’iniettamento (nell’esempio: un oggetto java.lang.reflect.Field che descrive il campo logger)

  • injectionPoint.getMember().getDeclaringClass() restituisce il Class della classe in cui è dichiarato il membro su cui avviene l’iniettamento (nell’esempio: un oggetto java.lang.Class che descrive la classe HomeController)

Quest’ultimo è proprio quello che serve. In questo modo il codice scopre in automatico che il punto di iniettamento si trova nella classe HomeController, quindi si può passare questo Class al LoggerFactory.getLogger(Class) di SLF4J per ottenere il “giusto” Logger appropriato per HomeController.

Nel codice si vede che il metodo slf4jLogger oltre a essere annotato @Bean è anche annotato @Scope("prototype"). C’è un motivo molto ben preciso e importante: se ci fossero 100 classi differenti che contengono un @Autowired Logger logger; allora per ciascuno di questi punti si deve creare un distinto oggetto Logger. Lo scope “prototype” serve proprio per indicare a Spring che bisogna creare un nuovo bean a ogni richiesta di wiring. Insomma, non si può certo fornire un bean “singleton”, non avrebbe alcun senso!

Utilizzo con altre librerie di logging

Il codice dell’esempio è basato su SLF4J ma come accennavo all’inizio, si può utilizzare qualunque altra libreria di logging. Chiaramente va cambiato il tipo del campo logger, il tipo restituito dal metodo @Bean e ovviamente anche la logica di “lookup” dell’oggetto Logger, dato che varia da libreria a libreria.

Se ad esempio si usa log4j 1.2 il tipo da trattare dovrà essere org.apache.log4j.Logger e la logica di “lookup” nel metodo @Bean dovrà usare Logger.getLogger(targetClass).

Se invece si usa log4j 2.x il tipo da trattare dovrà essere org.apache.logging.log4j.Logger e la logica di “lookup” nel metodo @Bean dovrà usare LogManager.getLogger(targetClass).

Il discorso è similare per qualunque altra libreria di logging o facade di logging. Quello che conta è che il metodo factory @Bean restituisca un tipo di oggetto che sia lecitamente iniettabile nel campo @Autowired di un bean Spring.

Locazione della classe LoggingConfig

La classe LoggingConfig (si può anche scegliere un nome differente) dove dovrebbe essere messa? In realtà il package non è molto importante. La cosa importante è che la classe venga “pescata” da Spring Framework/Boot.

Nelle applicazioni Spring Boot è tipico mettere la classe principale (quella annotata con @SpringBootApplication) in un package “base” e poi avere altri sotto-package dove ci sono i vari componenti. Spring Boot per default parte da quel package base e scansiona tutti i sotto-package per rintracciare i bean Spring, comprese le classi @Configuration.

Se invece si usa Spring Framework “puro”, è tutto meno immediato e meno predefinito. Si possono fare diverse scelte, ad esempio se si usa la XML configuration si può mettere nel xml la configurazione:

<context:component-scan base-package="xyz.myapp" />

Quello che alla fine conta è che la classe LoggingConfig venga “tirata su” da Spring correttamente.