Effective Java 3.用私有构造器或者枚举类型强化 Singleton 属性

194 阅读4分钟

Singleton 是指只能被实例化一次的类。书中实现单例的方法有两种。两者都基于构造器私有,提供公共静态成员变量作为唯一实例的访问。并且该实例被final修饰。并且书中的两种方式均为饿汉式

方式1:公开的静态成员变量,类名.成员变量名获取

public class Singleton {
    public static final Singleton INSTANCE = new Singleton();

    public void test(){
        System.out.println("this is test method...");
    }
	
	private Singleton() {}

    public static void main(String[] args) {
        Singleton.INSTANCE.test();
    }
}

私有构造器仅被调用一次,用来实例化公有的静态final对象INSTANCE。由于构造器不公开,保证了实例的全局唯一。但这种方式可以被反射破坏,如果需要抵御这种攻击,可以修改构造器,让它在被创建第二个实例的时候抛异常

方式2:私有静态成员变量,提供唯一获取实例的静态工厂方法

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton(){}

    public void test() {
        System.out.println("this is test method...");
    }
    public static Singleton getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) {
        Singleton.getInstance().test();
    }
}

这种方法的优势在于,灵活性:在不改变API的前提下,可以改变该类是否应该为 Singleton 的想法。工厂方法返回该类的唯一实例,但是,很容易被修改,比如改为每个调用该方法的线程返回一个唯一的实例。 第二个优势在于如果程序需要,可以编写一个泛型的单例工厂。 最后一个优势是可以通过方法引用作为提供者,比如Singleton::getInstance就是一个Supplier<Singleton>。(这一点感觉优势不是明显,难以理解) 除非满足以上任意一种优势,否则还是优先考虑第一种方式

为了让上述方式实现的单例类变成可以序列化的。仅仅实现Serializable接口是不够的。为了保证单例,必须声明所有实例都是瞬时(transient)的。 在类上实现Serializable接口,它就不再是一个单例。无论该类使用了怎样的序列化方式,也与它是否提供显式的readObject方法无关。任何一个readObject方法,都会返回一个新建的实例,这个新建实例和类初始化创建的实例不同 在Java中,利用ObjectInputStream的readObject方法进行对象读取时,当目标对象已经重写了readObject方法,则会执行目标对象readObject方法

public class DeserializeAttack implements Serializable {
    //该方法会打开您电脑的计算器
    private void readObject(ObjectInputStream stream)
            throws IOException, ClassNotFoundException {
        Runtime.getRuntime().exec("calc");
        System.out.println("readObject method is running...");
    }

    public static byte[] serialize(Object o) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            new ObjectOutputStream(bos).writeObject(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bos.toByteArray();
    }

    public static Object deserialize(byte[] arr) {
        try {
            return new ObjectInputStream(new ByteArrayInputStream(arr)).readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        DeserializeAttack attack = new DeserializeAttack();
        byte[] serialize = serialize(attack);
        Object deserialize = deserialize(serialize);
    }
}

可见在反序列化的过程中,如果目标对象的readObject进行了一些更复杂的操作的时候,那么极有可能给恶意代码提供可乘之机。

readResolve特性允许用readObject创建的实例代替另一个实例。对一个正在被反序列化的对象,如果类中正确定义了readResolve方法(看下面代码),那么在反序列化后,新建对象的readResolve方法就会被调用,该方法返回的对象引用就会取代新建的对象。简单来说就保留了原来的对象,没有返回新创建的对象

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

    private Object readResolve() {
        return INSTANCE;
    }

    private Object readObject(byte[] arr) throws IOException, ClassNotFoundException {
        System.out.println("field Singleton method is running...");
        return new ObjectInputStream(new ByteArrayInputStream(arr))
                .readObject();
    }

    private ForReadResolve() {
    }

    public static byte[] serialize(Object o) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            new ObjectOutputStream(bos).writeObject(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bos.toByteArray();
    }

    public static Object deserialize(byte[] arr) {
        try {
            return new ObjectInputStream(new ByteArrayInputStream(arr)).readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public void test() {
        System.out.println("this is test method...");
    }

    public static void main(String[] args) {
        ForReadResolve instance = ForReadResolve.INSTANCE;
        byte[] arr = serialize(instance);
        ForReadResolve o = (ForReadResolve) deserialize(arr);
        // true
        System.out.println(o == instance);
    }
}

而删除readResolve方法就会返回false.如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域(成员变量么?)都必须声明为transient。否则,攻击者有可能在readResolve方法被运行前,保护指向反序列化对象的引用

方式3:声明一个包含单个元素的枚举类型:

public enum EnumSingleton implements Serializable {
    /**
     * 实例
     */
    INSTANCE;

    EnumSingleton() {
    }

    private Object readObject(byte[] arr) throws IOException, ClassNotFoundException {
        System.out.println("枚举类型单例序列化 method is running...");
        return new ObjectInputStream(new ByteArrayInputStream(arr))
                .readObject();
    }

    public void test() {
        System.out.println("this is test method...");
    }

    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.INSTANCE;
        instance.test();
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        System.out.println(instance1 == instance);
    }
}

这种方式在功能上与公开静态成员变量的方法相似,但更简洁,并且可以防御反序列化的攻击单元素的枚举类型经常成为实现单例的最佳方法,但是需要注意,如果单例类必须有多个父类,虽然可以声明枚举去实现接口,则不宜使用这种方式