SPI: Java Service Provider Interface

Durante la programmazione in Java vi sarà capitato di imbattervi nell’acronimo SPI: non si tratta di un typo per API (la S è vici alla A d’altro canto…) ma di un altro acronimo, ovvero Service Provider Interface. Ma di cosa si tratta? In una parola, aderire a questo framework significa creare applicazioni “estendibili“, dove cioè si possono estendere certi servizi già presenti senza modificare il codice corrente, ma aggiungendo solo nuovi moduli (ovvero nuovi jar).

API vs SPI

Per evitare subito degli equivoci, cerchiamo di distinguere questi due acronimi:

  • API (Application Programming Interface): fa riferimento a quelle modalità di accesso ad una funzionalità di un certo sistema. Si tratta di interfacce, classi o metodi a cui l’utente accede per ottenere una certa funzionalità.
  • SPI (Service Provider Interface): è quel meccanismo che permette di estendere/modificare il comportamento all’interno di un sistema senza modificarne il sorgente. Si tratta di interfacce, classi o metodi che l’utente estende o implementa per ottenere una certa funzionalità.

Se volgiamo quindi, possiamo pensare ad una SPI come ad una micro funzionalità (un servizio appunto) corrispondente a un livello molto granulare nel sistema che può essere esteso: la combinazione di più servizi genera una vera e propria funzionalità per l’utente finale, esposta magari come API pubblica.

Per chiarire il concetto, prendiamo per esempio la specifica JavaMail: la parte di API è quella che permette di collegarsi ad un server e di inviare le mail in modo trasparente rispetto al protocollo. Questa trasparenza è garantita dallo strato SPI che permette di aggiungere la possibilità di parlare con server SMTP, IMAP o POP3. Se verrà un nuovo protocollo, aderendo alla SPI, non influenzerà il livello API che continuerà a funzionare.

Spesso però il confine tra entrambi è molto labile ed è difficile dare etichette. Per esempio in JDBC, la classe Driver è senza dubbio una SPI perché non viene mai usata dall’utente, mentre la classe Connection è l’interfaccia di accesso al JDBC ed è quindi una API, ma allo stesso tempo può essere vista come una SPI perché il suo funzionamento varia a seconda del database, ma a livello di funzionalità è sempre la stessa.

Il discorso non si allontana molto dalla programmazione ad interfacce: comunque, meglio non addentrarsi troppo in questi discorsi perché, come si suol dire, si rischia di finire a parlare del sesso degli angeli. Personalmente ritengo che il livello SPI sia quello riferito alle funzionalità interne al sistema e che, nell’uso di tutti i giorni, sia interessante soprattutto quando si ha bisogno di sviluppare un framework a cui si vuole dare degli hook, dei punti di estendibilità.

Applicazioni estendibili

In Effective Java viene menzionato il Service Provider Framework:

E’ un sistema nel quale molteplici “service provider” implementano un servizio che il sistema rende disponibile ai client disaccoppiando l’interfaccia dall’implementazione

All’interno di questo framework i concetti fondamentali che ritornano sempre sono tre:

  • Service: insieme di interfacce e classi che forniscono accesso ad una certa funzionalità. Sotto questo concetto rientra anche l’API per la registrazione di un servizio.
  • Service Provider Interface (SPI): insieme di interfacce o classi astratte che definiscono il comportamento di un servizio.
  • Service Provider: insieme di classi che implementano una SPI. In questo modo si possono aggiungere nuovi provider senza modificare l’applicazione originale.

Service Provider in Java

L’implementazione di questo framework in Java esiste dalla versione 1.3, ma è solo dalla 6 che è diventata pubblica, utilizzabile quindi anche dall’utente finale. Questo significa che molte delle funzionalità interne alla JVM seguono questo framework e adesso siamo liberi di usarlo anche noi. Ma come si usa?

Creare l’interfaccia del servizio

Immaginiamo di avere un servizio di traduzione che cerca di indovinare di che lingua sia una parola semplicemente provando a tradurla. Il servizio sarà basato su un dizionario per ogni lingua. Possiamo definire quindi una SPI Dictionary

package it.cosenonjaviste.spi;
...

public abstract class Dictionary {

    protected List<String> dictionary = new ArrayList<>();

    public abstract List<Translation> translate(String text);

    protected List<Translation> findTranslation(String text, String language) {
        return dictionary.stream()
                .filter(item -> item.equalsIgnoreCase(text))
                .map(item -> new Translation(text, language))
                .collect(toList());
    }
}

Creare le implementazioni del servizio

Una volta definita l’interfaccia, possiamo creare un Service Provider, ovvero una implementazione per ogni lingua supportata dal servizio, come per esempio:

public class ItalianProvider extends Dictionary {

    public ItalianProvider() {
        this.dictionary.add("a");
        this.dictionary.add("ciao");
        this.dictionary.add("sera");
        this.dictionary.add("notte");
    }

    public List<Translation> translate(String text) {
        return findTranslation(text, "IT");
    }
}

e così via.

Registrare il nuovo provider

Come fa il sistema a sapere che esistono diversi provider della SPI? Vanno registrati: la specifica dei JAR definisce come vanno registrati i Service Providers.

