破坏单例模式

上一章节,介绍了单例模式的几种方式,这次来学习一波我们创建的单例模式是否安全,能不能破坏。换句话说,也就是在程序运行中,不止有一个实例。

一. 序列化,反序列化破坏

以饿汉式的单例模式为例,先看下面的代码:

/**
* @program: designModel
* @description: 饿汉式,与懒汉式最大的区别,就是延时加载,但是饿汉式如果不用该实例,会占用资源
* @author: YuKai Fan
* @create: 2018-12-04 16:57
**/
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton; static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() { } }
public static HungrySingleton getInstance() {
return hungrySingleton;
} }
/**
* @program: designModel
* @description:
* @author: YuKai Fan
* @create: 2018-12-04 14:07
**/
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException { HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance); File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); HungrySingleton newInstance = (HungrySingleton)ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}

上面这段代码的输出结果是

可以看出,产生了两种不同的实例,并输出false。

那么为什么会这样,在输出流,和输入流的过程中,会将类进行序列化,但是输出的实例与输入的实例确实不一样的,这样单例模式就别破坏了。

现在改动一下上面的饿汉式单例模式代码,添加一个Object类型的readResolve方法,然后返回这个实例:

/**
* @program: designModel
* @description: 饿汉式,与懒汉式最大的区别,就是延时加载,但是饿汉式如果不用该实例,会占用资源
* @author: YuKai Fan
* @create: 2018-12-04 16:57
**/
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton; static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
if (hungrySingleton != null) {
throw new RuntimeException("单例构造器禁止反射调用");
} }
public static HungrySingleton getInstance() {
return hungrySingleton;
} private Object readResolve() {
return hungrySingleton;
}
}

在运行一下,得到的结果为:

可以看到,结果是true,只存在一个实例。

这其中的原理需要来解读ObjectInputStream的readObject()  源码才能知晓。

//ObjectInputStream中的readObject方法,会根据实例类中的是否存在readResolve方法,来返回最终的实例是原来的,还是创建新的

        方法readObject:
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
} 方法readObject0:
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared)); 方法readOrdinaryObject:
Object obj;
try {
//判断实例类是不是序列化的
obj = desc.isInstantiable() ? desc.newInstance() : null;
}
//如果是序列化的,在判断是否有readResolve方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);} 方法hasReadResolveMethod:
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
* 这个注释就是表达了,是否存在这个readResolve方法
*/
boolean hasReadResolveMethod() {
return (readResolveMethod != null);
} 方法invokeReadResolve:
if (readResolveMethod != null) {
try {
//利用反射的invoke,来调用实例中的readResolve方法,返回实例
return readResolveMethod.invoke(obj, (Object[]) null);
}

上面的源码了解到,为什么只在代码中添加了一个readResolve()方法,就解决了序列化攻击。

在readObject中的一层层封装的方法中,readOrdinaryObject()会判断类是否序列化,如果是,则调用hasReadResolveMethod()判断是否有readResolve()方法,如果存在readResolve()方法就调用invokeReadResolve()方法,利用反射来获取类中readResolve方法返回的实例。

二. 反射攻击破坏

之前学过单例模式,知道了。在单例模式中,通过创建一个私有的无参构造器在阻止类在其他地方被创建,从而保证只有一个实例。但是,通过反射的方式,可以改变构造器的类型,即改为public。看下面代码:

/**
* @program: designModel
* @description: 通过反射,来获取新的实例,破坏单例模式
* @author: YuKai Fan
* @create: 2018-12-05 14:55
**/
public class Test3 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException { //饿汉式
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
//通过反射,将类的构造器权限改为了public,这样就这样new出新的实例
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance(); System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}

得到的结果是:false

从上面代码可以看到,利用反射的getDeclaredConstructor()方法将构造器的权限修改为public,这样就可以创建不同的实例了

那么怎么样来阻止呢?

看下修改后的,饿汉式单例模式的代码:直接在私有构造器中加上一个判断即可

/**
* @program: designModel
* @description: 饿汉式,与懒汉式最大的区别,就是延时加载,但是饿汉式如果不用该实例,会占用资源
* @author: YuKai Fan
* @create: 2018-12-04 16:57
**/
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton; static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
if (hungrySingleton != null) {
throw new RuntimeException("单例构造器禁止反射调用");
} }
public static HungrySingleton getInstance() {
return hungrySingleton;
} private Object readResolve() {
return hungrySingleton;
}
}

上面的原理,是用饿汉式的特点,在类加载的时候就创建了实例,这样即使改变了构造器的权限也无法判断成功,因为此时实例已经创建了,无法在调用构造器方法。

但是这仅仅只适用于饿汉式,和静态内部类的方式。如果单例模式是延时加载,那就跟代码的执行顺序有关了。看下面这段代码:

懒汉式单例模式:

/**
* @program: designModel
* @description: 懒汉单例,懒汉式注重的就是延迟加载,当在使用到这个实例的时候才会初始化
* @author: YuKai Fan
* @create: 2018-12-04 14:04
**/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
// private static boolean flag = true;
private LazySingleton() {
/*if (flag) {
flag = false;
} else {
throw new RuntimeException("单例构造器禁止反射调用");
}*/
if (lazySingleton != null) {
throw new RuntimeException("单例构造器禁止反射调用");
} } //在代码块上加锁,让这个方法每次只能有一个线程访问,这样只会产生一个实例
//这种方式,锁的是class类,存在加锁和解锁的开销,对性能有一定影响
public static LazySingleton getInstance() {
synchronized(LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
}
/**
* @program: designModel
* @description: 通过反射,来获取新的实例,破坏单例模式
* @author: YuKai Fan
* @create: 2018-12-05 14:55
**/
public class Test3 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException { Class objectClass = LazySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
//通过反射,将类的构造器权限改为了public,这样就这样new出新的实例
constructor.setAccessible(true);
//如果按照上面两种方式在类加载的时候判断,依旧会就会产生不同的实例
*//*
可以看出,这跟创建实例的顺序是有关的,
如果先执行LazySingleton.getInstance()方法,由于getInstance是同步的,就会先拿到实例,后面反射在获取实例时,此时单例对象已经存在,就会抛出异常
在多线程环境下,如果获取单例一个线程后执行,反射单例一个线程先执行,那就会产生两个不同的实例
*//*
LazySingleton newInstance = (LazySingleton) constructor.newInstance();
LazySingleton instance = LazySingleton.getInstance(); System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}

根据上面代码的注释看得出,在延时加载的情况下单例模式的反射攻击并没有有效的防止措施。也不好在私有的构造器中添加判断。那么下面介绍一种推荐的单例模式,利用枚举类的特性来,实现单例。

枚举类:

/**
* @program: designModel
* @description: 使用枚举实现单例
* @author: YuKai Fan
* @create: 2018-12-05 15:55
**/
public enum EnumInstance {
INSTANCE{
protected void printTest() {
System.out.println("Print Test");
}
};
protected abstract void printTest();
private Object data; public Object getData() {
return data;
} public void setData(Object data) {
this.data = data;
} public static EnumInstance getInstance() {
return INSTANCE;
}
}

Test:

package com.javaDesign.designModel.creational.Singleton;

import java.io.*;
import java.lang.reflect.InvocationTargetException; /**
* @program: designModel
* @description: 通过反射,来获取新的实例,破坏单例模式
* @author: YuKai Fan
* @create: 2018-12-05 14:55
**/
public class Test3 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException { /**
* 枚举类型的单例模式下的,反射与序列化攻击
*/
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance); File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
/*
ObjectInputStream中有一个readEnum()方法,这是读取枚举类的方法,它会获取到枚举对象的名称name,
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
//根据name获取到枚举常量,由于name是唯一的,并且对应一个枚举常量,所以对于枚举类,实例也只会产生一个,所以枚举类对于序列化的破坏是不受影响的
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
}
*/
EnumInstance newInstance = (EnumInstance)ois.readObject();
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData()); }
}

阅读源码可知,由于ObjectInputStream中的readEnum()方法,根据枚举类类名来得到唯一的枚举常量,从而只会产生一个实例,所以枚举类对于序列化的破坏是不受影响的

下面模拟反射攻击:

package com.javaDesign.designModel.creational.Singleton;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; /**
* @program: designModel
* @description: 通过反射,来获取新的实例,破坏单例模式
* @author: YuKai Fan
* @create: 2018-12-05 14:55
**/
public class Test3 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
/**
* 枚举类型的单例模式下的,反射与序列化攻击
*/
/*EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance); File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
*//*
ObjectInputStream中有一个readEnum()方法,这是读取枚举类的方法,它会获取到枚举对象的名称name,
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
//根据name获取到枚举常量,由于name是唯一的,并且对应一个枚举常量,所以对于枚举类,实例也只会产生一个,所以枚举类对于序列化的破坏是不受影响的
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
}
*//*
EnumInstance newInstance = (EnumInstance)ois.readObject();
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());*/
Class objectClass = EnumInstance.class;
/*
因为枚举类中是没有无参构造器的,所以必须要传两个参数
*/
Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class);
//通过反射,将类的构造器权限改为了public,这样就这样new出新的实例
constructor.setAccessible(true);
EnumInstance instance = EnumInstance.getInstance();
EnumInstance newInstance = (EnumInstance) constructor.newInstance("测试",666);
/*
通过反射来获取实例时,会有一个判断,看是否是枚举类,如果是的话就会抛出异常,这样反射攻击也会失败
newInstance():
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
*/
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance); }
}

输出结果为:

同样阅读newInstance()方法源码,通过反射获取实例,会判断是否是枚举类,这样反射攻击也会无效。

java设计模式——单例模式(二)的更多相关文章

  1. JAVA设计模式-单例模式(Singleton)线程安全与效率

    一,前言 单例模式详细大家都已经非常熟悉了,在文章单例模式的八种写法比较中,对单例模式的概念以及使用场景都做了很不错的说明.请在阅读本文之前,阅读一下这篇文章,因为本文就是按照这篇文章中的八种单例模式 ...

  2. Java设计模式(二) 工厂方法模式

    本文介绍了工厂方法模式的概念,优缺点,实现方式,UML类图,并介绍了工厂方法(未)遵循的OOP原则 原创文章.同步自作者个人博客 http://www.jasongj.com/design_patte ...

  3. java设计模式单例模式 ----懒汉式与饿汉式的区别

    常用的五种单例模式实现方式 ——主要: 1.饿汉式(线程安全,调用率高,但是,不能延迟加载.) 2.懒汉式(线程安全,调用效率不高,可以延时加载.) ——其他: 1.双重检测锁式(由于JVM底层内部模 ...

  4. Java设计模式の单例模式

    -------------------------------------------------- 目录 1.定义 2.常见的集中单例实现 a.饿汉式,线程安全 但效率比较低 b.单例模式的实现:饱 ...

  5. Java设计模式 - - 单例模式 装饰者模式

    Java设计模式 单例模式 装饰者模式 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 静态代理模式:https://www.cnblogs.com/StanleyBlogs/p/1 ...

  6. Java设计模式(二十一):职责链模式

    职责链模式(Chain Of Responsibility Pattern) 职责链模式(Chain Of Responsibility Pattern):属于对象的行为模式.使多个对象都有机会处理请 ...

  7. 【设计模式】Java设计模式 - 单例模式

    [设计模式]Java设计模式 - 单例模式 不断学习才是王道 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 分享学习心得,欢迎指正,大家一起学习成长! 原创作品,更多关注我CSDN: ...

  8. Java 设计模式 —— 单例模式

    1. 概念: 单例模式是一种常用的软件设计模式.核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源.如果 ...

  9. Java设计模式 - 单例模式 (懒汉方式和饿汉方式)

    概念: Java中单例模式是一种常见的设计模式,单例模式的意思就是只有一个实例.单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个类称为单例类. 单例模式的写法有好几种,这 ...

随机推荐

  1. 一切从这里起始(左耳听风 ARTS 6号小组 week 1)

    ARTS 具体要求: 1.每周至少做一个 leetcode 的算法题2.阅读并点评至少一篇英文技术文章3.学习至少一个技术技巧4.分享一篇有观点和思考的技术文章 1.Algorithm Two Sum ...

  2. [转]Xcode概览:调试应用程序

    原文网址: blog.csdn.net/fhbystudy/article/details/12856261 本文由CocoaChina翻译组成员Creolophus(github主页)翻译自苹果官方 ...

  3. Spring AOP 自定义注解实现统一日志管理

    一.AOP的基本概念: AOP,面向切面编程,常用于日志,事务,权限等业务处理.AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容(Spring核心之一),是函数式编程 ...

  4. 树状数组的神操作QAQ

    卧槽 厉害了,我的树状数组 1.单点修改,单点查询 用差分数组维护 #include<cstdio> #include<iostream> using namespace st ...

  5. thinkphp5文件上传问题

    tp5中文件上传如果没有数据就会报错,所以要先做一个判断 //先接收文件数据 $isfile=$_FILES;//判断是否上传图片数据,如果没有上传数据二位数组中的name会为空,如下例:if($is ...

  6. css3中的变形(transform)、过渡(transtion)、动画(animation)

    Transform字面上就是变形,改变的意思.在CSS3中transform主要包括以下几种:旋转rotate.扭曲skew.缩放scale和移动translate以及矩阵变形matrix.下面我们一 ...

  7. C数据结构与算法-算法复杂度

    算法复杂度分为时间复杂度T(n)和空间复杂度F(n) 时间复杂度:也就是执行算法程序所需的时间,与硬件的速度.编程语言的级别.编译器的优化.数据的规模.执行的频度有关,前三个有很大的不确定性,所以衡量 ...

  8. Hadoop InputFormat详解

    InputFormat是MapReduce编程模型包括5个可编程组件之一,其余4个是Mapper.Partitioner.Reducer和OutputFormat. 新版Hadoop InputFor ...

  9. js 提示样式 ? 上写提示内容

    //再需要的地方放入 <img dms_map_key="zs_prise" src="${ctx }/static/image/tip.png" cla ...

  10. HBase基础讲解

    HBase定义        HBase 是一个高可靠.高性能.面向列.可伸缩的分布式存储系统,利用Hbase技术可在廉价PC Server上搭建 大规模结构化存储集群.        HBase 是 ...