当前位置: 首页 > news >正文

设计模式-创建型-常用:单例模式、工厂模式、建造者模式

单例模式

概念

一个类只允许创建一个对象(或实例),那这个类就是单例类,这种设计模式就叫做单例模式。对于一些类,创建和销毁比较复杂,如果每次使用都创建一个对象会很耗费性能,因此可以把它设置为单例类。有的地方会用数据库连接池来举例,实际上一些数据库连接池、线程池是没有被设计成单例类的,这点在下面单例模式存在的问题中会讲。

如何实现一个单例类

需要关注以下几点:

  • 构造函数需要时private访问权限,这样才能避免外部通过new创建实例
  • 对象创建是否是线程安全
  • 是否支持懒加载
  • getInstance()性能是否达标(有无加锁)
  • 能否通过反射破坏(通过反射创建实例)

饿汉式

public class Singleton {private Singleton() {} // 构造器私有private static final Singleton instance = new Singleton();public static Singleton getInstance() {return instance;}
}

在类加载的时候,instace静态实例就已经创建并初始化好了,所以饿汉式是线程安全的。不过饿汉式不支持懒加载(在真正用到Singleton的时候才创建实例)。

有人说这种实现方式不好,认为懒加载的好处是只有真正使用到的时候才会创建,防止一些对象的构建比较耗费性能(比如需要加载各种配置文件),且在下一次创建对象之前从没有被使用过,会造成资源浪费。

不过我更赞同另一种观点,如果初始化耗时长,那么我们最好不要等到真正要用它的时候才去执行这个初始化过程,比如在响应客户端请求的时候做这个初始化操作会导致请求的响应时间变长。并且如果实例占用资源多,按照fail-fast设计原则(有问题及早暴露),我们也希望在程序启动时就将这个实例初始化好,如果资源不够就会在程序启动时触发报错,也可以避免一些程序运行一段时间后因为初始化实例占用资源过多而报错的情况。

懒汉式

public class Singleton {private Singleton() {} // 构造器私有private static Singleton instance = null;// 初始化对象为nul1public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

懒汉式的缺点很明显,就是synchronized锁粒度太粗,同一时间只能有一个线程访问getInstace()方法去尝试获取实例,如果这个实例被频繁用到,那么加锁释放锁、以及方法的并发度问题会导致性能瓶颈。

双检锁

public class Singleton {private AtomicLong id = new AtomicLong(0);private static Singleton instance = null;private Singleton() {} // 构造器私有public static Singleton getInstance() {if (instance == null) {  // 1synchronized (Singleton.class) {  // 2if (instance == null) {    // 虽然只能有一个线程进入2,但可能有其他线程在1处等待释放锁,因此需要二次校验instance = new Singleton();}}}return instance;}public long getId(){return id.incrementAndGet();}
}

这种实现方式还有一个问题,因为指令重排序,可能会导致Singleton对象被new出来,并且赋值给instance后,还没来得及初始化(在指令层面,赋值那行被分为三步:1、分配内存;2、初始化对象;3、对象指向内存地址),就被另一个线程使用了。

要解决这个问题,instance实例需要加上volatile关键字禁止指令重排序。

不过还有人说只有很低版本的Java才会有这个问题,高版本的Java已经在jdk内部实现中解决了这个问题(通过把前面三步改为原子操作)。这块博主问了gpt后无果,暂时先放在这里。

静态内部类

public class IdGenerator {private AtomicLong id = new AtomicLong(0);private IdGenerator() {}private static class SingletonHolder{private static final IdGenerator instance = new IdGenerator();}public static IdGenerator getInstance() {return SingletonHolder.instance;}public long getId() {return id.incrementAndGet();}
}

静态内部类在程序启动的时候不会加载,只有在第一次被调用的时候才会加载。instance的唯一性、线程安全,都由JVM来保证。

枚举

public enum IdGenerator {INSTANCE;private AtomicLong id = new AtomicLong(0);public long getId() { return id.incrementAndGet();}
}

上面4种写法都是会被反射破坏的,不过反射是一种人为的方式,不会有太大影响。而这个枚举方式是不能通过反射进行构建的,在效果上类似饿汉式,通过Java枚举类型的特性,在类加载的时候就会创建对应的实例。

单例模式存在的问题

大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似IdGenerator.getInstance().getId()这样的方法来调用就可以了。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题。接下来,我们就具体看看到底有哪些问题。

1.单例对OOP特性的支持不友好
我们知道,OOP的四大特性是封装、抽象、继承、多态。单例这种设计模式对于其中的抽象、继承、多态都支持得不好。为什么这么说呢?我们还是通过IdGenerator这个例子来讲解。

public class Order {public void create(...) {//...long id = IdGenerator.getInstance().getId();//...}
}public class User {public void create(...) {// ...long id = IdGenerator.getInstance().getId();//...}
}


IdGenerator的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的OOP的抽象特性。如果未来某一天,我们希望针对不同的业务采用不同的ID生成算法。比如,订单ID和用户ID采用不同的ID生成器来生成。为了应对这个需求变化,我们需要修改所有用到IdGenerator类的地方,这样代码的改动就会比较大。

public class Order {public void create(...) {//...long id = IdGenerator.getInstance().getId();// 需要将上面一行代码,替换为下面一行代码long id = OrderIdGenerator.getIntance().getId();//...}
}public class User {public void create(...) {// ...long id = IdGenerator.getInstance().getId();// 需要将上面一行代码,替换为下面一行代码long id = UserIdGenerator.getIntance().getId();}
}


除此之外,单例对继承、多态特性的支持也不友好。这里我之所以会用“不友好”这个词,而非“完全不支持”,是因为从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

2.单例会隐藏类之间的依赖关系
我们知道,代码的可读性非常重要。在阅读代码的时候,我们希望一眼就能看出类与类之间的依赖关系,搞清楚这个类依赖了哪些外部类。

通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

3.单例对代码的扩展性不友好
我们知道,单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。你可能会说,会有这样的需求吗?既然单例类大部分情况下都用来表示全局类,怎么会需要两个或者多个实例呢?

实际上,这样的需求并不少见。我们拿数据库连接池来举例解释一下。

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

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

4.单例对代码的可测试性不友好
单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如DB,我们在写单元测试的时候,希望能通过mock的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现mock替换。

除此之外,如果单例类持有成员变量(比如IdGenerator中的id成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。

5.单例不支持有参数的构造函数
单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。针对这个问题,我们来看下都有哪些解决方案。

第一种解决思路是:创建完实例之后,再调用init()函数传递参数。需要注意的是,我们在使用这个单例类的时候,要先调用init()方法,然后才能调用getInstance()方法,否则代码会抛出异常。具体的代码实现如下所示:

public class Singleton {private static Singleton instance = null;private final int paramA;private final int paramB;private Singleton(int paramA, int paramB) {this.paramA = paramA;this.paramB = paramB;}public static Singleton getInstance() {if (instance == null) {throw new RuntimeException("Run init() first.");}return instance;}public synchronized static Singleton init(int paramA, int paramB) {if (instance != null){throw new RuntimeException("Singleton has been created!");}instance = new Singleton(paramA, paramB);return instance;}
}Singleton.init(10, 50); // 先init,再使用
Singleton singleton = Singleton.getInstance();


第二种解决思路是:将参数放到getIntance()方法中。具体的代码实现如下所示:

public class Singleton {private static Singleton instance = null;private final int paramA;private final int paramB;private Singleton(int paramA, int paramB) {this.paramA = paramA;this.paramB = paramB;}public synchronized static Singleton getInstance(int paramA, int paramB) {if (instance == null) {instance = new Singleton(paramA, paramB);}return instance;}
}Singleton singleton = Singleton.getInstance(10, 50);


不知道你有没有发现,上面的代码实现稍微有点问题。如果我们如下两次执行getInstance()方法,那获取到的singleton1和signleton2的paramA和paramB都是10和50。也就是说,第二次的参数(20,30)没有起作用,而构建的过程也没有给与提示,这样就会误导用户。这个问题如何解决呢?留给你自己思考,你可以在留言区说说你的解决思路。

Singleton singleton1 = Singleton.getInstance(10, 50);
Singleton singleton2 = Singleton.getInstance(20, 30);
第三种解决思路是:将参数放到另外一个全局变量中。具体的代码实现如下。Config是一个存储了paramA和paramB值的全局变量。里面的值既可以像下面的代码那样通过静态常量来定义,也可以从配置文件中加载得到。实际上,这种方式是最值得推荐的。

public class Config {public static final int PARAM_A = 123;public static final int PARAM_B = 245;
}public class Singleton {private static Singleton instance = null;private final int paramA;private final int paramB;private Singleton() {this.paramA = Config.PARAM_A;this.paramB = Config.PARAM_B;}public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

有何替代解决方案?

刚刚我们提到了单例的很多问题,你可能会说,即便单例有这么多问题,但我不用不行啊。我业务上有表示全局唯一类的需求,如果不用单例,我怎么才能保证这个类的对象全局唯一呢?

为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。这也是项目开发中经常用到的一种实现思路。比如,上一节课中讲的ID唯一递增生成器的例子,用静态方法实现一下,就是下面这个样子:

// 静态方法实现方式
public class IdGenerator {private static AtomicLong id = new AtomicLong(0);public static long getId() { return id.incrementAndGet();}
}
// 使用举例
long id = IdGenerator.getId();


不过,静态方法这种实现思路,并不能解决我们之前提到的问题。实际上,它比单例更加不灵活,比如,它无法支持延迟加载。我们再来看看有没有其他办法。实际上,单例除了我们之前讲到的使用方法之外,还有另外一种使用方法。具体的代码如下所示:

// 1. 老的使用方式
public demofunction() {//...long id = IdGenerator.getInstance().getId();//...
}// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {long id = idGenerator.getId();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);


基于新的使用方式,我们将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间依赖关系的问题。不过,对于单例存在的其他问题,比如对OOP特性、扩展性、可测性不友好等问题,还是无法解决。

所以,如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类。实际上,类对象的全局唯一性可以通过多种不同的方式来保证。我们既可以通过单例模式来强制保证,也可以通过工厂模式、IOC容器(比如Spring IOC容器)来保证,还可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。这就类似Java中内存对象的释放由JVM来负责,而C++中由程序员自己负责,道理是一样的。

总结

上面写出了五种单例模式的经典实现方案,其中懒汉式由于线程不安全是不可取的,其他几种实现在功能上都没有太大问题,可以根据需求选择。

单例存在哪些问题?

  • 单例对OOP特性的支持不友好
  • 单例会隐藏类之间的依赖关系
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好
  • 单例不支持有参数的构造函数

单例有什么替代解决方案?
为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC容器(比如Spring IOC容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。
有人把单例当作反模式,主张杜绝在项目中使用。我个人觉得这有点极端。模式没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方new的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。

工厂模式

概念

一般工厂模式分为:简单工厂、工厂方法、抽象工厂,在Gof-23中简单工厂被看做是工厂方法的一种特例,抽象工厂在实际项目中不算常见。

简单工厂

日常开发中,一些场景会遇到根据不同的条件创建不同的对象,比如根据配置文件的后缀创建不同的解析器对象等,把这些if判断和创建对象的代码单独提出一个函数,可以使逻辑更清晰,如果进一步把这个函数放到一个独立的类中,那么这个类就是简单工厂模式的类。示例:

public class RuleConfigParserFactory {public static IRuleConfigParser createParser(String configFormat) {IRuleConfigParser parser = null;if ("json".equalsIgnoreCase(configFormat)) {parser = new JsonRuleConfigParser();} else if ("xml".equalsIgnoreCase(configFormat)) {parser = new XmlRuleConfigParser();} else if ("yaml".equalsIgnoreCase(configFormat)) {parser = new YamlRuleConfigParser();} else if ("properties".equalsIgnoreCase(configFormat)) {parser = new PropertiesRuleConfigParser();}return parser;}
}

一般这个类会用Factory后缀命名,但也不是必须的,比如DateFormat、Calender。另外工厂类中创建对象的方法一般是create开头,有的也会命名为get。。new。。甚至String类的valueOf()函数。

上面的实现中,每次调用create方法都要创建一个新的parser,可以通过先创建好缓存起来,来节省内存和对象创建的时间,这可以说是单例模式和简单工厂模式的结合,示例:

public class RuleConfigParserFactory {private static final Map cachedParsers = new HashMap<>();static {cachedParsers.put("json", new JsonRuleConfigParser());cachedParsers.put("xml", new XmlRuleConfigParser());cachedParsers.put("yaml", new YamlRuleConfigParser());cachedParsers.put("properties", new PropertiesRuleConfigParser());}public static IRuleConfigParser createParser(String configFormat) {if (configFormat == null || configFormat.isEmpty()) {return null;//返回null还是IllegalArgumentException全凭你自己说了算}IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());return parser;}
}

对于上面两种实现方式,如果要添加新的解析器,就需要改动工厂类的代码,会违反开闭原则。不过实际上,如果不是频繁地改动这部分代码,稍微不符合开闭原则也是可以接受的。

工厂方法

如果非要不改动Factory类的代码该怎么做呢?一个经典的方式是利用多态,示例如下:

public interface IRuleConfigParserFactory {IRuleConfigParser createParser();
}public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {@Overridepublic IRuleConfigParser createParser() {return new JsonRuleConfigParser();}
}public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {@Overridepublic IRuleConfigParser createParser() {return new XmlRuleConfigParser();}
}public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {@Overridepublic IRuleConfigParser createParser() {return new YamlRuleConfigParser();}
}public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {@Overridepublic IRuleConfigParser createParser() {return new PropertiesRuleConfigParser();}
}

这样实现,如果新增parser,只需要新增一个实现了IRuleConfigParserFactory 接口的Factory类即可。所以,工厂方法模式比简单工厂模式更符合开闭原则。

但实际上上面的工厂方法在实现上有挺大的问题,因为工厂类没有实现if判断,所以需要先if判断类型后再决定创建哪个具体的类对象,这就和不使用工厂模式几乎没有区别了。下面这段是包括了创建Parser解析器之后使用他的一段代码示例:

public class RuleConfigSource {public RuleConfig load(String ruleConfigFilePath) {String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);IRuleConfigParserFactory parserFactory = null;// 工厂方法中只是创建了解析器,需要在前面if判断创建哪个类对象if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {parserFactory = new JsonRuleConfigParserFactory();} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {parserFactory = new XmlRuleConfigParserFactory();} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {parserFactory = new YamlRuleConfigParserFactory();} else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {parserFactory = new PropertiesRuleConfigParserFactory();} else {throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);}IRuleConfigParser parser = parserFactory.createParser();String configText = "";//从ruleConfigFilePath文件中读取配置文本到configText中RuleConfig ruleConfig = parser.parse(configText);return ruleConfig;}private String getFileExtension(String filePath) {//...解析文件名获取扩展名,比如rule.json,返回jsonreturn "json";}
}

这个问题的一般解决思路是为工厂类再创建一个简单工厂,这个简单工厂用来创建工厂类对象,代码示例如下:

public class RuleConfigSource {public RuleConfig load(String ruleConfigFilePath) {String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);if (parserFactory == null) {throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);}IRuleConfigParser parser = parserFactory.createParser();String configText = "";//从ruleConfigFilePath文件中读取配置文本到configText中RuleConfig ruleConfig = parser.parse(configText);return ruleConfig;}private String getFileExtension(String filePath) {//...解析文件名获取扩展名,比如rule.json,返回jsonreturn "json";}
}//因为工厂类只包含方法,不包含成员变量,完全可以复用,
//不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
public class RuleConfigParserFactoryMap { //工厂的工厂private static final Map cachedFactories = new HashMap<>();static {cachedFactories.put("json", new JsonRuleConfigParserFactory());cachedFactories.put("xml", new XmlRuleConfigParserFactory());cachedFactories.put("yaml", new YamlRuleConfigParserFactory());cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());}public static IRuleConfigParserFactory getParserFactory(String type) {if (type == null || type.isEmpty()) {return null;}IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());return parserFactory;}
}

其实也很好理解,就是把之前简单工厂的迭代过程重演了一遍,目标实例从解析器对象变成了类对象。

抽象工厂

不算常用,在上面两个例子中,类只有一种分类方式:IRuleConfigParser 。假设现在场景需要另一种解析规则,

针对规则配置的解析器:基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser

针对系统配置的解析器:基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser

创建的类就需要翻倍了,此时可以改为以下写法:

public interface IConfigParserFactory {IRuleConfigParser createRuleParser();ISystemConfigParser createSystemParser();//此处可以扩展新的parser类型,比如IBizConfigParser
}public class JsonConfigParserFactory implements IConfigParserFactory {@Overridepublic IRuleConfigParser createRuleParser() {return new JsonRuleConfigParser();}@Overridepublic ISystemConfigParser createSystemParser() {return new JsonSystemConfigParser();}
}public class XmlConfigParserFactory implements IConfigParserFactory {@Overridepublic IRuleConfigParser createRuleParser() {return new XmlRuleConfigParser();}@Overridepublic ISystemConfigParser createSystemParser() {return new XmlSystemConfigParser();}
}

工厂类中增加另一个解析规则的对象的实现方法。 

总结

如果某个代码块变的比较复杂,为了让代码更清晰,就可以考虑单独拆除一个工厂类。此外如果想避免if-else逻辑,就可以考虑工厂方法。简单工厂的单例实现在if-else方面和工厂方法没什么不同,感觉两个模式最大的区别是工厂方法可以在去除if-else的基础上定制化createParser()方法。

因此,当对象的创建逻辑比较简单时推荐用简单工厂模式,当创建逻辑比较复杂的时候,为了避免出现一个过于庞大的简单工厂类,推荐用工厂方法模式。

扩展-DI框架中的应用

一个简单的DI容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。

配置解析

在上面的工厂模式中,要创建哪个类对象是事先确定好的,并且是写死在工厂类代码中的。作为一个通用的框架,框架代码跟应用代码应该是高度解耦的,DI容器并不知道应用将会创建哪些对象。所以我们需要通过一种形式让应用告知DI容器要创建哪些对象,这种形式就是配置解析。

比如Spring中通过依赖注入的方式让DI容器解析配置。

对象创建

在DI容器中,如果我们给每个类都创建一个工厂类是不现实的,我们只需要将所有类对象的创建都放到一个工厂类中完成就可以了,比如BeanFactory。

你可能会说,要创建的类非常多,BeanFactory中的代码会不会线性膨胀,实际上不会,原因就是反射机制,在程序运行的过程中,动态地加载类、创建对象。

生命周期管理

在上面的简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个实现创建好的对象,也就是所谓的单例对象。比如Spring中通过@scope来配置。

除此之外,还可以配置对象是否支持懒加载。另外还可以配置对象的初始化方法和销毁前方法。

实现

最后,我们来看,BeansFactory是如何设计和实现的。这也是我们这个DI容器最核心的一个类了。它负责根据从配置文件解析得到的BeanDefinition来创建对象。

如果对象的scope属性是singleton,那对象创建之后会缓存在singletonObjects这样一个map中,下次再请求此对象的时候,直接从map中取出返回,不需要重新创建。如果对象的scope属性是prototype,那每次请求对象,BeansFactory都会创建一个新的对象返回。

实际上,BeansFactory创建对象用到的主要技术点就是Java中的反射语法:一种动态加载类和创建对象的机制。我们知道,JVM在启动的时候会根据代码自动地加载类、创建对象。至于都要加载哪些类、创建哪些对象,这些都是在代码中写死的,或者说提前写好的。但是,如果某个对象的创建并不是写死在代码中,而是放到配置文件中,我们需要在程序运行期间,动态地根据配置文件来加载类、创建对象,那这部分工作就没法让JVM帮我们自动完成了,我们需要利用Java提供的反射语法自己去编写代码。

搞清楚了反射的原理,BeansFactory的代码就不难看懂了。具体代码实现如下所示:

public class BeansFactory {private ConcurrentHashMap singletonObjects = new ConcurrentHashMap<>();private ConcurrentHashMap beanDefinitions = new ConcurrentHashMap<>();public void addBeanDefinitions(List beanDefinitionList) {for (BeanDefinition beanDefinition : beanDefinitionList) {this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition);}for (BeanDefinition beanDefinition : beanDefinitionList) {if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton()) {createBean(beanDefinition);}}}public Object getBean(String beanId) {BeanDefinition beanDefinition = beanDefinitions.get(beanId);if (beanDefinition == null) {throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);}return createBean(beanDefinition);}@VisibleForTestingprotected Object createBean(BeanDefinition beanDefinition) {if (beanDefinition.isSingleton() && singletonObjects.contains(beanDefinition.getId())) {return singletonObjects.get(beanDefinition.getId());}Object bean = null;try {Class beanClass = Class.forName(beanDefinition.getClassName());List args = beanDefinition.getConstructorArgs();if (args.isEmpty()) {bean = beanClass.newInstance();} else {Class[] argClasses = new Class[args.size()];Object[] argObjects = new Object[args.size()];for (int i = 0; i < args.size(); ++i) {BeanDefinition.ConstructorArg arg = args.get(i);if (!arg.getIsRef()) {argClasses[i] = arg.getType();argObjects[i] = arg.getArg();} else {BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());if (refBeanDefinition == null) {throw new NoSuchBeanDefinitionException("Bean is not defined: " + arg.getArg());}argClasses[i] = Class.forName(refBeanDefinition.getClassName());argObjects[i] = createBean(refBeanDefinition);}}bean = beanClass.getConstructor(argClasses).newInstance(argObjects);}} catch (ClassNotFoundException | IllegalAccessException| InstantiationException | NoSuchMethodException | InvocationTargetException e) {throw new BeanCreationFailureException("", e);}if (bean != null && beanDefinition.isSingleton()) {singletonObjects.putIfAbsent(beanDefinition.getId(), bean);return singletonObjects.get(beanDefinition.getId());}return bean;}
}

建造者模式

建造者模式可以简单概括为:

  1. 为了防止一个对象中属性过多,构造函数冗长;
  2. 如果用set赋值,无法把控有些参数必填的逻辑(如果必填参数放到构造函数中,同样会有第一点问题);
  3. 如果几个属性之间有依赖关系或约束条件,校验逻辑会变得无处安放;
  4. 如果希望对象是不可变对象,也就是创建好之后就不能在修改内部的属性,那么就不能暴露set方法;
  5. 在有些场景还能避免对象存在无效状态,比如定义一个长方形类,在创建类对象和set第一个值时,这个对象都是不可用的,只有在第二个值也set后,才是可以用的状态。

为了解决这些问题,建造者模式就派上用场了,代码示例:

public class ResourcePoolConfig {private String name;private int maxTotal;private int maxIdle;private int minIdle;private ResourcePoolConfig(Builder builder) {this.name = builder.name;this.maxTotal = builder.maxTotal;this.maxIdle = builder.maxIdle;this.minIdle = builder.minIdle;}//...省略getter方法...//我们将Builder类设计成了ResourcePoolConfig的内部类。//我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。public static class Builder {private static final int DEFAULT_MAX_TOTAL = 8;private static final int DEFAULT_MAX_IDLE = 8;private static final int DEFAULT_MIN_IDLE = 0;private String name;private int maxTotal = DEFAULT_MAX_TOTAL;private int maxIdle = DEFAULT_MAX_IDLE;private int minIdle = DEFAULT_MIN_IDLE;public ResourcePoolConfig build() {// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等if (StringUtils.isBlank(name)) {throw new IllegalArgumentException("...");}if (maxIdle > maxTotal) {throw new IllegalArgumentException("...");}if (minIdle > maxTotal || minIdle > maxIdle) {throw new IllegalArgumentException("...");}return new ResourcePoolConfig(this);}public Builder setName(String name) {if (StringUtils.isBlank(name)) {throw new IllegalArgumentException("...");}this.name = name;return this;}public Builder setMaxTotal(int maxTotal) {if (maxTotal <= 0) {throw new IllegalArgumentException("...");}this.maxTotal = maxTotal;return this;}public Builder setMaxIdle(int maxIdle) {if (maxIdle < 0) {throw new IllegalArgumentException("...");}this.maxIdle = maxIdle;return this;}public Builder setMinIdle(int minIdle) {if (minIdle < 0) {throw new IllegalArgumentException("...");}this.minIdle = minIdle;return this;}}
}// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder().setName("dbconnectionpool").setMaxTotal(16).setMaxIdle(10).setMinIdle(12).build();

与工厂模式的区别

建造者模式是让建造者类来负责对象的创建工作,工厂模式是让工厂类来负责对象的创建工作,他们之间的区别是工厂模式是创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,来具体创建不同的对象。

相关文章:

设计模式-创建型-常用:单例模式、工厂模式、建造者模式

单例模式 概念 一个类只允许创建一个对象&#xff08;或实例&#xff09;&#xff0c;那这个类就是单例类&#xff0c;这种设计模式就叫做单例模式。对于一些类&#xff0c;创建和销毁比较复杂&#xff0c;如果每次使用都创建一个对象会很耗费性能&#xff0c;因此可以把它设…...

【数据结构】【链表代码】随机链表的复制

/*** Definition for a Node.* struct Node {* int val;* struct Node *next;* struct Node *random;* };*/typedef struct Node Node; struct Node* copyRandomList(struct Node* head) {if(headNULL)return NULL;//1.拷贝结点&#xff0c;连接到原结点的后面Node…...

Linux 系统五种帮助命令的使用

Linux 系统五种帮助命令的使用 本文将介绍 Linux 系统中常用的帮助命令&#xff0c;包括 man、–help、whatis、apropos 和 info 命令。这些命令对于新手和有经验的用户来说&#xff0c;都是查找命令信息、理解命令功能的有力工具。 文章目录 Linux 系统五种帮助命令的使用一…...

Vueron引领未来出行:2026年ADAS激光雷达解决方案上市路线图深度剖析

Vueron ADAS激光雷达解决方案路线图分析&#xff1a;2026年上市展望 Vueron近期发布的ADAS激光雷达解决方案路线图&#xff0c;标志着该公司在自动驾驶技术领域迈出了重要一步。该路线图以2026年上市为目标&#xff0c;彰显了Vueron对未来市场趋势的精准把握和对技术创新的坚定…...

Java | Leetcode java题解之第458题可怜的小猪

题目&#xff1a; 题解&#xff1a; class Solution {public int poorPigs(int buckets, int minutesToDie, int minutesToTest) {if (buckets 1) {return 0;}int[][] combinations new int[buckets 1][buckets 1];combinations[0][0] 1;int iterations minutesToTest /…...

怎么不改变视频大小的情况下,修改视频的时长

视频文件太大怎么变小&#xff1f;不影响画质的四种方法 怎么不改变视频大小的情况下,修改视频的时长 截取结尾的时间你可以使用 ffmpeg 来裁剪视频的结尾部分。假设你想去掉视频最后的3秒钟&#xff0c;可以先使用 ffmpeg 获取视频的总时长&#xff0c;然后通过指定一个新的…...

数据结构:AVL树

前言 学习了普通二叉树&#xff0c;发现普通二叉树作用不大&#xff0c;于是我们学习了搜索二叉树&#xff0c;给二叉树新增了搜索、排序、去重等特性&#xff0c; 但是&#xff0c;在极端情况下搜索二叉树会退化成单边树&#xff0c;搜索的时间复杂度达到了O(N)&#xff0c;这…...

系统守护者:使用PyCharm与Python实现关键硬件状态的实时监控

目录 前言 系统准备 软件下载与安装 安装相关库 程序准备 主体程序 更改后的程序&#xff1a; 编写.NET程序 前言 在现代生活中&#xff0c;电脑作为核心工具&#xff0c;其性能和稳定性的维护至关重要。为确保电脑高效运行&#xff0c;我们不仅需关注软件优化&#xf…...

【工作流引擎集成】springboot+Vue+activiti+mysql带工作流集成系统,直接用于业务开发,流程设计,工作流审批,会签

前言 activiti工作流引擎项目&#xff0c;企业erp、oa、hr、crm等企事业办公系统轻松落地&#xff0c;一套完整并且实际运用在多套项目中的案例&#xff0c;满足日常业务流程审批需求。 一、项目形式 springbootvueactiviti集成了activiti在线编辑器&#xff0c;流行的前后端…...

SumatraPDF一打开就无响应怎么办?

结论&#xff1a;当前安装版不论32位还是64位都会出现问题。使用portable免安装版未发现相关问题。——sumatrapdf可以用于pdf, epub, mobi 等格式文件的浏览。 点击看相关问题和讨论...

棋牌灯控计时计费系统软件免费试用版怎么下载 佳易王计时收银管理系统操作教程

一、前言 【试用版软件下载&#xff0c;可以点击本文章最下方官网卡片】 棋牌灯控计时计费系统软件免费试用版怎么下载 佳易王计时收银管理系统操作教程 棋牌计时计费软件的应用也提升了顾客的服务体验&#xff0c;顾客可以清晰的看到自己的消费时间和费用。增加了消费的透明…...

Excel下拉菜单制作及选项修改

Excel下拉菜单 1、下拉菜单制作2、下拉菜单修改 下拉框&#xff08;选项菜单&#xff09;是十分常见的功能。Excel支持下拉框制作&#xff0c;通过预设选项进行菜单选择&#xff0c;可以避免手动输入错误和重复工作&#xff0c;提升数据输入的准确性和效率 1、下拉菜单制作 步…...

树莓派 mysql (兼容mariadb)登陆问题

树莓派 mysql &#xff08;兼容mariadb&#xff09;登陆问题 树莓派 MySQL 登陆问题 1 使用默认账号登陆 在首次登陆的情况下&#xff0c;系统默认为root用户授权 sudo su root ![切换到root 用户](https://img-blog.csdnimg.cn/20191019082911668.png) 2. 使用root用户登…...

智能手表(Smart Watch)项目

文章目录 前言一、智能手表&#xff08;Smart Watch&#xff09;简介二、系统组成三、软件框架四、IAP_F411 App4.1 MDK工程结构4.2 设计思路 五、Smart Watch App5.1 MDK工程结构5.2 片上外设5.3 板载驱动BSP5.4 硬件访问机制-HWDataAccess5.4.1 LVGL仿真和MDK工程的互相移植5…...

设计模式~~~

简单工厂模式(静态工厂模式) 工厂方法模式 抽象工厂角色 具体工厂角色...

Golang | Leetcode Golang题解之第458题可怜的小猪

题目&#xff1a; 题解&#xff1a; func poorPigs(buckets, minutesToDie, minutesToTest int) int {if buckets 1 {return 0}combinations : make([][]int, buckets1)for i : range combinations {combinations[i] make([]int, buckets1)}combinations[0][0] 1iterations…...

欢聚时代(BIGO)Android面试题及参考答案

网络 TCP 和 UDP 协议的区别是什么? TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Datagram Protocol,用户数据报协议)是两种不同的传输层协议,它们有以下主要区别: 一、连接性 TCP 是面向连接的协议。在通信之前,需要通过三次握手建立连接,通信结束…...

[C语言]指针和数组

目录 1.数组的地址 2.通过指针访问数组 3.数组和指针的不同点 4.指针数组 1.数组的地址 数组的地址是什么&#xff1f; 看下面一组代码 #include <stdio.h> int main() { int arr[5] {5,4,3,2,1}; printf("&arr[0] %p\n", &arr[0]); printf(&qu…...

Centos Stream 9备份与恢复、实体小主机安装PVE系统、PVE安装Centos Stream 9

最近折腾小主机&#xff0c;搭建项目环境&#xff0c;记录相关步骤 数据无价&#xff0c;丢失难复 1. Centos Stream 9备份与恢复 1.1 系统备份 root权限用户执行进入根目录&#xff1a; cd /第一种方式备份命令&#xff1a; tar cvpzf backup.tgz / --exclude/proc --exclu…...

Linux的发展历史与环境

目录&#xff1a; 引言Linux的起源早期发展企业级应用移动与嵌入式系统现代计算环境中的Linux结论 引言 Linux&#xff0c;作为开源操作系统的代表&#xff0c;已经深刻影响了全球的计算环境。从其诞生之初到如今成为服务器、嵌入式系统、移动设备等多个领域的核心&#xff0c…...

设计模式和设计原则回顾

设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...

反向工程与模型迁移:打造未来商品详情API的可持续创新体系

在电商行业蓬勃发展的当下&#xff0c;商品详情API作为连接电商平台与开发者、商家及用户的关键纽带&#xff0c;其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息&#xff08;如名称、价格、库存等&#xff09;的获取与展示&#xff0c;已难以满足市场对个性化、智能…...

.Net Framework 4/C# 关键字(非常用,持续更新...)

一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill

视觉语言模型&#xff08;Vision-Language Models, VLMs&#xff09;&#xff0c;为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展&#xff0c;机器人仍难以胜任复杂的长时程任务&#xff08;如家具装配&#xff09;&#xff0c;主要受限于人…...

Mysql故障排插与环境优化

前置知识点 最上层是一些客户端和连接服务&#xff0c;包含本 sock 通信和大多数jiyukehuduan/服务端工具实现的TCP/IP通信。主要完成一些简介处理、授权认证、及相关的安全方案等。在该层上引入了线程池的概念&#xff0c;为通过安全认证接入的客户端提供线程。同样在该层上可…...

小智AI+MCP

什么是小智AI和MCP 如果还不清楚的先看往期文章 手搓小智AI聊天机器人 MCP 深度解析&#xff1a;AI 的USB接口 如何使用小智MCP 1.刷支持mcp的小智固件 2.下载官方MCP的示例代码 Github&#xff1a;https://github.com/78/mcp-calculator 安这个步骤执行 其中MCP_ENDPOI…...

GraphRAG优化新思路-开源的ROGRAG框架

目前的如微软开源的GraphRAG的工作流程都较为复杂&#xff0c;难以孤立地评估各个组件的贡献&#xff0c;传统的检索方法在处理复杂推理任务时可能不够有效&#xff0c;特别是在需要理解实体间关系或多跳知识的情况下。先说结论&#xff0c;看完后感觉这个框架性能上不会比Grap…...

用鸿蒙HarmonyOS5实现国际象棋小游戏的过程

下面是一个基于鸿蒙OS (HarmonyOS) 的国际象棋小游戏的完整实现代码&#xff0c;使用Java语言和鸿蒙的Ability框架。 1. 项目结构 /src/main/java/com/example/chess/├── MainAbilitySlice.java // 主界面逻辑├── ChessView.java // 游戏视图和逻辑├── …...

关于 ffmpeg设置摄像头报错“Could not set video options” 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/148515355 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…...