JavaRush /Java Blog /Random-IT /Una breve escursione nell'iniezione di dipendenza o "Cos'...
Viacheslav
Livello 3

Una breve escursione nell'iniezione di dipendenza o "Cos'è il CDI?"

Pubblicato nel gruppo Random-IT
La base su cui sono ora costruiti i framework più popolari è l’iniezione di dipendenza. Suggerisco di guardare cosa dice la specifica CDI al riguardo, quali capacità di base abbiamo e come possiamo usarle.
Una breve escursione nell'iniezione di dipendenza o

introduzione

Vorrei dedicare questa breve recensione a qualcosa come CDI. Cos'è questo? CDI sta per Contesti e Iniezione di Dipendenze. Questa è una specifica Java EE che descrive l'inserimento delle dipendenze e i contesti. Per informazioni è possibile consultare il sito http://cdi-spec.org . Dato che CDI è una specifica (una descrizione di come dovrebbe funzionare, un insieme di interfacce), avremo bisogno anche di un'implementazione per usarla. Una di queste implementazioni è Weld - http://weld.cdi-spec.org/ Per gestire le dipendenze e creare un progetto, useremo Maven - https://maven.apache.org Quindi, abbiamo installato Maven, ora lo capirà nella pratica, per non capire l'astratto. Per fare ciò, creeremo un progetto utilizzando Maven. Apriamo la riga di comando (in Windows puoi usare Win+R per aprire la finestra "Esegui" ed eseguire cmd) e chiediamo a Maven di fare tutto per noi. Per questo, Maven ha un concetto chiamato archetipo: Maven Archetype .
Una breve escursione nell'iniezione di dipendenza o
Successivamente, alle domande “ Scegli un numero o applica filtro ” e “ Scegli la versione org.apache.maven.archetypes:maven-archetype-quickstart ” premi semplicemente Invio. Successivamente, inserisci gli identificatori del progetto, il cosiddetto GAV (vedi Guida alla convenzione di denominazione ).
Una breve escursione nell'iniezione di dipendenza o
Dopo aver creato con successo il progetto, vedremo la scritta "BUILD SUCCESS". Ora possiamo aprire il nostro progetto nel nostro IDE preferito.

Aggiunta di CDI a un progetto

Nell'introduzione, abbiamo visto che CDI ha un sito Web interessante: http://www.cdi-spec.org/ . C'è una sezione download, che contiene una tabella che contiene i dati di cui abbiamo bisogno:
Una breve escursione nell'iniezione di dipendenza o
Qui possiamo vedere come Maven descrive il fatto che utilizziamo l'API CDI nel progetto. L'API è un'interfaccia di programmazione dell'applicazione, ovvero un'interfaccia di programmazione. Lavoriamo con l'interfaccia senza preoccuparci di cosa e come funziona dietro questa interfaccia. L'API è un archivio jar che inizieremo a utilizzare nel nostro progetto, ovvero il nostro progetto inizierà a dipendere da questo jar. Pertanto, l'API CDI per il nostro progetto è una dipendenza. In Maven, un progetto è descritto nei file POM.xml ( POM - Project Object Model ). Le dipendenze sono descritte nel blocco delle dipendenze, al quale dobbiamo aggiungere una nuova voce:
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
Come avrai notato, non specifichiamo l'ambito con il valore fornito. Perché c'è questa differenza? Questo ambito significa che qualcuno ci fornirà la dipendenza. Quando un'applicazione viene eseguita su un server Java EE, significa che il server fornirà all'applicazione tutte le tecnologie JEE necessarie. Per semplicità di questa recensione, lavoreremo in un ambiente Java SE, quindi nessuno ci fornirà questa dipendenza. Puoi leggere ulteriori informazioni sull'ambito della dipendenza qui: " Ambito della dipendenza ". Ok, ora abbiamo la possibilità di lavorare con le interfacce. Ma abbiamo bisogno anche di attuazione. Come ricordiamo, useremo Weld. È interessante notare che ovunque vengono fornite dipendenze diverse. Ma seguiremo la documentazione. Leggiamo quindi " 18.4.5. Impostazione del classpath " e facciamo come dice:
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
È importante che le versioni della terza linea di Weld supportino CDI 2.0. Possiamo quindi contare sull'API di questa versione. Ora siamo pronti per scrivere il codice.
Una breve escursione nell'iniezione di dipendenza o

