设为首页 加入收藏

TOP

不是单例的单例——巧用ClassLoader(一)
2023-07-25 21:25:11 】 浏览:48
Tags:单例的 单例 巧用 ClassLoader

本文通过如何将一个单例类实例化两次的案例,用代码实践来引入 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);
//执行输出
//构造 Client
//callBaseRpc successcallRpc success

上述代码通过反射来获取私有化的构造方法,然后通过这个构造方法来实例化对象。这样确实能生成单例 RPCClient 的第二个对象。观察代码执行的输出能发现,通过反射生成的这个对象 rpcClient 确实是一个新对象,因为输出里有 RPCClient 构造方法的打印输出。但是并没有打印 BaseClient 这个对象的构造方法里的输出。rpcClient 这个对象里的 baseClient 永远都是只用一个,因为 baseClient 在静态代码块里赋值的,并且 BaseClient 又是一个单例类。这样,我们反射生成的对象与非反射生成的对象就不是完全隔离的。

上述的简单 Demo 里,使用反射好像都不太能够生成两个完全隔离的单例客户端。一个复杂的 RPC Client 类可远没有这么简单,Client 类里还有很多依赖的类,依赖的类里也会依赖其他类,其中不乏各种单例类。通过反射的方法好像行不太通。那还有什么方法能达到目的呢?

自定义类加载器

另一个方法是用一个自定义的类加载器来加载 RPCClient 类并实例化。业务代码默认使用的是 AppClassLoader 类加载器,这个类加载器来加载 RPCClient 类并实例化第一个 Client 对象,我们自定义的类加载器会加载并实例化第二个 Client 对象。那么在一个 JVM 进程里就存在了两个 RPCClient 对象了。这两个对象会不会存在上述反射中没有完全隔离的问题呢?

答案是不会。类加载是有传递性的,当一个类被加载时,这个类依赖的类如果需要加载,使用的类加载器就是当前类的类加载器。我们使用自定义类加载器加载 RPCClient 时,RPCClient 依赖的类也会被自定义加载器加载。这样依赖类也会被完全隔离,也就没有在上述反射中存在的 baseClient 属性还是同一个对象的情况。

自定义类加载器代码如下:

public class MyClassLoader extends ClassLoader{
    @Override
    public Class<?> loadClass(String name) {
      //通过 findLoadedClass 判断是否已经被加载 (下文会补充)
      Class<?> loadedClass = findLoadedClass(name);
      //如果已加载返回已加载的类
      if (loadedClass != null) {
          return loadedClass;
      }
      //通过类名获取类文件
      String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
      InputStream resourceAsStream = getClass().getResourceA
首页 上一页 1 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Springboot通过谷歌Kaptcha 组件.. 下一篇SpringBoot 使用 Sa-Token 完成注..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目