设计模式Java实战

文章目录

一、前置

1.1 目的

写出好的代码,个人认为依次重要程度为:

1)健壮性
个人理解为最重要的之一,好的代码,首先是无bug代码
代码中,常见可能引起问题的点(重要程度不分先后):

  • 限流、熔断、降级

    • 限流:对上游是否需要限流,提前对齐SLA,保护我们的服务

    • 熔断机制

    • 降级、兜底:下游接口拿不到数据或不可用,产品侧是否有兜底数据、技术侧是否有兜底方案

      (eg:使用leaf生成据号时,如果leaf短期不可用,业务上是否支持降级使用其它方式,如redis生成单据号等)

  • 一致性

    • 数据一致性:主从延时:写完读(最典型的就是,写后发MQ,收到MQ后读刚刚写的结果)
    • 状态一致性:上下游的状态是否一致(eg:服务A将任务状态置为终态,如果上游系统B有业务动作依赖A任务的状态,那A也要告诉上游B任务终态了。否则上游B发现下游A的任务一直未终态,他们可能有自己的重试机制等)
    • 单位问题:精度、元分。kg和mg等,尤其是涉及金钱时,一定对齐好上下游的统一单位
  • 幂等性

    • 接口幂等性:接口约定是否支持幂等
    • 消息幂等性:消息是否可能重复
  • 强弱依赖

    • 是否强依赖请求的第三方数据,是否需要阻断主流程

    • 是否强依赖中间件,如果中间件异常。是否阻断主流程

      (eg:使用redis做消息幂等卡控逻辑,一旦redis短期不可用,业务上是否允许跳过redis的卡控,保证主流程正常进行)

  • 数据库

    • 事务失效问题:rpc写和本地写、以及Spring 的事务依赖传递特性
    • 数据量评估:随着业务发展,数据量增大,甚至极端场景下,会不会有慢查询,索引是否合理
    • 字段类型(是否大小写敏感)、大小(是否需要截断)、update是否需要updateSelective、查询in(为空)则可能查询全量数据等
  • 性能:

    • 同步异步:是否需要异步处理(写OP日志、推送消息等)
  • 异常处理

    • 接口异常:是否需要重试、异常定义
  • 其它常见异常

    • 边界条件:while循环为了防止死循环,结合业务要设置最大的循环次数;终止条件最好是>=或<=,防止并发时跳过了
    • 参数校验:api接口一定不要相信传参,做好防御式编程
    • list转map,list的字段可能重复,作为map的key则可能Duplicate key异常
    • 日期判断或者日期作为查询条件也要特别注意
    • 集合get(0)首先是npe其次是集合的所有元素是否都一致,不一致就不能拿第一个元素的内容去赋值
    • switch要有default
    • if要和else if() else if()最好加上条件,避免落到if else中
    • npe问题:常见可能造成npe的点
    • /0
    • 锁的释放:超时时间是否设定、异常流程是否释放锁

2)可读性

代码终究是给人读的,参考《Clean Code》

3)可复用性

  • 不写重复的代码
  • 高内聚低耦合(模块内部内聚,模块之间解耦)

eg1:网关层封装的查询方法应该做的事项有:
封装并发(调用方只需要传入所有的skuIdList,gateway自己按照200、200拆分sku并发查询)
返回数据要自定义(如果rpc调用的返回对象为集合List<A>其中A有很多的属性,可能我们并不关心,那么gateway就需要自己定义对象DTO,属性只有几个我们关心的字段即可)
eg2:查询数据库的入参可能有很多字段,可以封装一个查询model,支持复用(只要传递model中相应字段即可)和扩展(新增查询条件)

4)可扩展性

  • 设计模式的七大原则、23种模式

    eg:需求需要对规则进行新增、删除,只需要调整枚举类,不需要修改现有代码逻辑

  • 兼容性:尤其是接口字段调整,要遵循新增而非更改

eg:skuId变为skuIdList,一般是新增字段,然后做好上线过度

  • 前端向两个字段中都传值
  • 后端在网关层,加一个逻辑,即如果skuIdList有值就用这个值,无值则使用skuId的值

1.2 面向对象

1、看似面向对象,实则面向过程的做法

  • 滥用get、set方法,违反了面向对象的特征:封装。除非需要否则,不要给属性定义setter

  • Constants常量类:不要单独设计此常量类。好的 做法:哪个类用的用到了某个常量,在此类汇总定义即可

    否则,不易维护:改一个常量,影响太多地方,不能轻易修改;不易复用:要在另一个项目中复用本项目的某类,此类中又依赖Constants,相当于把整个Constants都一并引入了

2、面向对象编程步骤

以:对接口进行鉴权为例

1)分析实现步骤

  • 调用方进行接口请求的时,将URL、useId、pwd、ts时间戳拼接在一起传递过来;通过加密生成token;并将token、useId、ts拼接在URL中传递
  • 接收到请求后,解析URL,获取token、useId、ts
  • 校验ts和当前时间,是否在合理的时间窗口内(生成的ts和当前时间间隔1min则认为token失效),失效则拒绝
  • 通过useId去缓存或db中获取pwd,通过同样的方式生成token,与调用传递的token对比,不一致则拒绝

2)划分职责,识别出有哪些类

  • 如果是大需求,涉及多个模块,则需要先把需求按照模块划分。eg:逆向计划自动建单分为(触发模块、获取可退sku、计算可退量、合单、下发、回掉等多个模块)

  • 将需求转换为每个模块要实现的内容;并拆解为小的功能,一条一条列出来,这里以接口鉴权为例

    1、把URL、useId、pwd、ts拼接为子串

    2、通过字符串,加密生成token

    3、将useId、token、ts形成新的url

    4、解析url,获取ts、useId、token

    5、根据useId去存储介质中获取pwd

    6、根据ts判断token是否在有效的窗口内

    7、根据获取的pwd同样方式生成token,比较和传递过来的token是否一致

  • 其中1、2、6、7和token相关,负责token的生成和比对 ; 3、4和URL相关,负责url的拼接、解析等;5是单独的获取pwd。

    这样,我们就定义了三个主要的类:AuthToken、Url、UseStorage

  • 这里体现了高内聚(将小的功能点理清楚到底属于哪个类,相同的都放在一起),低耦合(不属于这个类的属性和方法,则不要放在这个类里,比如URL信息,useId不应该属于Token,不要作为他的属性)

3)定义类 和 属性、方法

  • AuthToken:定义属性和方法:

    • 1和2:createToken(String url, Long useId,String pwd)
    • 6:isExpireed(Long ts)
    • 7:match(String token)
  • ApiUrl

    • 3:buildUrl()

    • 4:getTokenFromUrl(String url)

      :getUseIdFromUrl(String url)

      :getTsFromUrl(String url)

4)定义类和类之间的交互关系(继承、实现、聚合、组合、依赖等)

思考:

我理解的面向对象编程,就是要做到:以终为始
就好比要外出旅游,将这个需求分为:衣食住行四个模块

  • 衣:带什么衣服(上衣、裤子等)
  • 行:是坐火车还是飞机,如果是坐火车,如何去火车站等
  • 住:是住酒店还是民宿,住的地方和旅游景点的远近、交通的便利等

就是在未出发之前,衣食住行模块都想好,方法也想好(先公交、再火车),类之间如何衔接(对应类之间的关系)。然后按照这些去旅游。

而面向过程编程,则是准备去旅游。

  • 行模块:早上起来看有飞机航班么,没有则坐火车,最近的一趟火车,出发。
  • 住:到了目的地,随便找个地方先住下来

类似这种,我理解为面向过程。


1.3 接口和抽象类

接口是抽象方法的集合,是行为的抽象,是一种行为的规范
抽象类是用来捕捉子类的通用特性的,是一种模板设计。

  • 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。

1、什么时候使用接口

  • 需要将接口和实现相分离,封住不稳定的实现,暴露稳定的接口
    • 函数名不要暴露实现细节,否则后续需求变化,名称可能词不达意甚至描述有误。所以尽量抽象。
      eg:uploadPicture而非uploadPicture2Yun
    • 封装具体的实现细节
      eg:sku查询算法值(不同的sku对应的供货链路不同,不同的供货链路,对应查询不同的算法类型值),则queryAlQty(Integer supplyType,Long skuId)不如queryAlQty(Long skuId):内部封装了查询供货链路。
  • 上游系统面向接口编程,这样接口实现发生变化时,下游系统代码基本不需要改动。降低了代码的耦合性
    提升了扩展性

2、要用接口和抽象类时,选择哪个

  • 复用:
    • 抽象类是用来捕捉子类的通用特性的,是一种模板设计
    • 要表示is a(三角形是图形,圆形是图形—),同时目的是为了解决代码的复用性,则使用抽象类
  • 解耦:
    • 行为模型应该总是通过接口而不是抽象类定义
    • 表示has a,并且为了解耦,而非代码的复用,则使用接口

二、七大设计原则

23种设计模式本身的原则

2.1 依赖倒转原则

1、定义:高层模块(调用者)应该不直接依赖于低层模块(被调用者)的具体实现,而应该依赖于低层模块的抽象(如接口或抽象类)

2、作用:

当低层模块的具体实现发生变化时(逻辑变化、新增实现)

  • 解耦: 依赖抽象而不是具体实现,只要抽象保持不变,高层模块就不会受到影响
  • 可扩展性: 当需要添加新的功能时,只需要添加具体的实现类,高层模块就不会受到影响
  • 开闭原则:多扩展开放,对修改关闭

3、实战

3.1场景1-依赖注入DI

1)定义:

A类中使用B类,不同new B()的方法创建b,而是将B在外部创建好后,通过new A(b)构造函数、函数参数func(B b)、set属性等方式传递(注入)给A类使用

2)和控制反转的关系:

控制反转不是具体的实现技巧,而是一种用于指导框架设计的思想。而DI则是具体的编码技巧,是IOC的具体实现

3)依赖注入 和 非依赖注入

背景:

Notification类负责将商品的促销、验证码消息等给用户。它依赖MessageProductor生产者类发送消息

  • 非依赖注入

    B类(MessageProductor)

public class MessgaeProductor {
    public boolean send(String msg) {
        //
    }
}

​ A类(Notification)

public class Notification {
    private MessgaeProductor messgaeProductor;
    public Notification() {
        this.messgaeProductor = new MessgaeProductor();//A类中使用B类,通过new方式在A类中创建
    }
    
    public void sendMessage(String msg) {
        this.messgaeProductor.send(msg);
    }
}
Notification notification = new Notification();
notification.sendMessage("msg");
  • 依赖注入

    • B1、B2(MessgaeProductor接口实现类)
    public interface MessgaeProductor {
        public boolean send(String msg);
    }
    
    // B1:短信生产类
    public class SmsProductor implements MessgaeProductor{
        @Override
        public boolean send(String msg) {
            //发送短信
        }
    }
    
    // B2:微信消息生产类
    public class WeChatProductor implements MessgaeProductor{
        @Override
        public boolean send(String msg) {
            //发送微信信息
        }
    }
    
    • A(Notification类)
    public class Notification {
        private MessgaeProductor messgaeProductor;
        public Notification(MessgaeProductor messgaeProductor) {
            this.messgaeProductor = messgaeProductor;//A类中使用B类,通过构造器将b注入A中
        }
    
        public void sendMessage(String msg) {
            this.messgaeProductor.send(msg);
        }
    }
    
    • 外部使用
    public class Demo {
        public static void main(String[] args) {
            DaxiangProductor messgaeProductor = new DaxiangProductor();//创建对象b
            Notification notification = new Notification(messgaeProductor);//通过构造函数,将b依赖注入A类中
            notification.sendMessage("msg");
        }
    }
    

4)依赖注入框架

  • 产生背景

    • 对比依赖注入和非依赖注入,发现new B()的动作,只不过从在A类中new,变成了在更上次外部类Demo中new,还是需要程序员自己实现
    • 一个项目可能有成百上千个类的创建和依赖注入,如果全部都由程序员自己实现,将变得复杂容易出错
    • 对象的创建和依赖注入动作本身和业务不相关,完全可以抽象成框架来自动完成
  • 常见的依赖注入框架:Spring、Google的Guice

  • 作用

    类与类之间的依赖关系,可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等

  • 举例

    public class A{
        @Resource
        private B b;
        public static void main(String[] args) {
            b.send("msg");
        }
    }
    

Spring框架中只要声明@Resource private B b,就可以实现B的创建和生命周期的管理,同时后置处理器通过setField的方式将b注入A类中

3.2 实战2-日志记录

需要将日志记录到不同的地方(如控制台、文件、数据库等)。

通过定义一个抽象的日志记录接口,高层模块可以依赖于这个接口,而不是具体的日志记录实现


2.2 里氏替换原则

1、概念: 在程序中,子类对象可以替换父类对象,并且保证原来的逻辑和正确性不受影响。

一句话:子类重写父类的已实现方法,不要改变原有方法的逻辑

2、作用:指导子类设计

  • 重写父类已实现的方法时:不能改变父类原有的行为,也不能增加额外的条件或限制
  • 可以新增方法
  • 或干脆使用组合而非继承
  • 抽象方法,子类可以自定义实现逻辑

3、子类重写父类方法时,常见的违背里氏替换原则的场景有

  • 违背父类的输入

    父类输入Integer是整数,子类输入Integer要求是正整数(正确方式是要比父类方法的输入参数更宽松)

  • 违背父类的输出

    父类catch代码块中return的是空集合,子类重写方法中catch块中return的是null(正确方式:方法的返回值要比父类更严格)

  • 违背父类方法的声明

    父类sortBySkuId,查询结果按照skuId排序。子类sortBySkuId查询结果按照实时销量排序了

  • 违背父类异常的处理

    父类valid参数时,不满足时,抛出的是ArguementNullException。子类抛出的是illeagalException


2.3 不要重复原则

1、重复定义

1)重复代码

  • 描述:相同逻辑的代码,在多处被code

  • 解决:抽取为方法,父类模版模式抽象

2)功能语义重复

  • 描述:多个人定义多个枚举类、工具类、描述相同的事情

  • 解决:一起开发时,只要定义了枚举类,就告知

3)逻辑实现重复

  • 描述:A定义了网关接口,根据skuId查询skuInfo,只允许sku的个数小于200

    B有定义了相同的网关接口,根据skuId并行查询skuInfo,不限制sku个数

  • 解决:对于网关层,统一定义入参(不限制个数)、逻辑(并发)、返回(自定义DTO),大家一起使用

    代码下沉,下沉的代码尽量通用

4)代码重复执行

  • 描述:在上层controller层执行了对入参skuId的校验,再Dao层,再次对skuId进行校验

  • 解决:

    如果可以确定,只有controller层使用,则不需要再重复执行校验逻辑

    如果Dao层,这个逻辑,还有可能别的地方使用,那么就加上校验,重复也没问题

2、复用性

1)业务和非业务逻辑分离,越是和业务无关的代码,越容易复用

eg:生成单据号、查询仓、品类、日期

2)高内聚、低耦合

大而全的类,依赖它的代码就多。进而增加了代码的耦合度,影响代码的复用。粒度越小的代码,通用性越好越容易被复用

3)方法入参

  • 使用多态(List)
  • 使用Model类定义多个属性,尤其属性是数据库查询条件

4)封装

  • 方法1,入参为skuId和链路类型,获取算法值
  • 方法2,入参为skuId,获取算法值

方法2内部封装封装类查询链路类型,然后再查询算法的逻辑。

调用方、调用方2、调用3法各自不用重复实现这个逻辑,显然方法2的复用性更高


2.4 接口隔离原则

1、概念:接口的调用者,不应该被强迫依赖它不需要的接口

2、作用:

  • 高内聚、低耦合

3、实战

3.1 场景1

客户端不应该被迫依赖于它不使用的方法:一个接口中如果有太多的方法,而客户端只需要其中的一部分,那么客户端就会被迫依赖于它不需要的方法,这增加了客户端与接口之间的耦合度

  • 不满足接口隔离

    public interface UserService{
        boolean register(String phone, String pwd);
        boolean login(String phone, String pwd);
        UserInfo getUserInfo(String phone);
        boolean deleteUser(String phone, String pwd);//删除用户
    }
    
    public UserServiceImpl implements UserService{
      //---
    }
    

    正常情况下,用户在调用UserService接口中的方法时,一般不会也不允许调用deleteUser方法,只会用到CRU功能。

    根据接口隔离原则:接口的调用者,不应该强迫依赖他不需要的接口即deleteUser方法

    解决:将deleteUser方法抽出去

  • 满足接口隔离

    后端管理系统ManagerUserImpl才需要CRUD中的deleteUser功能

    public interface UserService{
        boolean register(String phone, String pwd);
        boolean login(String phone, String pwd);
        UserInfo getUserInfo(String phone);
    }
    
    public interface ManagerService{
        boolean deleteUser(String phone, String pwd);//删除用户
    }
    
    public class UserServiceImpl implements UserService{
    		//CRU功能---
    }
    
    public class ManagerServiceImpl implements UserService, ManagerService{
    		//CRUD功能---
    }
    

3.2 场景2

接口应该小而完备 (这里的接口指的是方法)

  • 不满足接口隔离

    public class Statistics {
        private Long max;
        private Long min;
        private double avg;
        private Integer count;
        
        public Statistics count(Collection data) {
            Statistics statistics = new Statistics();
            // 计算逻辑
            return statistics;
        }
    }
    

    count函数功能不单一,包含了max、min、count、avg等多个功能。

    接口应该小,解决:拆分成小的接口(方法)

  • 满足接口隔离

    将count方法拆分为max()、min()、avg()等方法。如何要想使用复合计算则可以直接使用

    LongSummaryStatistics statistics = new LongSummaryStatistics();
    statistics.accept(1);
    statistics.accept(2);
    statistics.accept(3);
    
    statistics.getMax();
    statistics.getAverage();
    statistics.getSum();
    

2.5 单一职责

1、概念:一个类或模块,应该只负责一项职责

2、作用:
1)大

  • 高内聚,相近功能放在一个类中,不相近的功能不放在一个类中。相近的功能往往会被同时修改,这样改动点比较集中
  • 可维护性:这样即使后续有改动,改动点范围可控

2)小

  • 类不会过于庞大,便于理解
  • 可复用性:功能单一的类更容易被其他类或模块使用

3、实战

3.1场景1

OrderRepository中不要涉及对SkuDO的CRUD

3.2 场景2

用户注册 和 用户登录功能,建议分成两个类

3.3 场景3

逆序计划流程 = 1触发建单 + 2【触发oih + 落sku + 计算可退量 + 合单并下发+回掉】

做了RDC退、协同退、PC退之后,发现流程2是完成可以复用。

但是流程1,不同的触发源尤其是RDC退和PC退,很多代码都写在一个类中,实际上违背了单一职责。改动PC退的流程1代码有可能影响RDC退。

4、前期如何定义一个单一职责的类

public class UserInfo {
    private Long userId;
    private String name;
    private Long createTime;
    private Long lastLoginTime;
    
    private String email;
    private Long phoneNo;
    
    private String province;
    private String city;
    private String region;
    private String detailAddress;
}
  • 类中大量的方法都在对某几个属性进行操作,则可以考虑将这几个属性抽取出来单独成一个类

比如:一开始类有几个属性,可以规划为操作URL、Token的。这个时候就可以把这个类分为URLService、TokenService

  • 请求接收类:可以按照属性分类
    • 档期相关:销售日期、履约日期、最大售卖量上下架、库存销量、坑位
    • 仓网信息:仓id、网店id
    • 品类温层信息
    • 算法值
      等进行分类

5、后期如何拆分类

可以先第一版比较粗的类UserInfo。随着业务迭代持续重构

1)业务角度

  • 比如后续有了物流业务,则用户的地址信息可以抽取出来独立类
  • 后续有了论坛、金融等业务需要对用户进行登录校验,则可以将email、phone拆出来独立类

2)代码角度

  • 代码属性过多(>10)、代码的行数过多(>200),可以排查下是否需要拆分
  • 代码的方法过多,考虑是否按照描述一类属性的方法,将类按照属性再拆分
  • 私有方法过多,为了复用性,可以抽取出来放到新类中作为public方法
  • 类已经找不到合适的词来形容了,职责定义已经不清晰了,可拆

2.6 开闭原则

1、概念: 当软件需要增加新功能时,可以通过增加新的代码(如类、方法等)来完成,而不是修改现有的代码

2、实战

2.1 场景1:简单工厂模式

使用简单工厂代理if -else

作用:可扩展性:新增场景、分支,只需要新增枚举即可,无需修改代码

2.2 场景2:职责链模式

定义接口对sku进行过滤,不同业务逻辑对应不同实现类,打各种标

作用:可扩展性,当新增业务场景,只需要新增打标实现类即可,无需修改代码

2.3 场景3 策略模式

将策略抽象为接口,下发单据接口(实现类1:下发退供单;实现类2:下发逆向调拨单)

作用:可扩展性,当新增下发单据场景,只需要新增实现类即可,无需修改代码

2.4 场景4:状态机模式

现在用户订单,有n个状态,可以使用状态机模式,这样后续新增单据状态,可以直接新增枚举即可,无需改动代码

2.5 场景5:装饰器模式
新增需求可以通过组合,装饰方式实现

3、如何做到满足开闭原则

  • 业务层面:扩展意识

    多想下,这块业务,后续会有哪些需求变更、新增,设计代码结构的时候,可以提前预留好扩展点,以便将来改动小,新的代码可灵活的插入(对于未来不确定的功能点,当下没必要过度设计,后续持续重构即可)

  • 技术层面:抽象意识,提升代码的可扩展性:基于抽象(接口、抽象类)编程、设计模式(简单工厂、职责链、策略、模板、状态、装饰着等)


2.7 迪米特最少知道法则

1、定义:一个类应该对自己依赖的类知道得尽可能少,且只和直接相关的类(直接朋友)进行通信,而不与陌生人(即不直接相关的类)说话

  • 直接朋友:出现在类属性、方法入出入参中的类
  • 间接朋友:出现在局部变量的类,和他们的交流(使用)就会违背迪米特法则

2、作用:低耦合
便于维护和扩展

3、实战1:

  • 背景:公司,让部门经理,打印此部门的员工姓名
    这里部门经理是公司的直接朋友,员工是公司的间接朋友,所以公司应该只和直接朋友通话

  • 违反迪米特法则的设计

    Employee作为局部变量出现在Company中

    /**
     * 公司
     */
    public class Company{
        @Resource
        private Manager manager;
    
        public void printEmployee(String departmentName) {
        	// 间接朋友-员工,出现在了公司的局部变量中
            List<Employee> employeeList = manager.getAllEmployeeInfoByDepartmentName(departmentName);
            for (Employee e : employeeList) {
                System.out.println(e.getName());
            }
        }
    }
    
    
  • 符合迪米特最少知道法则的设计

    /**
     * 公司
     */
    public class Company{
        @Resource
        private Manager manager;
    
        public void printEmployee(String departmentName) {
        	//Companyzh和直接直接朋友Manager交流
            manager.printEmployee(departmentName); 
        }
    }
    
    /**
     * 部门经理
     */
    public class Manager{
        public void getAllEmployeeInfoByDepartmentName(String departmentName) {
            // 1.获取员工信息(Manager内部实现)
            List<Employee> employeeList = getAllEmployeeInfoByDepartmentName(departmentName);
            // 2.打印员工姓名(Manager内部实现)
            printEmployeeName(employeeList);
        }
    }
    
    /**
     * 员工
     */
    public class Manager{
        private String name;
    }
    

实战2:中介者模式
在类与类之间进行交互时,可以通过中介者模式来使类之间的解耦合。


三、23种设计模式

3.1创建型:创建对象

单例、工厂、建造者、DI

3.1.1 单例模式

定义

一个类只允许创建唯一一个对象。这里唯一性作用的范围是进程

所以如果单例中引用了其它对象,这个被引用的对象在整个Jvm生命周期无法被GC回收,可能会内存泄漏

最佳实践:静态内部类
public class SkuDTO {

    private SkuDTO(){}

    private static class SkuDTOHolder {
        private static final SkuDTO INSTANCE = new SkuDTO();
    }

    public static SkuDTO getInstance() {
        return SkuDTOHolder.INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(
                    () -> System.out.println(getInstance().hashCode())//都是同一个对象
            ).start();
        }
    }
}

懒加载的线程安全单例对象

  • 懒加载:静态内部类SkuDTOHolder,不会随着SkuDTO类被加载。只有在SkuDTOHolder.INSTANCE首次调用其静态变量时,才会触发SkuDTOHolder类的加载,然后再初始化阶段,完成静态<init>静态代码块的执行和静态变量的赋值INSTANCE = new SkuDTO()

  • 线程安全:Jvm保证了,只有SkuDTOHolder.INSTANCE首次调用会执行INSTANCE = new SkuDTO()初始化赋值动作,后续的所有getInstance->SkuDTOHolder.INSTANCE 都直接返回实例对象

  • 其它单例的创建方式参考:单例模式

场景

场景1:表示全局唯一类

  • 配置类(@Configuration + @Bean)、ID生成器类等

场景2:处理共享资源访问冲突(写日志、共享数据库连接池等)

  • 线程不安全问题:同时写日志到txt文件中,可能出现内容被覆盖的情况
  • 原因:多线程并发写的时候,线程1和线程2都创建了FileWriter实例,获取到相同的pos待写入位置,都是从这个位置写入,造成内容覆盖
public class ConcurrentFileWriterExample {  
    private static final String LOG_FILE = "C:\\Users\\admin\\Desktop\\my.txt";  
  
    public static void main(String[] args) {  
        Thread thread1 = new Thread(() -> writeToFile("Thread 1: Hello from Thread 1!")); 
        Thread thread2 = new Thread(() -> writeToFile("Thread 2: Hello from Thread 2!")); 
        thread1.start();  
        thread2.start();   
    }  
  

    public static void writeToFile(String message) {  
        // 非单例,获取到相同的pos待写入位置
        try (FileWriter writer = new FileWriter(LOG_FILE)) { 			
        	// 每个线程都独立地写入文件,这可能导致内容覆盖  
            writer.write(message);  
            System.out.println("Wrote: " + message);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}

解决:创建单例对象、或new FileWriter(LOG_FILE, true),true: 追加内容到文件末尾 ,避免覆盖

线程级别的单例
public class IDGenerator {
    private static final AtomicLong id = new AtomicLong(0);
    private static final ThreadLocal<IDGenerator> tl = new ThreadLocal<>();
    
    public IDGenerator getInstance() {
        tl.set(new IDGenerator());
        return tl.get();
    }
    
    public Long getId() {
        return id.incrementAndGet();
    }
}

同一个线程获取到的对象实例是相同的,不同线程获取到的不同。属于多例

缺点

  • 单测不友好,全局变量可能会被修改,造成测试结果相互影响问题

为了保证全局唯一,除了单例外,我们还可以使用工厂模式来实现


3.1.2 工厂模式(简单工厂)

实质:工厂类根据传入的参数,动态决定应该创建哪一个产品类实例

实战1:Calendar类

1、创建Calendar实例

 Calendar instance = Calendar.getInstance();
private static Calendar createCalendar(TimeZone zone,Locale aLocale){
    //这里zone和aLocale(zh_CN)都是默认值
    Calendar cal = null;
    //根据地区的语言和国家来判断日历类型
     if (cal == null) {
            if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
                cal = new BuddhistCalendar(zone, aLocale);
            } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
                       && aLocale.getCountry() == "JP") {
                cal = new JapaneseImperialCalendar(zone, aLocale);
            } else {
                // 其他情况一律返回公历
                cal = new GregorianCalendar(zone, aLocale);
            }
        }
        return cal;
    }

2、好处:

单一职责

实战2:解析配置

将不同后缀的配置文件解析成类

  • 根据文件路径x.x.Redis.properties | x.x.MySQL.yaml、yml的文件后缀类型,创建对应的Parse解析类

  • 代码实现

public class Config{
    public Config load(String configFilePath) {
        // 1.获取配置文件后缀
        String fileSuffix = getFileSuffix(configFilePath);//(返回properties、yaml、xml等)
        
        // 2.根据后置,创建对应的解析类
        Configparser parser = createConfigParser(fileSuffix);
        
        // 3.解析文件内容
        return parser.parse(fileSuffix);
    }
    
    public Configparser createConfigParser(String fileSuffix) {
        Configparser parser;
        if ("xml".equalsIgnoreCase(fileSuffix)) {
            parser = new XmlConfigparser();
        } else if ("yaml".equalsIgnoreCase(fileSuffix)) {
            parser = new YamlConfigparser();
        } else if ("properties".equalsIgnoreCase(fileSuffix)) {
            parser = new PropertiesConfigparser();
        }
        return parser;
    }
}
  • 优化1:单一职责

    createConfigParser显然不属于Config类的内容

    根据单一职责(高内聚低耦合),将createConfigParser方法抽取到独立类中

    这个类专门负责Configparser的创建,这个类就是简单工厂类

public class ConfigparserFactory{
    public Configparser createConfigParser(String fileSuffix) {
        Configparser parser;
        if ("xml".equalsIgnoreCase(fileSuffix)) {
            parser = new XmlConfigparser();
        } else if ("yaml".equalsIgnoreCase(fileSuffix)) {
            parser = new YamlConfigparser();
        } else if ("properties".equalsIgnoreCase(fileSuffix)) {
            parser = new PropertiesConfigparser();
        }
        return parser;
    }
}
  • 优化2:

上述代码每次createConfigParser都会new一个新的Configparser对象。我们可以提前将Configparser对象创建好放到map中缓存起来,当调用createConfigParser方法时,直接从缓存中拿去。

public class ConfigParserFactory {
    private static final Map<String, Configparser> map = new HashMap<>();
    static {
        map.put("xml", new XmlConfigparser());
        map.put("yaml", new YamlConfigparser());
        map.put("properties", new PropertiesConfigparser());
    }
    
    // 这里Configparser是接口,XmlConfigparser是接口实现类
    public Configparser createConfigParser(String fileSuffix) {
        return map.get(fileSuffix);
    }
}
  • 优化3

当新增fileSuffix类型时,需要修改静态代码块中代码

可以定义fileSuffix类型枚举类,静态代码块中,遍历枚举类进行map.put即可

@Getter
@RequiredArgsConstructor
public enum FileSuffixEnum{
    YAML("yaml", XmlConfigparser.class),
    PROPERTIES("properties",YamlConfigparser.class);
    public final String type;
    public final Class<?> tClass;
}
static {
        FileSuffixEnum[] values = FileSuffixEnum.values();
        for (RuleExpEnum ruleExpEnum : values) {
            String type = ruleExpEnum.getType();
            Class<?> tClass = ruleExpEnum.getTClass();
            try {
                map.put(type, (Configparser) tClass.newInstance());
            } catch (Exception e) {
            }
        }
}
实战3:解析表达式

1、使用场景

  • 前后端交互:前端传[1,2,3],后端需要解析为[“n”,“a”,“o”]
  • 前后端交互:后端查db数据为[“n”,“a”,“o”],需要展示为[“不允许修改”,“允许修改”,“修改值大于等于补货算法可修改”]

2、规则枚举类

@Getter
@AllArgsConstructor
public enum RuleExpEnum{
    NOT_ALLOW(1,"n","不允许修改"),
    ALLOW(2,"a","允许修改"),
    OR_MODEL(3,"o","修改值大于等于补货算法可修改");
    public  final int value;
    public  final String rule;
    public  final String desc;
}

3、简单工厂类

@UtilityClass
public class RuleExpFactory {
    private static final Map<Integer, String> val2RuleMap = new HashMap<>();
    private static final Map<String, Integer> rule2ValMap = new HashMap<>();
    private static final Map<String, String> rule2DescMap = new HashMap<>();

    private static final String AND = "&&";

    static {
        for (RuleExpEnum ruleExpEnum : RuleExpEnum.values()) {
            // 1
            int value = ruleExpEnum.getValue();
            // "n"
            String ruleExp = ruleExpEnum.getRule();
            // "不允许修改"
            String desc = ruleExpEnum.getDesc();

            val2RuleMap.put(value, ruleExp);
            rule2ValMap.put(ruleExp, value);
            rule2DescMap.put(ruleExp, desc);
        }
    }

    /**
     * 1.将n&&o -> "不允许修改且修改值大于等于补货算法可修改"
     * 2. 将o  -> "修改值大于等于补货算法可修改"
     *
     * @param ruleExp 规则表达式"n"
     * @return        规则desc"不允许修改"
     */
    public String rule2Desc(String ruleExp) {
        if (StringUtils.isBlank(ruleExp)) {
            return StringUtils.EMPTY;
        }
        List<String> ruleList = Splitter.on(AND).splitToList(ruleExp);
        return ruleList.stream().map(rule2DescMap::get).collect(Collectors.joining("且"));
    }

    /**
     * 1.将[1,2] -> "n&&a"
     * 2. 将[1]  -> "n"
     *
     * @param valueList  [1,2,3]
     * @return          "n&&a&&o"
     */
    public String value2Rule(List<Integer> valueList) {
        if (CollectionUtils.isEmpty(valueList)) {
            return StringUtils.EMPTY;
        }
        return valueList.stream().map(val2RuleMap::get).collect(Collectors.joining(AND));
    }
}

4、分析

  • 原本实现是:使用StringBuilder进行append
StringBuilder sb = new StringBuilder();
if(Objects.equals(rule,"a")) {
    sb.append("允许修改")
} else if(Objects.equals(rule,"n")) {
    sb.append("且");
    sb,append("不允许修改")
} else if () {

}

当新增了规则rule,则需要再新增else if判断,再添加desc,不满足开闭原则。

且如果规则rule很多,则代码充斥着大量的else if分支判断

实战4:Mybaits中StatementHandler、Executor的创建

参考我另一篇:Mybatis源码

总结

当代码中存在大量if - else 和根据不同类型去获取指定内容、根据不同类型去创建对应实例场景时,则可以考虑使用简单工厂模式

  • 在工厂中可以使用静态代码块
  • 在静态代码块中,可以使用枚举定义,遍历枚举,完成获取、创建。

这样当新增类型时,开闭原则:只需要新增类 或 在枚举中新增实例即可


3.1.3 Builder建造者模式

和工厂模式的区别

工厂模式是创建一系列相同类型的对象

建造者模式是创建一个复杂属性的对象

和set、构造器的区别

1、构造器可能的缺点

  • 如果类中有很多的属性,则new X(太多的属性,容易赋值错)

2、set方法可能的缺点

  • 即使对象被final修饰,也是对象指向的地址是不可变的,但是堆地址的内容还是可以通过set赋值可变

当要求对象一旦被new其属性值就不允许被修改,则不能对外暴露set

  • set方法也无法校验传递参数是否正确,更无法校验多个属性之间的关系(eg:最大线程数 > 核心线程数)

3、建造者的缺点:

建造者内部类中也需要再定义一遍和外部类中一样的属性

建造者模式创建对象

1、private 构造器

2、只提供get方法,不提供set

3、定义成员内部类Builder类

  • 单个setXxx中可以校验某个属性
  • 最终build方法new 对象之前,可以校验各个属性之间的关系
@Getter
@ToString
public class ThreadConfig {
    private String name;
    private Integer coreCount;
    private Integer maxCount;
    
    private ThreadConfig(ThreadConfigBuilder threadConfigBuilder) {
        this.name = threadConfigBuilder.name;
        this.coreCount = threadConfigBuilder.coreCount;
        this.maxCount = threadConfigBuilder.maxCount;
    }

    @ToString
    private class ThreadConfigBuilder {
        public String name;
        public Integer coreCount;
        public Integer maxCount;
        
        public ThreadConfigBuilder setName(String name) {
            if (StringUtils.isBlank(name)) {
                throw new IllegalArgumentException("线程池名称不能为空");
            }
            this.name = name;
            return this;
        }

        public ThreadConfigBuilder setCoreCount(Integer coreCount) {
            if (coreCount == null || coreCount <= 0) {
                throw new IllegalArgumentException("线程池核心线程数必须为正整数");
            }
            this.coreCount = coreCount;
            return this;
        }

        public ThreadConfigBuilder setMaxCount(Integer maxCount) {
            if (maxCount == null || maxCount <= 0) {
                throw new IllegalArgumentException("线程池最大线程数必须为正整数");
            }
            this.maxCount = maxCount;
            return this;
        }
        
        public ThreadConfig build() {
            if (coreCount > maxCount) {
                throw new IllegalArgumentException("线程池最大线程数必须大于核心线程数");
            }
            return new ThreadConfig(this);
        }
    }
}
实战@Accessors
@Data
@Accessors(chain = true)
public class UserDemo {
    private String name;
    private Integer age;
}

UserDemo m = new UserDemo().setName("m").setAge(18);

3.1.4 DI依赖注入

定义

相当于一个大型工厂,负责在程序启动时,根据各种配置信息,创建对象。因为它持有一堆对象,所以又叫容器

和简单工厂区别
  • 简单工厂负责一类(如不同文本类型)对象的创建。一般要创建哪些对象,都是代码提前写死的new好
  • DI容器负责的是整个应用程序所有对象的创建。除此之外,它还要负责对象生命周期的管理。DI事先不知道要创建哪些对象,是根据解析配置来动态创建对象

具体内容参考我另一篇:Spring-IOC


3.2结构型:类或对象的组合

代理、装饰者、适配器、享元

3.2.1Proxy代理模式

定义

在不改变原有类的情况下,引入代理类来给原始类附加功能

场景

日志打印、权限校验、事务等

详解

参考我另一篇:动态代理

实战

Mybatis源码:参考我另一篇:Mybatis设计模式-Proxy代理


3.2.2 装饰器模式

作用

给原始类添加增强功能

和Proxy代理模式的区别
  • 代理模式中代理类附加的是跟原始类无关的功能(日志、权限校验等)
  • 装饰器类附加的是跟原始类相关的增强功能(原始类是直接读、装饰类增加的功能是缓存读)
实现

1、整体结构

  • 装饰器类(ADeractor)需要跟原始类(A)继承相同的抽象类(AbstractA) 或接口(IA)。
  • 装饰器类(ADeractor)中组合原始类(A)

可以对A嵌套使用多个装饰器类

2、实现

  • 接口或抽象类
public interface IA {
    void f();
}
  • 原始类
@Service
public class A implements IA{
    @Override
    public void f() {
        System.out.println("f");
    }
}
  • 装饰类
@Service
public class ADecorator implements IA{
    @Resource
    private A a;

    @Override
    public void f() {
        // 增强
        System.out.println("增强1");
        a.f();
        // 增强
        System.out.println("增强2");
    }
}
  • 使用
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
public class SpringTest {
    @Resource
    private ADecorator aDecorator;

    @Test
    public void test() {
        aDecorator.f();
    }
}
场景1:IO流
1、字符流和字节流
  • 字节流-读InpuStream
    • FileInputStream
    • ByteArrayInputStream
    • FilterInputStream
      • BufferedInputStream
      • DateInputStream
  • 字符流-读Reader
    • BufferedReader
    • InputStreamReader
      • FileReader
2、源码结构

抽象类:InputStream

A:FileInputStream

ADecorator:BufferedInputStream、DateInputStream

FileInputStream fis = new FileInputStream(new File("xxx.txt"));
BufferedInputStream bis = new BufferedInputStream(fis);
bis.read();
3、源码解析

3.1 抽象类InputStream-read()

3.2 A : read(是个nativate方法)

public class FileInputStream extends InputStream{
	private native int read() throws IOException;
}

3.3 ADecorator(BufferedInputStream)-read

这里的BufferedInputStream bis = new BufferedInputStream(fis);

=public BufferedInputStream(InputStream in, int size) {
        super(in);//super(fis)
        buf = new byte[size];
    }
=>
public class FilterInputStream extends InputStream {
    // ADecorator的父类具有InputStream属性,故ADecorator也具有此属性
    // 即ADecorator中组合了A(fis)
    protected volatile InputStream in;//fis
    protected FilterInputStream(InputStream in) {
        this.in = in;//this.fis = fis
    }   
}

==》等效

public class FilterInputStream extends InputStream {
    protected volatile FileInputStream fis;
    protected FilterInputStream(FisleInputStream fis) {
        this.fis = fis;
    }   
}

这样一来,就相当于子类ADecorator(Buffered)中通过继承父类也具有属性A(FileInputStream)

3.4 bis.read()

public synchronized int read() throws IOException {
	fill();//实现
	return getBufIfOpen()[pos++] & 0xff;
}
private void fill() throws IOException {
    // 增强功能缓存
    byte[] buffer = getBufIfOpen();
    // 调用属性a 的方法
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
}
private InputStream getInIfOpen() throws IOException {
   InputStream input = in;//fis
   return input;
}

getInIfOpen().read(buffer, pos, buffer.length - pos) ==相当于使用fis.read(buffer, pos, buffer.length - pos)

public class FileInputStream{
	public int read(byte b[], int off, int len) throws IOException {
        return readBytes(b, off, len);
    }
    
    private native int readBytes(byte b[], int off, int len) throws IOException;
}

这样一来,ADcorator中使用了buffer增强了fis的read

3.5 父类作用

完全可以直接将属性A a放入ADecorator中

public class BufferedInputStream extends InputStream {
    private FileInputStream fis;
    public int read(){
       //
    }
}

为什么采用,将A a 放入父类中,然后子类继承父类属性的方式从而子类ADecorator也具有了属性A。

父类作用:

  • 让自子类Buffered、Data只需要关注A(Fis)中需要增强的方法,比如A中的read方法的Buffered为其增强为具有缓存功能的字节流读。
  • A类的其它不需要增强的方法都交给父类FilterInputStream的去关注去实现,这样众多装饰者子类就无需重写
场景2:Mybatis二级缓存

1、作用
给原始类SimpleExecutor增加缓存读功能

2、实现结构

1)装饰器类(CachingExecutor)需要跟原始类(SimpleExecutor)继承相同的抽象类(AbstractA)或 接口(Executor)

2)装饰器类(CachingExecutor)中组合原始类(SimpleExecutor)

3、具体实现

Configuration#newExecutor

if (this.cacheEnabled) {
   executor = new CachingExecutor((Executor)executor);
}
public class CachingExecutor implements Executor {
    // 装饰着类持有原始类SimpleExecutor
    private final Executor delegate;

    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        delegate.setExecutorWrapper(this);
    }
   	// query方法:在原本方法基础上,增加读缓存功能
}
  • 装饰器类CachingExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
    // 1.获取缓存key:查询Mapper的全路径 + 方法名称 + 其它等
    Cache cache = ms.getCache();
    
    // 2.装饰增强功能!
    // 如果缓存key不为空,则尝试走缓存查询
    if (cache != null) {
       this.flushCacheIfRequired(ms);
       if (ms.isUseCache() && resultHandler == null) {
         this.ensureNoOutParams(ms, boundSql);
         // 2.1尝试查询缓存,缓存中有则直接返回
         List<E> list = (List)this.tcm.getObject(cache, key);
         if (list == null) {
            // 2.2 缓存中没有,则正常的走原始类的查询方法
            // this.delegate即原始类SimpleExecutor,原始类SimpleExecutor#query会
			//本质是调用其父类BaseExecutor的query方法
            list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
             // 2.3 将查询结果缓存
             this.tcm.putObject(cache, key, list);
          }
          return list;
       }
    }

	return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

3.2.3 适配器模式

定义

在不改变现有代码的情况下,使不兼容的接口之间能够正常工作

实现
1、类适配器-继承

使用场景

目标Target(准备有的)和原Adaptee(现有的)中大部分方法都一样,没有那么多方法需要适配。

此时可以使用类继承,这样很多方法(比如像add)Adapter都无需重写

  • 需要适配的(现有的)
public class Adaptee {
    public void query() {
        System.out.println("query");
    }
    
    public void add() {
        System.out.println("add");
    }
    
    public void delete() {
        System.out.println("delete");
    }
}
  • 适配成什么样子,即目标(准备有的)

    除了add方法还是使用Adaptee的,查询和删除都使用适配后的新方法

public interface Target {
    void queryNew();
    void add();
    void deleteNew();
}
  • 适配器(代替Adaptee)
@Service
public class Adapter extends Adaptee implements Target{
    @Override
    public void queryNew() {
        // 不做变化,仍使用现有的
        super.query();
    }

    @Override
    public void deleteNew() {
        if (true) {
            // 执行新的删除逻辑
            System.out.println("delete new");
        } else {
            super.delete();
        }
    }
    
    // 这里类适配器最大的特点就是:
    // 理论上需要重写接口的add方法,但是由于继承了父类的add,所以可以不用重写add
}
  • 使用
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
@Slf4j
public class SpringTest {
    @Resource
    private Target target;

    @Test
    public void test() throws IOException {
        target.queryNew();// Adapter#queryNew
        target.add();// Adaptee#add
        target.deleteNew();// Adapter#deleteNew
    }
}
query
add
delete new
2、对象适配器-组合
  • 适配器
@Service
public class AdapterObj implements Target{
    @Resource
    private Adaptee adaptee;

    @Override
    public void queryNew() {
        adaptee.query();
        // 再---
    }

    @Override
    public void add() {
        adaptee.add();
    }

    @Override
    public void deleteNew() {
        if (true) {
            System.out.println("delete new");
        } else {
            adaptee.delete();
        }
    }
}
  • 使用场景

目标Target和原Adaptee中大部分方法都不一样即定义不同

实战
场景1: 老代码接口不适应新需求*

1、背景

原本查询黑名单接口BlackListService#queryBlackList(Long poiId)根据仓id查询仓下的所有很名单。

本次需求查询黑名单变更为:除了需要仓id外,还需要businessType

2、思考

1)实现方式1

直接修改BlackListService#queryBlackList(Long poiId,Integer businessType)方法声明和逻辑

  • 优点:无需重构

  • 缺点:风险大,线上很多业务使用到这个方法,一旦方法有问题相当于全量了,风险不可控

2)实现方式2:适配器模式

@Service
public class BlackListAdaptee {
    public List<Object> queryBlackList(Long poiId){
        return Lists.newArrayList();
    }
}
public interface BlackListServiceTarget {
    List<Object> queryBlackListNew(Long poiId, Integer businessType);
}
@Service
public class BlackListServiceAdaptor extends BlackListAdaptee implements BlackListServiceTarget{
    @Override
    public List<Object> queryBlackListNew(Long poiId, Integer businessType) {
        //命中了灰度逻辑
        // 这里可以定义本次的灰度逻辑,或者干脆加个Lion开关,上线时候指定为true,一旦有问题,快速回切为false,继续走老逻辑
        if (true) {
            // 新逻辑
            System.out.println("根据poi和businessType查询黑名单");
            return Lists.newArrayList();
        } else {
            // 非灰度仓走老逻辑查询
            List<Object> blackList = super.queryBlackList(poiId);
            // 查询结果,再按照本次新增的businessType进行过滤即可
            return
                blackList.stream().filter(Objects::nonNull).collect(Collectors.toList());
        }

    }
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
@Slf4j
public class SpringTest {
    @Resource
    private BlackListServiceAdaptor blackListServiceAdaptor;
    
    // @Resource
    // private BlackListServiceAdaptee blackListServiceAdaptee;

    @Test
    public void test() throws IOException {
        // 原本的业务逻辑
        //List<Object> result = blackListServiceAdaptee.queryBlackList(323L);
        // 改为:
        List<Object> result = blackListServiceAdaptor.queryBlackListNew(323L, 1);
    }
}
  • 优点:风险可控,一旦方法有问题直接切灰度即可。
场景2:原代码很乱不便于继续修改

适配器,可以在不修改原有代码的基础上,增加新的功能或接口

场景3: 使用第三方类库

1、背景

当第三方类库提供的接口不符合需求时,可以通过适配器模式进行适配

2、实现方式

引入第三方Jar,实现敏感词过滤功能(A:性关键词相关、B:政治关键字相关)

public class RishManagement{
	@Resource
	private A a;
	@Resource
	private B b;
	public String filterSensitiveWords(String text) {
		String s = a.filterSexyWords(text);//内部默认实现使用xxx代替敏感词
		return b.filterPoliticalWords(s,"???");//使用???代替敏感词
	}
}

3、问题

当需要环境污染过滤C时,这个时候RishManagement会违背开闭原则

A依赖提供的方法-单个入参:使用默认实现使用xxx代替敏感词

B依赖提供的方法-两个入参,第二个入参是replace:使用String replace 代替敏感词

这样接口的调用也不统一,还需要人为指定replace

4、解决:使用对象适配器模式统一接口设计

  • Adaptee:AFilterAdaptee、BFilterAdaptee都是第三方依赖
  • Target:制定统一的接口设计,否是单入参
public interface SensitiveWordsFilterTarget {
    String filter(String text);
}
  • A-Adaptor
@Service
public class SexyWordsFilterAdapter implements SensitiveWordsFilterTarget{

    @Resource
    private AFilterAdaptee aFilterAdaptee;

    @Override
    public String filter(String text) {
        return aFilterAdaptee.filterSexyWords(text);
    }
}
  • B-Adaptor
@Service
public class PoliticalWordsFilterAdapter implements SensitiveWordsFilterTarget{
    @Resource
    private BFilterAdaptee bFilterAdaptee;

    @Override
    public String filter(String text) {
        return bFilterAdaptee.filterPoliticalWords(text, "???");
    }
}
  • RiskManager
@Service
public class RiskManager {
    @Resource
    private List<SensitiveWordsFilterTarget> sensitiveWordsFilterTargets;

    public String filterWords(String text) {
        String temp = text;
        for (SensitiveWordsFilterTarget filterAdaptor : sensitiveWordsFilterTargets) {
            temp = filterAdaptor.filter(temp);
        }
        return temp;
    }
}

这样当需要过滤:环境污染相关关键词引入C时,不需要修改RiskManager,只需要创建C-EnvironmentWordsFilterAdapter即可

遵循了开闭原则,同时也统一了接口设计


3.2.4 享元模式

定义

被共享的单元(比如类的属性,当这些属性是通用且不可变时,可以组成元,让系统共享使用)

作用

提高性能、节省内存

实战

当系统中有大量相似对象:这些对象可以通过共享内部状态来减少内存占用

场景1:QQ象棋房间

1、背景

建设一个象棋棋牌室游戏,同时在线1w个房间,每个房间是一盘对局(棋局类),对局中需要棋子(棋子类)

2、棋子类

@Data
@AllArgsConstructor
public class ChessPiece {
    /**
     * 棋子编号1-32(红黑各16)
     */
    private Integer id;
    
    private Color color;

    /**
     * 将、士、车、马---
     */
    private String name;

    /**
     * 棋子在棋局上的位置
     */
    private Integer x;
    private Integer y;
    
    public enum Color{
        RED,BLACk;
    }
}

3、棋局类

public class ChessBoard {
    private Map<Integer, ChessPiece> pieceIdMap = new HashMap<>();
    
    public ChessBoard() {
        init();
    }

    private void init() {
        // 创建棋子(棋盘开局,每个棋子的初始位置x,y不一样)
        pieceIdMap.put(1, new ChessPiece(1, ChessPiece.Color.RED, "车", 0 , 1));
        pieceIdMap.put(2, new ChessPiece(2, ChessPiece.Color.BLACk, "跑", 7 , 4));
        // 剩下30个棋子
    }
}

4、问题

如果游戏有1w个房间,则需要1w此的:

  • new ChessBoard(需要32次的new ChessPiece)

占用很大内存

5、解决

  • “变量”

我们发现棋子属性只有x、y坐标属性,对于不同房间的棋局棋子的坐标是不同的

  • “常量”

id、颜色、名称,对于不同棋局来说都是相同的属性,这些属性都是不可变的。

可以共享,可以抽取为享元类

6、优化

  • 引入享元类(最小单元)
@Data
@AllArgsConstructor
public class ChessPieceUnit {
    /**
     * 棋子编号1-32(红黑各16)
     */
    private Integer id;

    private ChessPiece.Color color;

    /**
     * 将、士、车、马---
     */
    private String name;

    public enum Color{
        RED,BLACk;
    }
}
  • 棋子类
@Data
@AllArgsConstructor
public class ChessPiece {
    /**
     * “常量”:享元类(id、color、name)
     */
    private ChessPieceUnit chessPieceUnit;

    /**
     * “变量”:棋子在棋局上的位置
     */
    private Integer x;
    private Integer y;
}
  • 引入享元工厂类-存取享元类
public class ChessPieceUnitFactory {
    private static final Map<Integer, ChessPieceUnit> pieceIdMap = new HashMap<>();

    static {
        pieceIdMap.put(1, new ChessPieceUnit(1, ChessPieceUnit.Color.RED, "车"));
        pieceIdMap.put(2, new ChessPieceUnit(2, ChessPieceUnit.Color.BLACk, "跑"));
        // 剩下30个棋子
    }

    private ChessPieceUnit getUnitByChessPieceId(Integer id) {
        return pieceIdMap.get(id);
    }
}
  • 棋局类
public class ChessBoard {
    private static final Map<Integer, ChessPiece> pieceIdAndPieceMap = new HashMap<>();
    
    public ChessBoard() {
        init();
    }

    private void init() {
        pieceIdAndPieceMap.put(1, new ChessPiece(ChessPieceUnitFactory.pieceIdMap.get(1), 0 , 1));
        pieceIdAndPieceMap.put(2, new ChessPiece(ChessPieceUnitFactory.pieceIdMap.get(2), 3 , 4));
        // 剩下30个棋子
    }
}

棋局类在put棋子的时候,棋子的享元部分属性是通过享元工厂获取的。

1w棋局,需要1w次new ChessBoard(32次new ChessPiece),但

棋子类ChessPiece中大量的属性即享元属性类ChessPieceUnit只需要创建1次,提升了性能降低内存使用

场景2:Integer实战

1、背景

Integer i3 = Integer.valueOf(123);

2、原理

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

low:-128、high:127

  • 当i的值,在大小为256的数组区间 -128-127内时,则从IntegerCache.cache缓存数组中获取
  • 不在这个区间,则new一个新的对象

显然,Integer.valueOf(1)即cache[1-(-128)] = cache[129] = 1

new Integer(123),不使用IntegerCache.cache缓存,直接创建对象

3、作用

若需要创建1w个-128-127的数字,实际上最多只需要new 256个对象,其它的都是从缓存中获取的

4、其它应用

String字符串常量池同理

线程安全问题

如果多个线程同时访问并修改共享对象,可能会导致线程安全问题


3.3行为型

观察者、模板、策略、职责链、迭代器、状态模式

3.3.1 观察者模式

定义

对象之间定义一个1 vs n的依赖,当1的状态改变时,所有依赖于它的n对象,都会接收到通知并更新

被观察者:subject

观察者: observe

和生产者消费者的区别
模式线程关系通信
观察者被观察者二者使用一个线程1:n被观察者持有观察者,在内部直接调用观察者方法
生产者消费者生产者和消费者使用不同线程n:m二者通过队列通信,生产者推,消费者拉,完全解耦
和订阅-发布模式的区别

订阅-发布模式

  • 是观察者的别名,但是后续演变成一种新的设计模式
  • 发布者,不再维护订阅者们的信息,不会再直接将信息推送个发布者们,实现了二者的完全解耦
  • 发布者和订阅者之间,存在中间件:调度中心Broker
    • 发布者只需要告诉Broker,我要发送的信息,topic为A
    • 订阅者只需要告诉Broker,我订阅的消息,topic为A
    • 当Broker接收到Topic为A的消息时,会统一调度那些订阅了Topic为A的订阅者们注册到Broker的处理代码
    • eg:你在微博上关注了A,其他人也关注了A。当A发布动态,即发送消息到微博调度中心Broker时,Broker就会为你们推送A的动态
观察者模式模版

1、观察者(N)

  • 抽象观察者
public interface Observe {
    void update();
}
  • 具体观察者
@Service
public class Observe1 implements Observe{
    @Override
    public void update() {
        System.out.println("观察者1更新");
    }
}

@Service
public class Observe2 implements Observe{
    @Override
    public void update() {
        System.out.println("观察者2更新");
    }
}

2、被观察(1)

@Service
public class Subject {
    // 持有观察者们
    @Resource
    private List<Observe> observes;

    public void notice() {
        for (Observe observe : observes) {
            observe.update();
        }
    }
}

3、使用

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
@Slf4j
public class SpringTest {
    @Resource
    private Subject subject;
    
    @Test
    public void test(){
        subject.notice();
    }
}
观察者1更新
观察者2更新
实战
场景1:用户注册成功后续动作

1、背景

用户注册app成功后,给用户发放新人券

2、实现

@RestController
public class UserAppController {
    @Resource
    private RegisterService registerService;

    @Resource
    private RegisterSuccessObserve registerSuccessObserve;

    public void register(Long iphone, String pwd) {
        // 注册
        boolean success = registerService.register(iphone, pwd);
        if (success) {
            registerSuccessObserve.issueNewConsumerCoupon(iphone);
        }
    }
}
  • 发送新人优惠券
@Service
public class RegisterSuccessObserve {
    public void issueNewConsumerCoupon(Long iphone){
        System.out.println("发送新人优惠券");
    }
}

后续新增需求:当用户注册成功后,除了发新人券还需要发送用户注册成功的短信给用户

那么UserAppController#register就必须改动了,违背了开闭原则

3、使用观察者模式重构

被观察者维护观察者们的信息,一旦注册成功后,将后续一系列动作推送给观察者们即可

1)观察者

  • 抽象观察者
public interface RegisterSSuccessObserver {
    void update(Long iphone);
}
  • 观察者们
@Service
public class IssueNewConsumerCouponObserve implements RegisterSSuccessObserver{
    @Override
    public void update(Long iphone) {
        System.out.println("发送新人优惠券");
    }
}
@Service
public class SendMsgObserve implements RegisterSSuccessObserver{
    @Override
    public void update(Long iphone) {
        System.out.println("发送注册成功短信");
    }
}

2)被观察者

@RestController
public class UserAppControllerSubject {
    @Resource
    private RegisterService registerService;

    // 持有观察者们
    @Resource
    private List<RegisterSSuccessObserver> registerSSuccessObservers;

    public void register(Long iphone, String pwd) {
        // 注册
        boolean success = registerService.register(iphone, pwd);
        if (success) {
            for (RegisterSSuccessObserver observer : registerSSuccessObservers) {
                observer.update(iphone);
            }
        }
    }
}

重构后,即使后续新增:注册成功后,给用户发送礼品卡。

也只需要礼品卡观察者即可。对于UserAppControllerSubject#register方法,满足开闭原则。遵循依赖倒转原则高层模块不直接依赖底层模块,二者通过抽象(接口或抽象类)交互。这样底层模块发生变化(更新、新增),对于高层模块不影响

4、优化- 异步非阻塞观察者模式

使用guava的EventBus模式实现异步阻塞

1)被观察者

@Slf4j
@RestController
public class UserAppControllerSubject {
    @Resource
    private RegisterService registerService;

    @Resource
    private List<RegisterSSuccessObserver> registerSSuccessObservers;
    
    private ExecutorService threadPool = Executors.newFixedThreadPool(2);
    
    private EventBus eventBus;
    
    public UserAppControllerSubject() {
        eventBus = new AsyncEventBus(threadPool, (e, context) -> {
           log.error("consumer{}, receive{},msg{} 流程异常", 
                   context.getSubscriber(), context.getSubscriberMethod() ,context.getEvent(), e);
        });
    }

    public void register(Long iphone, String pwd) {
        // 注册成功
        boolean success = registerService.register(iphone, pwd);
        if (success) {
            for (RegisterSSuccessObserver observer : registerSSuccessObservers) {
                // 订阅者-类似消费组
                eventBus.register(observer);
            }
            // 发布者send发布-类似MQ生产者的send
            eventBus.post(iphone);
        }
    }
}

2)观察者

  • 抽象观察者
public interface RegisterSSuccessObserver {
    void receive(Long iphone);
}
  • 观察者们
@Service
public class SendMsgObserve implements RegisterSSuccessObserver{
    @Override
    @Subscribe
    public void receive(Long iphone) {
        System.out.println("发送注册成功短信");
    }
}
@Service
public class IssueNewConsumerCouponObserve implements RegisterSSuccessObserver{
    @Override
    @Subscribe
    public void receive(Long iphone) {
        System.out.println("发送新人优惠券");
    }
}

3)使用

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
@Slf4j
public class SpringTest {

    @Resource
    private UserAppControllerSubject userAppControllerSubject;

    @Test
    public void test(){
        userAppControllerSubject.register(186L, "123mjp");
    }
}
发送注册成功短信
发送新人优惠券

5、优化为非阻塞

如果IssueNewConsumerCouponObserve观察者的receive方法内部流程较长,执行的慢。可能会影响整体的性能,则需要非阻塞模式,即其receive方法异步执行

@Service
public class IssueNewConsumerCouponObserve implements RegisterSSuccessObserver{
    private ExecutorService myThreadPool = Executors.newFixedThreadPool(2);

    @Override
    @Subscribe
    @Async("myThreadPool")
    public void receive(Long iphone) {
        try {
            TimeUnit.SECONDS.sleep(5L);
        } catch (Exception e) {

        }
        System.out.println("发送新人优惠券");
    }
}

启动类@EnableAsync

@EnableAsync
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ApplicationLoader {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(ApplicationLoader.class);
        springApplication.run(args);
    }
}
场景2:Spring观察者模式

事件、监听器、多播器,参考我另一篇:Spring观察者模式


3.3.2 模板模式

定义

在抽象类中定义一个逻辑:由a、b、c等小逻辑组成。

子类在不改变整体逻辑的情况下,重新定义a、b、c等一部分小逻辑

作用

复用和扩展

使用模板

1、父类模板

public abstract class Template {
    public void func() {
        m1();
        m2();
        m3();
    }
    
    protected abstract void m1();
    protected abstract void m2();
    private void m3() {
    }
}

2、子类

子类重写模板中某一个 或 多个步骤

  • 子类A
public class A extends Template{
    @Override
    protected void m1() {
        
    }

    @Override
    protected void m2() {

    }
}
  • 子类B同理
场景

适用场景

1)流程中,大部分处理方式相同,只有部分不同。相同部分可抽取为模版,不同部分抽象为方法,具体情况具体实现(eg:数据库中连接、关闭等流程相同,只有CRUD具体execute不同)

2)抽象方法,让子类去实现

场景1:AbstractList.addAll中add等效抽象方法

1、父类addAll

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        boolean modified = false;
        for (E e : c) {
            add(index++, e);
            modified = true;
        }
        return modified;
    }

其中add方法即父类预留的"抽象"方法,由子类自定义实现

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

add方法等效abstract抽象方法,因为如果子类不重写,而是直接使用父类的此add方法,显然执行报错。

通过这种方式强制子类必须重写add方法,等效抽象方法了

场景2:HttpServlet.service中doGet等效抽象方法

1、背景:

当页面请求打进来时,会首先走到HttpServlet#service方法

protected void service(HttpServletRequest req, HttpServletResponse resp){
        if (method.equals(METHOD_GET)) {
            if (lastModified == -1) {
                // 扩展点1:doGet
                doGet(req, resp);
            } else {
                //
            }
        } else if (method.equals(METHOD_POST)) {
            // 扩展点2:doPost
            doPost(req, resp);
        } else if (method.equals(METHOD_PUT)) {
            //
        }
}

其中doGet等效abstract方法

子类必须重写,否则会报错

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_get_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
        } else {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
        }
    }

2、子类重写doGet、doPost

public class MyHttpServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 自定义
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
        // 自定义
    }
}
场景3:Tomcat模版模式

1、背景

以SpringBoot内嵌的Tomcat为例

  • 接口:Lifecycle中定义了init、start、stop、destroy
  • 抽象类:LifecycleBase中重写了init、start、stop、destroy

2、以LifecycleBase重写的init方法为例:

    @Override
    public final synchronized void init() throws LifecycleException {
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }
        // m1
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        // m2
        initInternal();
        // m3
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    }

此处m2即抽象方法

protected abstract void initInternal() throws LifecycleException;

预留给子类组件:Connector、ContainerBase(StandardEngine、StandardHost、StandardContext)等去自定义实现

场景4:计算图形面积

1、父类

父类定义了抽象方法:计算图形面积

2、子类

不同子类(圆形、正方形、圆锥)等自定义实现计算面积方法


3.3.3回调函数

背景
  • 回调函数一般应用在模版模式中(所以很多回调方式直接叫XxxTemplate)

  • 当希望控制算法执行顺序,并在某些步骤上留下扩展点时,可以使用钩子函数

这里的钩子函数即回调函数,等效模版模式中的抽象方法

定义

A类中的方法a调用B类中方法b时,b方法会反过来调用A类a中注册给它的f方法

实战
场景1:
JDBCTemplate的演变

从普通版JDBC --> JDBCUtil --> JDBCTemplate

1、普通版JDBC使用

public class JDBCDemo {
    public List<User> queryUserById(Long id) {
        Connection con = null;
        Statement stm = null;
        List<User> ans = null;
        try {
            // 1.获取数据库连接对象
            con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/day21", "root", "root");
            // 2.获取sql语句的执行对象
            String sql = "select * from table_user where id = " + id;
            stm = con.prepareStatement(sql);
            // 3.执行sql
            ResultSet result = stm.executeQuery(sql);
            // 4.处理查询结果
            while (result.next()) {
                User user = new User().setName(result.getString("name"));
                ans.add(user);
            }
            return ans;
        } catch (Exception e) {

        } finally {
            // 5.关闭资源
            if (stm != null) {
                try {
                    stm.close();
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
            if (con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return ans;
    }
}

复用性问题:

获取数据库连接和关闭资源步骤,无论是查询方法还是更新方法,这俩步骤都是相同的,可以抽取出来

  • getConnection
  • close

2、优化版DBCUtils

1)jdbc.properties

driver = com.mysql.jdbc.Driver
url = "jdbc:mysql://127.0.0.1:3306/day21"
user = root
password = root

2)Util

public class JDBCUtil {
    private static String driver;
    private static String url;
    private static String user;
    private static String password;

    /**
     * 注册驱动 + 获取数据库连接对象con的前置配置
     */
    static {
        ClassLoader classLoader = JDBCUtil.class.getClassLoader();
        InputStream is = classLoader.getResourceAsStream("D:\\CodeBetter\\src\\main\\resources\\jdbc.properties");
        Properties properties = new Properties();
        try {
            properties.load(is);
            driver = properties.getProperty("driver");
            Class.forName(driver);
            url = properties.getProperty("url");
            user = properties.getProperty("user");
            password = properties.getProperty("password");
        } catch (Exception e) {
        }
    }

    /**
     * 获取数据库连接对象
     * 
     * @return
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, user, password);
    }

    /**
     * 关闭资源
     * 
     * @param con
     * @param stm
     */
    public static void close(Connection con , Statement stm){
        if (stm != null) {
            try {
                stm.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

3)使用

        Connection con = JDBCUtil.getConnection();
        PreparedStatement pst;
        // 1.查询
        String sql = "select * from table_user where id = " + 1;
        pst = con.prepareStatement(sql);
        ResultSet resultSet = pst.executeQuery();
        
        // 2.更新
        String updateSql = "update from table_user set name = mjp where id = 1";
        pst = con.prepareStatement(updateSql);
        int result = pst.executeUpdate();
		
        // 3.关闭
		JDBCUtil.close(con, pst);

问题:抽象的仍不彻底。不同的CRUD使用过程中,还是会出现con、stm等相同的对象

3、再优化版JDBCTemplate

public class DataSourceDemo {
    private static DataSource ds;
    private static Properties properties;
    static {
        ClassLoader classLoader = DataSourceDemo.class.getClassLoader();
        InputStream is = classLoader.getResourceAsStream("D:\\CodeBetter\\src\\main\\resources\\jdbc.properties");
        properties = new Properties();
        try {
            // 这里会把properties中所有属性都读取到
            properties.load(is);
        } catch (Exception e) {
        }
    }
    public static DataSource getDataSource() {
        try {
            ds = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return ds;
    }
}

使用

JdbcTemplate jtl = new JdbcTemplate(DataSourceDemo.getDataSource());
// 1.查询
String sql = "select * from table_user where id = " + 1;
Map<String, Object> map = jtl.queryForMap(sql, 1);

// 2.更新
String updateSql = "update from table_user set name = mjp where id = 1";
int result = jtl.update(updateSql, 1);

此时只有一个jtl对象(内部封装了con、pst对象),使用此对象进行CRUD即可

JDBCTemplate源码

1、含回调函数的接口

@FunctionalInterface
public interface StatementCallback<T> {
    T doInStatement(Statement stm) throws SQLException, DataAccessException;
}

2、JDBCTemplate(简化版)

1)模版方法

execute方法就属于模板方法

  • 其中:12356都是通用方法

  • 只有4是根据不同的sql,stm执行对应的CRUD语句

    方法入参action,就是预留扩展点,对应4

public <T> T execute(StatementCallback<T> action) throws DataAccessException {
		// 1、2加载数据库驱动、创建数据库连接
        Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
        
        Statement stmt = null;
        Object var11;
        try {
            // 3.创建sql语句执行对象
            stmt = con.createStatement();
            this.applyStatementSettings(stmt);
            // 4.使用回调方法执行stm.各种CRUD
            T result = action.doInStatement(stmt);
            this.handleWarnings(stmt);
            // 5.返回执行结果,可能是对象、List<对象>、int
            var11 = result;
        } catch (SQLException var9) {
            //6.关闭资源
        } finally {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(con, this.getDataSource());
        }

        return var11;
    }

3、定义查询、更新业务类CrudClass

@AllArgsConstructor
public class CrudClass {
    private JdbcTemplate jdbcTemplate;

    /**
     * 查询
     * @param sql
     * @return
     */
    public ResultSet query(String sql) {
        return (ResultSet) jdbcTemplate.execute(new StatementCallback<Object>() {
            @Override
            public Object doInStatement(Statement stm) throws SQLException, DataAccessException {
                ResultSet resultSet = stm.executeQuery(sql);
                return resultSet;
            }
        });
    }

    /**
     * 更新:直接使用Lambda表达式
     * @param sql
     * @return
     */
    public Integer update(String sql) {
        return (Integer) jdbcTemplate.execute((StatementCallback<Object>) stm -> {
            int result = stm.executeUpdate(sql);
            return result;
        });
    }
}

4、使用

JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceDemo.getDataSource());
CrudClass crudClass = new CrudClass(jdbcTemplate);

ResultSet resultSet = crudClass.query("select * from tb_user where id = 32");
Integer result = crudClass.update("update from tb_user set name = mjp where id = 32");
分析

1、回调函数定义:A类中的a调用B类b方法时,B类的b方法会反过来调用A类a中注册给它的f方法

2、JDBCTemplate中的回调函数

即CrudClass类中的query调用JdbcTemplate类execute方法时,execute方法会反过来调用CrudClass类中注册给它的doInStatement方法

3、执行顺序:

CrudClass#query–>> JdbcTemplate#execute

  • 执行步骤123

  • 步骤4QueryStatementCallback#doInStatement

    –>> CrudClass#query中的@Override doInStatement方法

  • 步骤56

4、再分析

上述的A类-CrudClass类,是我们自定义的,实际上JDBCTemplate,又充当了A类又充当了B类

1)其中作为B类b方法即jdbcTemplate.execute和上述分析中一样

2)作为A类a方法则如下

jdbcTemplate.query("", new RowMapper<Object>() {
            @Override
            public Object mapRow(ResultSet resultSet, int i) throws SQLException {
                return null;
            }
});

jdbcTemplate#query–>>query

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
	return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}

–>>this.execute即jdbcTemplate.query(即A类的a方法)

public <T> T query(final String sql, final ResultSetExtractor<T> rse){
        class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
            QueryStatementCallback() {
            }

            @Nullable
            public T doInStatement(Statement stmt) throws SQLException {
                ResultSet rs = null;
                Object var3;
                try {
                    rs = stmt.executeQuery(sql);
                    var3 = rse.extractData(rs);
                } 
                return var3;
            }
        }
    	// 即B类的b方法
        return this.execute((StatementCallback)(new QueryStatementCallback()));
    }

我们自定义的A类-CrudClass类中的query

public ResultSet query(String sql) {
        return (ResultSet) jdbcTemplate.execute(new StatementCallback<Object>() {
            @Override
            public Object doInStatement(Statement stm) throws SQLException, DataAccessException {
                ResultSet resultSet = stm.executeQuery(sql);
                return resultSet;
            }
        });
    }

和jdbcTemplate类中的query,其实一样

只不过,我们自定义CrudClass.qeury#execute方法入参是匿名内部类或Lambda表达式

而jdbcTemplate.query#execute方法入参是内部类

class QueryStatementCallback implements StatementCallback<T>, SqlProvider{

}

3)执行顺序:

  • 回调函数定义:A类中的a调用B类b方法时,B类的b方法会反过来调用A类a中注册给它的f方法
  • 其中JDBCTemplate#query即A#a
  • JDBCTemplate#execute 即B类#b
  • StatementCallback#doInStatement即f

JDBCTemplate#query -->> JDBCTemplate#execute -->>

  • 执行步骤123
  • 步骤4
T result = action.doInStatement(stmt);

此时会调用A类a中,注册给他的f方法:内部类方法(实现了接口StatementCallback#doInStatement)

QueryStatementCallback#doInStatement(内部stmt.executeQuery)

  • 步骤56
场景2: JVM Hook

1、背景

        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("JVM程序关闭时,会调用我");
            }
        }));

2、作用

JVM程序关闭时,会执行回调函数,即@Override的run方法

3、执行流程分析

1)回调函数定义:A类中的a调用B类b方法时,B类的b方法会反过来调用A类a中注册给它的f方法

  • Runtime#addShutdownHook,即B#b
  • @Override run即f

2)B#b:addShutdownHook

添加回调函数

    public void addShutdownHook(Thread hook) {
        ApplicationShutdownHooks.add(hook);
    }

    static synchronized void add(Thread hook) {
        //static IdentityHashMap<Thread, Thread> hooks;
        hooks.put(hook, hook);
    }

3)执行顺序

A#a -->Runtime#addShutdownHook() :A#a -->> B#b

  • –>> ApplicationShutdownHooks.add(hook)
    • 执行hooks.put,将钩子函数加入map
    • 同时:add方法是static方法,首次调用类的静态方法 ,会触发类加载,会执行init即静态代码块
  • ApplicationShutdownHooks#static{}
static {
        Shutdown.add(1 ,
                false,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
        );
    }
    static void runHooks() {
        // 对所有的hook线程执行start
        for (Thread hook : threads) {
            hook.start();
        }
    }

static{} -->> runHooks() -->> hook.start() -->> 执行f方法(Override的run方法)

4)总结

JvmHook,不是在在B#b中调用的f,而是利用类的加载机制,当执行B#b时,会触发执行static{}代码块,在静态代码块中调用的f方法

场景3:SpringBoot内置钩子线程关闭tomcat

1、背景

上文执行完成整个Spring的refresh方法后,会注册一个钩子函数

private void refreshContext(ConfigurableApplicationContext context) {
	//1.spring的refresh方法
	refresh(context);
	//2.注册钩子函数
	context.registerShutdownHook();	
}

2、钩子函数

AbstractApplicationContext#registerShutdownHook

public void registerShutdownHook() {
		if (this.shutdownHook == null) {
			this.shutdownHook = new Thread() {
				@Override
				public void run() {
					synchronized (startupShutdownMonitor) {
						doClose();
					}
				}
			};
			Runtime.getRuntime().addShutdownHook(this.shutdownHook);
		}
	}

3、钩子函数作用

执行doClose-关闭Tomcat容器

4、执行过程分析

回调函数定义:A类中的a调用B类b方法时,B类的b方法会反过来调用A类a中注册给它的f方法

1)执行B#b时,即Runtime#addShutdownHook

    public void addShutdownHook(Thread hook) {
        ApplicationShutdownHooks.add(hook);
    }
  • add方法体:hooks.put,将shutdownHook此线程注入hooks
  • 因为add方法是static静态方法,首次调用静态方法,会触发ApplicationShutdownHooks类的类加载。会执行init即static{}静态代码块
class ApplicationShutdownHooks {

    private static IdentityHashMap<Thread, Thread> hooks;
    
    static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        }
     }
}
  • runHooks
for (Thread hook : threads) {
    hook.start();
}

–>> f(Override run方法)–>> doClose(AbstractApplicationContext#doClose)

// Destroy all cached singletons in the context's BeanFactory.
destroyBeans();

// Close the state of this context itself.
closeBeanFactory();

// Let subclasses do some final clean-up if they wish...
// stop-tomcat
onClose();

onClose -->>ServletWebServerApplicationContext#onClose -->> stopAndReleaseWebServer -->> webServer.stop -->> TomcatWebServer#stop

stopTomcat();//停止
this.tomcat.destroy();//销毁

3.3.4 状态模式

定义

状态机由3部分组成

1、状态Status

2、事件Event

3、动作Action

其中事件Event也被称为状态转移条件。一旦触发了事件,则一定伴随着状态改变Status A -> B,可能会有相应的动作Action产生

作用

可扩展性好:后续新增状态或状态的改变带来动作的执行,只需要新增状态类 和 动作执行类即可

使用场景

1、不建议使用场景

对象的状态,单行道变换。举例:逆向任务的状态,初始化 1-> 已落表2 -> 已计算3 -> 成功8|部分成功6|失败7

2、建议使用场景

当一个对象的状态个数<=5,而且状态之间可以相互转换,当达到不同的状态会产生不同的动作时,建议使用状态模式

实战-电商订单
1、状态机

在这里插入图片描述

2、实现

2.1 抽象状态类

public interface OrderStatus {
}

2.2 状态类

  • 待付款
/**
 * 待付款状态类
 * 此状态类状态的可能转移为:
 *      待付款 -> 取消付款(用户点击了取消付款按钮)
 *      待付款 -> 待发货(用户付款了)
 */
public class WaitPay implements OrderStatus {

    private static final WaitPay instance = new WaitPay();
    private WaitPay(){}
    public static WaitPay getInstance() {
        System.out.println("订单生成,30分钟内有效");
        return instance;
    }

    /**
     * 事件Event:用户取消付款了
     */
    public void cancelPay(OrderStatusMachine machine) {
        System.out.println("===Event:用户取消了付款===");

        // 状态Status: 待付款 -> 取消付款
        machine.setOrderStatus(CancelPay.getInstance());

        // 动作Action
        System.out.println("更细订单状态为:已取消");
    }

    /**
     * 事件Event:用户付款了
     */
    public void clickPay(OrderStatusMachine machine) {
        System.out.println("===Event:用户付款了===");
        // 状态Status: 待付款 -> 待发货
        machine.setOrderStatus(WaitDeliver.getInstance());

        // 动作Action
        System.out.println("1、更细订单状态为:待发货");
        System.out.println("2、将钱存入支付宝");
        System.out.println("3、淘宝信息提醒:您的地址信息为xxx请核对");
    }
}
  • 取消付款
/**
 * 取消状态类
 * 此状态为终态,不会再转移:
 */
public class CancelPay implements OrderStatus {
    private static final CancelPay instance = new CancelPay();
    private CancelPay(){}
    public static CancelPay getInstance() {
        return instance;
    }
}
  • 待发货
/**
 * 待发货状态类
 * 此状态类状态的可能转移为:
 *      待发货 -> 退货退款(用户点击了申请退货退款按钮)
 *      待发货 -> 待收货(仓库提交发货)
 */
public class WaitDeliver implements OrderStatus{

    private static final WaitDeliver instance = new WaitDeliver();
    private WaitDeliver(){}
    public static WaitDeliver getInstance() {
        return instance;
    }

    /**
     * 事件Event:用户点击了申请退货退款按钮
     */
    public void waitDeliverApplyRefund(OrderStatusMachine machine) {
        System.out.println("===Event:用户申请了退货退款===");

        // 状态Status: 待发货 -> 退货退款
        machine.setOrderStatus(Refund.getInstance());

        // 2.动作
        System.out.println("1、更新订单状态为:退货退款");
        System.out.println("2、支付宝将钱原路退回给用户");

    }

    /**
     * 事件Event:仓库提交发货
     */
    public void submitDelivery(OrderStatusMachine machine) {
        System.out.println("===Event:仓库提交了发货===");
        // 状态Status: 待发货 -> 待收货
        machine.setOrderStatus(WaitReceive.getInstance());

        // 2.动作
        System.out.println("1、更细订单状态为:待收货");
        System.out.println("2、发送信息给用户:您的包裹正在快马加鞭的赶来");
    }
}
  • 待收货
/**
 * 待收货状态类
 * 此状态类状态的可能转移为:
 *      待收货 -> 退货退款(用户点击了申请退货退款按钮)
 *      待收货 -> 待评价(确认收货)
 */
public class WaitReceive implements OrderStatus{
    private static final WaitReceive instance = new WaitReceive();
    private WaitReceive(){}
    public static WaitReceive getInstance() {
        return instance;
    }

    /**
     * 事件Event:用户点击了申请退货退款按钮
     */
    public void waitReceiveApplyRefund(OrderStatusMachine machine) {
        System.out.println("===Event:用户申请了退货退款===");

        // 状态Status: 待收货 -> 退货退款
        machine.setOrderStatus(Refund.getInstance());

        // 2.动作
        System.out.println("1、更新订单状态为:退货退款");
        System.out.println("2、支付宝将钱原路退回给用户");
    }

    /**
     * 事件Event:用户确认收货
     */
    public void confirmReceive(OrderStatusMachine machine) {
        System.out.println("===Event:用户确认了收货===");

        // 状态: 待收货 -> 待评价
        machine.setOrderStatus(WaitReview.getInstance());

        // 动作
        System.out.println("1、更细订单状态为:待评价");
        System.out.println("2、发送信息给用户:亲,麻烦评价下商品");
    }
}
  • 退货退款
/**
 * 退货退款状态类
 * 此状态为终态,不会再转移:
 */
public class Refund implements OrderStatus{
    private static final Refund instance = new Refund();
    private Refund(){}
    public static Refund getInstance() {
        return instance;
    }
}
  • 待评价
/**
 * 待评价状态类
 * 此状态类状态的可能转移为:
 *      待评价 -> 订单完成(用户评价了商品)
 */
public class WaitReview implements OrderStatus{

    private static final WaitReview instance = new WaitReview();
    private WaitReview(){}
    public static WaitReview getInstance() {
        return instance;
    }

    /**
     * 事件Event:用户评价商品
     */
    public void reviewGoods(OrderStatusMachine machine) {
        System.out.println("===Event:用户评价了商品===");

        // 状态: 待评价 -> 完成
        machine.setOrderStatus(Finish.getInstance());

        // 动作
        System.out.println("1、更细订单状态为:已完成");
        System.out.println("2、支付宝将钱打给商家");
        System.out.println("3、用户的积分增加");
    }
}
  • 订单完成
/**
 * 完成状态类
 * 此状态为终态,不会再转移:
 */
public class Finish implements OrderStatus{

    private static final Finish instance = new Finish();
    private Finish(){}
    public static Finish getInstance() {
        return instance;
    }
}

2.3 状态机

每个非终态的状态类中的方法,都需要在状态机中定义下

@Data
public class OrderStatusMachine {

    private OrderStatus orderStatus;

    public OrderStatusMachine(){
        this.orderStatus = WaitPay.getInstance();
    }

    /**
     * 事件Event:用户取消付款了
     */
    public void cancelPay() {
        ((WaitPay) this.orderStatus).cancelPay(this);
    }

    /**
     * 事件Event:用户付款了
     */
    public void clickPay() {
        ((WaitPay) this.orderStatus).clickPay(this);
    }

    /**
     * 事件Event:待发货时,用户点击了申请退货退款
     */
    public void waitDeliverApplyRefund() {
        ((WaitDeliver) this.orderStatus).waitDeliverApplyRefund(this);
    }

    /**
     * 事件Event:用户确认收货
     */
    public void submitDelivery() {
        ((WaitDeliver) this.orderStatus).submitDelivery(this);
    }


    /**
     * 事件Event:待收货时,用户点击了申请退货退款
     */
    public void waitReceiveApplyRefund() {
        ((WaitReceive) this.orderStatus).waitReceiveApplyRefund(this);
    }

    /**
     * 事件Event:用户评价商品
     */
    public void confirmReceive() {
        ((WaitReceive) this.orderStatus).confirmReceive(this);
    }


    /**
     * 事件Event:用户评价商品
     */
    public void reviewGoods() {
        // 状态: 待评价 -> 订单完成
        ((WaitReview) this.orderStatus).reviewGoods(this);
    }
}

2.4 使用状态机

@RunWith(MockitoJUnitRunner.class)
@Slf4j
public class BaseTest {
    @Test
    public void test() {
        System.out.println("==================李四的订单start===========");
        OrderStatusMachine m1 = new OrderStatusMachine();
        // 1.李四(付款 -> 仓库发货 -> 确认收货 -> 评价)
        m1.clickPay();
        m1.submitDelivery();
        m1.confirmReceive();
        m1.reviewGoods();
        System.out.println("==================李四的订单end===========");

        System.out.println("==================王五的订单start==================");
        OrderStatusMachine m2 = new OrderStatusMachine();
        // 2.王五(付款 -> 退货退款)
        m2.clickPay();
        m2.waitDeliverApplyRefund();
        System.out.println("==================王五的订单end==================");

        System.out.println("==================赵六的订单start==================");
        OrderStatusMachine m3 = new OrderStatusMachine();
        // 3.赵六(付款 -> 仓库发货 -> 退货退款)
        m3.clickPay();
        m3.submitDelivery();
        m3.waitReceiveApplyRefund();
        System.out.println("==================赵六的订单end==================");
    }
}

3、总结

3.1 状态机中

    public void cancelPay() {
        ((WaitPay) this.orderStatus).cancelPay(this);
    }

如果不这样写,那么对于原状态为其他的比如待发货状态,不会有任何Event事件,更不会有Action动作,但是发货状态类也需要定义WaitPay方法,只不过方法内没东西

public class WaitDeliver implements OrderStatus{

    private static final WaitDeliver instance = new WaitDeliver();
    private WaitDeliver(){}
    public static WaitDeliver getInstance() {
        return instance;
    }
    
    // 因为对于WaitDeliver代发货状态而言,他没有取消付款事件,所以定义个空方法。
    // 同理,他也没有评价事件。
    public void cancelPay() {

    }

    /**
     * 事件Event:用户点击了申请退货退款按钮
     */
    public void waitDeliverApplyRefund(OrderStatusMachine machine) {
        System.out.println("===Event:用户申请了退货退款===");

        // 状态Status: 待发货 -> 退货退款
        machine.setOrderStatus(Refund.getInstance());

        // 2.动作
        System.out.println("1、更新订单状态为:退货退款");
        System.out.println("2、支付宝将钱原路退回给用户");

    }

    /**
     * 事件Event:仓库提交发货
     */
    public void submitDelivery(OrderStatusMachine machine) {
        System.out.println("===Event:仓库提交了发货===");
        // 状态Status: 待发货 -> 待收货
        machine.setOrderStatus(WaitReceive.getInstance());

        // 2.动作
        System.out.println("1、更细订单状态为:待收货");
        System.out.println("2、发送信息给用户:您的包裹正在快马加鞭的赶来");
    }
}

这样写的好处是:只有原状态为:待付款状态,才会有Event取消付款,才会有后续Action动作。其他状态类不需要关注,内部也不需要定义此方法

3.2 单例

private static final WaitDeliver instance = new WaitDeliver();

防止反复创建

3.3 扩展

对于像WaitPay状态类,当用户付款了后,状态变为:待发货。然后有好几个总做要去做

    /**
     * 事件Event:用户付款了
     */
    public void clickPay(OrderStatusMachine machine) {
        System.out.println("===Event:用户付款了===");
        // 状态Status: 待付款 -> 待发货
        machine.setOrderStatus(WaitDeliver.getInstance());

        // 动作Action
        System.out.println("1、更细订单状态为:待发货");
        System.out.println("2、将钱存入支付宝");
        System.out.println("3、淘宝信息提醒:您的地址信息为xxx请核对");
    }

当然这3个动作都可以在clickPay方法中定义,但是如果后续,用户付款后,新增加动作,或者原动作不执行了。则需要改clickPay中代码,违背了开闭原则。

  • 解决:责任链模式

参考责任链模式中场景3:自定义责任链。可以根据动作的执行先后,对应处理器的先后执行。

这样不同的动作,都实现了抽象动作,这样新增、删除等,直接增加对应的动作 - 处理器即可,满足开闭原则。

补充:前提是,clickPay事件Event触发后,动作确认存在频繁变动的场景


3.3.5 职责链模式

定义

一个请求经过A处理器处理 -->> 然后再把请求传递给B处理器处理 -->> 再传给C处理器。以此类推,形成一个链条

作用

复用、扩展

模板
1、职责链模式1:带终止

解释:有一个处理器可以处理此请求,则结束整个职责链。后续的处理器不会再被调用

  • 抽象处理器
public interface Handle {
    boolean handle();
}
  • 处理器
@Service
public class HandleA implements Handle{
    @Override
    public boolean handle() {
        // A处理器无法处理此请求
        boolean handled = false;
        // 自己的业务
        return handled;
    }
}
  • 职责链
public class HandleChain {
    @Resource
    private List<Handle> handleList;

    public void doXxx() {
        for (Handle handle : handleList) {
            boolean canDeal = handle.handle();
            if (canDeal) {
                break;
            }
        }
    }
}
2、职责链模式2:无终止

解释:职责链上的所有处理器都会依次处理此请求

  • 抽象处理器
public interface Handle {
    void handle();
}
  • 处理器
@Service
public class HandleA implements Handle{
    @Override
    public void handle() {
        // 处理请求
    }
}
  • 职责链
public class HandleChain {
    @Resource
    private List<Handle> handleList;

    public void doXxx() {
        for (Handle handle : handleList) {
            handle.handle();
        }
    }
}
场景0:敏感词过滤

1、背景

在文本发布时,如果text中如果含有性、政治、广告相关的关键字,则会被处理

处理方式一:直接禁止本次文本发布

处理方式二:过滤关键字违规词后,再发布

2、处理方式一:终止型责任链模式

  • 抽象处理器
public interface SensitiveWordFilterHandle {
    boolean doFilter(String text);
}
  • 处理器
@Service
public class SexyWordFilterHandle implements SensitiveWordFilterHandle{
    @Override
    public boolean doFilter(String text) {
        // 如果text中含有x、x、x词,则任务含有了性相关的敏感词。则会终止职责链
        if (true) {
            return true;
        }
        return false;
    }
}
其他处理器类似
  • 职责链
public class SensitiveFilterHandleChain {
    @Resource
    List<SensitiveWordFilterHandle> sensitiveWordFilterHandles;
    
    public void legalText(String text) {
        for (SensitiveWordFilterHandle handle : sensitiveWordFilterHandles) {
            boolean legal = handle.doFilter(text);
            if (legal) {
                // 允许发布
            } else {
                // 禁止
            }
        }
    }
}

3、处理方式二:无终止型责任链模式

  • 抽象处理器
public interface SensitiveWordFilterHandle {
    String doFilter(String text);
}
  • 处理器
@Service
public class SexyWordFilterHandle implements SensitiveWordFilterHandle{
    @Override
    public String doFilter(String text) {
    	// 如果text包含了a、b、c等性相关词,将这些词替换成xxx
        if (true) {
            return text.replace(abc, "xxx");
        }
        return text;
    }
}
其他处理器类似
  • 职责链
public class SensitiveFilterHandleChain {
    @Resource
    List<SensitiveWordFilterHandle> sensitiveWordFilterHandles;
    
    public String legalText(String text) {
        String temp = text;
        for (SensitiveWordFilterHandle handle : sensitiveWordFilterHandles) {
            temp = handle.doFilter(temp);
        }
        return temp;
    }
}

补充:
如果想让某个职责链实现类先执行,则可以在类上加上@Order(数字),数字越小优先级越高

1、接口

public interface StockUp {
    void make();

    void calculate();
}

2、实现类

  • 类1
@Service
@Order(2)
public class Xt implements StockUp{
    @Override
    public void make() {
        System.out.println("xt make");
    }

    @Override
    public void calculate() {
        System.out.println("xt calculate");
    }
}
  • 类2
@Service
@Order(1)
public class Trans implements StockUp {
    @Override
    public void make() {
        System.out.println("trans make");
    }
    
    @Override
    public void calculate() {
        System.out.println("trans calculate");
    }
}

3、场景

  • 问题1:假如需求变动,要求Trans转运备货类型,不需要执行make方法,

    如果Trans中仅有make方法,则直接删除Trans类即可。

但Trans中还有calculate计算方法,所以只能将其make方法内容置空。

  • 问题2:

在问题1的基础上,假如除了责任链模式用到了Trans#make(),其它地方也用到了Trans#make
这样就不能清空make方法,因为别处也在用

public interface StockUp {
    /**
     * 打标逻辑
     */
    void make();

    /**
     * 计算逻辑
     */
    void calculate();

    /**
     * 参加截单逻辑
     * @return
     */
    default boolean partCutOrder() {
        return true;
    }

    /**
     * 参加退货逻辑
     * @return
     */
    default boolean partReturn() {
        return true;
    }
}
@Service
@Order(2)
public class Xt implements StockUp{
    @Override
    public void make() {
        System.out.println("xt make");
    }

    @Override
    public void calculate() {
        System.out.println("xt calculate");
    }
}

@Service
@Order(1)
public class Trans implements StockUp{
    @Override
    public void make() {
        System.out.println("trans make");
    }
    @Override
    public void calculate() {
        System.out.println("trans calculate");
    }

    public boolean partCutOrder() {
        return false;
    }
}

  • 使用,提前过滤留下本次需要的Service
@Resource
    private List<StockUp> stockUpList;

    private List<StockUp> cutOrderList = Lists.newArrayList();

	// 其他地方调用make方法正常,不影响
    @Resource
    private Trans trans;

    @PostConstruct
    public void initCutOrderService() {
        cutOrderList = stockUpList.stream().filter(StockUp::partCutOrder).collect(Collectors.toList());
    }

    @Test
    public void test(){
        // 参加截单的
        for (StockUp stockUp : cutOrderList) {
            stockUp.make();
        }
        // 正常执行
        trans.make();
    }
场景1:Servlet-Filter
作用

可以实现对Http的请求过滤(鉴权、限流、参数验证)、对返回结果过滤(打印日志)等

作用域

支持Servlet的Web容器(tomcat、jetty)

解析
  • 抽象处理器
public interface Filter {
    public default void init(FilterConfig filterConfig) throws ServletException {}

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;

    public default void destroy() {}
}
  • 处理器
public class FilterHandler implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 处理req

        // 业务
        chain.doFilter(request, response);

        // 处理resp
    }
}
  • 职责链FilterChain

FilterChain是个规范,tomcat具体实现是ApplicationFilterChain

public final class ApplicationFilterChain implements FilterChain {
    // 当前执行到哪个Filter处理器
    private int pos = 0;
	//Filter处理器的个数
    private int n = 0;
    //职责链数组
    private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
    
    /**
     * 即chain.doFilter(request, response)方法
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {
        // ---
        internalDoFilter(request,response);
    }

    private void internalDoFilter(ServletRequest request,ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            // 获取职责链上的下一个Filter处理器
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                // 这里就有我们具体的处理器
                Filter filter = filterConfig.getFilter();
                filter.doFilter(request, response, this);
        	}
        }
      
        
      // 添加过滤器处理器
      void addFilter(ApplicationFilterConfig filterConfig) {
      }

   }
场景2:MVC-Interceptor
作用:同Filter
作用域

MVC框架的一部分

和Servlet的Filter区别
  • Filter对req、resp的过滤都在doFilter方法中,而HandlerInterceptor对req的拦截在preHandle方法中,对resp的拦截在postHandle中,是分开的
  • 执行顺序

http请求 -->> Filter -->>> doChain过滤req、resp -->> Servlet的service()中的doPost|doGet(如果自定义Servlet继承了HttpServlet) -->> DispatcherServlet的doDsipatcher()内含applyPreHandle|appltPostHandle -->> MVC HandlerIntercept的preHandle -->> XxxController

解析
  • 抽象拦截器处理器
public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		return true;
	}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}
  • 拦截器处理器
public class MyHandlerInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // 为false会拦截请求,true会放行
        // 业务逻辑
        // eg:根据req内容查询,请求是否合法、用户是否存在等。如果不满足,则请求被拦截掉,return false
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                            @Nullable ModelAndView modelAndView) throws Exception {
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                 @Nullable Exception ex) throws Exception {
        // 一定会执行,类似finally
    }
}
  • 职责链
public class HandlerExecutionChain {
    // 职责链数组
	private HandlerInterceptor[] interceptors;



	public void addInterceptor(HandlerInterceptor interceptor) {
		initInterceptorList().add(interceptor);
	}

	// 拦截req
	boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        
        // 获取职责链数组
		HandlerInterceptor[] interceptors = getInterceptors();
		if (!ObjectUtils.isEmpty(interceptors)) {
			for (int i = 0; i < interceptors.length; i++) {
                // 获取拦截器处理器
				HandlerInterceptor interceptor = interceptors[i];
                // 拦截req
				if (!interceptor.preHandle(request, response, this.handler)) {
					triggerAfterCompletion(request, response, null);
					return false;
				}
				this.interceptorIndex = i;
			}
		}
		return true;
	}
}

Filter和Interceptor具体的执行流程,参考我另一篇:SpringBoot-WebMvcAutoConfiguration

场景3:自定义职责链
  • 抽象处理器
public interface DefinedHandler {
    void postHandle(xxxReq req);
    
    void preHandle(Object req);
    
    // 执行顺序,值越小,优先级越高
    default Integer executeOrder() {
        return Integer.MIN_VALUE;
    }
}
  • 处理器
@Service
public class LockStatusHandle implements DefinedHandler{
    @Override
    public void preHandle(Object req) {
        // 处理req:比如查询某些数据
    }

    @Override
    public void postHandle(Object req) {
        // 处理resp:比如根据req过滤留下符合的数据
    }

    public Integer executeOrder() {
        return 10;
    }
}
@Service
public class AIQtyHandle implements DefinedHandler{
    @Override
    public void preHandle(Object req) {
        // 处理req:比如查询某些数据
    }

    @Override
    public void postHandle(Object req) {
        // 处理resp:比如根据req过滤留下符合的数据
    }

    public Integer executeOrder() {
        return 10;
    }
}
@Service
public class OverStockHandle implements DefinedHandler{
    @Override
    public void preHandle(Object req) {
        // 处理req:比如查询某些数据
    }

    @Override
    public void postHandle(Object req) {
        // 处理resp:比如根据req过滤留下符合的数据
    }

    public Integer executeOrder() {
        return 20;
    }
}
  • 职责链
@Component
public class HandlerChain {
    @Resource
    private List<DefinedHandler> definedHandlerList;
    
    private Map<Integer, List<DefinedHandler>> orderAndHandlerMap;
    
    @PostConstruct
    public void init() {
        // 职责链正排序(值越小,越先执行。值相同的处理器一同并发执行)
        definedHandlerList.sort((h1, h2) -> NumberUtils.compare(h1.executeOrder(), h2.executeOrder()));
        orderAndHandlerMap = definedHandlerList.stream()
                .collect(Collectors.groupingBy(DefinedHandler::executeOrder, TreeMap::new, Collectors.toList()));
    }
    
    public Map<Integer, List<DefinedHandler>> getHandlerMap() {
        return orderAndHandlerMap;
    }
}
  • 使用
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
@Slf4j
public class SpringTest {

    @Resource
    private HandlerChain handlerChain;

    private ExecutorService threadPool = Executors.newFixedThreadPool(20);
     
    
    @Test
    public void test(){
        // 获取职责链上所有处理器
        Map<Integer, List<DefinedHandler>> handlerMap = handlerChain.getHandlerMap();
        
        // 按照处理器的值大小执行处理器(值越小的处理器,先执行。相同值的处理器并发执行)
        handlerMap.forEach((order, handlerList) ->{
            handlerList.stream().map(handler -> CompletableFuture.runAsync(() ->{
                handler.preHandle(null);
            }, threadPool)).collect(Collectors.toList());
        });
    }
}
  • 分析

传统的职责链,例如Filter和HandlerInterceptor都是使用的数组存储的。然后从数组中按照处理器存储的index先后顺序一个一个取,直到全部处理器都执行完毕。

区别:这里的职责链不是数组[]存储,然后遍历index获取处理器。而是通过在抽象处理器中定义executeOrder值,每个具体的处理器,自己根据业务优先级自定义自己的处理顺序。优先级高的处理器先执行。相同优先级并行执行


3.3.6 策略模式

实战1:查看单据状态原因

背景

业务任务有1、2、3、6、7、8 六种状态。其中6和8是成功状态。剩余任务状态都是失败状态。现在想提供一个接口可以根据任务号,查询某个失败任务失败的原因。

接口入参为任务号 + 失败任务的状态

  • 定义接口
public interface Task {
    TaskStatusEnum getTaskStatus();

    String queryTaskStatus(Integer status);
}
  • 定义接口实现类
@Slf4j
@Service
public class FailedTask implements Task {
    @Override
    public TaskStatusEnum getTaskStatus() {
        return TaskStatusEnum.FAILED;
    }

    @Override
    public String queryTaskStatus(Integer status) {
        return "网路原因计算失败";
    }
}
  • 定义枚举类(和实现类一一对应)
@Getter
@RequiredArgsConstructor
public enum TaskStatusEnum {
    INIT(1,"初始化"),
    FAILED(2,"失败"),
    SUCCESS(3,"成功");

    private final Integer value;
    private final String desc;
}
  • 面向接口编程
@Resource
private List<Task> tasks;
private Map<TaskStatusEnum, Task> map;

@PostConstruct
private void initMap() {
    map = tasks.stream().collect(Collectors.toMap(Task::getTaskStatus, 			Function.identity()));
}

@Test
public void t() {
    TaskStatusEnum status = TaskStatusEnum.FAILED;
    Task task = map.get(status);
    System.out.println(task.queryTaskStatus(1));
}

这里是使用枚举 和 实现类一一对应的方式,达到set效果。

实战2:告警等级处理

1、背景

通过n分钟内业务告警m次来定级业务失败的严重程度。不同程度的告警 有不同的处理方式

3min内触发2次 ==》严重P1 ==》 电话告警

3min内触发1次 ==> 紧急P2 ==》 短信告警

10min内触发2次 ==> 正常P3 ==》 飞书告警

2、思路

规则引擎 + 策略模式

通过规则引擎来判定出严重程度

根据不同的严重程度,使用策略模式,做不同的处理
具体实现,可以参考我另一篇:EasyRules规则引擎

实战3:更新和新增使用同一个接口

根据前端入参,判断CRUD哪个场景(枚举值),执行对应的接口实现类(CRUD)

实战4:下发退供和逆向调拨单据

根据单据的类型,判断是调拨还是退供(1、2)在,执行对应的接口实现(逆向调拨、退供)


3.3.7 解释器模式

定义

描述如何 构建一个简单的"语言"解释器

作用

解释自定义“语言”

实战
背景

开发一个监控业务系统。当每分钟接口出错数目超过10,或者每分钟API的调用总数大于10w,则触发告警。当然具体的告警可以是短信、电话等

分析

我们可以把自定义的告警规则当做一种“语言”的语法规则,然后实现一个解释器即可。

针对用户的输入,判断是否触发告警

实现
  • 举例:

String exp = “key1 > 100 && key2 > 100000 || key3 == 404”;

表达式含义:

key1即每分钟的错误数大于100 ,同时key2即每分钟的接口调用量大于10w

或者即key3接口返回404

上述场景则返回true,需要告警

  • 使用
    @Test
    public void test() {
        String exp = "key1 > 100 && key2 > 100000 || key3 == 404";
        Expression expression = RuleExpressionFactory.getExpression(exp);

        Map<String, Long> map = new HashMap<>();
        map.put("key1", 200L);
        map.put("key2", 1000L);
        map.put("key3", 404L);
        boolean intercept = expression.intercept(map);
        System.out.println(intercept);
    }
  • 解释器接口
public interface Expression {
    boolean intercept(Map<String , Long> map);
}
  • 大于解释器
public class GreaterExpression implements Expression{
    private String key;
    private Long value;

    public GreaterExpression(String strExpression) {
        String[] elements = strExpression.trim().split("\\s+");
        this.key = elements[0].trim();
        this.value = NumberUtils.toLong(elements[2].trim());
    }

    public GreaterExpression(String key, Long value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public boolean intercept(Map<String, Long> map) {
        if (!map.containsKey(key)) {
            return false;
        }
        Long val = map.get(key);
        return val > value;
    }
}
  • 小于解释器
public class LessExpression implements Expression{
    private String key;
    private Long value;

    public LessExpression(String strExpression) {
        String[] elements = strExpression.trim().split("\\s+");
        this.key = elements[0].trim();
        this.value = NumberUtils.toLong(elements[2].trim());
    }

    public LessExpression(String key, Long value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public boolean intercept(Map<String, Long> map) {
        if (!map.containsKey(key)) {
            return false;
        }
        Long val = map.get(key);
        return val < value;
    }
}
  • 等于解释器
public class EqualsExpression implements Expression{
    private String key;
    private Long value;

    public EqualsExpression(String strExpression) {
        String[] elements = strExpression.trim().split("\\s+");
        this.key = elements[0].trim();
        this.value = NumberUtils.toLong(elements[2].trim());
    }

    public EqualsExpression(String key, Long value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public boolean intercept(Map<String, Long> map) {
        if (!map.containsKey(key)) {
            return false;
        }
        Long val = map.get(key);
        return Objects.equals(val, value);
    }
}
  • &&解释器
public class AndExpression implements Expression{
    private List<Expression> expressionList = new ArrayList<>();

    public AndExpression(String expression) {
        String[] elements = expression.split("&&");
        for (String ele : elements) {
            if (ele.contains("||")) {
                expressionList.add(new OrExpression(ele));
            } else if (ele.contains(">")) {
                expressionList.add(new GreaterExpression(ele));
            } else if (ele.contains("<")) {
                expressionList.add(new LessExpression(ele));
            } else if (ele.contains("==")) {
                expressionList.add(new EqualsExpression(ele));
            } else {
                throw new RuntimeException("错误的表达式");
            }
        }
    }

    public AndExpression(List<Expression> expressionList) {
        this.expressionList.addAll(expressionList);
    }
    @Override
    public boolean intercept(Map<String, Long> map) {
        for (Expression expression : expressionList) {
            if (expression.intercept(map)) {
                System.out.println(expression + "符合表达式");
            } else {
                return false;
            }
        }
        return true;
    }
}
  • ||解释器
public class OrExpression implements Expression{
    private List<Expression> expressionList = new ArrayList<>();

    public OrExpression(String expression) {
        String[] elements = expression.split("\\|\\|");
        for (String exp : elements) {
            Expression ruleExpression = RuleExpressionFactory.getExpression(exp);
            expressionList.add(ruleExpression);
        }
    }

    public OrExpression(List<Expression> expressionList) {
        this.expressionList.addAll(expressionList);
    }
    @Override
    public boolean intercept(Map<String, Long> map) {
        for (Expression expression : expressionList) {
            if (expression.intercept(map)) {
                return true;
            }
        }
        return false;
    }
}
  • 解释器工厂类
@UtilityClass
public class RuleExpressionFactory {

    public Expression getExpression(String exp) {
        if (exp.contains("&&")) {
            return new AndExpression(exp);
        } else if (exp.contains("||")) {
            return new OrExpression(exp);
        } else if (exp.contains(">")) {
            return new GreaterExpression(exp);
        } else if (exp.contains("<")) {
            return new LessExpression(exp);
        } else if (exp.contains("==")) {
            return new EqualsExpression(exp);
        }
        throw new RuntimeException();
    }
}
场景

编译器、规则引擎(这里举例阿里的规则引擎QLExpress)、正则表达式

1、地址:https://github.com/alibaba/QLExpress

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>QLExpress</artifactId>
            <version>3.3.2</version>
        </dependency>

2、 eg1:

    @Test
    public void t() throws Exception {
        ExpressRunner runner = new ExpressRunner();

        DefaultContext<String, Object> context = new DefaultContext<>();
        context.put("a", Boolean.TRUE);
        context.put("l", Boolean.TRUE);
        context.put("lo", Boolean.TRUE);
        context.put("s", Boolean.FALSE);
        String express = "a&&l&&lo&&s";//false
//        String express = "a&&llo||s"; //true
        Object res = runner.execute(express, context, null, true, false);
        System.out.println(res);
    }

eg2:

    @Test
    public void t() throws Exception {
        DefaultContext<String, MetaRuleResult> context = new DefaultContext<>();

        context.put("o", MetaRuleResult.builder().skuId(1L).result(true).metaRule("o").failureReason("").build());

        context.put("l", MetaRuleResult.builder().skuId(1L).result(false).metaRule("l").failureReason("锁库存不可更改").build());

        context.put("s", MetaRuleResult.builder().skuId(1L).result(true).metaRule("s").failureReason("").build());

        context.put("w", MetaRuleResult.builder().skuId(1L).result(false).metaRule("w").failureReason("售罄预警不可更改").build());

        context.put("lo", MetaRuleResult.builder().skuId(1L).result(true).metaRule("lo").failureReason("").build());

        context.put("llo", MetaRuleResult.builder().skuId(1L).result(false).metaRule("llo").failureReason("锁库且修改值小于等于OR值可以更改").build());


        ExpressRunner runner = new ExpressRunner();
        Object result;
        DefaultContext<String, Object> computeContext = new DefaultContext<>();
        for (Map.Entry<String, MetaRuleResult> entry : context.entrySet()) {
            computeContext.put(entry.getKey(), entry.getValue().getResult());
        }
        String ruleExpress = "o&&l&&s&&w&&lo&&llo";
        result = runner.execute(ruleExpress, computeContext, null, true, false);
        Boolean bResult = (Boolean) result;
        System.out.println(bResult);//false

        String failReason = buildFailureReason(ruleExpress, context);
        System.out.println(failReason);//售罄预警且锁库存不可更改且锁库且修改值小于等于OR值可以更改
    }

    private String buildFailureReason(String ruleExpress, DefaultContext<String, MetaRuleResult> context) {
        StringBuilder sb = new StringBuilder();
        sb.append("修改失败原因如下:");
        context.forEach((rule, meta) -> {
            if (! meta.getResult()) {
                sb.append(meta.getFailureReason() + "; ");
            }
        });
        return sb.toString();
    }
  • context

    key为规则rule内容eg:”a“允许、"llo"锁库且小于OR允许、"s"即20点后修改值小于可履约库存允许修改,

    value为,rpc查询依赖的各个数据,判断当前sku,是否满足这个规则,满足为true,不满足为false

    eg:key = “s”,此时为20点后修改最大售卖量,想把最大售卖量从50 -> 30,计算发现可履约库存为40,30 < 40,则允许修改,即MetaRuleResult的result值为true

  • express

    表达式为修改规则的组合;

    db中规则为"a&&llo"、"llo&&s&&t"等等

  • execute执行

    当满足所有的规则,全部为true,则本次此sku允许修改最大售卖量,一个规则不满足,最终结果res都会为false,不允许修改最大售卖量

    然后将每个规则,不满足的原因都记录下来 ”且“,返回给档期展示即可。

3.3.8 中介模式

定义

定义一个中介对象,用其来封装一组对象之间的交互。

将这组对象之间的交互 =》 这组对象与中介的交互。

作用
  • 避免这组对象之间的大量直接交互。解耦这组对象之间的交互
  • 将一组对象之间的交互图从网状关系,转换为星状
场景

航空管制:参与者之间的交互关系错综复杂,维护成本很高。

背景

为了让飞机在飞行的时候互相不干扰,每架飞机必须知道其他飞机每时每刻的位置,这样就需要飞机之间时刻通信。通信网络就会非常复杂

中介模式
  • 中介:塔台
  • 一组对象:飞机们
  • 通信:采用星形关系,每架飞机只和塔台交互,发送自己的位置给塔台,由中介来负责每架飞机的航线调度,这样就大大简化了对象们之间的交互
  • 风险:中介类可能变得异常复杂且庞大

四、其它

4.1 系统设计

4.1.1合理将功能划分到不同模块

  • eg:逆向计划中触发模块、计算模块、合单下发模块。模块内部高内聚,模块之间MQ交互低耦合

  • 如何判断模块的划分是否合理

    如果某个功能的修改或添加,经常需要跨系统才能完成,说明模划分不合理,职责不清晰,耦合严重。

    • good case:计划侧定量、oih定向
    • bad case:逆向计划 和 frc触发模块。原本链路为:前端 -> 物流 -> frc -> 计划。链路长,重试机制,校验机制,状态一致性复杂。重构后:前端 -> 计划

4.1.2模块之间的交互关系

  • 同步:接口(上下游)
  • 异步:MQ(同层、系统内部)
  • 也可以同步接口调用下游,下游简单校验req,然后返回resp。然后下游内部处理完成后,MQ再回掉我们本次请求的结果

4.1.3业务开发

  • 接口:设计原则 + 设计模式

  • 业务逻辑:

    • POExample放在Repository层,不要出现在Service业务逻辑层;Mapper中,只写CRUD;Repository中通过构建example调用Mapper的CRUD

    Service中对查询出的PO,进行Convert成BO|DTO

    • 接口的TReq不要渗透到底层

    • 每一层,只提供最基本的查询。至于查询的结处理成什么样,由上一层调用方自己决定。这样,每一层的方法复用性才更好。

      • 网关层,只写rpc查询。并发查询,业务侧自己封装
      • Repository只写查询出的List list。Service自己对list进行convert2DTO或别的
  • 数据体

    • PO -> BO|DTO -> VO
    • PO(id、name、age、sex、pwd、edu) -> BO(name, age, sex, edu) -> DTO(“mjp”, 1, “man”, “大学”) -> VO(“mjp”, “青年”, “男士”, “本科”)
    • BO 和 DTO基本等效 ; 数据需要返回给前端且大量字段需要后端优化展示内容才需要VO,正常DTO即可

4.2 重构

4.2.1what

在保持功能不变的前提下,利用设计原则、设计模式等理论,来修改设计上的不足 和 提高代码质量

4.2.2why

  • 避免一开始的过度设计
  • 运用模式

初级:在原有框架下写

高级:从零开始设计代码结构,搭建代码框架

资深:发觉代码、框架等存在的问题,重构

4.2.3 context

1、大型重构

  • 涉及到的面

    架构(商品、物流)、模块(触发、计算、合单下发)、代码结构(单据和单据明细)、类之间的交互(同步、异步)

  • how如何大型重构

    • 使用设计思想、原则 和模式。常见手段有
      • 模块化(计划内部划分为3个模块)
      • 解耦(计划内部,通过MQ解耦)
      • 抽象可复用(单据下发抽象成接口)
      • 分层(Controller、Service、Dao)
    • 解耦手段:封装、抽象、模块化、中间层
      • 作用
      • 哪些代码需要解耦
      • 如何解耦
    • 列出计划,分阶段重构
      • 每个阶段完成一小部分代码的重构,然后UT,再继续下一阶段的重构,让代码始终可运行
      • 每个阶段要控制重构影响的代码范围,考虑好新老兼容。切记不可无计划重构

2、小型重构

  • 涉及到的面
    • 类(单一职责、提取公共 代码)
    • 函数(里氏替换原则:Repository中方法只写查询,具体查询到的结果交于上层自己去convert,这样方法复用性更好)
    • 变量等(代码规范)

4.2.4 when

  • 新需求,涉及对老代码的改动。如果时间充足 + 老代码设计或质量存在问题,则重构
  • 重构意识,把重构作为开发的一部分,成为一种习惯。对自己和代码都好

4.2.5 如何避免重构出问题

UT

  • 本身也是一次CR
  • 代码的可测试性,从侧面也反映出代码的设计是否合理
ecute(express, context, null, true, false);
        System.out.println(res);
    }
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值