JavaRush /Blog Java /Random-PL /Używanie JNDI w Javie
Анзор Кармов
Poziom 31
Санкт-Петербург

Używanie JNDI w Javie

Opublikowano w grupie Random-PL
Cześć! Dziś przedstawimy Wam JNDI. Dowiedzmy się, co to jest, dlaczego jest potrzebne, jak działa i jak możemy z tym pracować. A potem napiszemy test jednostkowy Spring Boot, w którym będziemy bawić się tym właśnie JNDI. Używanie JNDI w Javie - 1

Wstęp. Usługi nazewnictwa i katalogowe

Zanim zagłębimy się w JNDI, przyjrzyjmy się, czym są usługi nazewnictwa i katalogowe. Najbardziej oczywistym przykładem takiej usługi jest system plików na dowolnym komputerze stacjonarnym, laptopie lub smartfonie. System plików zarządza (co dziwne) plikami. Pliki w takich systemach pogrupowane są w strukturę drzewiastą. Każdy plik ma unikalną pełną nazwę, na przykład: C:\windows\notepad.exe. Uwaga: pełna nazwa pliku to ścieżka od jakiegoś punktu głównego (dysk C) do samego pliku (notepad.exe). Węzłami pośrednimi w takim łańcuchu są katalogi (katalog Windows). Pliki wewnątrz katalogów mają atrybuty. Na przykład „Ukryty”, „Tylko do odczytu” itp. Szczegółowy opis tak prostej rzeczy, jak system plików, pomoże lepiej zrozumieć definicję nazewnictwa i usług katalogowych. Tak więc nazwa i usługa katalogowa to system zarządzający mapowaniem wielu nazw na wiele obiektów. W naszym systemie plików wchodzimy w interakcję z nazwami plików, które ukrywają obiekty — same pliki w różnych formatach. W usłudze nazewnictwa i katalogu nazwane obiekty są zorganizowane w strukturę drzewa. Obiekty katalogów mają atrybuty. Innym przykładem nazwy i usługi katalogowej jest DNS (Domain Name System). System ten zarządza mapowaniem pomiędzy nazwami domen czytelnymi dla człowieka (na przykład https://javarush.com/) i adresami IP odczytywalnymi maszynowo (na przykład 18.196.51.113). Oprócz DNS i systemów plików istnieje wiele innych usług, takich jak:

JNDI

JNDI, czyli Java Naming and Directory Interface, to interfejs API języka Java umożliwiający dostęp do usług nazewnictwa i katalogów. JNDI to interfejs API zapewniający jednolity mechanizm interakcji programu Java z różnymi usługami nazewniczymi i katalogowymi. Pod maską integracja JNDI z dowolną usługą odbywa się za pomocą interfejsu dostawcy usług (SPI). SPI umożliwia przejrzyste połączenie różnych usług nazewnictwa i usług katalogowych, umożliwiając aplikacji Java korzystanie z interfejsu API JNDI w celu uzyskania dostępu do połączonych usług. Poniższy rysunek ilustruje architekturę JNDI: Używanie JNDI w Javie - 2

Źródło: Poradniki Oracle Java

JNDI. Znaczenie w prostych słowach

Główne pytanie brzmi: po co nam JNDI? JNDI jest potrzebne, abyśmy mogli uzyskać obiekt Java z jakiejś „Rejestracji” obiektów z kodu Java według nazwy obiektu powiązanego z tym obiektem. Rozbijmy powyższe stwierdzenie na tezy, aby mnogość powtarzających się słów nas nie zmyliła:
  1. Ostatecznie musimy uzyskać obiekt Java.
  2. Pozyskamy ten obiekt z jakiegoś rejestru.
  3. W tym rejestrze znajduje się wiele obiektów.
  4. Każdy obiekt w tym rejestrze ma unikalną nazwę.
  5. Aby pobrać obiekt z rejestru, w naszym żądaniu musimy przekazać nazwę. Jakby chciał powiedzieć: „Proszę, oddaj mi to, co masz pod taką a taką nazwą”.
  6. Możemy nie tylko czytać obiekty po nazwie z rejestru, ale także zapisywać obiekty w tym rejestrze pod określonymi nazwami (jakoś tam trafiają).
Mamy więc pewnego rodzaju rejestr, czyli pamięć obiektową, czyli drzewo JNDI. Następnie na przykładzie spróbujmy zrozumieć znaczenie JNDI. Warto zauważyć, że w przeważającej części JNDI jest wykorzystywane w rozwoju przedsiębiorstw. A takie aplikacje działają wewnątrz jakiegoś serwera aplikacji. Serwerem tym może być serwer aplikacji Java EE, kontener serwletów, taki jak Tomcat, lub dowolny inny kontener. Sam rejestr obiektów, czyli drzewo JNDI, zwykle znajduje się wewnątrz tego serwera aplikacji. To drugie nie zawsze jest konieczne (można mieć takie drzewko lokalnie), ale jest najbardziej typowe. JNDI Tree może być zarządzane przez specjalną osobę (administratora systemu lub specjalistę DevOps), która „zapisuje w rejestrze” obiekty wraz z ich nazwami. Gdy nasza aplikacja i drzewo JNDI znajdują się w tym samym kontenerze, możemy łatwo uzyskać dostęp do dowolnego obiektu Java przechowywanego w takim rejestrze. Co więcej, rejestr i nasza aplikacja mogą znajdować się w różnych kontenerach, a nawet na różnych maszynach fizycznych. Nawet wtedy JNDI umożliwia zdalny dostęp do obiektów Java. Typowy przypadek. Administrator serwera Java EE umieszcza w rejestrze obiekt przechowujący informacje niezbędne do połączenia z bazą danych. W związku z tym, aby pracować z bazą danych, po prostu poprosimy o żądany obiekt z drzewa JNDI i będziemy z nim pracować. To jest bardzo wygodne. Wygoda polega również na tym, że w rozwoju przedsiębiorstwa istnieją różne środowiska. Istnieją serwery produkcyjne i serwery testowe (a często jest ich więcej niż 1 serwer testowy). Następnie umieszczając obiekt do połączenia z bazą danych na każdym serwerze wewnątrz JNDI i wykorzystując ten obiekt wewnątrz naszej aplikacji, nie będziemy musieli niczego zmieniać podczas wdrażania naszej aplikacji z jednego serwera (test, wydanie) na inny. Dostęp do bazy danych będzie możliwy z każdego miejsca. Przykład jest oczywiście nieco uproszczony, ale mam nadzieję, że pomoże lepiej zrozumieć, dlaczego JNDI jest potrzebne. Następnie bliżej poznamy JNDI w Javie, z pewnymi elementami ataku.

API JNDI

JNDI jest udostępniane w ramach platformy Java SE. Aby używać JNDI, należy zaimportować klasy JNDI, a także jednego lub więcej dostawców usług, aby uzyskać dostęp do usług nazewniczych i katalogowych. JDK obejmuje dostawców usług w zakresie następujących usług:
  • Lekki protokół dostępu do katalogów (LDAP);
  • Architektura brokera żądań wspólnego obiektu (CORBA);
  • usługa nazw Common Object Services (COS);
  • Rejestr zdalnego wywoływania metod Java (RMI);
  • Usługa nazw domen (DNS).
Kod API JNDI jest podzielony na kilka pakietów:
  • nazewnictwo javax;
  • javax.naming.directory;
  • javax.naming.ldap;
  • javax.naming.event;
  • javax.naming.spi.
Zaczniemy nasze wprowadzenie do JNDI od dwóch interfejsów – nazwy i kontekstu, które zawierają kluczową funkcjonalność JNDI

Nazwa interfejsu

Interfejs Name umożliwia kontrolowanie nazw komponentów oraz składni nazewnictwa JNDI. W JNDI wszystkie operacje na nazwach i katalogach wykonywane są w odniesieniu do kontekstu. Nie ma korzeni absolutnych. Dlatego JNDI definiuje obiekt początkowyContext, który stanowi punkt wyjścia dla operacji nazewnictwa i katalogów. Po uzyskaniu dostępu do kontekstu początkowego można go używać do wyszukiwania obiektów i innych kontekstów.
Name objectName = new CompositeName("java:comp/env/jdbc");
W powyższym kodzie zdefiniowaliśmy jakąś nazwę pod którą znajduje się jakiś obiekt (może nie jest zlokalizowany, ale na to liczymy). Naszym ostatecznym celem jest uzyskanie referencji do tego obiektu i wykorzystanie jej w naszym programie. Zatem nazwa składa się z kilku części (lub tokenów) oddzielonych ukośnikiem. Takie tokeny nazywane są kontekstami. Już pierwszy z nich to po prostu kontekst, wszystkie kolejne to podkontekst (zwany dalej podkontekstem). Konteksty są łatwiejsze do zrozumienia, jeśli pomyślisz o nich analogicznie do katalogów lub katalogów lub po prostu zwykłych folderów. Kontekstem głównym jest folder główny. Podkontekst to podfolder. Wszystkie komponenty (kontekst i podkontekst) danej nazwy możemy zobaczyć uruchamiając następujący kod:
Enumeration<String> elements = objectName.getAll();
while(elements.hasMoreElements()) {
  System.out.println(elements.nextElement());
}
Dane wyjściowe będą następujące:

java:comp
env
jdbc
Wynik pokazuje, że tokeny w nazwie są oddzielone od siebie ukośnikiem (jednak wspominaliśmy o tym). Każdy token nazwy ma swój własny indeks. Indeksowanie tokenów rozpoczyna się od 0. Kontekst główny ma indeks zero, następny kontekst ma indeks 1, następny 2 itd. Nazwę podkontekstu możemy uzyskać na podstawie jego indeksu:
System.out.println(objectName.get(1)); // -> env
Możemy także dodać dodatkowe tokeny (na końcu lub w konkretnym miejscu indeksu):
objectName.add("sub-context"); // Добавит sub-context в конец
objectName.add(0, "context"); // Добавит context в налачо
Pełną listę metod można znaleźć w oficjalnej dokumentacji .

Kontekst interfejsu

Interfejs ten zawiera zestaw stałych do inicjowania kontekstu, a także zestaw metod tworzenia i usuwania kontekstów, wiązania obiektów z nazwą oraz wyszukiwania i pobierania obiektów. Przyjrzyjmy się niektórym operacjom wykonywanym za pomocą tego interfejsu. Najczęstszą czynnością jest wyszukiwanie obiektu po nazwie. Odbywa się to za pomocą metod:
  • Object lookup(String name)
  • Object lookup(Name name)
Powiązanie obiektu z nazwą odbywa się za pomocą metod bind:
  • void bind(Name name, Object obj)
  • void bind(String name, Object obj)
Obie metody powiążą nazwę z obiektem.Odwrotna Object operacja wiązania, czyli odłączenie obiektu od nazwy, realizowana jest metodami unbind:
  • void unbind(Name name)
  • void unbind(String name)
Pełna lista metod jest dostępna na oficjalnej stronie internetowej z dokumentacją .

Kontekst początkowy

InitialContextto klasa reprezentująca element główny drzewa JNDI i implementująca platformę Context. Musisz wyszukiwać obiekty według nazwy w drzewie JNDI w stosunku do określonego węzła. Węzeł główny drzewa może służyć jako taki węzeł InitialContext. Typowy przypadek użycia JNDI to:
  • Dostawać InitialContext.
  • Służy InitialContextdo pobierania obiektów według nazwy z drzewa JNDI.
Można to uzyskać na kilka sposobów InitialContext. Wszystko zależy od środowiska, w którym znajduje się program Java. Na przykład, jeśli program Java i drzewo JNDI działają na tym samym serwerze aplikacji, uzyskanie InitialContext:
InitialContext context = new InitialContext();
Jeśli tak nie jest, uzyskanie kontekstu staje się nieco trudniejsze. Czasami konieczne jest przekazanie listy właściwości środowiska w celu zainicjowania kontekstu:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
    "com.sun.jndi.fscontext.RefFSContextFactory");

