51 單例模式的幾種hack

前言

今天[2019-04-01]在某位大大的"聊天" 過程中提到了這麼一個問題, 如果讓你選擇 單例的實現, 你會怎麼選擇 

我就不假思索的說了一個 使用靜態內部類來創建 單例對象吧

通常創建單例對象 有幾種方式 : 雙檢鎖 + volatile, 靜態內部類holder, 枚舉 

然後 大大又問了一下, 你選的這種方式在 反射反序列化 的情況下 會怎麼樣呢?, 攔不攔得住呢 ?

然後 我腦袋裏面想了一下, 反射過來, 靜態內部類holder 這種方式怕是gg了吧, 能夠通過反射創建出多個對象, 並且 通過反序列化 也是能夠創建出多個改類型的對象的 

然後 考慮了一下 枚舉, 枚舉 我記得 構造方法在規範上面約定的必須是 private, 然後使用反射調用 好像也記得有約束, 然後 反序列化這邊, 我記得 之前看過這塊地代碼, 是構造對象是調用的 枚舉的 "valueOf" 方法, 返回的是已經構造好枚舉對象 

但是回答問題的時候, 又說枚舉, 就給人一種很像是猜的結果的感覺 ... 

 

然後 針對這些東西 我們這裏回來進行一些 hack, 希望能夠打破所謂的只能存在一個實例的說法 

主要測試的方式是 : 通過反射處理, 通過反序列化處理, 通過 unsafe 處理 

一下內容僅僅是測試, 請勿在實際生產環境中騷操作 

 

以下測試代碼, 截圖 基於 jdk 8 

 

 

case01 雙檢鎖單例 

package com.hx.test04;

import sun.misc.Unsafe;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

/**
 * DoubleCheckSingleton
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2020-04-01 17:01
 */
public class Test18DoubleCheckSingleton implements Serializable {

  // INSTANCE
  private static volatile Test18DoubleCheckSingleton INSTANCE;

  // disable constructor
  private Test18DoubleCheckSingleton() {
//    throw new RuntimeException("can't instantiate !");
    System.out.println(" <init> called ");
  }

  // Test18DoubleCheckSingleton
  public static void main(String[] args) throws Exception {

    Test18DoubleCheckSingleton entity01 = Test18DoubleCheckSingleton.getInstance();

    // case 1 constructor
    Class<Test18DoubleCheckSingleton> clazz = Test18DoubleCheckSingleton.class;
    Constructor<Test18DoubleCheckSingleton> constructor = clazz.getDeclaredConstructor();
    Test18DoubleCheckSingleton entity02 = constructor.newInstance();

    // case 2 unsafe
    Unsafe unsafe = getUnsafe();
    Test18DoubleCheckSingleton entity03 = (Test18DoubleCheckSingleton) unsafe.allocateInstance(Test18DoubleCheckSingleton.class);

    // case 3 deserialize
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(entity01);
    byte[] serialized = baos.toByteArray();
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized));
    Test18DoubleCheckSingleton entity04 = (Test18DoubleCheckSingleton) ois.readObject();

    int x = 1;

  }

  /**
   * getInstance
   *
   * @return com.hx.test04.Test18DoubleCheckSingleton
   * @author Jerry.X.He<[email protected]>
   * @date 2020-04-01 17:02
   */
  public static Test18DoubleCheckSingleton getInstance() {
    if(INSTANCE == null) {
      synchronized (Test18DoubleCheckSingleton.class) {
        if(INSTANCE == null) {
          INSTANCE = new Test18DoubleCheckSingleton();
        }
      }
    }

    return INSTANCE;
  }

  /**
   * getUnsafe
   *
   * @return sun.misc.Unsafe
   * @author Jerry.X.He<[email protected]>
   * @date 2020-04-01 17:11
   */
  public static Unsafe getUnsafe() {
      try {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
      } catch (NoSuchFieldException e) {
        e.printStackTrace();
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      }
      return null;
  }

}

可以看出, 出了 getInstance 拿到的一個對象之外 

另外的三種 hack 方式 也拿到了一個新建的對象 

 

日誌這邊打印了兩次構造方法中的輸出, 具體的原因可以參見 : 42 不調用給定類的構造方法創建給定類的對象 

 

 

case02 靜態內部類 Holder

package com.hx.test04;

import sun.misc.Unsafe;

import java.io.*;
import java.lang.reflect.Constructor;

/**
 * DoubleCheckSingleton
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2020-04-01 17:01
 */
public class Test19StaticClazzHolderSingleton implements Serializable {

  // INSTANCE
  private static volatile Test19StaticClazzHolderSingleton INSTANCE;

  // disable constructor
  private Test19StaticClazzHolderSingleton() {
//    throw new RuntimeException("can't instantiate !");
    System.out.println(" <init> called ");
  }

  // Test18DoubleCheckSingleton
  public static void main(String[] args) throws Exception {

    Test19StaticClazzHolderSingleton entity01 = Test19StaticClazzHolderSingleton.getInstance();

    // case1 constructor
    Class<Test19StaticClazzHolderSingleton> clazz = Test19StaticClazzHolderSingleton.class;
    Constructor<Test19StaticClazzHolderSingleton> constructor = clazz.getDeclaredConstructor();
    Test19StaticClazzHolderSingleton entity02 = constructor.newInstance();

    // case2 unsafe
    Unsafe unsafe = Test18DoubleCheckSingleton.getUnsafe();
    Test19StaticClazzHolderSingleton entity03 = (Test19StaticClazzHolderSingleton) unsafe.allocateInstance(Test19StaticClazzHolderSingleton.class);

    // case 3 deserialize
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(entity01);
    byte[] serialized = baos.toByteArray();
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized));
    Test19StaticClazzHolderSingleton entity04 = (Test19StaticClazzHolderSingleton) ois.readObject();

    int x = 1;

  }

  /**
   * getInstance
   *
   * @return com.hx.test04.Test18DoubleCheckSingleton
   * @author Jerry.X.He<[email protected]>
   * @date 2020-04-01 17:02
   */
  public static Test19StaticClazzHolderSingleton getInstance() {
    return SingletonHolder.INSTANCE;
  }

  /**
   * SingletonHolder
   *
   * @author Jerry.X.He <[email protected]>
   * @version 1.0
   * @date 2020-04-01 18:07
   */
  private static class SingletonHolder {
    // instance
    private static Test19StaticClazzHolderSingleton INSTANCE = new Test19StaticClazzHolderSingleton();

  }

}

 

可以看到這種方式也淪陷了, 三種 hack 方式分別創建了 三個對象 

 

 

case03 枚舉單例

package com.hx.test04;

import com.sun.org.apache.bcel.internal.classfile.ConstantClass;
import mockit.Mock;
import mockit.MockUp;
import sun.misc.Unsafe;
import sun.reflect.ConstructorAccessor;
import sun.reflect.Reflection;

import java.io.*;
import java.lang.reflect.*;

/**
 * DoubleCheckSingleton
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2020-04-01 17:01
 */
public enum Test20EnumSingleton implements Serializable {

  INSTANCE;

  // disable constructor
  private Test20EnumSingleton() {
//    throw new RuntimeException("can't instantiate !");
    System.out.println(" <init> called ");
  }

  // Test18DoubleCheckSingleton
  public static void main(String[] args) throws Exception {

    Test20EnumSingleton entity01 = Test20EnumSingleton.getInstance();

    // case1 constructor
    // Cannot reflectively create enum objects
    Class<Test20EnumSingleton> clazz = Test20EnumSingleton.class;
    Constructor<Test20EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class, int.class);

    Method acquireConstructorAccessorMethod = Constructor.class.getDeclaredMethod("acquireConstructorAccessor");
    acquireConstructorAccessorMethod.setAccessible(true);
    acquireConstructorAccessorMethod.invoke(constructor);

    Field constructorAccessorField = Constructor.class.getDeclaredField("constructorAccessor");
    constructorAccessorField.setAccessible(true);
    ConstructorAccessor constructorAccessor = (ConstructorAccessor) constructorAccessorField.get(constructor);
    Test20EnumSingleton entity02 = (Test20EnumSingleton) constructorAccessor.newInstance(new Object[]{"xyz", 2});
//    Test20EnumSingleton entity02 = constructor.newInstance("xyz", 2);

    // case2 unsafe
    Unsafe unsafe = Test18DoubleCheckSingleton.getUnsafe();
    Test20EnumSingleton entity03 = (Test20EnumSingleton) unsafe.allocateInstance(Test20EnumSingleton.class);

    // case 3 deserialize
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(entity01);
    byte[] serialized = baos.toByteArray();
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized));
    Test20EnumSingleton entity04 = (Test20EnumSingleton) ois.readObject();

    int x = 1;

  }

  /**
   * getInstance
   *
   * @return com.hx.test04.Test18DoubleCheckSingleton
   * @author Jerry.X.He<[email protected]>
   * @date 2020-04-01 17:02
   */
  public static Test20EnumSingleton getInstance() {
    return INSTANCE;
  }

}

枚舉這裏通過反射來 hack 的話, 直接通過 Constructor.newInstance 會存在問題, 會被校驗攔住 

然後調整 clazz.getModifiers 似乎是又有些難度, 怕是要調試 更新運行時的數據, 所以 選擇了另外的一種 hack 方式 

獲取對應的 ConstructorAccessor, 來創建實例, 當然 這裏 hack 成功了 

另外 unsafe 這裏也 hack 成功了, 只不過類型的相關字段爲 null, 0 等 

再看一下 反序列化這邊, 我們發現 entity04 是和 entity01 是同一個對象, 因此 反序列化這邊 枚舉攔住了 

 

枚舉的反序列化這邊具體在代碼裏面的體現爲 

 

 

case04 雙檢鎖單例構造方法拋出異常

package com.hx.test04;

import sun.misc.Unsafe;

import java.io.*;

/**
 * DoubleCheckSingleton
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2020-04-01 17:01
 */
public class Test21ConstructorExSingleton extends Test21ConstructorExSingletonParent implements Serializable {

  // INSTANCE
  private static volatile Test21ConstructorExSingleton INSTANCE;

  // disable constructor
  private Test21ConstructorExSingleton() {

  }

  // Test18DoubleCheckSingleton
  public static void main(String[] args) throws Exception {

    Test21ConstructorExSingleton entity01 = Test21ConstructorExSingleton.getInstance();

    // case1 constructor
//    Class<Test21ConstructorExSingleton> clazz = Test21ConstructorExSingleton.class;
//    Constructor<Test21ConstructorExSingleton> constructor = clazz.getDeclaredConstructor();
//    Test21ConstructorExSingleton entity02 = constructor.newInstance();

    // case2 unsafe
    Unsafe unsafe = Test18DoubleCheckSingleton.getUnsafe();
    Test21ConstructorExSingleton entity03 = (Test21ConstructorExSingleton) unsafe.allocateInstance(Test21ConstructorExSingleton.class);

    // case 3 deserialize
//    ByteArrayOutputStream baos = new ByteArrayOutputStream();
//    ObjectOutputStream oos = new ObjectOutputStream(baos);
//    oos.writeObject(entity01);
//    byte[] serialized = baos.toByteArray();
//    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized));
//    Test21ConstructorExSingleton entity04 = (Test21ConstructorExSingleton) ois.readObject();

    int x = 1;

  }

  /**
   * getInstance
   *
   * @return com.hx.test04.Test18DoubleCheckSingleton
   * @author Jerry.X.He<[email protected]>
   * @date 2020-04-01 17:02
   */
  public static Test21ConstructorExSingleton getInstance() {
    if(INSTANCE == null) {
      synchronized (Test21ConstructorExSingleton.class) {
        if(INSTANCE == null) {
          INSTANCE = new Test21ConstructorExSingleton();
        }
      }
    }

    return INSTANCE;
  }

}

/**
 * Test21ConstructorExSingletonParent
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2020-04-01 19:18
 */
class Test21ConstructorExSingletonParent {

  // INSTANCE_CREATED
  private static volatile boolean INSTANCE_CREATED = false;

  public Test21ConstructorExSingletonParent() {
    synchronized (Test21ConstructorExSingletonParent.class) {
      if (INSTANCE_CREATED) {
        throw new RuntimeException("can't instantiate !");
      }

      INSTANCE_CREATED = true;
    }
  }

}

需要一個輔助的 parent 來處理 反序列化的場景[42 不調用給定類的構造方法創建給定類的對象], 因此這裏增加了一個 Test21ConstructorExSingletonParent 來處理, 如果是實例已經創建, 則拋出異常 

然後這裏是吧 反射 和 反序列化 的 hack 都攔住了 

unsafe 是一點辦法也米有, 別人可以只爲對象分配空間 而不初始化 .. 

 

 

case05 枚舉單例構造方法拋出異常

package com.hx.test04;

import sun.misc.Unsafe;

import java.io.*;

/**
 * DoubleCheckSingleton
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2020-04-01 17:01
 */
public enum Test22EnumConstructorExSingleton implements Serializable {

  INSTANCE;

  // disable constructor
  private Test22EnumConstructorExSingleton() {
    synchronized (BeanCreateFlagHolder.class) {
      if(BeanCreateFlagHolder.INSTANCE_CREATED) {
        throw new RuntimeException("can't instantiate !");
      }

      BeanCreateFlagHolder.INSTANCE_CREATED = true;
    }
  }

  // Test18DoubleCheckSingleton
  public static void main(String[] args) throws Exception {

    Test22EnumConstructorExSingleton entity01 = Test22EnumConstructorExSingleton.getInstance();

    // case1 constructor
    // Cannot reflectively create enum objects
//    Class<Test22EnumConstructorExSingleton> clazz = Test22EnumConstructorExSingleton.class;
//    Constructor<Test22EnumConstructorExSingleton> constructor = clazz.getDeclaredConstructor(String.class, int.class);
//
//    Method acquireConstructorAccessorMethod = Constructor.class.getDeclaredMethod("acquireConstructorAccessor");
//    acquireConstructorAccessorMethod.setAccessible(true);
//    acquireConstructorAccessorMethod.invoke(constructor);
//
//    Field constructorAccessorField = Constructor.class.getDeclaredField("constructorAccessor");
//    constructorAccessorField.setAccessible(true);
//    ConstructorAccessor constructorAccessor = (ConstructorAccessor) constructorAccessorField.get(constructor);
//    Test22EnumConstructorExSingleton entity02 = (Test22EnumConstructorExSingleton) constructorAccessor.newInstance(new Object[]{"xyz", 2});
//    Test20EnumSingleton entity02 = constructor.newInstance("xyz", 2);

    // case2 unsafe
    Unsafe unsafe = Test18DoubleCheckSingleton.getUnsafe();
    Test22EnumConstructorExSingleton entity03 = (Test22EnumConstructorExSingleton) unsafe.allocateInstance(Test22EnumConstructorExSingleton.class);

    // case 3 deserialize
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(entity01);
    byte[] serialized = baos.toByteArray();
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized));
    Test22EnumConstructorExSingleton entity04 = (Test22EnumConstructorExSingleton) ois.readObject();

    int x = 1;

  }

  /**
   * getInstance
   *
   * @return com.hx.test04.Test18DoubleCheckSingleton
   * @author Jerry.X.He<[email protected]>
   * @date 2020-04-01 17:02
   */
  public static Test22EnumConstructorExSingleton getInstance() {
    return INSTANCE;
  }

  /**
   * BeanCareatedHodler
   *
   * @author Jerry.X.He <[email protected]>
   * @version 1.0
   * @date 2020-04-01 18:38
   */
  static class BeanCreateFlagHolder {
    // INSTANCE_CREATED
    public static boolean INSTANCE_CREATED = false;
  }

}

呵呵 這個就同上面一樣了 

unsafe 是一點辦法也米有, 別人可以只爲對象分配空間 而不初始化 .. 

 

 

======================= add at 2020.04.25 =======================

 

case06 MagicAccessor  

之前看到這篇帖子的時候, 突然想到了本文的問題 : java 反射調用 private 相關

呵呵 除了反射之外, 也可以使用 MagicAccessor 來 hack 調用 私有的構造方法嘛 

package sun.reflect;

import com.hx.test04.Test18DoubleCheckSingleton;

import java.lang.reflect.InvocationTargetException;

/**
 * MyMethodAccessor
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2020-04-18 20:31
 */
public class MyMethodAccessor01 extends MethodAccessorImpl {
    public MyMethodAccessor01() {
        super();
    }

    @Override
    public Object invoke(Object o, Object[] objects) throws IllegalArgumentException, InvocationTargetException {
        new Test18DoubleCheckSingleton();
        return null;
    }
}

當然 不能直接這麼寫, 因爲 compiler 檢測到 Test18DoubleCheckSingleton 的構造方法不能訪問, 但是 邏輯意義是這樣 

可以 通過一些方式來生成 這個 MethodAccessorImpl 

1. 循環多次調用方法, 讓 jdk 自己自動生成 MethodAccessorImpl 

2. 通過相關字節碼處理的 api 來生成一個這樣的 class 

3. 通過 ClassFileTransformer 來攔截 class, 然後修改 class 

 

 

呵呵 不知道看完之後 您有何感想, 

 

 

 

參考 

42 不調用給定類的構造方法創建給定類的對象

 java 反射調用 private 相關

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章