JavaRush /Java 博客 /Random-ZH /在 Java 中使用 JNDI
Анзор Кармов
第 31 级
Санкт-Петербург

在 Java 中使用 JNDI

已在 Random-ZH 群组中发布
你好!今天我们将为您介绍JNDI。让我们了解它是什么、为什么需要它、它如何工作以及我们如何使用它。然后我们将编写一个 Spring Boot 单元测试,在其中我们将使用这个 JNDI。 在 Java 中使用 JNDI - 1

介绍。命名和目录服务

在深入研究 JNDI 之前,让我们先了解什么是命名和目录服务。此类服务最明显的例子是任何 PC、笔记本电脑或智能手机上的文件系统。文件系统管理(奇怪的是)文件。此类系统中的文件以树结构分组。每个文件都有一个唯一的全名,例如:C:\windows\notepad.exe。请注意:完整的文件名是从某个根点(驱动器 C)到文件本身(notepad.exe)的路径。这种链中的中间节点是目录(windows目录)。目录中的文件具有属性。例如“隐藏”、“只读”等。对文件系统这样简单的东西进行详细的描述,将有助于更好地理解命名和目录服务的定义。因此,名称和目录服务是一个管理多个名称到多个对象的映射的系统。在我们的文件系统中,我们与隐藏对象的文件名(各种格式的文件本身)进行交互。在命名和目录服务中,命名对象被组织成树结构。并且目录对象具有属性。名称和目录服务的另一个示例是 DNS(域名系统)。该系统管理人类可读的域名(例如,https://javarush.com/)和机器可读的IP地址(例如,18.196.51.113)之间的映射。除了 DNS 和文件系统之外,还有很多其他服务,例如:

JNDI

JNDI(Java 命名和目录接口)是用于访问命名和目录服务的 Java API。JNDI 是一个 API,它为 Java 程序提供与各种命名和目录服务交互的统一机制。在底层,JNDI 和任何给定服务之间的集成是使用服务提供者接口 (SPI) 完成的。SPI 允许透明地连接各种命名和目录服务,从而允许 Java 应用程序使用 JNDI API 访问连接的服务。下图展示了JNDI架构: 在 Java 中使用 JNDI - 2

来源:Oracle Java 教程

JNDI。用简单的话来说意思

主要问题是:为什么我们需要 JNDI?需要 JNDI,以便我们可以通过绑定到该对象的对象名称从 Java 代码中的对象“注册”中获取 Java 对象。让我们将上面的陈述分成几个主题,这样大量重复的单词就不会让我们感到困惑:
  1. 最终我们需要得到一个Java对象。
  2. 我们将从某个注册表中获取该对象。
  3. 这个注册表中有很多对象。
  4. 该注册表中的每个对象都有一个唯一的名称。
  5. 要从注册表获取对象,我们必须在请求中传递一个名称。仿佛在说:“请把你在某某名字下的东西给我。”
  6. 我们不仅可以通过注册表中的名称读取对象,还可以将对象以某些名称保存在注册表中(以某种方式它们最终会出现在那里)。
因此,我们有某种注册表、对象存储或 JNDI 树。接下来,我们通过一个例子来尝试理解JNDI的含义。值得注意的是,JNDI 大部分用于企业开发。此类应用程序在某些应用程序服务器内运行。该服务器可以是某个 Java EE 应用程序服务器,或者像 Tomcat 这样的 servlet 容器,或者任何其他容器。对象注册表本身,即 JNDI 树,通常位于该应用程序服务器内部。后者并不总是必要的(您可以在本地拥有这样一棵树),但它是最典型的。JNDI 树可以由特殊人员(系统管理员或 DevOps 专家)管理,他们将用名称“保存在注册表中”对象。当我们的应用程序和 JNDI 树位于同一个容器内时,我们可以轻松访问存储在此类注册表中的任何 Java 对象。而且,注册表和我们的应用程序可以位于不同的容器中,甚至位于不同的物理机器上。JNDI 甚至允许您远程访问 Java 对象。典型。Java EE 服务器管理员在注册表中放置一个对象,该对象存储连接到数据库所需的信息。因此,要使用数据库,我们只需从 JNDI 树请求所需的对象并使用它即可。非常舒服。方便还在于企业发展有不同的环境。有生产服务器,也有测试服务器(通常有不止 1 台测试服务器)。然后,通过在 JNDI 内的每台服务器上放置一个用于连接数据库的对象,并在我们的应用程序中使用该对象,我们在将应用程序从一台服务器(测试、发布)部署到另一台服务器时无需进行任何更改。到处都可以访问数据库。当然,这个例子有些简化,但我希望它能帮助您更好地理解为什么需要 JNDI。接下来,我们将通过一些攻击元素更深入地了解 Java 中的 JNDI。

JNDI API

JNDI 在 Java SE 平台内提供。要使用 JNDI,您必须导入 JNDI 类以及一个或多个服务提供程序以访问命名和目录服务。JDK 包括以下服务的服务提供者:
  • 轻量级目录访问协议(LDAP);
  • 通用对象请求代理架构(CORBA);
  • 通用对象服务(COS)名称服务;
  • Java 远程方法调用 (RMI) 注册表;
  • 域名服务 (DNS)。
JNDI API代码分为几个包:
  • javax.命名;
  • javax.命名.目录;
  • javax.naming.ldap;
  • javax.命名.事件;
  • javax.naming.spi。
我们将从两个接口开始介绍 JNDI - Name 和 Context,它们包含关键的 JNDI 功能

接口名称

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)
两种方法都会将名称 name 绑定到对象。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)某个名称下的某个对象;
    • 通过名称获取对象(查找);
    • 让我们检查该对象是否不为空。
让我们按顺序开始吧。 File->New->Project... 在 Java 中使用 JNDI - 3接下来,选择Spring Initializr项: 在 Java 中使用 JNDI - 4填写有关项目的元数据: 在 Java 中使用 JNDI - 5然后选择所需的 Spring Framework 组件。我们将绑定一些 DataSource 对象,因此我们需要组件来处理数据库:
  • 数据库接口;
  • H2 D数据库。
在 Java 中使用 JNDI - 6让我们确定文件系统中的位置: 在 Java 中使用 JNDI - 7项目已创建。事实上,系统会自动为我们生成一个单元测试,我们将使用它来进行演示。下面是项目结构和我们需要的测试: 在 Java 中使用 JNDI - 8让我们开始在 contextLoads 测试中编写代码。上面讨论的 Spring 的一个小技巧是类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=

结论

今天我们研究了 JNDI。我们了解了命名和目录服务是什么,以及 JNDI 是一种 Java API,它允许您通过 Java 程序与不同的服务进行统一交互。也就是说,借助JNDI,我们可以将对象以某个名称记录在JNDI树中,并通过名称接收这些相同的对象。作为奖励任务,您可以运行 JNDI 如何工作的示例。将一些其他对象绑定到上下文中,然后按名称读取该对象。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION