一个简单的并行加载对象属性的工具类

60 阅读2分钟

Java 一个简单的并行加载对象属性的工具类

​ 对于一些属性比较多的 DTO 或者 VO 特别是有些属性要请求其他服务获取的时候,如果是串行加载的话,一般都会比较耗时。当然也可以选择并行的方式实现,示例代码如下:

/*
* @auth JerryZeng
*/
class ReimburseDTO {
    /*
    * 报销人 id
    */
    String reimEmployeeId;
    /*
    * 成本中心 id
    */
    String costCenterId;
    /*
    * 报销费用 id
    */
    String expense;
}

/*
* @auth JerryZeng
*/
class ReimburseVO {
    /*
    * 报销人
    */
    Employee reimEmployee;
    /*
    * 成本中心
    */
    Department costCenter;
    /*
    * 报销费用
    */
    Expnese expense;
    
    // getter, setter 省略
}

ReimburseVO convert(ReimburseDTO dto) {
    ReimburseVO res = new ReimburseVO();
    
    List<CompletableFuture> futureList = new ArrayList<>();
    futureList.add(CompletableFuture.runAsync(() -> {
        res.setReimEmployee(employeeService.getById(dto.getReimEmployeeId()));
    }));
    
   	futureList.add(CompletableFuture.runAsync(() -> {
        res.setCostCenter(departmentService.getById(dto.getCostCenterId()));
    }));
    
    futureList.add(CompletableFuture.runAsync(() -> {
        res.setExpense(expenseService.getById(dto.getExpenseId()));
    }));
    
    CompletableFuture all = CompletableFuture.allOf(futureList.toArray(futureList.toArray(new CompletableFuture[0])));
    
    try {
        all.get(1, TimeUnit.SECONDS);
    } catch (Execption e) {
        // 异常处理
    }
    
    // 其他业务处理
    
    return res;
}

​ 在日常使用 CompletableFuture 需要注意的是,它所使用的线程池默认是 ForkJoinPool.commonPool() ,核心线程数为操作系统的核心数减1。它其实适用的就是计算密集型的操作,但是我们日常使用中大部分的都是业务操作,都是 IO 密集型的。所以要我们传入一个事先定义好的线程池作为参数,使用如下的方法(JDK21 CompletableFuture#Line2031):

    /**
     * Returns a new CompletableFuture that is asynchronously completed
     * by a task running in the given executor after it runs the given
     * action.
     *
     * @param runnable the action to run before completing the
     * returned CompletableFuture
     * @param executor the executor to use for asynchronous execution
     * @return the new CompletableFuture
     */
    public static CompletableFuture<Void> runAsync(Runnable runnable,
                                                   Executor executor) {
        return asyncRunStage(screenExecutor(executor), runnable);
    }

​ 这样的话使用 CompletableFuture.runAsync 就会变得更臃肿且麻烦。如果我们要在多个地方进行这种对象的加载的话,很多这种重复的代码就不优雅了,所以我们可以尝试将重复的代码封装起来,让我们更多的精力放在业务代码的编辑上,封装的代码如下:

public class DataFetcher<D> {

    private final D data;

    private final List<Node<?>> nodes;

    private static final Executor DEFAULT_EXECUTOR;

    private static final int DEFAULT_TIME_WAIT_SECONDS = 10;

    public DataFetcher(D data) {
        this.data = data;
        this.nodes = new ArrayList<>();
    }

    public void addNode(Node<?> node) {
        Objects.requireNonNull(node);
        this.nodes.add(node);
    }

    public <P> void addNode(Supplier<P> fetchFunction, BiConsumer<D, P> setter) {
        this.nodes.add(new Node<>(fetchFunction, setter));
    }

    public void fetch() {
        fetch(DEFAULT_TIME_WAIT_SECONDS, DEFAULT_EXECUTOR);
    }

    public void fetch(int timeWaitSeconds) {
        fetch(timeWaitSeconds, DEFAULT_EXECUTOR);
    }

    public void fetch(int timeWaitSeconds, Executor executor) {
        List<CompletableFuture<?>> futures = nodes.stream()
                .map(node -> CompletableFuture.runAsync(node::fetch, executor))
                .collect(Collectors.toList());

        try {
            CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).get(timeWaitSeconds, TimeUnit.SECONDS);
            nodes.forEach(Node::propSetter);
        } catch (Exception e) {
            throw new RuntimeException("Failed to fetch, causes " + e.getMessage(), e);
        }
    }

    public class Node<Prop> {

        private Prop p;

        private final Supplier<Prop> fetchFunction;

        private final BiConsumer<D, Prop> setter;

        public Node(Supplier<Prop> fetchFunction, BiConsumer<D, Prop> setter) {
            this.fetchFunction = fetchFunction;
            this.setter = setter;
        }

        void fetch() {
            p = this.fetchFunction.get();
        }

        void propSetter() {
            this.setter.accept(data, p);
        }
    }

    static {
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        DEFAULT_EXECUTOR = new ThreadPoolExecutor(
                availableProcessors * 2 + 1,
                availableProcessors * 3,
                30,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2048),
                new DefaultThreadFactory());
    }


    private static class DefaultThreadFactory implements ThreadFactory {
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            namePrefix = "dataFetcher-thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
}

​ 使用这个工具类我们就可以把上诉的代码改为:

ReimburseVO convert(ReimburseDTO dto) {
    ReimburseVO res = new ReimburseVO();
    
    DataFetcher<ReimburseVO> fetcher = new DataFetcher<>();
    
    fetcher.addNode(() -> employeeService.getById(dto.getReimEmployeeId()), res::setReimEmployee);
    fetcher.addNode(() -> departmentService.getById(dto.getCostCenterId()), res::setCostCenter);
    fetcher.addNode(() -> expenseService.getById(dto.getExpenseId()), res::setExpense);
    fetcher.featch();
    
    // 其他业务处理
    
    return res;
}

​ 这样的抽象虽然难度不高,但是从日常编码的重复点出发,有意识的训练自己的抽象能力,也算是一种锻炼了。工具类代码地址:github.com/BluebuleZJR…