OSGi#三:ClassLoader中嵌入Equinox

OSGi#3:ClassLoader中嵌入Equinox

Java语言的模块化之路似乎走得异常艰辛,但实际上技术难点看上去并不像是最大的问题,OSGi已经是业内公认的标准,正如这篇文章中作者所说的,

I suspect the answer to these questions has little to do with technology, and more to do with politics.

anyway,要等到Java语言级别来支持模块化,不知道要何年何月。最近做了个尝试,直接在ClassLoader中嵌入Equinox容器,这样是不是也算得上是在语言级别上支持了模块化:)

V1

在类加载的整体流程中,我们可以通过-Djava.system.class.loader这个system property来介入类加载过程当中。

看下ClassLoader的源码,

    private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
                scl = l.getClassLoader(); // sun.misc.Launcher$AppClassLoader
                try {
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }
class SystemClassLoaderAction
    implements PrivilegedExceptionAction<ClassLoader> {
    private ClassLoader parent;

    SystemClassLoaderAction(ClassLoader parent) {
        this.parent = parent; // 传入的是sun.misc.Launcher$AppClassLoader
    }

    public ClassLoader run() throws Exception {
        String cls = System.getProperty("java.system.class.loader");
        if (cls == null) {
            return parent;
        }

        Constructor ctor = Class.forName(cls, true, parent)
            .getDeclaredConstructor(new Class[] { ClassLoader.class });
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[] { parent });
        Thread.currentThread().setContextClassLoader(sys);
        return sys;
    }
}

可以看到,由-Djava.system.class.loader指定的ClassLoader的parent将会是sun.misc.Launcher$AppClassLoader,并且该ClassLoader必须有一个接收parent的构造函数。

开始写ClassLoader之前需要先准备下环境,见之前写的这篇栗子,Main类改一下,

import me.kisimple.just4fun.osgi.HelloOSGi;

public class Main {
    public static void main(String[] args) throws Throwable {
        new HelloOSGi().run();
    }
}

第一版的ClassLoader是这样子的,

package me.kisimple.just4fun;

import org.eclipse.core.runtime.adaptor.EclipseStarter;
import org.eclipse.osgi.container.ModuleWiring;
import org.eclipse.osgi.internal.framework.EquinoxBundle;
import org.eclipse.osgi.internal.loader.BundleLoader;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

public class IClassLoader extends ClassLoader {

    // -Djava.system.class.loader=me.kisimple.just4fun.IClassLoader

    private BundleContext bundleContext;

    public IClassLoader(ClassLoader parent) {
        super(parent);
        try {
            bundleContext = EclipseStarter.startup((new String[]{}), null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class c = findLoadedClass(name);
            if (c == null) {

                if(name.startsWith("java")) {
                    return getParent().loadClass(name);
                }

                if(bundleContext != null) {
                    for (Bundle osgiBundle : bundleContext.getBundles()) {
                        if (osgiBundle instanceof EquinoxBundle) {
                            EquinoxBundle equinoxBundle = (EquinoxBundle)osgiBundle;
                            ModuleWiring wiring = equinoxBundle.getModule().getCurrentRevision().getWiring();
                            if (wiring != null) {
                                BundleLoader loader = (BundleLoader)wiring.getModuleLoader();
                                String packageName = BundleLoader.getPackageName(name);
                                if (loader != null && loader.isExportedPackage(packageName)) {
                                    System.out.println("[LOADED] " + name);
                                    System.out.println("[FORM] Bundle#" + equinoxBundle);
                                    return equinoxBundle.loadClass(name);
                                }
                            }
                        }
                    }
                }

                return getParent().loadClass(name);
            }
            return c;
        }
    }

}

直接在构造函数中启动Equinox。但是这一版执行之后会报下面这个错,

Caused by: java.lang.InternalError: internal error: SHA-1 not available.
        at sun.security.provider.SecureRandom.init(Unknown Source)
        at sun.security.provider.SecureRandom.<init>(Unknown Source)
        at java.security.SecureRandom.getDefaultPRNG(Unknown Source)
        at java.security.SecureRandom.<init>(Unknown Source)
        at java.io.File$TempDirectory.<clinit>(Unknown Source)
        at java.io.File.createTempFile(Unknown Source)
        at org.eclipse.osgi.storage.StorageUtil.canWrite(StorageUtil.java:172)
        at org.eclipse.osgi.internal.location.EquinoxLocations.computeDefaultConfigurationLocation(EquinoxLocations.java:257)
        at org.eclipse.osgi.internal.location.EquinoxLocations.<init>(EquinoxLocations.java:97)
        at org.eclipse.osgi.internal.framework.EquinoxContainer.<init>(EquinoxContainer.java:70)
        at org.eclipse.osgi.launch.Equinox.<init>(Equinox.java:31)
        at org.eclipse.core.runtime.adaptor.EclipseStarter.startup(EclipseStarter.java:295)

看上去应该是一些安全相关的provider在这个时候还无法找到,so,此路不通。

V2

既然在构造函数中不行,那就挪到loadClass方法中试试,

package me.kisimple.just4fun;

import org.eclipse.core.runtime.adaptor.EclipseStarter;
import org.eclipse.osgi.container.ModuleWiring;
import org.eclipse.osgi.internal.framework.EquinoxBundle;
import org.eclipse.osgi.internal.loader.BundleLoader;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

public class IClassLoader extends ClassLoader {

    // -Djava.system.class.loader=me.kisimple.just4fun.IClassLoader

    private volatile static boolean equinoxStartupFlag = false;

    private BundleContext bundleContext;

    public IClassLoader(ClassLoader parent) {
        super(parent);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class c = findLoadedClass(name);
            if (c == null) {

                if(!equinoxStartupFlag) {
                    equinoxStartup();
                }

                if(name.startsWith("java")) {
                    return getParent().loadClass(name);
                }

                if(bundleContext != null) {
                    for (Bundle osgiBundle : bundleContext.getBundles()) {
                        if (osgiBundle instanceof EquinoxBundle) {
                            EquinoxBundle equinoxBundle = (EquinoxBundle)osgiBundle;
                            ModuleWiring wiring = equinoxBundle.getModule().getCurrentRevision().getWiring();
                            if (wiring != null) {
                                BundleLoader loader = (BundleLoader)wiring.getModuleLoader();
                                String packageName = BundleLoader.getPackageName(name);
                                if (loader != null && loader.isExportedPackage(packageName)) {
                                    System.out.println("[LOADED] " + name);
                                    System.out.println("[FORM] Bundle#" + equinoxBundle);
                                    return equinoxBundle.loadClass(name);
                                }
                            }
                        }
                    }
                }

                return getParent().loadClass(name);
            }
            return c;
        }
    }

    private synchronized void equinoxStartup() {
        if(!equinoxStartupFlag) {

            try {
                bundleContext = EclipseStarter.startup((new String[]{}), null);
                System.out.println("////////////////////");
                System.out.println("//Equinox Started.//");
                System.out.println("////////////////////");
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override
                    public void run() {
                        try {
                              EclipseStarter.shutdown();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
            }catch(Exception e){
                throw new RuntimeException(e);
            }
            equinoxStartupFlag = true;

        }
    }

}

执行的时候会发现,一直在输出错误。这其实是一个递归加载的问题。执行equinoxStartup方法的时候,会遇到其他需要加载的类,这时候loadClass方法会被递归调用,也就一直卡在equinoxStartup了。解决方案是,把equinoxStartupFlag = true;移到最前,

    private synchronized void equinoxStartup() {
        if(!equinoxStartupFlag) {

            equinoxStartupFlag = true;
            try {
                bundleContext = EclipseStarter.startup((new String[]{}), null);
                System.out.println("////////////////////");
                System.out.println("//Equinox Started.//");
                System.out.println("////////////////////");
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override
                    public void run() {
                        try {
                              EclipseStarter.shutdown();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
            }catch(Exception e){
                throw new RuntimeException(e);
            }

        }
    }

不过这时候会出现新的错误,

> java -Djava.system.class.loader=me.kisimple.just4fun.IClassLoader me.kisimple.just4fun.Main
////////////////////
//Equinox Started.//
////////////////////
Exception in thread "main" java.lang.NoClassDefFoundError: me/kisimple/just4fun/osgi/HelloOSGi
        at me.kisimple.just4fun.Main.main(Main.java:40)
Caused by: java.lang.ClassNotFoundException: me.kisimple.just4fun.osgi.HelloOSGi
        at java.net.URLClassLoader$1.run(Unknown Source)
        at java.net.URLClassLoader$1.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        ... 1 more

V3

上面的问题,找不到HelloOSGi,其实是由于并没有使用我们的IClassLoader来加载,可以通过在loadClass方法中输出正在加载的类来证实。

那么为什么没有使用IClassLoader呢?其实是由于Main-Class,也就是me.kisimple.just4fun.Main,并不是由IClassLoader所加载,而是由IClassLoader.getParent(),也就是sun.misc.Launcher$AppClassLoader,所完成的。所以接下来由Main-Class所触发的类加载都跟IClassLoader没有一毛钱关系了。

那接下来的问题就是要自己去加载Main-Class了。首先需要先找到Main-Class的名字,这个可以通过sun.java.command这个system property获得。然后就是怎么加载的问题了。既然sun.misc.Launcher$AppClassLoader可以加载Main-Class,那我们就复用它的代码呗,自我感觉是一个比较取巧的方法:)

package me.kisimple.just4fun;

import org.eclipse.core.runtime.adaptor.EclipseStarter;
import org.eclipse.osgi.container.ModuleWiring;
import org.eclipse.osgi.internal.framework.EquinoxBundle;
import org.eclipse.osgi.internal.loader.BundleLoader;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

import java.net.URL;
import java.net.URLClassLoader;

public class IClassLoader extends URLClassLoader {

    // -Djava.system.class.loader=me.kisimple.just4fun.IClassLoader

    private volatile static boolean equinoxStartupFlag = false;

    private BundleContext bundleContext;
    private String mainClassName = "";

    public IClassLoader(ClassLoader parent) {
        super(((URLClassLoader)parent).getURLs(), parent);
        mainClassName = System.getProperty("sun.java.command");
        System.out.println("[sun.java.command] " + mainClassName);
    }

    public IClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class c = findLoadedClass(name);
            if (c == null) {

                System.out.println("[IClassLoader] " + name);

                if(!equinoxStartupFlag) {
                    equinoxStartup();
                }

                if(name.startsWith("java")) {
                    return getParent().loadClass(name);
                }

                if(mainClassName.startsWith(name)) {
                    return findClass(name);
                }

                if(bundleContext != null) {
                    for (Bundle osgiBundle : bundleContext.getBundles()) {
                        if (osgiBundle instanceof EquinoxBundle) {
                            EquinoxBundle equinoxBundle = (EquinoxBundle)osgiBundle;
                            ModuleWiring wiring = equinoxBundle.getModule().getCurrentRevision().getWiring();
                            if (wiring != null) {
                                BundleLoader loader = (BundleLoader)wiring.getModuleLoader();
                                String packageName = BundleLoader.getPackageName(name);
                                if (loader != null && loader.isExportedPackage(packageName)) {
                                    System.out.println("[Equinox] LOADING# " + name);
                                    System.out.println("[Equinox] FORM# Bundle#" + equinoxBundle);
                                    return equinoxBundle.loadClass(name);
                                }
                            }
                        }
                    }
                }

                return getParent().loadClass(name);
            }
            return c;
        }
    }

    private synchronized void equinoxStartup() {
        if(!equinoxStartupFlag) {

            equinoxStartupFlag = true;
            try {
                bundleContext = EclipseStarter.startup((new String[]{}), null);
                System.out.println("////////////////////");
                System.out.println("//Equinox Started.//");
                System.out.println("////////////////////");
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override
                    public void run() {
                        try {
                              EclipseStarter.shutdown();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
            }catch(Exception e){
                throw new RuntimeException(e);
            }

        }
    }

}

不再是继承ClassLoader,而是直接继承URLClassLoader,并且在构造函数拿到加载Main-Class所需要的URL信息,然后匹配mainClassName,通过调用findClass方法来完成Main-Class的加载。

事实证明此路是可以通的:)输出如下,

> java -Djava.system.class.loader=me.kisimple.just4fun.IClassLoader me.kisimple.just4fun.Main
[sun.java.command] me.kisimple.just4fun.Main
[IClassLoader] me.kisimple.just4fun.Main
[IClassLoader] sun.security.provider.Sun
[IClassLoader] sun.security.rsa.SunRsaSign
[IClassLoader] sun.net.www.protocol.c.Handler
[IClassLoader] sun.net.www.protocol.e.Handler
[IClassLoader] sun.net.www.protocol.c.Handler
[IClassLoader] sun.util.resources.CalendarData
[IClassLoader] sun.util.resources.CalendarData_zh
[IClassLoader] sun.text.resources.FormatData
[IClassLoader] sun.text.resources.FormatData_zh
[IClassLoader] sun.text.resources.FormatData_zh_CN
[IClassLoader] sun.util.resources.CurrencyNames
[IClassLoader] sun.util.resources.CurrencyNames_zh_CN
////////////////////
//Equinox Started.//
////////////////////
[IClassLoader] java.lang.Object
[IClassLoader] java.lang.String
[IClassLoader] java.lang.Throwable
[IClassLoader] me.kisimple.just4fun.osgi.HelloOSGi
[Equinox] LOADING# me.kisimple.just4fun.osgi.HelloOSGi
[Equinox] FORM# Bundle#me.kisimple.just4fun.osgi.common_1.0.0 [3]
Hello,OSGi.

这种有点hack的方式,跟JDK版本应该还是相关的,下面是我的JDK版本,

> java -version
java version "1.7.0_51"
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.51-b03, mixed mode)

alright,今天就到这,have fun ^_^

参考资料

  • http://moi.vonos.net/java/osgi-classloaders/
  • http://java.dzone.com/news/java-modularity-osgi-and
  • http://*.com/questions/41894/0-program-name-in-java-discover-main-class