Использование JNDI в Java
Привет! Сегодня мы познакомимся с тобой с JNDI. Узнаем, что это такое, для чего оно нужно, как работает, как нам с ним работать. А затем напишем Spring Boot юнит тест, внутри которого будем играться с этим самым JNDI.
Введение. Службы имен и каталогов
JNDI
JNDI, или же Java Naming and Directory Interface, представляет собой Java API для доступа к службам имен и каталогов. JNDI — это API, которое предоставляет единообразный механизм взаимодействия Java-программы с различными службами имен и каталогов. “Под капотом” интеграция между JNDI и любой конкретной службой осуществляется с помощью интерфейса поставщика услуг (Service Provider Interface, SPI). SPI позволяет прозрачно подключать различные службы именования и каталогов, что позволяет Java-приложению использовать JNDI API для доступа к подключенным службам. Рисунок ниже иллюстрирует архитектуру JNDI:
JNDI. Смысл простыми словами
- В конечном итоге нам нужно получить Java-объект.
- Мы получим этот объект из некоторой регистратуры.
- В этой регистратуре есть куча объектов.
- Каждый объект в этой регистратуре обладает уникальным именем.
- Чтобы получить некоторый объект из регистратуры, мы должны в своем запросе передать имя. Как бы сказать: «Дайте мне, пожалуйста, то, что у вас лежит под таким то именем».
- Мы можем не только считывать объекты по их имени из регистратуры, но и сохранять в данной регистратуре объекты под определенными именами (как-то ведь они туда попадают).
JNDI API
- 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 elements = objectName.getAll(); while(elements.hasMoreElements())
Вывод демонстрирует, что токены в имени отделяются друг от друга слэшем (впрочем, мы это упоминали). Каждый токен имени имеет свой индекс. Индексация токенов начинается с 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)
- void bind(Name name, Object obj)
- void bind(String name, Object obj)
- void unbind(Name name)
- void unbind(String name)
InitialContext
- Получить InitialContext .
- Использовать InitialContext для извлечения объектов по имени из JNDI tree.
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.Далее выберем пункт Spring Initializr: Заполним метаданные о проекте: После чего выберем необходимы компоненты Spring Framework. Мы будем привязывать какие-нибудь DataSource-объекты, поэтому нам нужны компоненты для работы с БД:
- JDBC API;
- H2 DDatabase.
Определим расположение в файловой системе: И проект создан. На самом деле за нас автоматически был сгенерирован один юнит тест, которым мы и воспользуемся для демонстрационных целей. Ниже — структура проекта и нужный нам тест: Приступим к написанию кода внутри теста 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=
- simpleNamingContextBuilder.activate()
- new InitialContext()
- context.bind(. )
- context.lookup(. )
@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. Привязать в контекст какой-нибудь другой объект, а затем считать этот объект по имени.