JavaRush /Java-Blog /Random-DE /Verwendung von JNDI in Java
Анзор Кармов
Level 31
Санкт-Петербург

Verwendung von JNDI in Java

Veröffentlicht in der Gruppe Random-DE
Hallo! Heute stellen wir Ihnen JNDI vor. Lassen Sie uns herausfinden, was es ist, warum es benötigt wird, wie es funktioniert und wie wir damit arbeiten können. Und dann schreiben wir einen Spring Boot-Unit-Test, in dem wir mit genau diesem JNDI spielen. Verwendung von JNDI in Java – 1

Einführung. Namens- und Verzeichnisdienste

Bevor wir uns mit JNDI befassen, wollen wir verstehen, was Namens- und Verzeichnisdienste sind. Das offensichtlichste Beispiel für einen solchen Dienst ist das Dateisystem auf jedem PC, Laptop oder Smartphone. Das Dateisystem verwaltet (seltsamerweise) Dateien. Dateien in solchen Systemen sind in einer Baumstruktur gruppiert. Jede Datei hat einen eindeutigen vollständigen Namen, zum Beispiel: C:\windows\notepad.exe. Bitte beachten Sie: Der vollständige Dateiname ist der Pfad von einem Stammpunkt (Laufwerk C) zur Datei selbst (notepad.exe). Die Zwischenknoten in einer solchen Kette sind Verzeichnisse (Windows-Verzeichnis). Dateien in Verzeichnissen haben Attribute. Zum Beispiel „Versteckt“, „Schreibgeschützt“ usw. Eine detaillierte Beschreibung einer so einfachen Sache wie eines Dateisystems hilft, die Definition von Namens- und Verzeichnisdiensten besser zu verstehen. Ein Namens- und Verzeichnisdienst ist also ein System, das die Zuordnung vieler Namen zu vielen Objekten verwaltet. In unserem Dateisystem interagieren wir mit Dateinamen, die Objekte verbergen – die Dateien selbst in verschiedenen Formaten. Im Benennungs- und Verzeichnisdienst werden benannte Objekte in einer Baumstruktur organisiert. Und Verzeichnisobjekte haben Attribute. Ein weiteres Beispiel für einen Namens- und Verzeichnisdienst ist DNS (Domain Name System). Dieses System verwaltet die Zuordnung zwischen menschenlesbaren Domänennamen (z. B. https://javarush.com/) und maschinenlesbaren IP-Adressen (z. B. 18.196.51.113). Neben DNS und Dateisystemen gibt es noch viele andere Dienste, wie zum Beispiel:

JNDI

JNDI oder Java Naming and Directory Interface ist eine Java-API für den Zugriff auf Namens- und Verzeichnisdienste. JNDI ist eine API, die einem Java-Programm einen einheitlichen Mechanismus für die Interaktion mit verschiedenen Namens- und Verzeichnisdiensten bietet. Unter der Haube erfolgt die Integration zwischen JNDI und einem beliebigen Dienst mithilfe einer Service Provider Interface (SPI). SPI ermöglicht die transparente Verbindung verschiedener Namens- und Verzeichnisdienste, sodass eine Java-Anwendung die JNDI-API verwenden kann, um auf die verbundenen Dienste zuzugreifen. Die folgende Abbildung veranschaulicht die JNDI-Architektur: Verwendung von JNDI in Java - 2

Quelle: Oracle Java Tutorials

JNDI. Bedeutung in einfachen Worten

Die Hauptfrage ist: Warum brauchen wir JNDI? JNDI wird benötigt, damit wir ein Java-Objekt aus einer „Registrierung“ von Objekten aus Java-Code anhand des Namens des an dieses Objekt gebundenen Objekts erhalten können. Teilen wir die obige Aussage in Thesen auf, damit uns die Fülle wiederholter Wörter nicht verwirrt:
  1. Letztendlich müssen wir ein Java-Objekt erhalten.
  2. Wir werden dieses Objekt aus einer Registrierung erhalten.
  3. Es gibt eine Reihe von Objekten in dieser Registrierung.
  4. Jedes Objekt in dieser Registrierung hat einen eindeutigen Namen.
  5. Um ein Objekt aus der Registrierung zu erhalten, müssen wir in unserer Anfrage einen Namen übergeben. Als wollte man sagen: „Bitte gib mir, was du unter diesem und jenem Namen hast.“
  6. Wir können Objekte nicht nur anhand ihres Namens aus der Registry lesen, sondern auch Objekte in dieser Registry unter bestimmten Namen speichern (irgendwie landen sie dort).
Wir haben also eine Art Registrierung, einen Objektspeicher oder einen JNDI-Baum. Versuchen wir als Nächstes anhand eines Beispiels, die Bedeutung von JNDI zu verstehen. Es ist erwähnenswert, dass JNDI größtenteils in der Unternehmensentwicklung verwendet wird. Und solche Anwendungen funktionieren innerhalb eines Anwendungsservers. Dieser Server könnte ein Java EE-Anwendungsserver oder ein Servlet-Container wie Tomcat oder ein beliebiger anderer Container sein. Die Objektregistrierung selbst, also der JNDI-Baum, befindet sich normalerweise innerhalb dieses Anwendungsservers. Letzteres ist nicht immer notwendig (Sie können einen solchen Baum vor Ort haben), aber es ist am typischsten. Der JNDI-Baum kann von einer speziellen Person (Systemadministrator oder DevOps-Spezialist) verwaltet werden, die Objekte mit ihrem Namen „in der Registrierung speichert“. Wenn sich unsere Anwendung und der JNDI-Baum gemeinsam im selben Container befinden, können wir problemlos auf jedes Java-Objekt zugreifen, das in einer solchen Registrierung gespeichert ist. Darüber hinaus können sich die Registrierung und unsere Anwendung in verschiedenen Containern und sogar auf verschiedenen physischen Maschinen befinden. JNDI ermöglicht Ihnen auch dann den Fernzugriff auf Java-Objekte. Typischer Fall. Der Java EE-Serveradministrator platziert ein Objekt in der Registrierung, das die notwendigen Informationen zum Herstellen einer Verbindung zur Datenbank speichert. Um mit der Datenbank zu arbeiten, fordern wir dementsprechend einfach das gewünschte Objekt aus dem JNDI-Baum an und arbeiten damit. Es ist sehr bequem. Der Komfort liegt auch darin, dass es in der Unternehmensentwicklung unterschiedliche Umgebungen gibt. Es gibt Produktionsserver und Testserver (und oft gibt es mehr als einen Testserver). Indem wir dann auf jedem Server in JNDI ein Objekt für die Verbindung zur Datenbank platzieren und dieses Objekt in unserer Anwendung verwenden, müssen wir bei der Bereitstellung unserer Anwendung von einem Server (Test, Release) auf einem anderen nichts ändern. Der Zugriff auf die Datenbank wird überall möglich sein. Das Beispiel ist natürlich etwas vereinfacht, aber ich hoffe, es hilft Ihnen besser zu verstehen, warum JNDI benötigt wird. Als nächstes lernen wir JNDI in Java näher kennen, mit einigen Angriffselementen.

JNDI-API

JNDI wird innerhalb der Java SE-Plattform bereitgestellt. Um JNDI verwenden zu können, müssen Sie JNDI-Klassen sowie einen oder mehrere Dienstanbieter importieren, um auf Namens- und Verzeichnisdienste zuzugreifen. Das JDK umfasst Dienstleister für die folgenden Dienste:
  • Lightweight Directory Access Protocol (LDAP);
  • Common Object Request Broker-Architektur (CORBA);
  • Common Object Services (COS)-Namensdienst;
  • RMI-Registrierung (Java Remote Method Invocation);
  • Domain Name Service (DNS).
Der JNDI-API-Code ist in mehrere Pakete unterteilt:
  • javax.naming;
  • javax.naming.directory;
  • javax.naming.ldap;
  • javax.naming.event;
  • javax.naming.spi.
Wir beginnen unsere Einführung in JNDI mit zwei Schnittstellen – Name und Kontext –, die wichtige JNDI-Funktionen enthalten

Schnittstellenname

Mit der Namensschnittstelle können Sie Komponentennamen sowie die JNDI-Benennungssyntax steuern. In JNDI werden alle Namens- und Verzeichnisoperationen relativ zum Kontext ausgeführt. Es gibt keine absoluten Wurzeln. Daher definiert JNDI einen InitialContext, der einen Ausgangspunkt für Benennungs- und Verzeichnisoperationen bietet. Sobald auf den Ausgangskontext zugegriffen wird, kann dieser zur Suche nach Objekten und anderen Kontexten verwendet werden.
Name objectName = new CompositeName("java:comp/env/jdbc");
Im obigen Code haben wir einen Namen definiert, unter dem sich ein Objekt befindet (es befindet sich möglicherweise nicht, aber wir rechnen damit). Unser letztes Ziel ist es, eine Referenz auf dieses Objekt zu erhalten und es in unserem Programm zu verwenden. Der Name besteht also aus mehreren Teilen (oder Tokens), die durch einen Schrägstrich getrennt sind. Solche Token werden Kontexte genannt. Der allererste ist einfach Kontext, alle folgenden sind Unterkontext (im Folgenden als Unterkontext bezeichnet). Kontexte sind leichter zu verstehen, wenn man sie sich analog zu Verzeichnissen oder Verzeichnissen oder einfach nur normalen Ordnern vorstellt. Der Stammkontext ist der Stammordner. Unterkontext ist ein Unterordner. Wir können alle Komponenten (Kontext und Unterkontexte) eines bestimmten Namens sehen, indem wir den folgenden Code ausführen:
Enumeration<String> elements = objectName.getAll();
while(elements.hasMoreElements()) {
  System.out.println(elements.nextElement());
}
Die Ausgabe wird wie folgt sein:

java:comp
env
jdbc
Die Ausgabe zeigt, dass die Token im Namen durch einen Schrägstrich voneinander getrennt sind (wir haben dies jedoch erwähnt). Jeder Namenstoken hat seinen eigenen Index. Die Token-Indizierung beginnt bei 0. Der Stammkontext hat den Index Null, der nächste Kontext hat den Index 1, der nächste 2 usw. Wir können den Subkontextnamen anhand seines Index ermitteln:
System.out.println(objectName.get(1)); // -> env
Wir können auch zusätzliche Token hinzufügen (entweder am Ende oder an einer bestimmten Stelle im Index):
objectName.add("sub-context"); // Добавит sub-context в конец
objectName.add(0, "context"); // Добавит context в налачо
Eine vollständige Liste der Methoden finden Sie in der offiziellen Dokumentation .

Schnittstellenkontext

Diese Schnittstelle enthält eine Reihe von Konstanten zum Initialisieren eines Kontexts sowie eine Reihe von Methoden zum Erstellen und Löschen von Kontexten, zum Binden von Objekten an einen Namen sowie zum Suchen und Abrufen von Objekten. Schauen wir uns einige der Vorgänge an, die über diese Schnittstelle ausgeführt werden. Die häufigste Aktion ist die Suche nach einem Objekt anhand des Namens. Dies geschieht mit folgenden Methoden:
  • Object lookup(String name)
  • Object lookup(Name name)
Das Binden eines Objekts an einen Namen erfolgt mithilfe von Methoden bind:
  • void bind(Name name, Object obj)
  • void bind(String name, Object obj)
Beide Methoden binden den Namen an das Objekt. Object Der umgekehrte Vorgang des Bindens – das Lösen der Bindung eines Objekts von einem Namen – wird mit den folgenden Methoden ausgeführt unbind:
  • void unbind(Name name)
  • void unbind(String name)
Eine vollständige Liste der Methoden finden Sie auf der offiziellen Dokumentationswebsite .

InitialContext

InitialContextist eine Klasse, die das Wurzelelement des JNDI-Baums darstellt und die implementiert Context. Sie müssen im JNDI-Baum relativ zu einem bestimmten Knoten nach Objekten anhand ihres Namens suchen. Als solcher Knoten kann der Wurzelknoten des Baumes dienen InitialContext. Ein typischer Anwendungsfall für JNDI ist:
  • Erhalten InitialContext.
  • InitialContextZum Abrufen von Objekten nach Namen aus dem JNDI-Baum verwenden .
Es gibt mehrere Möglichkeiten, es zu bekommen InitialContext. Es hängt alles von der Umgebung ab, in der sich das Java-Programm befindet. Wenn beispielsweise ein Java-Programm und ein JNDI-Baum auf demselben Anwendungsserver ausgeführt werden, ist es InitialContextganz einfach, Folgendes zu erhalten:
InitialContext context = new InitialContext();
Ist dies nicht der Fall, wird es etwas schwieriger, den Kontext zu ermitteln. Manchmal ist es notwendig, eine Liste von Umgebungseigenschaften zu übergeben, um den Kontext zu initialisieren:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
    "com.sun.jndi.fscontext.RefFSContextFactory");

