Spring IOC 前世今生之 JDNI


Spring IOC 前世今生之 JDNI

提起 Spring,我們就想起 IOC(控制反轉),實現 IOC 有兩種技術:一是 DL(依賴查找 depency lookup),二是 DI(依賴注入 depency inject)。其實 Java 很早就有 DL 技術,本章讓我們走近 DL 的鼻祖 JNDI。

JNDI(Java Naming and Directory Interface,Java 命名和目錄接口)是一組在 Java 應用中訪問命名和目錄服務的API。簡單來說,就是根據 obj 名稱查找 obj,從而實現解耦。

JNDI 規范位於 javax.naming 包,屬於 JavaEE 規范,實現廠商有 Jboss、Tomcat 等。下面以 Tomcat 為例,對 JNDI 進行講解。

1. 基本用法

(1) maven 依賴

<!-- JNDI 規范實現 -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>9.0.19</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

(2)測試

// "java:comp/env/jdbc/mysql" 包含三層 Context,最后一層存儲元素 mysql
@Test
public void testSelectorContext() throws Exception {
    String jdbcPath = "jdbc/mysql";
    createSelectorContext();

    InitialContext ctx = new InitialContext();
    // 1. 綁定依賴
    createSubcontexts(ctx, "java:comp/env/" + jdbcPath);
    ctx.bind("java:comp/env/" + jdbcPath, new MysqlDataSource());

    // 2. 依賴查找
    Object ds = ctx.lookup("java:comp/env/" + jdbcPath);
    Assert.assertEquals(MysqlDataSource.class, ds.getClass());
}

// 創建 schema 為 “java:” 的命名空間
public void createSelectorContext() throws Exception {
    System.setProperty(NamingContext.URL_PKG_PREFIXES, "org.apache.naming");
    System.setProperty(NamingContext.INITIAL_CONTEXT_FACTORY, 
                       javaURLContextFactory.class.getName());
    NamingContext rootContext = createNamingContext();

    ContextBindings.bindContext(this, rootContext, null);
    ContextBindings.bindThread(this, null);
}

// 創建父容器 NamingContext
public NamingContext createNamingContext() throws Exception {
    NamingContext rootContext = new NamingContext(null, "rootContext");
    Context compCtx = rootContext.createSubcontext("comp");
    Context envCtx = compCtx.createSubcontext("env");
    return rootContext;
}

// 遞歸創建子容器 subContext
private void createSubcontexts(Context ctx, String name)
    throws NamingException {
    Context currentContext = ctx;
    StringTokenizer tokenizer = new StringTokenizer(name, "/");
    while (tokenizer.hasMoreTokens()) {
        String token = tokenizer.nextToken();
        if ((!token.equals("")) && (tokenizer.hasMoreTokens())) {
            try {
                currentContext = currentContext.createSubcontext(token);
            } catch (NamingException e) {
                // Silent catch. Probably an object is already bound in the context.
                currentContext = (Context) currentContext.lookup(token);
            }
        }
    }
}

說明: 可以看到,使用時還是非常簡單的。JNDI 提供了 InitialContext 作為入口,可以輕松的將 "java:comp/env/jdbc/mysql" 綁定(bind)到容器中,也可以很輕松的查找(lookup)。需要注意時,元素綁定到容器時,如果有多級(eg: jdbc/mysql),必須先創建子容器(jdbc)。

2. 功能說明

2.1 依賴查找

  • 單一資源查找

    Context 只支持根據名稱查找。

    DataSource ds = (DataSource) jdbcContext.lookup("mysql");
    
  • 集合資源查找

    不涉及。Context 只能根據 Name 查找,不能通過 Type 進行類型查找,也就是只能進行單一資源查找。

  • 層級資源查找

    DataSource ds = (DataSource) initialContext.lookup("java:comp/env/jdbc/mysql");
    

    依賴查找時,會依次從父容器往子容器查找,直到找到元素為至。

2.2 查找方法

Context 只支持根據名稱查找,不支持根據類型查找。

2.3 作用域

作用類似 Spring 中的 Scope,Context 也有單例、多例。

NamingContext#lookup 獲取對象時,會判斷 Reference 的屬性 singleton,如果為 true 則將 NamingEntry.type 屬性設置為 ENTRY,並將 obj 緩存越來。

  • REFERENCE:表示元素存儲的是對象的元信息,獲取元素時需要通過 REFERENCE 的屬性信息,查找時需要通過 REFERENCE 信息創建對象。
  • ENTRY:表示直接緩存元素對象,查找時直接返回,這樣下次就會獲取同一個對象。
