JavaRush /Java блог /Java Developer /Использование JNDI в Java
Анзор Кармов
31 уровень
Санкт-Петербург

Использование JNDI в Java

Статья из группы Java Developer
Привет! Сегодня мы познакомимся с тобой с 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-разработке. А подобные приложения работают внутри некоторого application сервера. Этим сервером может быть какой-нибудь Java EE Application Server или же контейнер сервлетов, вроде Tomcat, либо любой другой контейнер. Сама регистратура объектов, то есть, JNDI Tree, обычно находится внутри этого application сервера. Последнее не всегда обязательно (такое дерево можно иметь локально), но наиболее типично. JNDI Tree может управляться специальным человеком (системный администратор или DevOps специалист), который будет “сохранять в регистратуре” объекты с их именами. Когда наше приложение и JNDI Tree находятся совместно внутри одного контейнера, мы безо всяких проблем можем получить доступ к любому Java-объекту, который хранится в такой регистратуре. Более того, регистратура и наше приложения могут находиться в разных контейнерах и даже на разных физических машинах. JNDI даже в таком случае позволяет получать доступ к Java-объектам удаленно. Типичный кейс. Администратор Java EE сервера кладет в регистратуру объект, в котором хранится необходимая информация для подключения к базе данных. Соответственно, для работы с БД мы просто запросим нужный объект из JNDI tree и будем с ним работать. Это очень удобно. Удобство заключается еще и в том, что в enterprise-разработке существуют различные окружения. Есть продакшн сервера, есть тестовые (и часто тестовых бывает более 1 шт.). Тогда, разместив на каждом сервере внутри JNDI объект для подключения к БД и используя этот объект внутри нашего приложения, нам не придется ничего менять при деплое нашего приложения с одного сервера (тестового, релизного) на другой. Везде будет доступ к базе данных. Пример, конечно, в какой то мере упрощенный, но, надеюсь, он поможет лучше понять, зачем нужен JNDI. Далее будем знакомиться c 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. Привязать в контекст какой-нибудь другой объект, а затем считать этот объект по имени.
Комментарии (13)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
arteemmius Уровень 18
7 декабря 2022
Автору большое уважение! P.S. Заметил небольшую опечатку: Интерфейс Context ... Рассмотрим некоторые операции, которые выполняются с помощьюа данного интерфейса.
Romanya Уровень 33
27 июня 2022
хорошая статья, спасибо автору
Dmitriy Gordievskiy Уровень 36
27 ноября 2021
Спасибо за статью, просто и доступно написано. Время от времени мелькала аббревиатура JNDI, но полноценно прочитать что это такое и зачем руки не доходили)
Just me Уровень 41
22 июля 2021
Доступная и наглядная статья. Спасибо.
3 мая 2021
где можно увидеть пример вот этого "Типичный кейс. Администратор Java EE сервера кладет в регистратуру объект, в котором хранится необходимая информация для подключения к базе данных. Соответственно, для работы с БД мы просто запросим нужный объект из JNDI tree и будем с ним работать. Это очень удобно. Удобство заключается еще и в том, что в enterprise-разработке существуют различные окружения. Есть продакшн сервера, есть тестовые (и часто тестовых бывает более 1 шт.). Тогда, разместив на каждом сервере внутри JNDI объект для подключения к БД и используя этот объект внутри нашего приложения, нам не придется ничего менять при деплое нашего приложения с одного сервера (тестового, релизного) на другой. Везде будет доступ к базе данных." ? раз типичный кейс значит примерчик накидать - раз плюнуть ?
30 марта 2021
Предлагает на выбор несколько DataSource, надо выбрать

import javax.sql.DataSource;
George Matua Уровень 30
7 февраля 2021
👍
barracuda Уровень 41 Expert
27 января 2021
Хорошая статья. Дает первое представление о точ, что такое JNDI
6 ноября 2020
Спринг можно инициализировать на сайте спринга. Идея как раз это и делает. https://start.spring.io/
Андрей Уровень 17
16 апреля 2020
Все закончилось на пункте "Далее выберем пункт Spring Initializr:" - нет такого пункта в IDEA :(