Spring框架中的设计模式 (四)
本文是Spring框架中使用的设计模式第四篇。本文将在此呈现出新的3种模式。
一开始,我们会讨论2种结构模式:适配器和装饰器。在第三部分和最后一部分,我们将讨论单例模式。
适配器
当我们需要在给定场景下(也就是给定接口)想要不改变自身行为而又想做到一些事情的情况下(就是我给电也就是接口了,你来做事也就是各种电器),使用适配器设计模式(这里再说一点,就相当于我们再一个规章制度的环境下,如何去适应并达到我们期待的效果,放在架构设计这里,可以拿一个php系统和一个Java系统来说,假如两者要互相调用对方的功能,我们可以设计一套对外的api来适配)。这意味着在调用此对象之前,我们将更改使用对象而不改变机制。拿一个现实中的例子进行说明,想象一下你想要用电钻来钻一个洞。要钻一个小洞,你会使用小钻头,钻一个大的需要用大钻头。可以看下面的代码:
1 | public class AdapterTest { |
以上代码的结果如下:
1 | Small hole is made byt SmallDrillBit |
可以看到,hole 是由所匹配的DrillBit对象制成的。如果孔的直径小于10,则使用SmallDrillBit。如果它更大,我们使用BigDrillBit。
思路就是,要打洞,那就要有打洞的工具,这里提供一个电钻接口和钻头。电钻就是用来打洞的,所以,它就一个接口方法即可,接下来定义钻头的接口,无非就是钻头的尺寸标准,然后搞出两个钻头实现类出来,接下来就是把钻头和电钻主机组装起来咯,也就是Drill
类,里面有电钻接口+钻头(根据要钻的孔大小来确定用哪个钻头),其实也就是把几个单一的东西组合起来拥有丰富的功能,最后我们进行封装下:HoleMakerImpl
,这样只需要根据尺寸就可以打相应的孔了,对外暴露的接口极为简单,无须管内部逻辑是多么复杂
Spring使用适配器设计模式来处理不同servlet容器中的加载时编织(load-time-weaving)。在面向切面编程(AOP)中使用load-time-weaving,一种方式是在类加载期间将AspectJ的方面注入字节码。另一种方式是对类进行编译时注入或对已编译的类进行静态注入。
我们可以从关于Spring和JBoss的处理接口这里找到一个很好的例子,它包含在org.springframework.instrument.classloading.jboss包中。我们检索JBossLoadTimeWeaver类
负责JBoss容器
的编织管理。然而,类加载器对于JBoss 6
(使用JBossMCAdapter
实例)和JBoss 7/8
(使用JBossModulesAdapter
实例)是不同的。根据JBoss
版本,我们在JBossLoadTimeWeaver
构造函数中初始化相应的适配器(与我们示例中的Drill
的构造函数完全相同):
1 | public JBossLoadTimeWeaver(ClassLoader classLoader) { |
而且,此适配器所创建的实例用于根据运行的servlet容器版本进行编织操作:
1 |
|
总结:适配器模式,其实就是我们用第一人称的视角去看世界,我想拓展我自己的技能的时候,就实行拿来主义,就好比这里的我是电钻的视角,那么我想拥有钻大孔或者小孔的功能,那就把钻头拿到手组合起来就好。
和装饰模式的区别:装饰模式属于第三人称的视角,也就是上帝视角!我只需要把几个功能性的组件给拿到手,进行组合一下,实现一个更加
niubility
的功能这里提前说下,这样看下面的内容能好理解些。下面解释装饰模式
装饰
这里描述的第二种设计模式看起来类似于适配器。它是装饰模式。这种设计模式的主要作用是为给定的对象添加补充角色。举个现实的例子,就拿咖啡来讲。通常越黑越苦,你可以添加(装饰
)糖和牛奶,使咖啡不那么苦。咖啡在这里被装饰的对象,糖与牛奶是用来装饰的。可以参考下面的例子:
1 | public class DecoratorSample { |
上面这个简单的装饰器的小例子是基于对父方法的调用,从而改变最后的属性(我们这里是指价格和加糖多少)。在Spring中,我们在处理与Spring管理缓存同步事务的相关类中可以 发现装饰器设计模式的例子。这个类是org.springframework.cache.transaction.TransactionAwareCacheDecorator。
这个类的哪些特性证明它是org.springframework.cache.Cache对象的装饰器?首先,与我们的咖啡示例一样,TransactionAwareCacheDecorator
的构造函数接收参数装饰对象(Cache):
1 | private final Cache targetCache; |
其次,通过这个对象,我们可以得到一个新的行为:为给定的目标缓存创建一个新的TransactionAwareCache。这个我们可以在TransactionAwareCacheDecorator
的注释中可以阅读到,其主要目的是提供缓存和Spring事务之间的同步级别。这是通过org.springframework.transaction.support.TransactionSynchronizationManager中的两种缓存方法实现的:put
和 evict
(其实最终不还是通过targetCache
来实现的么):
1 |
|
这种模式看起来类似于适配器,对吧?但是,它们还是有区别的。我们可以看到,适配器将对象适配到运行时环境,即。如果我们在JBoss 6中运行,我们使用与JBoss 7不同的类加载器。Decorator每次使用相同的主对象(Cache)工作,并且仅向其添加新行为(与本例中的Spring事务同步),另外,可以通过我在解读这个设计模式之前的说法来区分二者。
我们再以springboot的初始化来举个例子的,这块后面会进行仔细的源码分析的,这里就仅仅用设计模式来说下的:
1 | /** |
从注释可以看出 ApplicationListener
要先行到位的,然后就是started的时候Event published
走起,接着就是Environment
配置好,ApplicationContext
进行初始化完毕,那我们去看ApplicationListener
的源码: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
36
37
38
39
40
41
42
43
44
45
46
47
48/**
* Listener for the {@link SpringApplication} {@code run} method.
* {@link SpringApplicationRunListener}s are loaded via the {@link SpringFactoriesLoader}
* and should declare a public constructor that accepts a {@link SpringApplication}
* instance and a {@code String[]} of arguments. A new
* {@link SpringApplicationRunListener} instance will be created for each run.
*
* @author Phillip Webb
* @author Dave Syer
*/
public interface SpringApplicationRunListener {
/**
* Called immediately when the run method has first started. Can be used for very
* early initialization.
*/
void started();
/**
* Called once the environment has been prepared, but before the
* {@link ApplicationContext} has been created.
* @param environment the environment
*/
void environmentPrepared(ConfigurableEnvironment environment);
/**
* Called once the {@link ApplicationContext} has been created and prepared, but
* before sources have been loaded.
* @param context the application context
*/
void contextPrepared(ConfigurableApplicationContext context);
/**
* Called once the application context has been loaded but before it has been
* refreshed.
* @param context the application context
*/
void contextLoaded(ConfigurableApplicationContext context);
/**
* Called immediately before the run method finishes.
* @param context the application context or null if a failure occurred before the
* context was created
* @param exception any run exception or null if run completed successfully.
*/
void finished(ConfigurableApplicationContext context, Throwable exception);
}
看类注释我们可以知道,需要实现此接口内所定义的这几个方法,ok,来看个实现类:
1 | /** |
从上可以看出,EventPublishingRunListener
里对接口功能的实现,主要是通过SpringApplication
ApplicationEventMulticaster
来实现的,自己不干活,挂个虚名,从上帝模式的角度来看,这不就是应用了装饰模式来实现的么。
更多源码解析请关注后续的本人对Spring框架全面的重点部分解析系列博文
单例
单例,我们最常用的设计模式。正如我们在很多Spring Framework中关于单例和原型bean的文章(网上太多了)中已经看到过的,单例是几个bean作用域中的中的一个。此作用域在每个应用程序上下文中仅创建一个给定bean的实例。与signleton设计模式有所区别的是,Spring将实例的数量限制的作用域在整个应用程序的上下文。而Singleton设计模式在Java应用程序中是将这些实例的数量限制在给定类加载器管理的整个空间中。这意味着我们可以为两个Spring的上下文(同一份配置文件起两个容器,也就是不同端口的容器实例)使用相同的类加载器,并检索两个单例作用域的bean。
在看Spring单例应用之前,让我们来看一个Java的单例例子:
1 | public class SingletonTest { |
这个测试例子证明,只有一个由SingletonsHolder所持有的President实例。在Spring中,我们可以在bean工厂中找到单例应用的影子(例如在org.springframework.beans.factory.config.AbstractFactoryBean中):
1 | /** |
我们看到,当需求对象被视为单例时,它只被初始化一次,并且在每次使用同一个bean类的实例后返回。我们可以在给定的例子中看到,类似于我们以前看到的President情况。将测试bean定义为:
1 | <bean id="shoppingCart" class="com.migo.data.ShoppingCart" /> |
测试用例如下所示:
1 | public class SingletonSpringTest { |
这个测试案例显示了Spring单例模式与纯粹的单例设计模式的主要区别。尽管使用相同的类加载器来加载两个应用程序上下文,但是ShoppingCart的实例是不一样的。但是,当我们比较两次创建并属于相同上下文的实例时,我们认为它们是相等的。
也正因为有了单例,Spring可以控制在每个应用程序上下文中只有一个这样指定的bean的实例可用。因为适配器,Spring可以决定使用由谁来处理JBoss servlet
容器中的加载时编织,也可以实现ConfigurableListableBeanFactory
的相应实例。第三种设计模式,装饰器,用于向Cache对象添加同步功能,还有Springboot的容器初始化。
其实对于适配器和装饰者确实有太多的相似的地方,一个是运行时选择,一个是加料组合产生新的化学效应,还有从看待事物的角度不同得到不同的行为,适配适配,更注重面向接口的实现,而内部又根据不同情况调用面向一套接口的多套实现的实例的相应方法来实现所要实现的具体功能,装饰者更注重添油加醋,通过组合一些其他对象实例来让自己的功能实现的更加华丽一些(达到1+1>2的这种效果)。一家之言,有更好的理解可以联系我。
有更多疑问请加qq群523409180 讨论的