if(entry.value instanceof ResourceRef) {
    boolean singleton = Boolean.parseBoolean(
        (String) ((ResourceRef) entry.value).get("singleton").getContent());
    if (singleton) {
        entry.type = NamingEntry.ENTRY;
        entry.value = obj;
    }
}

2.4 元信息 - Reference

作用類似 Spring 中的 BeanDefinition,Reference 定義了對象屬性。典型實現 ResourceRef。

與 Reference 相關的一個接口是 Referenceable,提供了 getReference 用於獲取 Reference 屬性。

public interface Referenceable {
    Reference getReference() throws NamingException;
}

對象實例可以實現 Reference 或 Referenceable 接口,如演示案例中的 MysqlDataSource 就實現了 Reference 接口。這樣依賴查找時就可以通過 Reference 創建對象。

2.5 對象工廠 - ObjectFactory

Spring 中也有一個 ObjectFactory,但個人覺得更類似 FactoryBean,用於根據 Reference 和 Context 創建對象。

JNDI 中的每個 Object 都對應 Reference,通過 ObjectFactory 創建對象,不像 Spring 是透明的,Spring 只有復雜對象的創建才會使用 FactoryBean(一般在整合三方框架時使用),這是因為 Spring 除了依賴查找外,還有依賴注入,面向 POJO 編程極大的簡化了編程模型。典型實現 ResourceFactory。

ObjectFactory 創建的實例對象即可以是 Context,也可以是通過 Reference 創建具體的實例 obj。

public interface ObjectFactory {
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
        Hashtable<?,?> environment) throws Exception;
}

2.6 JNDI 和 Spring 對比

JNDI 和 Spring 都實現了基本的依賴查找功能。

  1. JNDI 中也有對象元信息 Reference(BeanDefinition)、對象工廠 ObjectFactory(FactoryBean)、容器 Context(BeanFactory)等基本的依賴查找功能。
  2. JNDI 中對象需要實現 Reference 接口來暴露元信息 Reference,還需要工廠類 ObjectFactory 創建對象,和業務代碼耦合嚴重,有較大的侵入性。不像 Spring 中都是透明的,因為 Spring 是面向 POJO 編程,可以通過依賴注入創建復雜對象。在 Spring 中一般只有整合第三方框架時才會用到 FactoryBean。
  3. JNDI 只有依賴查找,Spring 還包括依賴注入。
  4. ...

3. 源碼分析

先回顧一下 JNDI 的基本用法:

// 獲得對數據源的引用
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/mysql");

// 或先獲取 subContext
Context envContext  = (Context)ctx.lookup("java:/comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/mysql");

3.1 JNDI 對象樹

"java:comp/env/jdbc/mysql" 后,Context 容器中 JNDI 對象樹結構如下:

  1. InitialContext:JNDI 提供的入口 Context,InitialContext 的主要功能是通過 getURLOrDefaultInitCtx() 方法獲取真實的 Context,在 Tomcat 中一般就是 SelectorContext。InitialContext 是一個簡單的代理模式,將所有的功能都委托給 SelectorContext。

  2. SelectorContext:用於處理 schema,在 Tomcat 中就是 "java"。通過配置屬性 java.naming.factory.url.pkgs=org.apache.naming,在 getURLOrDefaultInitCtx() 初始化時通過 ${java.naming.factory.url.pkgs}.${schema}.${schema}URLContextFactory 來獲取工廠類 ObjectFactory,再通過 javaURLContextFactory#getObjectInstance 創建 SelectorContext。

    SelectorContext 通過 lookup(name) 時,會先處理 name,將前綴 "java:" 去除,如 "java:comp/env/jdbc/mysql" 解析為 "comp/env/jdbc/mysql"。綁定 bind 元素時同理。

  3. NamingContext:真正的容器實現者,子元素可以是子容器(context),也可以是元素(element)。依賴查找時如果元素是子容器則委托給子容器繼續查找。元素綁定時,如果 name 有多級(eg: jdbc/mysql),則必須先創建子容器 jdbc,然后才能將元素綁定到容器中。

    JNDI 名稱 Name 也是有層級的,默認實現是 CompositeName(javadoc 有詳細的使用說明)。CompositeName 將 ”comp/env/jdbc/mysql“ 解析成 {”comp“, ”env“, ”jdbc“, ”mysql“},每層對應自己的 Context。

  4. NamingEntry:子節點(jdbc)將 jdbc 包裝成 NamingEntry 綁定到容器中。元素有以下幾種情況:

    • CONTEXT(子容器):表示元素是子容器,需要通過子容器繼續查找元素。作用類似 Spring 中的 BeanFactory。
    • REFERENCE(對象元信息):表示元素存儲的是對象的元信息,獲取元素時需要通過 REFERENCE 的屬性信息,如工廠類 ObjectFactory 創建元素。作用類似 Spring 中的 BeanDefinition。
    • LINK_REF:作用類似 Spring 中的別名 ailas。
    • ENTRY(對象實例):表示直接保存元素對象,查找時直接返回。作用類似 Spring 中的 bean。

