Ciao! Oggi vi presenteremo JNDI. Scopriamo cos'è, perché è necessario, come funziona, come possiamo lavorarci. E poi scriveremo uno unit test Spring Boot, all'interno del quale giocheremo proprio con questo JNDI.
Introduzione. Servizi di denominazione e directory
Prima di immergerci in JNDI, capiamo cosa sono i servizi di denominazione e di directory. L'esempio più ovvio di tale servizio è il file system su qualsiasi PC, laptop o smartphone. Il file system gestisce (stranamente) i file. I file in tali sistemi sono raggruppati in una struttura ad albero. Ogni file ha un nome completo univoco, ad esempio: C:\windows\notepad.exe. Nota: il nome completo del file è il percorso da un punto radice (unità C) al file stesso (notepad.exe). I nodi intermedi in tale catena sono directory (directory di Windows). I file all'interno delle directory hanno attributi. Ad esempio, "Nascosto", "Sola lettura", ecc. Una descrizione dettagliata di una cosa così semplice come un file system aiuterà a comprendere meglio la definizione di denominazione e servizi di directory. Quindi, un servizio di nomi e directory è un sistema che gestisce la mappatura di molti nomi su molti oggetti. Nel nostro file system interagiamo con nomi di file che nascondono oggetti: i file stessi in vari formati. Nel servizio di denominazione e directory, gli oggetti con nome sono organizzati in una struttura ad albero. E gli oggetti della directory hanno attributi. Un altro esempio di servizio di nomi e directory è il DNS (Domain Name System). Questo sistema gestisce la mappatura tra nomi di dominio leggibili dall'uomo (ad esempio, https://javarush.com/) e indirizzi IP leggibili dalla macchina (ad esempio, 18.196.51.113). Oltre a DNS e file system, ci sono molti altri servizi, come:- Protocollo di accesso leggero alla directory (LDAP) ;
- Servizio di denominazione CORBA ;
- Servizio di informazione di rete (NIS) ;
- E altri.
JNDI
JNDI, o Java Naming and Directory Interface, è un'API Java per l'accesso ai servizi di denominazione e directory. JNDI è un'API che fornisce un meccanismo uniforme affinché un programma Java possa interagire con vari servizi di denominazione e directory. Dietro le quinte, l'integrazione tra JNDI e qualsiasi servizio viene realizzata utilizzando un'interfaccia del fornitore di servizi (SPI). SPI consente la connessione trasparente di vari servizi di denominazione e directory, consentendo a un'applicazione Java di utilizzare l'API JNDI per accedere ai servizi connessi. La figura seguente illustra l'architettura JNDI:Fonte: tutorial Java Oracle
JNDI. Il significato in parole semplici
La domanda principale è: perché abbiamo bisogno di JNDI? JNDI è necessario in modo che possiamo ottenere un oggetto Java da qualche "Registrazione" di oggetti dal codice Java con il nome dell'oggetto associato a questo oggetto. Suddividiamo l'affermazione di cui sopra in tesi in modo che l'abbondanza di parole ripetute non ci confonda:- Alla fine dobbiamo ottenere un oggetto Java.
- Otterremo questo oggetto da qualche registro.
- Ci sono un sacco di oggetti in questo registro.
- Ogni oggetto in questo registro ha un nome univoco.
- Per ottenere un oggetto dal registro, dobbiamo passare un nome nella nostra richiesta. Come a dire: “Per favore, dammi quello che hai sotto questo o quel nome”.
- Non solo possiamo leggere gli oggetti in base al loro nome dal registro, ma anche salvare gli oggetti in questo registro con determinati nomi (in qualche modo finiscono lì).
API JNDI
JNDI viene fornito all'interno della piattaforma Java SE. Per utilizzare JNDI, è necessario importare le classi JNDI, nonché uno o più fornitori di servizi per accedere ai servizi di denominazione e directory. Il JDK comprende fornitori di servizi per i seguenti servizi:- Protocollo di accesso alle directory leggero (LDAP);
- Architettura Common Object Request Broker (CORBA);
- Servizio nomi Common Object Services (COS);
- Registro RMI (Remote Method Invocation) Java;
- Servizio dei nomi di dominio (DNS).
- javax.nome;
- javax.naming.directory;
- javax.naming.ldap;
- javax.naming.evento;
- javax.naming.spi.
Nome dell'interfaccia
L'interfaccia Nome consente di controllare i nomi dei componenti e la sintassi dei nomi JNDI. In JNDI, tutte le operazioni sui nomi e sulle directory vengono eseguite in relazione al contesto. Non esistono radici assolute. Pertanto, JNDI definisce un InitialContext, che fornisce un punto di partenza per le operazioni di denominazione e di directory. Una volta effettuato l'accesso al contesto iniziale, è possibile utilizzarlo per cercare oggetti e altri contesti.Name objectName = new CompositeName("java:comp/env/jdbc");
Nel codice sopra, abbiamo definito un nome con il quale si trova un oggetto (potrebbe non essere localizzato, ma ci contiamo). Il nostro obiettivo finale è ottenere un riferimento a questo oggetto e utilizzarlo nel nostro programma. Quindi, il nome è composto da più parti (o token), separate da una barra. Tali token sono chiamati contesti. Il primo è semplicemente contesto, tutti i successivi sono sottocontesto (di seguito denominato sottocontesto). I contesti sono più facili da capire se li consideri analoghi a directory o directory o semplicemente a cartelle normali. Il contesto root è la cartella root. Il sottocontesto è una sottocartella. Possiamo vedere tutti i componenti (contesto e sottocontesti) di un dato nome eseguendo il seguente codice:
Enumeration<String> elements = objectName.getAll();
while(elements.hasMoreElements()) {
System.out.println(elements.nextElement());
}
L'output sarà il seguente:
java:comp
env
jdbc
L'output mostra che i token nel nome sono separati l'uno dall'altro da una barra (tuttavia, ne abbiamo parlato). Ogni token del nome ha il proprio indice. L'indicizzazione dei token inizia da 0. Il contesto root ha indice zero, il contesto successivo ha indice 1, il successivo 2, ecc. Possiamo ottenere il nome del sottocontesto dal suo indice:
System.out.println(objectName.get(1)); // -> env
Possiamo anche aggiungere ulteriori token (alla fine o in una posizione specifica nell'indice):
objectName.add("sub-context"); // Добавит sub-context в конец
objectName.add(0, "context"); // Добавит context в налачо
Un elenco completo dei metodi è reperibile nella documentazione ufficiale .
Contesto dell'interfaccia
Questa interfaccia contiene un insieme di costanti per inizializzare un contesto, nonché un insieme di metodi per creare ed eliminare contesti, associare oggetti a un nome e cercare e recuperare oggetti. Diamo un'occhiata ad alcune delle operazioni eseguite utilizzando questa interfaccia. L'azione più comune è cercare un oggetto per nome. Questo viene fatto utilizzando i metodi:Object lookup(String name)
Object lookup(Name name)
bind
:
void bind(Name name, Object obj)
void bind(String name, Object obj)
Object
L'operazione inversa di associazione - svincolo di un oggetto da un nome, viene eseguita utilizzando i metodi unbind
:
void unbind(Name name)
void unbind(String name)
Contestoiniziale
InitialContext
è una classe che rappresenta l'elemento root dell'albero JNDI e implementa Context
. È necessario cercare gli oggetti per nome all'interno dell'albero JNDI relativo a un determinato nodo. Il nodo radice dell'albero può fungere da tale nodo InitialContext
. Un tipico caso d'uso per JNDI è:
- Ottenere
InitialContext
. - Utilizzare
InitialContext
per recuperare oggetti in base al nome dall'albero JNDI.
InitialContext
. Tutto dipende dall'ambiente in cui si trova il programma Java. Ad esempio, se un programma Java e un albero JNDI sono in esecuzione all'interno dello stesso server delle applicazioni, è InitialContext
abbastanza semplice ottenere:
InitialContext context = new InitialContext();
Se così non fosse, ottenere il contesto diventa un po’ più difficile. A volte è necessario passare un elenco di proprietà dell'ambiente per inizializzare il contesto:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
Context ctx = new InitialContext(env);
L'esempio sopra dimostra uno dei modi possibili per inizializzare un contesto e non comporta nessun altro carico semantico. Non è necessario immergersi nel codice in dettaglio.
Un esempio di utilizzo di JNDI all'interno di uno unit test SpringBoot
Sopra abbiamo detto che affinché JNDI possa interagire con il servizio di denominazione e directory, è necessario avere a portata di mano la SPI (Service Provider Interface), con l'aiuto della quale verrà effettuata l'integrazione tra Java e il servizio di denominazione. Il JDK standard viene fornito con diversi SPI diversi (li abbiamo elencati sopra), ognuno dei quali è di scarso interesse a scopo dimostrativo. Creare un'applicazione JNDI e Java all'interno di un contenitore è piuttosto interessante. Tuttavia, l'autore di questo articolo è una persona pigra, quindi per dimostrare come funziona JNDI, ha scelto il percorso di minor resistenza: eseguire JNDI all'interno di uno unit test dell'applicazione SpringBoot e accedere al contesto JNDI utilizzando un piccolo hack dallo Spring Framework. Quindi, il nostro piano:- Scriviamo un progetto Spring Boot vuoto.
- Creiamo uno unit test all'interno di questo progetto.
- All'interno del test dimostreremo come lavorare con JNDI:
- ottenere l'accesso al contesto;
- associare (associare) alcuni oggetti con un nome in JNDI;
- ottenere l'oggetto in base al nome (ricerca);
- Controlliamo che l'oggetto non sia nullo.
- API JDBC;
- Database H2D.
SimpleNamingContextBuilder
. Questa classe è progettata per generare facilmente JNDI all'interno di test unitari o applicazioni autonome. Scriviamo il codice per ottenere il contesto:
final SimpleNamingContextBuilder simpleNamingContextBuilder
= new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();
final InitialContext context = new InitialContext();
Le prime due righe di codice ci permetteranno di inizializzare facilmente il contesto JNDI in seguito. Senza di essi, InitialContext
verrà generata un'eccezione durante la creazione di un'istanza: javax.naming.NoInitialContextException
. Disclaimer. La classe SimpleNamingContextBuilder
è una classe deprecata. E questo esempio ha lo scopo di mostrare come puoi lavorare con JNDI. Queste non sono le migliori pratiche per l'utilizzo di JNDI all'interno dei test unitari. Si può dire che questo sia un supporto per costruire un contesto e dimostrare l'associazione e il recupero di oggetti da JNDI. Dopo aver ricevuto un contesto, possiamo estrarre oggetti da esso o cercare oggetti nel contesto. Non ci sono ancora oggetti in JNDI, quindi sarebbe logico inserire qualcosa lì. Per esempio, DriverManagerDataSource
:
context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));
In questa riga abbiamo associato l'oggetto classe DriverManagerDataSource
al nome java:comp/env/jdbc/datasource
. Successivamente, possiamo ottenere l'oggetto dal contesto in base al nome. Non abbiamo altra scelta che ottenere l'oggetto che abbiamo appena inserito, perché non ci sono altri oggetti nel contesto =(
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
Ora controlliamo che il nostro DataSource abbia una connessione (connessione, connessione o connessione è una classe Java progettata per funzionare con un database):
assert ds.getConnection() != null;
System.out.println(ds.getConnection());
Se abbiamo fatto tutto correttamente, l'output sarà qualcosa del genere:
conn1: url=jdbc:h2:mem:mydb user=
Vale la pena dire che alcune righe di codice potrebbero generare eccezioni. Vengono lanciate le seguenti righe javax.naming.NamingException
:
simpleNamingContextBuilder.activate()
new InitialContext()
context.bind(...)
context.lookup(...)
DataSource
può essere lanciato java.sql.SQLException
. A questo proposito è necessario eseguire il codice all'interno di un blocco try-catch
, oppure indicare nella firma dell'unità di test che può lanciare eccezioni. Ecco il codice completo della classe di test:
@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();
}
}
}
Dopo aver eseguito il test, puoi visualizzare i seguenti log:
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