从使用 ldap 想到的 jndi 机制分析

缘起

​ 最近做总部定制,一个组件开发任务. 要查询数据库,但是总部对接人说这边不提供直接数据库访问,只暴露ldap服务. 听过没用过是最令人兴奋的啦. 于是兴奋的花了一天时间了解了ldap, 并且写了ldap 的 demo(参见【1】). 但是依旧觉得不过瘾,因为写demo的时候发现了JNDI的影子. JNDI 最初是在学jdbc的时候,学过一种使用jndi技术将mysql数据源使用tomcat容器之类的Servlet容器管理起来. 然后在代码里面直接使用统一的JNDI API获取数据源即可. 而不论数据源是mysql数据源还是oracle数据源. 现在发现ldap、JDK内置DNS查询程序DnsContextFactory、甚至 MQ的资源都可以使用JNDI技术管理起来. 于是就产生了浓厚的兴趣想了解JDK是怎么做到的.

分析

关于JNDI 技术的介绍,推荐看看【1】的P189. 这里只PO一张图

a、b都是JNDI中的术语Context, 每个Context里面既可以存java serialized对象(dog、pig),也可以存属性(cat、mouse就是属性的集合,属性是\<属性名, 若干属性值>的键值对,其中一个属性名可以对应多个属性值, 就好像你可以有多个邮箱一样.).同一个Contex中的属性不能同属性名, 但是不同Context(比如ldap的Context和mq的Context)是完全可以有属性名相同的属性的. 所以其实JNDI的操作就分成2步.

第一步 定位到Context

第二步 对Context中的属性进行增删改查.

这里顺便说一句, LDAP中的术语——条目 其实就是JNDI中的Context,只是LDAP的Context只存若干属性,不存序列化对象而已.

下面直奔JDK源码.

一般使用的是

1
Context context = new InitDirContext(HashTable<?,?> environment)

所以我们跟源码到

1
javax.naming.InitialContext.init(Hashtable<?, ?> environment)

该方法的源码

1
2
3
4
5
6
7
myProps = (Hashtable<Object,Object>)
ResourceManager.getInitialEnvironment(environment);

if (myProps.get(Context.INITIAL_CONTEXT_FACTORY) != null) {
// user has specified initial context factory; try to get it
getDefaultInitCtx();
}

其中myProps你可以理解为 environment的一层封装. 其过程是(其实就是 ResourceManager.getInitialEnvironment源码的解读)

  1. 从入参env中获取

    1
    2
    3
    4
    5
    6
    7
    8
    Context.INITIAL_CONTEXT_FACTORY = "java.naming.factory.initial",
    Context.OBJECT_FACTORIES = "java.naming.factory.object";,
    Context.URL_PKG_PREFIXES = ""java.naming.factory.url.pkgs"",
    Context.STATE_FACTORIES = "java.naming.factory.state",
    Context.PROVIDER_URL = "java.naming.provider.url",
    Context.DNS_URL = "java.naming.dns.url",
    // The following shouldn't create a runtime dependence on ldap package.
    LdapContext.CONTROL_FACTORIES

    这5个参数的值

  2. 看看 env 中有没有参数com.sun.naming.disable.app.resource.files, 并且是不是true,如果是false或者不存在的话 就直接返回env给mypros. 否则的话, 进入下面的第三步

  3. 合并从env中获取的上面7个参数的值(可能为null,则进一步applet中的值)和 $JAVA_HOME/lib/jndi.properties文件以及classpath下的用户自定义的jndi.properties·文件的内容. 最后返回给mypros.

  4. 如果myProps中存在Context.INITIAL_CONTEXT_FACTORY, 即java.naming.factory.initial,则就开始getDefaultInitCtx();

  5. getDefaultInitCtx 中最核心的代码是

    1
    defaultInitCtx = NamingManager.getInitialContext(myProps);

    注意,NamingManager的角色就好比是SPI机制中的java.util.ServiceLoader 我们把它的源码po出来

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public static Context getInitialContext(Hashtable<?,?> env)
    throws NamingException {
    InitialContextFactory factory;

    InitialContextFactoryBuilder builder = getInitialContextFactoryBuilder();
    if (builder == null) {
    // No factory installed, use property
    // Get initial context factory class name

    String className = env != null ?
    (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null;
    if (className == null) {
    NoInitialContextException ne = new NoInitialContextException(
    "Need to specify class name in environment or system " +
    "property, or as an applet parameter, or in an " +
    "application resource file: " +
    Context.INITIAL_CONTEXT_FACTORY);
    throw ne;
    }

    try {
    factory = (InitialContextFactory)
    helper.loadClass(className).newInstance();
    } catch(Exception e) {
    NoInitialContextException ne =
    new NoInitialContextException(
    "Cannot instantiate class: " + className);
    ne.setRootCause(e);
    throw ne;
    }
    } else {
    factory = builder.createInitialContextFactory(env);
    }

    return factory.getInitialContext(env);
    }

    注意到第五行代码,其结果就是返回NamingManager中的static成员变量 initctx_factory_builder. 但是它是需要先NamingManager. setInitialContextFactoryBuilder的(同一个jvm进程至多只能set一次,否则抛出IllegalStateException异常). 一般如果没有set的话,则就是null. 则一般情况下我们走的是第10行代码的逻辑. 我们先从传入的myProps中查询 java.naming.factory.initial参数,如果是null的话,则抛NoInitialContextException异常. 所以一定要指定生产Context的InitialContextFactory实现类,不是在jndi.properties文件中指定,就是在传入InitContext构造器的hashtable中指定. 我们看看常用的InitialContextFactory实现类. 其实这和SPI机制中的 META-INF/services/文件方法是一模一样的套路.

    来到最关键的代码

    1
    factory = (InitialContextFactory)helper.loadClass(className).newInstance();

    继续跟代码

    1
    com.sun.naming.internal.VersionHelper12.loadClass(String)

    该方法源码如下

    1
    return loadClass(className, getContextClassLoader());

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ClassLoader getContextClassLoader() {

    return AccessController.doPrivileged(
    new PrivilegedAction<ClassLoader>() {
    public ClassLoader run() {
    ClassLoader loader =
    Thread.currentThread().getContextClassLoader();
    if (loader == null) {
    // Don't use bootstrap class loader directly!
    loader = ClassLoader.getSystemClassLoader();
    }

    return loader;
    }
    }
    );
    }

    看到了没有???第六行和第十行又见线程上下文类加载器!!! 可见JDK也是满满的套路!!! 一种机制,屡试不爽啊! 到了这里我们就明白整个JNDI机制是怎么工作的了. 最后根据获取的InitContextFactory实现类(譬如com.sun.jndi.ldap.LdapCtxFactory)获取Context, LdapCtxFactory.getInitialContext源码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public Context getInitialContext(Hashtable<?, ?> paramHashtable)
    throws NamingException
    {
    try
    {
    String str = paramHashtable != null ? (String)paramHashtable.get("java.naming.provider.url") : null;
    if (str == null)
    return new LdapCtx("", "localhost", 389, paramHashtable, false);
    arrayOfString = LdapURL.fromList(str);
    if (arrayOfString.length == 0)
    throw new ConfigurationException("java.naming.provider.url property does not contain a URL");
    return getLdapCtxInstance(arrayOfString, paramHashtable);
    }
    catch (LdapReferralException localLdapReferralException)
    {
    if ((paramHashtable != null) && ("throw".equals(paramHashtable.get("java.naming.referral"))))
    throw localLdapReferralException;
    String[] arrayOfString = paramHashtable != null ? (Control[])paramHashtable.get("java.naming.ldap.control.connect") : null;
    return (LdapCtx)localLdapReferralException.getReferralContext(paramHashtable, arrayOfString);
    }
    }

    此方法返回的是LdapCtx 即一个Ldap上下文, 该上下文中有LdapClient类, 里面持有Ldap环境参数可以和ldap服务器进行ldap协议通信.

    有了上面的分析,我们看一张经典的JNDI架构图

    就不难理解JNDI机制本质上和SPI机制是一回事(jdbc4.0之后采用的就是SPI机制,从而不需要写Class.forName 加载驱动包的字节码了). 实现了服务的热插拔. 都是基于线程上下文类加载器.

    至此, JNDI机制已经浅析完毕.

参考

【1】 《Java邮件开发详解 》 张孝祥 方立勋

DEMO

【1】https://github.com/yfsyfs/backend/tree/master/ldap-demo