3.2 核心類結構

說明: InitialContext、SelectorContext、NamingContext、NamingEntry 在上一小節對象樹上已經介紹過了。

InitialContext 初始化時會調用 javaURLContextFactory 初始化 SelectorContext,進而初始化 NamingContext。無論是資源綁定,還是依賴查找,最終都會調用 NamingContext。

  1. 資源綁定:資源綁定時最終調用 NamingContext#bind方法。綁定時會將對象包裝成 NamingEntry,需要注意的是,Context 中綁定的資源有多種類型,包括 CONTEXT、REFERENCE、LINK_REF、ENTRY 等。

  2. 依賴查找:依賴查找時最終調用 NamingContext#lookup 方法。如果是 CONTEXT 則繼續向下查找,如果是 REFERENCE 則需要根據對象元信息構建對象,如果是 ENTRY 則直接返回。比如 ResourceRef 就需要 ResourceFactory 來初始化對象返回。

3.3 初始化

(1)配置加載

public InitialContext() throws NamingException {
    init(null);
}
protected void init(Hashtable<?,?> environment) throws NamingException {
    // 1. 加載配置信息
    myProps = (Hashtable<Object,Object>)
        ResourceManager.getInitialEnvironment(environment);
    // 2. 加載默認Context,通過InitialContextFactory
    if (myProps.get(Context.INITIAL_CONTEXT_FACTORY) != null) {
        getDefaultInitCtx();
    }
}

ResourceManager#getInitialEnvironment 方法會加載相關配置,配置有兩種來源:一是 properties;二是系統變量 System.getProperties,它們的優先級如下:

System.getProperties() > ${classpath}/jndi.properties > ${java.home}/lib/jndi.properties

jndi.properties 配置如下:

java.naming.factory.url.pkgs=org.apache.naming
java.naming.factory.initial=org.apache.naming.java.javaURLContextFactory
java.naming.provider.url=127.0.0.1:1099

其中有兩個環境變量非常重要:URL_PKG_PREFIXES 和 INITIAL_CONTEXT_FACTORY。前者是根據 schema 加載 Context,后者則是加載默認的 Context,也就是通過 InitialContextFactory 加載。

// ObjectFactory, 默認 org.apache.naming.java.javaURLContextFactory
String URL_PKG_PREFIXES = "java.naming.factory.url.pkgs";
private static final String defaultPkgPrefix = "com.sun.jndi.url";

// InitialContextFactory, 默認 org.apache.naming.java.javaURLContextFactory
String INITIAL_CONTEXT_FACTORY = "java.naming.factory.initial";

(2)加載 Context

InitialContext 的所有操作都委托給了 getURLOrDefaultInitCtx() 初始化的 Context 完成,那它到底是如何加載到 SelectorContext 的呢?

protected Context getURLOrDefaultInitCtx(Name name) throws NamingException {
    // 1. 設置了全局的InitialContextFactoryBuilder
    if (NamingManager.hasInitialContextFactoryBuilder()) {
        return getDefaultInitCtx();
    }
    // 2. 根據schema加載,eg: "java:comp/env/jdbc/mysql" 的schema為java
    if (name.size() > 0) {
        String first = name.get(0);
        String scheme = getURLScheme(first);
        if (scheme != null) {
            Context ctx = NamingManager.getURLContext(scheme, myProps);
            if (ctx != null) {
                return ctx;
            }
        }
    }
    // 3. INITIAL_CONTEXT_FACTORY屬性對應的InitialContextFactory加載
    return getDefaultInitCtx();
}

總結: getURLOrDefaultInitCtx 要么是通過 schema 查找對應的 Context,要么是 INITIAL_CONTEXT_FACTORY屬性對應的 InitialContextFactory 加載 Context。

(1)schema 加載

// Tomcat 中為 org.apache.naming.java.javaURLContextFactory
${java.naming.factory.url.pkgs}.${schema}.${schema}URLContextFactory

在 javax.naming.spi.NamingManager 中默認為 com.sun.jndi.url 包下,如 schema 為 rmi 對應 com.sun.jndi.url.rmi.rmiURLContextFactory。Tomcat 修改了對應的配置,默認為 org.apache.naming.java.javaURLContextFactory。

// javax.naming.spi.NamingManager#getURLObject
private static Object getURLObject(String scheme, Object urlInfo, Name name, 
        Context nameCtx, Hashtable<?,?> environment) throws NamingException {
    ObjectFactory factory = (ObjectFactory)ResourceManager.getFactory(
        Context.URL_PKG_PREFIXES, environment, nameCtx,
        "." + scheme + "." + scheme + "URLContextFactory", defaultPkgPrefix);
    ...
    return factory.getObjectInstance(urlInfo, name, nameCtx, environment);
}

(2)InitialContextFactory 加載

getDefaultInitCtx 最終會讀取 env 中的 "java.naming.factory.initial" 屬性,加載 InitialContextFactory 初始化 Context。

// javax.naming.spi.NamingManager#getInitialContext
public static Context getInitialContext(Hashtable<?,?> env) throws NamingException {
    ...
    String className = (String)env.get(Context.INITIAL_CONTEXT_FACTORY);
    InitialContextFactory factory = (InitialContextFactory) helper.loadClass(className).newInstance();
    return factory.getInitialContext(env);
}

說明: 讀取 "java.naming.factory.initial" 屬性,加載 Context。

(3)javaURLContextFactory

public class javaURLContextFactory implements ObjectFactory, InitialContextFactory {
    // SelectorContext
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
            Hashtable<?,?> environment) throws NamingException {
        if ((ContextBindings.isThreadBound()) || (ContextBindings.isClassLoaderBound())) {
            return new SelectorContext((Hashtable<String,Object>)environment);
        }
        return null;
    }
    
    // 默認的Context
    @Override
    public Context getInitialContext(Hashtable<?,?> environment)
        throws NamingException {
        if (ContextBindings.isThreadBound() ||  (ContextBindings.isClassLoaderBound())) {
            return new SelectorContext(environment, true);
        }

        // If the thread is not bound, return a shared writable context
        if (initialContext == null) {
            initialContext = new NamingContext(environment, MAIN);
        }
        return initialContext;
    }
}

說明: 可以看到 Tomcat 最后加載了 SelectorContext,而 SelectorContext 最終委托 NamingContext 處理。

3.4 資源綁定

NamingContext#bind 資源綁定,肯定最終都是放到 bindings 的 Map 中,我們看一下是怎么處理的。

protected final HashMap<String,NamingEntry> bindings;
protected void bind(Name name, Object obj, boolean rebind) throws NamingException {
    // 1. 去除name名稱最前的空,如 "//x/y" -> {"", "", "x", "y"}
    while ((!name.isEmpty()) && (name.get(0).length() == 0))
        name = name.getSuffix(1);
    NamingEntry entry = bindings.get(name.get(0));

    // 2. 多層context,委托給subContext綁定
    if (name.size() > 1) {
        if (entry == null) {
            throw new NameNotFoundException("namingContext.nameNotBound");
        }
        if (entry.type == NamingEntry.CONTEXT) {
            if (rebind) {
                ((Context) entry.value).rebind(name.getSuffix(1), obj);
            } else {
                ((Context) entry.value).bind(name.getSuffix(1), obj);
            }
        } else {
            throw new NamingException("namingContext.contextExpected");
        }
    // 3. 綁定數據
    } else {
        // 3.1 如果entry已經綁定且不是rebind,則拋出異常。
        if ((!rebind) && (entry != null)) {
            throw new NameAlreadyBoundException("namingContext.alreadyBound");
        } else {
            // 3.2 CONTEXT/LinkRef/Reference/Referenceable/entry 分別處理
            Object toBind = NamingManager.getStateToBind(obj, name, this, env);
            if (toBind instanceof Context) {
                entry = new NamingEntry(name.get(0), toBind, NamingEntry.CONTEXT);
            } else if (toBind instanceof LinkRef) {
                entry = new NamingEntry(name.get(0), toBind, NamingEntry.LINK_REF);
            } else if (toBind instanceof Reference) {
                entry = new NamingEntry(name.get(0), toBind, NamingEntry.REFERENCE);
            } else if (toBind instanceof Referenceable) {
                toBind = ((Referenceable) toBind).getReference();
                entry = new NamingEntry(name.get(0), toBind, NamingEntry.REFERENCE);
            } else {
                entry = new NamingEntry(name.get(0), toBind, NamingEntry.ENTRY);
            }
            // 3.3 最終還是放到map中
            bindings.put(name.get(0), entry);
        }
    }
}

