类加载器特技:OSGi代码生成

我们将按照复杂性增加的顺序考察一些类加载的典型问题,开发一小段代码 来解决这些问题中最有趣的一个。即使你不打算马上写一个代码生成框架,这篇 文章也会让你对静态定义依赖的模块运行时(如OSGi系统)的低级操作有比较深 入的了解。

这篇文章还包括一个可以工作的演示项目,该项目不仅包含这里演示的代码 ,还有两个基于ASM的代码生成器可供实践。

类加载地点转换

把一个框架移植到OSGi系统通常需要把框架按照extender模式重构。这个模 式允许框架把所有的类加载工作委托给OSGi框架,与此同时保留对应用代码的生 命周期的控制。转换的目标是使用应用bundle的类加载来代替传统的繁复的类加 载策略。例如我们希望这样替换代码:

ClassLoader appLoader = Thread.currentThread ().getContextClassLoader();Class appClass = appLoader.loadClass ("com.acme.devices.SinisterEngine");...ClassLoader appLoader = ...Class appClass = appLoader.loadClass ("com.acme.devices.SinisterEngine");

替换为:

Bundle appBundle = ...Class appClass = appBundle.loadClass ("com.acme.devices.SinisterEngine");

尽管我们必须做大量的工作以便OSGi为我们加载应用代码,我们至少有一种 优美而正确的方式来完成我们的工作,而且会比以前工作得更好!现在用户可以 通过向OSGi容器安装/卸载bundle而增加/删除应用。用户还可以把他们的应用分 解为多个bundle,在应用之间共享库并利用模块化的其他能力。

由于上下文类加载器是目前框架加载应用代码的标准方式,我们在此对其多 说两句。当前OSGi没有定义设置上下文类加载器的策略。当一个框架依赖于上下 文类加载器时,开发者需要预先知道这点,在每次调用进入那个框架时手工设置 上下文类加载器。由于这样做易于出错而其不方便,所以在OSGi下几乎不使用上 下文类加载器。在定义OSGi容器如何自动管理上下文类加载器方面,目前有些人 正在进行尝试。但在一个官方的标准出现之前,最好把类加载转移到一个具体的 应用bundle。

适配器类加载器

有时候我们转换的代码有外部化的类加载策略。这意味着框架的类和方法接 收明确的ClassLoader 参数,允许我们来决定他们从哪里加载应用代码。在这种 情况下,把系统转换到OSGi就仅仅是让Bundle对象适配ClassLoader API的问题 。这是一个经典的适配器模式的应用。

public class BundleClassLoader extends ClassLoader {  private final Bundle delegate;  public BundleClassLoader(Bundle delegate) {   this.delegate = delegate;  }  @Override  public Class loadClass(String name) throws  ClassNotFoundException {   return delegate.loadClass(name);  }}

现在我们可以把这个适配器传给转换的框架代码。随着新bundle的增减,我 们还可以增加bundle跟踪代码来创建新的适配器 —— 例如,我们可以“在外部 ”把一个Java框架适配到OSGi,避免浏览该框架的代码库以及变换每个单独的类 加载场所。下面是将一个框架转换到使用OSGi 类加载的示意性的例子:

...Bundle app = ...BundleClassLoader appLoader = new BundleClassLoader (app);DeviceSimulationFramework simfw = ...simfw.simulate("com.acme.devices.SinisterEngine", appLoader);...

桥接类加载器

许多有趣的Java框架的客户端代码在运行时做了很多花哨的类处理工作。其 目的通常是在应用的类空间中构造本不存在的类。让我们把这些生成的类称作增 强(enhancement)。通常,增强类实现了一些应用可见的接口或者继承自一个 应用可见的类。有时,一些附加的接口及其实现也可以被混入。

增强类扩充了应用代码 - 应用可以直接调用生成的对象。例如,一个传递 给应用代码的服务代理对象就是这种增强类对象,它使得应用代码不必去跟踪一 个动态服务。简单的说,增加了一些AOP特征的包装器被作为原始对象的替代品 传递给应用代码。