Context ctx = new InitialContext(env);
Das obige Beispiel zeigt eine der möglichen Möglichkeiten zur Initialisierung eines Kontexts und bringt keine weitere semantische Last mit sich. Es ist nicht erforderlich, sich im Detail mit dem Code zu befassen.

Ein Beispiel für die Verwendung von JNDI in einem SpringBoot-Einheitentest

Oben haben wir gesagt, dass für die Interaktion von JNDI mit dem Namens- und Verzeichnisdienst SPI (Service Provider Interface) erforderlich ist, mit dessen Hilfe die Integration zwischen Java und dem Namensdienst durchgeführt wird. Das Standard-JDK enthält mehrere verschiedene SPIs (wir haben sie oben aufgelistet), die für Demonstrationszwecke jeweils wenig interessant sind. Es ist einigermaßen interessant, eine JNDI- und Java-Anwendung in einem Container zu erstellen. Allerdings ist der Autor dieses Artikels ein fauler Mensch. Um zu demonstrieren, wie JNDI funktioniert, wählte er den Weg des geringsten Widerstands: Führen Sie JNDI in einem SpringBoot-Anwendungskomponententest aus und greifen Sie mithilfe eines kleinen Hacks aus dem Spring Framework auf den JNDI-Kontext zu. Also, unser Plan:
  • Schreiben wir ein leeres Spring Boot-Projekt.
  • Lassen Sie uns in diesem Projekt einen Komponententest erstellen.
  • Im Rahmen des Tests demonstrieren wir die Arbeit mit JNDI:
    • Zugriff auf den Kontext erhalten;
    • binden (binden) Sie ein Objekt unter einem bestimmten Namen in JNDI;
    • Holen Sie sich das Objekt anhand seines Namens (Suche);
    • Überprüfen wir, ob das Objekt nicht null ist.
Beginnen wir der Reihe nach. Datei->Neu->Projekt... Verwendung von JNDI in Java – 3 Wählen Sie als Nächstes das Element Spring Initializr aus : Verwendung von JNDI in Java – 4Geben Sie die Metadaten zum Projekt ein: Verwendung von JNDI in Java – 5Wählen Sie dann die erforderlichen Spring Framework-Komponenten aus. Wir werden einige DataSource-Objekte binden, daher benötigen wir Komponenten, um mit der Datenbank zu arbeiten:
  • JDBC-API;
  • H2 DDatabase.
Verwendung von JNDI in Java – 6Bestimmen wir den Speicherort im Dateisystem: Verwendung von JNDI in Java – 7Und das Projekt wird erstellt. Tatsächlich wurde für uns automatisch ein Unit-Test generiert, den wir zu Demonstrationszwecken verwenden werden. Nachfolgend finden Sie die Projektstruktur und den Test, den wir benötigen: Verwendung von JNDI in Java – 8Beginnen wir mit dem Schreiben von Code innerhalb des contextLoads-Tests. Ein kleiner Hack von Spring, der oben besprochen wurde, ist die Klasse SimpleNamingContextBuilder. Diese Klasse wurde entwickelt, um JNDI problemlos innerhalb von Unit-Tests oder eigenständigen Anwendungen zu erhöhen. Schreiben wir den Code, um den Kontext zu erhalten:
final SimpleNamingContextBuilder simpleNamingContextBuilder
       = new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();

