天山--Spring系列之设计模式

小宋现在也步入了买基金的队伍,虽然错过了双十二的白酒,但是在新能源上还是小赚了点零花钱,虽然花了一点精力在基金股票上,但是博客还是要写的。🤣
在这里插入图片描述
本章小宋带大家重新回顾一下Spring中涉及到的一些设计模式。

控制反转(IoC)和依赖注入(DI)

IoC(Inversion of Control,控制反转) 是Spring 中一个非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容器管理对象,你只管使用即可),从而降低代码之间的耦合度。IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。
在这里插入图片描述
Spring IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 IOC 容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁。

在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

关于Spring IOC 的详细理解如下:

我们去理解控制反转(IoC)的时候,首先得带大家理解一下软件设计中的依赖倒置原则。

什么是依赖倒置原则呢:
假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。

这样的设计看起来没问题,但是可维护性却很低。假设设计完工之后,上司却突然说根据市场需求的变动,要我们把车子的轮子设计都改大一码。这下我们就蛋疼了:因为我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改;同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改——整个设计几乎都得改!我们现在换一种思路。我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身, 车身依赖汽车。

这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘,车身,汽车的设计了。这就是依赖倒置原则——把原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的“牵一发动全身”的情况。

控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。这几种概念的关系大概如下:在这里插入图片描述
去理解这几个概念,还是用上面汽车的例子。这次写成代码。我们先定义四个Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。代码如下:

    //车模型
    Class Car{
        private Framework framework;
        
        Car(){
            this.framework = new Framework();
        }
        public void run(){
            //....
        }
    }

    //车身框架
    Class Framework{
        private Bottom bottom;

        Framework(){
            this.bottom = new Bottom();
        }
    }

    //车底盘
    Class Bottom{
        private Tire tire;

        Bottom(){
            this.tire = new Tire();
        }
    }

    //轮胎
    Class Tire{
        private int size;

        Tire(){
            this.size = 30;
        }
    }
    
    //初始化车
    Car mycar = new Car();
	//运行
    mycar.run();

这里相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要这样改:
Tire类修改:

 	//轮胎
    Class Tire{
        private int size;

        Tire(int size){
            this.size = size;
        }
    }

由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动

	//车模型
    Class Car{
        private Framework framework;

        Car(int size){
            this.framework = new Framework(size);
        }
        public void run(){
            //....
        }
    }

    //车身框架
    Class Framework{
        private Bottom bottom;

        Framework(int size){
            this.bottom = new Bottom(size);
        }
    }

    //车底盘
    Class Bottom{
        private Tire tire;

        Bottom(int size){
            this.tire = new Tire(size);
        }
    }

    //轮胎
    Class Tire{
        private int size;

        Tire(int size){
            this.size = size;
        }
    }

    //初始化车
    int size = 40;
    Car mycar = new Car(size);

    mycar.run();

由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。

所以我们需要进行控制反转(IoC),及上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:

    //车模型
    Class Car{
        private Framework framework;

        Car(Framework framework){
            this.framework = framework;
        }
        public void run(){
            //....
        }
    }

    //车身框架
    Class Framework{
        private Bottom bottom;

        Framework(Bottom bottom){
            this.bottom = bottom;
        }
    }

    //车底盘
    Class Bottom{
        private Tire tire;

        Bottom(Tire tire){
            this.tire = tire;
        }
    }

    //轮胎
    Class Tire{
        private int size;

        Tire(){
            this.size = 30;
        }
    }

    //初始化车
    Tire tire = new Tire();
    Bottom bottom = new Bottom(tire);
    Framework framework = new Framework(bottom);
    Car mycar = new Car(framework);
    //运行
    mycar.run();

然后再做之前一样的操作对轮胎处的代码进行动态修改:

    //车模型
    Class Car{
        private Framework framework;

        Car(Framework framework){
            this.framework = framework;
        }
        public void run(){
            //....
        }
    }

    //车身框架
    Class Framework{
        private Bottom bottom;

        Framework(Bottom bottom){
            this.bottom = bottom;
        }
    }

    //车底盘
    Class Bottom{
        private Tire tire;

        Bottom(Tire tire){
            this.tire = tire;
        }
    }

    //轮胎
    Class Tire{
        private int size;

        Tire(int size){
            this.size = size;
        }
    }

    //初始化车
    int size = 40;
    Tire tire = new Tire(size);
    Bottom bottom = new Bottom(tire);
    Framework framework = new Framework(bottom);
    Car mycar = new Car(framework);
    //运行
    mycar.run();

通过上面代码的比对,会发现这里只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。不仅如此,在实际的工程中,这种设计模式还有利于不同组的协同合作和单元测试:比如开发这四个类的分别是四个不同的组,那么只要定义好了接口,四个不同的组可以同时进行开发而不相互受限制;而对于单元测试,如果我们要写Car类的单元测试,就只需要Mock一下Framework类传入Car就行了,而不用把Framework, Bottom, Tire全部new一遍再来构造Car。这里我们是采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递接口传递。这里就不多讲了,核心思路都是一样的,都是为了实现控制反转。

到这里应该能理解什么控制反转和依赖注入了。在上面举的代码里,什么地方是控制反转容器(IoC Container)呢?其实就是对车类进行初始化的那段代码发生的地方,就是控制反转容器。

显然你也应该观察到了,因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。IoC Container的第二个好处是:我们在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车instance时候,是从底层往上层new的:
在这里插入图片描述
这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入。而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历):
在这里插入图片描述
IoC 容器这里可以直接隐藏具体创建实例的细节,其实看起来就像一个工厂
在这里插入图片描述
我们就像是工厂的客户。我们只需要向工厂请求一个Car实例,然后它就给我们按照Config创建了一个Car实例。我们完全不用管这个Car实例是怎么一步一步被创建出来。
实际项目中,有的Service Class可能是十年前写的,有几百个类作为它的底层。假设我们新写的一个API需要实例化这个Service,我们总不可能回头去搞清楚这几百个类的构造函数吧?IoC Container的这个特性就很完美的解决了这类问题——因为这个架构要求你在写class的时候需要写相应的Config文件,所以你要初始化很久以前的Service类的时候,前人都已经写好了Config文件,你直接在需要用的地方注入这个Service就可以了。这大大增加了项目的可维护性且降低了开发难度。

工厂设计模式

Spring使用工厂模式可以通过BeanFactoryApplicationContext创建bean对象。

BeanFactory和ApplicationContext比较

  • BeanFactory:依次注入(使用到某个bean的时候才会注入),ApplicationContext会占用更多的内存,程序启动速度更快。
  • ApplicationContext:容器启动的时候,不管你用没用到,一次性创建所有的bean,BeanFactory仅提供了最基本的依赖注入支持, ApplicationContext扩展了BeanFactory,有除了BeanFactory的功能还有额外更多功能,一般所以开发人员使用 ApplicationContext会更多。

ApplicationContext的三个实现类:

  • ClassPathXmlApplication:把某些文件当成类路径资源。
  • FileSystemXmlApplication:从文件系统中的XML文件加载上下文定义信息。
  • XmlWebApplicationContext:从Web系统中的XML文件加载上下文定义信息。

eg.

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new FileSystemXmlApplicationContext(
                "C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");

        HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
        obj.getMsg();
    }
}

单例设计模式

在我们的系统中,有一些对象其实我们只需要一个,例如说:线程池,缓存,数组,记录,日志对象,打印机,打印机等设备驱动程序的对象。只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,例如:程序的行为异常,资源使用过量,或者不一致性的结果。

使用单例模式的好处

  • 对于大量使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔支出系统;
  • 由于新操作的次数减少,因此对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。

Bean的作用域

Bean的默认作用域就是单例的。除了单作用域,bean还有下面几种作用域:

prototype:每次请求都会创建一个新的bean实例。
request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内部有效。
session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。
global-session:全局会话作用域,仅在基于portlet的web应用中才回收,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与servlet不同,每个portlet都有不同的会话

Spring中实现单例的方式

xml: <bean id="userService" class="top.snailclimb.UserService" scope="singleton"/>
注解:@Scope(value = "singleton")

Spring如何实现单例模式

Spring通过ConcurrentHashMap实现单例校准的特殊方式实现单例模式。Spring实现单例的核心代码如下

// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "'beanName' must not be null");
        synchronized (this.singletonObjects) {
            // 检查缓存中是否存在实例  
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                //...省略
                try {
                    singletonObject = singletonFactory.getObject();
                }
                //...省略
                // 如果实例对象在不存在,我们注册到单例注册表中。
                addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
        }
    }
    //将对象添加到单例注册表
    protected void addSingleton(String beanName, Object singletonObject) {
            synchronized (this.singletonObjects) {
                this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

            }
        }
}

代理设计模式

代理模式在AOP中的应用

AOP(面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理,日志管理,权限控制等)封装起来,以减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。

Spring AOP是基于动态代理的,如果要代理的对象,实现一个接口,那么Spring AOP会使用JDK Proxy(jdk 动态代理),去创建代理对象,而对于没有实现接口的对象,就无法使用JDK Proxy去进行代理了,这时候Spring AOP会使用Cglib,这时候Spring AOP会使用Cglib生成一个被代理对象的子类来作为代理。

如下图所示:
在这里插入图片描述
当然你也可以使用AspectJ,Spring AOP已经集成了AspectJ,AspectJ应该算的上是Java生态系统中最完整的AOP框架了。

使用AOP之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。事务管理等等场景都用到了AOP。

Spring AOP 和 AspectJ AOP 有什么区别?

上面提到了spring aop和aspectj那他们之间有什么区别呢?

  • Spring AOP基于代理(代理),而AspectJ基于字节码操作(字节码操作)。

  • Spring AOP已经集成了AspectJ,AspectJ应该算上上是Java生态系统中最完整的AOP框架了。AspectJ引用于Spring AOP功能更加强大,但是Spring AOP相对来说更简单,

如果,我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择AspectJ,它比Spring AOP快很多。

模板方法

模板方法模式是一种行为设计模式,它定义一个操作中的算法骨架,而将一些步骤延迟到子类中。
eg.

public abstract class Template {
    //这是我们的模板方法
    public final void TemplateMethod(){
        PrimitiveOperation1();  
        PrimitiveOperation2();
        PrimitiveOperation3();
    }

    protected void  PrimitiveOperation1(){
        //当前类实现
    }

    //被子类实现的方法
    protected abstract void PrimitiveOperation2();
    protected abstract void PrimitiveOperation3();

}
public class TemplateImpl extends Template {

    @Override
    public void PrimitiveOperation2() {
        //当前类实现
    }

    @Override
    public void PrimitiveOperation3() {
        //当前类实现
    }
}

Spring中jdbcTemplate,hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是Spring并没有使用这种方式,而是使用回调模式与模板方法模式配合,既达到了代码附加的效果,同时又增加了可选。

观察者模式

观察者模式是一种对象行为类型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring事件驱动模型就是观察者模式很经典的一个应用。Spring事件驱动模型非常有用,在很多场景都可以分解我们的代码。有时我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。

Spring 事件驱动模型中的三种角色

事件角色

ApplicationEvent(org.springframework.context包下)先前事件的角色,这是一个抽象类,它继承了java.util.EventObject并实现了java.io.Serializable接口。

Spring中标准化存在以下事件,他们都是对ApplicationContextEvent的实现(继承自ApplicationContextEvent):

  • ContextStartedEvent:ApplicationContext启动后触发的事件;
  • ContextStoppedEvent:ApplicationContext停止后触发的事件;
  • ContextRefreshedEvent:ApplicationContext初始化或刷新完成后触发的事件;
  • ContextClosedEvent:ApplicationContext关闭后触发的事件。
    在这里插入图片描述

事件监听者角色

ApplicationListener中断了事件监听者角色,它是一个接口,里面只定义了一个onApplicationEvent()方法来处理ApplicationEvent。ApplicationListener接口类源码如下,可以切割接口定义切削接口中的事件只要实现了ApplicationEvent就可以了。所以,在Spring中我们只要实现ApplicationListener接口的onApplicationEvent()方法即可完成监听事件

package org.springframework.context;
import java.util.EventListener;
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}

事件发布者角色

ApplicationEventPublisher 宣布了事件的发布者,它也是一个接口。
在这里插入图片描述
ApplicationEventPublisher内部的publishEvent()这个方法在AbstractApplicationContext类中被实现,阅读这个方法的实现,你会发现实际事件真正是通过ApplicationEventMulticaster来广播出去的。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Spring 的事件流程总结

  1. 定义一个事件:实现一个继承自ApplicationEvent,并且写相应的构造函数;
  2. 定义一个事件监听者:实现ApplicationListener接口,转化onApplicationEvent()方法;
  3. 使用事件发布者发布消息:可以通过ApplicationEventPublisher 的publishEvent()方法发布消息。
    eg.
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
    private static final long serialVersionUID = 1L;

    private String message;

    public DemoEvent(Object source,String message){
        super(source);
        this.message = message;
    }

    public String getMessage() {
         return message;
          }


// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{

    //使用onApplicationEvent接收消息
    @Override
    public void onApplicationEvent(DemoEvent event) {
        String msg = event.getMessage();
        System.out.println("接收到的信息是:"+msg);
    }

}
// 发布事件,可以通过ApplicationEventPublisher  的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {

    @Autowired
    ApplicationContext applicationContext;

    public void publish(String message){
        //发布事件
        applicationContext.publishEvent(new DemoEvent(this, message));
    }
}

当调用DemoPublisher 的publish()方法,例如demoPublisher.publish(“你好”),控制台就会打印出:接收到的信息是:你好。

适配器模式

适配器模式(Adapter Pattern)将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器。

spring AOP中的适配器模式

我们知道Spring AOP的实现是基于代理模式,但是Spring AOP的通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter 。Advice常用的类型有:(BeforeAdvice目标方法调用前,作为通知),AfterAdvice(目标方法调用后,后置通知),AfterReturningAdvice(目标方法执行结束后,返回之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor,AfterReturningAdviceInterceptor。 Spring预定义的通知要通过对应的适配器,适配成MethodInterceptor接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor负责适配MethodBeforeAdvice)。

spring MVC中的适配器模式

在Spring MVC中,DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler。解析到对应的Handler(也就是我们平常说的Controller控制器)后,开始由HandlerAdapter适配器处理。HandlerAdapter作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller作为需要适配的类。

为什么要在Spring MVC中使用适配器模式? Spring MVC中的Controller种类众多,不同类型的Controller通过不同的方法来对请求进行处理。如果不利用适配器模式的话,直接DispatcherServlet获取对应类型的Controller,需要的自行来判断,像下面这段代码一样:

if(mappedHandler.getHandler() instanceof MultiActionController){  
   ((MultiActionController)mappedHandler.getHandler()).xxx  
}else if(mappedHandler.getHandler() instanceof XXX){  
    ...  
}else if(...){  
   ...  
}  

Controller假如我们再增加一个类型就要在上面的代码中再加入一行的判断语句,这种形式就可以程序维护,也违反了设计模式中的开闭原则–对扩展开放,对修改关闭。

装饰者模式

装饰者模式可以动态地给对象添加一些额外的属性或行为。引用于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改所有权的功能,但我们又不愿直接去修改其实在JDK中就有很多地方用到了装饰者模式,InputStream诸如此类的家族,InputStream类下有FileInputStream(读取文件),BufferedInputStream(增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream代码的情况下扩展了它的功能。
装饰者模式示意图
Spring中配置DataSource的时候,DataSource可能是不同的数据库和数据源。我们能否根据客户的需求在少修改变量类的代码下动态切换不同的数据源?这个时候就要用到装饰者模式(这中我到的包装器模式在类名上包含Wrapper或者Decorator。这些类基本上都是动态地给一个对象添加了一些额外的职责

讲到这里本章对Spring的设计模式讲解也就结束了,如果想了解更多知识可以在对应的专栏中看系列文章,谢谢大家的观看,希望能给各位同学带来帮助。如果觉得博主写的还可以的,可以点赞收藏。 😉

posted @ 2020-12-17 15:42  奋斗的小宋  阅读(16)  评论(0)    收藏  举报