JNDI和在Jetty中的运用
转载请注明出处 http://blog.csdn.net/lovingprince
第一部分 引子
我们应用想用统一的方式去查找我们想要的服务,通过格式化的名称例如: jdbc:comp/testa 去获取 jdbc 的服务, Ldap:comp/testa 去获取 ldap 服务,每种服务如何去查找我们并不关心,服务查找实现方式变更了,我应用程序也不需要改变。
看下要求:
1、 统一根据特定名称就可以获取到特定的服务
2、 应用程序不关心具体如何查找的方式
3、 服务查找实现方式变更应用程序不需要任何更改
我们可以如何去做来满足这种需求,来分析一下,
² 第一点需求:需要统一根据名称来获取服务,可以考虑统一的操作界面如:
² 第二点需求:应用不关心服务具体的查找方法,具体如何查找服务可以由服务提供方来实现
这点需求稍微麻烦点,首先需要根据用户提供的名称来确定使用的是什么类型的服务,然后再使用该类型的服务工厂提供一个查找该类服务方法,例如定义以下:
应用程序要使用只需要这样
² 第三点需求: 服务查找实现方式变更应用程序不需要任何更改,其实第二部就已经实现了部分,比如服务提供者把查找的实现方式都封装在一个用工厂创建的上下文中了,如果查找实现方式更改了,由于是硬编码,需要修改 InitContext ,也就需要应用修改 , 那么就需要将这个工厂实现想办法与 InitContext 剥离出来,如何剥离?
可以这样修改下 lookup:
这样,如果实现变了,或者有新的服务,完全不需要修改修改 InitContext 了,当然应用程序也不需要修改了,只需要一个 jar 包中包含了对应的 URLContextFactory 工厂实现就可以了。这样就完全剥离了服务提供者的服务查找实现与应用之间的耦合,可以支持各种不同服务的查找方式。
以上就是一个简单的查找服务的分析过程。可以简单总结为几个部分:
统一的界面的 API(Context)-> 服务类型路由工具 (NamingManager)-> 服务提供者具体查找实现 (jdbc URLContextFactory 和其实现了 Context 接口的具体路由方法 )
其中只有第三步底层需要服务提供者自己来实现,对于应用程序而言,则无需知道。
第二部分 JNDI
JNDI ( Java Naming and Directory Interface )框架其实已经解决了上面的问题,思路一致,不过他包含的东西却比上面要复杂得多,毕竟上面只是一个简单的演示过程。 JNDI 包含了命名服务和目录服务两部分,我这里以命名服务做解释。
JNDI 框架结构如下图:
基本上也是包含了统一的 JNDI API , NamingManger 中包含了一些路由到不同 URL 工具, JNDI SPI 则是服务提供者在提供实现时需要实现的接口。
JDK 中将这一系列 API 划分为了以下几个包
n javax.naming ,包含访问命名服务的类和接口定义
n javax.naming.directory ,包含访问目录服务的类和接口定义
n javax.naming.ldap ,为 ldapv3 提供的扩展操作提供支持
n javax.naming.event ,为访问命名和目录服务时的事件通知提供支持
n javax.naming.spi ,为服务提供商提供的接口
我们先关注下命名服务中的几个基本的概念:
名字:通过名字我们可以来查找关联的对象,格式可能根据系统不同,命名规范也会有所不同。
绑定:将名字和一个对象进行关联
上下文:它在其中存储了名字和对象绑定的集合,并且提供了统一绑定、取消绑定、查找、创建子上下文等常用操作,也就是说这里绑定的对象也可以是另外一个上下文,熟称子上下文。上下文存储结构可以理解成我们常用目录,目录中的文件就是我们的对象,当然目录中也可以再有目录。上下文用绿色表示,其他真实对象用橙色表示
上面的表示可以是如下的名字:
Java:comp/env
Java:TestDB
Java:jms/TestJms
基本使用方法:
这里以查找文件系统中文件为例:
是不是很简单 ? 在介绍上面几步操作内部的具体工作之前,再来熟悉几个 JNDI 中标准环境属性的含义:
- java.naming.factory.initial 创建上下文的工厂,如果没有找到支持对应 name 上下文环境,就用这个工厂创建 创建默认的 context
- java.naming.provider.url ,用来配置 context 的初始 url
- java.naming.factory.object 创建特定的对象的工厂
- java.naming.factory.state ,用来创建查询 jndi state 状态的工厂
- java.naming.factory.url.pkgs 包名列表,在指定的包名下查找创建特定 url 的上下文的工厂,如果没有,则使用
- java.naming.factory.initial 创建的上下文
其中,我们最初常用,也经常设置的标准属性其实只有两个, java.naming.factory.initial 和 java.naming.factory.url.pkgs ,包括 Tomcat 和 Jetty 容器对 JNDI 的支持也是只设置了这两个属性。
我们可以有很多不同方式来设置,这些属性值都被保存在上下文中,子上下文可以从父上下文中继承。
现在来看下内部工作:
- Context ctx = new InitialContext(env);
这个 是执行命名操作的初始上下文,我们所有的访问入口点都在这里。所有命名操作都相对于某一上下文。该初始上下文实现 Context 接口并提供解析名称的起始点。
说的比较抽象,简单点,就是说这个创建工作做了以下工作:
1、 合并JNDI 标准环境属性
2、 构造初始化默认上下文
Ø 合并JNDI 标准环境属性
JNDI 标准获取这些参数的方式:
1、 InitContext 时指定 HashTable
2、 System.getProperties() 系统属性中获取
3、 从 classpath 的 jndi.properties 文件中获取
由于可以从多个地方获取,如果多个地方都存在时,他们的合并规则分为三个大步骤:
一、从 System.getProperties() 中获取以下几个属性
如果 HashTable 中没有,就 put 进去,可以看出,这里是构造参数中的 hashtable 中的值优先。
二、依次搜索应用中 classpath 中的 jndi.properties 文件属性,再搜索 <java.home>/lib/jndi.properties 属性,这里 classpath 中可能有多个 jndi.properties 文件,他们中如果都出现了配置项,他们的合并规则是:
1 、如果是以下属性之一
则将属性值做串联起来,用冒号分隔,例如
java.naming.factory.object=com.sun.jndi.ldap.AttrsToCorba:com.wiz.from.Person
2 、其他属性, 只取第一个 首先遇到的属性值
三、将第二步得到的属性值合并到第一步的 table ,规则同第二步的合并规则一致。最后得到一份最终的上下文的属性配置。
最终
Context.OBJECT_FACTORIES,
Context.URL_PKG_PREFIXES,
Context.STATE_FACTORIES,
javax.naming.ldap.LdapContext.CONTROL_FACTORIES
可能会有多个值。
Ø 构造初始化默认上下文
如果 javax.naming.Context.INITIAL_CONTEXT_FACTORY 最终存在,这个工厂需要实现 javax.naming.spi .InitialContextFactor 接口,实现
getInitialContext (Hashtable <?,?> environment) |
方法,并且用这个初始化工厂来构建一个默认的上下文 Context.
- File obj = (File)ctx.lookup(name);
name 如果使用了 URL 形式的参数例如: file:/test 或者 java:comp/env 或者 ldap:/xxx ,则分析出 scheme( 这里是 file 或者 java 或者 ldap), 在 Context.URL_PKG_PREFIXES = "java.naming.factory.url.pkgs" 指定的包下查找
包名 + "." + scheme + "." + scheme + "URLContextFactory" 的工厂类,并且使用该工厂创建上下文。没有找到对应的工厂就使用 javax.naming.Context.INITIAL_CONTEXT_FACTORY 创建的 默认上下文。
然后用上面得到的上下文来查找这个 name 对应的服务。
举例:
ctx.lookup(“java:comp/env”);
在 jetty 中提供者在 jndi.properties 中有如下配置:
那么就会根据以上规则就会查找 org.eclipse.jetty.jndi.java.javaURLContextFactory 工厂类,如果存在那么使用他创建上下文,如果没有,就会直接用 org.eclipse.jetty.jndi.InitialContextFactory 创建默认上下文。
第三部分 Jetty 容器 中 JNDI 实现 (SPI)
Jetty 中对 JNDI 进行了支持,在 7.3.1 版本中,如果要使用 JNDI ,首先配置 start.ini 的 OPTIONS=Server,jsp,jmx,resources,websocket,ext,jndi ,此时就引入了 jetty JNDI 相关的 jar 包 . 在 jetty-jndi-7.3.1.v20110307.jar 中有一个 jndi.properties 文件,文件中的配置:
org.eclipse.jetty.jndi.java.javaURLContextFactory 工厂维护了 java: 这个上下文,所有的 java:comp 和 java: 的查找都会使用这个工厂创建的上下文。我们平常使用的 java:comp/env 私有环境也是在这里维护的,为什么叫 env 为私有环境,是因为每个应用都有自己的 env 环境,即使部署在同一个容器上也一样,互相不影响。
org.eclipse.jetty.jndi.InitialContextFactory 除了 java: 以外,其他的上下文都会使用初始化上下文来绑定和查找。
意思已经很明了,不再多做解释。
现在我们就可以在 jetty 中定义资源了 ,jetty 中资源可以分为三类: JVM 范围的资源、 server 范围的资源、 app 范围的资源。
- JVM 范围的资源
也就是说定义好后,所有的应用都可以使用
- Server 范围的资源
- App 范围的资源
三类资源在开始创建时都会在默认的上下文 ( org.eclipse.jetty.jndi.InitialContextFactory 创建的上下文中 ) 中先进行注册,注册名字为
其中 scope 就是对应的范围的类。
如果 scope 为 null ,则 注册名字是 myds
例:
App 范围的资源就会在默认的上下文中以下名字放置资源
org.eclipse.jetty.webapp.WebAppContext@20f237/myds
而 JVM 范围的名字资源会是
myds
Server 范围的资源名字:
org.eclipse.jetty.server@20f238/myds
也就说,如果你在应用中使用 ctx.loopup() 时,如果能够构造这些资源的名字,你同样可以访问资源,但是我们不建议这么做,因为这样不利于应用移植,我们应该使用我们的标准 ENC 环境,也就是 java:comp/env 私有资源。这就要求私有资源与全局资源进行一个映射,这个映射如何完成?
这就还需要 在 jetty 的 context.xml 中配置
org.eclipse.jetty.plus.webapp.EnvConfiguration 负责创建 java:comp/env 子环境,同时将 EnvEntry 定义的资源放置到 java:comp/env 中,也就是说 EnvEntry 不需要在 web.xml 中申明也可以使用。
org.eclipse.jetty.plus.webapp.PlusConfiguration 负责处理将创建处理器,处理 web.xml 等中的 env-entry 、 resource-ref 、 resource-env-ref 、 message-destination-ref 标签,只要在这些标签里面提到的资源名都会加到自己应用的 java:comp/env/ 上下文下,加进去后,就可以通过 ctx.loopup(“java:comp/env/test”) 等方式访问该资源了。也就说除了 env-entry 外,其他的 jetty 中的 Resource 定义的资源都需要在 web.xml 中进行申明,否则,是不会加入到 java:comp/env 子上下文中的,这点请注意。
最后再强调下, jetty 中定义的资源都需要在 web.xml 中进行一次申明,如此 jetty 才会把默认上下文中资源在 java:comp/env 下做一个映射。
YY 一下, Tomcat 7 中没有 jndi.properties 这个属性文件,他是在 org.apache.naming.NameService 这个 MBean 中做的初始化,并且默认值如下:
具体分析,各位可以自行去看看。
第四部分 参考
http://download.oracle.com/javase/jndi/tutorial/trailmap.html
http://docs.codehaus.org/display/JETTY/JNDI