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.
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:- Lightweight Directory Access Protocol (LDAP) ;
- CORBA-Namensdienst ;
- Netzwerkinformationsdienst (NIS) ;
- Und andere.
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: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:- Letztendlich müssen wir ein Java-Objekt erhalten.
- Wir werden dieses Objekt aus einer Registrierung erhalten.
- Es gibt eine Reihe von Objekten in dieser Registrierung.
- Jedes Objekt in dieser Registrierung hat einen eindeutigen Namen.
- 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.“
- 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).
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).
- javax.naming;
- javax.naming.directory;
- javax.naming.ldap;
- javax.naming.event;
- javax.naming.spi.
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)
bind
:
void bind(Name name, Object obj)
void bind(String name, Object obj)
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)
InitialContext
InitialContext
ist 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
. InitialContext
Zum Abrufen von Objekten nach Namen aus dem JNDI-Baum verwenden .
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 InitialContext
ganz 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.
- JDBC-API;
- H2 DDatabase.
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 InitialContext
wird beim Erstellen einer Instanz eine Ausnahme ausgelöst: javax.naming.NoInitialContextException
. Haftungsausschluss. Die Klasse SimpleNamingContextBuilder
ist 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 DriverManagerDataSource
an 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(...)
DataSource
kann es geworfen werden java.sql.SQLException
. In diesem Zusammenhang ist es notwendig, den Code innerhalb eines Blocks auszuführen try-catch
oder 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=
GO TO FULL VERSION