Inizializzazione di un contenitore CDI

Il CDI è un meccanismo. Qualcuno deve controllare questo meccanismo. Come abbiamo già letto sopra, un tale manager è un contenitore. Pertanto, dobbiamo crearlo; esso stesso non apparirà nell'ambiente SE. Aggiungiamo quanto segue al nostro metodo principale:
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
Abbiamo creato manualmente il contenitore CDI perché... Lavoriamo in un ambiente SE. Nei tipici progetti di combattimento, il codice viene eseguito su un server, che fornisce varie tecnologie al codice. Di conseguenza, se il server fornisce CDI, significa che il server ha già un contenitore CDI e non avremo bisogno di aggiungere nulla. Ma ai fini di questo tutorial, prenderemo l'ambiente SE. Inoltre, il contenitore è qui, in modo chiaro e comprensibile. Perché abbiamo bisogno di un contenitore? Il contenitore al suo interno contiene fagioli (bean CDI).
Una breve escursione nell'iniezione di dipendenza o

Fagioli CDI

Allora, fagioli. Cos'è un contenitore CDI? Questa è una classe Java che segue alcune regole. Queste regole sono descritte nelle specifiche, nel capitolo " 2.2. Che tipi di classi sono i bean? ". Aggiungiamo un bean CDI allo stesso pacchetto della classe App:
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
Ora possiamo chiamare questo bean dal nostro mainmetodo:
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
Come puoi vedere, non abbiamo creato il bean utilizzando la parola chiave new. Abbiamo chiesto al contenitore CDI: "Contenitore CDI. Ho davvero bisogno di un'istanza della classe Logger, dammela per favore. " Questo metodo è chiamato " Ricerca dipendenze ", ovvero ricerca delle dipendenze. Ora creiamo una nuova classe:
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
Una classe primitiva che restituisce una rappresentazione testuale di una data. Aggiungiamo ora l'output della data al messaggio:
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
È apparsa un'interessante annotazione @Inject. Come indicato nel capitolo " 4.1. Punti di iniezione " della documentazione cdi weld, utilizzando questa annotazione definiamo il punto di iniezione. In russo questo può essere letto come “punti di implementazione”. Vengono utilizzati dal contenitore CDI per inserire le dipendenze durante l'istanziazione dei bean. Come puoi vedere, non stiamo assegnando alcun valore al campo dateSource. La ragione di ciò è il fatto che il contenitore CDI consente all'interno dei bean CDI (solo quei bean che lui stesso ha istanziato, cioè che gestisce) di utilizzare la “ Dependency Injection ”. Questo è un altro modo di Inversione del Controllo , un approccio in cui la dipendenza è controllata da qualcun altro invece che da noi che creiamo esplicitamente gli oggetti. L'inserimento delle dipendenze può essere eseguito tramite un metodo, un costruttore o un campo. Per maggiori dettagli, vedere il capitolo delle specifiche CDI " 5.5. Iniezione di dipendenze ". La procedura per determinare cosa deve essere implementato si chiama risoluzione typesafe, ed è ciò di cui dobbiamo parlare.
Una breve escursione nell'iniezione di dipendenza o

Risoluzione dei nomi o risoluzione Typesafe

