基于SpringBoot实现管道模式

前言

最近在写需求的时候,遇到了一个场景,大概流程就是接收一个外部请求,然后启动一个异步任务,在异步任务里面进行一系列的操作,比如:数据处理、调用A服务、调用B服务等等;这些步骤有着先后顺序,存在数据依赖;很自然地我就开始面向过程编程了,在写代码的过程中,我就意识到这些数据依赖比较混乱,我要创建比较多的对象,个个对象之间又存在一定的耦合;我思考了一下,一不做二不休,我直接整了一个context对象,把所有的数据全装进去;我这么干了之后,又发现,这些所有的子步骤方法调用传入参数都是context,然后也不需要返回值,因为返回值相关的对象也都写进了context;自此,我开始研究,应该存在某种设计模式来处理这类问题。搜了一圈发现,比较类似就是责任链模式和管道模式了。

简介

以下简介来自参考资料:设计模式最佳套路2 —— 愉快地使用管道模式
管道模式(Pipeline Pattern) 是责任链模式(Chain of Responsibility Pattern)的常用变体之一。在管道模式中,管道扮演着流水线的角色,将数据传递到一个加工处理序列中,数据在每个步骤中被加工处理后,传递到下一个步骤进行加工处理,直到全部步骤处理完毕。 PS:纯的责任链模式在链上只会有一个处理器用于处理数据,而管道模式上多个处理器都会处理数据。

实现

我的代码实现基本上也是参考设计模式最佳套路2 —— 愉快地使用管道模式,然后根据自己的实际需求进行了一定改造,这篇文章是一个简单的总结,如果你有相关的需求,建议阅读设计模式最佳套路2 —— 愉快地使用管道模式

定义任务上下文

任务上下文很简单,把整个任务需要用的数据都往里面塞就好了

public class TaskContext {
    private long startTime;
    private long endTime;

    /**
     * 任务ID
     */
    private String taskId;

    /**
     * 数据A
     */
    private DataA dataA;
    
    /**
     * 数据B
     */
    private DataB dataB;
    /**
     * 结果A
     */
    private ResultA resultA;

    /**
     * 结果B
     */
    private ResultB resultB;
}

上下文处理器接口

接口有个handle()方法,入参是TaskContext,子处理器通过实现该方法来进行相关的业务逻辑处理

public interface ContextHandler {
    /**
     * 处理任务上下文
     *
     * @param context 上下文
     */
    void handle(TaskContext context);
}

上下文子处理器

可以定义多个子处理器进行相关的业务逻辑处理

@Slf4j
@Component
public class SubContextHandler implements ContextHandler {
    @Override
    public void handle(TaskContext context) {
        log.info("SubContextHandler: taskId = [{}]", context.getTaskId());
		// 相关的业务逻辑处理
    }
}

处理器配置

我们在定义了多个子处理器后,需要申明他们调用的先后顺序,这里通过配置类来实现

@Configuration
public class HandlersConfig implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Bean("contextHandlers")
    public List<? extends ContextHandler> getHandlers() {
    	// 这里申明子处理器的调用顺序
        return Stream.of(SubContextHandlerA.class, SubContextHandlerB.class, SubContextHandlerC.class)
            .map(applicationContext::getBean)
            .collect(Collectors.toList());
    }
}

对子处理类使用@Component注解,然后使用applicationContext获取相关的类对象@Bean(“contextHandlers”);

服务调用

服务调用这块的话,因为我是异步任务,就使用了SpringBoot的@Async注解,在异步方法里面完成整个任务

@Service
@Slf4j
public class AsyncService {
	// 注入处理器对象列表
    @Resource
    private List<? extends ContextHandler> contextHandlers;

    /**
     * 启动异步任务
     *
     * @param taskContext 任务上下文
     */
    @Async(ExecutorConfig.ASYNC_TASK)
    public void runTask(TaskContext taskContext) {
        String taskId = taskContext.getTaskId();
        log.info("start asyncTask: taskId = [{}]", taskId);
        long startTime = System.currentTimeMillis();
        try {
            for (ContextHandler handler : contextHandlers) {
                handler.handle(taskContext);
            }
        } catch (Exception ex) {
            throw new RuntimeException();
        }
        long endTime = System.currentTimeMillis();
        log.info("finish asyncTask: taskId = [{}], cost times = [{}]", taskId, endTime - startTime);
    }
}

总结

好了,代码讲的差不多,这实际上不能算是一个真正意义上的管道模式,但是在真正的业务场景下,我们不可能说直接套用任何的设计模式,基本上都是要结合具体业务进行化用和改造;这里再说一点,管道模式实际上还不能满足我的需求;在最后的异步任务中,我使用了

        try {
            for (ContextHandler handler : contextHandlers) {
                handler.handle(taskContext);
            }
        } catch (Exception ex) {
            throw new RuntimeException();
        }

try-catch来处理异常,而我真正的需求是在任务处理结束后,去回调一个接口通知上游任务结果,也就是说不管任务成功还是失败我都要去回调接口,我思考的点在于,回调接口是应该作为最后一个handler步骤(即一个subHandler),还是说作为handler步骤结束后的一个步骤,即是说catch到异常通知任务失败,否则通知任务成功。很纠结,先这样吧!我(bu)爱(xiang)上班!!!

参考资料

设计模式最佳套路2 —— 愉快地使用管道模式

填坑

把回调接口作为独立的步骤,对管道进行try-catch,使用标记isSmooth判断任务是否顺利,实现function接口完成任务回调

        // 标志任务管道是否畅通
        boolean isSmooth = true;
        for (ContextHandler handler : contextHandlers) {
            try {
                isSmooth = handler.handle(taskContext);
            } catch (Exception ex) {
                isSmooth = false;
            }
            if (!isSmooth) {
                break;
            }
        }
        return isSmooth;
    public void createAsyncTask(TaskContext taskContext,
        BiConsumer<TaskContext, Boolean> callback) {
        boolean isSmooth = runTask(taskContext);

        // 回调
        callback.accept(taskContext, isSmooth);
    }

callback实现BiConsumer接口,accept()方法内部完成回调

补充

  1. 反射获取实例的属性值
  2. 自定义线程池拒绝策略
  3. bean继承,通常是private,但是如何使用context的话,涉及到参数转换,不用继承写起来很冗余

踩坑

最初是使用

    @Resource
    private List<? extends ContextHandler> contextHandlers;

注入ContextHandler
后来因为某些规范要求(就是大家都用,所以你最好也用,减少不必要的问题),改成了@Autowired注解
结果问题出现了,执行结果乱七八糟;最终debug发现:注入的contextHandlers list乱序了;
由此去研究一下@Autowired和@Resource的区别。
简单讲,@Autowired 注入list对象,你必须去每个bean声明@order(int);多的不展开,反正就是个坑,面试的时候可以吹

Resource和Autowired的区别

前文提到声明Bean对象,是一个ContextHandler实现类的list

    @Bean("contextHandlers")
    public List<? extends ContextHandler> getHandlers() {
    	// 这里申明子处理器的调用顺序
        return Stream.of(SubContextHandlerA.class, SubContextHandlerB.class, SubContextHandlerC.class)
            .map(applicationContext::getBean)
            .collect(Collectors.toList());
    }

然后使用Autowired注入

    @Autowired
    private List<? extends ContextHandler> contextHandlers;

此时发现该list的顺序跟声明bean中list的顺序是不一致的;根本原因是Autowired默认按byType自动装配(可以用@Qualifier声明具体注入的bean对象),它会获取数组元素的类型(ContextHandler)的子类,查找到下面这些SubContextHandler,注入到数组里面去,返回一个contextHandlers数组;所以用@Autowired注入的contextHandlers跟@Bean(“contextHandlers”)并不是同一个对象;但是用Resource就不会有这个问题;因为Resource是支持byName自动装配的,它查找到contextHandlers这个bean对象就直接注入了。
Spring 注入 Bean 到 List / Map 中

@Slf4j
@Component
public class SubContextHandler implements ContextHandler {
    @Override
    public void handle(TaskContext context) {
        log.info("SubContextHandler: taskId = [{}]", context.getTaskId());
		// 相关的业务逻辑处理
    }
}

那么用Autowired注入的list如何保证元素顺序呢?

@Slf4j
@Component
@Order(1)
public class SubContextHandler implements ContextHandler {
    @Override
    public void handle(TaskContext context) {
        log.info("SubContextHandler: taskId = [{}]", context.getTaskId());
		// 相关的业务逻辑处理
    }
}

在子类bean对象加Order注解;注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序,Bean的加载顺序不受@Order或Ordered接口的影响;

@Autowired和@Resource的区别

每日一个奇淫巧技:
今天有个需求,把之前异步实现的接口再同步实现一个???
主要原因是:数据处理跟算法那块会优化,后续如果响应速度足够了,就直接切到同步接口,属于是未雨绸缪了。

那么,应该怎么样实现呢?这里我个人总结了主要有几点:

  1. 同步跟异步代码差别不大,但是又有差别;
  2. 差别在哪:异步是返回一个id,提供一个查询接口去获取任务结果;同步是直接返回任务结果;
  3. 同步是一条路走到黑返回结果,异步是丢到线程池就返回结果;
  4. 后续肯定会干掉某个接口,所以我们不能让这两者柔和在一起,也就是说,我们不能写if-else,我们要优雅;
    实现步骤:

抽取基类

把之前实现的异步类抽出来作为一个基类

public class BaseService {
	// 注入处理器对象列表
    @Resource
    private List<? extends ContextHandler> contextHandlers;
    }
    // method

继承基类实现同步和异步类

@Service
@Slf4j
public class AsyncService extends BaseService {
    public AsyncService (List<? extends ContextHandler> contextHandlers) {
        super(contextHandlers);
    }
    @Override
    @Async
    public void createTask(TaskContext taskContext,
        BiConsumer<TaskContext, Boolean> callback) {
        log.info("create async task.");
        super.createTask(taskContext, callback);
    }
}
public class SyncService extends BaseService {
    public SyncService(List<? extends ContextHandler> contextHandlers) {
        super(contextHandlers);
    }
    @Override
    public void createTask(TaskContext taskContext,
        BiConsumer<TaskContext, Boolean> callback) {
        log.info("create sync task.");
        super.createTask(taskContext, callback);
    }
}

两个实现类的差别就在@Async;后续两者还有什么差异的话,可以在自己内部实现就好啦,两者没什么耦合;

然后,针对解决返回值不同的问题:前文提到我是用了taskContext,所以在代码形式上,我都是直接返回taskContext,同步任务从中取taskResult;异步任务从中取taskId(要注意异步任务这时taskResult是空的);

    TaskContext createTask(Request request, BaseService service);

这里在提供了方法,根据传入的service去执行对应的任务;

总结:

  1. 从入口处就区分了同步和异步,后面的流程一致;减少了冗余代码;
  2. 这样设计还是有风险的,因为哪天,它用异步任务去做其他事情,代码就要改动了,但是好像也不影响,就相当于一个新的功能需求,不影响同步接口;

我在思考怎么设计这个的时候,实现找不到一个合适的设计模式;最相关的可能就是泛型和模板设计模式了,这里也算是用了一点点吧。泛型和模板设计模式
看一遍好像我实现的就是模板设计模式,只不过是它返回值用泛型,我用的是context,hhhh

之前我的主管说过一句话:当你在思考怎么去除重复代码的时候,就是设计模式出现的时候~
虽然我的设计不够优雅,但是就针对目前的需求来看,它基本上是最小化重复代码了。

继续努力!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值