說明: bind 將 name 和 obj 綁定到 bindings 中:

  1. 將 name 解析為標准 name,就是去除 name 前的空格,如 "//x/y" 解析為 "x/y"。
  2. 如果 name 有多個層,委托給子容器進行綁定。
  3. 將 name 和 obj 封閉成 NamingEntry,綁定到 bindings 中。這里要關注一下元素的類型有以下幾種:CONTEXT/LinkRef/Reference/Referenceable/entry,分別做了那些處理。

3.5 依賴查找

NamingContext#lookup資源綁定,最終肯定是根據 name 從 bindings 中查找 obj,最重要的是對不同的元素類型是怎么處理的。

protected Object lookup(Name name, boolean resolveLinks) throws NamingException {
    ...
    NamingEntry entry = bindings.get(name.get(0));
    ...
    if (name.size() > 1) {
        // 1. Context類型,父子Context,委托 subContext查找
        if (entry.type != NamingEntry.CONTEXT) {
            throw new NamingException("namingContext.contextExpected");
        }
        return ((Context) entry.value).lookup(name.getSuffix(1));
    } else {
        // 2. LINK_REF類型,類似Spring中的別名
        if ((resolveLinks) && (entry.type == NamingEntry.LINK_REF)) {
            String link = ((LinkRef) entry.value).getLinkName();
            if (link.startsWith(".")) {
                // Link relative to this context
                return lookup(link.substring(1));
            } else {
                return new InitialContext(env).lookup(link);
            }
        // 3. REFERENCE類型,類似Spring中的BeanDefinition
        } else if (entry.type == NamingEntry.REFERENCE) {
            Object obj = NamingManager.getObjectInstance(entry.value, name, this, env);
            // 單例,由REFERENCE -> ENTRY,直接返回
            if(entry.value instanceof ResourceRef) {
                boolean singleton = Boolean.parseBoolean(
                    (String) ((ResourceRef) entry.value).get("singleton").getContent());
                if (singleton) {
                    entry.type = NamingEntry.ENTRY;
                    entry.value = obj;
                }
            }
            return obj;
        // 4. ENTRY類型,直接返回。所以多次查找都是同一對象,相當於單例
        } else {
            return entry.value;
        }
    }
}

說明: lookup 處理了 Context/LINK_REF/REFERENCE/ENTRY 幾種類型。

  1. 名稱解析:同 bind,如 "//x/y" 解析為 "x/y"。
  2. Context 類型:如果 name 有多個層,委托給子容器進行查找,如 "jdbc/mysql"。
  3. LINK_REF 類型:LinkRef#getLinkName() 重新在容器中查找。
  4. REFERENCE 類型:NamingManager#getObjectInstance() 方法根據 obj 元信息獲取 ObjectFactory 創建對象。效果類似多例。如果是單例,則將類型改寫成 ENTRY,並緩存 obj,直接返回。
  5. ENTRY 類型:直接返回,效果類似單例。

3.6 對象創建

在依賴查找中,如果對象是 NamingEntry.REFERENCE 類型,則會調用 NamingManager#getObjectInstance 創建對象。有以下幾個問題:

  1. 什么時候綁定的對象是 REFERENCE 類型?
  2. Reference 屬性元信息結構?
  3. NamingManager#getObjectInstance 是如何創建對象?
  4. MysqlDataSource 示例說明。

(1)什么時候綁定的對象是 REFERENCE 類型

