【如何破坏单例模式(详解)】

在这里插入图片描述

✅如何破坏单例模式

  • 💡典型解析
  • ✅拓展知识仓
    • ✅反射破坏单例
    • ✅反序列化破坏单例
    • ✅ObjectlnputStream
  • ✅总结
    • ✅如何避免单例被破坏
      • ✅ 避免反射破坏单例
      • ✅ 避免反序列化破坏单例

💡典型解析


单例模式主要是通过把一个类的构造方法私有化,来避免重复创建多个对象的。那么,想要破坏单例,只要想办注能够执行到这个私有的构造方法就行了。


✅拓展知识仓


一般来说做法有使用反射及使用反序列化都可以破坏单例。


我们先通过双重校验锁的方式创建一个单例,后文会通过反射及反序列化的方式尝试破坏这个单例。


package com.yangxiaoyuan;

import java.io.Serializable;

/**
 * Created by yangxiaoyuan on 23/12/24
 * 使用双重校验锁方式实现单例
*/

public class Singleton implements Serializable {
	private volatile static Singleton singleton;
	private Singleton () {
		
	}

	public static Singleton getsingleton()  {
		if (singleton == null) {
			synchronized (Singleton.class)  {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}

		return singleton;
	}	
}

✅反射破坏单例


我们尝试通过反射技术,来破坏单例:


Singleton singleton1 = Singleton.getSingleton();

//通过反射获取到构造函数
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
//将构造函数设置为可访问类型
constructor.setAccessible(true);
//调用构造函数的newInstance创建一个对象
Singleton singleton2 = constructor.newInstance();
//判断反射创建的对象和之前的对象是不是同一个对象
System.out.println(s1 == s2);

以上代码,输出结果为false,也就是说通过反射技术,我们给单例对象创建出来了一个 "兄弟” 。


setAccessible(true),使得反射对象在使用时应该取消Java 语言访检查,使得私有的构造函数能够被访问。


✅反序列化破坏单例


我们尝试通过序列化+反序列化来破坏一下单例:


package com.yangxiaoyuan;

import java.io.*;

public class SerializableDemo1 {
	//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
	//Exception直接抛出


	public static void main(String  args) throws IOException, ClassNotFoundException {
		
		//Write Obj to file
		ObjectOutputStream oos = new ObjectOutputStream(new File0utputStream("tempFile"));
		oos.writeObject(Singleton.getsingleton());
		//Read Obi from file
		File file = new File("tempFile");
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
		Singleton newInstance = (Singleton) ois.readObject();
		//判断是否是同一个对象
		System.out.println(newInstance == Singleton.getSingleton());
	}
}

//false


输出结构为false,说明:


通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。


这里,在介绍如何解决这个问题之前,我们先来深入分析一下,为什么会这样?在反序列化的过程中到底发生了什么。


✅ObjectlnputStream


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


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


在这里插入图片描述

这里看一下重点代码,readOrdinaryObject 万法的代码片段: code 3


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;
}

code 3 中主要贴出两部分代码。先分析第一部分:


code3.1


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对象,就是本方法要返回的对象,也可以暂时理解为是ObjectlnputStream的 readobject 返回的对象。


![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d9179a634e2a462dafeaf5c696d1a6f7.png#pic_center在这里插入图片描述


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




desc.newInstance:该方法通过反射的方式新建一个对象。


然后看一下 newInstance 的源码:


public T newInstance(Object ... initargs) throws InstantiationExceptionIllegalAccessException,
IllegalArgumentExceptionInvocationTargetException {
	if (!override) {
		if (!Reflection.quickCheckMemberAccess(clazz,modifiers)) {
			Class<?> caller = Reflection.getCallerClass();
			checkAccess(caller, clazz, nul1, 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();
	}
	@Suppresslarnings("unchecked")
	T inst = (T) ca.newInstance(initargs];
	return inst;
}

其中关键的就是 T inst = (T) ca.newInstance(initargs);这一步,这里实现的话在BootstrapConstructorAccessorlmpl中,实现如下:


public Object newInstance(Object[] args)
throws IllegalArgumentExceptionInvocationTargetException {
	try {
		return UnsafeFieldAccessorImpl.unsafe.allocateInstance(constructor.getDeclaringClass());
	} catch (InstantiationException e)  {
		throw new InvocationTargetException(e);
	}
}

可以看到,这里通过Java 的 Unsafe 机制来创建对象的,而不是通过调用构造函数。这意味着即使类的构造函数是私有的,反序列化仍然可以创建该类的实例,因为它不依赖于常规的构造过程。


So,到目前为止,也就可以解释,为什么序列化可以破坏单例?

答:序列化会通过Unsafe直接分配的方式来创建一个新的对象。

✅总结


在涉及到序列化的场景时,要格外注意他对单例的破坏。


✅如何避免单例被破坏


✅ 避免反射破坏单例


反射是调用默认的构造函数创建出来的,只需要我们改造下构造函数,使其在反射调用的时候识别出来对象是不是被创建过就行了:


private Singleton() {
	if (singleton != null) {
		throw new RuntimeException("单例对象只能创建一次...");
	}
}

✅ 避免反序列化破坏单例


解决反序列化的破坏单例,只需要我们自定义反序列化的策略就行了,就是说我们不要让他走默认逻辑一直调用至Unsafe创建对象,而是我们干预他的这个过程,干预方式就是在Singleton类中定义 readResolve ,这样就可以解决该问题:


package com.yangxiaoyuan;


import java.io.Serializable;

// 使用双重校验锁方式实现单例

public class Singleton implements Serializable {
	private volatile static Singleton singleton;
	private Singleton (){}
	public static Singleton getSingleton()  {
		if (singleton == null) {
			synchronized (Singleton.class)  {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
	private Object readResolve() {
		return singleton;
	}
}

还是运行以下测试类


package com.yangxiaoyuan;

import java.io.*;

public class SerializableDemo1 {
	//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
	//Exception直接抛出

	public static void main(Stringl] args) throws IOException, ClassNotFoundException {
		//Write Obj to file
		ObjectOutputStream oos = new ObiectOutputStream(new File0utputstream("tempFile")):
		oos.writeObject(Singleton.getSingleton());
		//Read Obj from file
		File file = new File("tempFile");
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
		Singleton newInstance = (Singleton) ois.readObject();
		//判断是否是同一个对象
		System.out.println(newInstance == Singleton.getSingleton());
	}
}

//true

本次输出结果为true。具体原理,我们回过头继续分析code 3中的第二段代码:


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);
	}
}

hasReadResolveMethod :如果实现了serializable 或者 externalizable接口的类中包含 readResolve 则返回true。


invokeReadResolve :通过反射的方式调用要被反序列化的类的readResolve方法。


所以,原理也就清楚了,只要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可可以防止单例被破坏。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/272474.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙系统的分布式技术:重塑智能终端的未来

华为鸿蒙系统自发布以来&#xff0c;凭借其创新的分布式技术&#xff0c;改变了我们对智能终端的认知和使用方式。鸿蒙系统的分布式技术是一种全新的设计理念&#xff0c;它将不同设备、不同应用场景视为一个整体&#xff0c;通过共享、协同和无缝连接&#xff0c;为用户带来前…

android setText不生效问题

1.直接说解决方案&#xff1a; 在代码没问题的情况下&#xff0c;将你的TextView的Id改一下&#xff0c;然后再重启编译器即可(注意&#xff0c;不修改TextView的ID&#xff0c;单独重启是没有作用的&#xff01;) 2.出现问题的过程&#xff1a; 产品新增一个需求&#xff0c…

SpringBoot整合JWT+Spring Security+Redis实现登录拦截(一)登录认证

一、JWT简介 JWT 全称 JSON Web Token&#xff0c;JWT 主要用于用户登录鉴权&#xff0c;当用户登录之后&#xff0c;返回给前端一个Token&#xff0c;之后用户利用Token进行信息交互。 除了JWT认证之外&#xff0c;比较传统的还有Session认证&#xff0c;如何选择可以查看之前…

MAGVIT: Masked Generative Video Transformer

Paper name MAGVIT: Masked Generative Video Transformer Paper Reading Note Paper URL: https://arxiv.org/abs/2212.05199 Project URL: https://magvit.cs.cmu.edu/ Code URL: https://github.com/google-research/magvit TL;DR 2023 年 CMU、google 等发表 CVPR20…

[Python工程化之路] 搭建Python开发环境 包管理环境以及Linter

个人博客:Sekyoro的博客小屋 个人网站:Proanimer的个人网站 在工程化上,Python相比于Java,C#这类语言还是差了不少,不过整个生态还是不错的. 项目结构 一般有两种,一种称为flat另一种为src. ├── sample │ ├── AUTHORS.rst │ ├── docs | | ├── conf.py │ │ └…

深入Apache Commons Config:管理和使用配置文件

第1章&#xff1a;引言 咱们都知道&#xff0c;在软件开发中&#xff0c;管理配置文件是一件既重要又让人头疼的事。想象一下&#xff0c;咱们的应用程序有一堆设置需要调整&#xff0c;比如数据库的连接信息、应用的端口号&#xff0c;或者是一些功能的开关。如果这些信息硬编…

java实现广度优先搜索算法

广度优先搜索算法&#xff08;BFS&#xff09;是一种用于图遍历的算法。它从图的某个节点开始&#xff0c;依次访问其所有邻接节点&#xff0c;再依次访问邻接节点的邻接节点&#xff0c;以此类推&#xff0c;直到遍历完所有节点。 BFS使用队列数据结构来实现遍历过程。具体步…

关于 Appium 各种版本的安装,都在这里

大家在初次接触 Appium 时会看到网上各种帖子讲解如何安装 Appium&#xff0c;各种 Appium 版本的安装教程满天飞&#xff0c;而很多帖子中提供的安装教程是已经过时了的&#xff0c;容易误导初学者。 这篇文章带着你一起全面了解 Appium 各种版本如何选择如何安装。 一句话概述…

Superset 二次开发之自定义Viz Plugins(Hello World v2)

环境&#xff1a; Node.js 16npm 7 or 8安装webpack 全局安装 npm install webpack -g 安装eslint superset-frontend> npm install eslint 1.Yeoman 生成器 全局安装Yo> npm i -g yo 2.进入/superset-frontend/packages/generator-superset目录 npm i && npm…

传感器原理与应用--传感器基本特性与应变式传感器

文章目录 上一篇传感器的基本特性应变式传感器应变式传感器的应用下一篇 上一篇 传感器的基本特性 一般来说能把特定被测量信息按一定规律转换成某种可用信号的器件或装置&#xff0c;称为传感器 静态特性 灵敏度 定义&#xff1a;输出量增量 Δ y \Delta y Δy与引起输出量…

xstream 远程代码执行 CVE-2021-29505 已亲自复现

xstream 远程代码执行 CVE-2021-29505 已亲自复现 漏洞名称漏洞描述影响版本 漏洞复现环境搭建漏洞利用 修复建议总结 漏洞名称 漏洞描述 XStream 是用于将 Java 对象序列化为 XML 并再次序列化的软件。 1.4.17 之前的 XStream 版本中存在一个漏洞&#xff0c;可能允许远程攻…

集成钉钉机器人消息推送

一、简介 背景 客户需要通过钉钉接收消息通知 名词解释 群聊机器人&#xff1a;钉钉群里可以创建一个机器人&#xff0c;平台通过机器人把告警/通知推送到群里私聊机器人&#xff1a;钉钉后台开启机器人配置&#xff0c;平台绑定此机器人后&#xff0c;可以通过私聊的方式将…

C/S医院检验LIS系统源码

一、检验科LIS系统概述&#xff1a; LIS系统即实验室信息管理系统。LIS系统能实现临床检验信息化&#xff0c;检验科信息管理自动化。其主要功能是将检验科的实验仪器传出的检验数据经数据分析后&#xff0c;自动生成打印报告&#xff0c;通过网络存储在数据库中&#xff…

20231226在Firefly的AIO-3399J开发板上在Android11下调通后摄像头ov13850

20231226在Firefly的AIO-3399J开发板上在Android11下调通后摄像头ov13850 2023/12/26 8:22 开发板&#xff1a;Firefly的AIO-3399J【RK3399】 SDK&#xff1a;rk3399-android-11-r20211216.tar.xz【Android11】 Android11.0.tar.bz2.aa【ToyBrick】 Android11.0.tar.bz2.ab And…

一文搞懂类加载过程

废话不多说&#xff0c;先上一张图 1、“加载”过程做了什么&#xff1f;什么是双亲委派&#xff1f;为什么要使用双亲委派机制&#xff1f;有什么利弊&#xff1f; **加载&#xff1a;**就是将编译后的.class字节码文件【jvm只认.class文件&#xff0c;.class文件也并非只有…

C++ std::string使用效率优化

字符串操作是任何一个C开发程序无法绕过的点&#xff0c;很多时候针对字符串的操作需要进行优化&#xff0c;从而达到更优的使用效率和内存利用率。一般会采用标准的std::string替代C字符串&#xff0c;一方面是std::string为一个成熟的类对象&#xff0c;其成员操作基本能满足…

阿赵UE学习笔记——4、新建关卡

阿赵UE学习笔记目录 大家好&#xff0c;我是阿赵。   之前介绍了虚幻引擎的常用窗口功能&#xff0c;这次开始创建游戏内的世界了。首先先从创建关卡开始。 一、创建新关卡 在使用UE引擎制作游戏&#xff0c;首先要有一个场景作为基础&#xff0c;这个场景在UE里面成为关卡。…

若依(Spring boot)框架中如何在不同的控制器之间共享与使用数据

在若依框架或Spring boot框架中&#xff0c;控制器共享和使用数据是为了确保数据一致性、传递信息、提高效率和降低系统复杂性。这可以通过全局变量、依赖注入或数据库/缓存等方式实现。共享和使用数据对框架的正常运行非常关键&#xff0c;有助于促进控制器之间的协同工作&…

消息走漏提前做空腾讯爆赚30倍?逐帧分析还原真相

数量技术宅团队在CSDN学院推出了量化投资系列课程 欢迎有兴趣系统学习量化投资的同学&#xff0c;点击下方链接报名&#xff1a; 量化投资速成营&#xff08;入门课程&#xff09; Python股票量化投资 Python期货量化投资 Python数字货币量化投资 C语言CTP期货交易系统开…

C语言结构体内存对齐

文章目录 一、结构体内存对齐问题二、查看结构体成员起始位置三、设置内存对齐方式 一、结构体内存对齐问题 如下的info_s结构体类型&#xff0c;包含一个int型成员age, 一个char型成员gender, 一个int型成员id。 单从数据成员的大小进行分析&#xff0c;整个结构体的大小应为…