JavaRush /Java блог /Random UA /Використання JNDI в Java
Анзор Кармов
31 рівень
Санкт-Петербург

Використання JNDI в Java

Стаття з групи Random UA
Вітання! Сьогодні ми познайомимося з тобою з JNDI. Дізнаємося, що це таке, для чого воно потрібне, як працює, як нам з ним працювати. А потім напишемо Spring Boot юніт тест, усередині якого гратимемося з цим самим JNDI. Використання JNDI в Java - 1

Вступ. Служби імен та каталогів

Перш ніж занурюватися в JNDI, розберемося з тим, що таке служби імен і каталогів. Найбільш наочним прикладом такої служби є файлова система на будь-якому ПК, ноутбуці або смартфоні. Файлова система управляє (хоч як це дивно) файлуми. Файли у таких системах згруповані у деревоподібну структуру. Кожен файл має унікальне повне ім'я, наприклад: C:\windows\notepad.exe. Зверніть увагу: повне ім'я файлу є шлях від деякої кореневої точки (диск C) до самого файлу (notepad.exe). Проміжними вузлами такого ланцюга є каталоги (каталог windows). Файли всередині каталогів мають атрибути. Наприклад "Прихований", "Тільки для читання" та ін. Детальний опис такої простої речі як файлова система допоможе краще зрозуміти визначення служби імен та каталогів. Отже,- Це система, яка управляє відображенням безлічі імен у безлічі об'єктів. У нашій файловій системі ми взаємодіємо з іменами файлів, за якими ховаються об'єкти – самі файли у різних форматах. У службі імен та каталогів іменовані об'єкти зібрані у деревоподібну структуру. А об'єкти каталогу мають атрибути. Ще одним прикладом служби імен та каталогів є DNS (англ. - Domain Name System, "система доменних імен"). Ця система керує відповідністю між зрозумілими людині доменними іменами (наприклад, https://javarush.com/) та зрозумілими комп'ютеру IP-адресаами (наприклад, 18.196.51.113). Крім DNS і файлових систем, є ще безліч інших служб, таких як:

JNDI

JNDI, або ж Java Naming and Directory Interface, є Java API для доступу до служб імен і каталогів. JNDI – це API, яке надає одноманітний механізм взаємодії Java-програми з різними службами імен та каталогів. "Під капотом" інтеграція між JNDI та будь-якою конкретною службою здійснюється за допомогою інтерфейсу постачальника послуг (Service Provider Interface, SPI). SPI дозволяє прозоро підключати різні служби іменування та каталогів, що дозволяє Java-програмі використовувати JNDI API для доступу до підключених служб. Рисунок нижче ілюструє архітектуру JNDI: Використання JNDI в Java - 2

Джерело: Oracle Java Tutorials

JNDI. Сенс простими словами

Головне питання: навіщо потрібний JNDI? JNDI потрібен для того, щоб ми могли з Java-коду отримати Java-об'єкт з деякої "Реєстратури" об'єктів на ім'я об'єкта, прив'язаного до цього об'єкта. Розіб'ємо твердження вище на тези, щоб розмаїття повторюваних слів не збило нас з пантелику:
  1. Зрештою, нам потрібно отримати Java-об'єкт.
  2. Ми отримаємо цей об'єкт із деякої реєстратури.
  3. У цій реєстратурі є купа об'єктів.
  4. Кожен об'єкт у цій реєстратурі має унікальне ім'я.
  5. Щоб отримати певний об'єкт із реєстратури, ми маємо у своєму запиті передати ім'я. Як би сказати: "Дайте мені, будь ласка, те, що у вас лежить під таким ім'ям".
  6. Ми можемо не тільки зчитувати об'єкти за їхнім ім'ям з реєстратури, але й зберігати в цій реєстратурі об'єкти під певними іменами (адже вони туди потрапляють).
Отже, у нас є якась реєстратура, або сховище об'єктів, або JNDI Tree. Далі, наприклад, спробуємо зрозуміти сенс JNDI. Варто відзначити, що здебільшого JNDI використовується в Enterprise-розробці. А подібні програми працюють всередині деякого застосування сервера. Цим сервером може бути якийсь Java EE Application Server або контейнер сервлетів, на зразок Tomcat, або будь-який інший контейнер. Сама реєстратура об'єктів, тобто JNDI Tree, зазвичай знаходиться всередині цього application сервера. Останнє не завжди є обов'язковим (таке дерево можна мати локально), але найбільш типове. JNDI Tree може керуватися спеціальною людиною (системний адміністратор або DevOps фахівець), яка "зберігатиме в реєстратурі" об'єкти з їх іменами. Коли наша програма та JNDI Tree знаходяться спільно всередині одного контейнера, ми без будь-яких проблем можемо отримати доступ до будь-якого Java-об'єкта, який зберігається в такій реєстратурі. Більше того, реєстратура та наше застосування можуть знаходитися в різних контейнерах і навіть на різних фізичних машинах. JNDI навіть у такому разі дозволяє отримувати доступ до Java-об'єктів віддалено. Типовий кейс. Адміністратор Java EE сервера кладе в реєстратуру об'єкт, де зберігається необхідна інформація для підключення до бази даних. Відповідно, для роботи з БД ми просто запитаємо потрібний об'єкт із JNDI tree та будемо з ним працювати. Це дуже зручно. Зручність полягає ще й у тому, що в enterprise-розробці існують різні оточення. Є продакшн сервера, є тестові (і часто тестових буває більше 1 шт.). Тоді, розмістивши на кожному сервері всередині JNDI об'єкт для підключення до БД і використовуючи цей об'єкт усередині нашої програми, нам не доведеться нічого змінювати при депло нашого додатка з одного сервера (тестового, релізного) на інший. Всюди буде доступ до бази даних. Приклад, звичайно, певною мірою спрощений, але, сподіваюся, він допоможе краще зрозуміти, навіщо потрібний JNDI. Далі будемо знайомитись з JNDI in 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).
Код JNDI API поділено на кілька пакетів:
  • javax.naming;
  • javax.naming.directory;
  • javax.naming.ldap;
  • javax.naming.event;
  • javax.naming.spi.
Знайомство з JNDI ми почнемо з двох інтерфейсів – Name та Context, які містять ключову функціональність JNDI

Інтерфейс 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)
Обидва методи прив'яжуть ім'я name до об'єкта 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.
Почнемо по порядку. File->New->Project... Використання JNDI в Java - 3 Далі виберемо пункт Spring Initializr : Використання JNDI в Java - 4Заповним метадані про проект: Використання JNDI в Java - 5Після чого виберемо необхідні компоненти Spring Framework. Ми будемо прив'язувати якісь DataSource-об'єкти, тому нам потрібні компоненти для роботи з БД:
  • JDBC API;
  • H2 DDatabase.
Використання JNDI в Java - 6Визначимо розташування у файловій системі: Використання JNDI в Java - 7І проект створено. Насправді за нас автоматично згенерували один юніт тест, яким ми й скористаємося для демонстраційних цілей. Нижче - структура проекту і потрібний нам тест: Використання JNDI в Java - 8Приступимо до написання коду всередині тесту contextLoads. Невеликий хак від спрингу, про який йшлося вище - це клас 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=

Висновок

Сьогодні ми розбирали JNDI. Дізналися про те, що таке служби імен і каталогів, і що JNDI - це Java API, яке дозволяє одноманітно взаємодіяти з різними службами Java програми. А саме за допомогою JNDI ми можемо записувати об'єкти в JNDI tree під деяким ім'ям і отримувати ці об'єкти на ім'я. Як бонусне завдання можна запустити приклад роботи JNDI. Прив'язати в контекст будь-який інший об'єкт, а потім рахувати цей об'єкт на ім'я.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