本文通过如何将一个单例类实例化两次的案例,用代码实践来引入 Java 类加载器相关的概念与工作机制。理解并熟练掌握相关知识之后可以扩宽解决问题的思路,另辟蹊径,达到目的。
背景
单例模式是最常用的设计模式之一。其目的是保证一个类在进程中仅有一个实例,并提供一个它的全局访问方式。那什么场景下一个进程里需要单例类的两个对象呢?很明显这破坏了单例模式的设计初衷。
这里举例一个我司的特殊场景:
RPC 的调用规范是每个业务集群里只能有一个调用方,如果一个业务节点已经实例化了一个客户端,就无法再实例化另一个。这个规范的目的是让一个集群统一个调用方,方便服务数据的收集、展示、告警等操作。
一个项目有多个集群,多个项目组维护,各个集群都有一个共同特点,需要调用相同的 RPC 服务。如果严格按照上述 RPC 规范的话,每一个集群都需要申请一个自己调用方,每一个调用方都申请相同的 RPC 服务。这样做完全没有问题,只是相同的工作会被各个集群都做一遍,并且生成了多个 RPC 的调用方。
最终方案是将相同的逻辑代码打包成一个公用 jar 包,然后其他集群引入这个包就能解决我们上述的问题。这么做的话就碰到了 RPC 规范中的约束问题,jar 包里的公用逻辑会调用 RPC 服务,那么势必会有一个 RPC 的公用调用方。我们的业务代码里也会有自己业务需要调用的其他 RPC 服务,这个调用方和 jar 包里的调用方就冲突了,只能有一个调用方会被成功初始化,另一个则会报错。这个场景是不是就要实例化两个单例模式的对象呢。
有相关经验的读者可能会想到,能不能把各个集群中相同的工作抽取出来,做成一个类似网关的集群,然后各个集群再来调用这个公用集群,这样同一个工作也不会被做多遍,RPC 的调用方也被整合成了一个。这个方案也是很好的,考虑到一些客观因素,最终并没有选择这种方式。
实例化两个单例类
我们假设下述单例类代码是 RPC 的调用 Client:
public class RPCClient {
private static BaseClient baseClient;
private volatile static RPCClient instance;
static {
baseClient = BaseClient.getBaseClient();
}
private RPCClient() {
System.out.println("构造 Client");
}
public String callRpc() {
return "callRpc success";
}
public static RPCClient getClient() {
if (instance == null) {
synchronized (RPCClient.class) {
if (instance == null) {
instance = new RPCClient();
}
}
}
return instance;
}
}
public class BaseClient {
...
private BaseClient() {
System.out.println("构造 BaseClient");
}
...
}
这个单例 Client 有一点点不同,就是有一个静态属性 baseClient,BaseClient 也是一个简单的单例类,构造方法里有一些打印操作,方便后续观察。baseClient 属性通过静态代码块来赋值。
我们可以想一想,有什么办法可以将这个单例的 Client 类实例化两个对象出来?
无所不能的反射大法
最容易想到的就是利用反射获取构造方法,来规避单例类私有化构造方法的约束来实例化:
Constructor<?> declaredConstructor = RPCClient.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Object rpcClient = declaredConstructor.newInstance();
Method sayHi = rpcClient.getClass().getMethod("callRpc");
Object invoke = sayHi.invoke(rpcClient