在上一篇 小白的web优化之路 三、使用MQ来实现事务异步处理 中,我们使用MQ来将任务异步话,使接口速度得到的极大的提高,这一节中,我们就要考虑一个需要同步返回多次请求的问题。
在完成了产品经理的那个发送短信通知的‘古怪’需求之后,小白觉得自己终于可以继续开发CURD的任务了,心里不免有点小激动。突然,小白收到了产品经理的一个消息,小白心里一震,打开消息,屏幕上写着“小白我这有一个获取信息的需求,你过来一下吧”。
小白心里一想,不就是获取信息么,谁不会呢?哪知道去了产品经理的需求后,小白才意思到自己还是太年轻,这个获取信息的接口需要调用公司其它组的多个接口,然后将数据聚合起来,而且数据要求实时,并根据用户喜好返回不同的数据,这就要求很难去做一个缓存。但是如果一个一个去调用接口的话,那岂不是这个接口的总时长要爆炸?前端肯定是不会同意的?那怎么办呢?
这时候小白的主管过来了,看到这种情况,跟小白说,我们可以使用异步请求接口,这样假使有5个接口,我们也可以以请求一个接口的速度来获取到数据。小白开始嘀咕,如果这样的话,那我是启动五个线程,去执行对应的Runnable,那我岂不是在主线程中循环的判断这个Runnable有没有执行完,并获取到相应的结果,听起来好复杂啊。主管笑了笑,你可以使用Java中的Callable与Future。
在Java中,其实已经有了对异步处理任务并返回相应结果的类,即为Callable与Future。其中Callable为我们执行任务的接口,我们实现了Callable接口之后,将其封装到Future中,既可以使用Future来获取异步数据值,停止异步任务,捕捉异步操作中的异常等。
我们以接口中异步获取5个网站数据为例,下面是Callable接口:
package com.happyheng.callable;
import com.happyheng.utils.HttpUtils;
import org.springframework.util.StringUtils;
import java.util.concurrent.Callable;
/**
* 模拟访问的数据
* Created by happyheng on 2018/6/23.
*/
public class RequestCallable implements Callable{
private String requestUrl;
public RequestCallable(String requestUrl) {
this.requestUrl = requestUrl;
}
public String getRequestUrl() {
return requestUrl;
}
public void setRequestUrl(String requestUrl) {
this.requestUrl = requestUrl;
}
@Override
public String call() throws Exception {
if (StringUtils.isEmpty(requestUrl)) {
return null;
}
long beginTime = System.currentTimeMillis();
// 开始http请求
String result = HttpUtils.get(requestUrl);
System.out.println("callable url: " + requestUrl + " 用时 --------"
+ (System.currentTimeMillis() - beginTime) + " ms");
return result;
}
}
可以看到,Callable代码很简单,传入一个url,然后调用HttpUtils来获取数据。
下面是异步获取数据的Service,里面使用了Future来封装Callable,并使用线程池异步执行,最后获取到最终数据:
package com.happyheng.service;
import com.happyheng.callable.RequestCallable;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
*
* Created by happyheng on 2018/6/23.
*/
@Service
public class CallableService {
private static final int FUTURE_TIMEOUT = 5000;
// 使用先进先出的队列
public final static BlockingQueue queue = new LinkedBlockingQueue<>();
// 执行callable的线程池
public static ExecutorService executorService = new ThreadPoolExecutor(5,
15 ,
10,
TimeUnit.SECONDS,
queue);
public List getCallableData(List requestUrlList) {
if (CollectionUtils.isEmpty(requestUrlList)) {
return new ArrayList<>();
}
List> futureTaskList = new ArrayList<>();
for (String url : requestUrlList) {
RequestCallable callable = new RequestCallable(url);
FutureTask futureTask = new FutureTask<>(callable);
futureTaskList.add(futureTask);
// 线程池开始获取数据
executorService.submit(futureTask);
}
// 获取数据
List resultDataList = new ArrayList<>();
for (FutureTask futureTask : futureTaskList) {
try {
// 获取数据,注意有超时时间,如果超出,即获取不到数据
String resultData = futureTask.get(FUTURE_TIMEOUT, TimeUnit.MILLISECONDS);
resultDataList.add(resultData);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("超时");
e.printStackTrace();
}finally {
// 最后一定要调用cancel方法,里面的参数 mayInterruptIfRunning 是是否在运行的时候也关闭,如果设置为true,那么在
// 运行的时候也能关闭,之后的代码不会再执行。
// 如果正在运行,暂停成功,会返回true,如果运行完了,那么不管 mayInterruptIfRunning 是什么值,都会返回false。
futureTask.cancel(true);
}
}
return resultDataList;
}
}
最后Controller进行调用:
package com.happyheng.controller;
import com.alibaba.fastjson.JSON;
import com.happyheng.service.CallableService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
/**
*
* Created by happyheng on 2018/6/18.
*/
@RestController
@RequestMapping("/callable")
public class CallableController {
@Autowired
private CallableService callableService;
@RequestMapping("/test")
public String test(HttpServletRequest request) {
long beginTime = System.currentTimeMillis();
// 访问网站
List requestUrlList = new ArrayList<>();
requestUrlList.add("https://www.baidu.com");
requestUrlList.add("http://news.baidu.com");
requestUrlList.add("http://www.qq.com");
requestUrlList.add("http://www.sina.com.cn");
requestUrlList.add("http://www.163.com");
// 异步获取数据
List resultData = callableService.getCallableData(requestUrlList);
System.out.println("总用时 ------- " + (System.currentTimeMillis() - beginTime) + " ms");
return JSON.toJSONString(resultData);
}
}
最后的结果为:
可以看到,我们的总时长差不多是接口中请求最高的时长,是符合预期的。
示例代码已放到github上,地址为 项目源码地址 ,欢迎大家star。