In genere, un'interfaccia viene utilizzata come tipo di oggetto da implementare e lo stesso contenitore CDI determina quale implementazione scegliere. Ciò è utile per molte ragioni, di cui parleremo. Quindi abbiamo un'interfaccia logger:
public interface Logger {
    void print(String message);
}
Dice che se abbiamo un logger, possiamo inviargli un messaggio e lui completerà il suo compito: log. Come e dove non sarà interessante in questo caso. Creiamo ora un'implementazione per il logger:
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
Come puoi vedere, questo è un logger che scrive su System.out. Meraviglioso. Ora, il nostro metodo principale funzionerà come prima. Logger logger = container.select(Logger.class).get(); Questa linea verrà comunque ricevuta dal logger. E il bello è che dobbiamo solo conoscere l'interfaccia e il contenitore CDI pensa già all'implementazione per noi. Supponiamo di avere una seconda implementazione che dovrebbe inviare il registro da qualche parte in un archivio remoto:
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
Se ora eseguiamo il nostro codice senza modifiche, otterremo un errore, perché Il contenitore CDI vede due implementazioni dell'interfaccia e non può scegliere tra loro: org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger cosa fare? Sono disponibili diverse varianti. La più semplice è l' annotazione @Vetoed per un bean CDI in modo che il contenitore CDI non percepisca questa classe come un bean CDI. Ma esiste un approccio molto più interessante. Un bean CDI può essere contrassegnato come "alternativa" utilizzando l'annotazione @Alternativedescritta nel capitolo " 4.7. Alternative " della documentazione di Weld CDI. Cosa significa? Ciò significa che a meno che non diciamo esplicitamente di usarlo, non verrà selezionato. Questa è una versione alternativa del fagiolo. Contrassegniamo il bean NetworkLogger come @Alternative e possiamo vedere che il codice viene eseguito nuovamente e utilizzato da SystemOutLogger. Per abilitare l'alternativa, dobbiamo avere un file Beans.xml . Potrebbe sorgere la domanda: " beans.xml, dove ti metto? " Pertanto, posizioniamo correttamente il file:
Una breve escursione nell'iniezione di dipendenza o
Non appena avremo questo file, l'artefatto con il nostro codice si chiamerà “ Archivio bean esplicito ”. Ora abbiamo 2 configurazioni separate: software e xml. Il problema è che caricheranno gli stessi dati. Ad esempio, la definizione del bean DataSource verrà caricata 2 volte e il nostro programma si bloccherà quando verrà eseguito, perché Il contenitore CDI li considererà come 2 bean separati (anche se in realtà sono la stessa classe, di cui il contenitore CDI ha appreso due volte). Per evitare ciò ci sono 2 opzioni:
  • rimuovere la riga initializer.addPackages(App.class.getPackage())e aggiungere l'indicazione dell'alternativa al file xml:
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • aggiungi un attributo bean-discovery-modecon il valore " none " all'elemento root bean e specifica un'alternativa a livello di codice:
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
Pertanto, utilizzando l'alternativa CDI, il contenitore può determinare quale bean selezionare. È interessante notare che se il contenitore CDI conosce diverse alternative per la stessa interfaccia, allora possiamo dirlo indicando la priorità utilizzando un'annotazione @Priority(a partire da CDI 1.1).
Una breve escursione nell'iniezione di dipendenza o

Qualificatori

