概述:
- 之前学习了单例模式的几种实现,解决了多线程情况下,单例的线程安全问题,保证了单例的实现。但是单例模式在下面两种情况下也会被破坏:反射、序列化。
反射:
- 通过反射是可以破坏单例的,例如使用内部类实现的单例。通过反射获取其默认的构造函数,然后使默认构造函数可访问,就可以创建新的对象了。如下面代码:
public class MainClass {
public static void main(String[] args) {
PersonLazyInnerClass person1 = PersonLazyInnerClass.getPersonLazyInnerClass();
PersonLazyInnerClass person2 = null;
try {
Class<PersonLazyInnerClass> cla = PersonLazyInnerClass.class;
//获得默认构造函数
Constructor<PersonLazyInnerClass> cons = cla.getDeclaredConstructor();
//使默认构造函数可访问
cons.setAccessible(true);
//创建对象
person2 = cons.newInstance();
}catch (Exception e){
e.printStackTrace();
}
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
}
}
-
测试结果
所以想要阻止破坏单例,思路是阻止外部能调用类的构造函数一次以上。所以可以增加一个标志位,用于判断构造函数是否被调用过。如下代码:
public class PersonLazyInnerClassSafe {
//申明一个标志位,用于标志构造函数是否被调用过
private static Boolean alreadyNew = false;
private PersonLazyInnerClassSafe(){
//加锁防止并发
synchronized (PersonLazyInnerClassSafe.class){
//第一次被调用,仅修改标志位;后续被调用抛异常
if(alreadyNew == false){
alreadyNew = true;
}else {
throw new RuntimeException("单例模式被破坏!");
}
}
}
private static class Holder{
private static PersonLazyInnerClassSafe personLazyInnerClassSafe = new PersonLazyInnerClassSafe();
}
public static PersonLazyInnerClassSafe getInstance(){
return Holder.personLazyInnerClassSafe;
}
}
-
测试结果
- 增加标志位的确能阻止单例的破坏,但是这个代码有一个BUG,那就是如果单例是先用的反射创建的,那如果你再用正常的方法getInstance()获取单例,就会报错。因为此时标志位已经标志构造函数被调用过了。这种写法除非你能保证getInstance先于反射执行。
- 先试正常执行代码,此时是可以正确返回单例。这里简单解释一下,person2执行的时候不会去调用PersonLazyInnerClassSafe类的构造函数。因为Holder内部类里面personLazyInnerClassSafe属性是静态的,静态属性在类加载的时候就初始化一次,在生命周期内就不会再被初始化了。
public class MainClass {
public static void main(String[] args) {
//正常执行代码
PersonLazyInnerClassSafe person1 = PersonLazyInnerClassSafe.getInstance();
PersonLazyInnerClassSafe person2 = PersonLazyInnerClassSafe.getInstance();
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
}
}
- 先反射后正常调用,此时会发现正常调用没法用了。
public class MainClass {
public static void main(String[] args) {
//先反射获取单例
PersonLazyInnerClassSafe person2 = null;
try {
Class<PersonLazyInnerClassSafe> cla = PersonLazyInnerClassSafe.class;
//获得默认构造函数
Constructor<PersonLazyInnerClassSafe> cons = cla.getDeclaredConstructor();
//使默认构造函数可访问
cons.setAccessible(true);
//调用默认构造函数,创建对象
person2 = cons.newInstance();
}catch (Exception e){
e.printStackTrace();
}
System.out.println("反射对象: " + person2.hashCode());
//在用正常的方法获取单例,此时会报错
PersonLazyInnerClassSafe person1 = PersonLazyInnerClassSafe.getInstance();
System.out.println("正常获取" + person1.hashCode());
}
}
-
测试结果
反序列化:
1. 破坏单例
- 反序列化也是一种会破坏单例的方法。简单来讲,反序列化也是通过反射调用newInstance()实例化对象,下面我一步步解释。例如下面代码可以通过反序列化破坏单例:
public class MainClass {
public static void main(String[] args) {
//序列化
PersonLazyInnerClassSafe person3 = PersonLazyInnerClassSafe.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempfile"));
out.writeObject(PersonLazyInnerClassSafe.getInstance());
File file = new File("tempfile");
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
//调用readObject()反序列化
PersonLazyInnerClassSafe person4 = (PersonLazyInnerClassSafe)in.readObject();
System.out.println("正常构造:" + person3.hashCode());
System.out.println("反序列化Person:" + person4.hashCode());
}
}
-
测试结果
可以看到,反序列化后,生成了一个新的实例。
- 原理解释
反序列化为什么能生成新的实例,必须从源码看起。这里分析readObject()里面的调用源码。会发现readObject()方法后进入了readObject0(false)方法。
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false); //通过debug会发现进入此方法
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
分析readObject0方法,会发现进入了readOrdinaryObject()方法。
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
try {
switch (tc) {
...省略部分源码
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");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
- 通过分析,readOrdinaryObject()中有两处关键代码,其中关键代码1中的关键语句为:
obj = desc.isInstantiable() ? desc.newInstance() : null;
- 此处代码是通过描述对象desc,先判断类是否可以实例化,如果可以实例化,则执行desc.newInstance()通过反射实例化类,否则返回null。
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false); //根据对象的格式,下一步是读取类描述信息
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class //读取Class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
/**===============关键代码1====================== **/
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null; //通过类描述信息,初始化对象obj。
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
/**===============关键代码1====================== **/
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);
/**===============关键代码2====================== **/
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);
}
}
/**===============关键代码2====================== **/
return obj;
}
- 此处通过debug可以发现,类是可实例化的,所以执行desc.newInstance() 去实例化对象,此处就是重点,让我疑问的地方了。我们知道newInstance必定会调用默认的无参构造函数。但是我们在上面PersonLazyInnerClassSafe单例类已经禁止了反射创建新实例,但是反序列化还是能创建出新实例,那么此处的反序列化中的反射具体是如何执行的呢?继续分析newInstance()源码。
- newInstance()的源码如下所示,可以发现,程序会自动加载合适的构造函数cons,然后再去根据这个构造函数去cons.newInstance()创建对象。通过debug发现,此处的cons对象是Object,真像大白了。所以反序列化是通过Object的构造函数去反射,生成了新的实例。
/** serialization-appropriate constructor, or null if none */
private Constructor<?> cons;
...省略部分源码
Object newInstance()
throws InstantiationException, InvocationTargetException,
UnsupportedOperationException
{
requireInitialized();
if (cons != null) {
try {
return cons.newInstance();
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
-
此处可以给大家看下debug 的截图
- 然后我改写了上面的测试序列化破坏单例的代码,验证一下反序列化是通过Object的构造函数去反射,生成了新的实例,具体代码如下
public class MainClass {
public static void main(String[] args) {
//验证一下反序列化是通过Object的构造函数去反射,生成了新的实例
PersonLazyInnerClassSafe person3 = PersonLazyInnerClassSafe.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempfile"));
out.writeObject(PersonLazyInnerClassSafe.getInstance());
File file = new File("tempfile");
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
//反序列化生成的Object对象
Object ob = in.readObject();
//将Object强转为PersonLazyInnerClassSafe
PersonLazyInnerClassSafe person4 = (PersonLazyInnerClassSafe)ob;
System.out.println("正常构造:" + person3.hashCode());
System.out.println("反序列化Object:" + ob.hashCode());
System.out.println("反序列化Person:" + person4.hashCode());
}
}
- 其中会发现反序列化出来的Object对象强制转换为PersonLazyInnerClassSafe时,只是把ob 对象的引用指向了person4 ,并没有调用PersonLazyInnerClassSafe 的构造函数。
//反序列化生成的Object对象
Object ob = in.readObject();
//将Object强转为PersonLazyInnerClassSafe
PersonLazyInnerClassSafe person4 = (PersonLazyInnerClassSafe)ob;
-
运行结果如下,可以看到ob和person4 是相同的对象:
2. 阻止破坏单例
- 上面解释了那么多序列化是如何通过反射破坏的单例,那么如何阻止单例被序列化破坏呢?关键点在上面readOrdinaryObject()方法源码的关键代码2中,下面单独截取该段代码:
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) //判断对象是否实现readResolve方法
{
Object rep = desc.invokeReadResolve(obj); //反射调用对象的readResolve方法
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) { //如果对象readResolve返回的对象与 默认序列化对象不等,返回readResolve方法返回的对象
handles.setObject(passHandle, obj = rep);
}
}
- 通过阅读上面代码的注释,我们即可知道,只需要实现我们自己的readResolve()方法,在这个方法中返回我们想要的单例,就可以解决序列化破坏单例。具体改写如下:
public class PersonLazyInnerClassSafe implements Serializable{
//申明一个标志位,用于标志构造函数是否被调用过
public static Boolean alreadyNew = false;
private PersonLazyInnerClassSafe(){
System.out.println("调用构造函数!!!");
//加锁防止并发
synchronized (PersonLazyInnerClassSafe.class){
//第一次被调用,仅修改标志位;后续被调用抛异常
if(alreadyNew == false){
alreadyNew = true;
}else {
throw new RuntimeException("单例模式被破坏!");
}
}
}
private static class Holder{
private static PersonLazyInnerClassSafe personLazyInnerClassSafe = new PersonLazyInnerClassSafe();
}
public static PersonLazyInnerClassSafe getInstance(){
return Holder.personLazyInnerClassSafe;
}
//阻止序列化破坏单例
private Object readResolve(){
return Holder.personLazyInnerClassSafe;
}
}
-
测试如下,成功解决单例被破坏问题
总结:
- 破坏单例有两种方式 反射、反序列化
- 反射破坏的原理是:通过反射获取其默认的构造函数,并且改变其构造函数的访问域,从而实现调用构造函数创建新实例。解决方案是:在构造函数中增加一个标志位,用于判断构造函数是否被调用过,阻止外部能调用类的构造函数一次以上。
- 反序列化破坏构造函数的原理:通过Object的构造函数,反射出单例类对象,从而创建了新的实例。解决方案是:在单例类中写一个readResolve()方法,在这个方法中返回我们想要的单例,就可以解决序列化破坏单例。