Вітання! Сьогодні ми познайомимося з тобою з JNDI. Дізнаємося, що це таке, для чого воно потрібне, як працює, як нам з ним працювати. А потім напишемо Spring Boot юніт тест, усередині якого гратимемося з цим самим JNDI.
Вступ. Служби імен та каталогів
Перш ніж занурюватися в JNDI, розберемося з тим, що таке служби імен і каталогів. Найбільш наочним прикладом такої служби є файлова система на будь-якому ПК, ноутбуці або смартфоні. Файлова система управляє (хоч як це дивно) файлуми. Файли у таких системах згруповані у деревоподібну структуру. Кожен файл має унікальне повне ім'я, наприклад: C:\windows\notepad.exe. Зверніть увагу: повне ім'я файлу є шлях від деякої кореневої точки (диск C) до самого файлу (notepad.exe). Проміжними вузлами такого ланцюга є каталоги (каталог windows). Файли всередині каталогів мають атрибути. Наприклад "Прихований", "Тільки для читання" та ін. Детальний опис такої простої речі як файлова система допоможе краще зрозуміти визначення служби імен та каталогів. Отже,- Це система, яка управляє відображенням безлічі імен у безлічі об'єктів. У нашій файловій системі ми взаємодіємо з іменами файлів, за якими ховаються об'єкти – самі файли у різних форматах. У службі імен та каталогів іменовані об'єкти зібрані у деревоподібну структуру. А об'єкти каталогу мають атрибути. Ще одним прикладом служби імен та каталогів є DNS (англ. - Domain Name System, "система доменних імен"). Ця система керує відповідністю між зрозумілими людині доменними іменами (наприклад, https://javarush.com/) та зрозумілими комп'ютеру IP-адресаами (наприклад, 18.196.51.113). Крім DNS і файлових систем, є ще безліч інших служб, таких як:- Lightweight Directory Access Protocol (LDAP) ;
- сервіс іменування CORBA ;
- Network Information Service (NIS) ;
- Та інші.
JNDI
JNDI, або ж Java Naming and Directory Interface, є Java API для доступу до служб імен і каталогів. JNDI – це API, яке надає одноманітний механізм взаємодії Java-програми з різними службами імен та каталогів. "Під капотом" інтеграція між JNDI та будь-якою конкретною службою здійснюється за допомогою інтерфейсу постачальника послуг (Service Provider Interface, SPI). SPI дозволяє прозоро підключати різні служби іменування та каталогів, що дозволяє Java-програмі використовувати JNDI API для доступу до підключених служб. Рисунок нижче ілюструє архітектуру JNDI:Джерело: Oracle Java Tutorials
JNDI. Сенс простими словами
Головне питання: навіщо потрібний JNDI? JNDI потрібен для того, щоб ми могли з Java-коду отримати Java-об'єкт з деякої "Реєстратури" об'єктів на ім'я об'єкта, прив'язаного до цього об'єкта. Розіб'ємо твердження вище на тези, щоб розмаїття повторюваних слів не збило нас з пантелику:- Зрештою, нам потрібно отримати Java-об'єкт.
- Ми отримаємо цей об'єкт із деякої реєстратури.
- У цій реєстратурі є купа об'єктів.
- Кожен об'єкт у цій реєстратурі має унікальне ім'я.
- Щоб отримати певний об'єкт із реєстратури, ми маємо у своєму запиті передати ім'я. Як би сказати: "Дайте мені, будь ласка, те, що у вас лежить під таким ім'ям".
- Ми можемо не тільки зчитувати об'єкти за їхнім ім'ям з реєстратури, але й зберігати в цій реєстратурі об'єкти під певними іменами (адже вони туди потрапляють).
JNDI API
JNDI постачається усередині платформи Java SE. Для використання JNDI необхідно імпортувати JNDI класи, а також один або більше постачальників послуг для доступу до служб імен та каталогів. JDK включає постачальників послуг до наступних служб:- Lightweight Directory Access Protocol (LDAP);
- Common Object Request Broker Architecture (CORBA);
- Common Object Services (COS) name service;
- Java Remote Method Invocation (RMI) Registry;
- Domain Name Service (DNS).
- javax.naming;
- javax.naming.directory;
- javax.naming.ldap;
- javax.naming.event;
- javax.naming.spi.
Інтерфейс Name
За допомогою інтерфейсу Name можна керувати іменами компонентів, а також синтаксисом імен JNDI. У JNDI всі операції з іменами та каталогами виконуються щодо контексту. Абсолютного коріння немає. Тому JNDI визначає InitialContext, який забезпечує відправну точку для іменування та операцій із каталогами. Після отримання доступу до початкового контексту його можна використовувати для пошуку об'єктів та інших контекстів.Name objectName = new CompositeName("java:comp/env/jdbc");
У коді вище ми визначабо деяке ім'я, під яким знаходиться певний об'єкт (можливо, і не перебуває, але розраховуємо на це). Наша кінцева мета – отримати посилання на цей об'єкт і використовувати його в нашій програмі. Отже, ім'я складається з кількох частин (або токенів), розділених слешем. Такі токени називають контекстами (context). Найперший – просто context, всі наступні – sub-context (далі за текстом – підконтекст). Контексти простіше розуміти, якщо їх як аналогію каталогів чи директорій, чи навіть звичайних папок. Кореневий контекст – коренева папка. Підконтекст – вкладена папка. Ми можемо побачити всі складові (контекст та підконтексти) даного імені, виконавши наступний код:
Enumeration<String> elements = objectName.getAll();
while(elements.hasMoreElements()) {
System.out.println(elements.nextElement());
}
Висновок буде наступним:
java:comp
env
jdbc
Висновок демонструє, що токени відокремлюються один від одного слешем (втім, ми це згадували). Кожен токен імені має власний індекс. Індексація токенів починається з 0. Нульовим індексом має кореневий контекст, наступний контекст має індекс 1, наступний 2 і т.д. Ми можемо отримати ім'я підконтексту за його індексом:
System.out.println(objectName.get(1)); // -> env
Можемо також додавати додаткові токени (або в кінець, або у певне місце за індексом):
objectName.add("sub-context"); // Добавит sub-context в конец
objectName.add(0, "context"); // Добавит context в налачо
З повним переліком методів можна ознайомитись офіційною в документації .
Інтерфейс Context
Цей інтерфейс містить набір констант для ініціалізації контексту, а також набір методів для створення та видалення контекстів, прив'язки об'єктів до імені, а також для пошуку та отримання об'єктів. Розглянемо деякі операції, які виконуються за допомогою даного інтерфейсу. Найчастіша дія - пошук об'єкта на ім'я. Здійснюється за допомогою методів:Object lookup(String name)
Object lookup(Name name)
bind
:
void bind(Name name, Object obj)
void bind(String name, Object obj)
Object
Зворотня операція прив'язки - відв'язування об'єкта від імені здійснюється за допомогою unbind
методів
void unbind(Name name)
void unbind(String name)
InitialContext
InitialContext
- Це клас, який являє собою кореневий елемент JNDI tree і реалізує інтерфейс Context
. Шукати об'єкти на ім'я всередині JNDI tree потрібно щодо деякого вузла. Таким вузлом може бути кореневий вузол дерева — InitialContext
. Типовим сценарієм використання JNDI є:
- Отримати
InitialContext
. - Використовувати
InitialContext
для вилучення об'єктів на ім'я з JNDI tree.
InitialContext
буває кілька. Все залежить від оточення, де знаходиться Java-програма. Наприклад, якщо Java-програма та JNDI tree запущені всередині одного і того ж application сервера, отримати InitialContext
досить просто:
InitialContext context = new InitialContext();
Якщо це не так, отримати контекст стає трохи складніше. Деколи буває необхідно передати список пропертів оточення для ініціалізації контексту:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
Context ctx = new InitialContext(env);
Приклад вище демонструє один із можливих способів ініціалізації контексту та іншого смислового навантаження у собі не несе. Детально занурюватись у код не потрібно.
Приклад використання JNDI усередині SpringBoot unit тесту
Вище ми говорабо про те, що для взаємодії JNDI зі службою імен та каталогів необхідно мати під рукою SPI (Service Provider Interface), за допомогою якого здійснюватиметься інтеграція між Джавою та службою імен. Стандартна JDK поставляється з декількома різними SPI (вище ми їх перераховували), кожен із яких не викликає великого інтересу для демонстраційних цілей. Підняти JNDI і Java додаток всередині якогось контейнера якоюсь мірою цікаво. Однак автор цієї статті - людина лінива, тому для демонстрації роботи JNDI вибрав шлях найменшого опору: запустити JNDI всередині юніт-тесту SpringBoot програми та отримати доступ до контексту JNDI за допомогою невеликого хаку від Spring Framework. Отже, наш план:- Напишемо порожній Spring Boot проект.
- Усередині цього проекту створимо юніт-тест.
- Усередині тесту продемонструємо роботу з JNDI:
- отримаємо доступ до контексту;
- прив'яжемо (bind) деякий об'єкт під деяким ім'ям JNDI;
- отримаємо об'єкт на його ім'я (lookup);
- перевіримо, що об'єкт не null.
- JDBC API;
- H2 DDatabase.
SimpleNamingContextBuilder
. Даний клас призначений для того, щоб легко піднімати JNDI всередині юніт-тестів або stand-alone додатків. Напишемо код для отримання контексту:
final SimpleNamingContextBuilder simpleNamingContextBuilder
= new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();
final InitialContext context = new InitialContext();
Перші два рядки коду дозволять нам простим чином ініціалізувати контекст JNDI. Без них під час створення екземпляра InitialContext
буде викинуто виняток: javax.naming.NoInitialContextException
. Дисклеймер. Клас SimpleNamingContextBuilder
є Deprecated класом. І цей приклад має показати, як можна попрацювати з JNDI. Це не найкращі практики з використання JNDI усередині юніт-тестів. Це можна сказати мабоця для побудови контексту та демонстрації прив'язки та отримання об'єктів з JNDI. Отримавши контест, ми можемо витягувати з нього об'єкти або шукати об'єкти в контексті. Поки що в JNDI об'єктів немає, тому логічно буде покласти туди щось. Наприклад DriverManagerDataSource
:
context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));
У цьому рядку ми прив'язали об'єкт класу DriverManagerDataSource
до імені java:comp/env/jdbc/datasource
. Далі ми можемо отримати об'єкт із контексту на ім'я. Нам нічого іншого не залишається, окрім як отримати об'єкт, який ми поклали щойно, тому що інших об'єктів у контексті немає =(
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
Тепер перевіримо, що наш DataSource має коннекшн (коннекшн, connection або з'єднання – це Java-клас, який призначений для роботи з базою даних):
assert ds.getConnection() != null;
System.out.println(ds.getConnection());
Якщо ми все зробабо правильно, висновок буде приблизно таким:
conn1: url=jdbc:h2:mem:mydb user=
Варто сказати, деякі рядки коду можуть залишити винятки. Наступні рядки кидають javax.naming.NamingException
:
simpleNamingContextBuilder.activate()
new InitialContext()
context.bind(...)
context.lookup(...)
DataSource
може бути кинуто java.sql.SQLException
. У зв'язку з цим необхідно виконувати код усередині блоку try-catch
або вказувати в сигнатурі юніт тесту, що він може викинути винятки. Наведемо повний код тестового класу:
@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();
}
}
}
Після запуску тесту можна спостерігати такі логи:
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=