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 来创建对象。


每天用心记录一点点。内容也许不重要,但习惯很重要!

posted on 2020-02-07 14:46  binarylei  阅读(1475)  评论(0编辑  收藏  举报

导航