JavaRush /Blog Java /Random-PL /Krótka wycieczka do zastrzyku zależności lub „Co jeszcze ...
Viacheslav
Poziom 3

Krótka wycieczka do zastrzyku zależności lub „Co jeszcze to CDI?”

Opublikowano w grupie Random-PL
Podstawą, na której budowane są obecnie najpopularniejsze frameworki, jest wstrzykiwanie zależności. Proponuję przyjrzeć się, co na ten temat mówi specyfikacja CDI, jakie podstawowe możliwości mamy i jak możemy je wykorzystać.
Krótka wycieczka do zastrzyku zależności lub

Wstęp

Tę krótką recenzję chciałbym poświęcić takiemu czemuś jak CDI. Co to jest? CDI oznacza konteksty i wstrzykiwanie zależności. To jest specyfikacja Java EE opisująca wstrzykiwanie zależności i konteksty. Informacje można znaleźć na stronie internetowej http://cdi-spec.org . Ponieważ CDI jest specyfikacją (opisem tego, jak powinno działać, zestawem interfejsów), aby z niego skorzystać, będziemy potrzebować także implementacji. Jedną z takich implementacji jest Weld - http://weld.cdi-spec.org/ Do zarządzania zależnościami i tworzenia projektu użyjemy Mavena - https://maven.apache.org Mamy więc zainstalowanego Mavena, teraz możemy zrozumiem to w praktyce, żeby nie rozumieć abstrakcji. W tym celu utworzymy projekt za pomocą Mavena. Otwórzmy wiersz poleceń (w systemie Windows możesz użyć Win+R, aby otworzyć okno „Uruchom” i wykonać polecenie cmd) i poproś Mavena, aby zrobił wszystko za nas. W tym celu Maven ma koncepcję zwaną archetypem: Archetyp Mavena .
Krótka wycieczka do zastrzyku zależności lub
Następnie w przypadku pytań „ Wybierz numer lub zastosuj filtr ” i „ Wybierz org.apache.maven.archetypes:maven-archetype-quickstart wersja ” po prostu naciśnij Enter. Następnie należy wprowadzić identyfikatory projektu, tzw. GAV (patrz Przewodnik po konwencji namingowej ).
Krótka wycieczka do zastrzyku zależności lub
Po pomyślnym utworzeniu projektu pojawi się napis „BUDUJ SUKCES”. Teraz możemy otworzyć nasz projekt w naszym ulubionym IDE.

Dodawanie CDI do projektu

We wstępie widzieliśmy, że CDI ma ciekawą stronę internetową - http://www.cdi-spec.org/ . Znajduje się tam sekcja pobierania, w której znajduje się tabela zawierająca potrzebne nam dane:
Krótka wycieczka do zastrzyku zależności lub
Tutaj możemy zobaczyć jak Maven opisuje fakt wykorzystania w projekcie API CDI. API to interfejs programowania aplikacji, czyli jakiś interfejs programistyczny. Pracujemy z interfejsem nie martwiąc się o to, co i jak działa za tym interfejsem. API to archiwum jar, które zaczniemy wykorzystywać w naszym projekcie, czyli nasz projekt zaczyna zależeć od tego słoika. Dlatego API CDI dla naszego projektu jest zależnością. W Maven projekt opisywany jest w plikach POM.xml ( POM - Project Object Model ). Zależności opisane są w bloku zależności, do którego musimy dodać nowy wpis:
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
Jak być może zauważyłeś, nie określamy zakresu za pomocą podanej wartości. Dlaczego jest taka różnica? Ten zakres oznacza, że ​​ktoś udostępni nam zależność. Gdy aplikacja działa na serwerze Java EE, oznacza to, że serwer zapewni aplikacji wszystkie niezbędne technologie JEE. Dla uproszczenia tej recenzji będziemy pracować w środowisku Java SE, dlatego nikt nie udostępni nam tej zależności. Więcej o zakresie zależności możesz przeczytać tutaj: „ Zakres zależności ”. OK, mamy teraz możliwość pracy z interfejsami. Ale potrzebujemy też wdrożenia. Jak pamiętamy, będziemy używać Weld. Co ciekawe, wszędzie podawane są różne zależności. Ale będziemy postępować zgodnie z dokumentacją. Dlatego przeczytajmy „ 18.4.5. Ustawianie ścieżki klasy ” i zróbmy tak, jak jest tam napisane:
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
Ważne jest, aby wersje trzeciej linii Weld obsługiwały CDI 2.0. Dlatego możemy liczyć na API tej wersji. Teraz jesteśmy gotowi do napisania kodu.
Krótka wycieczka do zastrzyku zależności lub

Inicjowanie kontenera CDI

CDI to mechanizm. Ktoś musi kontrolować ten mechanizm. Jak już przeczytaliśmy powyżej, takim menedżerem jest kontener. Dlatego musimy go stworzyć, on sam nie pojawi się w środowisku SE. Dodajmy do naszej metody main następujące elementy:
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
Kontener CDI utworzyliśmy ręcznie, ponieważ... Pracujemy w środowisku SE. W typowych projektach bojowych kod działa na serwerze, który udostępnia kodowi różne technologie. Odpowiednio, jeśli serwer udostępnia CDI, oznacza to, że serwer ma już kontener CDI i nie będziemy musieli nic dodawać. Jednak na potrzeby tego samouczka zajmiemy się środowiskiem SE. Poza tym pojemnik jest tutaj, wyraźnie i zrozumiale. Dlaczego potrzebujemy kontenera? Wewnątrz pojemnika znajdują się ziarna (fasola CDI).
Krótka wycieczka do zastrzyku zależności lub

Fasola CDI

A więc fasola. Co to jest pojemnik CDI? To jest klasa Java, która podlega pewnym regułom. Zasady te opisano w specyfikacji, w rozdziale „ 2.2. Jakimi klasami są fasole? ”. Dodajmy komponent CDI do tego samego pakietu co klasa App:
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
Teraz możemy wywołać tę fasolę z naszej mainmetody:
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
Jak widać, nie utworzyliśmy komponentu bean przy użyciu słowa kluczowego new. Zapytaliśmy kontener CDI: "Kontener CDI. Naprawdę potrzebuję instancji klasy Logger, daj mi ją proszę." Ta metoda nazywa się „ Wyszukiwaniem zależności ”, czyli wyszukiwaniem zależności. Stwórzmy teraz nową klasę:
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
Prymitywna klasa, która zwraca tekstową reprezentację daty. Dodajmy teraz datę do wiadomości:
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
Pojawiła się ciekawa adnotacja @Inject. Jak stwierdzono w rozdziale „ 4.1. Punkty wtrysku ” dokumentacji spoiny CDI, za pomocą tej adnotacji definiujemy Punkt wtrysku. W języku rosyjskim można to odczytać jako „punkty realizacji”. Są one używane przez kontener CDI do wstrzykiwania zależności podczas tworzenia instancji komponentów bean. Jak widać nie przypisujemy żadnych wartości do pola dateSource. Powodem tego jest fakt, że kontener CDI pozwala wewnątrz ziaren CDI (tylko tych ziaren, które sam utworzył, czyli którymi zarządza) na użycie „ Wstrzykiwania zależności ”. Jest to kolejny sposób Odwrócenia Kontroli , podejście, w którym zależność jest kontrolowana przez kogoś innego, a nie przez nas, którzy jawnie tworzą obiekty. Wstrzykiwanie zależności można wykonać za pomocą metody, konstruktora lub pola. Więcej szczegółów znajdziesz w rozdziale specyfikacji CDI " 5.5. Wstrzykiwanie zależności ". Procedura określania, co należy wdrożyć, nazywa się rozdzielczością typu bezpiecznego i właśnie o tym musimy porozmawiać.
Krótka wycieczka do zastrzyku zależności lub

Rozpoznawanie nazw lub rozpoznawanie typów

Zwykle jako typ obiektu do zaimplementowania używany jest interfejs, a kontener CDI sam określa, którą implementację wybrać. Jest to przydatne z wielu powodów, które omówimy. Mamy więc interfejs rejestratora:
public interface Logger {
    void print(String message);
}
Mówi, że jeśli mamy jakiś loger, to możemy wysłać do niego wiadomość, a on wykona swoje zadanie - log. Jak i gdzie nie będzie w tym przypadku interesujące. Stwórzmy teraz implementację dla rejestratora:
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
Jak widać, jest to rejestrator, który zapisuje dane do System.out. Wspaniały. Teraz nasza główna metoda będzie działać jak poprzednio. Logger logger = container.select(Logger.class).get(); Linia ta nadal będzie odbierana przez rejestrator. A piękno polega na tym, że wystarczy nam znajomość interfejsu, a kontener CDI już myśli o implementacji za nas. Załóżmy, że mamy drugą implementację, która powinna wysłać dziennik gdzieś do zdalnej pamięci:
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
Jeśli teraz uruchomimy nasz kod bez zmian, otrzymamy błąd, ponieważ Kontener CDI widzi dwie implementacje interfejsu i nie może wybierać pomiędzy nimi: org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger Co robić? Dostępnych jest kilka odmian. Najprostszym z nich jest adnotacja @Vetoed dla komponentu bean CDI, dzięki której kontener CDI nie postrzega tej klasy jako komponentu CDI. Ale istnieje o wiele bardziej interesujące podejście. Ziarno CDI można oznaczyć jako „alternatywę” za pomocą adnotacji @Alternativeopisanej w rozdziale „ 4.7. Alternatywy ” dokumentacji Weld CDI. Co to znaczy? Oznacza to, że jeśli wyraźnie nie powiemy, aby go użyć, nie zostanie on wybrany. To alternatywna wersja fasolki. Oznaczmy komponent fasoli NetworkLogger jako @Alternative i zobaczymy, że kod jest wykonywany ponownie i używany przez SystemOutLogger. Aby włączyć alternatywę, musimy mieć plik beans.xml . Może pojawić się pytanie: „ beans.xml, gdzie cię umieścić? ” Dlatego umieśćmy plik poprawnie:
Krótka wycieczka do zastrzyku zależności lub
Gdy tylko będziemy mieli ten plik, artefakt z naszym kodem zostanie nazwany „ Jawnym archiwum fasoli ”. Teraz mamy 2 oddzielne konfiguracje: programową i xml. Problem w tym, że ładują te same dane. Na przykład definicja komponentu bean DataSource zostanie załadowana 2 razy, a nasz program ulegnie awarii po uruchomieniu, ponieważ Kontener CDI pomyśli o nich jak o dwóch oddzielnych komponentach (chociaż w rzeczywistości są to tej samej klasy, o czym kontener CDI dowiedział się dwukrotnie). Aby tego uniknąć, istnieją 2 opcje:
  • usuń linię initializer.addPackages(App.class.getPackage())i dodaj wskazanie alternatywy do pliku 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>
  • dodaj atrybut bean-discovery-modeo wartości „ none ” do elementu głównego fasoli i programowo określ alternatywę:
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
Zatem, korzystając z alternatywy CDI, kontener może określić, który komponent bean wybrać. Co ciekawe, jeśli kontener CDI zna kilka alternatyw dla tego samego interfejsu, to możemy to stwierdzić, wskazując priorytet za pomocą adnotacji @Priority(od CDI 1.1).
Krótka wycieczka do zastrzyku zależności lub

Kwalifikacje

Osobno warto omówić coś takiego jak kwalifikatory. Kwalifikator jest oznaczony adnotacją nad komponentem bean i uściśla wyszukiwanie komponentu bean. A teraz więcej szczegółów. Co ciekawe, w każdym przypadku dowolny komponent CDI ma co najmniej jeden kwalifikator - @Any. Jeśli nie podamy ŻADNEGO kwalifikatora nad komponentem bean, to kontener CDI sam doda @Anydo kwalifikatora kolejny kwalifikator - @Default. Jeśli cokolwiek określimy (na przykład jawnie określimy @Any), wówczas kwalifikator @Default nie zostanie automatycznie dodany. Ale piękno kwalifikatorów polega na tym, że możesz tworzyć własne kwalifikatory. Kwalifikator prawie nie różni się od adnotacji, ponieważ w istocie jest to po prostu adnotacja napisana w specjalny sposób. Na przykład możesz wpisać Enum jako typ protokołu:
public enum ProtocolType {
    HTTP, HTTPS
}
Następnie możemy stworzyć kwalifikator, który uwzględni ten typ:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
Warto zaznaczyć, że pola oznaczone jako @Nonbindingnie mają wpływu na ustalenie kwalifikatora. Teraz musisz określić kwalifikator. Jest on wskazany nad typem fasoli (aby CDI wiedziało jak go zdefiniować) oraz nad Punktem Wstrzyknięcia (z adnotacją @Inject, abyś wiedział jakiego ziarna szukać w tym miejscu do iniekcji). Na przykład możemy dodać klasę z kwalifikatorem. Dla uproszczenia w tym artykule zrobimy je wewnątrz NetworkLoggera:
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");
	}
}
A następnie, gdy wykonamy Inject, określimy kwalifikator, który będzie miał wpływ na to, która klasa zostanie użyta:
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
Świetnie, prawda?) Wydaje się piękne, ale nie jest jasne dlaczego. Teraz wyobraź sobie, co następuje:
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);
W ten sposób możemy zastąpić pobieraną wartość, aby można ją było obliczać dynamicznie. Można to na przykład pobrać z niektórych ustawień. Wtedy możemy zmieniać implementację nawet na bieżąco, bez konieczności rekompilowania czy restartowania programu/serwera. Robi się znacznie ciekawiej, prawda? )
Krótka wycieczka do zastrzyku zależności lub

Producenci

Kolejną przydatną funkcją CDI są producenci. Są to metody specjalne (oznaczone specjalną adnotacją), które są wywoływane, gdy jakiś komponent bean zażądał wstrzyknięcia zależności. Więcej szczegółów opisano w dokumentacji w rozdziale „ 2.2.3. Metody producenta ”. Najprostszy przykład:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
Teraz podczas wstrzykiwania do pól typu Integer zostanie wywołana ta metoda i zostanie z niej uzyskana wartość. W tym miejscu powinniśmy od razu zrozumieć, że kiedy zobaczymy słowo kluczowe new, musimy od razu zrozumieć, że NIE jest to komponent CDI. Oznacza to, że instancja klasy Random nie stanie się komponentem bean CDI tylko dlatego, że wywodzi się z czegoś, co kontroluje kontener CDI (w tym przypadku producenta).
Krótka wycieczka do zastrzyku zależności lub

Przechwytywacze

Przechwytywacze to przechwytywacze, które „ingerują” w pracę. W CDI jest to zrobione dość wyraźnie. Zobaczmy, jak możemy wykonać rejestrowanie za pomocą interpreterów (lub przechwytywaczy). Najpierw musimy opisać powiązanie z przechwytywaczem. Podobnie jak wiele innych rzeczy, odbywa się to za pomocą adnotacji:
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
Najważniejsze jest to, że jest to powiązanie dla przechwytywacza ( @InterceptorBinding), który będzie dziedziczony przez rozszerzenia ( @InterceptorBinding). Teraz napiszmy sam przechwytywacz:
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
Więcej o tym jak zapisuje się przechwytywacze możesz przeczytać na przykładzie ze specyfikacji: " 1.3.6. Przykład przechwytywacza ". Cóż, jedyne co musimy zrobić to włączyć inerceptor. Aby to zrobić, określ adnotację wiązania nad wykonywaną metodą:
@ConsoleLog
public void print(String message) {
A teraz kolejny bardzo ważny szczegół. Przechwytywacze są domyślnie wyłączone i należy je włączyć w taki sam sposób, jak ich alternatywy. Na przykład w pliku beans.xml :
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
Jak widać, jest to całkiem proste.
Krótka wycieczka do zastrzyku zależności lub

Wydarzenia i obserwatorzy

CDI zapewnia również model zdarzeń i obserwatorów. Tutaj nie wszystko jest tak oczywiste jak w przypadku przechwytywaczy. Tak więc wydarzeniem w tym przypadku może być absolutnie dowolna klasa, do opisu nie jest potrzebne nic specjalnego. Na przykład:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
Teraz ktoś powinien poczekać na wydarzenie:
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
Najważniejsze jest tutaj podanie adnotacji @Observes, która wskazuje, że nie jest to tylko metoda, ale metoda, którą należy wywołać w wyniku obserwacji zdarzeń typu LogEvent. Cóż, teraz potrzebujemy kogoś, kto będzie oglądał:
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
Mamy jedną metodę, która poinformuje kontener, że wystąpiło zdarzenie Event dla typu zdarzenia LogEvent. Teraz pozostaje już tylko skorzystać z obserwatora. Przykładowo w NetworkLoggerze możemy dodać zastrzyk naszego obserwatora:
@Inject
private LogObserver observer;
A w metodzie print możemy powiadomić obserwatora, że ​​mamy nowe wydarzenie:
public void print(String message) {
	observer.observe(new LogEvent());
Warto wiedzieć, że zdarzenia mogą być przetwarzane w jednym wątku lub w kilku. W przypadku przetwarzania asynchronicznego użyj metody .fireAsync(zamiast .fire) i adnotacji @ObservesAsync(zamiast @Observes). Na przykład, jeśli wszystkie zdarzenia są wykonywane w różnych wątkach, to jeśli 1 wątek zgłosi wyjątek, pozostałe będą mogły wykonać swoją pracę dla innych zdarzeń. Więcej o wydarzeniach w CDI tradycyjnie przeczytacie w specyfikacji, w rozdziale „ 10. Zdarzenia ”.
Krótka wycieczka do zastrzyku zależności lub

Dekoratorzy

Jak widzieliśmy powyżej, pod skrzydłem CDI gromadzone są różne wzorce projektowe. A oto kolejny - dekorator. To bardzo interesująca rzecz. Przyjrzyjmy się tej klasie:
@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);
    }
}
Deklarując go jako dekorator, mówimy, że gdy zostanie użyta jakakolwiek implementacja Loggera, zostanie użyty ten „dodatek”, który zna rzeczywistą implementację, która jest przechowywana w polu delegata (ponieważ jest oznaczona adnotacją @Delegate). Dekoratory można powiązać jedynie z komponentem CDI, który sam w sobie nie jest ani przechwytywaczem, ani dekoratorem. Przykład widać także w specyfikacji: " 1.3.7. Przykład dekoratora ". Dekorator, podobnie jak przechwytywacz, musi być włączony. Na przykład w pliku beans.xml :
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
Aby uzyskać więcej informacji, zobacz odniesienie do spoiny: „ Rozdział 10. Dekoratory ”.

Koło życia

Fasola ma swój własny cykl życia. Wygląda to mniej więcej tak:
Krótka wycieczka do zastrzyku zależności lub
Jak widać na obrazku, mamy tak zwane wywołania zwrotne cyklu życia. Są to adnotacje, które powiedzą kontenerowi CDI, aby wywołał określone metody na określonym etapie cyklu życia komponentu bean. Na przykład:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
Ta metoda zostanie wywołana, gdy kontener CDI utworzy instancję. To samo stanie się z @PreDestroy, gdy komponent bean zostanie zniszczony, gdy nie będzie już potrzebny. Nie bez powodu w akronimie CDI znajduje się litera C – Kontekst. Fasony w CDI są kontekstowe, co oznacza, że ​​ich cykl życia zależy od kontekstu, w jakim istnieją w kontenerze CDI. Aby lepiej to zrozumieć, powinieneś przeczytać sekcję specyfikacji „ 7. Cykl życia instancji kontekstowych ”. Warto też wiedzieć, że sam kontener ma cykl życia, o którym przeczytasz w „ Wydarzeniach cyklu życia kontenera ”.
Krótka wycieczka do zastrzyku zależności lub

Całkowity

Powyżej przyjrzeliśmy się samemu wierzchołkowi góry lodowej zwanej CDI. CDI jest częścią specyfikacji JEE i jest używane w środowisku JavaEE. Ci, którzy korzystają ze Springa, nie używają CDI, ale DI, czyli są to nieco inne specyfikacje. Ale znając i rozumiejąc powyższe, możesz łatwo zmienić zdanie. Biorąc pod uwagę, że Spring obsługuje adnotacje ze świata CDI (ten sam Inject). Dodatkowe materiały: #Wiaczesław
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION