tomcat架构分析 (JNDI体系绑定)

来源:互联网 发布:火眼金睛答题软件使用 编辑:程序博客网 时间:2024/04/30 12:12
出处: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项目

先看一个概念图 


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

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

初始化NamingResources 
先看看配置; 
<tomcat>/conf/server.xml 
Xml代码  收藏代码
  1. <Server port="8005">    
  2. <Service>  
  3.    <Engine>  
  4.       <Host>  
  5.          <Context>  
  6.                 <Resource  
  7.                                     name="jdbc/mysql"  
  8.                                     type="javax.sql.DataSource"  
  9.                                     username="root"  
  10.                                     password="root"  
  11.                                     driverClassName="com.mysql.jdbc.Driver"  
  12.                                     maxIdle="200"  
  13.                                     maxWait="5000"  
  14.                                     url="……"  
  15.                                     maxActive="100"/>  
  16.   
  17.          </Context>  
  18.       </Host>  
  19.     </Engine>  
  20. </Service>  
  21. ……  
  22. </Server>  

通过这个配置,可以非常清楚的看出tomcat内部的层次结构,不同的层次实现不同的作用域,同时每个层次都有相应的类进行逻辑封装,这是tomcat面向对象思想的体现。那么相应的,Context节点下的Resource节点也有类进行封装; 
Java代码  收藏代码
  1. 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对象的逻辑; 
Java代码  收藏代码
  1. public void addResource(ContextResource resource) {  
  2.         //确保每一个资源对象的name都是唯一的  
  3.         //不仅是Resource对象之间,包括Service等所有的资源对象  
  4.         if (entries.containsKey(resource.getName())) {  
  5.             return;  
  6.         } else {  
  7.             entries.put(resource.getName(), resource.getType());  
  8.         }  
  9.         //建立一个name和资源对象的mapping  
  10.         synchronized (resources) {  
  11.             resource.setNamingResources(this);  
  12.             resources.put(resource.getName(), resource);  
  13.         }  
  14.         support.firePropertyChange("resource"null, resource);  
  15.     }  

需要说明的是,不仅仅是Resource一种对象,还有Web Service资源对象,EJB对象等,这里就是拿数据库连接的Resource对象举例。 

启动JNDI绑定 
当tomcat启动时,会抛出一个start event,由StandardContext的NamingContextListener监听对象捕捉到,响应start event。 
Java代码  收藏代码
  1. public void lifecycleEvent(LifecycleEvent event) {  
  2.   
  3.     container = event.getLifecycle();  
  4.   
  5.     if (container instanceof Context) {  
  6.         //这个namingResources对象就是StandardContext的namingResources对象  
  7.         namingResources = ((Context) container).getNamingResources();  
  8.         logger = log;  
  9.     } else if (container instanceof Server) {  
  10.         namingResources = ((Server) container).getGlobalNamingResources();  
  11.     } else {  
  12.         return;  
  13.     }  
  14.     //响应start event  
  15.     if (event.getType() == Lifecycle.START_EVENT) {  
  16.   
  17.         if (initialized)  
  18.             return;  
  19.         Hashtable contextEnv = new Hashtable();  
  20.         try {  
  21.             //生成这个StandardContext域的JNDI对象树根NamingContext对象  
  22.             namingContext = new NamingContext(contextEnv, getName());  
  23.         } catch (NamingException e) {  
  24.             // Never happens  
  25.         }  
  26.         ContextAccessController.setSecurityToken(getName(), container);  
  27.         //将此StandardContext对象与JNDI对象树根NamingContext对象绑定  
  28.         ContextBindings.bindContext(container, namingContext, container);  
  29.         if( log.isDebugEnabled() ) {  
  30.             log.debug("Bound " + container );  
  31.         }  
  32.         // Setting the context in read/write mode  
  33.         ContextAccessController.setWritable(getName(), container);  
  34.         try {  
  35.             //将初始化时的资源对象绑定JNDI对象树  
  36.             createNamingContext();  
  37.         } catch (NamingException e) {  
  38.             logger.error  
  39.                 (sm.getString("naming.namingContextCreationFailed", e));  
  40.         }  
  41.   
  42.         // 针对Context下配置Resource对象而言  
  43.         if (container instanceof Context) {  
  44.             // Setting the context in read only mode  
  45.             ContextAccessController.setReadOnly(getName());  
  46.             try {  
  47.                 //通过此StandardContext对象获取到JNDI对象树根NamingContext对象  
  48.                 //同时将此app的classloader与此JNDI对象树根NamingContext对象绑定  
  49.                 ContextBindings.bindClassLoader  
  50.                     (container, container,   
  51.                      ((Container) container).getLoader().getClassLoader());  
  52.             } catch (NamingException e) {  
  53.                 logger.error(sm.getString("naming.bindFailed", e));  
  54.             }  
  55.         }  
  56.         // 针对global资源而言,这里不用关注  
  57.         if (container instanceof Server) {  
  58.             namingResources.addPropertyChangeListener(this);  
  59.             org.apache.naming.factory.ResourceLinkFactory.setGlobalContext  
  60.                 (namingContext);  
  61.             try {  
  62.                 ContextBindings.bindClassLoader  
  63.                     (container, container,   
  64.                      this.getClass().getClassLoader());  
  65.             } catch (NamingException e) {  
  66.                 logger.error(sm.getString("naming.bindFailed", e));  
  67.             }  
  68.             if (container instanceof StandardServer) {  
  69.                 ((StandardServer) container).setGlobalNamingContext  
  70.                     (namingContext);  
  71.             }  
  72.         }  
  73.         initialized = true;  
  74.     }   
  75.     //响应stop event  
  76.     else if (event.getType() == Lifecycle.STOP_EVENT) {  
  77.       ......  
  78.     }  
  79. }  

