博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入理解单例模式
阅读量:7209 次
发布时间:2019-06-29

本文共 6385 字,大约阅读时间需要 21 分钟。

ObjectInputStream

对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream的readObject 方法执行情况到底是怎样的。

为了节省篇幅,这里给出ObjectInputStream的readObject的调用栈:

大家顺着此图的关系,去看readObject方法的实现。

首先进入readObject0方法里,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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"
);
}
//省略部分代码

这里就是判断目标对象的类型,不同类型执行不同的动作。我们的是个普通的Object对象,自然就是进入case TC_OBJECT的代码块中。然后进入readOrdinaryObject方法中。

readOrdinaryObject方法的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}

重点看代码块:

1
2
3
4
5
6
7
8
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);
}

这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。

isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。

desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。

所以。到目前为止,也就可以解释,为什么序列化可以破坏单例了?即序列化会通过反射调用无参数的构造方法创建一个新的对象

接下来再看,为什么在单例类中定义readResolve就可以解决该问题呢?还是在readOrdinaryObjec方法里继续往下看。

1
2
3
4
5
6
7
8
9
10
11
12
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方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)。那readResolve 方法里是什么?就是直接返回我们的单例对象。

1
2
3
4
5
6
7
8
9
10
11
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方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

单元素枚举类型

第三种实现单例的方式是,声明一个单元素的枚举类:

1
2
3
4
5
// Enum singleton - the preferred approach
public
enum
Elvis {
INSTANCE;
public
void
leaveTheBuilding() { ... }
}

这个方法跟提供公有的字段方法很类似,但它更简洁,提供天然的可序列化机制和能够强有力地保证不会出现多次实例化的情况 ,甚至面对复杂的序列化和反射的攻击下。这种方法可能看起来不太自然,但是拥有单元素的枚举类型可能是实现单例模式的最佳实践。注意,如果单例必须要继承一个父类而非枚举的情况下是无法使用该方式的(不过可以声明一个实现了接口的枚举)。

我们分析一下,枚举类型是如何阻止反射来创建实例的?直接源码:
看Constructor类的newInstance方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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"),无法通过反射创建枚举对象!很显然,反射无效了。

接下来,再看一下反序列化是如何预防的。依然按照上面说的顺序去找到枚举类型对应的readEnum方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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是唯一,切对应一个枚举常量。所以我们获取到了唯一的常量对象。这样就没有创建新的对象,维护了单例属性。

看看Enum.valueOf 的JavaDoc文档:

  • 返回具有指定名称的指定枚举类型的枚举常量。 该名称必须与用于声明此类型中的枚举常量的标识符完全匹配。 (不允许使用无关的空白字符。)

具体实现:

1
2
3
4
5
6
7
8
9
10
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里获取已经声明的枚举常量,通过这个缓存池一样的组件,让我们可以重用这个枚举常量!

总结

  1. 常见的单例写法有他的弊端,存在安全性问题,如:反射,序列化的影响。
  2. 《Effective Java》作者Josh Bloch 提倡使用单元素枚举类型的方式来实现单例,首先创建一个枚举很简单,其次枚举常量是线程安全的,最后有天然的可序列化机制和防反射的机制。

转载地址:http://eylum.baihongyu.com/

你可能感兴趣的文章
html知识点总结
查看>>
博客园
查看>>
Three.js加载3D模型
查看>>
Basic: Fisher's transform
查看>>
mint-ui vue双向绑定
查看>>
Eclipse快捷键一览表(转载)
查看>>
随机访问流 RandomAccessFile
查看>>
图片上传读取代码
查看>>
服务器安装环境基本操作
查看>>
nslookup命令
查看>>
c++函数模板作为类的成员函数,编译报错LNK2019的解决方法
查看>>
Tensorflow安装
查看>>
判断个十百千位之后是否大于20 java——CSDN博客
查看>>
.Net插件编程模型:MEF和MAF[转载]
查看>>
java实现excel的导入导出(poi详解)[转]
查看>>
[转载]浅析jQuery框架与构造对象
查看>>
微信小程序基本入门
查看>>
oracl 数字型函数
查看>>
Q443 压缩字符串
查看>>
Bootstrap——网站添加字体图标
查看>>