你好!今天我们将为您介绍JNDI。让我们了解它是什么、为什么需要它、它如何工作以及我们如何使用它。然后我们将编写一个 Spring Boot 单元测试,在其中我们将使用这个 JNDI。
介绍。命名和目录服务
在深入研究 JNDI 之前,让我们先了解什么是命名和目录服务。此类服务最明显的例子是任何 PC、笔记本电脑或智能手机上的文件系统。文件系统管理(奇怪的是)文件。此类系统中的文件以树结构分组。每个文件都有一个唯一的全名,例如:C:\windows\notepad.exe。请注意:完整的文件名是从某个根点(驱动器 C)到文件本身(notepad.exe)的路径。这种链中的中间节点是目录(windows目录)。目录中的文件具有属性。例如“隐藏”、“只读”等。对文件系统这样简单的东西进行详细的描述,将有助于更好地理解命名和目录服务的定义。因此,名称和目录服务是一个管理多个名称到多个对象的映射的系统。在我们的文件系统中,我们与隐藏对象的文件名(各种格式的文件本身)进行交互。在命名和目录服务中,命名对象被组织成树结构。并且目录对象具有属性。名称和目录服务的另一个示例是 DNS(域名系统)。该系统管理人类可读的域名(例如,https://javarush.com/)和机器可读的IP地址(例如,18.196.51.113)之间的映射。除了 DNS 和文件系统之外,还有很多其他服务,例如:- 轻量级目录访问协议(LDAP);
- CORBA 命名服务;
- 网络信息服务(NIS);
- 和别的。
JNDI
JNDI(Java 命名和目录接口)是用于访问命名和目录服务的 Java API。JNDI 是一个 API,它为 Java 程序提供与各种命名和目录服务交互的统一机制。在底层,JNDI 和任何给定服务之间的集成是使用服务提供者接口 (SPI) 完成的。SPI 允许透明地连接各种命名和目录服务,从而允许 Java 应用程序使用 JNDI API 访问连接的服务。下图展示了JNDI架构:JNDI。用简单的话来说意思
主要问题是:为什么我们需要 JNDI?需要 JNDI,以便我们可以通过绑定到该对象的对象名称从 Java 代码中的对象“注册”中获取 Java 对象。让我们将上面的陈述分成几个主题,这样大量重复的单词就不会让我们感到困惑:- 最终我们需要得到一个Java对象。
- 我们将从某个注册表中获取该对象。
- 这个注册表中有很多对象。
- 该注册表中的每个对象都有一个唯一的名称。
- 要从注册表获取对象,我们必须在请求中传递一个名称。仿佛在说:“请把你在某某名字下的东西给我。”
- 我们不仅可以通过注册表中的名称读取对象,还可以将对象以某些名称保存在注册表中(以某种方式它们最终会出现在那里)。
JNDI API
JNDI 在 Java SE 平台内提供。要使用 JNDI,您必须导入 JNDI 类以及一个或多个服务提供程序以访问命名和目录服务。JDK 包括以下服务的服务提供者:- 轻量级目录访问协议(LDAP);
- 通用对象请求代理架构(CORBA);
- 通用对象服务(COS)名称服务;
- Java 远程方法调用 (RMI) 注册表;
- 域名服务 (DNS)。
- javax.命名;
- javax.命名.目录;
- javax.naming.ldap;
- javax.命名.事件;
- javax.naming.spi。
接口名称
Name 接口允许您控制组件名称以及 JNDI 命名语法。在 JNDI 中,所有名称和目录操作都是相对于上下文执行的。没有绝对的根源。因此,JNDI定义了一个InitialContext,它提供了命名和目录操作的起点。一旦访问了初始上下文,就可以使用它来搜索对象和其他上下文。Name objectName = new CompositeName("java:comp/env/jdbc");
在上面的代码中,我们定义了一些名称,某个对象位于该名称下(它可能没有找到,但我们指望它)。我们的最终目标是获得对该对象的引用并在我们的程序中使用它。因此,名称由多个部分(或标记)组成,并用斜杠分隔。这样的标记称为上下文。第一个是简单的上下文,后面的都是子上下文(以下简称子上下文)。如果您将上下文视为类似于目录或目录,或者只是常规文件夹,则更容易理解上下文。根上下文是根文件夹。子上下文是一个子文件夹。我们可以通过运行以下代码来查看给定名称的所有组件(上下文和子上下文):
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 в налачо
完整的方法列表可以在官方文档中找到。
接口上下文
该接口包含一组用于初始化上下文的常量,以及一组用于创建和删除上下文、将对象绑定到名称以及搜索和检索对象的方法。让我们看一下使用此接口执行的一些操作。最常见的操作是按名称搜索对象。这是使用以下方法完成的: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
是一个类,表示 JNDI 树的根元素并实现Context
. 您需要在 JNDI 树中相对于某个节点按名称搜索对象。树的根节点可以作为这样的节点InitialContext
。JNDI 的典型用例是:
- 得到
InitialContext
。 - 用于
InitialContext
从 JNDI 树中按名称检索对象。
InitialContext
。这一切都取决于Java程序所在的环境。例如,如果 Java 程序和 JNDI 树运行在同一应用程序服务器内,则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);
上面的示例演示了初始化上下文的可能方法之一,并且不携带任何其他语义负载。无需深入研究代码的细节。
在 SpringBoot 单元测试中使用 JNDI 的示例
上面我们说过,JNDI要与命名和目录服务进行交互,就需要有SPI(Service Provider Interface),借助它来进行Java与命名服务的集成。标准 JDK 附带了几个不同的 SPI(我们在上面列出了它们),其中每一个对于演示目的来说都没什么意义。在容器内生成 JNDI 和 Java 应用程序有些有趣。然而,本文的作者是个懒人,因此为了演示 JNDI 的工作原理,他选择了阻力最小的路径:在 SpringBoot 应用程序单元测试中运行 JNDI,并使用 Spring 框架中的一个小技巧来访问 JNDI 上下文。所以,我们的计划:- 让我们编写一个空的 Spring Boot 项目。
- 让我们在这个项目中创建一个单元测试。
- 在测试中,我们将演示如何使用 JNDI:
- 访问上下文;
- 在 JNDI 中绑定(bind)某个名称下的某个对象;
- 通过名称获取对象(查找);
- 让我们检查该对象是否不为空。
- 数据库接口;
- H2 D数据库。
SimpleNamingContextBuilder
. 此类旨在轻松地在单元测试或独立应用程序中引发 JNDI。让我们编写代码来获取上下文:
final SimpleNamingContextBuilder simpleNamingContextBuilder
= new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();
final InitialContext context = new InitialContext();
前两行代码将允许我们稍后轻松地初始化 JNDI 上下文。如果没有它们,InitialContext
创建实例时将会抛出异常:javax.naming.NoInitialContextException
。免责声明。该类SimpleNamingContextBuilder
是一个已弃用的类。此示例旨在展示如何使用 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 是否有一个连接(连接、连接或连接是一个旨在与数据库一起使用的 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=
GO TO FULL VERSION