All’interno della cartella META-INF/services del jar è necessario creare un file di configurazione per ogni SPI che vogliamo esporre. Il nome deve essere il fully-qualified name dell’interfaccia/classe astratta che espone il comportamento del servizio. In questo caso quindi, verrà creato un file:

META-INF/services/it.cosenonjaviste.spi.Dictionary

che al suo interno contiene l’elenco dei fully-qualified name dei Service Providers, uno per riga. Il file deve avere encoding UTF-8, mentre i commenti sono preceduti dal carattere cancelletto (#):

it.cosenonjaviste.providers.EnglishProvider
it.cosenonjaviste.providers.ItalianProvider

Posso quindi avere un unico jar che definisce entrambi i provider, oppure avere due jar, uno per ogni provider: a runtime il comportamento non cambia. Su GitHub è disponibile un progetto con un test in questa seconda configurazione.

API del servizio

A questo punto non ci resta che creare una API che espone il nostro servizio a dei client, capace di gestire tante lingue quanti provider abbiamo deployato:

public class GuessLanguage {

    private ServiceLoader<Dictionary> services = ServiceLoader.load(Dictionary.class);

    public List<Translation> guess(String text) {
        Iterator<Dictionary> iterator = this.services.iterator();

        List<Translation> translations = new ArrayList<>();
        while (iterator.hasNext()) {
            Dictionary dictionary = iterator.next();
            translations.addAll(dictionary.translate(text));
        }

        return translations;
    }
}

La classe ServiceLoader è quella che permette di accedere ai provider deployati o nel classpath dell’applicativo o nella cartella delle estensioni JVM (jre/lib/ext). Posso quindi aggiungere un jar a runtime che viene automaticamente riconosciuto senza riavviare nessun servizio?! Purtroppo pare di no… sembra che l’unico modo sia utilizzare l’overload del metodo load che riceve un classloader come secondo argomento (che andrebbe comunque implementato per dargli questa capacità!).

Conclusioni

Personalmente quindi ho imparato una cosa nuova studiando questo argomento. Serve davvero? In certi contesti si. Lo userò in futuro? Non so, dipende da cosa si sta sviluppando, diciamo che non mi viene in mente “Accidenti! Come ho fatto senza fino adesso?”. Sicuramente è un modo comodo per recuperare tutte le istanze di una certa interfaccia e quindi aggiungere un nuovo comportamento, ma per esempio una cosa simile la fa anche CDI. In definitiva, come si diceva all’inizio del post, è un buon modo per creare “hook” e permettere ad altri di estendere il proprio framework: Java stessa usa questo meccanismo, per esempio, per l’Annotation Processing, permettendo di eseguire del codice al momento della compilazione, basta implementare una opportuna SPI. Ma di questo ne parleremo nel prossimo post.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+

  • Kib

    Interessante articolo. In effetti stavo cercando un modo per realizzare degli hook ma non capivo quale fosse la differenza tra SPI e il CDI. Il CDI mi sembra la strada più semplice per ottenere il risultato.

    Non conoscevo questo sito ma ora lo metto nei bookmarks

  • Giampaolo Trapasso

    Ciao Kib, siamo contenti ti sia piaciuto il post, mandaci il tuo indirizzo via mail, ti spediremo un adesivo così ci puoi bookmarkare ancora meglio 🙂

  • CDI e SPI sono due mondi molto diversi tra loro: il primo è un container che ti dà la dependency injection in Java EE (o in Java SE con le opportune dipendenze). Il secondo è una standardizzazione della programmazione ad interfacce, intrinseca nella piattaforma Java. Certi problemi però si sovrappongono, quindi puoi decidere di gestirli in uno dei due modi.
    Continua a seguirci! 😉

  • Pingback: Java Annotation Processing - CoseNonJaviste()

  • Ledion Spaho

    Per iniziare, non mischiamo le mele con le pere. L’annotazione @Alternative serve a risolvere un problema simile.

    “Serve davvero?” – Beh, dipende. Spetta a te decidere se vuoi progettare un applicazione estensibile (modulare) e fornire un modo per aggiornare o migliorare parti specifiche di un prodotto senza modificare l’applicazione principale seguendo l’OCP. Un altro modo per fare tutto ciò è usare OSGi Services. La terza scelta (priorità più bassa rispetto agli altri elencati più sopra) è usare un Service Locator.

    Per concludere vorrei citare una frase del guru Martin Fowler: “The choice between them is less important than the principle of separating service configuration from the use of services within an application.”

    Quà si trova un esempio completo: https://docs.oracle.com/javase/tutorial/ext/basics/spi.html

    • Ciao. Non credo che l’annotazione @Alternative sia il paragone giusto perché fornisce, appunto, un comportamento alternativo ad un servizio che non puoi scegliere a runtime e che esclude l’implementazione principale (di solito infatti si usa per creare dei servizi mock). Trovo invece simile ad SPI il fatto che con CDI puoi usare @Inject @Any Instance per avere a disposizione tutte le istanze dei servizi da scegliere a runtime, o magari da applicare tutti insieme. Ovviamente se aggiungi una nuova implementazione, come per SPI, non puoi fare l'”hot deploy” a differenza invece di OSGi, che trovo una soluzione molto interessante.

  • Antonio Musarra

    Ciao a tutti.
    Dove possibile io andrei su SPI. Non sempre sei in contesti dove si ha la disponibilità della CDI, invece, sulla SPI puoi sempre contare.