今天我们来学习设计模式中的原型模式,首先我们还是对原型模式做一个简单介绍。
原型模式是一个创建型模式。
目的:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
结构:
Prototype
声明一个克隆自身的接口
ConcretePrototype
实现Clone方法的类,该方法用来克隆自身
Client
客户代码,客户可以通过调用ProtoType实例的Clone方法来获取一个ProtoType实例克隆自身新创建的对象。
优点:Prototype模式对客户隐藏了特定的产品类,使得客户代码无需改变就可以使用相关应用的的类,其实这就是框架的核心思想之一,即隐藏底层实现,客户只需要关心自己的业务逻辑代码。这也是很多其他设计模式的优点。接下来介绍独属于原型模式最大的优点,即原型模式大大简化了我们创建一个新对象的成本,由于现在已经存在一个类似的原型对象,假如我们使用new创建一个新对象,如果该对象的创建涉及复杂的计算,数据库访问,网络请求的消耗大量时间的操作。这无疑不增加了我们获取一个新对象的成本,使用原型模式我们可以从已有对象中获取数据,使用Clone方法简化构造方法中非必须执行的耗时操作,从而提高我们创建对象的效率。
接下来我们来分析原型模式在java中的应用实例。
首先介绍的一个实例就是我们的Objec类,其实Object就是一个原型接口我们可以看到Object类里存在一个Clone方法
我们可以看一下jdk源码中有多少个类实现了这个方法
一共有332个类实现了这个方法,我们一起来看几个常见的类,第一个ArrayList,我们来看一下它的Clone方法:
首先该方法调用了父类的父类的Clone方法即Object类的Clone方法,protected native Object clone() throws CloneNotSupportedException;
在 Object
类中定义了一个可以被重写的本地方法,用于创建对象的一个副本,但一定注意默认行为是浅克隆,并且只有在对象所属的类实现了 Cloneable
接口时才能成功调用。浅克隆意味着对象内部的属性如果引用其他对象,那么复制时只会复制引用地址,而不是重新分配内存给引用的对象,由于是浅拷贝所以接下来使用Arrays.copy方法来创建一个新数组,Arrays.copyOf(elementData, size) 是 Java 中的一个静态方法,用于复制指定数组 elementData 到一个新的数组,新数组的长度由 size 参数指定。其详细原理可以看我的这篇文章。
这就相当于进行了为v的数据成员elementData进行了新的内存分配,即进行了深拷贝。modCout变量是用来实现java快速失败机制的。
Java的快速失败机制是一种用于保证多线程环境下集合类的线程安全性的机制。在多线程环境下,如果一个线程对集合进行了修改(增删改操作),而另一个线程正在遍历这个集合,那么就可能会出现并发修改异常(ConcurrentModificationException)。为了避免这种情况,Java引入了快速失败机制。所谓快速失败机制,就是在集合被修改时立即抛出异常,而不是等到遍历时才发现异常。这样可以在修改集合时立即通知正在遍历集合的线程,从而避免了并发修改造成的问题。快速失败机制的实现是通过在集合的内部维护一个计数器(modCount),每次修改集合时都会增加计数器的值。而在遍历集合时,会将计数器的值保存下来,并在遍历过程中检查计数器的值是否发生变化。如果计数器的值发生变化,就说明集合发生了修改,就会立即抛出并发修改异常。需要注意的是,快速失败机制并不能保证线程安全,只能在一定程度上避免并发修改带来的问题。如果需要在多线程环境下使用集合,还需要额外采取其他措施来保证线程安全,如使用同步机制(如synchronized、Lock等)或使用线程安全的集合类(如ConcurrentHashMap、CopyOnWriteArrayList等)。
最后返回拷贝对象v即可。
我们再来看LinkedList的clone方法:
方法思想是一致的,即先调用Object的clone方法进行浅拷贝,然后再根据自身的数据结构以及自身需求完成对自身的拷贝创建一个新对象。
看了这两个clone方法源码之后我们知道使用原型模式棘手问题就是如何实现clone方法,实现克隆方法一定要注意的是根据我们自己的需求我们得知道对象内的所有数据成员到底是需要哪些深拷贝还是浅拷贝,区别的关键就是这些对象是否需要重新开辟一段内存空间。
我们再来看第二个例子即spring框架中我们可以设置bean对象的获取方式为prototype,我们写一段代码来看一下spring容器是如何获取原型的bean对象的。
测试代码也很简单,代码如下:
代码比较简单,我们使用Component注解将该类注入到Spring容器里,使用@Scope注解设置获取bean对象的方式为原型模式,我们通过打断点调试,看getbean方是如何获取到该对象的。
上面这一部分我已在单例模式讲解的时候详细分析过,这是对获取模式为单例的bean对象的获取方式,这里不在赘述,详细原理的看我的另外一篇文章。
深入JAVA框架学习23种设计模式-单例模式https://blog.csdn.net/weixin_61854715/article/details/140511247
我们接着向下分析doGetBean方法。
向下的第一个判断并不会进入,由于该对象是原型获取,所以shareInstance对象为null,前面一段,代码获取的是单例的bean实例,第一个判断代码功能其实就是一个日志追踪功能,根据当前bean实例的状态信息输出不同的日志信息,两个判断的bean情况分别如下。
1.如果当前Bean正在创建中(this.isSingletonCurrentlyInCreation(beanName)
返回true
),这通常意味着存在循环引用的情况。循环引用是指两个或多个Bean相互依赖,导致它们无法单独完成初始化。在这种情况下,日志会指出“正在返回尚未完全初始化的单例Bean的急切缓存实例”,这是循环引用处理的一部分,Spring会提前暴露Bean的引用,以便在初始化过程中能够相互注入。
2.如果当前Bean不是正在创建中,则简单地记录“正在返回缓存的单例Bean实例”,表示这是直接从缓存中获取的一个已经完全初始化的Bean实例。
这一块的代码就是负责获取单例的bean对象。原理的话看我们上面的文章,我们继续看底下的代码。
首先也是调用了一个isPrototypeCurrentlyInCreation方法进行判断该方法功能与this.isSingletonCurrentlyInCreation类似及判断Bean是否还处于创建中,即处理循环依赖问题,区别就是一个判断的是单例bean的对象,一个判断的是原型bean对象,在的话就抛出一个BeanCurrentlyInCreationException异常。
我们接着看下面的代码,首先定义了一个parentBeanFactory变量并调用this.getParentBeanFactory进行初始化,该方法非常简单就是获取AbstractBeanFactory类的一个数据成员。
回到doGetBean方法,由于parentBeanFactory为空,也不会执行下面一段代码,也不是今天的重点我们就不展开讲解。大致说一下其功能。
-
获取父BeanFactory:首先,通过
this.getParentBeanFactory()
方法获取当前BeanFactory的父BeanFactory。这是因为在Spring的BeanFactory层次结构中,子BeanFactory可以继承父BeanFactory中的Bean定义。 -
检查Bean定义:接着,通过
this.containsBeanDefinition(beanName)
方法检查当前BeanFactory是否包含指定名称的Bean定义。如果不包含(即返回false
),则继续执行后续逻辑。 -
准备查找的Bean名称:通过
this.originalBeanName(name)
方法获取原始的Bean名称(如果进行了别名或其他名称转换)。这是因为有时候我们可能通过别名或其他方式引用Bean,但查找时应该使用原始的Bean名称。 -
父BeanFactory的类型检查:判断父BeanFactory是否是
AbstractBeanFactory
的实例。如果是,那么可以直接调用doGetBean
方法,这个方法提供了更丰富的参数来支持Bean的获取,包括类型检查(typeCheckOnly
)、构造函数参数(args
)等。 -
根据参数类型获取Bean:
- 如果
args
(构造函数参数)不为空,则调用parentBeanFactory.getBean(nameToLookup, args)
来尝试获取Bean。这种方式允许通过指定构造函数参数来获取Bean实例。 - 如果
requiredType
(需要的Bean类型)不为空,但args
为空,则调用parentBeanFactory.getBean(nameToLookup, requiredType)
来尝试根据Bean的类型获取Bean实例。 - 如果
args
和requiredType
都为空,则直接调用parentBeanFactory.getBean(nameToLookup)
来获取Bean实例。
- 如果
-
返回结果:根据以上逻辑,如果在父BeanFactory中找到了Bean,则返回该Bean实例;否则,如果当前BeanFactory及其父BeanFactory中都没有找到指定的Bean,那么可能会抛出异常(具体取决于
doGetBean
方法的实现和调用时的配置)。
我们继续看下一段代码,
还是一个判断,功能是用于指示当前的Bean获取操作是否仅用于类型检查,而不实际创建或返回Bean实例。
当typeCheckOnly为true时,表示当前的调用仅用于类型检查,不需要真正创建或获取Bean实例。因此,不会调用markBeanAsCreated(beanName)来标记Bean为已创建。
当typeCheckOnly为false时,表示需要实际获取Bean实例。在成功获取或创建Bean实例后,会调用markBeanAsCreated(beanName)方法,将Bean标记为已创建状态。这个标记通常用于跟踪Bean的生命周期状态,确保Bean在后续操作中不会被重复创建或初始化。
我们继续看下一段代码块。
我们也大致说一下其功能。
-
获取合并后的Bean定义:通过
this.getMergedLocalBeanDefinition(beanName)
方法获取指定Bean名称的合并后的Bean定义。在Spring中,Bean定义可以包含多个源(如XML配置文件、注解等),getMergedLocalBeanDefinition
方法会将这些源合并成一个完整的Bean定义。 -
检查合并后的Bean定义:通过
this.checkMergedBeanDefinition(mbd, beanName, args)
方法对合并后的Bean定义进行检查。这个方法的具体实现可能包括验证Bean定义的完整性、类型匹配等。 -
处理依赖关系:
- 通过
mbd.getDependsOn()
方法获取当前Bean所依赖的其他Bean的名称数组。 - 如果存在依赖(即
dependsOn
不为空),则遍历这些依赖名称。 - 对于每个依赖名称,首先检查是否存在循环依赖(即当前Bean依赖于另一个Bean,而那个Bean又依赖于当前Bean)。如果存在循环依赖,则抛出
BeanCreationException
异常。 - 如果不是循环依赖,则通过
this.registerDependentBean(dep, beanName)
方法注册依赖关系,表示当前Bean依赖于另一个Bean。 - 接下来,尝试通过
this.getBean(dep)
方法获取依赖的Bean实例。如果无法获取(即抛出NoSuchBeanDefinitionException
异常),则同样抛出BeanCreationException
异常,指出当前Bean依赖于一个缺失的Bean。
- 通过
接下来这段代码是我们今天的重点第一个判断显然是对单例模式bean对象的获取,最主要我们来看第二个,即对原型模式bean对象获取方法,首先将prototypeInstance赋为空值null,定义了一个Object对象prototypeInstance,注意这两个对象并不是一个对象,它们所处的作用域不同即位于不同的{}内的代码块,prototypeInstance在上个代码块中时String[]对象。注意在代码块中变量优先级最高的是当前代码块定义的局部变量。即该对象是现在是Object而不是String[].我们继续看try语句块。
首先调用了beforPropotypeCreanation方法进行一些bean对象创建前的检查工作,比如日志记录等。
接下来是关键方法createBean方法,这是真正获取bean对象的方法,我们来看这个方法的源码:
该方法首先进行了日志记录工作,我们不仔细讲解,我们来看下面关键代码块。
首先将一个RootBeanDefinition对象mbdToUse赋值为mbd,赋值为传入的mbd对象,该对象存储的就是创建的类信息。
Root bean: class [com.lsy.service8001.test.BeanTest]; scope=prototype; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [D:\JAVA\springcloudStudy\springcloudStudy\springcloud-provider-dept-8001\target\classes\com\lsy\service8001\test\BeanTest.class]
我们可以看到存储了我们要创建类的具体信息。
接着使用Class类的resolveBeanClass方法动态获取创建类,这个方法会根据Bean定义中的信息(如类名beanClassName
)来加载并返回对应的Class
对象。接着判断resolvedClass,mdb存储Bean对象的类信息和类名是否都不为空,都不为空,就赋值给mbdToUse为一个新的RootBeanDefinition对象,设置mbdToUse存储的BeanClass类信息为resolvedClass。
接下来是一段try -catch语句块,代码尝试调用mbdToUse.prepareMethodOverrides()
方法来准备Bean定义中的方法覆盖。在Spring中,方法覆盖允许你通过配置文件或注解来覆盖Bean中的方法。这一步是验证这些覆盖是否有效,并确保它们不会与Bean类中原有的方法发生冲突。如果在准备方法覆盖的过程中发生了BeanDefinitionValidationException
异常,那么代码会捕获这个异常,并抛出一个新的BeanDefinitionStoreException
异常。这个新异常包含了原始Bean定义的资源描述、Bean名称以及一个描述性的错误消息。
我们继续看第二段代码块,首先定义了一个beanInstance Object对象,第一段try-catch语句块先调用resolveBeforeInstntiation方法对beanInstance进行初始化,我们来看该方法的源码:
该方法首先检查是否已解决实例化前的问题:首先,它检查 mbd.beforeInstantiationResolved 是否未被显式设置为 false。这个标志用于避免重复处理相同的 Bean。如果已经设置为 true 或 false 之外的值,则不执行任何操作。检查 Bean 是否为合成的:接着,它检查 Bean 定义 (mbd) 是否不是合成的。合成的 Bean 是由 Spring 容器内部自动创建的,通常不需要用户干预或额外的 BeanPostProcessor 处理。检查是否存在 InstantiationAwareBeanPostProcessor:然后,它检查 Spring 容器中是否存在实现了 InstantiationAwareBeanPostProcessor 接口的 BeanPostProcessor。这个接口提供了在实例化之前和之后进行回调的方法。
检查完后确定目标类型:如果上述条件都满足,它会尝试确定 Bean 的目标类型 (targetType)。这通常是通过解析 Bean 定义中的信息来完成的。应用实例化前的处理器:如果目标类型不为 null,则调applyBeanPostProcessorsBeforeInstantiation 方法。这个方法会遍历所有注册的 InstantiationAwareBeanPostProcessor,并调用它们的 postProcessBeforeInstantiation 方法。这些处理器有机会返回一个早期创建的 Bean 实例,从而绕过 Spring 的标准实例化过程。
最后,无论是否成功解析了 Bean 实例,它都会更新 mbd.beforeInstantiationResolved 标志,以反映是否已经在实例化之前解析了 Bean。
我们回到CreateBean方法,假如我们未从resolveBeforeInstntiation方法获取到类实例我们继续原型bean实例在spring标准化获取类对象的流程,其实这就是一种缓存机制。
我们接着看第二段try-catch语句块,这个语句块就是bean实例在spring标准化获取类对象的过程,这个语句块调用重名方法doCreateBean方法来看spring标准化获取类对象的过程,该方法源码如下。
这个函数非常长,首先定义了一个BeanWrapper对象InstanceWrapper第一个是对单例bean对象的处理我们跳过,我们看第二个,第二个方法调用的是createBeanInstance方法初始化instanceWrapper,我们来看这个方法的源码和功能介绍如下。
createBeanInstance
方法是 Spring 框架中用于创建 Bean 实例的核心方法之一。这个方法根据 RootBeanDefinition
中的定义和提供的参数,来实例化一个 Bean。以下是该方法的逐步解析:
-
解析 Bean 类:首先,通过调用
resolveBeanClass
方法解析出 Bean 的类(beanClass
)。这是根据 Bean 定义中的信息来完成的。 -
检查访问权限:接着,检查解析出的 Bean 类是否是公开的(
public
)。如果不是公开的,并且 Bean 定义中没有允许非公开访问(nonPublicAccessAllowed
),则抛出BeanCreationException
异常。
接下来是使用4种情况来获取bean实例对象
-
使用实例供应商(Instance Supplier):如果 Bean 定义中提供了实例供应商(
instanceSupplier
),则直接通过调用obtainFromSupplier
方法从该供应商中获取 Bean 实例。 -
使用工厂方法:如果定义了工厂方法(
factoryMethodName
),则通过调用instantiateUsingFactoryMethod
方法来实例化 Bean。这通常发生在 Bean 的实现是一个工厂类,并且需要通过工厂方法来获取 Bean 实例的情况。 -
构造函数解析和自动装配:
- 如果没有使用实例供应商或工厂方法,那么需要进一步检查构造函数。
- 如果已经解析了构造函数或工厂方法(
resolvedConstructorOrFactoryMethod
不为null
),则根据是否需要自动装配(constructorArgumentsResolved
)来调用autowireConstructor
方法进行自动装配,或者直接调用instantiateBean
方法进行实例化。 - 如果没有预先解析的构造函数,那么会通过
determineConstructorsFromBeanPostProcessors
方法从 Bean 后处理器中确定构造函数。如果没有找到合适的构造函数,并且 Bean 定义中允许自动装配,并且没有显式的构造函数参数值,也没有提供额外的参数(args
),则会尝试使用 Bean 定义中指定的首选构造函数(preferredConstructors
)进行自动装配。如果没有首选构造函数,则直接调用instantiateBean
方法进行实例化。
-
实例化 Bean:最后,如果上述条件都不满足,或者确定了要使用的构造函数后,会调用
instantiateBean
方法来实例化 Bean。这个方法会根据 Bean 的类、构造函数以及任何可用的构造函数参数来创建 Bean 的新实例。
从这个方法我们已经可以看出spring框架获取原型实例并不是严格意义上的原型模式,它是一种更加广泛的概念,但他的核心目的和依然和原型模式一样就是获取一个新的对象,同时他也设置了各种缓存机制简化我们获取新对象的成本,在思想上它就是原型模式的思想,即简化获取一个新对象的成本。假如你看了上面单例模式的链接文章,你可以对比一下spring容器中单例的bean对象获取成本是远小于原型的bean对象的,这启示我们在自己的项目开发中注册bean对象的时候尽量使用单例,这样可以大大提高我们系统的性能。