增强类的生命始于字节数组byte[],由你喜爱的类工程库(ASM,BCEL, CGLIB)产生。一旦我们生成了我们的类,必须把这些原始的字节转换为一个 Class对象,换言之,我们必须让某个类加载器对我们的字节调用它的 defineClass()方法。我们有三个独立的问题要解决:

类空间完整性 – 首先我们必须决定可以定义我们增强类的类空间。该类空间 必须“看到”足够多的类以便让增强类能够被完全链接。

可见性 – ClassLoader.defineClass()是一个受保护的方法。我们必须找到 一个好方法来调用它。

类空间一致性 – 增强类从框架和应用bundle混入类,这种加载类的方式对于 OSGi容器来说是“不可见的”。作为结果,增强类可能被暴露给相同类的不兼容 的版本。

类空间完整性

增强类的背后支持代码对于生成它们的Java框架来说是未公开的 – 这意味着 该框架应该会把该新类加入到其类空间。另一方面,增强类实现的接口或者扩展 的类在应用的类空间是可见,这意味着我们应该在这里定义增强类。我们不能同 时在两个类空间定义一个类,所以我们有个麻烦。

因为没有类空间能够看到所有需要的类,我们别无选择,只能创建一个新的 类空间。一个类空间等同于一个类加载器实例,所以我们的第一个工作就是在所 有的应用bundle之上维持一个专有的类加载器。这些叫做桥接类加载器,因为他 们通过链接加载器合并了两个类空间:

public class BridgeClassLoader extends ClassLoader {  private final ClassLoader secondary;  public BridgeClassLoader(ClassLoader primary, ClassLoader  secondary) {   super(primary);  }  @Override  protected Class findClass(String name) throws  ClassNotFoundException {   return secondary.loadClass(name);  }}

现在我们可以使用前面开发的BundleClassLoader:

/* Application space */  Bundle app = ...  ClassLoader appSpace = new BundleClassLoader(app);  /*   * Framework space   *   * We assume this code is executed inside the  framework   */  ClassLoader fwSpace = this.getClass().getClassLoader ();  /* Bridge */  ClassLoader bridge = new BridgeClassLoader(appSpace,  fwSpace);

这个加载器首先从应用空间为请求提供服务 – 如果失败,它将尝试框架空间 。请注意我们仍然让OSGi为我们做很多重量工作。当我们委托给任何一个类空间 时,我们实际上委托给了一个基于OSGi的类加载器 – 本质上,primary和 secondary加载器可以根据他们各自bundle的导入/导出(import/export)元数 据将加载工作委托给其他 bundle加载器。

此刻我们也许会对自己满意。然而,真相是苦涩的,合并的框架和应用类空 间也许并不够!这一切的关键是JVM链接类(也叫解析类)的特殊方式。对于JVM 链接类的工作有很多解释:

简短的回答:

JVM以一种细粒度(一次一个符号)的方式来做解析工作的。

冗长的回答:

当JVM链接一个类时,它不需要被连接类的所有引用类的完整的描述。它只需 要被链接类真正使用的个别的方法、字段和类型的信息。我们直觉上认为对于 JVM来说,其全部是一个类名称,加上一个超类,加上一套实现的接口,加上一 套方法签名,加上一套字段签名。所有这些符号是被独立且延迟解析的。例如, 要链接一个方法调用,调用者的类空间只需要给类对象提供目标类和方法签名中 用到的所有类型。目标类中的其他许多定义是不需要的,调用者的类加载器永远 不会收到加载它们(那些不需要的定义)的请求。

正式的答案:

类空间SpaceA的类A必须被类空间SpaceB的相同类对象所代表,当且仅当:

SpaceB存在一个类B,在它的符号表(也叫做常量池)中引用着A。

OSGi容器已经将SpaceA作为类A的提供者(provider)提供给SpaceB。该联系 是建立在容器所有bundle的静态元数据之上的。

例如:

假设我们有一个bundle BndA导出一个类A。类A有3个方法,分布于3个接口中 :

IX.methodX(String)

IY.methodY(String)

IZ.methodZ(String)

还假设我们有一个bundle BndB,其有一个类B。类B中有一个引用 A a = … …和一个方法调用a.methodY(“Hello!”)。为了能够解析类B,我们需要为BndB的 类空间引入类A和类String。这就够了!我们不需要导入IX或者IZ。我们甚至不 需要导入IY,因为类B没有用IY – 它只用了A。在另一方面,bundle BndA导出时 会解析类A,必须提供IX,IY,IZ,因为他们作为被实现的接口直接被引用。最 终,BndA也不需要提供IX,IY,IZ的任何父接口,因为他们也没有被直接引用。

现在假设我们希望给类空间BndB的类B呈现类空间BndA的类A的一个增强版本 。该增强类需要继承类A并覆盖它的一些或全部方法。因此,该增强类需要看到 在所有覆盖的方法签名中使用的类。然而,BndB仅当调用了所有被覆盖的方法时 才会导入所有这些类。BndB恰好调用了我们的增强覆盖的所有的A 的方法的可能 性非常小。因此,BndB很可能在他的类空间中不会看到足够的类来定义增强类。 实际上完整的类集合只有BndA能够提供。我们有麻烦了!

结果是我们必须桥接的不是框架和应用空间,而是框架空间和增强类的空间 – 所以,我们必须把策略从“每个应用空间一个桥”变为“每个增强类空间一个 桥”。我们需要从应用空间到一些第三方bundle的类空间做过渡跳接,在那里, 应用导入其想让我们增强的类。但是我们如何做过渡跳接呢?很简单!如我们所 知,每个类对象可以告诉我们它第一次被定义的类空间是什么。例如,我们要得 到A 的类加载器,所需要做的就是调用A.class.getClassLoader()。在很多情况 下我们没有一个类对象,只有类的名字,那么我们如何从一开始就得到A.class ?也很简单!我们可以让应用bundle给我们它所看到的名称“A”对应的类对象 。然后我们就可以桥接那个类的空间与框架的空间。这是很关键的一步,因为我 们需要增强类和原始类在应用内是可以互换的。在类A可能的许多版本中,我们 需要挑选被应用所使用的那个类的类空间。下面是框架如何保持类加载器桥缓存 的示意性例子:

.../* Ask the app to resolve the target class */Bundle app = ...Class target = app.loadClass ("com.acme.devices.SinisterEngine");/* Get the defining classloader of the target */ClassLoader targetSpace = target.getClassLoader();/* Get the bridge for the class space of the target  */BridgeClassLoaderCache cache = ...ClassLoader bridge = cache.resolveBridge(targetSpace);

桥缓存看起来会是这样:

public class BridgeClassLoaderCache {  private final ClassLoader primary;  private final Map<ClassLoader,  WeakReference> cache;  public BridgeClassLoaderCache(ClassLoader primary) {   this.primary = primary;   this.cache = new WeakHashMap<ClassLoader,  WeakReference>();  }  public synchronized ClassLoader resolveBridge(ClassLoader  secondary) {   ClassLoader bridge = null;   WeakReference ref = cache.get (secondary);   if (ref != null) {    bridge = ref.get();   }   if (bridge == null) {    bridge = new BridgeClassLoader(primary, secondary);    cache.put(secondary, new WeakReference (bridge));   }   return bridge;  }}

为了防止保留类加载器带来的内存泄露,我们必须使用弱键和弱值。目标是 不在内存中保持一个已卸载的bundle的类空间。我们必须使用弱值,因为每个映 射项目的值(BridgeClassLoader)都强引用着键(ClassLoader),于是以此方 式否定它的“弱点”。这是WeakHashMap javadoc规定的标准建议。通过使用一 个弱缓存我们避免了跟踪所有的bundle,而且不必对他们的生命周期做出反应。

可见性

好的,我们终于有了自己的外来的桥接类空间。现在我们如何在其中定义我 们的增强类?如前所述问题,defineClass()是 BridgeClassLoader的一个受保 护的方法。我们可以用一个公有方法来覆盖它,但这是粗野的做法。如果做覆盖 ,我们还需要自己编码来检查所请求的增强类是否已经被定义。更好的办法是遵 循类加载器设计的意图。该设计告诉我们应该覆盖findClass(),当findClass() 认为它可以由任意二进制源提供所请求类时会调用defineClass()方法。在 findClass()中我们只依赖所请求的类的名称来做决定。所以我们的 BridgeClassLoade必须自己拿主意:

这是一个对“A$Enhanced”类的请求,所以我必须调用一个叫做”A”的类的增 强类生成器!然后我在生成的字节数组上调用defineClass()方法。然后我返回 一个新的类对象。

这段话中有两个值得注意的地方。

我们为增强类的名称引入了一个文本协议

– 我们可以给我们的类加载器传入数据的单独一项 – 所请求的类的名称的字 符串。同时我们需要传入数据中的两项 – 原始类的名称和一个标志,将其(原 始类)标志为增强类的主语。我们将这两项打包为一个字符串,形式为[目标类 的名称]”$Enhanced”。现在 findClass()可以寻找增强类的标志$Enhanced,如 果存在,则提取出目标类的名称。这样我们引入了我们增强类的命名约定。无论 何时,当我们在堆栈中看到一个类名以$Enhanced结尾,我们知道这是一个动态 生成的类。为了减少与正常类名称冲突的风险,我们将增强类标志做得尽可能特 殊(例如:$__service_proxy__)

增强是按需生成的

– 我们永远不会把一个增强类生成两次。我们继承的loadClass()方法首先会 调用findLoadedClass(),如果失败会调用 parent.loadClass(),只有失败的时 候它才会调用 findClass()。由于我们为名称用了一个严格的协议,保证 findLoadedClass()在第二次请求相同类的增强类时候不会失败。这与桥接类加 载器缓存相结合,我们得到了一个非常有效的方案,我们不会桥接同样的bundle 空间两次,或者生产冗余的增强类。

这里我们必须强调通过反射调用defineClass()的选项。cglib使用这种方法 。当我们希望用户给我们传递一个可用的类加载器时这是一种可行的方案。通过 使用反射我们避免了在类加载器之上创建另一个类加载器的需要,只要调用它的 defineClass()方法即可。

类空间一致性

到了最后,我们所做的是使用OSGi的模块层合并两个不同的、未关联的类空 间。我们还引入了在这些空间中一种搜索顺序,其与邪恶的Java类路径搜索顺序 相似。实际上,我们破坏了OSGi容器的类空间一致性。这里是糟糕的事情发生的 一个场景:

框架使用包com.acme.devices,需要的是1.0版本。

应用使用包com.acme.devices,需要的是2.0版本。

类A直接饮用com.acme.devices.SinisterDevice。

类A$Enhanced在他自己的实现中使用了com.acme.devices.SinisterDevice。

因为我们搜索应用空间,首先A$Enhanced会被链接到 com.acme.devices.SinisterDevice 2.0版,而他的内部代码是基于 com.acme.devices.SinisterDevice 1.0编译的。

结果应用将会看到诡异的LinkageErrors或者ClassCastExceptions。不用说 ,这是个问题。

唉,自动处理这个问题的方式还不存在。我们必须简单的确保增强类的内部 代码直接引用的是“非常私有的”类实现,不会被其他类使用。我们甚至可以为 任何我们可能希望使用的外部API定义私有的适配器,然后在增强类代码中引用 这些适配器。一旦我们有了一个良好定义的实现子空间,我们可以用这个知识来 限制类泄露。现在我们仅仅向框架空间委托特殊的私有实现类的请求。这还会限 定搜索顺序问题,使得应用优先搜索还是框架优先搜索对结果没有影响。让所有 的事情都可控的一个好策略是有一个专有的包来包含所有增强类实现代码。那么 桥接加载器就可以检查以那个包开头的类的名称并将它们委托给框架加载器做加 载。最终,我们有时候可以对特定的单实例(singleton)包放宽这个隔离策略 ,例如org.osgi.framework – 我们可以安全的直接基于org.osgi.framework编 译我们的增强类代码,因为在运行时所有在OSGi容器中的代码都会看到相同的 org.osgi.framework – 这是由OSGi核心保证的。

把事情放到一起