對於第一個問題,當綁定的對象實現 Reference 或 Referenceable 時,對象為類型是 REFERENCE 類型,也就是說可以從綁定的對象中獲取對象的元信息。

大家想一下這有什么問題,綁定對象 obj 和對象元信息 Reference 是繼承關系,必須實現 Reference 接口,對業務代碼侵入性太大。如 MysqlDataSource 就實現了 Reference 接口。在 Spring 中 bean 和 beanDefinition 是沒有關系的,業務方不感知 beanDefinition,實現更優雅一些。

(2)Reference 屬性元信息結構

public class Reference {
    // 對象類型和工廠類型(ObjectFactory)
    protected String className;
    protected String classFactory = null;
    // RefAddr 屬性元信息,KV 結構 <type, content>
    protected Vector<RefAddr> addrs = null;
    ...
}

Reference 中保存了 obj 對象的對象類型和工廠類型(ObjectFactory),以及屬性信息。這樣就可以通過 ObjectFactory 創建對象,當然每次創建的對象都不是同一個,也就是多例。

(3)NamingManager#getObjectInstance 是如何創建對象

public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,
                      Hashtable<?,?> environment) throws Exception {
    ObjectFactory factory;
    // 1. 全局ObjectFactory創建對象,一般沒有設置
    ObjectFactoryBuilder builder = getObjectFactoryBuilder();
    if (builder != null) {
        factory = builder.createObjectFactory(refInfo, environment);
        return factory.getObjectInstance(refInfo, name, nameCtx, environment);
    }

    // 2. 獲取對象元信息 Reference
    Reference ref = null;
    if (refInfo instanceof Reference) {
        ref = (Reference) refInfo;
    } else if (refInfo instanceof Referenceable) {
        ref = ((Referenceable)(refInfo)).getReference();
    }
    Object answer;
    // 2. 根據對象元信息 Reference 創建對象
    if (ref != null) {
        String f = ref.getFactoryClassName();
        if (f != null) {
            // 2.1 loadClass ref.factoryClassName,自定義ObjectFactory加載對象
            factory = getObjectFactoryFromReference(ref, f);
            if (factory != null) {
                return factory.getObjectInstance(ref, name, nameCtx, environment);
            }
            return refInfo;
        } else {
            // 2.2 ref屬性:"URL": "rmi://host:port/xxx",根據URL中的schema加載對象
            //     ${java.naming.factory.url.pkgs}.${schema}.${schema}URLContextFactory
            answer = processURLAddrs(ref, name, nameCtx, environment);
            if (answer != null) {
                return answer;
            }
        }
    }

    // 3. 使用全局定義的工廠ObjectFactory,加載對象,任意一個ObjectFactory加載對象成功即返回
    //    屬性 "java.naming.factory.object"
    answer = createObjectFromFactories(refInfo, name, nameCtx, environment);
    return (answer != null) ? answer : refInfo;
}

說明: 對象創建的關鍵是如何獲取對象工廠 ObjectFactory。

  1. 全局 ObjectFactoryBuilder:根據 ObjectFactoryBuilder 創建 ObjectFactory。

  2. 設置對象元信息 Reference:分兩種情況。

    • 設置 factoryClassName:根據 factoryClassName 加載 ObjectFactory。
    • 設置 URL 屬性:根據 URL 的 schema 加載 xxxURLContextFactory。${java.naming.factory.url.pkgs}.${schema}.${schema}URLContextFactory。
  3. 全局 ObjectFactory:如果變量 "java.naming.factory.object" 定義有全局 ObjectFactory,則根據 ObjectFactory 創建對象,只要有一個能創建成功,則返回。

(4)MysqlDataSource 示例說明

MysqlDataSource 實現了 Referenceable 接口,getReference 方法

public Reference getReference() throws NamingException {
    String factoryName = MysqlDataSourceFactory.class.getName();
    Reference ref = new Reference(this.getClass().getName(), factoryName, (String)null);
    ref.add(new StringRefAddr(PropertyKey.USER.getKeyName(), this.getUser()));
    ref.add(new StringRefAddr(PropertyKey.PASSWORD.getKeyName(), this.password));
    ref.add(new StringRefAddr("serverName", this.getServerName()));
    ref.add(new StringRefAddr("port", "" + this.getPort()));
    ref.add(new StringRefAddr("databaseName", this.getDatabaseName()));
    ...
}

