需求背景
在教育中心所有课程详情页都大致包含下面这么些,每种卡片的数据可能来自不同的数据源。数据源可能就是缓存数据,也可能是第三方的接口返回数据。因此对于这个接口一次性返回这么多卡片信息还是有点重的,我们尽量要使得每个卡片请求的数据要够快。对于头部影藏卡片而言,在数据适配阶段串行调用了三个第三方接口,而且每个接口最大超时时间outTime=200~300ms,对整个接口的性能还是有很大影响。
课程详情头部隐藏卡片,课程详情头部促销卡片,
课程详情介绍卡片,课程详情促销卡片,
课程详情推荐卡片
解决方案
经过业务代码分析,多个接口之间的出入参是没有相互依赖关系的,因此可以考虑使用CompletableFuture多线程并发调用这几个接口,在数据适配的方法入口就发起并发异步调用,这样可以同时执行主流程业务,待方法完毕统一收集结果数据,这样可以不耽误主流程又可以在执行主流程同时等待第三方数据返回,降低future.get()方法阻塞的可能性。
使用原理
1、CompletableFuture< T > implements Future< T >, CompletionStage< T >
它具有Future的功能(即可以拿到异步调用出参的引用), 同时具有CompletionStage 功能也就是说代表一个完成的阶段,因此该接口很多方法是以thenXXX命名的。
2、supplyAsync(),runAsync()是Java 8中引入的CompletableFuture静态方法,两者的区别在于前者是有出入参的,后者没有出参。
supplyAsync(Supplier< U > supplier) 任务完成在ForkJoinPool.commonPool()线程中。
supplyAsync(Supplier< U > supplier,Executor executor) 任务完成在给定的Executor异步线程池中。
在现实使用的过程中尽量使用现有的线程池异步执行想要的任务,因为使用默认的ForkJoinPool执行时,如果还存在后阶段任务 thenXXX(),那么很难决定使用主线程还是刚刚使用的ForkJoinPool线程来执行下一个阶段的任务。
3、在获取future中的值(即出参引用,future.get()), 因为这个方法是阻塞的, 尽量配置超时outTime是时间,同时存在的超时异常需要发出系统警告,通知相关人员。
代码设计
private static final Map<String, BiConsumer<Object, LayoutEntry>> consumeFutureMap =
new HashMap<String, BiConsumer<Object, LayoutEntry>>(){
{
put(FIELD_IS_FAVORITE, (obj, layoutEntry) -> layoutEntry.setIsFavorite((Boolean) obj));
put(FIELD_SCHEDULED, (obj, layoutEntry) -> layoutEntry.setScheduled((Boolean) obj));
put(FIELD_ORDER_STATE, (obj, layoutEntry) -> layoutEntry.setOrderState((Integer) obj));
}
};
@Autowired
@Qualifier("asyncInvokeExecutor")
private TaskExecutor invokeExecutor;
@Override
public LayoutEntry testFuture() throws Exception {
LayoutEntry layoutEntry = new LayoutEntry();
Map<String, Future> futureMap = Maps.newHashMap();
futureMap.put(FIELD_SCHEDULED, getScheduledFuture());
futureMap.put(FIELD_IS_FAVORITE, getIsFavoriteFuture());
futureMap.put(FIELD_ORDER_STATE, getOrderStateFuture());
Thread.sleep(3000);
setThirdFieldVal(layoutEntry, futureMap);
return layoutEntry;
}
private void setThirdFieldVal(LayoutEntry layoutEntry, Map<String, Future> futureMap) {
futureMap.forEach((fieldName, future) -> {
try {
Object fieldVal = future.get(300, TimeUnit.MILLISECONDS);
Optional.ofNullable(consumeFutureMap.get(fieldName)).ifPresent(consumer -> consumer.accept(fieldVal, layoutEntry));
} catch (Exception e) {
log.error("set third field val have Exception------>", e);
}
});
}
private Future getIsFavoriteFuture(){
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
String threadName = Thread.currentThread().getName() + "---isFavorite";
System.out.println(threadName+" have execute!!!");
return Boolean.TRUE;
}, invokeExecutor);
return future;
}
private Future getScheduledFuture(){
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
String threadName = Thread.currentThread().getName() + "---scheduled";
System.out.println(threadName+" have execute!!!");
return Boolean.TRUE;
}, invokeExecutor);
return future;
}
private Future getOrderStateFuture(){
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
String threadName = Thread.currentThread().getName() + "---isFavorite";
System.out.println(threadName+" have execute!!!");
return 100;
}, invokeExecutor);
return future;
}
CleanCode的思考
@1、 future不能使用List去接收,遍历后无法预知应该对实体类哪个属性设值,需要使用Map, 但是Map需要做两种映射 [ (fieldName <----> Future), (fieldName <----> BiConsumer) ]
从而在遍历futureMap的时候,可以在方法预定义的Map里面找到方法去执行。
@2、代码结构上面要做到统一化对实体类不同属性设值,使用预定义的思想,consumeFutureMap 就是将不同属性名映射到不同的预定义方法中。那么关于Java的设值的预定义有一种特殊用法就是实现子类 + 子类代码块里面的方法访问父类方法。需要注意的点就是代码块方法是子类要能访问得到。
如下:
@3、关于任何未确定结果的数据,取值是都应该使用Optional.ofNullable(xxxx),以防空指针。
@4、对于函数式类的选择做一个总结吧,以前一直用但是没有具体分清楚
Function:功能,是指有出入参数时候使用,
Consumer:消费,就是消费掉入参,没有出参,
Supplier:提供,就是指没有条件的往外输出,没有入参,只有出参
Predicate:预测断言,就是对入参校验,是否符合预期的值
BiFunction、BiConsumer、BiSupplier都是可以支持多个入参的