「源码分析」— 为什么枚举是单例模式的最佳方法
1. 引言
枚举类型(enum type)是在 Java 1.5 中引入的一种新的引用类型,是由 Java 提供的一种语法糖,其本质是 int 值。关于其用法之一,便是单例模式,并且在《Effective Java》中有被提到:
单元素的枚举类型已经成为实现 Singleton 的最佳方法
本文便是探究 “为什么枚举是单例模式的最佳方法?”。
答案先写在前面,两个字:“简单”。
public enum EnumSingleton {
INSTANCE;
}
Java 在我们使用它的同时,解决了危害单例模式安全性的两个问题:“反射攻击” * 和 *“反序列化攻击”。
本文的内容概要如下:
- 回顾常见的单例模式方法;
- 探索 Java 中的枚举是如何防止两种攻击;
- 若不使用枚举,又如何防止两种攻击。
2. 常见单例模式方法
本小节将回顾下常见的单例模式方法,熟悉的同学可以直接跳过这节。
(1)懒汉式
特点:懒加载,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
(2)饿汉式
特点:提前加载,线程安全
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
(3)双重校验锁
特点:懒加载,线程安全
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
(4)静态内部类
特点:懒加载,线程安全
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3. 防止反射攻击
从第 2 节中列举的常用单例模式方法,可看出这些方法具有共同点之一是私有的构造函数。这是为了防止在该类的外部直接调用构建函数创建对象了。但是该做法却无法防御反射攻击:
public class ReflectAttack {
public static void main(String[] args) throws Exception {
Singleton instance = Singleton.getInstance();
// 获取无参的构造函数
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
// 使用构造函数创建对象
constructor.setAccessible(true);
Singleton reflectInstance = constructor.newInstance();
System.out.println(instance == reflectInstance);
}
}
// output:
// false
下面我们反射攻击枚举类型:
public class ReflectAttack {
public static void main(String[] args) throws Exception {
EnumSingleton instance = EnumSingleton.INSTANCE;
// 获取无参的构造函数
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
// 使用构造函数创建对象
constructor.setAccessible(true);
EnumSingleton reflectInstance = constructor.newInstance();
System.out.println(instance == reflectInstance);
}
}
// output:
// Exception in thread "main" java.lang.NoSuchMethodException: com.chaycao.java.EnumSingleton.<init>()
// at java.lang.Class.getConstructor0(Class.java:3082)
// at java.lang.Class.getDeclaredConstructor(Class.java:2178)
// at com.chaycao.java.ReflectAttack.main(ReflectAttack.java:14)
报了 NoSuchMethodException
异常,是由于 EnumSingleton
中没有无参构造器,那枚举类中的构造函数是怎么样的?
Java 生成的枚举类都会继承 Enum
抽象类,其只有一个构造函数:
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
// name: 常量的名称
// ordinal: 常量的序号(枚举声明中的位置,从0开始递增)
// 若以 EnumSingleton 的 INSTANCE 常量为例:
// name = “INSTANCE”;ordinal = 0
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
}
那我们修改下 getDeclaredConstructor
方法的参数,重新获取构造函数试下:
public class ReflectAttack {
public static void main(String[] args) throws Exception {
EnumSingleton instance = EnumSingleton.INSTANCE;
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingleton reflectInstance = constructor.newInstance("REFLECT_INSTANCE", 1);
System.out.println(instance == reflectInstance);
}
}
// output:
// Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
// at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
// at com.chaycao.java.ReflectAttack.main(ReflectAttack.java:16)
这次虽然成功获取到了构造函数,但是仍然报错,并提示我们不能反射创建枚举对象。
错误位于 Constructor
的 newInstance
方法,第 417 行,代码如下:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
如果该类是 ENUM
类型,则会抛出 IllegalArgumentException
异常,便也阻止了反射攻击。
4. 防止反序列化攻击
下面是对于常用方法的反序列化攻击:
public class DeserializeAttack {
public static void main(String[] args) throws Exception {
Singleton instance = Singleton.getInstance();
byte[] bytes = serialize(instance);
Object deserializeInstance = deserialize(bytes);
System.out.println(instance == deserializeInstance);
}
private static byte[] serialize(Object object) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
}
private static Object deserialize(byte[] bytes) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}
}
// output:
// false
无法阻止反序列攻击,可以成功创建出两个对象。我们改成枚举类型试下:
public class DeserializeAttack {
public static void main(String[] args) throws Exception {
EnumSingleton instance = EnumSingleton.INSTANCE;
byte[] bytes = serialize(instance);
Object deserializeInstance = deserialize(bytes);
System.out.println(instance == deserializeInstance);
}
//....
}
// true
反序列得到的仍是同样的对象,这是为什么,下面深入 ObjectOutputStream
的序列化方法看下 Enum
类型的序列化内容,顺着 writeobject
方法找到 writeObject0
方法。
// ObjectOutputStream > writeobject0()
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
}
对于 Enum
类型将执行专门的 writeEnum
方法进行序列化,该方法内容如下:
private void writeEnum(Enum<?> en,
ObjectStreamClass desc,
boolean unshared) throws IOException
{
// 1. ENUM类型标志(常量):“126”
bout.writeByte(TC_ENUM);
ObjectStreamClass sdesc = desc.getSuperDesc();
// 2. 完整类名:“com.chaycao.java.EnumSingleton: static final long serialVersionUID = 0L;”
writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
handles.assign(unshared ? null : en);
// 3. Enum对象的名称:“INSTANCE”
writeString(en.name(), false);
}
从上述代码已经可以看出 EnumSingleton.INSTANCE
的反序列化内容。
接着我们再来观察 Enum
类型的反序列化, ObjectInputStream
与 ObjectOutputStream
类似,对于 Enum
类型也使用专用的 readEnum
方法:
private Enum<?> readEnum(boolean unshared) throws IOException {
// 1. 检查标志位
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
// 2. 检查类名是否是Enum类型
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;
// 3. 加载类,并使用类的valueOf方法获取Enum对象
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;
}
其过程对应了之前的序列化过程,而其中最重要的便是 Enum.valueOf
方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
// name = "INSTANCE"
// 根据名称查找
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);
}
根据名称查找对象,再返回,所以仍会返回 EnumSingleton
中的 INSTANCE
,不会存在反序列化的危险。
综上所述,可知枚举类型在 Java 中天生就不惧怕反射和反序列化的攻击,这是由 Java 自身提供的逻辑保证。那第 2 节中所提及的单例模式方法,是否也有办法能防止反射和反序列攻击?
5.非枚举的防守方法
本节以懒汉式为例,其他单例模式方法同样适用。
(1)防止反射
增加一个标志变量,在构造函数中检查是否已被调用过,若已被调用过,将抛出异常,保证构造函数只被调用一次:
public class Singleton {
private static Singleton instance;
private static boolean isInstance = false;
private Singleton() {
synchronized (Singleton.class) {
if (!isInstance) {
isInstance = true;
} else {
throw new RuntimeException("单例模式受到反射攻击!已成功阻止!");
}
}
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
(2)防止序列化
增加一个 readResolve
方法并返回 instance
对象。当 ObjectInputStream
类反序列化时,如果对象存在 readResolve
方法,则会调用该方法返回对象。
public class Singleton implements Serializable {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Object readResolve() {
return instance;
}
}
6. 小结
由于 Java 的特殊处理,为枚举防止了反射、序列化攻击,我们可以直接使用枚举,不用担心单例模式的安全性,十分便利。但同时我们也需要记住反射攻击和序列化攻击的存在。
- ASP.NET MVC 2 转换工具
- 使用Sysinternals工具定时休眠Windows Server 2008 R2
- Android中BroadcastReceiver广播
- 启用Windows 7/2008 R2 XPS Viewer
- Spring历史版本变迁和如今的生态帝国
- Android中Services之异步IntentService
- 使用GitHub搭建个人博客
- 这个用来玩儿游戏的算法,是谷歌收购DeepMind的最大原因
- asp.net安全检测工具 --Padding Oracle 检测
- Android中Services简析
- VUE 入门基础(2)
- VUE 入门基础(1)
- AndroidManifest.xml配置文件 android.theme大全权限设置Android Permission中英对照
- Reactive框架:简化异步及事件驱动编程
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Go语言(二十)日志采集项目(二)Etcd的使用
- prometheus入门(一)
- Go语言(十九)日志采集项目之logagent开发(一)
- Go语言(十 八)context&日志项目
- 使用梯度上升欺骗神经网络,让网络进行错误的分类
- Go语言(十七) 配置文件库项目
- Python 相对路径问题:“No such file or directory“
- 基于etcd服务发现的overlay跨多宿主机容器网络
- Go语言(十六) 日志项目升级
- PyQt5 技术篇-设置窗口相对桌面位置,按屏幕比例
- Go语言(十五) 反射
- SpringBoot应用跨域访问解决方案
- Spring Boot 2.2都有哪些新变化
- Go语言(十四)日志项目
- 如何在Spring Boot中使用Cookies