中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

深入理解單例模式(下)

2018-08-01    來源:importnew

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

Effective Java》已經(jīng)告訴我們,在單例類中提供一個readResolve方法就可以完成單例特性。這里大家可以自己去測試。

接下來,我們去看看Java提供的反序列化是如何創(chuàng)建對象的!

ObjectInputStream

對象的序列化過程通過ObjectOutputStream和ObjectInputputStream來實現(xiàn)的,那么帶著剛剛的問題,分析一下ObjectInputputStream的readObject 方法執(zhí)行情況到底是怎樣的。

為了節(jié)省篇幅,這里給出ObjectInputStream的readObject的調用棧:

大家順著此圖的關系,去看readObject方法的實現(xiàn)。
首先進入readObject0方法里,關鍵代碼如下:

switch (tc) {
    //省略部分代碼

    case TC_STRING:
    case TC_LONGSTRING:
        return checkResolve(readString(unshared));

    case TC_ARRAY:
        return checkResolve(readArray(unshared));

    case TC_ENUM:
        return checkResolve(readEnum(unshared));

    case TC_OBJECT:
        return checkResolve(readOrdinaryObject(unshared));

    case TC_EXCEPTION:
        IOException ex = readFatalException();
        throw new WriteAbortedException("writing aborted", ex);

    case TC_BLOCKDATA:
    case TC_BLOCKDATALONG:
        if (oldMode) {
            bin.setBlockDataMode(true);
            bin.peek();             // force header read
            throw new OptionalDataException(
                bin.currentBlockRemaining());
        } else {
            throw new StreamCorruptedException(
                "unexpected block data");
        }

    //省略部分代碼

這里就是判斷目標對象的類型,不同類型執(zhí)行不同的動作。我們的是個普通的Object對象,自然就是進入case TC_OBJECT的代碼塊中。然后進入readOrdinaryObject方法中。
readOrdinaryObject方法的代碼片段:

private Object readOrdinaryObject(boolean unshared)
        throws IOException {
    //此處省略部分代碼

    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }

    //此處省略部分代碼

    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

重點看代碼塊:

 Object obj;
 try {
      obj = desc.isInstantiable() ? desc.newInstance() : null;
  } catch (Exception ex) {
      throw (IOException) new InvalidClassException(
          desc.forClass().getName(),
          "unable to create instance").initCause(ex);
  }

這里創(chuàng)建的這個obj對象,就是本方法要返回的對象,也可以暫時理解為是ObjectInputStream的readObject返回的對象。

isInstantiable:如果一個serializable/externalizable的類可以在運行時被實例化,那么該方法就返回true。針對serializable和externalizable我會在其他文章中介紹。?
desc.newInstance:該方法通過反射的方式調用無參構造方法新建一個對象。

所以。到目前為止,也就可以解釋,為什么序列化可以破壞單例了?即序列化會通過反射調用無參數(shù)的構造方法創(chuàng)建一個新的對象。

接下來再看,為什么在單例類中定義readResolve就可以解決該問題呢?還是在readOrdinaryObjec方法里繼續(xù)往下看。

if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
     if (unshared && rep.getClass().isArray()) {
         rep = cloneArray(rep);
     }
     if (rep != obj) {
         handles.setObject(passHandle, obj = rep);
     }
}

這段代碼也很清楚地給出答案了!
如果目標類有readResolve方法,那就通過反射的方式調用要被反序列化的類的readResolve方法,返回一個對象,然后把這個新的對象復制給之前創(chuàng)建的obj(即最終返回的對象)。那readResolve?方法里是什么?就是直接返回我們的單例對象。

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { 
        System.err.println("Elvis Constructor is invoked!");
    }

    private Object readResolve() {
       return INSTANCE;
    }
}

所以,原理也就清楚了,主要在Singleton中定義readResolve方法,并在該方法中指定要返回的對象的生成策略,就可以防止單例被破壞。

單元素枚舉類型

第三種實現(xiàn)單例的方式是,聲明一個單元素的枚舉類:

// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}

這個方法跟提供公有的字段方法很類似,但它更簡潔,提供天然的可序列化機制和能夠強有力地保證不會出現(xiàn)多次實例化的情況 ,甚至面對復雜的序列化和反射的攻擊下。這種方法可能看起來不太自然,但是擁有單元素的枚舉類型可能是實現(xiàn)單例模式的最佳實踐。注意,如果單例必須要繼承一個父類而非枚舉的情況下是無法使用該方式的(不過可以聲明一個實現(xiàn)了接口的枚舉)。
我們分析一下,枚舉類型是如何阻止反射來創(chuàng)建實例的?直接源碼:
看Constructor類的newInstance方法。

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

這行代碼(clazz.getModifiers() & Modifier.ENUM) != 0?就是用來判斷目標類是不是枚舉類型,如果是拋出異常IllegalArgumentException("Cannot reflectively create enum objects"),無法通過反射創(chuàng)建枚舉對象!很顯然,反射無效了。

接下來,再看一下反序列化是如何預防的。依然按照上面說的順序去找到枚舉類型對應的readEnum方法,如下:

private Enum<?> readEnum(boolean unshared) throws IOException {
    if (bin.readByte() != TC_ENUM) {
        throw new InternalError();
    }

    ObjectStreamClass desc = readClassDesc(false);
    if (!desc.isEnum()) {
        throw new InvalidClassException("non-enum class: " + desc);
    }

    int enumHandle = handles.assign(unshared ? unsharedMarker : null);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(enumHandle, resolveEx);
    }

    String name = readString(false);
    Enum<?> result = null;
    Class<?> cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            Enum<?> en = Enum.valueOf((Class)cl, name);
            result = en;
        } catch (IllegalArgumentException ex) {
            throw (IOException) new InvalidObjectException(
                "enum constant " + name + " does not exist in " +
                cl).initCause(ex);
        }
        if (!unshared) {
            handles.setObject(enumHandle, result);
        }
    }

    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

readString(false):首先獲取到枚舉對象的名稱name。
Enum<?> en = Enum.valueOf((Class)cl, name):再指定名稱的指定枚舉類型獲得枚舉常量,由于枚舉中的name是唯一,切對應一個枚舉常量。所以我們獲取到了唯一的常量對象。這樣就沒有創(chuàng)建新的對象,維護了單例屬性。

看看Enum.valueOf?的JavaDoc文檔:

  • 返回具有指定名稱的指定枚舉類型的枚舉常量。?該名稱必須與用于聲明此類型中的枚舉常量的標識符完全匹配。 (不允許使用無關的空白字符。)

具體實現(xiàn):

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

enumConstantDirectory():返回一個Map,維護著名稱到枚舉常量的映射。我們就是從這個Map里獲取已經(jīng)聲明的枚舉常量,通過這個緩存池一樣的組件,讓我們可以重用這個枚舉常量!

總結

  1. 常見的單例寫法有他的弊端,存在安全性問題,如:反射,序列化的影響。
  2. 《Effective Java》作者Josh Bloch 提倡使用單元素枚舉類型的方式來實現(xiàn)單例,首先創(chuàng)建一個枚舉很簡單,其次枚舉常量是線程安全的,最后有天然的可序列化機制和防反射的機制。

參考

  • 《單例模式的七種寫法》
  • 《單例與序列化的那些事兒》
  • 《Effective Java》

標簽: idc ssd 安全 代碼

版權申明:本站文章部分自網(wǎng)絡,如有侵權,請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:SpringBoot | 第三章:springboot配置詳解

下一篇:別罵了,這次通過?Microsoft Store 推送系統(tǒng)更新信息