JNDI的原理和应用
JNDI是Java Naming and Directory Interface(JAVA命名和目录接口)的英文简写,它是为JAVA应用程序提供命名和目录访问服务的API(Application Programing Interface,应用程序编程接口)。
1.命名的概念与应用
JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。读者也许会感到奇怪:自己创建一个Java对象,将其绑定到JNDI容器环境中后又查询出来,这有什么意思?在真实的项目应用中,通常是由系统程序或框加程序先将资源对象绑定到JNDI环境中,以后在该系统或框架中运行的模块程序就可以从JNDI环境中查找这些资源对象了。例如,Tomcat服务器在启动时可以创建一个连接到某种数据库系统的数据源(DataSource)对象,并将该数据源(DataSource)对象绑定到JNDI环境中,以后在这个Tomcat服务器中运行的Servlet和JSP程序就可以从JNDI环境中查询出这个数据源(DataSource)对象进行使用,而不用关心数据源(DataSource)对象是如何创建出来的,这种方式极大地增强了系统的可维护性,当数据库系统的连接参数发生变更时,这只是Tomcat系统管理员一个人要关心的事情,而与所有的应用程序开发人员无关。
容器环境(Context)本身也是一个Java对象,它也可以通过一个名称绑定到另一个容器环境(Context)中。将一个Context对象绑定到另外一个Context对象中,这就形成了一种父子级联关系,多个Context对象最终可以级联成一种树状结构,树中的每个Context对象中都可以绑定若干个Java对象,如图6.10所示。
图6.10
图6.10中的每个方框分别代表一个Context对象,它们绑定的名称分别为a、b、c、d、e,b和c是a的子Context,d是b的子Context,e又是d的子Context。图9.x中的各个方框内的每个小椭圆分别代表一个Java对象,它们也都有一个绑定的名称,这些绑定名称分别为dog、pig、sheet等,在同一个Context不能绑定两个相同名称的Java对象,在不同的Context中可以出现同名的绑定对象。可见,Context树的级联结构与文件系统中的目录结构非常类似,Context与其中绑定的Java对象的关系也非常类似于文件系统中的目录与文件的关系。从图6.10中可以看到,要想得到Context树中的一个Java对象,首先要得到其所在的Context对象,只要得到了一个Context对象,就可以调用它的查询(lookup)方法来获得其中绑定的Java对象。另外,调用某个Context对象的lookup方法也可以获得Context树中的任意一个Context对象,这只需要在lookup方法中指定相应的Context路径即可。在JNDI中不存在着“根”Context的概念,也就是说,执行JNDI操作不是从一个“根”Context对象开始,而是可以从Context树中的任意一个Context开始。无论如何,程序必须获得一个作为操作入口的Context对象后才能执行各种JNDI命名操作,为此,JNDI API中提供了一个InitialContext类来创建用作JNDI命名操作的入口Context对象。Context是一个接口,Context对象实际上是Context的某个实现类的实例对象,选择这个具体的Context实现类并创建其实例对象的过程是由一个Context工厂类来完成的,这个工厂类的类名可以通过JNDI的环境属性java.naming.factory.initial指定,也可以根据Context的操作方法的url参数的Schema来选择。
2.目录的概念与应用
JNDI中的目录(Directory)与文件系统中的目录概念有很大的不同,JNDI中的目录(Directory)是指将一个对象的所有属性信息保存到一个容器环境中。JNDI的目录(Directory)原理与JNDI的命名(Naming)原理非常相似,主要的区别在于目录容器环境中保存的是对象的属性信息,而不是对象本身,所以,目录提供的是对属性的各种操作。事实上,JNDI的目录(Directory)与命名(Naming)往往是结合在一起使用的,JNDI API中提供的代表目录容器环境的类为DirContext,DirContext是Context的子类,显然它除了能完成目录相关的操作外,也能完成所有的命名(Naming)操作。DirContext是对Context的扩展,它在Context的基础上增加了对目录属性的操作功能,可以在其中绑定对象的属性信息和查找对象的属性信息。JNDI中的目录(Directory)的结构示意图如图6.11所示。
图6.11
图6.11中的每个最外层的方框分别代表一个DirContext对象,它们绑定的名称分别为a、b,b是a的子DirContext。图6.11中的各个最外层的方框内的每个小椭圆分别代表一个Java对象,各个里层的方框分别代表一个对象的属性。从名称为a的DirContext中的内容可以看到,一个DirContext容器环境中即可以绑定对象自身,也可以绑定对象的属性信息,绑定的对象和绑定的属性是完全独立的两个事物,即使它们的绑定名称相同,它们的操作也是完全独立的。另外,一个属性可以有多个属性值,例如,dog对象的category属性就设置了两个属性值:meat和pet。从名称为b的DirContext中的内容可以看到,一个DirContext容器环境中也可以只绑定对象的属性信息,而不绑定任何对象自身。与Context的操作原理类似,JNDI API中提供了一个InitialDirContext类来创建用作JNDI命名与目录属性操作的入口DirContext对象。
3. 用于DNS查询的JNDI服务程序
JNDI API是面向应用程序开发人员的编程接口,它在运行时需要调用某个具体的JNDI服务程序,JNDI API与JNDI服务程序之间的关系,犹如JDBC与JDBC驱动程序之间的关系。从JDK 1.3开始,JDK中就集成了JNDI API,从JDK 1.4开始的版本又集成了用于DNS查询的JNDI服务程序,所以,如果我们使用JDK 1.4及更高的JDK版本来开发DNS信息查询程序时,不需要下载和安装JNDI API和用于DNS查询的JNDI服务程序。SUN公司提供的用于查询DNS信息的JNDI服务程序,将某个域名的DNS信息以属性的形式绑定到代表该域名的DirContext对象上。打开JDK帮助文档的首页,在其中搜索“jndi”关键字,可以看到一条“jndi”的超链接,单击这个超链接,就可以进入“Java Naming and Directory Interface”的帮助页面,如图6.12所示。
图6.12
单击图6.12中的“The DNS Service Provider”超链接,进入DNS服务程序的帮助页面。只要我们具备JDNI编程的一些基本知识,再加上该帮助文档页提供的信息,我们就知道如何调用这个DNS服务程序来获得某个域的DNS信息和MX记录了。
下面编写一个试验性的JNDI程序,这个程序用于帮助我们熟悉和掌握JNDI API的使用,也帮助我们了解DNS的JNDI服务程序以怎样的形式返回DNS信息。
:动手实践:使用JNDI API获取DNS信息
()按例程6-5编写一个名为DNSQuery.java的程序,这个程序使用JNDI API来获得某个域的DNS信息,并从中提取出域的一台SMTP服务器的名称,其中的很多代码都是为了帮助我们熟悉JNDI API的使用和了解DNS的JNDI服务程序返回的DNS信息内容而加入的。运行这个程序时,需要指定一个或两个参数,第一个参数是必须的,为要查询的域名,第二个参数是可选的,为查询时所使用的DNS服务器的IP地址,如果没有指定第二个参数,DNS的JNDI服务程序将使用底层操作系统上设置的DNS服务器。
例程6-5 DNSQuery.java
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
public class DNSQuery
{
public static void main(String[] args) throws NamingException
{
/*第一个参数指定要查询的域或主机名,第二个参数指定查询的DNS服务器,
为了程序的简单易读性,省略了严格的参数错误检查*/
String domain = args[0];
String dnsServer = args.length<2 ? "" : ("//" + args[1]);
//通过环境属性来指定Context的工厂类
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns:" + dnsServer);
DirContext ctx = new InitialDirContext(env);
//分别获取包含所有属性和只包含Mx属性的Attributes对象
Attributes attrsAll = ctx.getAttributes(domain);
Attributes attrsMx = ctx.getAttributes(domain, new String[]{"MX"});
/*上面的整段程序代码也可以用下面这段程序代码来替代,下面这段程序
代码通过查询URL中的Schema信息来自动选择Context的工厂类*/
/*
DirContext ctx = new InitialDirContext();
Attributes attrsAll = ctx.getAttributes("dns:" + dnsServer + "/" + domain);
Attributes attrsMx = ctx.getAttributes(
"dns:" + dnsServer + "/" + domain, new String[]{"MX"});
*/
System.out.println("打印出域" + domain +
"的Attributes对象中的信息:");
System.out.println(attrsAll);
System.out.println("--------------------------");
System.out.println("打印只检索域" + domain +
"的MX记录的Attributes对象:");
System.out.println(attrsMx);
System.out.println("--------------------------");
System.out.println("逐一打印出Attributes对象中的各个属性:");
NamingEnumeration attributes = attrsAll.getAll();
while(attributes.hasMore())
{
System.out.println(attributes.next());
}
System.out.println("--------------------------");
//直接调用get方法从attrsMx集合检索MX属性
System.out.println("直接检索Attributes对象中的MX属性:");
Attribute attrMx = attrsAll.get("MX");
System.out.println(attrMx);
System.out.println("--------------------------");
//获取Mx属性中的第一个值:
System.out.println("获取Mx属性中的第一个值:");
String recordMx = (String)attrMx.get();
System.out.println(recordMx);
//从Mx属性的第一个值中提取邮件服务器地址
System.out.println("从MX属性值中提取的邮件服务器地址:");
String smtpServer = recordMx.substring(
recordMx.indexOf(" ") + 1);
System.out.println(smtpServer);
}
(2)在Windows命令行窗口中编译DNSQuery.java程序后,接着在命令行窗口中执行如下命令:
ipconfig /all
如果ipconfig命令显示的结果中包含有DNS Server的信息,那么我们接着就可以使用如下命令来启动执行DNSQuery类:
java DNSQuery sina.com
上面的命令的运行结果如图6.13。
图6.13
()假设在上一步用ipconfig命令查看到的本地计算机上配置的DNS Server为202.106.46.151,那么,我们接着执行如下命令:
java DNSQuery sina.com 202.106.46.151
这个命令执行完后,也能显示出图6.13中的信息。我们接着故意将上面命令中的DNS服务器参数指定为一个错误的IP地址进行执行,修改后的命令语句如下所示:
java DNSQuery sina.com 192.168.1.151
这个命令执行完后的结果如图6.14所示:
图6.14
如果计算机只能通过代理服务器连接到Internet,那么在该计算机上直接执行如下命令:
java DNSQuery sina.com
这也将导致图6.14中的错误。如果要想在通过代理服务器上网的情况下,正确执行上面的程序,可以采用如下命令:
java -DsocksProxyHost=162.105.1.200 -DsocksProxyPort=808
DNSQuery sina.com 202.106.46.151
由于上面的命令太长,在排版时分成了两行来书写,读者在输入上面这条命令时,不要手工换行。读者应该根据自己的实际情况,修改其中的代理服务器地址、代理端口号和DNS服务器的地址。
那么,JNDI到底起什么作用?
要了解JNDI的作用,我们可以从“如果不用JNDI我们怎样做?用了JNDI后我们又将怎样做?”这个问题来探讨。
没有JNDI的做法:
程序员开发时,知道要开发访问MySQL数据库的应用,于是将一个对 MySQL JDBC 驱动程序类的引用进行了编码,并通过使用适当的 JDBC URL 连接到数据库。
就像以下代码这样:
Connection conn=null; try { Class.forName("com.mysql.jdbc.Driver", true, Thread.currentThread().getContextClassLoader()); conn=DriverManager. getConnection("jdbc:mysql://MyDBServer?user=qingfeng&password=mingyue"); ...... conn.close(); } catch(Exception e) { e.printStackTrace(); } finally { if(conn!=null) { try { conn.close(); } catch(SQLException e) {} } }
这是传统的做法,也是以前非Java程序员(如Delphi、VB等)常见的做法。这种做法一般在小规模的开发过程中不会产生问题,只要程序员熟悉Java语言、了解JDBC技术和MySQL,可以很快开发出相应的应用程序。
没有JNDI的做法存在的问题:
1、数据库服务器名称MyDBServer 、用户名和口令都可能需要改变,由此引发JDBC URL需要修改;
2、数据库可能改用别的产品,如改用DB2或者Oracle,引发JDBC驱动程序包和类名需要修改;
3、随着实际使用终端的增加,原配置的连接池参数可能需要调整;
4、......
解决办法:
程序员应该不需要关心“具体的数据库后台是什么?JDBC驱动程序是什么?JDBC URL格式是什么?访问数据库的用户名和口令是什么?”等等这些问题,程序员编写的程序应该没有对 JDBC 驱动程序的引用,没有服务器名称,没有用户名称或口令 —— 甚至没有数据库池或连接管理。而是把这些问题交给J2EE容器来配置和管理,程序员只需要对这些配置和管理进行引用即可。
由此,就有了JNDI。
//看的出来,是为了一个最最核心的问题:是为了解耦,是为了开发出更加可维护、可扩展的系统
用了JNDI之后的做法:
首先,在在J2EE容器中配置JNDI参数,定义一个数据源,也就是JDBC引用参数,给这个数据源设置一个名称;然后,在程序中,通过数据源名称引用数据源从而访问后台数据库。
//红色的字可以看出,JNDI是由j2ee容器提供的功能
具体操作如下:
1、配置数据源
以Tomcat服务器为例子,假设你的应用名为MyApp,则在MyWeb/META-INF/context.xml中配置数据源。
修改 mysql-ds.xml 文件的内容,使之能通过JDBC正确访问你的MySQL数据库,如下:
<?xml version="1.0" encoding="UTF-8"?> <Context antiJARLocking="true" path="/MyWeb"> <Resource name="jndi/mybatis" auth="Container" type="javax.sql.DataSource" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/tlshop" username="root" password="root" maxActive="20" maxIdle="10" maxWait="10000"/> </Context>
这里,定义了一个名为MySqlDS的数据源,其参数包括JDBC的URL,驱动类名,用户名及密码等。
2、在程序中引用数据源:
Connection conn=null; try { Context ctx=new InitialContext(); Object datasourceRef=ctx.lookup("java:comp/env/jndi/mybatis"); //引用数据源 DataSource ds=(Datasource)datasourceRef; conn=ds.getConnection(); ...... c.close(); } catch(Exception e) { e.printStackTrace(); } finally { if(conn!=null) { try { conn.close(); } catch(SQLException e) { } } }
直接使用JDBC或者通过JNDI引用数据源的编程代码量相差无几,但是现在的程序可以不用关心具体JDBC参数了。//解藕了,可扩展了
在系统部署后,如果数据库的相关参数变更,只需要重新配置 mysql-ds.xml 修改其中的JDBC参数,只要保证数据源的名称不变,那么程序源代码就无需修改。
由此可见,JNDI避免了程序与数据库之间的紧耦合,使应用更加易于配置、易于部署。
JNDI的扩展:
JNDI在满足了数据源配置的要求的基础上,还进一步扩充了作用:所有与系统外部的资源的引用,都可以通过JNDI定义和引用。
//注意什么叫资源
所以,在J2EE规范中,J2EE 中的资源并不局限于 JDBC 数据源。引用的类型有很多,其中包括资源引用(已经讨论过)、环境实体和 EJB 引用。特别是 EJB 引用,它暴露了 JNDI 在 J2EE 中的另外一项关键角色:查找其他应用程序组件。
EJB 的 JNDI 引用非常类似于 JDBC 资源的引用。在服务趋于转换的环境中,这是一种很有效的方法。可以对应用程序架构中所得到的所有组件进行这类配置管理,从 EJB 组件到 JMS 队列和主题,再到简单配置字符串或其他对象,这可以降低随时间的推移服务变更所产生的维护成本,同时还可以简化部署,减少集成工作。外部资源”。
总结:
J2EE 规范要求所有 J2EE 容器都要提供 JNDI 规范的实现。//sun 果然喜欢制定规范JNDI 在 J2EE 中的角色就是“交换机” —— J2EE 组件在运行时间接地查找其他组件、资源或服务的通用机制。在多数情况下,提供 JNDI 供应者的容器可以充当有限的数据存储,这样管理员就可以设置应用程序的执行属性,并让其他应用程序引用这些属性(Java 管理扩展(Java Management Extensions,JMX)也可以用作这个目的)。JNDI 在 J2EE 应用程序中的主要角色就是提供间接层,这样组件就可以发现所需要的资源,而不用了解这些间接性。
在 J2EE 中,JNDI 是把 J2EE 应用程序合在一起的粘合剂,JNDI 提供的间接寻址允许跨企业交付可伸缩的、功能强大且很灵活的应用程序。这是 J2EE 的承诺,而且经过一些计划和预先考虑,这个承诺是完全可以实现的。
从上面的文章中可以看出:
1、JNDI 提出的目的是为了解藕,是为了开发更加容易维护,容易扩展,容易部署的应用。
2、JNDI 是一个sun提出的一个规范(类似于jdbc),具体的实现是各个j2ee容器提供商,sun 只是要求,j2ee容器必须有JNDI这样的功能。
3、JNDI 在j2ee系统中的角色是“交换机”,是J2EE组件在运行时间接地查找其他组件、资源或服务的通用机制。
4、JNDI 是通过资源的名字来查找的,资源的名字在整个j2ee应用中(j2ee容器中)是唯一的。