Separatamente, vale la pena discutere di cose come le qualificazioni. Il qualificatore è indicato da un'annotazione sopra il bean e affina la ricerca del bean. E ora maggiori dettagli. È interessante notare che ogni bean CDI ha in ogni caso almeno un qualificatore - @Any. Se non specifichiamo NESSUN qualificatore sopra il bean, ma il contenitore CDI stesso aggiunge @Anyun altro qualificatore al qualificatore - @Default. Se specifichiamo qualcosa (ad esempio, specifichiamo esplicitamente @Any), il qualificatore @Default non verrà aggiunto automaticamente. Ma il bello dei qualificatori è che puoi creare i tuoi qualificatori. Il qualificatore non è quasi diverso dalle annotazioni, perché in sostanza, questa è solo un'annotazione scritta in modo speciale. Ad esempio, puoi inserire Enum per il tipo di protocollo:
public enum ProtocolType {
    HTTP, HTTPS
}
Successivamente possiamo creare un qualificatore che terrà conto di questo tipo:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
Vale la pena notare che i campi contrassegnati come @Nonbindingnon influiscono sulla determinazione del qualificatore. Ora è necessario specificare il qualificatore. È indicato sopra il tipo di bean (in modo che CDI sappia definirlo) e sopra l'Injection Point (con l'annotazione @Inject in modo da capire quale bean cercare per l'iniezione in questo posto). Ad esempio, possiamo aggiungere alcune classi con un qualificatore. Per semplicità, per questo articolo li faremo all'interno del NetworkLogger:
public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTPS");
	}
}
E poi quando eseguiremo Inject, specificheremo un qualificatore che influenzerà quale classe verrà utilizzata:
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
Fantastico, vero?) Sembra bellissimo, ma non è chiaro il perché. Ora immagina quanto segue:
Protocol protocol = new Protocol() {
	@Override
	public Class<? extends Annotation> annotationType() {
		return Protocol.class;
	}
	@Override
	public ProtocolType value() {
		String value = "HTTP";
		return ProtocolType.valueOf(value);
	}
};
container.select(NetworkLogger.Sender.class, protocol).get().send(null);
In questo modo possiamo sovrascrivere il valore ottenuto in modo che possa essere calcolato dinamicamente. Ad esempio, può essere preso da alcune impostazioni. Quindi possiamo modificare l'implementazione anche al volo, senza ricompilare o riavviare il programma/server. Diventa molto più interessante, non è vero? )
Una breve escursione nell'iniezione di dipendenza o

Produttori

Un'altra caratteristica utile del CDI sono i produttori. Si tratta di metodi speciali (sono contrassegnati con un'annotazione speciale) che vengono chiamati quando qualche bean ha richiesto l'inserimento delle dipendenze. Maggiori dettagli sono descritti nella documentazione, nella sezione " 2.2.3. Metodi del produttore ". L'esempio più semplice:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
Ora, quando si inserisce nei campi di tipo Integer, verrà chiamato questo metodo e da esso verrà ottenuto un valore. Qui dobbiamo subito capire che quando vediamo la parola chiave new, dobbiamo subito capire che questo NON è un bean CDI. Cioè, un'istanza della classe Random non diventerà un bean CDI solo perché deriva da qualcosa che controlla il contenitore CDI (in questo caso, il produttore).
Una breve escursione nell'iniezione di dipendenza o

Intercettori

