将出色的基础架构移植到OSGi通常意味着解决复杂的类加载问题。 本文致力于解决该领域最困难的框架:进行动态代码生成的框架。 顺便说一句,这些也是最酷的框架:AOP包装器,ORM映射器和服务代理生成器仅是几个示例。
我们将按复杂度增加的顺序检查一些典型的类加载问题,并开发一些代码来解决最有趣的问题。 即使您不打算很快编写代码生成框架,本文也可以使您对带有静态定义的依赖项(例如OSGi)的模块化运行时的低级操作有所了解。
本文带有一个有效的演示项目,该项目不仅包含此处提供的代码,还包含两个可以使用的基于ASM的代码生成器。
类加载站点转换
将框架移植到OSGi通常需要将其重构为扩展程序模式。 这种模式允许框架将所有类加载委托给OSGi,但同时保留对应用程序代码生命周期的控制。 转换的目的是用从应用程序捆绑包中加载类来代替传统的过多类加载策略。 例如,我们要替换这样的代码:
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容器中安装/卸载捆绑软件即可添加/删除应用程序。 用户还可以根据需要将其应用程序分成多个束,在应用程序之间共享库,并利用其他此类模块化功能。
由于上下文类加载器是框架加载应用程序代码的当前标准方法,因此值得多加一些提示。 当前,OSGi尚未定义用于设置上下文类加载器的策略。 因此,开发人员需要事先知道框架何时依赖于上下文加载器,并在每次调用该框架时对其进行手动设置。 因为这容易出错并且不方便,所以OSGi几乎不使用上下文加载器。 目前正在努力定义OSGi容器应如何自动管理上下文类加载器。 在正式标准出台之前,最好将其使用的站点转换为来自具体应用程序捆绑包的类负载。
适配器ClassLoader
有时,我们转换的代码已经外部化了其类加载策略。 这意味着框架的类和方法采用显式的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);
}
}
现在我们可以将此适配器传递给转换后的框架代码。 我们还可以添加束跟踪代码以随着新束的出现而创建适配器-例如,我们可以“外部”使Java框架适应OSGi,从而避免了浏览代码库和转换每个类加载站点的工作。 这是一些代码的高度示意性示例,这些代码将框架转换为使用OSGi类加载:
...
Bundle app = ...
BundleClassLoader appLoader = new BundleClassLoader(app);
DeviceSimulationFramework simfw = ...
simfw.simulate("com.acme.devices.SinisterEngine", appLoader);
...
桥类加载器
许多有趣的Java框架在运行时都对客户端代码进行了精美的类工作。 通常的目标是从生活在应用程序类空间中的东西中动态构建类。 让我们称这些生成的类增强 。 通常,增强功能实现一些应用程序可见的接口或扩展应用程序可见的类。 有时还会混入其他接口及其实现。
增强功能增强了应用程序代码-生成的对象应由应用程序直接调用。 例如,将服务代理传递给应用程序代码,以使其免于跟踪动态服务的需要。 同样,添加一些AOP功能的包装程序将代替原始对象传递到应用程序代码。
增强功能是由您最喜欢的类工程库(ASM,BCEL,CGLIB等)产生的byte[]
块开始的。 生成类后,必须将原始字节转换为Class
对象,即,必须对字节进行一些ClassLoader
调用其defineClass()
方法。 我们有三个单独的问题要解决:
- 类空间的完整性 -首先,我们必须确定可以定义增强功能的类空间。 它必须“看到”足够的类以使增强功能完全链接
- 可见性
ClassLoader.defineClass()
是受保护的方法。 我们必须找到一个好方法 - 类空间一致性 -增强功能以OSGi容器“不可见”的方式混合了框架和应用程序捆绑包中的类。 因此,这些增强功能可能会暴露给同一类的不兼容版本
类空间完整性
增强由生成它们的Java框架专用的代码支持-这意味着该框架应将新类引入其自己的类空间中。 另一方面,这些增强实现了接口或扩展了在应用程序类空间中可见的类,这意味着我们应该在此处定义增强类。 我们不能同时在两个类空间中定义一个类,因此存在一个问题。
因为没有可以看到所有必需类的类空间,所以我们别无选择,只能创建一个新的类空间。 一个类空间等于一个ClassLoader
实例,因此我们的第一项工作是在每个应用程序包的顶部维护一个专用的ClassLoader
。 之所以称为Bridge ClassLoaders ,是因为它们通过链接其加载器来合并两个类空间:
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
开发的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支持的ClassLoader
基本上,主装载器和辅助装载器可以根据各自捆绑软件的导入/导出元数据委托给其他捆绑软件装载器。
在这一点上,我们可能对自己感到满意。 但是,令人痛苦的事实是,框架和应用程序类空间的组合可能还不够! 一切都取决于JVM 链接类的特殊方式(也称为解析类 )。 关于其工作原理有多种解释:
简短的答案: JVM解析在细粒度 (一次一个符号)级别上起作用。
答案很长:当JVM链接一个类时,它不需要链接的类引用的所有类的完整描述。 它只需要有关链接类实际使用的各个方法,字段和类型的信息。 我们的直觉是JVM的整体,是一个类名,再加上一个超类,再加上一组已实现的接口,再加上一组方法签名,再加上一组字段签名。 所有这些符号都是独立和惰性地解析的。 例如,要链接一个方法调用站点,调用者的类空间仅需要为目标类和方法签名中使用的所有类型提供Class
对象。 不需要定义目标类可能包含的许多其他内容,并且调用者的ClassLoader
将永远不会收到加载它们的请求。
正式的答案:类A
类空间SpaceA
必须由同一个代表Class
类空间物体SpaceB
当且仅当:
- 存在
SpaceB
的类B
,它从其符号表(也称为常量池 )中引用A
- OSGi容器已将
SpaceA
布线为SpaceB
A
类提供者。 连线是基于容器中所有捆绑包的静态元数据建立的。
例如:假设我们有一个BndA
捆绑包,它输出A
类。 类A
有3种方法,第3个接口之间分布:
-
IX.methodX(String)
-
IY.methodY(String)
-
IZ.methodZ(String)
还要想象一下,我们有一个具有B
类的BndB
束。 在类B
某个地方有一个引用A a = ...
和一个方法调用a.methodY("Hello!")
。 为了使类B
得以解决,我们需要将BndB
类A
的类空间和String
类引入。 就这样! 我们不需要导入IX
或IZ
。 我们甚至不需要导入IY
,因为类B
不使用它-它仅使用A
另一方面,当导出束BndA
解析类A
它必须提供IX, IY, IZ
,因为它们被直接引用为已实现的接口。 最后,甚至BndA
也不必提供IX, IY, IZ
任何超级接口,因为它们也没有直接引用。
现在,让我们想象一下,我们想A
从类空间BndA
到类B
BndB
的类A
的增强版本。 增强功能需要扩展A
类并覆盖其某些或所有方法。 因此,增强功能需要查看所有重写方法的签名中使用的类。 但是,仅当BndB
包含调用每个重写方法的代码时,才会导入所有这些类。 BndB
不太可能确切地调用我们打算通过增强来覆盖的A
方法。 因此, BndB
可能看不到足够的类来定义其类空间中的增强。 实际上,全套类只能由BndA
提供。 我们出现了问题!
事实证明,我们不应该桥接框架和应用程序空间,而是桥接框架空间和增强类的空间-因此,我们必须将策略转移到“桥接每个增强的空间”,而不是“桥接每个应用程序空间”。 我们需要从应用程序到某个第三方捆绑包的类空间进行传递跃迁,应用程序从该位置导入要我们增强的类。 我们如何实现过渡性飞跃? 简单! 众所周知,每个Class
对象都可以告诉我们哪个是第一次定义它的类空间。 例如,要获取A
的定义类加载器,我们要做A
就是调用A.class.getClassLoader()
。 但是,在许多情况下,我们使用String
名称而不是Class
对象,那么如何首先获得A.class
? 再次简单! 我们可以要求应用程序捆绑包以名称"A"
为我们提供它所看到的确切Class
对象。 比起我们可以将那个Class
的空间与框架空间联系起来。 这是关键的一步,因为我们需要增强的类和原始类在应用程序内是可互换的。 在类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<ClassLoader>> cache;
public BridgeClassLoaderCache(ClassLoader primary) {
this.primary = primary;
this.cache = new WeakHashMap<ClassLoader, WeakReference<ClassLoader>>();
}
public synchronized ClassLoader resolveBridge(ClassLoader secondary) {
ClassLoader bridge = null;
WeakReference<ClassLoader> ref = cache.get(secondary);
if (ref != null) {
bridge = ref.get();
}
if (bridge == null) {
bridge = new BridgeClassLoader(primary, secondary);
cache.put(secondary, new WeakReference<ClassLoader>(bridge));
}
return bridge;
}
}
为了防止由于保留ClassLoader
而导致的内存泄漏,我们必须同时使用弱键和弱值。 目的是在内存中不保留已卸载捆绑软件的类空间。 我们必须使用弱值,因为每个映射条目的值( BridgeClassLoader
)强烈引用键( ClassLoader
),从而消除了它的“弱点”。 这是WeakHashMap javadoc规定的标准建议。 通过使用弱缓存,我们避免了跟踪大量捆绑软件并对其生命周期做出积极React的需求。
能见度
好的,我们终于有了异国情调的桥类空间。 现在我们如何定义其中的增强功能? 如前面提到的问题,是defineClass()
是一个受保护的方法BridgeClassLoader
。 我们可以使用公共方法覆盖它,但这是不礼貌的。 另外,我们将必须编写自己的检查代码,以查看请求的增强功能是否已定义。 遵循ClassLoader
的预期设计是一个更好的主意。 此设计规定我们应该重写findClass()
,当它确定可以从任意二进制源提供所请求的类时,可以调用defineClass()
。 在findClass()
我们只能依靠请求的类的名称来进行决策。 因此,我们的BridgeClassLoader
必须考虑一下:
This is a request for "A$Enhanced", so I must call the enhancement generator for a class named "A"! Then I call defineClass() on the produced byte[]. Then I return the new Class object.
该声明有两点非凡之处。
- 我们为增强类的名称引入了文本协议 -我们可以将单个数据项传递给我们的
ClassLoader
所请求的类的名称的String
。 同时,我们需要传递两项数据-原始类的名称和一个标志,将其标记为增强对象。 我们将这两个项目打包成以下形式的单个字符串
现在,[name of target class]"$Enhanced"
findClass()
可以查找增强标记$Enhanced
并在存在时提取目标类的名称。 这样,我们还为增强功能的名称引入了约定。 每当我们在堆栈跟踪中看到以$Enhanced
结尾的类名时,我们就知道这是动态生成的类。 为了减轻与普通类发生名称冲突的风险,我们将增强标记设置为Java允许的外来标记(例如$__service_proxy__
) - 增强功能是按需生成的 -我们绝不会尝试两次生成增强功能。 我们继承的
loadClass()
方法将首先调用findLoadedClass()
,如果失败,它将调用parent.loadClass()
,只有失败了,它将调用findClass()
。 我们对名称使用严格的协议这一事实保证了findLoadedClass()
将在第二次收到增强同一个类的请求时起作用。 结合使用桥接ClassLoader
的缓存,我们得到了一个非常有效的解决方案,在该解决方案中,我们没有机会将相同的包空间桥接两次或生成冗余增强类。
在这里,我们还必须提到通过反射调用defineClass()
的选项。 cglib使用此方法。 当我们希望用户向我们传递可以使用的ClassLoader
时,这是一个可行的选择。 通过使用反射,我们避免了在此之上再创建另一个加载器的需求,这样我们就可以访问其defineClass()
方法。
类空间一致性
最终,我们要做的是使用OSGi模块化层合并两个不同的,未连接的类空间。 另外,我们在这些空间之间引入了搜索顺序,类似于邪恶的Java类路径的搜索顺序。 实际上,我们在某种程度上削弱了OSGi容器的类空间一致性。 这是一个坏事如何发生的场景:
- 框架使用包
com.acme.devices
并且需要版本1.0 - 应用程序使用程序包
com.acme.devices
并且完全需要2.0版。 - 类
A
直接指com.acme.devices. SinisterDevice
com.acme.devices. SinisterDevice
。 - 碰巧
A$Enhanced
类使用com.acme.devices. SinisterDevice
com.acme.devices. SinisterDevice
来自其内部实现。 - 因为我们搜索应用程序空间,所以第
A$Enhanced
将链接到com.acme.devices. SinisterDevice
com.acme.devices. SinisterDevice
版本2.0,虽然已针对com.acme.devices. SinisterDevice
编译了内部代码com.acme.devices. SinisterDevice
com.acme.devices. SinisterDevice
版本1.0。
结果,应用程序将看到神秘的LinkageError
和/或ClassCastException
。 不用说,这是一个问题。
las,解决此问题的自动化方法尚不存在。 我们必须简单地确保内部增强功能代码仅直接引用不太可能被其他任何人使用的“非常私有”的实现类。 我们甚至可以为我们可能要使用的任何外部API构建专用适配器,然后从增强代码中引用它们。 一旦有了定义良好的实现子空间,就可以使用该知识来限制类泄漏。 现在,我们仅将私有实现类的特殊子集的请求委托给框架空间。 这也将限制搜索顺序问题,从而使我们进行应用程序优先或框架优先的搜索变得无关紧要。 使事物处于受控状态的一种好的策略是为所有增强实现代码提供专用的程序包。 然后,桥接加载程序可以检查其名称以该包开头的类,并将其加载委托给框架加载程序。 最后,我们有时需要审慎放宽某些单包这样的隔离政策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<ClassLoader>> cache;
public Enhancer(ClassLoader privateSpace, Namer namer, Generator generator) {
this.privateSpace = privateSpace;
this.namer = namer;
this.generator = generator;
this.cache = new WeakHashMap<ClassLoader , WeakReference<ClassLoader>>();
}
@SuppressWarnings("unchecked")
public <T> Class<T> enhance(Class<T> target) throws ClassNotFoundException {
ClassLoader context = resolveBridge(target.getClassLoader());
String name = namer.map(target.getName());
return (Class<T>) context.loadClass(name);
}
private synchronized ClassLoader resolveBridge(ClassLoader targetSpace) {
ClassLoader bridge = null;
WeakReference<ClassLoader> ref = cache.get(targetSpace);
if (ref != null) {
bridge = ref.get();
}
if (bridge == null) {
bridge = makeBridge(targetSpace);
cache.put(appSpace, new WeakReference<ClassLoader>(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
。 生成器接收一个上下文ClassLoader
从中可以拉出类并在类上进行反射以驱动代码生成。 增强类名称的文本协议也可以通过Namer
接口插入。 这是如何使用这种增强框架的最终示意图代码:
...
/* 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<SinisterDevice> enhanced = enhancer.enhance(target);
...
上面介绍的增强器框架不只是伪代码。 实际上,在本文的研究过程中,它实际上是由两个在同一OSGi容器中同时运行的演示代码生成器构建和测试的。 结果带来了很多乐趣,现在每个人都可以在Google Code上使用它。
那些对类生成过程本身感兴趣的人可以检查两个基于ASM的演示生成器。 那些阅读有关服务动态的文章的人可能会注意到, 代理生成器使用此处提供的ServiceHolder
代码作为私有实现。
结论
在OSGi下,许多基础架构中都使用了所提供的类负载杂技。 例如,Guice,Peaberry和Spring Dynamic Modules使用类加载器桥接来使其AOP包装程序和服务代理正常工作。 当我们听到Spring员工说他们在Tomcat上做了认真的工作以使其适应OSGi时,我们可以推测他们必须进行类加载站点转换,或者进行更认真的重构才能完全外部化Tomcat的servlet类加载。
致谢
本文中的许多课程均摘自Stuart McCulloch为Google Guice和Peaberry编写的出色代码。 有关工业强度类负载桥接的示例,请参阅 Google Guice的BytecodeGen.java和Peaberry的ImportProxyClassLoader.java 。 在那里,您将看到如何处理其他一些方面,例如安全性,系统类加载器,更好的延迟缓存和并发性。 谢谢斯图尔特!
作者还必须承担Peter Kriens 对Tricky Proxies的经典解决方案 。 希望本文中有关JVM链接的说明将对Peter的工作做出有益的贡献。 谢谢彼得!
关于作者
托多尔 Boev已涉及使用OSGi在过去八年在雇员ProSyst 。 他热衷于将OSGi开发为JVM的通用编程环境。 目前,他在专业上和作为Peaberry项目的贡献者都在探讨这个主题。 他在rinswind.blogspot.com上维护一个博客。