书接上回,我们继续较为深入的分析Spring中常用的一些设计模式.
上一章节,我们主要分析Spring的一些架构原则(其实也是软件设计的6大设计原则),并且较为详细的阐述了工厂模式和代理模式的常用实现方式。这一节主要包含以下内容:
- 单例模式
- 委派模型(不是23中设计模式的成员)
- 策略模式
- 模式方法描述
单例模式,顾名思义,就是在JVM的整个运行期间,一个类只会存在一个对象。因此叫做单例模式。
如Spring实现的IOC,默认就是单例模式(可以通过修改scope=“prototype”来改变该行为),并且通过三级缓存的架构,完美解决了循环依赖的问题。
单例模式实现的一个基本原则是:定义默认构造方法为private。
public class HungrySingletion {
private static final HungrySingletion instance = new HungrySingletion();
private HungrySingletion(){
}
public static HungrySingletion getInstance(){
return HungrySingletion.instance;
}
}
优点:
- 实现及其简单。
- 不存在线程安全问题。
缺点:
- 无论用不用该单例对象,都会默认初始化,造成资源浪费。
是否推荐:不推荐。
public class UnsafeLazySingleton {
private static UnsafeLazySingleton instance = null;
private UnsafeLazySingleton(){
}
public static UnsafeLazySingleton getInstance(){
if (instance == null){
// Step1
instance = new UnsafeLazySingleton(); // Step2
}
return instance;// Step3
}
}
优点:
- 设计简单
- 用到时才会初始化,节约资源。
缺点:
- 存在线程安全问题
是否推荐:
- 单线程程序:推荐
- 多线程程序:禁用
也许有不了解多线程的小伙伴不清楚为什么会线程不安全,我简单介绍一下。注意上面程序注释Step的一行。
时间节点 | thread-1 | thread-2 |
---|---|---|
t1 | 执行到Step1:判断instance == null为true | 为获得CPU时间片 |
t2 | 失去CPU时间片 | 获得CPU时间片:执行Step1语句:判断instance == null为true |
t3 | 失去CPU时间片 | 继续执行Step2和Step3,返回一个对象A |
t4 | 获得CPU时间片:执行Step2,Step3 | 失去CPU时间片 |
t5 | 返回,返回对象B |
public class BadSafeLazySingleton {
private static BadSafeLazySingleton instance= null;
private BadSafeLazySingleton(){
}
public static synchronized BadSafeLazySingleton getInstance(){
if (instance == null){
instance = new BadSafeLazySingleton();
}
return instance;
}
}
优点
- 设计简单
- 用到时才会初始化,节约资源。
- 线程安全
缺点:
- 无论instance是否为null,都需要加锁。高并发情况下,效率会很低。
是否推荐:
- 并发较低时:推荐
- 高并发情况下:不推荐
public class LikeSafeLazySingleton {
// 注意这两行代码:一个多了volatile关键字
// 如果不加volatile关键字,则由于new 一个对象时,可能发生指令重排序,导致另一个线程获取到一个未初始化完成的对象。
// private static LikeSafeLazySingleton instance = null;
private static volatile LikeSafeLazySingleton instance = null;
private LikeSafeLazySingleton(){
}
public static LikeSafeLazySingleton getInstance(){
// 先进行第一次判断instance是否为null,不为null,这直接返回
if (instance == null){
// 同步阻塞,执行初始化过程
synchronized (LikeSafeLazySingleton.class){
// 再次判断instance是否为null,避免第一次判断为null后,另一个线程初始化了instance对象
if (instance == null){
// 初始化instance对象
instance = new LikeSafeLazySingleton();
}
}
}
return instance;
}
}
优点:
- 用到时才会初始化,节约资源。
- 线程安全。(注意上面instance定义时的volatile关键字)
缺点:
- 设计相对复杂。
- 初学者(或者对并发原理不了解的小伙伴)可能会遗漏instance定义时的volatile关键字,造成线程安全问题。
是否推荐:较为推荐
public class PlaceholdLazySingleton {
private PlaceholdLazySingleton() {
}
/*
* 根据JVM类加载特性,内部类不会再启动时加载,只有调用时,才会加载
* */
private static class Placehold {
private static PlaceholdLazySingleton instance = new PlaceholdLazySingleton();
}
public static final PlaceholdLazySingleton getInstance() {
return Placehold.instance;
}
}
优点:
- 用到时才会初始化,节约资源。
- 线程安全。
缺点:
- 设计相对复杂,需要较为深入理解JVM的类加载原理。
是否推荐:较为推荐
注意:以上单例模式,都无法避免两个问题:
- 可以通过反射的方式暴力获取多个对象,从而破坏单例。
- 如果单例类实现了Serializable接口,可以通过序列化与反序列化的方式获取到多个对象,从而破坏单例
因此下面介绍一种可以避免反射暴力创建新对象,即序列化与反序列化获取新对象的一种单例模式。即枚举单例模式-----顾名思义,就是通过枚举实现单例模式。
不过通过枚举实现的单例模式,如果反编译字节码分析的话,可以看到,其实也是一种饿汉式单例模式。
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return EnumSingleton.INSTANCE;
}
}
优点:
- 设计简单
- 线程安全。
是否推荐:推荐
委派模式的基本作用就是负责任务的调用和分配,跟代理模式很像,但是代理模式注重过程,而委派模式注重结果。如SpringMVC中的DispatcherServlet
就是典型的委派模式,另外NIO编程中的Reactor模型也是基于IO事件派发的委派模式来完成的。
现在模拟一下经典的IO事件派发模型
:
- 首先,我们需要定义一系列的事件,此处可以通过枚举来表示一个个的IO事件。方便起见,这里只抽象两个事件READ,WRIET
public enum IOEvent {
READ,
WRITE
}
- 其次,我们定义事件的抽象处理器,并针对每个IO事件定制自己的处理逻辑。此处可以通过策略模式或者模板方法模式来实现。此处我们介绍模板方法的实现方式。
// 抽象的模板类:其中定义两个抽象方法
public abstract class AbstractEventHandler {
public abstract IOEvent support();
public void handler(IOEvent event, Object params){
System.out.println("common code: the event is " + event.name());
doHandler(params);
}
protected abstract void doHandler(Object params);
}
// Read事件的处理器类
public class ReadEventHandler extends AbstractEventHandler {
@Override
public IOEvent support() {
return IOEvent.READ;
}
@Override
protected void doHandler(Object params) {
System.out.println("I am ReadEventHandler: " + params);
}
}
// Write事件的处理器类
public class WriteEventHandler extends AbstractEventHandler {
@Override
public IOEvent support() {
return IOEvent.WRITE;
}
@Override
protected void doHandler(Object params) {
System.out.println("I am WriteEventHandler: " + params);
}
}
- 应用工厂方法,对事件的处理器做一个顶层的抽象,屏蔽底层实现细节。如果是基于Spring的应用,此处可以用Spring的Bean容器来管理。这里我们通过
HashMap
来管理这些工厂对象。同时结合上面提到的单例实现,实现一个单例的工厂类。
// 此处如果通过Spring,能够更好的管理这些对象
public class EventHandlerFactory {
private final Map<IOEvent, AbstractEventHandler> handlerMapping;
private EventHandlerFactory(){
handlerMapping = new HashMap<>();
handlerMapping.put(IOEvent.READ, new ReadEventHandler());
handlerMapping.put(IOEvent.WRITE, new WriteEventHandler());
}
private static class Placehold {
public static final EventHandlerFactory INSTANCE = new EventHandlerFactory();
}
public static EventHandlerFactory getInstance(){
return Placehold.INSTANCE;
}
public AbstractEventHandler getHandler(IOEvent event){
return handlerMapping.get(event);
}
}
最后,进入我们这个委派模式的主角,定义一个委派类,相当于此时需要定义Boss了,这些的底层工人已经准备完毕,等待Boss上场了。
public class EventHandlerDispatcher {
private EventHandlerFactory handlerFactory = EventHandlerFactory.getInstance();
public void handler(IOEvent event, Object params){
handlerFactory.getHandler(event).handler(event, params);
}
}
- 示例代码:
/*
* common code: the event is READ
* I am ReadEventHandler: drink tea
* common code: the event is WRITE
* I am WriteEventHandler: drink tea
**/
public class DispatcherDemo {
public static void main(String[] args){
String param = "drink tea";
EventHandlerDispatcher dispatcher = new EventHandlerDispatcher();
dispatcher.handler(IOEvent.READ, param);
dispatcher.handler(IOEvent.WRITE, param);
}
}
通过上面的示例,我们可以看到,Boss(
派发器
)做的事情很少,基本遵循如下几步:
- 找到一个小秘(
工厂类
),然后把任务(Event事件
)丢给小秘(工厂类
)处理。典型的”有事小秘干,没事…“。- 小秘通过Boss安排的任务类型,选择一个最合适的人选(
Handler处理器
)来具体的执行这个任务。
策略模式是指定义了算法家族,并分别封装起来,让它们之间可以互相替换,此模式使得算法的变化不会影响使用算法的用户。
策略模式的一般使用场景如下:
- 系统中有很多类,而它们的区别仅仅在于行为不同。
- 一个系统需要动态地在几种算法中选择一种。
下面我将结合我们工作中经常遇到的一个例子来演示代码:
工作中,手机号+密码登录是很常见的例子了,但是随着OAuth2.0协议的兴起,越来越多的公司会选择接入微信,QQ等第三方账号进行快捷登录,但是其实底层我们自己的账号体系还是一直的,在我们自身的账号体系中,还是一个账号。这个时候,就可以很好的通过策略模式来实现:
- 定义一个枚举,用于表示用户选择的登录方式。**注意,实际工作中,枚举经常被用来区分策略的类型。**同样,为了方便起见,我们这里只列举微信和QQ两种类型。
public enum LoginMode {
WECHAT,
QQ;
}
- 定义一个接口,以及各个登录方式的鉴权过程。注意:三方登录其实就是通过OAuth协议,通过http请求的方式,从第三方平台获取用户基本信息,包括用户昵称,性别等。当然还有更重要的就是用户在该第三方平台的唯一标识
openId
。
// 定义用户通过三方鉴权之后获取的信息
@Data
public class AuthInfo {
// 用户唯一标识
private String openId;
// 三方账号昵称
private String userName;
}
// 遵循面向接口编程,定义顶层接口
public interface AuthStrategy {
public LoginMode getMode();
public AuthInfo auth();
}
// QQ的http鉴权过程
public class QQAuthStrategy implements AuthStrategy {
@Override
public LoginMode getMode() {
return LoginMode.QQ;
}
@Override
public AuthInfo auth() {
System.out.println("通过http请求QQ三方认证平台。。。。");
AuthInfo authInfo = new AuthInfo();
authInfo.setOpenId("qq-2222222222");
authInfo.setUserName("小花^_^");
return authInfo;
}
}
// 微信的http鉴权过程
public class WechatAuthStrategy implements AuthStrategy {
@Override
public LoginMode getMode() {
return LoginMode.WECHAT;
}
@Override
public AuthInfo auth() {
System.out.println("通过http请求微信开发平台。。。。");
AuthInfo authInfo = new AuthInfo();
authInfo.setOpenId("wechat-11111111");
authInfo.setUserName("Xiaoming");
return authInfo;
}
}
- 和派发模式类似,我们通常也需要建立一个策略的工厂。同样可以通过Spring和HashMap两种方式实现。所以在上一章节中,并没有过多的介绍工厂模式,就是因为工厂模式往往不会单独使用,通常会结合策略模式,模板方法描述等一起使用。
// 此处通过静态工厂的方式提供。同样可以使用Spring更好的管理这些对象
public class AuthStrategyFactory {
public static Map<LoginMode, AuthStrategy> strategyMap;
static {
strategyMap = new HashMap<>();
strategyMap.put(LoginMode.WECHAT, new WechatAuthStrategy());
strategyMap.put(LoginMode.QQ, new QQAuthStrategy());
}
public static AuthStrategy getAuthStrategy(LoginMode mode){
return strategyMap.get(mode);
}
}
- 模拟用户登录场景:
/*
======login by wechat======
通过http请求微信开发平台。。。。
authInfo: AuthInfo(openId=wechat-11111111, userName=Xiaoming)
========login by QQ========
通过http请求QQ三方认证平台。。。。
authInfo: AuthInfo(openId=qq-2222222222, userName=小花^_^)
**/
public class StrategyDemo {
public static void main(String[] args){
System.out.println("======login by wechat======");
AuthInfo wechatInfo = AuthStrategyFactory.getAuthStrategy(LoginMode.WECHAT).auth();
System.out.println("authInfo: " + wechatInfo);
System.out.println("========login by QQ========");
AuthInfo qqInfo = AuthStrategyFactory.getAuthStrategy(LoginMode.QQ).auth();
System.out.println("authInfo: " + qqInfo);
}
}
实际应用中,多种策略之间可以继承使用。如Spring中的
CglibSubclassingInstantiationStrategy
继承了SimpleInstantiationStrategy
。
优点:
- 符合开闭原则
- 可避免使用多重条件语句。当遇到使用switch语句时,大概率可以通过策略模式替换。
- 使用策略模式还可以提高算法的保密性和安全性。
缺点:
- 客户端必须知道所有的策略,并且自行决定使用哪个策略类。
- 代码中会产生非常多的策略类,增加了代码的维护难度。