final InitialContext context = new InitialContext();
Mit den ersten beiden Codezeilen können wir den JNDI-Kontext später einfach initialisieren. Ohne sie InitialContextwird beim Erstellen einer Instanz eine Ausnahme ausgelöst: javax.naming.NoInitialContextException. Haftungsausschluss. Die Klasse SimpleNamingContextBuilderist eine veraltete Klasse. Und dieses Beispiel soll zeigen, wie man mit JNDI arbeiten kann. Dies sind keine Best Practices für die Verwendung von JNDI innerhalb von Unit-Tests. Man kann sagen, dass dies eine Krücke für den Aufbau eines Kontexts und die Demonstration der Bindung und des Abrufens von Objekten aus JNDI ist. Nachdem wir einen Kontext erhalten haben, können wir Objekte daraus extrahieren oder im Kontext nach Objekten suchen. Es gibt noch keine Objekte in JNDI, daher wäre es logisch, dort etwas abzulegen. Zum Beispiel, DriverManagerDataSource:
context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));
In dieser Zeile haben wir das Klassenobjekt DriverManagerDataSourcean den Namen gebunden java:comp/env/jdbc/datasource. Als nächstes können wir das Objekt anhand des Namens aus dem Kontext abrufen. Wir haben keine andere Wahl, als das Objekt zu erhalten, das wir gerade eingefügt haben, da sich im Kontext keine anderen Objekte befinden =(
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
Überprüfen wir nun, ob unsere DataSource eine Verbindung hat (Verbindung, Verbindung oder Verbindung ist eine Java-Klasse, die für die Arbeit mit einer Datenbank konzipiert ist):
assert ds.getConnection() != null;
System.out.println(ds.getConnection());
Wenn wir alles richtig gemacht haben, sieht die Ausgabe etwa so aus:

conn1: url=jdbc:h2:mem:mydb user=
Es ist erwähnenswert, dass einige Codezeilen Ausnahmen auslösen können. Die folgenden Zeilen werden geworfen javax.naming.NamingException:
  • simpleNamingContextBuilder.activate()
  • new InitialContext()
  • context.bind(...)
  • context.lookup(...)
Und wenn man mit einer Klasse arbeitet, DataSourcekann es geworfen werden java.sql.SQLException. In diesem Zusammenhang ist es notwendig, den Code innerhalb eines Blocks auszuführen try-catchoder in der Signatur der Testeinheit anzugeben, dass sie Ausnahmen auslösen kann. Hier ist der vollständige Code der Testklasse:
@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();
        }
    }
}
Nach dem Ausführen des Tests können Sie die folgenden Protokolle sehen:

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=

Abschluss

Heute haben wir uns JNDI angesehen. Wir haben erfahren, was Namens- und Verzeichnisdienste sind und dass JNDI eine Java-API ist, die Ihnen die einheitliche Interaktion mit verschiedenen Diensten eines Java-Programms ermöglicht. Mit Hilfe von JNDI können wir nämlich Objekte im JNDI-Baum unter einem bestimmten Namen aufzeichnen und dieselben Objekte namentlich empfangen. Als Bonusaufgabe können Sie ein Beispiel für die Funktionsweise von JNDI ausführen. Binden Sie ein anderes Objekt in den Kontext ein und lesen Sie dieses Objekt dann anhand seines Namens.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION