设为首页 收藏本站
查看: 502|回复: 0

[经验分享] tomcat架构分析 (JNDI体系绑定)

[复制链接]

尚未签到

发表于 2017-1-29 09:14:07 | 显示全部楼层 |阅读模式
出处:http://gearever.iteye.com
在tomcat架构分析 (JNDI配置)一文里,以配置JDBC数据库连接为例,介绍了tomcat中常用的JNDI配置的几种用法。使用这种配置,在app里可以通过JNDI API非常简单的调用相应的资源对象。但是调用越简单,那其背后封装的逻辑越多。就好比汽车分为手动档自动挡一样。对司机而言,自动挡开起来会轻松很多,那是因为很多复杂的操作,已经封装起来由机器来完成了。
本篇就是从代码原理角度来揭示tomcat中JNDI的配置是如何生效的,以及app中的调用逻辑是如何实现的。通过这些,可以看到tomcat中一块比较重要的体系结构,同时加深对JNDI的理解。
上文介绍了两种配置方案,一个是global的配置,在各个app中引用;一个是各个app自己配置资源对象。这两种方案,从实现角度来看,原理一样,只是第一种比第二种多了一层mapping关系。所以为了方便理解,先从第二种方案,即各个app配置自己的资源对象来说明。
另外,需要说明的是,本章涉及的代码

  • Tomcat源码
  • JNDI源码(javax.naming.*),参考OpenJDK项目

先看一个概念图
DSC0000.jpg
JNDI体系分为三个部分;

  • 在tomcat架构分析 (容器类)中介绍了StandardContext类,它是每个app的一个逻辑封装。当tomcat初始化时,将根据配置文件,对StandardContext中的NamingResources对象进行赋值,同时,将实例化一个NamingContextListener对象作为这个context作用域内的事件监听器,它会响应一些例如系统启动,系统关闭等事件,作出相应的操作;
  • 初始化完成后,tomcat启动,完成启动逻辑,抛出一个系统启动event,由那个NamingContextListener捕获,进行处理,将初始化时的NamingResources对象中的数据,绑定到相应的JNDI对象树(namingContext)上,即java:comp/env分支,然后将这个根namingContext与这个app的classloader进行绑定,这样每个app只有在自己的JNDI对象树上调用,互不影响;
  • 每个app中的类都由自己app的classloader加载,如果需要用到JNDI绑定对象,也是从自己classloader对应的JNDI对象树上获取资源对象

这里需要说明的是,在后面会经常涉及到两类context,一个是作为tomcat内部实现逻辑的容器StandardContext;一个是作为JNDI内部分支对象NamingContext;它们实现不同接口,互相没有任何关系,不要混淆。
开始看看每个部分详细情况吧。
初始化NamingResources
先看看配置;
<tomcat>/conf/server.xml

<Server port="8005">
<Service>
<Engine>
<Host>
<Context>
<Resource
name="jdbc/mysql"
type="javax.sql.DataSource"
username="root"
password="root"
driverClassName="com.mysql.jdbc.Driver"
maxIdle="200"
maxWait="5000"
url="……"
maxActive="100"/>
</Context>
</Host>
</Engine>
</Service>
……
</Server>

通过这个配置,可以非常清楚的看出tomcat内部的层次结构,不同的层次实现不同的作用域,同时每个层次都有相应的类进行逻辑封装,这是tomcat面向对象思想的体现。那么相应的,Context节点下的Resource节点也有类进行封装;
org.apache.catalina.deploy.ContextResource
    上面例子中Resource节点配置的所有属性会以键值对的方式存入ContextResource的一个HashMap对象中,这一步只是初始化,不会用到每个属性,它只是为了每个真正处理的资源对象用到,例如后面会说的缺省的tomcat的数据库连接池对象BasicDataSourceFactory,如果用其他的数据库连接池,例如c3p0,那么其配置的属性对象就应该按照c3p0中需要的属性名称来配。
    但是,这些属性中的name和type是ContextResource需要的,name是JNDI对象树的分支节点,上面配的“jdbc/mysql”,那么这个数据库连接池对象就对应在“java:comp/env/jdbc/mysql”的位置。type是这个对象的类型,如果是“javax.sql.DataSource”,tomcat会有一些特殊的逻辑处理。
    当tomcat初始化时,StandardContext对象内部会生成一个NamingResources对象,这个对象就是做一些预处理,存储一些Resource对象,看一下NamingResources存储Resource对象的逻辑;

public void addResource(ContextResource resource) {
//确保每一个资源对象的name都是唯一的
//不仅是Resource对象之间,包括Service等所有的资源对象
if (entries.containsKey(resource.getName())) {
return;
} else {
entries.put(resource.getName(), resource.getType());
}
//建立一个name和资源对象的mapping
synchronized (resources) {
resource.setNamingResources(this);
resources.put(resource.getName(), resource);
}
support.firePropertyChange("resource", null, resource);
}

需要说明的是,不仅仅是Resource一种对象,还有Web Service资源对象,EJB对象等,这里就是拿数据库连接的Resource对象举例。
启动JNDI绑定
当tomcat启动时,会抛出一个start event,由StandardContext的NamingContextListener监听对象捕捉到,响应start event。

public void lifecycleEvent(LifecycleEvent event) {
container = event.getLifecycle();
if (container instanceof Context) {
//这个namingResources对象就是StandardContext的namingResources对象
namingResources = ((Context) container).getNamingResources();
logger = log;
} else if (container instanceof Server) {
namingResources = ((Server) container).getGlobalNamingResources();
} else {
return;
}
//响应start event
if (event.getType() == Lifecycle.START_EVENT) {
if (initialized)
return;
Hashtable contextEnv = new Hashtable();
try {
//生成这个StandardContext域的JNDI对象树根NamingContext对象
namingContext = new NamingContext(contextEnv, getName());
} catch (NamingException e) {
// Never happens
}
ContextAccessController.setSecurityToken(getName(), container);
//将此StandardContext对象与JNDI对象树根NamingContext对象绑定
ContextBindings.bindContext(container, namingContext, container);
if( log.isDebugEnabled() ) {
log.debug("Bound " + container );
}
// Setting the context in read/write mode
ContextAccessController.setWritable(getName(), container);
try {
//将初始化时的资源对象绑定JNDI对象树
createNamingContext();
} catch (NamingException e) {
logger.error
(sm.getString("naming.namingContextCreationFailed", e));
}
// 针对Context下配置Resource对象而言
if (container instanceof Context) {
// Setting the context in read only mode
ContextAccessController.setReadOnly(getName());
try {
//通过此StandardContext对象获取到JNDI对象树根NamingContext对象
//同时将此app的classloader与此JNDI对象树根NamingContext对象绑定
ContextBindings.bindClassLoader
(container, container,
((Container) container).getLoader().getClassLoader());
} catch (NamingException e) {
logger.error(sm.getString("naming.bindFailed", e));
}
}
// 针对global资源而言,这里不用关注
if (container instanceof Server) {
namingResources.addPropertyChangeListener(this);
org.apache.naming.factory.ResourceLinkFactory.setGlobalContext
(namingContext);
try {
ContextBindings.bindClassLoader
(container, container,
this.getClass().getClassLoader());
} catch (NamingException e) {
logger.error(sm.getString("naming.bindFailed", e));
}
if (container instanceof StandardServer) {
((StandardServer) container).setGlobalNamingContext
(namingContext);
}
}
initialized = true;
}
//响应stop event
else if (event.getType() == Lifecycle.STOP_EVENT) {
......
}
}

注意上面方法中有两层绑定关系;
ContextBindings.bindContext()

public static void bindContext(Object name, Context context,
Object token) {
if (ContextAccessController.checkSecurityToken(name, token))
//先是将StandardContext对象与JNDI对象树根NamingContext对象绑定
//注意,这里第一个参数name是StandardContext对象
contextNameBindings.put(name, context);
}

ContextBindings.bindClassLoader()

