事实上,您可以实现这样的单例。 cmets 中向您描述的问题是一个类可能被多个ClassLoaders 加载。然后,这些ClassLoaders 中的每一个都可以定义一个相同名称的类,这会错误地认为是唯一的。
但是,您可以通过实现对您的单例的访问器来避免这种情况,该访问器显式依赖于检查特定的ClassLoader 以查找再次包含您的单例的给定名称的类。这样,您可以避免由两个不同的ClassLoaders 提供一个单例实例,从而避免复制您需要在整个 JVM 中唯一的实例。
出于稍后解释的原因,我们将Singleton 和SingletonAccessor 分成两个不同的类。对于以下类,我们需要稍后确保始终使用特定的ClassLoader 访问它:
package pkg;
class Singleton {
static volatile Singleton instance;
}
一个方便的ClassLoader 是系统类加载器。系统类加载器知道 JVM 类路径上的所有类,并且根据定义将扩展和引导类加载器作为其父类。这两个类加载器通常不知道任何特定于域的类,例如我们的Singleton 类。这可以保护我们免受不必要的惊喜。此外,我们知道它可以在整个 JVM 运行实例中全局访问和知晓。
现在,让我们假设Singleton 类在类路径上。这样,我们就可以通过这个访问器使用反射来接收实例:
class SingletonAccessor {
static Object get() {
Class<?> clazz = ClassLoader.getSystemClassLoader()
.findClass("pkg.Singleton");
Field field = clazz.getDeclaredField("instance");
synchronized (clazz) {
Object instance = field.get(null);
if(instance == null) {
instance = clazz.newInstance();
field.set(null, instance);
}
return instance;
}
}
}
通过明确指定我们要从系统类加载器加载pkg.Singleton,我们确保我们始终接收到相同的实例,无论哪个类加载器加载了我们的SingletonAccessor。在上面的例子中,我们还确保Singleton 只被实例化一次。或者,您可以将实例化逻辑放入 Singleton 类本身,并让未使用的实例失效,以防加载其他 Singleton 类。
但是有一个很大的缺点。你错过了所有类型安全的方法,因为你不能假设你的代码总是从ClassLoader 运行,它将Singleton 的类加载委托给系统类加载器。对于在应用程序服务器上运行的应用程序尤其如此,该应用程序通常为其类加载器实现子优先语义并且不向系统类加载器询问已知类型但首先尝试加载其自己的类型.请注意,运行时类型具有两个特征:
- 它的完全限定名称
- 它的
ClassLoader
因此,SingletonAccessor::get 方法需要返回 Object 而不是 Singleton。
另一个缺点是必须在类路径中找到Singleton 类型才能使其工作。否则,系统类加载器不知道这种类型。如果您可以将 Singleton 类型放到类路径中,那么您就完成了。没问题。
如果您无法做到这一点,还有另一种方法,例如使用我的code generation library Byte Buddy。使用这个库,我们可以简单地在运行时定义这样一个类型并将其注入到系统类加载器中:
new ByteBuddy()
.subclass(Object.class)
.name("pkg.Singleton")
.defineField("instance", Object.class, Ownership.STATIC)
.make()
.load(ClassLoader.getSytemClassLoader(),
ClassLoadingStrategy.Default.INJECTION)
您刚刚为系统类加载器定义了一个类pkg.Singleton,上述策略再次适用。
此外,您可以通过实现包装类型来避免类型安全问题。您还可以在 Byte Buddy 的帮助下自动执行此操作:
new ByteBuddy()
.subclass(Singleton.class)
.method(any())
.intercept(new Object() {
@RuntimeType
Object intercept(@Origin Method m,
@AllArguments Object[] args) throws Exception {
Object singleton = SingletonAccessor.get();
return singleton.getClass()
.getDeclaredMethod(m.getName(), m.getParameterTypes())
.invoke(singleton, args);
}
})
.make()
.load(Singleton.class.getClassLoader(),
ClassLoadingStrategy.Default.INJECTION)
.getLoaded()
.newInstance();
您刚刚创建了一个委托器,它覆盖了 Singleton 类的所有方法,并将它们的调用委托给 JVM 全局单例实例的调用。请注意,即使它们签名相同,我们也需要重新加载反射方法,因为我们不能依赖委托的 ClassLoaders 和 JVM 全局类相同。
实际上,您可能希望缓存对SingletonAccessor.get() 的调用,甚至可能是反射方法查找(与反射方法调用相比,这相当昂贵)。但是这种需求在很大程度上取决于您的应用程序域。如果您的构造函数层次结构有问题,您还可以将方法签名分解为一个接口,并为上述访问器和您的 Singleton 类实现此接口。