說明:Reference 定義了 factoryName 和必要的屬性屬性。MysqlDataSourceFactory 實現了 ObjectFactory 接口,通過 ObjectFactory 就可以創建 MysqlDataSource 對象。

4. Tomcat JNDI 整體流程分析

文章推薦:

NamingContextListener#createNamingContext 創建 Context 時會調用 addResourceLink、addResource、addResourceEnvRef、addEnvironment、addEjb 等。這些方法會將從 xml 文件中加載的配置信息 ResourceBase 等包裝成 Reference。

4.1 Tomat 資源類型

方法 ResourceBase Reference ObjectFactory
addResource ContextResource ResourceRef ResourceFactory
addResourceLink ContextResourceLink ResourceLinkRef ResourceLinkFactory
addEnvironment ContextEnvironment obj --
addService ContextService ServiceRef ServiceRefFactory
-- -- TransactionRef TransactionFactory

Reference 和 ObjectFactory 類圖關系如下:

說明: Tomcat 資源綁定和依賴查找

  1. 資源綁定時,將 ContextResource 轉化為 Reference,然后再綁定到 Context。
  2. 依賴查找時,會讀取 Reference 元信息,然后調用 ObjectFactory 創建對象。

4.2 資源綁定

以 addResource 為例,會將從 xml 文件中加載的配置信息 ContextResource 等包裝成 ResourceRef 等,然后綁定到 Context 中。

public void addResource(ContextResource resource) {
    // 1. resource包裝成Reference
    Reference ref = lookForLookupRef(resource);
    if (ref == null) {
        // ContextResource 包裝成 ResourceRef
        ref = new ResourceRef(resource.getType(), resource.getDescription(),
                     resource.getScope(), resource.getAuth(), resource.getSingleton());
        // 添加屬性信息拷貝到 ref 中
        Iterator<String> params = resource.listProperties();
        while (params.hasNext()) {
            String paramName = params.next();
            String paramValue = (String) resource.getProperty(paramName);
            StringRefAddr refAddr = new StringRefAddr(paramName, paramValue);
            ref.add(refAddr);
        }
    }
	
    // 2. Reference綁定到Context中
    createSubcontexts(envCtx, resource.getName());
    envCtx.bind(resource.getName(), ref);
    ...
}

4.3 依賴查找

在 NamingManager#getObjectInstance 時,會根據 ref.getFactoryClassName() 獲取 factoryClassName,對於 ResourceRef 而言,默認的工廠類是 ResourceFactory。

說明: 如果綁定的對象是 ResourceRef,則需要獲取對應的工廠 ResourceFactory 來創建對象。 ResourceFactory 其實也是一個代理模式,查找真正的 ObjectFactory 工廠,創建對象。

// AbstractRef#getFactoryClassName
@Override
public final String getFactoryClassName() {
    // 1. Reference中指定factoryName
    String factory = super.getFactoryClassName();
    if (factory != null) {
        return factory;
    } else {
        // 2. 配置項"java.naming.factory.object"
        factory = System.getProperty(Context.OBJECT_FACTORIES);
        if (factory != null) {
            return null;
        // 3. 默認工廠,對於 ResourceRef 就是 ResourceFactory。子類實現
        } else {
            return getDefaultFactoryClassName();
        }
    }
}

說明: 對於 ResourceRef 類型,則委托給 ResourceFactory 來創建對象。

// FactoryBase#getObjectInstance
@Override
public final Object getObjectInstance(Object obj, Name name, Context nameCtx,
        Hashtable<?,?> environment) throws Exception {
    if (isReferenceTypeSupported(obj)) {
        Reference ref = (Reference) obj;
        // 1. 獲取真正的ObjectFactory
        ObjectFactory factory = null;
        RefAddr factoryRefAddr = ref.get(Constants.FACTORY);
        if (factoryRefAddr != null) {
            String factoryClassName = factoryRefAddr.getContent().toString();
            factoryClass = Class.forName(factoryClassName);
            factory = (ObjectFactory) factoryClass.getConstructor().newInstance();
        } else {
            factory = getDefaultFactory(ref);
        }
        // 2. 通過ObjectFactory創建對象
        return factory.getObjectInstance(obj, name, nameCtx, environment);
    }
    return null;
}

說明: ResourceFactory 通過 ref.get(Constants.FACTORY) 加載 ObjectFactory 來創建對象。


每天用心記錄一點點。內容也許不重要,但習慣很重要!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM