Одноэлементный шаблон Java с отражением и сериализацией

Java JVM Безопасность

Примечания к шаблону singleton

Я думаю, что все знакомы с шаблоном singleton, поэтому мы не будем обсуждать несколько способов написания шаблона singleton, его преимущества и недостатки. Сегодня мы выделим несколько реализаций синглетонов, чтобы увидеть, как эффективно защищаться от атак отражения и сериализации. Если вы не понимаете рефлексию и сериализацию, вы можете прочитать эти две статьи.
отражение
Сериализация

Паттерн Singleton и отражение

Наиболее фундаментальным в шаблоне синглтона является то, что у класса может быть только один экземпляр. Если экземпляр этого класса создается с помощью отражения, шаблон синглтона будет уничтожен. Давайте посмотрим на пример ниже:

/**
 * 静态内部类式单例模式
 */
class Singleton implements Serializable{
	
	private static class SingletonClassInstance {
	    private static final Singleton instance = new Singleton();
	}
	
	//方法没有同步,调用效率高
	public static Singleton getInstance() {
	    return SingletonClassInstance.instance;
	}
	
	private Singleton() {}
}

Я полагаю, что все знакомы с этой реализацией этого синглтона Давайте посмотрим, разрушит ли создание экземпляра класса с помощью отражения шаблон синглтона. Код основной функции выглядит следующим образом:

Singleton sc1 = Singleton.getInstance();
Singleton sc2 = Singleton.getInstance();
System.out.println(sc1); // sc1,sc2是同一个对象
System.out.println(sc2);

/*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/
Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.learn.example.Singleton");
Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 跳过权限检查
Singleton sc3 = c.newInstance();
Singleton sc4 = c.newInstance();
System.out.println("通过反射的方式获取的对象sc3:" + sc3);  // sc3,sc4不是同一个对象
System.out.println("通过反射的方式获取的对象sc4:" + sc4);

Посмотрим на вывод:

com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
通过反射的方式获取的对象sc3:com.learn.example.Singleton@25154f
通过反射的方式获取的对象sc4:com.learn.example.Singleton@10dea4e

Мы видим, что обычный вызов getInstance соответствует нашим ожиданиям.Если мы используем рефлексию (минуя инспекцию, мы можем вызвать приватную через рефлексию), то режим singleton фактически недействителен.Мы создаем два совершенно разных объекта sc3 и sc4. Как мы можем решить эту проблему? Отражение должно вызывать конструктор, тогда мы можем судить в конструкторе. Код ремонта следующий:

class Singleton implements Serializable{
	
    private static class SingletonClassInstance {
    	private static final Singleton instance = new Singleton();
    }
    
    //方法没有同步,调用效率高
    public static Singleton getInstance() {
    	return SingletonClassInstance.instance;
    }
    
    //防止反射获取多个对象的漏洞
    private Singleton() {
    	if (null != SingletonClassInstance.instance)
    	    throw new RuntimeException();
    }
}

Единственное улучшение, которое мы видим, заключается в том, что в конструктор добавлено суждение, чтобы предотвратить создание объекта отражением путем создания исключения, если в данный момент существует экземпляр. Посмотрим на вывод:

com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.learn.example.RunMain.main(RunMain.java:45)
Caused by: java.lang.RuntimeException
	at com.learn.example.Singleton.<init>(RunMain.java:28)
	... 5 more

Мы видим, что возникает исключение, когда мы создаем объект через отражение.

Шаблон Singleton и сериализация

Помимо отражения процесс десериализации также уничтожит шаблон singleton.Давайте посмотрим на результаты вывода десериализации на этом этапе:

com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@16ec8df

Мы видим, что десериализованный объект и исходный объект sc1 больше не являются одним и тем же объектом. Нам нужно обработать процесс десериализации, и код обработки выглядит следующим образом:

//防止反序列化获取多个对象的漏洞。
//无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
//实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象
private Object readResolve() throws ObjectStreamException {  
    return SingletonClassInstance.instance;
}

Из комментариев также видно, что метод readResolve перезапишет исходный десериализованный объект. Мы отбрасываем исходный десериализованный объект и перезаписываем его уже созданным одноэлементным объектом. Посмотрим на текущий вывод:

com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@52e922

Подробное объяснение метода Readresolve можно увидеть в этой статье:
Метод введения корреляционной последовательности

Реализация синглтона с использованием перечисления

В Effective Java рекомендуется использовать перечисления для реализации синглетонов, потому что перечисления реализуют синглтоны для предотвращения уязвимостей отражения и сериализации.Давайте рассмотрим следующие примеры:

class Resource{}

/**
 * 使用枚举实现单例
 */
enum SingletonEnum{
    INSTANCE;
    
    private Resource instance;
    SingletonEnum() {
        instance = new Resource();
    }
    public Resource getInstance() {
        return instance;
    }
}

Вызываем код в основном методе:

Resource resource1 = SingletonEnum.INSTANCE.getInstance();
Resource resource2 = SingletonEnum.INSTANCE.getInstance();
System.out.println(resource1);
System.out.println(resource2);

Результат выглядит следующим образом:

com.learn.example.Resource@52e922
com.learn.example.Resource@52e922

Мы видим, что реализовали синглтон через перечисление, так как же перечисление гарантирует синглтон (как соблюсти стандарты многопоточности и сериализации)? По сути, перечисление — это обычный класс, который наследуется от класса java.lang.Enum. После того, как мы декомпилируем вышеуказанный файл класса, мы получим следующий код:

public final class SingletonEnum extends Enum<SingletonEnum> {
    public static final SingletonEnum INSTANCE;
    public static SingletonEnum[] values();
    public static SingletonEnum valueOf(String s);
    static {};
}

Из декомпилированного кода видно, что INSTANCE объявлен как static.В процессе загрузки класса мы можем знать, что виртуальная машина обеспечивает правильную блокировку и синхронизацию метода () класса в многопоточной среде. Таким образом, реализация enum является потокобезопасной при создании экземпляра.

Реализация и сериализация enum

Спецификация Java предусматривает, что каждый тип перечисления и его определенные переменные перечисления уникальны в JVM, поэтому в Java предусмотрены специальные условия для сериализации и десериализации типов перечисления.
При сериализации Java просто выводит атрибут имени объекта перечисления в результат, а при десериализации использует метод valueOf() класса java.lang.Enum для поиска объекта перечисления по имени.
То есть, взяв в качестве примера следующее перечисление, во время сериализации выводится только имя INSTANCE, а затем это имя используется для поиска соответствующего типа перечисления во время десериализации, поэтому десериализованный экземпляр также будет таким же, как и раньше. сериализуемый экземпляр объекта одинаков.
Одноэлементный тип перечисления в Effective Java автор считает лучшим способом реализации Singleton.