• 单例设计模式


    1. 单例设计模式

    单例设计模式(Singleton Design Pattern), 一个类只允许创建 一个对象(或者实例),那这个类就是一个单例类,这种设计模式称为单例设计模式,简称单例模式。

    1.1 为什么要使用单例

    1.1.1 表示全局唯一

    如果有些数据在系统中应该且只能保存一份,那就应该设计为单例类。

    • 配置类:在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,应该被映射为一个唯一的【配置实例】,此时就可以使用单例,当然也可以不用。
    • 全局计数器:我们使用一个全局的计数器进行数据统计、生成全局递增ID等功能。若计数器不唯一,很有可能产生统计无效,ID重复等。

    实现全局ID生成器的代码 :

    public class GlobalCounter {
        private AtomicLong atomicLong = new AtomicLong(0);
        private static final GlobalCounter instance = new GlobalCounter();
        // 私有化无参构造器
        private GlobalCounter() {
            
        }
        public static GlobalCounter getInstance() {
        	return instance;
        }
        public long getId() {
        	return atomicLong.incrementAndGet();
        }
    }
    // 查看当前的统计数量
    long courrentNumber = GlobalCounter.getInstance().getId();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.1.2 处理资源访问冲突

    我们简单的设计一个日志输出的功能

    v1版本

    public class Logger {
        private String basePath = "D://info.log";
        private FileWriter writer;
        // new Logger的时候初始化writer
        public Logger() {
            File file = new File(basePath);
            try {
            	writer = new FileWriter(file, true); //true表示追加写入
            } catch (IOException e) {
            	throw new RuntimeException(e);
            }
        }
        public void log(String message) {
            try {
            	writer.write(message);
            } catch (IOException e) {
            	throw new RuntimeException(e);
            }
        }
        public void setBasePath(String basePath) {
        	this.basePath = basePath;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    使用 V1版本

    @RestController("user")
    public class UserController {
        public Result login(){
            // 登录成功
            Logger logger = new Logger();
            logger.log("tom logged in successfully.");
            // ...
            return new Result();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上面的版本会产生如下的问题:多个 logger实例在多个线程中同时操作同一个文件,可能产生相互覆盖的问题。 因为tomcat处理每一个请求都会使用一个新的线程(暂且不考虑多路复用)。此时日志文件就成了一个共享资源,但凡是多线程访问共享资源,我们都要考虑并发修改 产生的问题。

    V2版本,对log加锁处理。 这样加锁毫无卵用,方法级别的锁可以保证new出来的同一个实例多线程下可以同步执行log方法,然而你却new了很多个Logger实例。

    public synchronized void log(String message) {
        try {
        	writer.write(message);
        } catch (IOException e) {
        	throw new RuntimeException(e);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    其实,writer方法本身也是加了锁的,我们这样加锁就没有了意义:

    public void write(String str, int off, int len) throws IOException {
    	synchronized (lock) {
            char cbuf[];
            if (len <= WRITE_BUFFER_SIZE) {
                if (writeBuffer == null) {
                    writeBuffer = new char[WRITE_BUFFER_SIZE];
                }
                cbuf = writeBuffer;
            } else { // Don't permanently allocate very large buffers.
                cbuf = new char[len];
            }
            str.getChars(off, (off + len), cbuf, 0);
            write(cbuf, 0, len);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    加锁是一定能解决共享资源冲突问题的,我们只要放大锁的范围从【this】到 【class】,这个问题也是能解决的,代码如下:

    在这里插入图片描述

    public void log(String message) {
        synchronized (Logger.class) {
            try {
            	writer.write(message);
            } catch (IOException e) {
            	throw new RuntimeException(e);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    从以上的内容我们发现:

    • 如果使用单个实例输出日志,锁【this】即可。
    • 如果要保证JVM级别防止日志文件访问冲突,锁【class】即可。
    • 如果要保证集群服务级别的防止日志文件访问冲突,加分布式锁即可。

    同一个Logger无法并行输出到一个文件中,那么针对这个日志文件创建多个 Logger实例也就失去了意义,如果工程要求我们所有的日志输出到同一个日志文件中,并不需要创建大量的Logger实例,这样的好处有:

    • 一方面节省内存空间。
    • 另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能 随便浪费)。

    1.2 如何实现单例

    在编写单例代码的时候要注意以下几点:

    • 构造器私有化
    • 暴露一个公共的获取单例对象的方法
    • 是否支持延迟加载
    • 是否线程安全

    1.2.1 饿汉式

    在类加载的时候,instance 静态实例就已经创建并初始化了,所以instance 实例的创建过程是线程安全的。

    public class EagerSingleton implements Serializable {
    
        //    持有一个jvm全局唯一的实例
        private static final EagerSingleton instance = new EagerSingleton();
    
        //    避免别人随意的创建,需要私有化构造器
        private EagerSingleton() {
            // 防止反射入侵创建对象
            /*if (instance != null) {
                throw new RuntimeException("实例:【"
                        + this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
            }*/
        }
    
        //    暴露一个方法,用来获取实例
        public static EagerSingleton getInstance() {
            return instance;
        }
        
        public static void main(String[] args) throws Exception {
            // 测试饿汉式单例
            System.out.println("测试饿汉式单例>>>" + (EagerSingleton.getInstance() == EagerSingleton.getInstance()));
        }
    }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    恶汉式在工作中反而应该被提倡 ,很多人觉得饿汉式不能支持懒加载,即使不使用也会浪费资源,一方面是内存资源,一 方面会增加初始化的开销。

    • 现代计算机不缺这一个对象的内存。
    • 如果一个实例初始化的过程复杂,那更应该放在启动时处理,避免卡顿或者构造问题发生在运行时

    1.2.2 懒汉式

    懒汉式相对于饿汉式的优势是支持延迟加载 。

    public class LazySingleton {
    
        //    持有一个jvm全局唯一的实例
        private static LazySingleton instance;
    
        //    避免别人随意的创建,需要私有化构造器
        private LazySingleton() {
            // 防止反射入侵创建对象
            if (instance != null) {
                throw new RuntimeException("实例:【"
                        + this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
            }
        }
    
        //    暴露一个方法,用来获取实例
        public static LazySingleton getInstance() {
            if (null == instance) {
                instance = new LazySingleton();
            }
    
            return instance;
        }
        
        public static void main(String[] args) {
            System.out.println("测试懒汉式单例instance>>>" + (LazySingleton.getInstance() == LazySingleton.getInstance()));
        }
    }
    
    • 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

    当大量并发请求时,上面的写法是无法保证其单例的特性,很有可能会有超过一个线程同时执行了new Singleton(); 从而出现线程安全问题。当然可以加锁来解决, 虽然synchronized锁确实可以保证jvm中有且仅有一个单例实例存在,但是方法上加锁会极大的降低获取单例对象的并发度。

    public class LazySingleton {
        //    持有一个jvm全局唯一的实例
        private static LazySingleton instance;
    
        //    避免别人随意的创建,需要私有化构造器
        private LazySingleton() {
            // 防止反射入侵创建对象
            if (instance != null) {
                throw new RuntimeException("实例:【"
                        + this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
            }
        }
    
        //    暴露一个方法,用来获取实例  synchronized在并发场景下,会排队等待,性能一般
        public static synchronized LazySingleton getInstance() {
            if (null == instance) {
                instance = new LazySingleton();
            }
    
            return instance;
        }
        
         public static void main(String[] args) {
            System.out.println("测试懒汉式单例instance>>>" + (LazySingleton.getInstance() == LazySingleton.getInstance()));
    }
    
    • 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

    1.2.3 双重检查锁

    饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。 既要支持延迟加载、又要支持高并发的单例实现方式,双重检查锁。

    public class DoubleCheckLockSingleton {
        //    持有一个jvm全局唯一的实例
        private static volatile DoubleCheckLockSingleton instance;
        //    避免别人随意的创建,需要私有化构造器
        private DoubleCheckLockSingleton() {
            // 防止反射入侵创建对象
            if (instance != null) {
                throw new RuntimeException("实例:【"
                        + this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
            }
        }
    
        /**
         * 暴露一个方法,用来获取实例
         * cpu底层是乱序执行的,volatile如果不加可能会出现半初始化的对象, volatile保证内存可见,保证有序性。
         * 现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)
         * 

    */ public static DoubleCheckLockSingleton getInstance() { // 多个线程过来,一旦一个线程抢到锁并完成实例化。后面的线程就不会排队等待锁,直接返回单例对象 if (null == instance) { synchronized (DoubleCheckLockSingleton.class) { if (null == instance) { instance = new DoubleCheckLockSingleton(); } } } return instance; } public static void main(String[] args) { System.out.println("测试双重检查锁创建单例instance>>>" + (DoubleCheckLockSingleton.getInstance() == DoubleCheckLockSingleton.getInstance())); } }

    • 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

    1.2.4 静态内部类

    还有另一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。 它有点类似饿汉式,但又能做到了延迟加载。具体是怎么做到的呢?

    public class InnerSingleton {
    
        private InnerSingleton() {
        }
    
        // 对外提供公共的访问方法
        public static InnerSingleton getInstance() {
            return SingletonHolder.instance;
        }
    
        /**
         * 定义静态内部类来持有单例对象。
         * 静态在第一次使用的时候加载且只加载一次。(在第一次调用getInstance()方法的时候才会加载去实例化单例对象)
         */
        private static class SingletonHolder  {
            private static final InnerSingleton instance = new InnerSingleton();
        }
        
        public static void main(String[] args) {
            System.out.println("测试静态内部类创建单例 instance>>>" + (InnerSingleton.getInstance() == InnerSingleton.getInstance()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    SingletonHolder 是一个静态内部类,当外部类 InnerSingleton 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时, SingletonHolder 才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全, 又能做到延迟加载。

    1.2.5 枚举类

    Java 枚举类本身的特性保证了实例创建的线程安全性和唯一性。具体的代码如下所示:

    public enum EnumSingleton {
        // INSTANCE就是单例对象,相当于 public static final EnumSingleton = new EnumSingleton();
    	INSTANCE}
    
    • 1
    • 2
    • 3
    • 4

    更通用的写法如下:

    public class EnumSingleton {
    
        private EnumSingleton() {
        }
    
        public enum Singleton {
            // SINGLETON实例化是会执行new Singleton()构造器方法,会实例化EnumSingleton单例对象
            SINGLETON;
    
            private EnumSingleton instance;
    
            Singleton() {
                instance = new EnumSingleton();
            }
    		// 暴露一个方法,用来获取实例
            public EnumSingleton getInstance() {
                return instance;
            }
        }
        
        public static void main(String[] args) {
            // 测试枚举单例
            System.out.println("测试枚举类创建单例>>>" + (EnumSingleton.Singleton.SINGLETON.getInstance() == EnumSingleton.Singleton.SINGLETON.getInstance()));
        }
    }
    
    • 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

    1.3 如何破坏单例

    1.3.1 反射入侵

    想要阻止其他人构造实例仅仅私有化构造器还是不够的,因为还可以使用反射获取私有构造器进行构造,当然使用枚举的方式是可以解决这个问题的,对于其他的书写方案,我们通过下边的方式解决:

    public class EagerSingleton implements Serializable {
    
        //    持有一个jvm全局唯一的实例
        private static final EagerSingleton instance = new EagerSingleton();
    
        //    避免别人随意的创建,需要私有化构造器
        private EagerSingleton() {
            // 防止反射入侵创建对象
            if (instance != null) {
                throw new RuntimeException("实例:【"
                        + this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
            }
        }
    
        //    暴露一个方法,用来获取实例
        public static EagerSingleton getInstance() {
            return instance;
        }
    
        public static void main(String[] args) throws Exception {
            // 反射入侵
            Class<EagerSingleton> eagerSingletonClass = EagerSingleton.class;
            Constructor<EagerSingleton> declaredConstructor = eagerSingletonClass.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            System.out.println("测试反射入侵单例>>>" + (EagerSingleton.getInstance() == declaredConstructor.newInstance()));
    
    }
    
    • 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

    测试结果:

    # 第一次
    测试反射入侵单例>>>false
    # 私有构造中加入校验后
    Exception in thread "main" java.lang.reflect.InvocationTargetException
    	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    	at cn.itcast.designPatterns.singleton.EagerSingleton.main(EagerSingleton.java:56)
    Caused by: java.lang.RuntimeException: 实例:【cn.itcast.designPatterns.singleton.EagerSingleton】已经存在,该实例只允许实例化一次
    	at cn.itcast.designPatterns.singleton.EagerSingleton.(EagerSingleton.java:23)
    	... 5 more
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    1.3.2 序列化与反序列化

    到目前为止,我们的单例依然是有漏洞的,看如下代码:

    public static void main(String[] args) throws Exception {
            // 序列化和反序列化入侵 https://blog.csdn.net/leo187/article/details/104332138
            // 获取单例并序列化
            EagerSingleton singleton = EagerSingleton.getInstance();
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
            out.writeObject(singleton);
    
            // 反序列化读取实例
            ObjectInputStream input = new ObjectInputStream(new FileInputStream("singleton.txt"));
            Object o = input.readObject();
            System.out.println("测试序列化和反序列化入侵, 是同一个实例吗?" + (singleton == o));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    测试结果始终返回false

    测试序列化和反序列化入侵, 是同一个实例吗?false
    
    • 1

    readResolve()方法可以替换从流中读取的对象,在进行反序列化时会尝试执行readResolve方法,并将返回值作为反序列化的结果而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在, 我们只需要重写readResolve()方法即可, 代码如下:

    public class EagerSingleton implements Serializable {
    
        //    持有一个jvm全局唯一的实例
        private static final EagerSingleton instance = new EagerSingleton();
    
        //    避免别人随意的创建,需要私有化构造器
        private EagerSingleton() {
            // 防止反射入侵创建对象
            if (instance != null) {
                throw new RuntimeException("实例:【"
                        + this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
            }
        }
    
        //    暴露一个方法,用来获取实例
        public static EagerSingleton getInstance() {
            return instance;
        }
    
        /**
         * readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在
         *
         * @return
         */
        public Object readResolve() {
            return instance;
        }
    }
    
    • 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

    再来看测试结果 , 序列化和反序列的对象是同一个.

    测试序列化和反序列化入侵, 是同一个实例吗?true
    
    • 1

    1.4 源码应用

    在JDK或其他通用框架中很少能看到标准的单例设计模式,这也意味着单例设计模式确实很经典,但严格的单例设计确实有它的问题和局限性,我们先看看在源码中的一些案例。

    1.4.1 JDK中的单例应用

    Runtime类封装了运行时的环境, 每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。

    一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。

    public class Runtime {
        private static Runtime currentRuntime = new Runtime();
     
        public static Runtime getRuntime() {
            return currentRuntime;
        }
    
        /** Don't let anyone else instantiate this class */
        private Runtime() {}
        
        public Process exec(String command) throws IOException {
            return exec(command, null, null);
        }    
        //...
    }    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Runtime测试用例

    @Test
    public void testRuntime() throws IOException {
        Runtime runtime = Runtime.getRuntime();
        Process exec = runtime.exec("ping 127.0.0.1");
        InputStream inputStream = exec.getInputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inputStream.read(buffer)) > 0) {
            System.out.println(new String(buffer, 0, len, Charset.forName("GBK")));
        }
    
        long maxMemory = runtime.maxMemory();
        System.out.println("maxMemory>>>" + maxMemory);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.4.2 Mybatis中的单例应用

    Mybaits中的org.apache.ibatis.io.VFS使用到了单例模式。VFS就是Virtual FileSystem的意思,mybatis通过VFS来查找指定路径下的资源。VFS的角色就是对更“底层”的查找指定资源的方法的封装,将复杂的“底层”操作封装到易于使用的高层模块中,方便使用.

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    1.5 单例存在的问题

    1.5.1 无法支持面向对象编程

    OOP 的三大特性是封装、继承、多态。单例将构造私有化,直接导致它无法成为其他类的父类,这就相当于直接放弃了继承和多态的特性,损失了可以应对未来需求变化的扩展性,以后一旦有扩展需求,比如写一个具有绝大部分相同功能的单例,我们不得不新建一个十分【雷同】的单例。

    1.5.2 难以横向扩展

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

    在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。

    为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。

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

    spring提供的单例容器,确保一个实例在容器级别的单例,并且可以在容器启动时完成初始化,他的优势如下:
    1、所有的bean以单例形式存在于容器中,避免大量的对象被创建,造成jvm内存抖动严重,频繁gc。
    2、程序启动时,初始化单例bean,满足fast-fail,将所有构建过程的异常暴露在启动时,而非运行时,更加安全。
    3、缓存了所有单例bean,启动的过程相当于预热的过程,运行时不必进行对象创建,效率更高。
    4、容器管理bean的生命周期,结合依赖注入使得解耦更加彻底、扩展性无敌。

  • 相关阅读:
    【Web】浅聊Hessian异常toString姿势学习&复现
    OpenCV分水岭分割算法2
    C++学习规划“的 PPT 大纲设计
    LeetCode 热题 HOT 100 (026/100)【宇宙最简单版】
    Linux分区指南
    python 笔记
    项目代驾(Delegate):新式灵活的协作模式
    盒子模型的基础
    阿里云服务器u1和e实例有什么区别?哪个比较好?
    【Vue3】定义全局变量和全局函数
  • 原文地址:https://blog.csdn.net/u013044713/article/details/134497368