谈谈单例设计模式的源码应用和安全问题

一、源码应用

事实上,我们在JDK或者其他的通用框架中很少能看到标准的单例设计模式,这也就

意味着他确实很经典,但严格的单例设计确实有它的问题和局限性,我们先看看在源

码中的一些案例

1、jdk中的单例

jdk中有一个类的实现是一个标准单例模式(饿汉式),即Runtime类,该类封装了运行时的环境。每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。 一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime类实例,可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。

java 复制代码
public class Runtime {
    private static final Runtime currentRuntime = new Runtime();

    private static Version version;

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class {@code Runtime} are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the {@code Runtime} object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    
    ...
}

2、MyBatis中的单例

Mybaits中的org.apache.ibatis.io.VFS使用到了单例模式。VFS就是Virtual File

System的意思,mybatis通过VFS来查找指定路径下的资源。查看VFS以及它的实现

类,不难发现,VFS的角色就是对更"底层"的查找指定资源的方法的封装,将复杂的

"底层"操作封装到易于使用的高层模块中,方便使用者使用。

java 复制代码
public class public abstract class VFS {
	// 使用了内部类
	private static class VFSHolder {
		static final VFS INSTANCE = createVFS();
		@SuppressWarnings("unchecked")
		static VFS createVFS() {
			// ...省略创建过程
			return vfs;
		}
	}
	
	public static VFS getInstance() {
		return VFSHolder.INSTANCE;
	}
	
	// ...
}

二、安全问题

1、反射入侵

我们可以通过反射获取私有构造器进行构造,如下代码:

java 复制代码
@Slf4j
public class TestReflectSingleton {

    private static volatile TestReflectSingleton instance;

    private TestReflectSingleton(){}

    public static TestReflectSingleton getInstance(){
        if(instance == null){
            synchronized (TestReflectSingleton.class){
                if(instance == null){
                    instance = new TestReflectSingleton();
                }
            }
        }
        return instance;
    }

    /**
     * 测试反射入侵
     * @param args
     */
    public static void main(String[] args) {
        Class<TestReflectSingleton> cls = TestReflectSingleton.class;
        try {
            Constructor<TestReflectSingleton> constructor = cls.getDeclaredConstructor();
            // 设置为可见
            constructor.setAccessible(true);
            TestReflectSingleton instance1 = TestReflectSingleton.getInstance();
            TestReflectSingleton instance2 = constructor.newInstance();
            boolean flag = instance2 == instance1;
            log.info("flag -> {}", flag);
            log.info("flag -> {}", flag);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

这样输出的结果是false,证明对象不是同一个对象,我们可以在构造方法中再加一个判断对象是否为空的条件即可。代码如下:

java 复制代码
@Slf4j
public class TestReflectSingleton {

    private static volatile TestReflectSingleton instance;

    private TestReflectSingleton(){
        if(instance != null){
            // 实例化直接抛出异常
            throw new RuntimeException("实例:【" + this.getClass().getName() + "】已经存在,该实例只被实例化一次!");
        }
    }

    public static TestReflectSingleton getInstance(){
        if(instance == null){
            synchronized (TestReflectSingleton.class){
                if(instance == null){
                    instance = new TestReflectSingleton();
                }
            }
        }
        return instance;
    }

    /**
     * 测试反射入侵
     * @param args
     */
    public static void main(String[] args) {
        Class<TestReflectSingleton> cls = TestReflectSingleton.class;
        try {
            Constructor<TestReflectSingleton> constructor = cls.getDeclaredConstructor();
            constructor.setAccessible(true);
            TestReflectSingleton instance1 = TestReflectSingleton.getInstance();
            TestReflectSingleton instance2 = constructor.newInstance();
            boolean flag = instance2 == instance1;
            log.info("flag -> {}", flag);
            log.info("flag -> {}", flag);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

直接抛出异常,这样就可以防止反射入侵啦

2、序列化与反序列化问题

java 复制代码
@Slf4j
public class TestSerializeSingleton implements Serializable {
    /**
     * 简单的写个懒加载吧
     */
    private static TestSerializeSingleton instance;

    private TestSerializeSingleton() {}

    public static TestSerializeSingleton getInstance(){
        if(instance == null){
            instance = new TestSerializeSingleton();
        }
        return instance;
    }


    public static void main(String[] args) {
        String url = "DesignPatterns/src/main/resources/singleton.txt";
        // 获取单例并序列化
        TestSerializeSingleton singleton = TestSerializeSingleton.getInstance();
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try {
            fos = new FileOutputStream(url);
            oos = new ObjectOutputStream(fos);
            oos.writeObject(singleton);
            // 将实例反序列化出来
            fis = new FileInputStream(url);
            ois = new ObjectInputStream(fis);
            Object o = ois.readObject();
            log.info("他们是同一个实例吗?{}",o == singleton);  // return false
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if(fos != null){
                    fos.close();
                }
                if(oos != null){
                    oos.close();
                }
                if(fis != null){
                    fis.close();
                }
                if(ois != null){
                    ois.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

输出结果如下:

readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在,所以在单例中添加readResolve方法:

java 复制代码
public Object readResolve(){
     return instance;
}

再次运行代码输出结果如下:

三、单例存在的一些问题

1、它不支持面向对象编程

我们都知道,面向对象的三大特性是封装、继承、多态。单例将构造私有化,直接导致的结果就是,他无法成为其他类的父类,这就相当于直接放弃了继承和多态的特性,也就相当于损失了可以应对未来需求变化的扩展性,以后一旦有扩展需求,我们不得不新建一个十分【雷同】的单例。

2、极难的横向扩展

单例类只能有一个对象实例。如果未来某一天,一个实例已经无法满足我们的需求,我们需要创建一个,或者更多个实例时,就必须对源代码进行修改,无法友好扩展。

在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。

如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

相关推荐
用户962377954482 天前
VulnHub DC-3 靶机渗透测试笔记
安全
叶落阁主3 天前
Tailscale 完全指南:从入门到私有 DERP 部署
运维·安全·远程工作
willow3 天前
Axios由浅入深
设计模式·axios
用户962377954485 天前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机5 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机5 天前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户962377954485 天前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star5 天前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
七月丶5 天前
别再手动凑 PR 了:这个 AI Skill 会按仓库习惯自动建分支、拆提交、提 PR
人工智能·设计模式·程序员