JNDI Glean
邓赤岩
2023-12-01
[b]JNDI 基本说明[/b]
JNDI ( Java Naming and Directory Interface ),是 Java 提供的一组与各种命名目录服务系统交互的接口。它是一套标准的接口,独立于各种命名目录服务系统,与具体类型的命名目录服务无关。Java 应用程序通过 JNDI 接口与命名目录服务交互,交互细节由服务提供者实现,Java 应用程序无需关心。
(个人觉得这是JAVA 的特点:向Java应用程序开发人员开放简单的接口,复杂的实现细节由服务提供者完成)
命令目录服务包括两个服务:命名服务(Naming Service)和目录(Directory Service)服务。命名服务就是用一个人类容易理解的名称与对象相关联,并且允许通过该名称找到与之对应的对象。我们天天都会用到的命名服务:DNS。个人觉得,目录服务是命名服务的超集,目录服务除了有命名服务的功能外,它的对象还可以有属性。常用的目录服务类型有 LDAP 等。
[b]JNDI 上下文[/b]
JNDI 一个主要功能就是用名字(name)来标识对象(object)。name 在 JNDI 中唯一,它跟某个特定的 object 关联,这种关联关系称之为 binding。binding的集合在 JNDI 中称为上下文 context。在JNDI 特定的上下文中,通过 name就可以找到相应的对象。
Naming Service Context:命名服务上下文视图:
[img]http://dl.iteye.com/upload/attachment/439218/b4bdef6a-4fea-357a-b03b-36e1bc796f4e.png[/img]
在上图中,a 为根 context,在它下面绑定有 dog、pig和 sheep 三个对象,同时又有 b 和 c 两个子 context,而 b 中有 d 这个子 context,d 又有 e 这个子 context,每个 context 中都绑定了相应的对象。
如果当前 context 为 a,那么 acontext.lookup(“dog”)就得到 dog 对象,而 acontext.lookup(“b/d/e/cat”) 就可得到猫这个对象。
如果当地 context 为 d,那么 dcontext.lookup(“wolf”) 就可得到 wolf 对象,dcontext.lookup(“e/rabit”) 就可得到 rabit 对象。
Directory Service Context:目录服务上下文视图:
[img]http://dl.iteye.com/upload/attachment/439208/3b7a4585-44f6-39fa-9a8b-7ffc84ab4a3e.png[/img]
可以看到,目录服务上下文 context 与命名服务上下文结构关系相似,只是目录服务上下文的中的对象多了属性。如在 a 上下文中,dog 对象有 name、color 和category属性,在 b 上下文中,cat 对象也有相同的属性。
也可以用map来帮助理解JNDI 的context。一个map 就相当于一个 JNDI context, map 的一个 entry 就是 JNDI context 的一个 binding,而 entry 的 key 相当于 binding 的 name,entry 的value 相当于 binding 的 object。当然这种理解不是非常准确的,因为 JNDI 的 context 还有目录层次的结构,但是可以用子 map 的方式来理解整个 JNDI 的 context,如下图所示:
[img]http://dl.iteye.com/upload/attachment/439216/fe19939f-e159-38c6-b090-16e92591e95c.png[/img]
在上图中,如果当前 context 为 rootMap,那么 context.lookup(“/mysqlDS”) 将得到 mysql 的连接池对象 DataSource。因为name “/mysqlDS”是跟 DataSource 绑定的;如果用context.lookup(“/jmxServerUrl”) ,将得到 JMX 服务器的连接 URL 对象 JMX_URL;同理,context.lookup(“/name_n+1/sub_name_1”) 将得到对象 sub_object1。
如果当前context为 subMap1,那么用context.lookup(“mysqlDS”) 就可以得到DataSource,而不用在 “mysqlDS” 中加上 “/”。同时,context.getSubContext(“name_n+1”) 可以得到 subMap2 的 context subcontext,并且 subcontext.lookup(“sub_name_1”) 就可得到对象 sub_object1。
在上图中,上下文结构及对象信息是维护在命名目录服务器中的, JNDI 只是操作接口。
[b]JNDI 架构[/b]
JNDI 的实现原理其实跟 JDBC 的实现原理是一样的。在 JDBC 中,Java 定义了一套标准的、独立于数据库系统的 API。JDBC 开发者只管使用 JDBC 的 API,并加载(安装)相应数据库的驱动即可,完全不用理会该数据库驱动是怎么跟它的数据库交互的。如 JDBC 的设计架构如下图所示:
[img]http://dl.iteye.com/upload/attachment/439210/202b9ec2-4e52-342d-8e3b-8e1206abab3e.png[/img]
在上图中,Java 应用程序 Application1 和 Application2 只与 JDBC API 交互,而不用管数据库的 JDBC驱动是怎么跟数据库交互的(JAVA 又是这样:向Java应用程序开发人员开放 JDBC接口,复杂的实现细节由相应的 JDBC 驱动完成)。如果系统用的是 MySQL 数据库,那么就得加载 MySQL JDBC 驱动,如果是 Oracle 数据库,那么就得提供 Oracle 的 JDBC 驱动,驱动管理者(DriverManager)会自动帮Java 应用程序 Application1 和 Application2 选择对应的 JDBC 驱动与数据库进行交互。
JNDI 的实现架构跟 JDBC 的实现架构相似,如下图所示:
[img]http://dl.iteye.com/upload/attachment/439212/ac982638-f5ea-3c2f-b720-c1a724d3e5d6.png[/img]
JNDI API 对应于 JDBC API
Naming Manager 对应于 Driver Manager
LDAP、DNS、RMI对应于 MySQL、Oracle 的 JDBC 驱动
Java Application 只与 JNDI API 交互,而不管具体的命名目录服务系统是 LDAP、DNS 还是 RMI。如果系统与 LDAP 命名目录服务系统交互,那么必须加载(安装)LDAP 驱动,如果与 DNS 命名服务系统交互,那么必须加载(安装)DNS 驱动。
[b]JNDI SPI[/b]
在 JNDI 中,有一个概念叫做 SPI(Server Provider Interface),服务提供者接口,是 JNDI 定义的标准接口。服务提供者根据命名命令服务系统的不同,需对接口做相应的实现,并使用该实现与命名命令服务系统交互。如上图中,LDAP、DNS、RMI 就是服务提供者,它们都实现了标准的 LDAP、DNS、RMI 访问协议,使用这些协议,可以与 LDAP 系统、DNS 系统和RMI 服务器交互。
如,MySQL 和 Oracle 的 JDBC 驱动就相当于 JNDI 中的服务提供者。
Java 从 1.3 版本开始就包含了 LDAP、CORBA和 RMI 的驱动实现(服务提供者),所以访问者三种类型的命名目录服务的话不用再另外加载(安装)驱动,但如果不是,就需加载相应的驱动,就像Java 访问 MySQL 数据库需加载 MySQL 的 JDBC 驱动一样。例如通过 JNDI 访问本地文件系统的话,需加载相应的访问文件系统的驱动,sun 有一个这样的驱动实现:com.sun.jndi.fscontext.RefFSContextFactory。
[b]Tomcat 的 JNDI SPI 实现[/b]
在上面的 JNDI 架构中,命名目录服务系统有可能部署在远程主机中,JNDI 使用相应的驱动与命名目录服务系统进行远程通信。如,LDAP 的驱动将与远程 LDAP 服务器进行通信,命名和目录信息存储于远程命名目录服务系统中。
但是在Tomcat 中稍有不同。Tomcat也有相应的 JNDI SPI 实现,但是该 SPI 实现(LDAP驱动)并不访问远程命名目录服务,它只是在内存中实现了一个虚拟命名目录服务,用于存储命名和对象信息并维护相应的绑定关系。Tomcat 中的数据源就对象就是存储于 Tomcat 实现的本地命名目录服务中的。
关于 Tomcat 的 JNDI SPI 实现,可以通过这两个类进行跟踪:
org.apache.naming.java.javaURLContextFactory
org.apache.naming.NamingContext
下面用两张图说明 LDAP 驱动与 Tomcat 驱动(SPI 实现)访问过程的不同:
LDAP 访问过程:
[img]http://dl.iteye.com/upload/attachment/439214/e4ea39da-ed0e-35be-ac2e-c23e911cf319.png[/img]
TOMCAT 访问过程图:
[img]http://dl.iteye.com/upload/attachment/439220/df4dd9d1-8d0f-37e4-b064-638c60c493a8.png[/img]
由上图可以看到,Java App 通过 JNDI 访问 LDAP 服务,LDAP 驱动要跟 LDAP服务进行远程通信,即LDAP 服务是在 Java App 的 JVM 之外的(Java App 与 LDAP 驱动在同一 JVM 内)。
但是 Tomcat 的 JNDI SPI 驱动实现不同。由上图可以看出,Servlet 通过 JNDI 接口访问 Tomcat 的虚拟命名目录服务时,Tomcat 的 JNDI SPI 实现直接访问其内存虚拟命名目录服务,而不需要跨越 JVM,即在 Tomcat 中,Servlet、Tomcat JNDI SPI 驱动以及 Tomcat 的内存虚拟命名目录都在同一 JVM 。
Tomcat 的 JNDI SPI 实现规定,命名空间必须以 java: 开头,如,我们通常在 Tomcat 中这样查找数据源(如 Servlet):
Context ctx=new InitialContext();
Object o = ctx.lookup("java:comp/env/mysqlDataSource");
DataSource ds=(DataSource)o;
这里,我们在初始化 context 的时候,不用指定加载的实现驱动,是因为 Tomcat 在启动的时候已经指定了其 SPI 实现到系统属性中:
见:org.apache.catalina.startup.Embedded
System.setProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY,
"org.apache.naming.java.javaURLContextFactory");
当初始化 context 不指定加载驱动时,JVM 自动从指定的系统属性(javax.naming.Context.INITIAL_CONTEXT_FACTORY)中查找该驱动。
所以,我们对 mysqlDataSource 的查找请求转到 Tomcat 的 JNDI SPI 实现中,JNDI SPI 实现
根据名称从其内存的虚拟命名目录服务查找 mysql 的数据源对象,并返回给应用程序。
[b]JNDI 简单使用[/b]
JNDI 的使用相对简单,JNDI 开发人员只需初始化一个 context 实例(可以把该实例看成是命名目录服务系统的一个镜像),并通过该 context 实例绑定对象、查找对象以及解除对象。
在初始化context 实例时,需根据目标命名目录服务系统提供相应的驱动类供 JNDI 加载使用:
Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.fscontext.RefFSContextFactory");
Context ctx = new InitialContext(env);
在上例中,要访问的目标命名目录服务系统是本地文件系统,所以需提供 JNDI 的文件系统驱动。如果是访问 LDAP 系统,那么 context 的初始化应该提供 LDAP 驱动类:
Hashtable env = new Hashtable(11);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL,"ldap://localhost:389/o=JNDITutorial");
DirContext ctx = new InitialDirContext(env);
常用的 JNDI 操作有
查找对象:lookup(String name)
绑定对象:bind(String name, Object obj)
解除对象:unbind(String name)
还有重命名、重绑定、创建子 context 等。
关于 JNDI 的更多细节,请参阅相应书籍。