所有关于这个类加载的传说可以被浓缩为下面的100行代码:

public class Enhancer {  private final ClassLoader privateSpace;  private final Namer namer;  private final GeneraTor generaTor;  private final Map<ClassLoader ,  WeakReference> cache;  public Enhancer(ClassLoader privateSpace, Namer namer,  GeneraTor generaTor) {   this.privateSpace = privateSpace;   this.namer = namer;   this.generaTor = generaTor;   this.cache = new WeakHashMap<ClassLoader ,  WeakReference>();  }  @SuppressWarnings("unchecked")   public  Class enhance(Class  target) throws ClassNotFoundException {   ClassLoader context = resolveBridge(target.getClassLoader ());   String name = namer.map(target.getName());   return (Class) context.loadClass(name);  }  private synchronized ClassLoader resolveBridge(ClassLoader  targetSpace) {   ClassLoader bridge = null;   WeakReference ref = cache.get (targetSpace);   if (ref != null) {    bridge = ref.get();   }   if (bridge == null) {    bridge = makeBridge(targetSpace);    cache.put(appSpace, new WeakReference (bridge));   }   return bridge;  }  private ClassLoader makeBridge(ClassLoader targetSpace) {   /* Use the target space as a parent to be searched  first */   return new ClassLoader(targetSpace) {    @Override    protected Class findClass(String name) throws  ClassNotFoundException {     /* Is this used privately by the enhancements?  */     if (generaTor.isInternal(name)) {      return privateSpace.loadClass(name);     }     /* Is this a request for enhancement? */     String unpacked = namer.unmap(name);     if (unpacked != null) {      byte[] raw = generaTor.generate(unpacked, name,  this);      return defineClass(name, raw, 0, raw.length);     }     /* Ask someone else */     throw new ClassNotFoundException(name);    }   };  }}public interface Namer {  /** Map a target class name to an enhancement class  name. */  String map(String targetClassName);  /** Try to extract a target class name or return null.  */  String unmap(String className);}public interface GeneraTor {  /** Test if this is a private implementation class.  */  boolean isInternal(String className);  /** Generate enhancement bytes */  byte[] generate(String inputClassName, String  outputClassName, ClassLoader context);}

Enhancer仅仅针对桥接模式。代码生成逻辑被具体化到一个可插拔的 GeneraTor中。该GeneraTor接收一个上下文类加载器,从中可以得到类,使用反 射来驱动代码生成。增强类名称的文本协议也可以通过Name接口插拔。这里是一 个最终的示意性代码,展示这么一个增强类框架是如何使用的:

.../* Setup the Enhancer on top of the framework class space  */ClassLoader privateSpace = getClass().getClassLoader();Namer namer = ...;GeneraTor generaTor = ...;Enhancer enhancer = new Enhancer(privateSpace, namer,  generaTor);.../* Enhance some class the app sees */Bundle app = ...Class target = app.loadClass ("com.acme.devices.SinisterEngine");Class enhanced = enhancer.enhance (target);...

这里展示的Enhance框架不仅是伪代码。实际上,在撰写这篇文章期间,这个 框架被真正构建出来并用两个在同一OSGi容器中同时运行的样例代码生成器进行 了测试。结果是类加载正常,现在代码在Google Code上,所有人都可以拿下来 研究。

对于类生成过程本身感兴趣的人可以研究这两个基于ASM的生成器样例。那些 在service dynamics上阅读文章的人也许注意到proxy generaTor使用 ServiceHolder代码作为一个私有实现。

结论

这里展现的类加载特技在许多OSGi之外的基础框架中使用。例如桥接类加载 器被用在Guice,Peaberry中,Spring Dynamic Modules则用桥接类加载器来使 他们的AOP包装器和服务代理得以工作。当我们听说Spring的伙计们在将Tomcat 适配到OSGi方面做了大量工作时,我们可以推断他们还得做类加载位置转换或者 更大量的重构来外化Tomcat的servlet加载。

没有伞的孩子必须努力奔跑!

类加载器特技:OSGi代码生成

相关文章:

你感兴趣的文章:

标签云: