Java基础、中级、高级、架构面试资料

灭霸所有单例模式,克隆、序列化、反射机制破坏7种单例模式

JAVA herman 3323浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

单例模式实际上也不止 7 种。但是,每一种都并非安全的。今天我给大家讲一讲如何利用克隆、序列化、反射机制破坏单例模式。

我今天以痴汉式单例为例来讲,其他的单例模式破坏方式类似。

public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance(){
        if (singleton == null){
            synchronized (Singleton.class){
                if (singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

上面这个单例实现,看似很完美。但我们通过克隆、序列化、反射机制,来击破这个单例模式。

创建一个 Java 对象一般有 4 种方式:new 、克隆、序列化、反射!现在 new 这种方式不能使用了,那我们还可以使用剩下的 3 种方式!

先来看克隆!

实现 Cloneable 接口,尽管构造函数是私有,但还会创建一个对象。因为 clone 方法不会调用构造函数,会直接从内存中 copy 内存区域。所以单例模式的类是切记不要实现 Cloneable 接口。

public static void main(String[] args) throws CloneNotSupportedException {
    Singleton singleton = getInstance();
    Singleton singleton1 = (Singleton) singleton.clone();
    Singleton singleton2 = getInstance();
    System.out.println(singleton.hashCode());
    System.out.println(singleton1.hashCode());
    System.out.println(singleton2.hashCode());
}

自己运行一下,hash 值不一样,所以克隆成功了,生成了一个新对象。单例模式被成功破坏!

那么怎么抵制被克隆呢?

/**
 * 防止克隆攻击
 * @return
* @throws CloneNotSupportedException
 */
@Override
protected Object clone() throws CloneNotSupportedException {
    return getInstance();
}

就是重写 clone 方法,调用 getInstance() 方法,返回已有的实例即可!

现在我们再来看序列化是如何破坏单例模式的。现在假设你的单例模式,实现了 Serializable 接口。看我下面反序列化的案例!

public static void main(String[] args) throws IOException, ClassNotFoundException {
    Singleton singleton = getSingleton();
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\xttblog.obj"));
    oos.writeObject(singleton);
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:\\xttblog.obj")));
    Singleton singleton1 = (Singleton) ois.readObject();
    System.out.println(singleton.hashCode());
    System.out.println(singleton1.hashCode());
    System.out.println(singleton == singleton1);
}

执行之后,hash 值不一样了,获取的对象非同一对象。结论,单例模式又被破坏了!那么怎么防止被反序列化呢?

很简单,自定义实现对象的 readResolve() 方法。

private Object readResolve() {
    return getSingleton();
}

为什么实现对象的 readResolve() 方法就可以了呢?这个你可以自己 debug 一下,上面反序列化的代码。其中有一个 readOrdinaryObject 方法在做怪!

Class<?> cl = desc.forClass();//获取SingletonIn的Class
if (cl == String.class || cl == Class.class
       || cl == ObjectStreamClass.class) {
    throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {//如果所代表的类是可序列化或者自定义序列化并且可以序列化runtime时候被实例化,则返回true
    obj = desc.isInstantiable() ? desc.newInstance() : null;
//debug在这里通过反射创建了对象
} catch (Exception ex) {
    //省略
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
    handles.markException(passHandle, resolveEx);
}
//是否实现了自定义序列化接口
if (desc.isExternalizable()) {
    readExternalData((Externalizable) obj, desc);
} else {
    readSerialData(obj, desc);//读取持久化的数据并写入对象
}
handles.finish(passHandle);
if (obj != null & handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
//如果所代表对象实现了可序列化或者自定义序列化接口,并且定义了readResolve方法,则返回true
{
    //获取readResolve方法中的对象,并替换obj
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        handles.setObject(passHandle, obj = rep);
    }
}
return obj;//返回返序列化对象

关键代码都注射的比较全,我相信你能看明白。如果还不明白,加我微信ID:xttblog。

最后,我们再来看反射是如何破坏单例模式的!

public static void main(String[] args)
        throws ReflectiveOperationException {
    Class cls = Singleton.class;
    Constructor<Singleton> constructor = cls.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton singleton = constructor.newInstance();
    Singleton singleton1 = getSingletonIn();
    System.out.println(singleton.hashCode());
    System.out.println(singleton1.hashCode());
    System.out.println(singleton == singleton1);
}

执行之后,hash 值不一样了,获取的对象非同一对象。结论,单例模式又被破坏了!那么如何解决呢?很简单,加入下面的代码。

private Singleton() {
    if (null != instance) {
        throw new RuntimeException();
    }
}

因为执行反射会调用无参构造函数,所以上面的判断就可以起作用了!

综上所述,单例模式需要考虑,线程安全问题,效率问题,防止反射、反序列化、克隆。要不然,就有可能被黑客利用!

看到这里,有些人可能会问,这也太麻烦了,有没有更简便的方法呢?有,枚举模式。枚举类型是绝对单例的,可以无责任使用。

public enum Singleton implements Cloneable, Serializable{
    Instance;
    private Singleton() {
        init();
    }
    public void init() {
        //省略
    }
}

一个枚举,就算实现双接口,也是无论如何都无法被破坏的。枚举无法克隆,没有这样的方法。没有构造函数,会抛出异常。就算你在枚举里加了构造函数,也是一样的。对于反序列化 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法。所以,枚举才是实现单例模式的最好方式!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » 灭霸所有单例模式,克隆、序列化、反射机制破坏7种单例模式