Spring 源码中使用了很多设计模式,本文主要以 springboot 启动源码来介绍下 spring 中主要使用的设计模式。

目录

模板方法模式

单例模式

工厂方法模式

建造者模式

代理模式

适配器模式

观察者模式

策略模式


模板方法模式

模板方法模式指的是,父类定义一个方法的骨架,主方法中的子方法在父类中可以不作具体实现,而是由子类来完成。

springboot 在启动的时候,也会调用大名鼎鼎的 AbstractApplicationContext 中的 refresh() 方法,来进行 IOC 容器的初始化。

refresh() 方法就是一个模板方法,它定义了方法的骨架,但是里面有的子方法没有在 AbstractApplicationContext 具体实现,而是交给了子类来实现。比如 postProcessBeanFactory、onRefresh()。

spring Gradle 模板 spring 模板方法_spring Gradle 模板

 postProcessBeanFactory 方法在 AbstractApplicationContext 是一个空方法!

spring Gradle 模板 spring 模板方法_spring Gradle 模板_02

那么 postProcessBeanFactory 是在哪个地方实现的呢?通过 debug 代码我们可以知道,springboot 启动时,默认用的 ApplicationContext 是 AnnotationConfigServletWebServerApplicationContext,它最终继承了  AbstractApplicationContext,并且对 postProcessBeanFactory 空方法进行了实现,最终调用的也确实是这个方法。

spring Gradle 模板 spring 模板方法_spring_03

spring 源码中还有大量的地方用到了模板方法模式!

单例模式

一个单一的类,该类负责创建自己的对象,同时确保只有一个对象被创建。

众所周知,spring 中的 bean 默认是单例的(而且默认是饿汉式,也就是说在IOC容器启动时,bean 就被实例化了)!这个就是单例模式的运用。

单例模式有很多种实现方式:

  • 懒汉式,线程不安全
  • 懒汉式,线程安全(synchronized)
  • 饿汉式
  • 双重检查锁 DCL(spring中使用的就是这个)
  • 静态内部类

当我们调用 BeanFactory 的 getBean 方法时,最终会调用到 AbstractBeanFactory 中的 getBean方法,在它的 doGetBean 方法中,调用了 getSingleton 方法来得到单例对象。getSingleton 方法如下图所示:

spring Gradle 模板 spring 模板方法_java_04

这里的 singletonObjects 是一个 ConcurrentHashMap,主键为 beanName,值为 bean 实例。

从源代码中可以看出,spring 的单例模式是通过双重检查锁

首先检查单例对象是否存在,不存在的话对 singletonObjects 加锁,然后再次判断对象是否存在,不存在的话进行实例化。

需要第二次判断对象是否存在是因为:如果多个线程同时通过第一次检查,但是其中有一个线程,在其他线程加锁之前,已经率先实例化好对象并释放了锁,那么其他线程就不需要再去创建对象了!

使用双重检查锁的方式,可以保证线程安全,且在多线程情况下依然能保持高性能!

那么,IOC 容器在启动时,是在什么地方调用的 getBean 呢?

通过查看 refresh 方法的源码,可以找到是在 finishBeanFactoryInitialization 方法中对所有 non-lazy-init(非懒加载) 的 bean 进行实例化的,也是通过调用 getBean 方法实现的。

spring Gradle 模板 spring 模板方法_spring_05

spring Gradle 模板 spring 模板方法_策略模式_06

spring Gradle 模板 spring 模板方法_策略模式_07

这里的 getBean 方法,最终会调用到上面提到的 doSingleton 方法。这个流程很复杂,感兴趣的可以看我的另一篇文章《spring ioc源码详解》

工厂方法模式

工厂方法模式指的是:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

spring 中的 FactoryBean 接口用的就是工厂方法模式!

FactoryBean 是一个工厂 bean,我们可以通过实现 FactoryBean 接口并重写它的 getObject() 方法来自定义工厂 bean,并自定义我们需要生成的 bean。

例如:我们自定义 MyFactoryBean 类实现 FactoryBean,重写 getObject() 方法,返回的是 MyBean 实例。

@Component
public class MyFactoryBean implements FactoryBean<MyBean> {
    @Override
    public MyBean getObject() {
        MyBean myBean = new MyBean();
        myBean.setMsg("hello world");
        return myBean;
    }
    @Override
    public Class<?> getObjectType() {
        return MyFactoryBean.class;
    }
}

@Data
public class MyBean {
    private String msg;
}

编写测试类从 IOC 容器中根据 "myFactoryBean" 这个 beanName 来获取 bean,并打印结果。得到的打印结果是:object instanceof MyBean!

@SpringBootTest
public class FactoryBeanTest {
    @Autowired
    private ApplicationContext context;
    @Test
    public void test() {
        Object object = context.getBean("myFactoryBean");
        if (object instanceof MyBean) {
            System.out.println("object instanceof MyBean");
        } else if (object instanceof MyFactoryBean) {
            System.out.println("object instanceof MyFactoryBean");
        }
    }
}

可以看到,虽然我们用的是 MyFactoryBean 对应的 beanName 来进行 getBean 的,但实际获取到的却是 MyBean 的实例。因为 bean 的实例是从 MyFactoryBean 的 getObject() 方法中返回的。这个就是工厂方法模式的体现。

Spring 中自身就有很多 FactoryBean 的实现,他们隐藏了实例化一些复杂 bean 的细节,调用者无需关注那些复杂 bean 是如何创建的,只需要通过这个工厂 bean 来获取就行了!

FactoryBean 原理

那么 spring 底层是如何通过 FactoryBean 来返回我们想要的 bean 的呢?那就要进入源码中寻找答案。

单例模式中提到了 AbstractBeanFactory 中的 doGetBean 方法,它首先根据 myFactoryBean 这个 beanName 找到实例,此时是 MyFactoryBean 实例。

spring Gradle 模板 spring 模板方法_设计模式_08

 继续往下看:

首先会判断获取到的 bean 是不是 FactoryBean 类型的,如果不是,直接返回 bean 实例了。如果是的话,会继续往下走,调用 getObjectFromFactoryBean 方法

spring Gradle 模板 spring 模板方法_spring_09

 在 getObjectFromFactoryBean 方法中,继续调用 doGetObjectFromFactoryBean 方法,可以看到:最终返回的是 MyFactoryBean 的 getObject() 方法得到的结果,也就是 MyBean 实例。

spring Gradle 模板 spring 模板方法_java_10

建造者模式

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。

spring 中的 BeanDefinitionBuilder 使用的就是建造者模式!

BeanDefinition 是一个复杂对象,通过 BeanDefinitionBuilder 来创建它。

IOC 容器启动时,会自动将 bean 注册到 IOC 容器中,而在 spring 中,是通过 BeanDefinition 来描述 bean 的。它存储了 bean 的相关信息。

在启动过程中,会通过 BeanDefinitionBuilder 来一步步构造复杂对象 BeanDefinition,然后通过 getBeanDefinition() 方法获取 BeanDefinition 对象。

spring Gradle 模板 spring 模板方法_设计模式_11

spring Gradle 模板 spring 模板方法_spring_12

 得到 BeanDefinition 后,将它注册到 IOC 容器中(存放在 beanDefinitionMap 中)

spring Gradle 模板 spring 模板方法_java_13

代理模式

代理模式指的是为其他对象提供一种代理以控制对这个对象的访问

Spring AOP 就是通过代理模式实现的。

学过 Spring AOP 的都知道,通过 aop,我们可以在调用接口的前后,加上一些额外的逻辑,比如打印日志,事务控制等。这个操作就是通过给目标对象创建一个代理对象来实现的。

我写了一个接口 UserSerivce,以及它的实现类 UserServiceImpl。里面只有一个方法 getById。

启动服务,通过 debug 代码可以知道,服务启动的时候默认已经给 userServiceImpl 这个 bean 生成一个代理对象了!我们后面使用的其实就是代理对象,只是默认还没有织入通知!

spring Gradle 模板 spring 模板方法_spring Gradle 模板_14

进入 createProxy,一步步往下看,找到创建 AopProxy 代理对象的地方。可以看到, springboot2 默认使用的是 cglib 动态代理。起代理的是实现类(UserServiceImpl),而不是接口 UserService。

spring Gradle 模板 spring 模板方法_java_15

时候我们写一个测试类来进行验证:

@SpringBootTest
public class SpringAopTest {
    @Autowired
    private UserService userService;

    @Test
    public void test() {
        userService.getById(1L);
    }
}

执行 test 方法,打上断点,并 F7 进入 getById 方法,可以发现进入的不是 UserServiceImpl 中,而是 cglib 代理类中。

spring Gradle 模板 spring 模板方法_策略模式_16

接下来我们加入一个切面类 MyLogAspect

@Aspect
@Component
public class MyLogAspect {

    @Pointcut("execution(* cn.xujingyi.source.service.UserService.*(..))")
    public void pointCut() { };

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("目标方法[" + methodName + "]前置通知");
    }

    @After(value = "pointCut()")
    public void after(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("目标方法[" + methodName + "]后置通知");
    }

}

然后重新执行 test 方法,debug 进入方法中,发现 chain 里多了几个元素(之前是空的),就是我们在 MyLogAspect 中增加的前置和后置通知,执行的时候会先执行前置通知,再执行目标方法,之后执行后置通知。

spring Gradle 模板 spring 模板方法_设计模式_17

适配器模式

适配器模式是指:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

Spring AOP 中的 AdvisorAdapter ,使用的就是适配器模式。

AdvisorAdapter 有三个实现类:MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、以及 ThrowsAdviceAdapter。

spring 容器会将 advice 封装成对应的拦截器,返回给容器,对 advice 的转换就用到了适配器模式。通过循环遍历三个 adapter,判断是否支持这个 advice,支持的话返回对应的 Interceptor

spring Gradle 模板 spring 模板方法_策略模式_18

它将我们定义的通知(在前面 aop 的例子中,我们定义了两个通知,使用 @Before 和 @After 标识),统一转化成了 Interceptor 拦截器(也就是定义中提到的用户需要的接口),最终使用 Interceptor 来拦截我们的接口。这就是适配器模式的应用。

观察者模式

跟消息中间件类似

spring 中的事件驱动模型,使用的是观察者模式。

spring 事件驱动模型中几个主要的角色:

  • 事件(ApplicationEvent)
  • 事件监听(ApplicationListener)
  • 事件发布(ApplicationEventMulticaster)

ApplicationEvent
ApplicationEvent(事件)是所有事件对象的父类。所有的事件都需要继承 ApplicationEvent
自定义事件类时,可以继承 ApplicationEvent 类 

ApplicationListener 
事件监听器,也就是观察者
自定义事件监听器时,可以使用 @EventListener 或者实现 ApplicationListener 接口

ApplicationEventMulticaster
用于事件监听器的注册和事件的广播发送

ApplicationEventMulticaster 和 Listeners,然后在 finisheRefresh() 方法中会发布一个 ContextRefreshedEvent 事件。

spring Gradle 模板 spring 模板方法_策略模式_19

spring Gradle 模板 spring 模板方法_spring_20

进入到 publishEvent 方法可以看到,最后调用的是 SimpleApplicationEventMulticaster(默认情况下是这个,我们也可以自己定义Multicaster) 的 multicastEvent 方法:

spring Gradle 模板 spring 模板方法_spring Gradle 模板_21

 从这段代码可以看出,接收处理事件分为异步和同步两种方式!

异步:如果配置了 Executor,则使用线程池的方式接收并处理事件

同步:如果没有配置 Executor,则在当前线程中处理,调用监听器的 onApplicationEvent方法。默认是同步。

如果我们希望异步方式处理,则可以自己定义一个 beanName 为 applicationEventMulticaster 的多播器,这样在前面初始化 Multicaster 时,就会用自己定义的 Multicaster。在自定义的 Multicaster 中,我们设置 Executor。

invokeListener 方法中,调用了监听器的 onApplicationEvent 方法(这个方法就是实现 ApplicationListener 接口时,需要重写的方法)对事件进行处理!

spring Gradle 模板 spring 模板方法_策略模式_22

策略模式

策略模式主要解决的问题是:在有多种算法相似的情况下,使用 if...else 所带来的复杂性和难以维护。

spring 中 Resource 接口 就是策略模式的应用!

Resource 接口有很多实现类,比如 FileSystemResource、ClassPathResource、UrlResource、ByteArrayResource 等。它们之间最主要的区别就是获取资源的方法(这就是上面提到的"多种算法"),是从文件系统中获取,还是从类路径,或者是字节流中获取等等。。。这就很符合策略模式的特点。

这就像诸葛亮的锦囊一样,每一个锦囊就是一个策略。

@SpringBootTest
public class ResourceTest {
    @Test
    public void test() throws Exception {
        // 从文件系统中获取资源
        Resource fileSystemResource = 
                new FileSystemResource("D:\\xujingyi\\temp.json");
        File file1 = fileSystemResource.getFile();
        System.out.println(file1.getName());

        // 从类路径中获取资源
        Resource classPathResource = 
                new ClassPathResource("application.properties");
        File file2 = classPathResource.getFile();
        System.out.println(file2.getName());
    }
}

输出结果为:temp.json 和 application.properties

使用策略模式,避免了重条件判断,而且扩展性非常好。如果想要增加一种获取资源的方式,只要另外加个类实现 Resource 接口并编写获取资源的逻辑就行了,不需要对源码进行修改!

策略模式和工厂方法模式的区别

策略模式和工厂方法模式,看起来很相似(实际也确实比较相似),不过还是有区别的。

工厂方法模式作用是创建对象,而策略模式是关注点是行为,让一个对象在许多种行为中选择一种。