CompletableFuture

文章目录

CompletableFuture

CompletableFuture实现了CompletionStage接口 和 Future接口,前者是对后者的一个扩展,增加了异步回调、流式处理、多个Future组合处理的能力,使Java在处理多任务的协同工作时更加顺畅便利。

一、创建异步任务

1、ExecutorService.submit

ExecutorServicesubmit会返回Future对象,通过Futureget方法获取返回值。

如果有异常,submit 会存下异常,只有当调用Futureget方法才会将任务执行时的异常重新抛出。

java 复制代码
@Slf4j
public class ExecutorServiceSubmitTest {

    private static final boolean errorFlag = false;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<Integer> future = executorService.submit(() -> {
            String currentThreadName = Thread.currentThread().getName();
            log.info(currentThreadName + ":start");

            if (errorFlag) {
                log.warn(currentThreadName + ":error");
                throw new RuntimeException();
            }

            log.info(currentThreadName + ":finish");
            return 666;
        });

        TimeUnit.SECONDS.sleep(2);
        log.info("future result:");

        // 等待子任务执行完成。
        // 如果已完成则直接返回结果;如果执行任务异常,则get方法会把之前捕获的异常重新抛出
        log.info("future result:" + future.get());

        executorService.shutdown();
    }

}

errorFlag为false的执行结果:2s后调用get获取结果

java 复制代码
23:09:11.086 [pool-1-thread-1] INFO com.test.ExecutorServiceSubmitTest - pool-1-thread-1:start
23:09:11.087 [pool-1-thread-1] INFO com.test.ExecutorServiceSubmitTest - pool-1-thread-1:finish
23:09:13.088 [main] INFO com.test.ExecutorServiceSubmitTest - future result:
666

errorFlag为false的执行结果:2s后调用get才将异常抛出

java 复制代码
23:09:54.075 [pool-1-thread-1] INFO com.test.ExecutorServiceSubmitTest - pool-1-thread-1:start
23:09:54.078 [pool-1-thread-1] WARN com.test.ExecutorServiceSubmitTest - pool-1-thread-1:error
23:09:56.079 [main] INFO com.test.ExecutorServiceSubmitTest - future result:
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException
	at java.util.concurrent.FutureTask.report(FutureTask.java:122)
	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
	at com.test.ExecutorServiceSubmitTest.main(ExecutorServiceSubmitTest.java:35)
    ...

2、supplyAsync / runAsync

这两方法的效果跟submit是一样的

java 复制代码
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    // 创建「无返回值」的异步任务,相当于`ExecutorService submit(Runnable task)`方法
    public static CompletableFuture<Void> runAsync(Runnable runnable) {...}
    
    // 创建「有返回值」的异步任务,相当于`ExecutorService submit(Runnable task)`方法
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {...}
}

这两方法各有一个重载版本,可以指定执行异步任务的Executor实现

java 复制代码
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    // 创建「无返回值」的异步任务,相当于`ExecutorService submit(Runnable task)`方法
    public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) {...}
    
    // 创建「有返回值」的异步任务,相当于`ExecutorService submit(Runnable task)`方法
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) {...}
}
  • 如果不指定,默认使用ForkJoinPool.commonPool()
  • 如果机器是单核的,则默认使用ThreadPerTaskExecutor,该类是一个内部类,每次执行execute都会创建一个新线程。

【代码示例 - RunAsync】

java 复制代码
@Slf4j
public class RunAsyncDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> runAsyncFuture1 = CompletableFuture.runAsync(() -> {
            log.info("do something");
        });

        CompletableFuture<Void> runAsyncFuture2 = CompletableFuture.runAsync(() -> {
            log.info("do something");
        }, Executors.newSingleThreadExecutor());

        CompletableFuture<Void> runAsyncFuture3 = CompletableFuture.runAsync(() -> {
            log.info("do something");
            throw new RuntimeException();
        });

        TimeUnit.SECONDS.sleep(2);
        log.info("future result:");
        runAsyncFuture3.get();  // 这里必须要get才会抛出异常
    }

}
java 复制代码
23:17:57.783 [ForkJoinPool.commonPool-worker-2] INFO com.test.RunAsyncDemo - do something
23:17:57.783 [pool-1-thread-1] INFO com.test.RunAsyncDemo - do something
23:17:57.783 [ForkJoinPool.commonPool-worker-1] INFO com.test.RunAsyncDemo - do something
23:17:59.786 [main] INFO com.test.RunAsyncDemo - future result:
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException
	at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1908)
	at com.test.RunAsyncDemo.main(RunAsyncDemo.java:28)
    ...

【代码示例 - SupplyAsync】

java 复制代码
@Slf4j
public class SupplyAsyncDemo {

    private static final boolean errorFlag = true;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> supplyAsyncFuture1 = CompletableFuture.supplyAsync(() -> {
            log.info("do something");
            return "result";
        });
        log.info(supplyAsyncFuture1.get());

        CompletableFuture<String> supplyAsyncFuture2 = CompletableFuture.supplyAsync(() -> {
            log.info("do something");
            return "result";
        }, Executors.newSingleThreadExecutor());
        log.info(supplyAsyncFuture2.get());

        CompletableFuture<String> supplyAsyncFuture3 = CompletableFuture.supplyAsync(() -> {
            log.info("do something");
            if (errorFlag) throw new RuntimeException();
            return "result";
        });
        TimeUnit.SECONDS.sleep(2);
        log.info("future result:");
        log.info(supplyAsyncFuture3.get()); // 这里必须要get才会抛出异常

    }

}
java 复制代码
23:14:35.583 [ForkJoinPool.commonPool-worker-1] INFO com.test.SupplyAsyncDemo - do something
23:14:35.585 [main] INFO com.test.SupplyAsyncDemo - result
23:14:35.586 [pool-1-thread-1] INFO com.test.SupplyAsyncDemo - do something
23:14:35.587 [main] INFO com.test.SupplyAsyncDemo - result
23:14:35.587 [ForkJoinPool.commonPool-worker-1] INFO com.test.SupplyAsyncDemo - do something
23:14:37.592 [main] INFO com.test.SupplyAsyncDemo - future result:
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException
	at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1908)
	at com.test.SupplyAsyncDemo.main(SupplyAsyncDemo.java:34)

二、异步回调

下述的多个方法,每个方法都有两个以Async结尾的方法,一个使用默认的Executor实现,一个使用指定的Executor实现

  • 不以Async结尾的方法:由触发该任务的线程执行该任务。
  • 以Async结尾的方法:由触发该任务的线程将任务提交到线程池,执行任务的线程跟触发任务的线程不一定是同一个。
    • 没有指定 Executor:默认使用ForkJoinPool.commonPool()
    • 指定了 Executor:使用指定的 Executor

1、thenApply / thenApplyAsync

接收上一个任务的返回值作为Function函数的参数,有返回值

java 复制代码
// java.util.concurren.CompletableFuture

// 由触发该任务的线程执行该任务
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {
    return uniApplyStage(null, fn);
}

// 默认使用ForkJoinPool.commonPool()
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) {
    return uniApplyStage(asyncPool, fn);
}

// 可以指定线程池
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) {
    return uniApplyStage(screenExecutor(executor), fn);
}

1)案例1

thenApply案例。都不指定 Executors

java 复制代码
@Slf4j
public class ThenApplyDemo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = cf1.thenApply((result) -> {
            log.info("cf2 do something....");
            result += 2;
            return result;
        });

        log.info("cf1结果:" + cf1.get()); // 1
        log.info("cf2结果:" + cf2.get()); // 3
    }
}
java 复制代码
23:30:47.444 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenApplyDemo - cf1 do something....
// 沿用触发该任务的线程
23:30:47.445 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenApplyDemo - cf2 do something....
23:30:47.445 [main] INFO com.test.ThenApplyDemo - cf1结果:1
23:30:47.445 [main] INFO com.test.ThenApplyDemo - cf2结果:3

2)案例2

thenApply案例。调用任务指定 Executors,回调任务不指定 Executors

java 复制代码
@Slf4j
public class ThenApplyDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        }, Executors.newFixedThreadPool(3));

        CompletableFuture<Integer> cf2 = cf1.thenApply((result) -> {
            log.info("cf2 do something....");
            result += 2;
            return result;
        });

        log.info("cf1结果:" + cf1.get()); // 1
        log.info("cf2结果:" + cf2.get()); // 3
    }
}
java 复制代码
23:31:30.142 [pool-1-thread-1] INFO com.test.ThenApplyDemo - cf1 do something....
// 沿用触发该任务的线程
23:31:30.144 [pool-1-thread-1] INFO com.test.ThenApplyDemo - cf2 do something....
23:31:30.144 [main] INFO com.test.ThenApplyDemo - cf1结果:1
23:31:30.144 [main] INFO com.test.ThenApplyDemo - cf2结果:3

3)案例3

thenApplyAsync案例。调用任务指定 Executors,回调任务不指定 Executors

java 复制代码
@Slf4j
public class ThenApplyDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        }, Executors.newFixedThreadPool(3));

        CompletableFuture<Integer> cf2 = cf1.thenApplyAsync((result) -> {
            log.info("cf2 do something....");
            result += 2;
            return result;
        });

        log.info("cf1结果:" + cf1.get()); // 1
        log.info("cf2结果:" + cf2.get()); // 3
    }
}
java 复制代码
23:32:30.244 [pool-1-thread-1] INFO com.test.ThenApplyDemo - cf1 do something....
// 默认使用`ForkJoinPool.commonPool()`
23:32:30.246 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenApplyDemo - cf2 do something....
23:32:30.246 [main] INFO com.test.ThenApplyDemo - cf1结果:1
23:32:30.246 [main] INFO com.test.ThenApplyDemo - cf2结果:3

4)案例4

thenApplyAsync案例。都指定 Executors

java 复制代码
@Slf4j
public class ThenApplyDemo4 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        }, Executors.newFixedThreadPool(3));

        CompletableFuture<Integer> cf2 = cf1.thenApplyAsync((result) -> {
            log.info("cf2 do something....");
            result += 2;
            return result;
        }, Executors.newFixedThreadPool(3));

        log.info("cf1结果:" + cf1.get()); // 1
        log.info("cf2结果:" + cf2.get()); // 3
    }
}
java 复制代码
23:33:01.813 [pool-1-thread-1] INFO com.test.ThenApplyDemo - cf1 do something....
// 使用指定的Executor
23:33:01.814 [pool-2-thread-1] INFO com.test.ThenApplyDemo - cf2 do something....
23:33:01.814 [main] INFO com.test.ThenApplyDemo - cf1结果:1
23:33:01.815 [main] INFO com.test.ThenApplyDemo - cf2结果:3

2、thenAccept / thenAcceptAsync

接收上一个任务的返回值作为Consumer函数的参数,无返回值

java 复制代码
// java.util.concurren.CompletableFuture

// 由触发该任务的线程执行该任务
public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
    return uniAcceptStage(null, action);
}

// 默认使用ForkJoinPool.commonPool()
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
    return uniAcceptStage(asyncPool, action);
}

// 可以指定线程池
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor) {
    return uniAcceptStage(screenExecutor(executor), action);
}

案例

java 复制代码
@Slf4j
public class ThenAcceptDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Void> cf2 = cf1.thenAccept((result) -> {
            log.info("cf1 result is " + result);
        });
        
        TimeUnit.SECONDS.sleep(1);
    }
}
java 复制代码
23:58:05.771 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenAcceptDemo - cf1 do something....
23:58:05.773 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenAcceptDemo - cf1 result is 1

3、thenRun / thenRunAsync

当某个任务执行完成后执行的回调方法,无返回值

java 复制代码
// java.util.concurren.CompletableFuture

// 由触发该任务的线程执行该任务
public CompletableFuture<Void> thenRun(Runnable action) {
    return uniRunStage(null, action);
}

// 默认使用ForkJoinPool.commonPool()
public CompletableFuture<Void> thenRunAsync(Runnable action) {
    return uniRunStage(asyncPool, action);
}

// 可以指定线程池
public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor) {
    return uniRunStage(screenExecutor(executor), action);
}

案例

java 复制代码
@Slf4j
public class ThenRunDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return 1;
        });

        CompletableFuture<Void> cf2 = cf1.thenRun(() -> {
            log.info("cf2 do something....");	// 等cf1执行完执行
        });

        TimeUnit.SECONDS.sleep(2);
    }

}
java 复制代码
23:59:01.862 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenRunDemo - cf1 do something....
23:59:02.869 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenRunDemo - cf2 do something....

4、whenComplete / whenCompleteAsync

whenComplete是当某个任务执行完成后执行的回调方法,会将 执行结果执行期间抛出的异常 传递给回调方法

  • 调用任务 正常执行,对回调任务来说:
    • 结果参数:调用任务的结果
    • 异常参数:null
    • get方法:返回调用任务的结果
  • 调用任务 异常执行,对回调任务来说:
    • 结果参数:null
    • 异常参数:调用任务抛出的异常
    • get方法:抛出调用任务抛出的异常
java 复制代码
// java.util.concurren.CompletableFuture

// 由触发该任务的线程执行该任务
public CompletableFuture<T> whenComplete(
    BiConsumer<? super T, ? super Throwable> action) {
    return uniWhenCompleteStage(null, action);
}

// 默认使用ForkJoinPool.commonPool()
public CompletableFuture<T> whenCompleteAsync(
    BiConsumer<? super T, ? super Throwable> action) {
    return uniWhenCompleteStage(asyncPool, action);
}

// 可以指定线程池
public CompletableFuture<T> whenCompleteAsync(
    BiConsumer<? super T, ? super Throwable> action, Executor executor) {
    return uniWhenCompleteStage(screenExecutor(executor), action);
}

1)正常执行

java 复制代码
@Slf4j
public class WhenCompleteDemo1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // cf1正常执行
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = cf1.whenComplete((result, ex) -> {
            log.info("cf1结果:" + result);
            log.info("cf1异常:" + ex);
            log.info("cf2 do something....");
        });

        // 返回cf1的结果
        log.info("cf2结果:" + cf2.get());
    }

}
java 复制代码
19:35:41.890 [ForkJoinPool.commonPool-worker-1] INFO com.test.WhenCompleteDemo1 - cf1 do something....
19:35:41.891 [ForkJoinPool.commonPool-worker-1] INFO com.test.WhenCompleteDemo1 - cf1结果:1
19:35:41.891 [ForkJoinPool.commonPool-worker-1] INFO com.test.WhenCompleteDemo1 - cf1异常:null
19:35:41.891 [ForkJoinPool.commonPool-worker-1] INFO com.test.WhenCompleteDemo1 - cf2 do something....
19:35:41.891 [main] INFO com.test.WhenCompleteDemo1 - cf2结果:1

2)异常执行

java 复制代码
@Slf4j
public class WhenCompleteDemo2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // cf1异常执行
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            int a = 1/0;
            return 1;
        });

        CompletableFuture<Integer> cf2 = cf1.whenComplete((result, ex) -> {
            log.info("cf1结果:" + result);
            log.info("cf1异常:" + ex);
            log.info("cf2 do something....");
        });

        // 抛出cf1抛出的异常
        log.info("cf2结果:" + cf2.get());
    }

}
java 复制代码
19:38:46.366 [ForkJoinPool.commonPool-worker-1] INFO com.test.WhenCompleteDemo2 - cf1 do something....
19:38:46.368 [ForkJoinPool.commonPool-worker-1] INFO com.test.WhenCompleteDemo2 - cf1结果:null
19:38:46.368 [ForkJoinPool.commonPool-worker-1] INFO com.test.WhenCompleteDemo2 - cf1异常:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
19:38:46.368 [ForkJoinPool.commonPool-worker-1] INFO com.test.WhenCompleteDemo2 - cf2 do something....
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
	at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1908)
	at com.test.WhenCompleteDemo2.main(WhenCompleteDemo2.java:24)
    ...

5、handle / handleAsync

handle是当某个任务执行完成后执行的回调方法,会将 执行结果执行期间抛出的异常 传递给回调方法

  • 调用任务 正常执行,对回调任务来说:
    • 结果参数:调用任务的结果
    • 异常参数:null
    • get方法:返回handle回调的结果
  • 调用任务 异常执行,对回调任务来说:
    • 结果参数:null
    • 异常参数:调用任务抛出的异常
    • get方法:不会抛出调用任务的异常
java 复制代码
// 由触发该任务的线程执行该任务
public <U> CompletableFuture<U> handle(
    BiFunction<? super T, Throwable, ? extends U> fn) {
    return uniHandleStage(null, fn);
}

// 默认使用ForkJoinPool.commonPool()
public <U> CompletableFuture<U> handleAsync(
    BiFunction<? super T, Throwable, ? extends U> fn) {
    return uniHandleStage(asyncPool, fn);
}

// 可以指定线程池
public <U> CompletableFuture<U> handleAsync(
    BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) {
    return uniHandleStage(screenExecutor(executor), fn);
}

1)正常执行

java 复制代码
@Slf4j
public class HandleDemo1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // cf1正常执行
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = cf1.handle((result, ex) -> {
            log.info("cf1结果:" + result);
            log.info("cf1异常:" + ex);
            log.info("cf2 do something....");
            return 3;
        });

        // 返回cf2的结果
        log.info("cf2结果:" + cf2.get());
    }

}
java 复制代码
19:59:40.994 [ForkJoinPool.commonPool-worker-1] INFO com.test.HandleDemo1 - cf1 do something....
19:59:40.996 [ForkJoinPool.commonPool-worker-1] INFO com.test.HandleDemo1 - cf1结果:1
19:59:40.996 [ForkJoinPool.commonPool-worker-1] INFO com.test.HandleDemo1 - cf1异常:null
19:59:40.996 [ForkJoinPool.commonPool-worker-1] INFO com.test.HandleDemo1 - cf2 do something....
19:59:40.996 [main] INFO com.test.HandleDemo1 - cf2结果:3

2)异常执行

java 复制代码
@Slf4j
public class HandleDemo2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // cf1异常执行
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            int a = 1/0;
            return 1;
        });

        CompletableFuture<Integer> cf2 = cf1.handle((result, ex) -> {
            log.info("cf1结果:" + result);
            log.info("cf1异常:" + ex);
            log.info("cf2 do something....");
            return 3;
        });

        // 返回cf2的结果
        log.info("cf2结果:" + cf2.get());
    }

}
java 复制代码
20:00:20.659 [ForkJoinPool.commonPool-worker-1] INFO com.test.HandleDemo2 - cf1 do something....
20:00:20.660 [ForkJoinPool.commonPool-worker-1] INFO com.test.HandleDemo2 - cf1结果:null
20:00:20.660 [ForkJoinPool.commonPool-worker-1] INFO com.test.HandleDemo2 - cf1异常:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
20:00:20.660 [ForkJoinPool.commonPool-worker-1] INFO com.test.HandleDemo2 - cf2 do something....
20:00:20.660 [main] INFO com.test.HandleDemo2 - cf2结果:3

6、exceptionally

将调用任务中 抛出的异常作为参数 传递到回调方法中。

  • 调用任务正常执行:直接返回调用任务的执行结果,不执行exceptionally回调方法内部代码
  • 调用任务异常执行:将异常作为参数传递给exceptionally回调方法,并执行exceptionally回调方法内部代码
java 复制代码
public CompletableFuture<T> exceptionally(
    Function<Throwable, ? extends T> fn) {
    return uniExceptionallyStage(fn);
}

1)正常执行

java 复制代码
@Slf4j
public class ExceptionallyDemo1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // cf1正常执行
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        // 内部不执行
        CompletableFuture<Integer> cf2 = cf1.exceptionally((ex) -> {
            log.info("cf1异常:" + ex);
            log.info("cf2 do something....");
            return 3;
        });

        // 返回cf1的结果
        log.info("cf2结果:" + cf2.get());
    }

}
java 复制代码
20:14:55.423 [ForkJoinPool.commonPool-worker-1] INFO com.test.ExceptionallyDemo1 - cf1 do something....
20:14:55.424 [main] INFO com.test.ExceptionallyDemo1 - cf2结果:1

2)异常执行

java 复制代码
@Slf4j
public class ExceptionallyDemo2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // cf1异常执行
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            int a = 1/0;
            return 1;
        });

        // 内部执行
        CompletableFuture<Integer> cf2 = cf1.exceptionally((ex) -> {
            log.info("cf1异常:" + ex);
            log.info("cf2 do something....");
            return 3;
        });

        // 返回cf2的结果
        log.info("cf2结果:" + cf2.get());
    }

}
java 复制代码
20:16:51.299 [ForkJoinPool.commonPool-worker-1] INFO com.test.ExceptionallyDemo2 - cf1 do something....
20:16:51.300 [ForkJoinPool.commonPool-worker-1] INFO com.test.ExceptionallyDemo2 - cf1异常:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
20:16:51.300 [ForkJoinPool.commonPool-worker-1] INFO com.test.ExceptionallyDemo2 - cf2 do something....
20:16:51.300 [main] INFO com.test.ExceptionallyDemo2 - cf2结果:3

三、组合处理

1、thenCombine / thenAcceptBoth / runAfterBoth

这三个方法都是将两个CompletableFuture组合起来处理,只有两个任务都正常完成时,才进行下阶段任务。

  • thenCombine:将两个任务的执行结果 作为函数入参,有返回值
  • thenAcceptBoth:将两个任务的执行结果 作为函数入参,无返回值
  • runAfterBoth没有入参 ,也没有返回值

注意:两个任务中只要有一个执行异常,则调用get方法就会抛出该异常。

1)thenCombine 案例

java 复制代码
@Slf4j
public class ThenCombineDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return 2;
        });

        CompletableFuture<Integer> cf3 = cf1.thenCombine(cf2, (a, b) -> {
            // 这里会阻塞2s
            log.info("cf3 do something....");
            return a + b;
        });


        log.info("cf3结果:" + cf3.get());
    }

}
java 复制代码
20:21:47.216 [ForkJoinPool.commonPool-worker-2] INFO com.test.ThenCombineDemo - cf2 do something....
20:21:47.216 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenCombineDemo - cf1 do something....
// 阻塞了2s
20:21:49.223 [ForkJoinPool.commonPool-worker-2] INFO com.test.ThenCombineDemo - cf3 do something....
20:21:49.223 [main] INFO com.test.ThenCombineDemo1 - cf3结果:3

2)thenAcceptBoth 案例

java 复制代码
@Slf4j
public class ThenAcceptBothDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return 2;
        });

        CompletableFuture<Void> cf3 = cf1.thenAcceptBoth(cf2, (a, b) -> {
            log.info("cf3 do something....");
            log.info("a={}, b={}", a, b);
        });

        log.info("cf3结果:" + cf3.get());
    }

}
java 复制代码
20:27:21.025 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenAcceptBothDemo - cf1 do something....
20:27:21.025 [ForkJoinPool.commonPool-worker-2] INFO com.test.ThenAcceptBothDemo - cf2 do something....
// 阻塞了2s
20:27:23.030 [ForkJoinPool.commonPool-worker-2] INFO com.test.ThenAcceptBothDemo - cf3 do something....
20:27:23.030 [ForkJoinPool.commonPool-worker-2] INFO com.test.ThenAcceptBothDemo - a=1, b=2
20:27:23.033 [main] INFO com.test.ThenAcceptBothDemo - cf3结果:null

3)runAfterBoth 案例

java 复制代码
@Slf4j
public class RunAfterBothDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return 2;
        });

        CompletableFuture<Void> cf3 = cf1.runAfterBoth(cf2, () -> {
            log.info("cf3 do something....");
        });

        log.info("cf3结果:" + cf3.get());
    }

}
java 复制代码
20:29:17.640 [ForkJoinPool.commonPool-worker-1] INFO com.test.RunAfterBothDemo - cf1 do something....
20:29:17.640 [ForkJoinPool.commonPool-worker-2] INFO com.test.RunAfterBothDemo - cf2 do something....
// 阻塞了2s
20:29:19.649 [ForkJoinPool.commonPool-worker-2] INFO com.test.RunAfterBothDemo - cf3 do something....
20:29:19.650 [main] INFO com.test.RunAfterBothDemo - cf3结果:null

2、applyToEither / acceptEither / runAfterEither

这三个方法和上面一样也是将两个CompletableFuture组合起来处理,当有一个任务正常完成时,就会进行下阶段任务。

  • applyToEither:将先完成任务的执行结果 作为函数入参,有返回值
  • acceptEither:将先完成任务的执行结果 作为函数入参,无返回值
  • runAfterEither没有入参 ,也没有返回值

1)applyToEither 案例

java 复制代码
@Slf4j
public class ApplyToEitherDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return 2;
        });

        CompletableFuture<Integer> cf3 = cf1.applyToEither(cf2, (firstResult) -> {
            log.info("cf3 do something....");
            return firstResult;
        });

        log.info("cf3结果:" + cf3.get());
    }

}
java 复制代码
20:33:28.433 [ForkJoinPool.commonPool-worker-2] INFO com.test.ApplyToEitherDemo - cf2 do something....
20:33:28.433 [ForkJoinPool.commonPool-worker-1] INFO com.test.ApplyToEitherDemo - cf1 do something....
20:33:28.435 [ForkJoinPool.commonPool-worker-1] INFO com.test.ApplyToEitherDemo - cf3 do something....
20:33:28.435 [main] INFO com.test.ApplyToEitherDemo - cf3结果:1

2)acceptEither 案例

java 复制代码
@Slf4j
public class ThenAcceptEitherDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return 2;
        });

        CompletableFuture<Void> cf3 = cf1.acceptEither(cf2, (firstResult) -> {
            log.info("cf3 do something....");
            log.info("firstResult={}", firstResult);
        });

        log.info("cf3结果:" + cf3.get());
    }

}
java 复制代码
20:35:11.357 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenAcceptEitherDemo - cf1 do something....
20:35:11.359 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenAcceptEitherDemo - cf3 do something....
20:35:11.357 [ForkJoinPool.commonPool-worker-2] INFO com.test.ThenAcceptEitherDemo - cf2 do something....
20:35:11.359 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenAcceptEitherDemo - firstResult=1
20:35:11.360 [main] INFO com.test.ThenAcceptEitherDemo - cf3结果:null

3)runAfterEither 案例

java 复制代码
@Slf4j
public class RunAfterEitherDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return 1;
        });

        CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return 2;
        });

        CompletableFuture<Void> cf3 = cf1.runAfterEither(cf2, () -> {
            log.info("cf3 do something....");
        });

        log.info("cf3结果:" + cf3.get());
    }

}
java 复制代码
20:36:36.994 [ForkJoinPool.commonPool-worker-2] INFO com.test.RunAfterEitherDemo - cf2 do something....
20:36:36.994 [ForkJoinPool.commonPool-worker-1] INFO com.test.RunAfterEitherDemo - cf1 do something....
20:36:36.996 [ForkJoinPool.commonPool-worker-1] INFO com.test.RunAfterEitherDemo - cf3 do something....
20:36:36.996 [main] INFO com.test.RunAfterEitherDemo - cf3结果:null

3、allOf

allOf 是多个任务都执行完成后才会执行

  • 如果都是正常执行,则get返回null

  • 只要有一个任务执行异常,执行get方法时就会抛出异常(没有异常的任务会正常执行)

    但是要等所有方法执行完毕才抛出异常。

1)全部正常执行

java 复制代码
@Slf4j
public class AllOfDemo1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return "cf1 任务完成";
        });

        CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return "cf2 任务完成";
        });

        CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> {
            log.info("cf3 do something....");
            return "cf3 任务完成";
        });

        CompletableFuture<Void> cfAll = CompletableFuture.allOf(cf1, cf2, cf3);
        log.info("allOf结果:" + cfAll.get());
    }

}
java 复制代码
20:38:54.369 [ForkJoinPool.commonPool-worker-3] INFO com.test.AllOfDemo1 - cf3 do something....
20:38:54.369 [ForkJoinPool.commonPool-worker-1] INFO com.test.AllOfDemo1 - cf1 do something....
20:38:54.369 [ForkJoinPool.commonPool-worker-2] INFO com.test.AllOfDemo1 - cf2 do something....
// 阻塞2s
20:38:56.372 [main] INFO com.test.AllOfDemo1 - allOf结果:null

2)存在异常

java 复制代码
@Slf4j
public class AllOfDemo2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // cf1发生异常,并且异常比其余两个任务先发生
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            int a = 1 / 0;
            return "cf1 任务完成";
        });

        CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return "cf2 任务完成";
        });

        CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> {
            log.info("cf3 do something....");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            return "cf3 任务完成";
        });

        CompletableFuture<Void> cfAll = CompletableFuture.allOf(cf1, cf2, cf3);
        try {
            log.info("allOf结果:" + cfAll.get());
        } catch (Exception e) {
            log.error("allOf异常:", e);
        }
    }

}
java 复制代码
20:46:34.074 [ForkJoinPool.commonPool-worker-1] INFO com.test.AllOfDemo2 - cf1 do something....
// 没有异常的任务会正常执行
20:46:34.074 [ForkJoinPool.commonPool-worker-3] INFO com.test.AllOfDemo2 - cf3 do something....
20:46:34.074 [ForkJoinPool.commonPool-worker-2] INFO com.test.AllOfDemo2 - cf2 do something....
// 等所有任务执行完毕才抛出异常
20:46:37.086 [main] ERROR com.test.AllOfDemo2 - allOf异常:
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
	at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1908)
	at com.test.AllOfDemo2.main(AllOfDemo2.java:40)

4、anyOf

anyOf 是多个任务只要有一个任务执行完成就会执行

  • 如果都是正常执行,则get返回执行完成的任务的结果。(返回后不会再执行其余任务)
  • 如果没有任何任务完成 时发生异常,则执行get方法时就会抛出异常
  • 如果发生异常时已经有任务完成 ,则执行get方法时不会抛出异常

1)全部正常执行

java 复制代码
@Slf4j
public class AnyOfDemo1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            log.info("cf1 finish....");
            return "cf1 result";
        });

        CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            log.info("cf2 finish....");
            return "cf2 result";
        });

        CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> {
            log.info("cf3 do something....");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            log.info("cf3 finish....");
            return "cf3 result";
        });

        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(cf1, cf2, cf3);
        log.info("anyOf result:" + anyOf.get());
    }

}
java 复制代码
20:52:40.925 [ForkJoinPool.commonPool-worker-1] INFO com.test.AnyOfDemo1 - cf1 do something....
20:52:40.925 [ForkJoinPool.commonPool-worker-3] INFO com.test.AnyOfDemo1 - cf3 do something....
20:52:40.925 [ForkJoinPool.commonPool-worker-2] INFO com.test.AnyOfDemo1 - cf2 do something....
20:52:41.932 [ForkJoinPool.commonPool-worker-1] INFO com.test.AnyOfDemo1 - cf1 finish....
// cf1任务完成,cf2和cf3就不会执行了
20:52:41.933 [main] INFO com.test.AnyOfDemo1 - anyOf result:cf1 result

2)异常时,没有任务完成

java 复制代码
@Slf4j
public class AnyOfDemo2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            int a = 1/0;    // 异常(还没有任务完成)
            log.info("cf1 finish....");
            return "cf1 result";
        });

        CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            log.info("cf2 finish....");
            return "cf2 result";
        });

        CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> {
            log.info("cf3 do something....");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            log.info("cf3 finish....");
            return "cf3 result";
        });

        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(cf1, cf2, cf3);
        log.info("anyOf result:" + anyOf.get());
    }

}
java 复制代码
20:55:37.652 [ForkJoinPool.commonPool-worker-2] INFO com.test.AnyOfDemo2 - cf2 do something....
20:55:37.652 [ForkJoinPool.commonPool-worker-1] INFO com.test.AnyOfDemo2 - cf1 do something....
20:55:37.652 [ForkJoinPool.commonPool-worker-3] INFO com.test.AnyOfDemo2 - cf3 do something....
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
	at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1908)
	at com.test.AnyOfDemo2.main(AnyOfDemo2.java:46)
    ...

3)异常时,有任务完成

java 复制代码
@Slf4j
public class AnyOfDemo3 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            log.info("cf1 finish....");
            return "cf1 result";
        });

        CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
            log.info("cf2 do something....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            int a = 1/0;    // 异常(cf1任务已经完成)
            log.info("cf2 finish....");
            return "cf2 result";
        });

        CompletableFuture<String> cf3 = CompletableFuture.supplyAsync(() -> {
            log.info("cf3 do something....");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // Todo Handle Exception
            }
            log.info("cf3 finish....");
            return "cf3 result";
        });

        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(cf1, cf2, cf3);
        log.info("anyOf result:" + anyOf.get());
    }

}
java 复制代码
20:56:24.220 [ForkJoinPool.commonPool-worker-3] INFO com.test.AnyOfDemo3 - cf3 do something....
20:56:24.220 [ForkJoinPool.commonPool-worker-2] INFO com.test.AnyOfDemo3 - cf2 do something....
20:56:24.220 [ForkJoinPool.commonPool-worker-1] INFO com.test.AnyOfDemo3 - cf1 do something....
20:56:25.229 [ForkJoinPool.commonPool-worker-1] INFO com.test.AnyOfDemo3 - cf1 finish....
20:56:25.230 [main] INFO com.test.AnyOfDemo3 - anyOf result:cf1 result

cf2发生异常时,cf1已经完成任务,所以异常不会抛出

5、thenCompose

thenCompose方法会在某个任务执行完成后,将该任务的执行结果作为方法入参然后执行指定的方法

案例

java 复制代码
@Slf4j
public class ThenComposeDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
            log.info("cf1 do something....");
            return "cf1 result";
        });

        CompletableFuture<String> cf2 = cf1.thenCompose((result) -> {
            log.info("cf2 do something....");
            return CompletableFuture.supplyAsync(() -> result);
        });

        log.info("cf1结果:" + cf1.get());
        log.info("cf2结果:" + cf2.get());
    }

}
java 复制代码
21:01:04.228 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenComposeDemo - cf1 do something....
21:01:04.229 [ForkJoinPool.commonPool-worker-1] INFO com.test.ThenComposeDemo - cf2 do something....
21:01:04.229 [main] INFO com.test.ThenComposeDemo - cf1结果:cf1 result
21:01:04.230 [main] INFO com.test.ThenComposeDemo - cf2结果:cf1 result

6、thenCompose 对比 thenAccept

java 复制代码
// java.util.concurren.CompletableFuture

public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {
    return uniApplyStage(null, fn);
}

public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(null, fn);
}

可以看到,两个方法的返回值都是CompletionStage<U>,不同之处在于它们的传入参数fn.

  • 对于thenApplyfn函数是一个对一个已完成的stage 或 CompletableFuture的返回值进行计算、操作;
  • 对于thenComposefn函数是对另一个CompletableFuture进行计算、操作。

案例

java 复制代码
public class CompareTest {
    public static void main(String[] args) {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> 100)
            .thenApply(num -> num + " to String");

        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> 100)
            .thenCompose(num -> CompletableFuture.supplyAsync(() -> num + " to String"));

        System.out.println(f1.join()); // 100 to String
        System.out.println(f2.join()); // 100 to String
    }
}

例子中,thenApplythenCompose都是将一个CompletableFuture<Integer>转换为CompletableFuture<String>

  • 不同的是,thenApply中的传入函数的返回值是String
  • thenCompose的传入函数的返回值是CompletableFuture<String>

有点像stream中的mapflatMap

回想我们做过的二维数组转一维数组,使用stream().flatMap映射时,我们是把流中的每个数据(数组)又展开为了流。

相关推荐
hanbarger10 分钟前
mybatis框架——缓存,分页
java·spring·mybatis
cdut_suye17 分钟前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
苹果醋329 分钟前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx
小蜗牛慢慢爬行31 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
azhou的代码园34 分钟前
基于JAVA+SpringBoot+Vue的制造装备物联及生产管理ERP系统
java·spring boot·制造
wm10431 小时前
java web springboot
java·spring boot·后端
smile-yan1 小时前
Provides transitive vulnerable dependency maven 提示依赖存在漏洞问题的解决方法
java·maven
老马啸西风1 小时前
NLP 中文拼写检测纠正论文-01-介绍了SIGHAN 2015 包括任务描述,数据准备, 绩效指标和评估结果
java
Earnest~1 小时前
Maven极简安装&配置-241223
java·maven
皮蛋很白1 小时前
Maven 环境变量 MAVEN_HOME 和 M2_HOME 区别以及 IDEA 修改 Maven repository 路径全局
java·maven·intellij-idea