• Springboot 使用管道设计模式 , 实践案例玩一玩


    前言

    这段时间,学习群里大家讨论设计模式频率很高,可以看出来 日常搬砖 CRUD 已经让人从麻木到想脱离麻木,对代码有了些许追求。

    当然也有还没放开的小伙(N重打码照顾兄弟),不敢参与讨论,但是私下还是非常愿意学的:
     

    继续啰嗦下:

    还是那句话,学习是自己的事情,但是跟着我学未尝不可。

    不局限于参与讨论,但是我个人推崇参与讨论。

    因为在叙述一个idea,一个design ,或者是 一个 question 的时候 ,

    可能你在叙述的过程中,你就发现了自己的 漏洞;

    可能在与各位兄弟(姐妹)在思想碰撞的时候,无形中就成长了的。

    ???

     开搞。

    正文

    先看一个简图 :

    再看一下大白话 :
     

    管道 (Pipeline ) , 一节一节 管子 (Handler)组成 

    里面流通的 是 水 (Context)  

    而 每一节 管子, 直接连接的地方 ,叫 阀 (Boolean 阀值)(角阀、三角阀)

    这个触发流程工艺, 我们交给一个 执行者负责 (Executor)

    ps : 当然 你说你只有一节管子,当我没说,你现在就出去,不要再看这篇文章了。

    补充简述 接下来实例内容: 
     

    管与阀的设计


    从 第一节 管子Handler 流到 第二节 管子Handler ,能不能流过去, 我们可以在这俩 管子 之间的

    阀 做设计。


    简单举例 , 

    例如 必须在第一节管子 里面, 把水的沙子清除掉, 我这个阀能检测沙子占比, 不通过是不会让水流到第二节管子的。


    所以第一节管子 Handler, 起个名字 叫 过滤沙子管Handler 。

    然后同样, 第二节 管子, 是个加热管,升温到 100摄氏度, 因为能够流到第三节管子的阀 要求温度到100摄氏度 ,起个名字 叫 升温管Handler 。


    最后一节管子的业务,那就 加点 赤藓糖醇吧 ,就叫 加点糖管Handler 吧。

    有了这些管子, 顺序一旦被 阀 组合固定后,  就成了 固定顺序、固定步骤的 ‘责任链’。

    看到这里,这管道模式的设计,似乎不咋滴啊 ?

    且慢,还没说完。

    哪天,我们想调整某节管子的业务功能 

    例如  过滤沙子 改成 过滤沙子 + 微生物, 我们只需要对某一节 管子的 业务功能做处理即可    :

     


    又比如说, 经过讨论, 认为 一块把沙子和 微生物 过滤 ,不行,需要分开,多一节管子 处理微生物,  不慌, 直接加一节管子 就行 (具体后面实战会教大家怎么玩,轻轻松松加管子):
     

     当然,这节 过滤微生物管子Handler  ,新增在哪,都是随随便便的 :
     


    又假如有一天, 接到新的业务了 (产品又提需求了) ,

    说之前的这个 加赤藓糖醇 水  就持续保留 。

    然后我们需要整 一种水 , 这个玩意啊, 前面也是 需要过滤沙子 ,也需要升温 ,不过最后不加糖,改成加盐!


    一条新的工艺管道 , 执行者 还是 Executor , 但是这个新的工艺管道 组建起来,非常简单 。

    1. 职责单一分治
    2. 随意组合
    3. 拓展简易 
    4. 新业务易克隆新生 
    5. 角阀式固序,环环相扣

    开始敲代码 。

    ①pom文件引入依赖:

    1. commons-collections
    2. commons-collections
    3. 3.1
    4. org.springframework.boot
    5. spring-boot-starter-web
    6. org.projectlombok
    7. lombok
    8. true
    9. org.springframework.boot
    10. spring-boot-starter-test
    11. test

    ②创建一个 管道内容(父类),PipelineContext.java:

    1. import lombok.Data;
    2. import java.time.LocalDateTime;
    3. /**
    4. * @Author: JCccc
    5. * @Date: 2022-09-06 11:24
    6. * @Description: 传递到管道的上下文
    7. */
    8. @Data
    9. public class PipelineContext {
    10. /**
    11. * 模型ID 管道内容
    12. */
    13. private Long modelId;
    14. /**
    15. * 故障信息
    16. */
    17. private String failureMsg;
    18. /**
    19. * 开始时间
    20. */
    21. private LocalDateTime beginTime;
    22. /**
    23. * 结束
    24. */
    25. private LocalDateTime endTime;
    26. /**
    27. * 获取模型名
    28. * @return
    29. */
    30. public String getModelName() {
    31. return this.getClass().getSimpleName();
    32. }
    33. }

    ③ 第一个业务流程所需的 管道 Context , 赤藓糖醇工艺Context ,EditorialContext.java:
     

    1. import com.jc.pipelinedemo.context.PipelineContext;
    2. import lombok.Data;
    3. import java.util.Map;
    4. /**
    5. * @Author: JCccc
    6. * @Date: 2022-10-13 11:26
    7. * @Description: 加赤藓糖醇的工艺 Context
    8. */
    9. @Data
    10. public class EditorialContext extends PipelineContext {
    11. /**
    12. * 溯源ID 一次业务流程一个ID
    13. */
    14. private String traceId;
    15. /**
    16. * 操作人 ID
    17. */
    18. private long operatorId;
    19. /**
    20. * 业务输入参
    21. */
    22. private Map inputParams;
    23. //其他业务参数......
    24. @Override
    25. public String getModelName() {
    26. return "赤藓糖醇MODEL";
    27. }
    28. }

    ④ 创建 context处理器 ,也就是每一节管子的处理能力(interface)  ContextHandler.java :

    1. /**
    2. * @Author: JCccc
    3. * @Date: 2022-08-17 11:26
    4. * @Description: 管道中的上下文处理器
    5. */
    6. public interface ContextHandlerextends PipelineContext> {
    7. /**
    8. * 处理输入的上下文数据
    9. *
    10. * @param context 处理时的上下文数据
    11. * @return 返回 (阀值) true 则表示由下一个 ContextHandler 继续处理 ; 返回 false 则表示处理结束.
    12. */
    13. boolean handle(T context);
    14. }

    ⑤ 创建 第一节管子    砂砾浸出器  (管道处理器)    ContextGritLeacher.java :

    1. import com.jc.pipelinedemo.context.mycontext.EditorialContext;
    2. import com.jc.pipelinedemo.handler.ContextHandler;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.stereotype.Component;
    6. import org.springframework.util.CollectionUtils;
    7. import org.springframework.util.StringUtils;
    8. import java.util.Map;
    9. /**
    10. * @Author: JCccc
    11. * @Date: 2022-10-13 11:28
    12. * @Description: 砂砾浸出器 (管道处理器)
    13. */
    14. @Component
    15. public class ContextGritLeacher implements ContextHandler {
    16. private final Logger logger = LoggerFactory.getLogger(this.getClass());
    17. @Override
    18. public boolean handle(EditorialContext context) {
    19. Map formInput = context.getInputParams();
    20. if ((CollectionUtils.isEmpty(formInput))) {
    21. context.setFailureMsg("业务输入数据不能为空");
    22. return false;
    23. }
    24. //模拟 砂砾处理 业务逻辑
    25. String source = (String) formInput.get("source");
    26. if (StringUtils.isEmpty(source)) {
    27. context.setFailureMsg("材料必须存在来源地");
    28. return false;
    29. }
    30. //业务逻辑省略
    31. return true;
    32. }
    33. }

    ⑥ 创建 第二节管子    微生物消杀器(管道处理器)    ContextGermDisinfector.java :

    1. import com.jc.pipelinedemo.context.mycontext.EditorialContext;
    2. import com.jc.pipelinedemo.handler.ContextHandler;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.stereotype.Component;
    6. import org.springframework.util.CollectionUtils;
    7. import org.springframework.util.StringUtils;
    8. import java.util.Map;
    9. /**
    10. * @Author: JCccc
    11. * @Date: 2022-10-13 11:28
    12. * @Description: 微生物消杀器 (管道处理器)
    13. */
    14. @Component
    15. public class ContextGermDisinfector implements ContextHandler {
    16. private final Logger logger = LoggerFactory.getLogger(this.getClass());
    17. @Override
    18. public boolean handle(EditorialContext context) {
    19. Map formInput = context.getInputParams();
    20. if ((CollectionUtils.isEmpty(formInput))) {
    21. context.setFailureMsg("业务输入数据不能为空");
    22. return false;
    23. }
    24. //模拟 微生物处理 业务逻辑
    25. String disinfectantCode = (String) formInput.get("disinfectantCode");
    26. if (StringUtils.isEmpty(disinfectantCode)) {
    27. context.setFailureMsg("材料必须包含消毒挤编码");
    28. return false;
    29. }
    30. //业务逻辑省略
    31. return true;
    32. }
    33. }

    ⑦ 创建 第三节管子    温度加热器  (管道处理器)    ContextTempHeater.java :

    1. import com.jc.pipelinedemo.context.mycontext.EditorialContext;
    2. import com.jc.pipelinedemo.handler.ContextHandler;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.stereotype.Component;
    6. import org.springframework.util.CollectionUtils;
    7. import org.springframework.util.StringUtils;
    8. import java.util.Map;
    9. /**
    10. * @Author: JCccc
    11. * @Date: 2022-10-13 11:28
    12. * @Description: 温度加热器 (管道处理器)
    13. */
    14. @Component
    15. public class ContextTempHeater implements ContextHandler {
    16. private final Logger logger = LoggerFactory.getLogger(this.getClass());
    17. @Override
    18. public boolean handle(EditorialContext context) {
    19. Map formInput = context.getInputParams();
    20. if ((CollectionUtils.isEmpty(formInput))) {
    21. context.setFailureMsg("业务输入数据不能为空");
    22. return false;
    23. }
    24. //模拟 业务逻辑
    25. String tempRequire = (String) formInput.get("tempRequire");
    26. if (StringUtils.isEmpty(tempRequire)) {
    27. context.setFailureMsg("材料必须包含温度要求");
    28. return false;
    29. }
    30. //业务逻辑省略
    31. return true;
    32. }
    33. }

    ⑧创建 第三节管子    配料反应器  (管道处理器)    ContextMixReactor.java :

    1. import com.jc.pipelinedemo.context.mycontext.EditorialContext;
    2. import com.jc.pipelinedemo.handler.ContextHandler;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.stereotype.Component;
    6. import org.springframework.util.CollectionUtils;
    7. import org.springframework.util.StringUtils;
    8. import java.util.Map;
    9. /**
    10. * @Author: JCccc
    11. * @Date: 2022-10-13 11:28
    12. * @Description: 配料反应器 (管道处理器)
    13. */
    14. @Component
    15. public class ContextMixReactor implements ContextHandler {
    16. private final Logger logger = LoggerFactory.getLogger(this.getClass());
    17. @Override
    18. public boolean handle(EditorialContext context) {
    19. Map formInput = context.getInputParams();
    20. if ((CollectionUtils.isEmpty(formInput))) {
    21. context.setFailureMsg("业务输入数据不能为空");
    22. return false;
    23. }
    24. //模拟 配料添加 业务逻辑
    25. String mixtureScale = (String) formInput.get("mixtureScale");
    26. if (StringUtils.isEmpty(mixtureScale)) {
    27. context.setFailureMsg("材料必须包含配料比例");
    28. return false;
    29. }
    30. //业务逻辑省略
    31. return true;
    32. }
    33. }

    handler 处理器,也就是对应管道的每一节管子 :

    现在是 有了 Context 和 Context 处理器, 还差 角阀 来把 这些管子 contextHandler 组合固序 。

    ⑨ 创建 PipelineRouteConfig.java :

    1. import com.jc.pipelinedemo.context.PipelineContext;
    2. import com.jc.pipelinedemo.context.mycontext.EditorialContext;
    3. import com.jc.pipelinedemo.handler.ContextHandler;
    4. import com.jc.pipelinedemo.handler.myhandler.editorial.ContextGermDisinfector;
    5. import com.jc.pipelinedemo.handler.myhandler.editorial.ContextGritLeacher;
    6. import com.jc.pipelinedemo.handler.myhandler.editorial.ContextMixReactor;
    7. import com.jc.pipelinedemo.handler.myhandler.editorial.ContextTempHeater;
    8. import org.springframework.beans.BeansException;
    9. import org.springframework.context.ApplicationContext;
    10. import org.springframework.context.ApplicationContextAware;
    11. import org.springframework.context.annotation.Bean;
    12. import org.springframework.context.annotation.Configuration;
    13. import java.util.Arrays;
    14. import java.util.HashMap;
    15. import java.util.List;
    16. import java.util.Map;
    17. import java.util.stream.Collectors;
    18. /**
    19. * @Author: JCccc
    20. * @Date: 2022-10-13 11:28
    21. * @Description: 管道每一节管子的路由顺序配置
    22. */
    23. @Configuration
    24. public class PipelineRouteConfig implements ApplicationContextAware {
    25. private static final
    26. Mapextends PipelineContext>,
    27. Listextends ContextHandlerextends PipelineContext>>>> PIPELINE_ROUTE_MAP = new HashMap<>(4);
    28. static {
    29. PIPELINE_ROUTE_MAP.put(EditorialContext.class,
    30. Arrays.asList(
    31. ContextGritLeacher.class,
    32. ContextGermDisinfector.class,
    33. ContextTempHeater.class,
    34. ContextMixReactor.class
    35. ));
    36. }
    37. /**
    38. * 在 Spring 启动时,根据路由表生成对应的管道映射关系,
    39. * PipelineExecutor 从这里获取处理器列表
    40. */
    41. @Bean("pipelineRouteMap")
    42. public Mapextends PipelineContext>, Listextends ContextHandlerextends PipelineContext>>> getHandlerPipelineMap() {
    43. return PIPELINE_ROUTE_MAP.entrySet()
    44. .stream()
    45. .collect(Collectors.toMap(Map.Entry::getKey, this::toPipeline));
    46. }
    47. /**
    48. * 根据给定的管道中 ContextHandler 的类型的列表,构建管道
    49. */
    50. private Listextends ContextHandlerextends PipelineContext>> toPipeline(
    51. Map.Entryextends PipelineContext>, Listextends ContextHandlerextends PipelineContext>>>> entry) {
    52. return entry.getValue()
    53. .stream()
    54. .map(appContext::getBean)
    55. .collect(Collectors.toList());
    56. }
    57. private ApplicationContext appContext;
    58. @Override
    59. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    60. appContext = applicationContext;
    61. }
    62. }

    简述作用 :

    spring启动, 把 Context作为key ,然后把相关的 管道每一节都按照我们想要的循序固定好丢到list里面,作为value , 放进  PIPELINE_ROUTE_MAP 里面去。

     

    为什么要这样做的,其实就是 相当于把责任链的 责任序 做成配置化。

    目前是通过static 代码块 实现, 其实可以改成从 数据库读取 简单设置一个sort, 然后这样可以更加动态地去变换顺序。

    ⑩ 最后就是我们的 管道负责者,执行器 , PipelineExecutor.java :
     

    1. import com.jc.pipelinedemo.context.PipelineContext;
    2. import com.jc.pipelinedemo.handler.ContextHandler;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.stereotype.Component;
    6. import org.springframework.util.CollectionUtils;
    7. import javax.annotation.Resource;
    8. import java.util.List;
    9. import java.util.Map;
    10. import java.util.Objects;
    11. /**
    12. * @Author: JCccc
    13. * @Date: 2022-10-13 13:39
    14. * @Description: 管道执行器
    15. */
    16. @Component
    17. public class PipelineExecutor {
    18. private final Logger logger = LoggerFactory.getLogger(this.getClass());
    19. /**
    20. * PipelineRouteConfig 中的 pipelineRouteMap
    21. */
    22. @Resource
    23. private Mapextends PipelineContext>,
    24. Listextends ContextHandlersuper PipelineContext>>> pipelineRouteMap;
    25. /**
    26. * 同步处理输入的上下文数据
    27. * 如果处理时上下文数据流通到最后一个处理器且最后一个处理器返回 true,则返回 true,否则返回 false
    28. *
    29. * @param context 输入的上下文数据
    30. * @return 处理过程中管道是否畅通,畅通返回 true,不畅通返回 false
    31. */
    32. public boolean acceptSync(PipelineContext context) {
    33. Objects.requireNonNull(context, "上下文数据不能为 null");
    34. // 拿到context数据类型
    35. Classextends PipelineContext> dataType = context.getClass();
    36. // 获取数据处理管道list (每一节)
    37. Listextends ContextHandlersuper PipelineContext>> pipeline = pipelineRouteMap.get(dataType);
    38. if (CollectionUtils.isEmpty(pipeline)) {
    39. logger.error("{} 的管道为空", dataType.getSimpleName());
    40. return false;
    41. }
    42. // 管道是否畅通
    43. boolean lastSuccess = true;
    44. for (ContextHandlersuper PipelineContext> handler : pipeline) {
    45. try {
    46. lastSuccess = handler.handle(context);
    47. } catch (Throwable ex) {
    48. lastSuccess = false;
    49. logger.error("[{}] 处理异常,handler={}", context.getModelName(), handler.getClass().getSimpleName(), ex);
    50. }
    51. // 终止处理
    52. if (!lastSuccess) { break; }
    53. }
    54. return lastSuccess;
    55. }
    56. }

    最后就是 玩一下,简单示例写个调用service:
     

    1. import javax.annotation.Resource;
    2. /**
    3. * @Author: JCccc
    4. * @Date: 2022-10-13 13:43
    5. * @Description:
    6. */
    7. @Service
    8. public class DealService{
    9. private final Logger logger = LoggerFactory.getLogger(this.getClass());
    10. @Resource
    11. PipelineExecutor pipelineExecutor;
    12. public ResultResponse dealSync(InstanceBuildRequest request) {
    13. PipelineContext data = convertPipelineContext(request);
    14. if (Objects.isNull(data)) {
    15. return ResultResponse.returnFail("数据异常");
    16. }
    17. boolean success = pipelineExecutor.acceptSync(data);
    18. if (success) {
    19. return ResultResponse.returnSuccess(data.getModelId());
    20. }
    21. logger.error("管道处理失败:{}", data.getFailureMsg());
    22. return ResultResponse.returnFail(data.getFailureMsg());
    23. }
    24. }

    其实核心就是通过executor调用一下 : 


    看看执行效果 ,如果某个管道条件不符合,处理不了,直接终止:

    成功的效果:

    演变/变动:

    哪天需要新增某节管子 ,例如  参数预处理器 ContextPreParamProcessor

     

     

     然后在配置路由类里面,安排上对应的管子即可:


     

     假如完全来了一个新的业务流程 , 那么直接在这里 简简单单配置起来相关的管道链即可 (当然如果有些管子的共用的,也是可以自由组合起来):

     

    好的该篇就到这。 

  • 相关阅读:
    2024年csdn最新最全pytest系列——pytest-rerunfailures插件之测试用例失败重跑
    分布式金融的攻击与防护
    HTML+CSS综合案例二:CSS简介
    npm包管理器
    【网络安全】【深度学习】【入侵检测】SDN模拟网络入侵攻击并检测,实时检测,深度学习【二】
    通过T-DIAG指令对S7通信或TCP通信进行连接状态诊断的具体方法示例
    【基于pyAudioKits的Python音频信号处理(八)】语音增强:谱减法、维纳滤波和卡尔曼滤波
    前端框架—Vue
    基于粒子群优化二维Otsu的肺CT图像分割算法
    今日睡眠质量记录80分
  • 原文地址:https://blog.csdn.net/qq_35387940/article/details/127315766