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.
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:- Lekki protokół dostępu do katalogów (LDAP) ;
- usługa nazewnictwa CORBA ;
- Sieciowy serwis informacyjny (NIS) ;
- I inni.
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:Ź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:- Ostatecznie musimy uzyskać obiekt Java.
- Pozyskamy ten obiekt z jakiegoś rejestru.
- W tym rejestrze znajduje się wiele obiektów.
- Każdy obiekt w tym rejestrze ma unikalną nazwę.
- 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ą”.
- 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ą).
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).
- nazewnictwo javax;
- javax.naming.directory;
- javax.naming.ldap;
- javax.naming.event;
- javax.naming.spi.
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)
bind
:
void bind(Name name, Object obj)
void bind(String name, Object obj)
Object
operacja wiązania, czyli odłączenie obiektu od nazwy, realizowana jest metodami unbind
:
void unbind(Name name)
void unbind(String name)
Kontekst początkowy
InitialContext
to 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
InitialContext
do pobierania obiektów według nazwy z drzewa JNDI.
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.
- API JDBC;
- H2 DBaza danych.
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 InitialContext
podczas tworzenia instancji zostanie zgłoszony wyjątek: javax.naming.NoInitialContextException
. Zastrzeżenie. Ta klasa SimpleNamingContextBuilder
jest 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 DriverManagerDataSource
z 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(...)
DataSource
można ją wyrzucić java.sql.SQLException
. W związku z tym konieczne jest wykonanie kodu wewnątrz bloku try-catch
lub 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=
GO TO FULL VERSION