Gli intercettori sono intercettori che “interferiscono” nel lavoro. Nel CDI questo viene fatto in modo abbastanza chiaro. Vediamo come possiamo effettuare il logging utilizzando interpreti (o intercettori). Per prima cosa dobbiamo descrivere il legame con l'interceptor. Come molte cose, questo viene fatto utilizzando le annotazioni:
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
La cosa principale qui è che questo è un legame per l'interceptor ( @InterceptorBinding), che verrà ereditato da extends ( @InterceptorBinding). Ora scriviamo l'interceptor stesso:
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
Puoi leggere di più su come sono scritti gli interceptor nell'esempio dalle specifiche: " 1.3.6. Esempio di interceptor ". Bene, tutto quello che dobbiamo fare è accendere l'inerceptor. Per fare ciò, specifica l'annotazione di associazione sopra il metodo da eseguire:
@ConsoleLog
public void print(String message) {
E ora un altro dettaglio molto importante. Gli intercettori sono disabilitati per impostazione predefinita e devono essere abilitati allo stesso modo delle alternative. Ad esempio, nel file Beans.xml :
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
Come puoi vedere, è abbastanza semplice.
Una breve escursione nell'iniezione di dipendenza o

Evento e osservatori

Il CDI fornisce anche un modello di eventi e osservatori. Qui tutto non è così ovvio come con gli intercettori. Quindi, l'Evento in questo caso può essere assolutamente qualsiasi classe; non è necessario nulla di speciale per la descrizione. Per esempio:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
Ora qualcuno dovrebbe aspettare l'evento:
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
La cosa principale qui è specificare l'annotazione @Observes, che indica che questo non è solo un metodo, ma un metodo che dovrebbe essere chiamato come risultato dell'osservazione di eventi del tipo LogEvent. Bene, ora abbiamo bisogno di qualcuno che guardi:
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
Abbiamo un unico metodo che indicherà al contenitore che si è verificato un evento Evento per il tipo di evento LogEvent. Ora non resta che utilizzare l'osservatore. Ad esempio, in NetworkLogger possiamo aggiungere un'iniezione del nostro osservatore:
@Inject
private LogObserver observer;
E nel metodo print possiamo notificare all'osservatore che abbiamo un nuovo evento:
public void print(String message) {
	observer.observe(new LogEvent());
È importante sapere che gli eventi possono essere elaborati in uno o più thread. Per l'elaborazione asincrona, utilizzare un metodo .fireAsync(invece di .fire) e un'annotazione @ObservesAsync(invece di @Observes). Ad esempio, se tutti gli eventi vengono eseguiti in thread diversi, se 1 thread genera un'eccezione, gli altri saranno in grado di svolgere il proprio lavoro per altri eventi. Puoi leggere di più sugli eventi nel CDI, come al solito, nelle specifiche, nel capitolo " 10. Eventi ".
Una breve escursione nell'iniezione di dipendenza o

Decoratori

Come abbiamo visto in precedenza, vari modelli di progettazione sono raccolti sotto l’ala CDI. Ed eccone un altro: un decoratore. Questa è una cosa molto interessante. Diamo un'occhiata a questa classe:
@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}
Dichiarandolo decoratore, diciamo che quando verrà utilizzata una qualsiasi implementazione di Logger, verrà utilizzato questo “add-on” che conosce l'implementazione reale, che viene memorizzata nel campo delegate (poiché è contrassegnato con l'annotazione @Delegate). I decoratori possono essere associati solo a un bean CDI, che di per sé non è né un interceptor né un decoratore. Un esempio è visibile anche nella specifica: " 1.3.7. Esempio decoratore ". Il decoratore, come l'intercettore, deve essere acceso. Ad esempio, in Beans.xml :
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
Per maggiori dettagli, vedere il riferimento alla saldatura: " Capitolo 10. Decoratori ".

Ciclo vitale

I fagioli hanno un proprio ciclo vitale. Sembra qualcosa del genere:
Una breve escursione nell'iniezione di dipendenza o
Come puoi vedere dall'immagine, abbiamo i cosiddetti callback del ciclo di vita. Queste sono annotazioni che diranno al contenitore CDI di richiamare determinati metodi in una determinata fase del ciclo di vita del bean. Per esempio:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
Questo metodo verrà chiamato quando un bean CDI viene istanziato da un contenitore. Lo stesso accadrà con @PreDestroy quando il bean verrà distrutto quando non sarà più necessario. Non per niente l’acronimo CDI contiene la lettera C – Context. I bean in CDI sono contestuali, il che significa che il loro ciclo di vita dipende dal contesto in cui esistono all'interno del contenitore CDI. Per capirlo meglio, dovresti leggere la sezione specifica “ 7. Ciclo di vita delle istanze contestuali ”. Vale anche la pena sapere che il contenitore stesso ha un ciclo di vita, di cui puoi leggere in “ Eventi del ciclo di vita del contenitore ”.
Una breve escursione nell'iniezione di dipendenza o

Totale

Sopra abbiamo guardato la punta dell'iceberg chiamato CDI. CDI fa parte delle specifiche JEE e viene utilizzato nell'ambiente JavaEE. Chi usa Spring non usa CDI, ma DI, cioè queste sono specifiche leggermente diverse. Ma conoscendo e comprendendo quanto sopra, puoi facilmente cambiare idea. Considerando che Spring supporta le annotazioni provenienti dal mondo CDI (lo stesso Inject). Materiali aggiuntivi: #Viacheslav
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION