前言
最近在写需求的时候,遇到了一个场景,大概流程就是接收一个外部请求,然后启动一个异步任务,在异步任务里面进行一系列的操作,比如:数据处理、调用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)上班!!!
参考资料
填坑
把回调接口作为独立的步骤,对管道进行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()方法内部完成回调
补充
- 反射获取实例的属性值
- 自定义线程池拒绝策略
- 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接口的影响;
每日一个奇淫巧技:
今天有个需求,把之前异步实现的接口再同步实现一个???
主要原因是:数据处理跟算法那块会优化,后续如果响应速度足够了,就直接切到同步接口,属于是未雨绸缪了。
那么,应该怎么样实现呢?这里我个人总结了主要有几点:
- 同步跟异步代码差别不大,但是又有差别;
- 差别在哪:异步是返回一个id,提供一个查询接口去获取任务结果;同步是直接返回任务结果;
- 同步是一条路走到黑返回结果,异步是丢到线程池就返回结果;
- 后续肯定会干掉某个接口,所以我们不能让这两者柔和在一起,也就是说,我们不能写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去执行对应的任务;
总结:
- 从入口处就区分了同步和异步,后面的流程一致;减少了冗余代码;
- 这样设计还是有风险的,因为哪天,它用异步任务去做其他事情,代码就要改动了,但是好像也不影响,就相当于一个新的功能需求,不影响同步接口;
我在思考怎么设计这个的时候,实现找不到一个合适的设计模式;最相关的可能就是泛型和模板设计模式了,这里也算是用了一点点吧。泛型和模板设计模式
看一遍好像我实现的就是模板设计模式,只不过是它返回值用泛型,我用的是context,hhhh
之前我的主管说过一句话:当你在思考怎么去除重复代码的时候,就是设计模式出现的时候~
虽然我的设计不够优雅,但是就针对目前的需求来看,它基本上是最小化重复代码了。
继续努力!