注意上面方法中有两层绑定关系; 
ContextBindings.bindContext() 
Java代码  收藏代码
  1. public static void bindContext(Object name, Context context,   
  2.                                    Object token) {  
  3.         if (ContextAccessController.checkSecurityToken(name, token))  
  4.             //先是将StandardContext对象与JNDI对象树根NamingContext对象绑定  
  5.             //注意,这里第一个参数name是StandardContext对象  
  6.             contextNameBindings.put(name, context);  
  7.     }  

ContextBindings.bindClassLoader() 
Java代码  收藏代码
  1. public static void bindClassLoader(Object name, Object token,   
  2.                                        ClassLoader classLoader)   
  3.         throws NamingException {  
  4.         if (ContextAccessController.checkSecurityToken(name, token)) {  
  5.             //根据上面的StandardContext对象获取刚才绑定的NamingContext对象  
  6.             Context context = (Context) contextNameBindings.get(name);  
  7.             if (context == null)  
  8.                 throw new NamingException  
  9.                     (sm.getString("contextBindings.unknownContext", name));  
  10.             //将classloader与NamingContext对象绑定  
  11.             clBindings.put(classLoader, context);  
  12.             clNameBindings.put(classLoader, name);  
  13.         }  
  14.     }  

主要看一下将初始化时的资源对象绑定JNDI对象树的createNamingContext()方法; 
Java代码  收藏代码
  1. private void createNamingContext()  
  2.       throws NamingException {  
  3.   
  4.       // Creating the comp subcontext  
  5.       if (container instanceof Server) {  
  6.           compCtx = namingContext;  
  7.           envCtx = namingContext;  
  8.       } else {  
  9.           //对于StandardContext而言,在JNDI对象树的根namingContext对象上  
  10.           //建立comp树枝,以及在comp树枝上建立env树枝namingContext对象  
  11.           compCtx = namingContext.createSubcontext("comp");  
  12.           envCtx = compCtx.createSubcontext("env");  
  13.       }  
  14.       ......  
  15.       // 从初始化的NamingResources对象中获取Resource对象加载到JNDI对象树上  
  16.       ContextResource[] resources = namingResources.findResources();  
  17.       for (i = 0; i < resources.length; i++) {  
  18.           addResource(resources[i]);  
  19.       }        
  20.       ......  
  21.   }  

看一下addResource的具体加载逻辑; 
Java代码  收藏代码
  1. public void addResource(ContextResource resource) {  
  2.   
  3.     // Create a reference to the resource.  
  4.     Reference ref = new ResourceRef  
  5.         (resource.getType(), resource.getDescription(),  
  6.          resource.getScope(), resource.getAuth());  
  7.     // 遍历Resource对象的各个属性,这些属性存在一个HashMap中  
  8.     Iterator params = resource.listProperties();  
  9.     while (params.hasNext()) {  
  10.         String paramName = (String) params.next();  
  11.         String paramValue = (String) resource.getProperty(paramName);  
  12.         //封装成StringRefAddr,这些都是JNDI的标准API  
  13.         StringRefAddr refAddr = new StringRefAddr(paramName, paramValue);  
  14.         ref.add(refAddr);  
  15.     }  
  16.     try {  
  17.         if (logger.isDebugEnabled()) {  
  18.             logger.debug("  Adding resource ref "   
  19.                          + resource.getName() + "  " + ref);  
  20.         }  
  21.         //在上面创建的comp/env树枝节点上,根据Resource配置的name继续创建新的节点  
  22.         //例如配置的name=”jdbc/mysql”,则在comp/env树枝节点下再创建一个jdbc树枝节点  
  23.         createSubcontexts(envCtx, resource.getName());  
  24.         //绑定叶子节点,它不是namingContext对象,而是最后的Resource对象  
  25.         envCtx.bind(resource.getName(), ref);  
  26.     } catch (NamingException e) {  
  27.         logger.error(sm.getString("naming.bindFailed", e));  
  28.     }  
  29.     //这就是上面说的对于配置type="javax.sql.DataSource"时的特殊逻辑  
  30.     //将数据库连接池类型的资源对象注册到tomcat全局的JMX中,方便管理及调试  
  31.     if ("javax.sql.DataSource".equals(ref.getClassName())) {  
  32.         try {  
  33.             ObjectName on = createObjectName(resource);  
  34.             Object actualResource = envCtx.lookup(resource.getName());  
  35.             Registry.getRegistry(nullnull).registerComponent(actualResource, on, null);  
  36.             objectNames.put(resource.getName(), on);  
  37.         } catch (Exception e) {  
  38.             logger.warn(sm.getString("naming.jmxRegistrationFailed", e));  
  39.         }  
  40.     }      
  41. }  

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