Future的使用和理解
最近遇到了一个场景,系统需要调另外一个接口来获取配置文件的内容,但是接口没有返回值,这种情况下,这样的接口就不能直接使用了。首先想到的是在接口实现中回调自身系统的接口,把配置文件传过来,我把过程简化成了一个流程图。
1. 自身系统生成一个唯一单号,在数据库中保存一行,单号所在行的配置文件此时还是空
2. 自身系统调用Jenkins,Jenkins执行脚本,
3. Jenkins去生产服务器上拉取配置文件,之后通过自身系统中的一个回调接口将配置文件内容传回到自身系统,保存在对应单号所在行;此时还有另外一件事在做,自身系统需要在超时时间为20s的情况下轮询单号对应的配置文件内容,如果内容已经被填充,那么立即返回结果。
这里,Jenkins执行获取配置文件的内容是一个耗时任务,只有配置文件通过回调接口调用成功后,任务才算是完成。所以自身系统不得不做轮询。
一开始,我们代码可以这样写在一个线程中。
public CommonResponse getNewestConfigFile(ApplyOrderRequest request) { BuildWithDetails details = jenkinsClient.build(Jobs.JOB_GET_CONFIG_FILE, params); if (details == null || !details.getResult().name().equals("SUCCESS")) { return aCommonResponse().withMsg("获取最新配置文件失败") .withCode(ResponseEnum.FAILURE.ordinal()).build(); } // call jenkins job to get file and callback LocalDateTime start = LocalDateTime.now(); String content = null; // 如果配置文件为空时候,不要返回空字符串,返回一个带有注释的语句,比如 # ignore this line do { // 测试设置20s 超时 if (start.plusSeconds(20).isBefore(LocalDateTime.now())) { break; }
// 数据库配置文件字段被填充,立即返回 content = fileService.getFile(applyOrderTmpId); } while (StringUtils.isEmpty(content)); return content; }
这里的写法是同步的,如果调用Jenkins出错,会直接返回获取配置文件失败的状态;如果该步没有出错,那么需要等待耗时任务完成,若Jenkins任务只在返回配置文件内容后没有其他事情,那么这种方式可行,若任务总耗时10s,第2s会有10%的概率报错,不报错第5s就会回调接口,并且后5s需要做剩余的事情。那么这种写法就不合适,因为调用Jenkins的接口是阻塞的,需要等待job任务执行完后才能返回BuildWithJob对象,才可以进入后面的轮询,这种是不合适的。
于是,代码加入future对象来处理。代码如下
public CommonResponse getNewestConfigFile(ApplyOrderRequest request) { ExecutorService executor = Executors.newFixedThreadPool(1); Future<CommonResponse> future = executor.submit(() -> { BuildWithDetails details = null; try { details = jenkinsClient.build(Jobs.JOB_GET_CONFIG_FILE, params); } catch (Exception e) { log.info("Getting config file occurs exception", e); } finally { if (details == null || !details.getResult().name().equals("SUCCESS")) { return aCommonResponse().withMsg("获取最新配置文件失败") .withCode(ResponseEnum.FAILURE.ordinal()).build(); } } return aCommonResponse().withMsg("获取最新配置文件成功") .withCode(ResponseEnum.SUCCESS.ordinal()).build(); }); // call jenkins job to get file and callback LocalDateTime start = LocalDateTime.now(); String content = null; // 如果配置文件为空时候,不要返回空字符串,返回一个带有注释的语句,比如 # ignore this line do { // 测试设置20s 超时 if (start.plusSeconds(20).isBefore(LocalDateTime.now())) { break; } try { System.out.println("loop to get file"); // todo temporal future.get() is sucked here which should not. // todo find one method to get error result from sub thread and not block simultaneously if (future.get().getCode() != 0) { return future.get(); } } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } content = fileService.getFile(applyOrderTmpId); } while (StringUtils.isEmpty(content));
return content;
}
这种写法将调用Jenkins阻塞的方法交给两外一个线程来执行。这也是解决阻塞接口调用的一个比较有用的解决方案。但是由于Jenkins任务有10%的概率会失败,所以需要从另外的线程中及时获取失败的状态,返回给前端,所以就使用了Callable<V>接口。这样可以在loop中通过future.get()来拿到Jenkins任务执行结果。因为future.get() 方法也是阻塞的,导致引入了新的callable解决方案,整个线程在获取配置文件之前都要阻塞一会,所以需要在future.get()这里加入一个与操作的判
if (future.get().getCode() != 0 && !StringUti.isEmptyfileService.getFile(applyOrderTmpId))
前者用来解决Jenkins任务出错能够及时受到错误信息,后者可以在配置文件返回时候立即获取,这样做减少了等待时间。
.