public static void bindClassLoader(Object name, Object token,
ClassLoader classLoader)
throws NamingException {
if (ContextAccessController.checkSecurityToken(name, token)) {
//根据上面的StandardContext对象获取刚才绑定的NamingContext对象
Context context = (Context) contextNameBindings.get(name);
if (context == null)
throw new NamingException
(sm.getString("contextBindings.unknownContext", name));
//将classloader与NamingContext对象绑定
clBindings.put(classLoader, context);
clNameBindings.put(classLoader, name);
}
}

主要看一下将初始化时的资源对象绑定JNDI对象树的createNamingContext()方法;

private void createNamingContext()
throws NamingException {
// Creating the comp subcontext
if (container instanceof Server) {
compCtx = namingContext;
envCtx = namingContext;
} else {
//对于StandardContext而言,在JNDI对象树的根namingContext对象上
//建立comp树枝,以及在comp树枝上建立env树枝namingContext对象
compCtx = namingContext.createSubcontext("comp");
envCtx = compCtx.createSubcontext("env");
}
......
// 从初始化的NamingResources对象中获取Resource对象加载到JNDI对象树上
ContextResource[] resources = namingResources.findResources();
for (i = 0; i < resources.length; i++) {
addResource(resources);
}      
......
}

看一下addResource的具体加载逻辑;

public void addResource(ContextResource resource) {
// Create a reference to the resource.
Reference ref = new ResourceRef
(resource.getType(), resource.getDescription(),
resource.getScope(), resource.getAuth());
// 遍历Resource对象的各个属性,这些属性存在一个HashMap中
Iterator params = resource.listProperties();
while (params.hasNext()) {
String paramName = (String) params.next();
String paramValue = (String) resource.getProperty(paramName);
//封装成StringRefAddr,这些都是JNDI的标准API
StringRefAddr refAddr = new StringRefAddr(paramName, paramValue);
ref.add(refAddr);
}
try {
if (logger.isDebugEnabled()) {
logger.debug("  Adding resource ref "
+ resource.getName() + "  " + ref);
}
//在上面创建的comp/env树枝节点上,根据Resource配置的name继续创建新的节点
//例如配置的name=”jdbc/mysql”,则在comp/env树枝节点下再创建一个jdbc树枝节点
createSubcontexts(envCtx, resource.getName());
//绑定叶子节点,它不是namingContext对象,而是最后的Resource对象
envCtx.bind(resource.getName(), ref);
} catch (NamingException e) {
logger.error(sm.getString("naming.bindFailed", e));
}
//这就是上面说的对于配置type="javax.sql.DataSource"时的特殊逻辑
//将数据库连接池类型的资源对象注册到tomcat全局的JMX中,方便管理及调试
if ("javax.sql.DataSource".equals(ref.getClassName())) {
try {
ObjectName on = createObjectName(resource);
Object actualResource = envCtx.lookup(resource.getName());
Registry.getRegistry(null, null).registerComponent(actualResource, on, null);
objectNames.put(resource.getName(), on);
} catch (Exception e) {
logger.warn(sm.getString("naming.jmxRegistrationFailed", e));
}
}   
}

这就是上面配置的jdbc/mysql数据库连接池的JNDI对象树;
DSC0001.jpg
到目前为止,完成了JNDI对象树的绑定,可以看到,每个app对应的StandardContext对应一个JNDI对象树,并且每个app的各个classloader与此JNDI对象树分别绑定,那么各个app之间的JNDI可以不互相干扰,各自配置及调用。
需要注意的是,NamingContext对象就是JNDI对象树上的树枝节点,类似文件系统中的目录,各个Resource对象则是JNDI对象树上的叶子节点,类似文件系统的具体文件,通过NamingContext对象将整个JNDI对象树组织起来,每个Resource对象才是真正存储数据的地方。
本篇就描述tomcat内部是如何构造JNDI对象树的,如何通过JNDI获取对象,涉及到JNDI API内部运作了,将在另一篇中继续。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-334762-1-1.html 上篇帖子: cas与tomcat简单搭建SSO 下篇帖子: Tomcat的增加/查看jvm虚拟内存&设置JRE
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表