Context ctx = new InitialContext(env);
Powyższy przykład ilustruje jeden z możliwych sposobów inicjalizacji kontekstu i nie niesie ze sobą żadnego innego obciążenia semantycznego. Nie ma potrzeby szczegółowego zagłębiania się w kod.

Przykład użycia JNDI w teście jednostkowym SpringBoot

Powyżej powiedzieliśmy, że aby JNDI mogło współdziałać z usługą nazewniczą i katalogową, konieczne jest posiadanie pod ręką SPI (Service Provider Interface), za pomocą którego zostanie przeprowadzona integracja pomiędzy Javą a usługą nazewniczą. Standardowy JDK zawiera kilka różnych interfejsów SPI (wymieniliśmy je powyżej), z których każdy nie jest przydatny w celach demonstracyjnych. Podnoszenie aplikacji JNDI i Java w kontenerze jest dość interesujące. Jednak autor tego artykułu jest leniwą osobą, więc aby zademonstrować, jak działa JNDI, wybrał ścieżkę najmniejszego oporu: uruchom JNDI w teście jednostkowym aplikacji SpringBoot i uzyskaj dostęp do kontekstu JNDI za pomocą małego hacka ze Spring Framework. A więc nasz plan:
  • Napiszmy pusty projekt Spring Boot.
  • Stwórzmy test jednostkowy w tym projekcie.
  • W teście zademonstrujemy współpracę z JNDI:
    • uzyskać dostęp do kontekstu;
    • powiązać (powiązać) jakiś obiekt pod jakąś nazwą w JNDI;
    • pobierz obiekt według jego nazwy (wyszukiwanie);
    • Sprawdźmy, czy obiekt nie jest pusty.
Zacznijmy od porządku. Plik->Nowy->Projekt... Używanie JNDI w Javie - 3 Następnie wybierz element Spring Individualizr : Używanie JNDI w Javie - 4Wypełnij metadane dotyczące projektu: Używanie JNDI w Javie - 5Następnie wybierz wymagane komponenty Spring Framework. Będziemy wiązać niektóre obiekty DataSource, dlatego potrzebujemy komponentów do współpracy z bazą danych:
  • API JDBC;
  • H2 DBaza danych.
Używanie JNDI w Javie - 6Określmy lokalizację w systemie plików: Używanie JNDI w Javie - 7I projekt zostanie utworzony. Tak naprawdę został dla nas automatycznie wygenerowany jeden test jednostkowy, który wykorzystamy w celach demonstracyjnych. Poniżej znajduje się struktura projektu i test, którego potrzebujemy: Używanie JNDI w Javie - 8Zacznijmy pisać kod wewnątrz testu ContextLoads. Mały hack ze Springa, omówiony powyżej, to klasa SimpleNamingContextBuilder. Ta klasa została zaprojektowana w celu łatwego uruchamiania testów jednostkowych JNDI lub aplikacji autonomicznych. Napiszmy kod, aby uzyskać kontekst:
final SimpleNamingContextBuilder simpleNamingContextBuilder
       = new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();

final InitialContext context = new InitialContext();
Pierwsze dwie linijki kodu pozwolą nam później łatwo zainicjować kontekst JNDI. Bez nich InitialContextpodczas tworzenia instancji zostanie zgłoszony wyjątek: javax.naming.NoInitialContextException. Zastrzeżenie. Ta klasa SimpleNamingContextBuilderjest klasą przestarzałą. Ten przykład ma na celu pokazanie, jak można pracować z JNDI. Nie są to najlepsze praktyki używania JNDI wewnątrz testów jednostkowych. Można to powiedzieć, że jest to podstawa do budowania kontekstu oraz demonstrowania wiązania i pobierania obiektów z JNDI. Otrzymawszy kontekst, możemy z niego wyodrębnić obiekty lub poszukać obiektów w kontekście. W JNDI nie ma jeszcze obiektów, więc logicznym byłoby coś tam umieścić. Na przykład, DriverManagerDataSource:
context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));
W tej linii powiązaliśmy obiekt klasy DriverManagerDataSourcez nazwą java:comp/env/jdbc/datasource. Następnie możemy pobrać obiekt z kontekstu po nazwie. Nie mamy innego wyjścia, jak tylko pobrać obiekt, który właśnie umieściliśmy, ponieważ w kontekście nie ma innych obiektów =(
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
Sprawdźmy teraz, czy nasz DataSource ma połączenie (połączenie, połączenie lub połączenie to klasa Java zaprojektowana do pracy z bazą danych):
assert ds.getConnection() != null;
System.out.println(ds.getConnection());
Jeśli zrobiliśmy wszystko poprawnie, wynik będzie mniej więcej taki:

conn1: url=jdbc:h2:mem:mydb user=
Warto powiedzieć, że niektóre linie kodu mogą generować wyjątki. Zgłaszane są następujące linie javax.naming.NamingException:
  • simpleNamingContextBuilder.activate()
  • new InitialContext()
  • context.bind(...)
  • context.lookup(...)
A podczas pracy z klasą DataSourcemożna ją wyrzucić java.sql.SQLException. W związku z tym konieczne jest wykonanie kodu wewnątrz bloku try-catchlub wskazanie w podpisie jednostki testowej, że może zgłaszać wyjątki. Oto pełny kod klasy testowej:
@SpringBootTest
class JndiExampleApplicationTests {

    @Test
    void contextLoads() {
        try {
            final SimpleNamingContextBuilder simpleNamingContextBuilder
                    = new SimpleNamingContextBuilder();
            simpleNamingContextBuilder.activate();

            final InitialContext context = new InitialContext();

            context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));

            final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");

            assert ds.getConnection() != null;
            System.out.println(ds.getConnection());

        } catch (SQLException | NamingException e) {
            e.printStackTrace();
        }
    }
}
Po uruchomieniu testu możesz zobaczyć następujące logi:

o.s.m.jndi.SimpleNamingContextBuilder    : Activating simple JNDI environment
o.s.mock.jndi.SimpleNamingContext        : Static JNDI binding: [java:comp/env/jdbc/datasource] = [org.springframework.jdbc.datasource.DriverManagerDataSource@4925f4f5]
conn1: url=jdbc:h2:mem:mydb user=

Wniosek

Dzisiaj przyjrzeliśmy się JNDI. Dowiedzieliśmy się, czym są usługi nazewnictwa i katalogowe oraz że JNDI to interfejs API języka Java, który umożliwia jednolitą interakcję z różnymi usługami programu Java. Mianowicie za pomocą JNDI możemy rejestrować obiekty w drzewie JNDI pod określoną nazwą i odbierać te same obiekty po nazwie. Jako zadanie dodatkowe możesz uruchomić przykład działania JNDI. Powiąż inny obiekt z kontekstem, a następnie przeczytaj ten obiekt po nazwie.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION