跳至主要內容

apzs...大约 64 分钟

5.6、商城业务-异步

5.6.1、初始化线程的4种方式

初始化线程的4种方式

  1. 继承 Thread
  2. 实现 Runnable 接口
  3. 实现 Callable 接口 + FutureTask (可以拿到返回结果,可以处理异常)
  4. 线程池

gulimall-search模块的com.atguigu.gulimall.search包下新建thread文件夹,在thread文件夹里新建ThreadTest类(线程的测试不适合使用测试方法来测试,只适合在main方法里测试)

1、继承Thread

运行以下测试代码:

public static void main(String[] args) {
    System.out.println("==========main start==========");

    Thread01 thread01 = new Thread01();
    //启动线程
    thread01.start();

    System.out.println("==========main end============");
}

public static class Thread01 extends Thread{
    @SneakyThrows
    @Override
    public void run() {
        System.out.println("当前线程:"+Thread.currentThread().getId());
        Thread.sleep(2000);
        int i = 10 /2 ;
        System.out.println("运行结果:" + i);
    }
}

运行结果:

==========main start==========
==========main end============
当前线程:12
运行结果:5

可以看出main线程和刚刚创建的线程是两个线程,main线程执行完后刚刚创建的线程依旧在执行

image-20220731104410716
image-20220731104410716

2、实现Runnable接口

运行以下测试代码:

public static void main(String[] args) {
    System.out.println("==========main start==========");

    Runnable02 runnable02 = new Runnable02();
    Thread thread02 = new Thread(runnable02);
    thread02.start();

    System.out.println("==========main end============");
}


public static class Runnable02 implements Runnable{

    @SneakyThrows
    @Override
    public void run() {
        System.out.println("当前线程:"+Thread.currentThread().getId());
        Thread.sleep(2000);
        int i = 10 /2 ;
        System.out.println("运行结果:" + i);
    }
}

运行结果:

==========main start==========
==========main end============
当前线程:12
运行结果:5

可以看出继承Thread类和实现Runnable接口达到的效果一样

image-20220731104823314
image-20220731104823314

3、实现 Callable 接口 + FutureTask

1、实现 Callable 接口

运行以下测试代码:

public static void main(String[] args) {
    System.out.println("==========main start==========");

    Callable03 callable03 = new Callable03();
    FutureTask<Integer> futureTask03 = new FutureTask<>(callable03);
    Thread thread03 = new Thread(futureTask03);
    thread03.start();
    System.out.println("==========main end============");
}

public static class Callable03 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println("当前线程:"+Thread.currentThread().getId());
        Thread.sleep(2000);
        int i = 10 /2 ;
        System.out.println("运行结果:" + i);
        return i;
    }
}

运行结果:

==========main start==========
==========main end============
当前线程:12
运行结果:5

可以看出如果不获取返回值,实现 Callable 接口和继承Thread类、实现Runnable接口达到的效果一样

image-20220731105554127
image-20220731105554127
2、获取返回值

运行以下测试代码:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    Callable03 callable03 = new Callable03();
    FutureTask<Integer> futureTask03 = new FutureTask<>(callable03);
    Thread thread03 = new Thread(futureTask03);
    thread03.start();
    //阻塞式等待整个线程执行完成,获取返回结果
    Integer integer = futureTask03.get();
    System.out.println("返回的结果:" + integer);

    System.out.println("==========main end============");
}

public static class Callable03 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println("当前线程:"+Thread.currentThread().getId());
        Thread.sleep(2000);
        int i = 10 /2 ;
        System.out.println("运行结果:" + i);
        return i;
    }
}

运行结果:

==========main start==========
当前线程:12
运行结果:5
返回的结果:5
==========main end============

可以看到FutureTask泛型类型即为返回类型,其get()方法为阻塞方法,调用get()方法后,如果没有获取到返回结果会一直等待,获取到返回结果后才会往下执行

image-20220731105922499
image-20220731105922499
3、再次测试

运行以下测试代码:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    Callable03 callable03 = new Callable03();
    FutureTask<Integer> futureTask03 = new FutureTask<>(callable03);
    Thread thread03 = new Thread(futureTask03);
    thread03.start();
    System.out.println("开启线程之后,获取线程执行结果之前...");
    //阻塞式等待整个线程执行完成,获取返回结果
    Integer integer = futureTask03.get();
    System.out.println("返回的结果:" + integer);

    System.out.println("==========main end============");
}

public static class Callable03 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println("当前线程:"+Thread.currentThread().getId());
        Thread.sleep(2000);
        int i = 10 /2 ;
        System.out.println("运行结果:" + i);
        return i;
    }
}

运行结果:

==========main start==========
开启线程之后,获取线程执行结果之前...
当前线程:12
运行结果:5
返回的结果:5
==========main end============

可以看到只有调用FutureTask类的get()方法才会阻塞,其前面的代码可以执行

image-20220731110952167
image-20220731110952167
4、FutureTask参数

FutureTask<V>实现了RunnableFuture<V>接口,可以接收FutureTask(Callable<V> callable)FutureTask(Runnable runnable, V result)

image-20220731110130569
image-20220731110130569

其中FutureTask<V>实现的RunnableFuture<V>接口继承Runnable Future<V>

📑接口可以多继承,但接口只能继承接口,接口不能实现接口、抽象类、实体类

抽象类不可以继承接口,但可以实现接口

抽象类可以继承实体类,但实体类必须有明确的构造函数。

/**
 * A {@link Future} that is {@link Runnable}. Successful execution of
 * the {@code run} method causes completion of the {@code Future}
 * and allows access to its results.
 * @see FutureTask
 * @see Executor
 * @since 1.6
 * @author Doug Lea
 * @param <V> The result type returned by this Future's {@code get} method
 */
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}
image-20220731110517293
image-20220731110517293

FutureTask<V>的构造器可以传Callable<V> callable参数

/**
 * Creates a {@code FutureTask} that will, upon running, execute the
 * given {@code Callable}.
 *
 * @param  callable the callable task
 * @throws NullPointerException if the callable is null
 */
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}
image-20220731110236524
image-20220731110236524

FutureTask<V>的构造器也可以传Runnable runnable, V result两个参数,其中V result为一个对象,可以通过这个对象获取结果的返回值

/**
 * Creates a {@code FutureTask} that will, upon running, execute the
 * given {@code Runnable}, and arrange that {@code get} will return the
 * given result on successful completion.
 *
 * @param runnable the runnable task
 * @param result the result to return on successful completion. If
 * you don't need a particular result, consider using
 * constructions of the form:
 * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
 * @throws NullPointerException if the runnable is null
 */
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}
image-20220731110324080
image-20220731110324080

4、线程池

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args){
    System.out.println("==========main start==========");
    executorService.execute(new Runnable02());


    System.out.println("==========main end============");
}

运行结果:

==========main start==========
==========main end============
当前线程:12
运行结果:5

可以看到固定线程数量的线程池创建线程后,该线程池不会消失,控制台可以看出还在一直运行

image-20220731111834984
image-20220731111834984

ExecutorService类有submit方法,该方法有返回值。而execute方法没有返回值

image-20220731111723003
image-20220731111723003

5、线程池七大参数

七大参数:
1、int corePoolSize    [5]核心线程数; 线程池,创建好以后就准备就一直存在【除非设置(allowCoreThreadTimeOut)】
                       5个 Thread thread = new Thread(); thread.start();
2、int maximumPoolSize 最大线程数量,控制资源
3、long keepAliveTime  存活时间。如果当前的线程数量大于core数量,并且线程空闲时间大于指定的存活时间,就释放空闲的线程(最少保							留corePoolSize个)。
4、TimeUnit unit       时间单位
5、BlockingQueue<Runnable> workQueue 阻塞队列。如果任务有很多,就会将目前多的任务放在队列里面。
                                      只要有线程空用,就会去队列里面取出新的任务继续执行。
 6、ThreadFactory threadFactory 线程的创建工厂
7、RejectedExecutionHandler handler 拒绝策略 如果队列满了,按照我们指定的拒绝策略拒绝执行任务
1、拒绝策略

java.util.concurrent.RejectedExecutionHandler类里,使用ctrl + H快捷键,查看其具体实现类,可以看到主要有以下四个常用拒绝测试

java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy 丢弃最老的任务
java.util.concurrent.ThreadPoolExecutor.AbortPolicy         丢弃刚来的任务,并抛异常
java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy    不执行start(),直接调用run()方法
java.util.concurrent.ThreadPoolExecutor.DiscardPolicy       丢弃刚来的任务,不抛异常
image-20220731131107234
image-20220731131107234
2、测试代码

以下为ThreadPoolExecutor类创建对象的示例代码

/**
 * 七大参数:
 * 1、int corePoolSize    [5]核心线程数; 线程池,创建好以后就准备就一直存在【除非设置(allowCoreThreadTimeOut)】
 *                        5个 Thread thread = new Thread(); thread.start();
 * 2、int maximumPoolSize 最大线程数量,控制资源
 * 3、long keepAliveTime  存活时间。如果当前的线程数量大于core数量,并且线程空闲时间大于指定的存活时间,就释放空闲的线程(最少保留corePoolSize个)。
 * 4、TimeUnit unit       时间单位
 * 5、BlockingQueue<Runnable> workQueue 阻塞队列。如果任务有很多,就会将目前多的任务放在队列里面。
 *                                      只要有线程空用,就会去队列里面取出新的任务继续执行。
 * 6、ThreadFactory threadFactory 线程的创建工厂
 * 7、RejectedExecutionHandler handler 拒绝策略 如果队列满了,按照我们指定的拒绝策略拒绝执行任务
 *
 * 运行流程:
 * 1、线程池创建,准备好 core  数量的核心线程,准备接受任务
 * 2、新的任务进来,用 core 准备好的空闲线程执行。
 *      (1)、core 满了,就将再进来的任务放入阻塞队列中。空闲的 core 就会自己去阻塞队列获取任务执行
 *      (2)、阻塞队列满了,就直接开新线程执行,最大只能开到 max  指定的数量
 *      (3)、max 都执行好了。max-core 数量空闲的线程会在 keepAliveTime 指定的时间后自动销毁。最终保持到 core 大小
 *      (4)、如果线程数开到了 max 的数量,还有新任务进来,就会使用 reject 指定的拒绝策略进行处理
 * 3、所有的线程创建都是由指定的 factory 创建的。
 */
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
        200,
        10,
        TimeUnit.SECONDS,
        //new LinkedBlockingDeque<>() 时一定要设置容量,默认是Integer.MAX_VALUE,会导致内存不够
        new LinkedBlockingDeque<>(100000),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy());
image-20220731132530881
image-20220731132530881
3、线程池工具类

Executors工具类有以下4个常用方法

/**
 *
 * 一个线程池core 7;max 20; queue 50, 100并发进来怎么分配的;
 * 答:7个会立即得到执行,50个会进入队列,再开13个进行执行。剩下的30个就使用拒绝策略。
 */

Executors.newCachedThreadPool();        //core是0,所有都可回收
Executors.newFixedThreadPool(5);        //固定大小,core=max; 都不可回收
Executors.newScheduledThreadPool(5);    //定时任务的线程池 DelayedWorkQueue
Executors.newSingleThreadExecutor();    //单线程的线程池,后台从队列里面获取任务,挨个执行
image-20220731133132597
image-20220731133132597
4、源代码

ThreadPoolExecutor类位于java.util.concurrent.ThreadPoolExecutor(JUC)包,其七大参数为

/**
 * Creates a new {@code ThreadPoolExecutor} with the given initial
 * parameters.
 *使用给定的初始化参数创建一个新的{@code ThreadPoolExecutor}。 
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 *        保留在池中的线程数,即使它们是空闲的,除非设置了 {@code allowCoreThreadTimeOut} 
 * @param maximumPoolSize the maximum number of threads to allow in the
 *        pool
 *		  池中允许的最大线程数 
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 *        当线程数大于核心线程数时,这些多余的空闲线程在终止前等待新任务的最长时间。
 * @param unit the time unit for the {@code keepAliveTime} argument
 *        参数的时间单位
 * @param workQueue the queue to use for holding tasks before they are
 *        executed.  This queue will hold only the {@code Runnable}
 *        tasks submitted by the {@code execute} method.
 *		  用于在执行任务之前保存任务的队列。此队列将仅保存由 {@code execute} 方法提交的 {@code Runnable} 任务。 
 * @param threadFactory the factory to use when the executor
 *        creates a new thread
 *        执行程序创建新线程时使用的工厂 
 * @param handler the handler to use when execution is blocked
 *        because the thread bounds and queue capacities are reached
 		  执行因达到线程边界和队列容量而被阻塞时使用的处理程序
 * @throws IllegalArgumentException if one of the following holds:<br>
 *		   @throws IllegalArgumentException 如果满足以下条件之一:
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *         or {@code threadFactory} or {@code handler} is null
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
image-20220731123732874
image-20220731123732874

6、开发中使用线程池的原因

  • 降低资源的消耗
  • 通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗
  • 提高响应速度
  • 因为线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行
  • 提高线程的可管理性
  • 线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配

7、完整代码

点击查看ThreadTest类完整代码

5.6.2、CompletableFuture 异步编排

Future的继承关系如下图所示

image-20220801213339920

业务场景

查询商品详情页的逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间。

假如商品详情页的每个查询,需要如下标注的时间才能完成。那么,用户需要 5.5s 后才能看到商品详情页的内容。很显然是不能接受的。如果有多个线程同时完成这 6 步操作,也许只需要 1.5s 即可完成响应。

序号要执行的业务耗时
1获取sku的基本信息0.5s
2获取sku的图片信息0.5s
3获取sku的促销信息1s
4获取spu的所有销售属性1s
5获取规格参数组及组下的规格参数1.5s
6spu详情1s

123可以同时执行,而456必须得到1的返回结果后才能执行,456之间可以同时执行

1、创建异步对象

1、常用方法

CompletableFuture类提供了四个静态方法来创建一个异步操作。

public static CompletableFuture<Void> runAsync(Runnable runnable);
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor);
  • runAsync方法都是没有返回结果的
  • supplyAsync方法都是可以获取返回结果的
  • 可以传入自定义的线程池,否则就用默认的线程池;
2、runAsync方法

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) {
    System.out.println("==========main start==========");

    CompletableFuture.runAsync(()->{
        System.out.println("当前线程:"+Thread.currentThread().getId());
        int i = 10 /2 ;
        System.out.println("运行结果:" + i);
    },executorService);

    System.out.println("==========main end============");
}

运行结果:

==========main start==========
==========main end============
当前线程:12
运行结果:5

可以看到该方法不需要有返回值,且该方法不会停止,线程池里的核心线程不会消失

image-20220731140708664
image-20220731140708664
3、supplyAsync方法

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) {
    System.out.println("==========main start==========");

    CompletableFuture.supplyAsync(()->{
        System.out.println("当前线程:"+Thread.currentThread().getId());
        int i = 10 /2 ;
        System.out.println("运行结果:" + i);
        return i;
    },executorService);

    System.out.println("==========main end============");
}

运行结果:

==========main start==========
==========main end============
当前线程:12
运行结果:5

可以看到该方法有返回值,且该方法不会停止,线程池里的核心线程不会消失

image-20220731140835585
image-20220731140835585
4、supplyAsync方法获取返回值

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("运行结果:" + i);
        return i;
    }, executorService);

    System.out.println("开启线程之后,获取线程执行结果之前...");
    //阻塞式等待整个线程执行完成,获取返回结果
    System.out.println(future.get());
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
开启线程之后,获取线程执行结果之前...
当前线程:12
运行结果:5
5
==========main end============

可以通过future.get()方法获取方法的返回值,且该方法是阻塞方法,没有获取到返回值会一直等待,直到获取到返回值后,才会执行下面的代码

image-20220731141206092
image-20220731141206092
5、源码

CompletableFuture类位于java.util.concurrent(JUC)包下

/**
 * Returns a new CompletableFuture that is asynchronously completed
 * by a task running in the {@link ForkJoinPool#commonPool()} with
 * the value obtained by calling the given Supplier.
 *
 * @param supplier a function returning the value to be used
 * to complete the returned CompletableFuture
 * @param <U> the function's return type
 * @return the new CompletableFuture
 */
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    return asyncSupplyStage(asyncPool, supplier);
}

/**
 * Returns a new CompletableFuture that is asynchronously completed
 * by a task running in the given executor with the value obtained
 * by calling the given Supplier.
 *
 * @param supplier a function returning the value to be used
 * to complete the returned CompletableFuture
 * @param executor the executor to use for asynchronous execution
 * @param <U> the function's return type
 * @return the new CompletableFuture
 */
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                   Executor executor) {
    return asyncSupplyStage(screenExecutor(executor), supplier);
}

/**
 * Returns a new CompletableFuture that is asynchronously completed
 * by a task running in the {@link ForkJoinPool#commonPool()} after
 * it runs the given action.
 *
 * @param runnable the action to run before completing the
 * returned CompletableFuture
 * @return the new CompletableFuture
 */
public static CompletableFuture<Void> runAsync(Runnable runnable) {
    return asyncRunStage(asyncPool, runnable);
}

/**
 * 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);
}
image-20220731140402932
image-20220731140402932

2、方法完成后的感知

1、常用方法

whenCompletewhenCompleteAsync不可以修改返回结果,exceptionally可以修改返回结果

//whenComplete  可以处理正常和异常的计算结果 (T为上一步的返回结果,? super Throwable为上一步的异常信息)
public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action);
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action);
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor);
//exceptionally  处理异常情况(可以修改返回结果)。((Throwable为上一步的异常信息,? extends T为这一步要返回的结果)
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn);

whenComplete 和 whenCompleteAsync 的区别:

  • whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
  • whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
  • 方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)
2、whenComplete感知异常(没有异常)

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        System.out.println("运行结果:" + i);
        return i;
    }, executorService).whenComplete((result,exception)->{
        System.out.println("异步任务完成后的返回结果:" + result);
        System.out.println("异步任务抛出的异常:" + exception);
    });

    System.out.println("==========main end============");
}

运行结果:

==========main start==========
当前线程:12
运行结果:5
异步任务完成后的返回结果:5
异步任务抛出的异常:null
==========main end============

可以看到whenComplete可以获取到该线程的返回结果异常信息

image-20220731145834946
image-20220731145834946
3、whenComplete感知异常(有异常)

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 0;
        System.out.println("运行结果:" + i);
        return i;
    }, executorService).whenComplete((result,exception)->{
        System.out.println("异步任务完成后的返回结果:" + result);
        System.out.println("异步任务抛出的异常:" + exception);
    });

    System.out.println("==========main end============");
}

运行结果:

==========main start==========
当前线程:12
异步任务完成后的返回结果:null
异步任务抛出的异常:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
==========main end============

可以看到whenComplete可以获取到该线程的返回结果异常信息

image-20220731145952658
image-20220731145952658
4、exceptionally处理异常

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 0;
        System.out.println("运行结果:" + i);
        return i;
    }, executorService).whenComplete((result,exception)->{
        //虽然能得到异常信息,但是没法修改返回数据。
        System.out.println("异步任务完成后的返回结果:" + result);
        System.out.println("异步任务抛出的异常:" + exception);
    }).exceptionally(throwable -> {
        //可以感知异常,同时返回默认值
        return 10;
    });
    System.out.println("线程返回的结果:"+future.get());
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
当前线程:12
异步任务完成后的返回结果:null
异步任务抛出的异常:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
线程返回的结果:10
==========main end============

exceptionally方法可以对该线程的异常进行处理,但是如果线程没有异常就不能对正常返回结果进行处理

image-20220731150404465
image-20220731150404465
5、源码
public CompletableFuture<T> whenComplete(
    BiConsumer<? super T, ? super Throwable> action) {
    return uniWhenCompleteStage(null, action);
}

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);
}

// not in interface CompletionStage

/**
 * Returns a new CompletableFuture that is completed when this
 * CompletableFuture completes, with the result of the given
 * function of the exception triggering this CompletableFuture's
 * completion when it completes exceptionally; otherwise, if this
 * CompletableFuture completes normally, then the returned
 * CompletableFuture also completes normally with the same value.
 * Note: More flexible versions of this functionality are
 * available using methods {@code whenComplete} and {@code handle}.
 *
 * @param fn the function to use to compute the value of the
 * returned CompletableFuture if this CompletableFuture completed
 * exceptionally
 * @return the new CompletableFuture
 */
public CompletableFuture<T> exceptionally(
    Function<Throwable, ? extends T> fn) {
    return uniExceptionallyStage(fn);
}
image-20220731145421477
image-20220731145421477
image-20220731145427210
image-20220731145427210

3、方法完成后的处理(handle)

1、常用方法
//(? super T为上一步的返回结果,Throwable为上一步的异常信息,? extends U> fn为这一步的返回结果)
public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor)
2、handle处理异常返回值(有异常)

可以修改返回结果

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 0;
        System.out.println("运行结果:" + i);
        return i;
    }, executorService).handle((result,throwable)->{
        if (result!=null){
            return result*2;
        }
        if (throwable!=null){
            return 0;
        }
        return 0;
    });
    System.out.println("线程返回的结果:"+future.get());
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
当前线程:12
线程返回的结果:0
==========main end============

handle可以对该线程的正常返回结果异常返回结果进行处理

image-20220731151320171
image-20220731151320171
3、handle处理异常返回值(没有异常)

如果把int i = 10 / 0;改为

int i = 10 / 2;

运行结果:

==========main start==========
当前线程:12
运行结果:5
线程返回的结果:10
==========main end============

handle可以对该线程的正常返回结果异常返回结果进行处理

image-20220731151354996
image-20220731151354996
4、源码
public <U> CompletableFuture<U> handle(
    BiFunction<? super T, Throwable, ? extends U> fn) {
    return uniHandleStage(null, fn);
}

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);
}
image-20220731162118455
image-20220731162118455

4、线程串行化方法

1、常用方法

thenRun不能感知上一步结果,thenAccept能感知上一步处理结果

//A处理完后B处理,B不需要A的返回结果
public CompletableFuture<Void> thenRun(Runnable action);
public CompletableFuture<Void> thenRunAsync(Runnable action);
public CompletableFuture<Void> thenRunAsync(Runnable action,Executor executor);
//A处理完后B处理,B需要A的返回结果
//(Consumer<? super T> action为上一步的返回结果)
public CompletableFuture<Void> thenAccept(Consumer<? super T> action);
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);
//A处理完后B处理,B需要A的返回结果、B处理完后还需要返回本次处理后的结果,别人感知
//(Function<? super T为上一步的返回结果,? extends U> fn为这一步的返回结果)
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn);
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn);
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor);
  • thenRun 方法:只要上面的任务执行完成,就开始执行 thenRun,只是处理完任务后,执行 thenRun 的后续操作
  • thenAccept 方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
  • thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
  • 方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)
2、thenRunAsyncB不需要A的返回值

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("运行结果:" + i);
        return i;
    }, executorService).thenRun(() -> {
        System.out.println("任务2启动了...");
    });
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
当前线程:12
==========main end============
运行结果:5
任务2启动了...

thenRunAsync方法是任务1执行完后任务2再执行,任务2不能获取到任务1的返回值

image-20220731153402668
image-20220731153402668
3、thenAcceptAsyncB需要A的返回值

运行以下测试代码:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("运行结果:" + i);
        return i;
    }, executorService).thenAccept((result) -> {
        System.out.println("任务2启动了...");
        System.out.println("获取到了上一步的返回结果:" + result);
    });
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
当前线程:12
==========main end============
运行结果:5
任务2启动了...
获取到了上一步的返回结果:5

thenAcceptAsync方法是任务1执行完后任务2再执行,任务2可以获取到任务1的返回值

image-20220731154341218
image-20220731154341218
4、thenApplyAsyncA、B都返回结果

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        System.out.println("当前线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService).thenApply((result) -> {
        System.out.println("任务2启动了...");
        System.out.println("任务2获取到了任务1的返回结果:" + result);
        return "Hello " + result;
    });
    System.out.println("任务都完成后返回结果:" + future.get());
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
当前线程:12
任务1运行结果:5
任务2启动了...
任务2获取到了任务1的返回结果:5
任务都完成后返回结果:Hello 5
==========main end============

thenApplyAsync方法是任务1执行完后任务2再执行,任务2可以获取到任务1的返回值,且任务2也需返回结果

image-20220731154900573
image-20220731154900573
5、源码
public <U> CompletableFuture<U> thenApply(
    Function<? super T,? extends U> fn) {
    return uniApplyStage(null, fn);
}

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);
}

public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
    return uniAcceptStage(null, action);
}

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);
}

public CompletableFuture<Void> thenRun(Runnable action) {
    return uniRunStage(null, action);
}

public CompletableFuture<Void> thenRunAsync(Runnable action) {
    return uniRunStage(asyncPool, action);
}

public CompletableFuture<Void> thenRunAsync(Runnable action,
                                            Executor executor) {
    return uniRunStage(screenExecutor(executor), action);
}
image-20220731152222563
image-20220731152222563

5、两任务组合 - 都要完成

1、常用方法
//CompletableFuture<T> implements Future<T>, CompletionStage<T>
//任务1和任务2(CompletionStage<?> other)都做完后,再做Runnable action指定的事情
//不能获取任务1和任务2(CompletionStage<?> other)的返回结果,且这次执行不用返回结果
//CompletionStage<?> other:要完成的另一个任务(任务2),Runnable action:接下来要完成的任务
public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor executor)

//可以获取任务1和任务2(CompletionStage<?> other)的返回结果,这次执行不用返回结果
//CompletionStage<? extends U> other:要完成的另一个任务(任务2),BiConsumer<? super T, ? super U> action:任务1和任务2的返回值
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action, Executor executor)

//可以获取任务1和任务2(CompletionStage<?> other)的返回结果,并返回本次的执行结果
//CompletionStage<?> other:要完成的另一个任务(任务2)
//? super T:接下来要完成的任务的返回值,? super U:任务1的返回值,? extends V:任务2的返回值
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn, Executor executor)
  • runAfterBoth:组合两个 future,不需要获取 future 的结果,只需两个 future 处理完任务后,处理该任务。
  • thenAcceptBoth:组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有返回值。
  • thenCombine:组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值
  • 方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)
2、runAfterBothAsync

运行以下测试代码:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    future1.runAfterBothAsync(future2,()->{
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        String hello = "hello3";
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务3运行结果:" + hello);
    },executorService);
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
==========main end============
任务1运行结果:5
任务2运行结果:hello2
任务3线程:15
任务3运行结果:hello3

runAfterBothAsync任务1任务2都执行完成后,任务3才能开始执行,且任务3不需要任务1任务2的返回值

image-20220731165058934
image-20220731165058934
3、thenAcceptBothAsync

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    future1.thenAcceptBothAsync(future2,(f1,f2)->{
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        String hello = "hello3";
        System.out.println("任务3获取到的任务1结果:" + f1);
        System.out.println("任务3获取到的任务2结果:" + f2);
        System.out.println("任务3运行结果:" + hello);
    },executorService);
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
==========main end============
任务1运行结果:5
任务2运行结果:hello2
任务3线程:15
任务3获取到的任务1结果:5
任务3获取到的任务2结果:hello2
任务3运行结果:hello3

thenAcceptBothAsync任务1任务2都执行完成后,任务3才能开始执行,且任务3可以获取到任务1任务2的返回值

image-20220731165718791
image-20220731165718791
4、thenCombineAsync

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    CompletableFuture<String> future3 = future1.thenCombineAsync(future2, (f1, f2) -> {
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        String hello = "hello3";
        System.out.println("任务3获取到的任务1结果:" + f1);
        System.out.println("任务3获取到的任务2结果:" + f2);
        System.out.println("任务3运行结果:" + hello);
        return f1 + " => " + f2 + " => " + hello;
    }, executorService);
    System.out.println("三个任务执行完的返回结果:" + future3.get());
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
任务2运行结果:hello2
任务1运行结果:5
任务3线程:14
任务3获取到的任务1结果:5
任务3获取到的任务2结果:hello2
任务3运行结果:hello3
三个任务执行完的返回结果:5 => hello2 => hello3
==========main end============

thenCombineAsync任务1任务2都执行完成后,任务3才能开始执行,任务3可以获取的任务1任务2的返回值,且任务3需要有返回值

image-20220731170401151
image-20220731170401151
5、源码
public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,
                                            Runnable action) {
    return biRunStage(null, other, action);
}

public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,
                                                 Runnable action) {
    return biRunStage(asyncPool, other, action);
}

public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,
                                                 Runnable action,
                                                 Executor executor) {
    return biRunStage(screenExecutor(executor), other, action);
}

public <U> CompletableFuture<Void> thenAcceptBoth(
    CompletionStage<? extends U> other,
    BiConsumer<? super T, ? super U> action) {
    return biAcceptStage(null, other, action);
}

public <U> CompletableFuture<Void> thenAcceptBothAsync(
    CompletionStage<? extends U> other,
    BiConsumer<? super T, ? super U> action) {
    return biAcceptStage(asyncPool, other, action);
}

public <U> CompletableFuture<Void> thenAcceptBothAsync(
    CompletionStage<? extends U> other,
    BiConsumer<? super T, ? super U> action, Executor executor) {
    return biAcceptStage(screenExecutor(executor), other, action);
}

public <U,V> CompletableFuture<V> thenCombine(
    CompletionStage<? extends U> other,
    BiFunction<? super T,? super U,? extends V> fn) {
    return biApplyStage(null, other, fn);
}

public <U,V> CompletableFuture<V> thenCombineAsync(
    CompletionStage<? extends U> other,
    BiFunction<? super T,? super U,? extends V> fn) {
    return biApplyStage(asyncPool, other, fn);
}

public <U,V> CompletableFuture<V> thenCombineAsync(
    CompletionStage<? extends U> other,
    BiFunction<? super T,? super U,? extends V> fn, Executor executor) {
    return biApplyStage(screenExecutor(executor), other, fn);
}

runAfterBothAsync

image-20220731163529661
image-20220731163529661

thenAcceptBothAsync

image-20220731163512341
image-20220731163512341

thenCombineAsync

image-20220731163441464
image-20220731163441464

6、两任务组合 - 一个完成

1、常用方法
//任务1和任务2(CompletionStage<?> other)其中一个做完后,再做Runnable action指定的事情
//不能获取任务1和任务2(CompletionStage<?> other)的返回结果,且这次执行不用返回
//CompletionStage<?> other:要完成的另一个任务(任务2),Runnable action:接下来要完成的任务
public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor)

//可以获取任务1或任务2(CompletionStage<?> other)的返回结果,这次执行不用返回结果
//CompletionStage<? extends U> other:要完成的另一个任务(任务2),Consumer<? super T> action:先执行完的那个任务的返回值
public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action,Executor executor)

//可以获取任务1或任务2(CompletionStage<?> other)的返回结果,这次执行需要返回结果
//CompletionStage<? extends T> other:要完成的另一个任务(任务2)
//Function<? super T, U> fn:先执行完的那个任务的返回值 和 接下来要完成的任务的返回值
public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn,Executor executor)
2、runAfterEitherAsync

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    future1.runAfterEitherAsync(future2, () -> {
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        String hello = "hello3";
        System.out.println("任务3运行结果:" + hello);
    }, executorService);
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
==========main end============
任务1运行结果:5
任务3线程:15
任务3运行结果:hello3
任务2运行结果:hello2

runAfterEitherAsync任务1任务2其中一个执行完成后,任务3才能开始执行,任务3不可以获取任务1任务2的返回值

image-20220731185139964
image-20220731185139964
3、acceptEitherAsync

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    future1.acceptEitherAsync(future2, (result) -> {
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        System.out.println("任务3获取到的前两个任务其中一个执行完的返回值"+result);
        String hello = "hello3";
        System.out.println("任务3运行结果:" + hello);
    }, executorService);
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
==========main end============
任务1运行结果:5
任务3线程:15
任务3获取到的前两个任务其中一个执行完的返回值5
任务3运行结果:hello3
任务2运行结果:hello2

acceptEitherAsync任务1任务2其中一个执行完成后,任务3才能开始执行,任务3可以获取已成功执行的那个线程的返回值

任务1任务2任务要求返回值必须是一致的,因为需要获取已成功执行的那个线程的返回值,所以两个任务都必须要有相同类型的返回值

image-20220731185547647
image-20220731185547647

任务1任务2的返回值类型都修改为Object,可以看到已经不报错了

image-20220731185639506
image-20220731185639506

执行成功后成功获取到任务1的返回值

image-20220731190107420
image-20220731190107420
4、applyToEitherAsync

运行以下测试代码:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    CompletableFuture<String> future = future1.applyToEitherAsync(future2, (result) -> {
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        System.out.println("任务3获取到的前两个任务其中一个执行完的返回值" + result);
        String hello = "hello3";
        System.out.println("任务3运行结果:" + hello);
        return result + " => " + hello;
    }, executorService);
    System.out.println("任务3执行完后的返回结果:" + future.get());
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
任务1运行结果:5
任务3线程:14
任务3获取到的前两个任务其中一个执行完的返回值5
任务3运行结果:hello3
任务3执行完后的返回结果:5 => hello3
==========main end============
任务2运行结果:hello2

applyToEitherAsync任务1任务2其中一个执行完成后,任务3才能开始执行,任务3可以获取已成功执行的那个线程的返回值,且任务3需要有返回值

image-20220731190543612
image-20220731190543612
5、源码
public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,
                                              Runnable action) {
    return orRunStage(null, other, action);
}

public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,
                                                   Runnable action) {
    return orRunStage(asyncPool, other, action);
}

public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,
                                                   Runnable action,
                                                   Executor executor) {
    return orRunStage(screenExecutor(executor), other, action);
}

public CompletableFuture<Void> acceptEither(
    CompletionStage<? extends T> other, Consumer<? super T> action) {
    return orAcceptStage(null, other, action);
}

public CompletableFuture<Void> acceptEitherAsync(
    CompletionStage<? extends T> other, Consumer<? super T> action) {
    return orAcceptStage(asyncPool, other, action);
}

public CompletableFuture<Void> acceptEitherAsync(
    CompletionStage<? extends T> other, Consumer<? super T> action,
    Executor executor) {
    return orAcceptStage(screenExecutor(executor), other, action);
}

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

public <U> CompletableFuture<U> applyToEitherAsync(
    CompletionStage<? extends T> other, Function<? super T, U> fn) {
    return orApplyStage(asyncPool, other, fn);
}

public <U> CompletableFuture<U> applyToEitherAsync(
    CompletionStage<? extends T> other, Function<? super T, U> fn,
    Executor executor) {
    return orApplyStage(screenExecutor(executor), other, fn);
}

当两个任务中,任意一个 future 任务完成的时候,执行任务。

applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。 acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。 runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返回值。

image-20220731170716550
image-20220731170716550
image-20220731170825404
image-20220731170825404
image-20220731170916342
image-20220731170916342

7、多任务组合

1、常用方法
//所有的都完成
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
//其中一个完成
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
2、allOf都要完成
1、测试1

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    CompletableFuture<Object> future3 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        String hello = "hello3";
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务3运行结果:" + hello);
        return hello;
    }, executorService);

    CompletableFuture<Void> future = CompletableFuture.allOf(future1, future2, future3);
    long start = System.currentTimeMillis();
    System.out.println("3个任务的返回结果:" + future.get());
    long end = System.currentTimeMillis();
    System.out.println("阻塞式等待所消耗:" + (end - start) + "s");
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
任务3线程:14
任务1运行结果:5
任务3运行结果:hello3
任务2运行结果:hello2
3个任务的返回结果:null
阻塞式等待所消耗:3000s
==========main end============

allOf方法要去所有任务都要完成,可以看到调用最终的future.get()会阻塞线程,其返回值为null

image-20220731191825653
image-20220731191825653
2、测试2

运行以下测试代码:

//一个固定数量的线程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    CompletableFuture<Object> future3 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        String hello = "hello3";
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务3运行结果:" + hello);
        return hello;
    }, executorService);

    CompletableFuture<Void> future = CompletableFuture.allOf(future1, future2, future3);
    long start = System.currentTimeMillis();
    System.out.println("3个任务的返回结果:" + future.get());
    long end = System.currentTimeMillis();
    System.out.println("阻塞式等待所消耗:" + (end - start) + "s");
    System.out.println("获取任务1返回的结果:" + future1.get());
    System.out.println("获取任务2返回的结果:" + future2.get());
    System.out.println("获取任务3返回的结果:" + future3.get());
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
任务3线程:14
任务1运行结果:5
任务3运行结果:hello3
任务2运行结果:hello2
3个任务的返回结果:null
阻塞式等待所消耗:2999s
获取任务1返回的结果:5
获取任务2返回的结果:hello2
获取任务3返回的结果:hello3
==========main end============

可以通过各自的future.get()方法,来获取各自的返回值

image-20220731192539083
image-20220731192539083
3、测试3

如果注释掉最终的future.get()方法,则不会被阻塞,main方法先退出

future.get()

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
任务3线程:14
==========main end============
任务1运行结果:5
任务3运行结果:hello3
任务2运行结果:hello2
image-20220731192051270
image-20220731192051270
3、anyOf任何一个完成

运行以下测试代码:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    System.out.println("==========main start==========");

    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务1线程:" + Thread.currentThread().getId());
        int i = 10 / 2;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务1运行结果:" + i);
        return i;
    }, executorService);

    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务2线程:" + Thread.currentThread().getId());
        String hello = "hello2";
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务2运行结果:" + hello);
        return hello;
    }, executorService);

    CompletableFuture<Object> future3 = CompletableFuture.supplyAsync(() -> {
        System.out.println("任务3线程:" + Thread.currentThread().getId());
        String hello = "hello3";
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务3运行结果:" + hello);
        return hello;
    }, executorService);

    CompletableFuture<Object> future = CompletableFuture.anyOf(future1, future2, future3);
    long start = System.currentTimeMillis();
    System.out.println("3个任务的返回结果:" + future.get());
    long end = System.currentTimeMillis();
    System.out.println("阻塞式等待所消耗:" + (end - start) + "s");
    System.out.println("==========main end============");
}

运行结果:

==========main start==========
任务1线程:12
任务2线程:13
任务3线程:14
任务1运行结果:5
3个任务的返回结果:5
阻塞式等待所消耗:1000s
==========main end============
任务3运行结果:hello3
任务2运行结果:hello2

anyOf方法要求,所有任务中,任何一个完成即可

image-20220731192832799
image-20220731192832799
4、源码
/* ------------- Arbitrary-arity constructions -------------- */

/**
 * Returns a new CompletableFuture that is completed when all of
 * the given CompletableFutures complete.  If any of the given
 * CompletableFutures complete exceptionally, then the returned
 * CompletableFuture also does so, with a CompletionException
 * holding this exception as its cause.  Otherwise, the results,
 * if any, of the given CompletableFutures are not reflected in
 * the returned CompletableFuture, but may be obtained by
 * inspecting them individually. If no CompletableFutures are
 * provided, returns a CompletableFuture completed with the value
 * {@code null}.
 *
 * <p>Among the applications of this method is to await completion
 * of a set of independent CompletableFutures before continuing a
 * program, as in: {@code CompletableFuture.allOf(c1, c2,
 * c3).join();}.
 *
 * @param cfs the CompletableFutures
 * @return a new CompletableFuture that is completed when all of the
 * given CompletableFutures complete
 * @throws NullPointerException if the array or any of its elements are
 * {@code null}
 */
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
    return andTree(cfs, 0, cfs.length - 1);
}

/**
 * Returns a new CompletableFuture that is completed when any of
 * the given CompletableFutures complete, with the same result.
 * Otherwise, if it completed exceptionally, the returned
 * CompletableFuture also does so, with a CompletionException
 * holding this exception as its cause.  If no CompletableFutures
 * are provided, returns an incomplete CompletableFuture.
 *
 * @param cfs the CompletableFutures
 * @return a new CompletableFuture that is completed with the
 * result or exception of any of the given CompletableFutures when
 * one completes
 * @throws NullPointerException if the array or any of its elements are
 * {@code null}
 */
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
    return orTree(cfs, 0, cfs.length - 1);
}
image-20220731191323588
image-20220731191323588

8、完整代码

点击查看ThreadTest类完整代码

5.6.3、商城业务-商品详情

1、环境搭建

1、修改host文件

打开SwitchHosts软件,点击本地方案里的gulimall,在里面添加192.168.56.10 item.gulimall.com域名映射,用于商品详情页展示

# gulimall
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
192.168.56.10 item.gulimall.com
image-20220731193725444
2、添加nginx配置

打开Xshell,修改/mydata/nginx/conf/gulimall.conf文件

cd /mydata/nginx/conf/
ls
cd conf.d/
ls
vi gulimall.conf
image-20220731194052069
image-20220731194052069

由于server_name配置的是*.gulimall.com,已经把gulimall.com的子域名都配置了,所有不用再配置了

image-20220731193944867
image-20220731193944867
3、配置gateway

在网关中添加如下配置,把item.gulimall.com域名下的请求负载均衡到gulimall-product服务

spring:
  cloud:
    gateway:
      routes:
      	- id: gulimall_host_route
        uri: lb://gulimall-product
        predicates:
        - Host=gulimall.com,item.gulimall.com
image-20220731194707345
image-20220731194707345
4、导入文件

将资源里的2.分布式高级篇(微服务架构篇)\资料源码\代码\html\详情页目录里的shangpinxiangqing.html文件复制到gulimall-product模块的src/main/resources/templates目录下,并将shangpinxiangqing.html文件修改为item.html

image-20220731194607792
image-20220731194607792

linux虚拟机里的/mydata/nginx/html/static目录里面新建item文件夹,将资源里的2.分布式高级篇(微服务架构篇)\资料源码\代码\html\详情页里的其他文件夹复制到linux虚拟机/mydata/nginx/html/static/item目录里面

GIF 2022-7-31 19-54-25
GIF 2022-7-31 19-54-25

gulimall-product模块的src/main/resources/templates/item.html文件里的部分href="(除了href="#")替换为href="/static/item/,将部分src="(除了src="实际url")替换为src="/static/item/

点击查看完整item页面

5、新建skuItem方法

gulimall-product模块的com.atguigu.gulimall.product.web.ItemController包下新建skuItem方法

package com.atguigu.gulimall.product.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author 无名氏
 * @date 2022/7/31
 * @Description: 商品的详情信息
 */
@Controller
public class ItemController {

    /**
     * 展示sku的详情
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}.html")
    public String skuItem(@PathVariable("skuId") Long skuId){

        System.out.println("准备查询" + skuId + "的详情");

        return "item";
    }
}
image-20220731202407973
image-20220731202407973
6、添加页面跳转

打开http://search.gulimall.com/list.html?catalog3Id=225页面,打开控制台,定位到某个商品,复制class="da"

image-20220731201122217
image-20220731201122217

class="da"<p>标签里的<a>标签里添加th:href="|http://item.gulimall.com/${prodect.skuId}.html|"属性,点击该图片跳转到相应的页面,|thymeleaf语法,可以快速拼串

<!--排序内容-->
<div class="rig_tab">
    <div th:each="prodect:${result.getProducts()}">
        <div class="ico">
            <i class="iconfont icon-weiguanzhu"></i>
            <a href="#">关注</a>
        </div>
        <p class="da">
            <a th:href="|http://item.gulimall.com/${prodect.skuId}.html|">
                <img th:src="${prodect.skuImg}" class="dim">
            </a>
        </p>
        <ul class="tab_im">
            <li>
                <a href="#" title="黑色">
                    <img th:src="${prodect.skuImg}"></a>
            <li>
        </ul>
        <p class="tab_R">
            <span th:text="'¥'+${prodect.skuPrice}">¥5199.00</span>
        </p>
        <p class="tab_JE">
            <a href="#" th:utext="${prodect.skuTitle}">
                Apple iPhone 7 Plus (A1661) 32G 黑色 移动联通电信4G手机
            </a>
        </p>
        <p class="tab_PI">已有<span>11万+</span>热门评价
            <a href="#">二手有售</a>
        </p>
        <p class="tab_CP"><a href="#" title="谷粒商城Apple产品专营店">谷粒商城Apple产品...</a>
            <a href='#' title="联系供应商进行咨询">
                <img src="/static/search/img/xcxc.png">
            </a>
        </p>
        <div class="tab_FO">
            <div class="FO_one">
                <p>自营
                    <span>谷粒商城自营,品质保证</span>
                </p>
                <p>满赠
                    <span>该商品参加满赠活动</span>
                </p>
            </div>
        </div>
    </div>
</div>
image-20220731202128687
image-20220731202128687
7、测试

重启GulimallProductApplication服务和GulimallGatewayApplication服务,打开http://search.gulimall.com/list.html页面,定位到某个商品的<a>标签,可以看到其href属性已修改为http://item.gulimall.com/9.html

image-20220731202040269
image-20220731202040269

点击该商品,已经跳转到了http://item.gulimall.com/9.html页面,不过不是动态的数据

image-20220731202143743
image-20220731202143743

打开GulimallProductApplication服务的控制台,可以看到控制台输出准备查询9的详情

image-20220731202258611
image-20220731202258611

2、封装详情页数据

1、准备工作

gulimall-product模块的com.atguigu.gulimall.product.vo包里新建SkuItemVo类用于封装商品详情页数据

package com.atguigu.gulimall.product.vo;

/**
 * @author 无名氏
 * @date 2022/7/31
 * @Description: 商品详情页数据
 */
public class SkuItemVo {
}
image-20220731202609160
image-20220731202609160

修改gulimall-product模块的com.atguigu.gulimall.product.web.ItemController类的skuItem方法

@Autowired
SkuInfoService skuInfoService;

/**
 * 展示sku的详情
 * @param skuId
 * @return
 */
@GetMapping("/{skuId}.html")
public String skuItem(@PathVariable("skuId") Long skuId){

    //System.out.println("准备查询" + skuId + "的详情");

    SkuItemVo vo = skuInfoService.item(skuId);

    return "item";
}
image-20220731203013826
image-20220731203013826

gulimall-product模块的com.atguigu.gulimall.product.service.SkuInfoService接口里添加item抽象方法

SkuItemVo item(Long skuId);
image-20220731203048457
image-20220731203048457

gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl类里实现item抽象方法

@Override
public SkuItemVo item(Long skuId) {
    SkuItemVo skuItemVo = new SkuItemVo();

    //1、sku基本信息获取pms_sku_info

    //2、sku的图片信息pms_sku_images

    //3、获取spu的销售属性组合。

    //4、获取spu的介绍

    //5、获取spu的规格参数信息。


    return skuItemVo;
}
image-20220731203543095
image-20220731203543095
2、查出详情页的数据

gulimall-product模块的com.atguigu.gulimall.product.vo.SkuItemVo类里添加需要封装的数据字段

点击查看SkuItemVo类完整代码

image-20220731205828359
image-20220731205828359

gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl类的item方法里添加具体实现

@Override
public SkuItemVo item(Long skuId) {
    SkuItemVo skuItemVo = new SkuItemVo();

    //1、sku基本信息获取pms_sku_info
    SkuInfoEntity skuInfoEntity = this.getById(skuId);
    skuItemVo.setInfo(skuInfoEntity);
    Long spuId = skuInfoEntity.getSpuId();
    Long catalogId = skuInfoEntity.getCatalogId();

    //2、sku的图片信息pms_sku_images
    List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
    skuItemVo.setImages(images);
    //3、获取spu的销售属性组合。
    List<SkuItemVo.SkuItemSaleAttrVo> skuItemSaleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
    skuItemVo.setSaleAttr(skuItemSaleAttrVos);
    //4、获取spu的介绍 pms_spu_info_desc
    SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(spuId);
    skuItemVo.setDesp(spuInfoDescEntity);
    //5、获取spu的规格参数信息。
    List<SkuItemVo.SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuIdAndCatalogId(spuId,catalogId);
    skuItemVo.setGroupAttrs(attrGroupVos);

    return skuItemVo;
}
image-20220802100658259
image-20220802100658259
3、获取sku图片信息

gulimall-product模块的com.atguigu.gulimall.product.service.SkuImagesService接口里添加getImagesBySkuId抽象方法

List<SkuImagesEntity> getImagesBySkuId(Long skuId);
image-20220802100745220
image-20220802100745220

gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuImagesServiceImpl类里实现getImagesBySkuId抽象方法

@Override
public List<SkuImagesEntity> getImagesBySkuId(Long skuId) {

   LambdaQueryWrapper<SkuImagesEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
   lambdaQueryWrapper.eq(SkuImagesEntity::getSkuId, skuId);
    return this.baseMapper.selectList(lambdaQueryWrapper);
}
image-20220802101301908
image-20220802101301908
4、获取spu规格参数信息

gulimall-product模块的com.atguigu.gulimall.product.service.AttrGroupService接口里添加getAttrGroupWithAttrsBySpuIdAndCatalogId抽象方法

List<SkuItemVo.SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuIdAndCatalogId(Long spuId, Long catalogId);
image-20220802101801380
image-20220802101801380

gulimall-product模块的com.atguigu.gulimall.product.service.impl.AttrGroupServiceImpl类里实现getAttrGroupWithAttrsBySpuIdAndCatalogId抽象方法

@Override
public List<SkuItemVo.SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuIdAndCatalogId(Long spuId, Long catalogId) {
    //查出当前spu对应的所有属性的分组信息以及当前分组下的所有属性对应的值
    return this.baseMapper.getAttrGroupWithAttrsBySpuIdAndCatalogId(spuId,catalogId);
}
image-20220802102053960
image-20220802102053960

gulimall-product模块的com.atguigu.gulimall.product.dao.AttrGroupDao接口里添加getAttrGroupWithAttrsBySpuIdAndCatalogId抽象方法

List<SkuItemVo.SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuIdAndCatalogId(@Param("spuId") Long spuId, @Param("catalogId") Long catalogId);
GIF 2022-8-2 10-23-33
GIF 2022-8-2 10-23-33
5、编写sql

想要封装的数据

/**
 * 商品的基本属性分组
 * (基本信息:
 *    机身长度(mm) 150.5
 *    机身宽度(mm) 77.8
 *    机身厚度(mm) 8.2
 *    机身重量(g)  约186g(含电池)
 * )
 */
@Data
public static class SpuItemAttrGroupVo{
    /**
     * 组名
     */
    private String groupName;
    /**
     * 属性列表
     */
    private List<SpuBaseAttrVo> attrs;
}

/**
 * 商品详细信息里的基本属性
 */
@Data
public static class SpuBaseAttrVo{
    /**
     * 属性名
     */
    private String attrName;
    /**
     * 属性值
     */
    private String attrValue;
}
image-20220804192407328
image-20220804192407328

gulimall_pms数据库里,可以根据catelog_id可以在pms_attr_group表里查出attr_group_nameattr_group_id

image-20220731212330393
image-20220731212330393

根据catelog_idpms_attr_group表里查出attr_group_nameattr_group_idsql语句

select * from pms_attr_group where catelog_id = 225;
image-20220802090316099
image-20220802090316099

根据刚刚查出的attr_group_id可以在pms_attr_attrgroup_relation表里查出对应的所有attr_id

image-20220804192923497
image-20220804192923497

根据刚刚查出的attr_group_idpms_attr_attrgroup_relation表里查出对应的所有attr_idsql语句

select pag.attr_group_name,pag.attr_group_id,paar.attr_id
from pms_attr_group pag
left join pms_attr_attrgroup_relation paar on pag.attr_group_id = paar.attr_group_id
where catelog_id = 225;
image-20220802090959411
image-20220802090959411

根据刚刚查询到的attr_id可以在pms_attr表里查询出attr_name

⚠️这里的pms_attr表里value_select为可选的value列表,不是我们想要的attrValue

image-20220804193530836
image-20220804193530836

根据查询到的attr_idpms_attr表里查询出attr_namesql语句

select pag.attr_group_name,pag.attr_group_id,paar.attr_id,pattr.attr_name
from pms_attr_group pag
left join pms_attr_attrgroup_relation paar on pag.attr_group_id = paar.attr_group_id
left join pms_attr pattr on paar.attr_id = pattr.attr_id
where pag.catelog_id = 225;
image-20220802091511570
image-20220802091511570

根据刚刚查出的attr_id可以在pms_product_attr_value表里查对应的attr_value(其实这里面有attr_name完全没有必要在pms_attr表里面查)

image-20220804194438412
image-20220804194438412

根据刚刚查出的attr_idpms_product_attr_value表里查对应的attr_valuesql语句

select pag.attr_group_name,pag.attr_group_id,paar.attr_id,pattr.attr_name,ppav.attr_value
from pms_attr_group pag
left join pms_attr_attrgroup_relation paar on pag.attr_group_id = paar.attr_group_id
left join pms_attr pattr on paar.attr_id = pattr.attr_id
left join pms_product_attr_value ppav on pattr.attr_id = ppav.attr_id
where pag.catelog_id = 225;
image-20220802092257211
image-20220802092257211

刚刚已经查询出了当前catelog_id的所有attr_group_nameattr_nameattr_value,再筛选掉不是当前spu_id的数据就行了(这样做感觉好像不太高效,我觉得应该先在pms_product_attr_value表里查询出当前spu_id的所有attr_id,在根据attr_id查出attr_group_name)

select pag.attr_group_name,pag.attr_group_id,paar.attr_id,pattr.attr_name,ppav.attr_value,ppav.spu_id
from pms_attr_group pag
left join pms_attr_attrgroup_relation paar on pag.attr_group_id = paar.attr_group_id
left join pms_attr pattr on paar.attr_id = pattr.attr_id
left join pms_product_attr_value ppav on pattr.attr_id = ppav.attr_id
where pag.catelog_id = 225 and ppav.spu_id = 1;
image-20220802092848294
image-20220802092848294

gulimall-product模块的src/main/resources/mapper/product/AttrGroupDao.xml文件里添加如下代码(内部类前面使用$连接,而不是.

<resultMap id="spuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SkuItemVo$SpuItemAttrGroupVo">
    <result property="groupName" column="attr_group_name"/>
    <collection property="attrs" ofType="com.atguigu.gulimall.product.vo.SkuItemVo$SpuBaseAttrVo">
        <result property="attrName" column="attr_name"/>
        <result property="attrValues" column="attr_value"/>
    </collection>
</resultMap>

<select id="getAttrGroupWithAttrsBySpuIdAndCatalogId" resultMap="spuItemAttrGroupVo">
    select pag.attr_group_name,pag.attr_group_id,paar.attr_id,pattr.attr_name,ppav.attr_value,ppav.spu_id
    from gulimall_pms.pms_attr_group pag
             left join gulimall_pms.pms_attr_attrgroup_relation paar on pag.attr_group_id = paar.attr_group_id
             left join gulimall_pms.pms_attr pattr on paar.attr_id = pattr.attr_id
             left join gulimall_pms.pms_product_attr_value ppav on pattr.attr_id = ppav.attr_id
    where pag.catelog_id = #{catalogId} and ppav.spu_id = #{spuId};
</select>
image-20220802104501856
image-20220802104501856
6、测试

gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl类的item方法的3、获取spu的销售属性组合。的部分注释掉

List<SkuItemVo.SkuItemSaleAttrVo> skuItemSaleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
skuItemVo.setSaleAttr(skuItemSaleAttrVos);
image-20220802104358079
image-20220802104358079

gulimall-product模块的com.atguigu.gulimall.product.GulimallProductApplicationTests测试类里添加如下代码,执行测试

@Autowired
AttrGroupService attrGroupService;
@Test
public void attrGroupServiceTest(){
   System.out.println(attrGroupService.getAttrGroupWithAttrsBySpuIdAndCatalogId(1L, 225L));

报了如下错误

Caused by: java.lang.IllegalStateException: No typehandler found for property attrValues
	at org.apache.ibatis.mapping.ResultMapping$Builder.validate(ResultMapping.java:151)
	at org.apache.ibatis.mapping.ResultMapping$Builder.build(ResultMapping.java:140)
	at org.apache.ibatis.builder.MapperBuilderAssistant.buildResultMapping(MapperBuilderAssistant.java:391)
	at org.apache.ibatis.builder.xml.XMLMapperBuilder.buildResultMappingFromContext(XMLMapperBuilder.java:393)
	at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:280)
	at org.apache.ibatis.builder.xml.XMLMapperBuilder.processNestedResultMappings(XMLMapperBuilder.java:402)
	at org.apache.ibatis.builder.xml.XMLMapperBuilder.buildResultMappingFromContext(XMLMapperBuilder.java:383)
	at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:280)
	at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:253)
	at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElements(XMLMapperBuilder.java:245)
	at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:118)
	... 95 more
image-20220802104717746
image-20220802104717746

gulimall-product模块的com.atguigu.gulimall.product.vo.SkuItemVo类的SpuBaseAttrVo内部类的private List<String> attrValues;修改为private String attrValue;

image-20220802104829696
image-20220802104829696

gulimall-product模块的src/main/resources/mapper/product/AttrGroupDao.xml文件里的<result property="attrValues" column="attr_value"/>修改为<result property="attrValue" column="attr_value"/>

然后重启GulimallProductApplication模块,刷新http://item.gulimall.com/9.html页面,GulimallProductApplication服务的控制台成功输出了skuItemVo的数据

[SkuItemVo.SpuItemAttrGroupVo(groupName=主体, attrs=[SkuItemVo.SpuBaseAttrVo(attrName=入网型号, attrValue=LIO-A00), SkuItemVo.SpuBaseAttrVo(attrName=上市年份, attrValue=2019)]), SkuItemVo.SpuItemAttrGroupVo(groupName=基本信息, attrs=[SkuItemVo.SpuBaseAttrVo(attrName=机身颜色, attrValue=黑色), SkuItemVo.SpuBaseAttrVo(attrName=机身长度(mm), attrValue=158.3), SkuItemVo.SpuBaseAttrVo(attrName=机身材质工艺, attrValue=其他)]), SkuItemVo.SpuItemAttrGroupVo(groupName=主芯片, attrs=[SkuItemVo.SpuBaseAttrVo(attrName=CPU品牌, attrValue=海思(Hisilicon)), SkuItemVo.SpuBaseAttrVo(attrName=CPU型号, attrValue=HUAWEI Kirin 980)])]
image-20220802105046822
image-20220802105046822
7、获取spu销售属性集合

去掉刚刚在gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl类的item方法的3、获取spu的销售属性组合。的注释

List<SkuItemVo.SkuItemSaleAttrVo> skuItemSaleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
skuItemVo.setSaleAttr(skuItemSaleAttrVos);

gulimall-product模块的com.atguigu.gulimall.product.service.SkuSaleAttrValueService接口里添加getSaleAttrsBySpuId抽象方法

List<SkuItemVo.SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId);
image-20220802105245574
image-20220802105245574

gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuSaleAttrValueServiceImpl类里实现getSaleAttrsBySpuId抽象方法

@Override
public List<SkuItemVo.SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId) {

    return this.getBaseMapper().getSaleAttrsBySpuId(spuId);
}
image-20220802105432422
image-20220802105432422

gulimall-product模块的com.atguigu.gulimall.product.dao.SkuSaleAttrValueDao接口里添加getSaleAttrsBySpuId抽象方法

List<SkuItemVo.SkuItemSaleAttrVo> getSaleAttrsBySpuId(@Param("spuId") Long spuId);
GIF 2022-8-2 10-56-24
GIF 2022-8-2 10-56-24
8、编写sql

根据spu_idpms_sku_info表里可以查出对应的sku_id

select * from pms_sku_info where spu_id=1;
image-20220802110511143
image-20220802110511143

根据sku_id可以在pms_sku_sale_attr_value表里查出对应的attr_idattr_value

select pssav.attr_id attr_id,pssav.attr_value attr_value
from pms_sku_info info
left join pms_sku_sale_attr_value pssav on info.sku_id = pssav.sku_id
where info.spu_id=1;
image-20220802110549989
image-20220802110549989

可以对pssav.attr_idpssav.attr_name进行分组,group_concat(distinct pssav.attr_value)函数可以将未分组的数据用,连接起来

select pssav.attr_id attr_id,pssav.attr_name attr_name, group_concat(distinct pssav.attr_value) attr_value
from pms_sku_info info
left join pms_sku_sale_attr_value pssav on info.sku_id = pssav.sku_id
where info.spu_id=1
group by pssav.attr_id,pssav.attr_name
image-20220802110623514
image-20220802110623514

gulimall-product模块的src/main/resources/mapper/product/SkuSaleAttrValueDao.xml文件里添加如下代码(内部类前面使用$连接,而不是.

<resultMap id="skuItemSaleAttrVo" type="com.atguigu.gulimall.product.vo.SkuItemVo$SkuItemSaleAttrVo">
    <id property="attrId" column="attr_id"/>
    <result property="attrName" column="attr_name"/>
    <collection property="attrValues" ofType="java.lang.String">
        <result column="attr_value"/>
    </collection>
</resultMap>

<select id="getSaleAttrsBySpuId" resultMap="skuItemSaleAttrVo">
    select pssav.attr_id attr_id,pssav.attr_name attr_name,pssav.attr_value attr_value
    from gulimall_pms.pms_sku_info info
             left join gulimall_pms.pms_sku_sale_attr_value pssav on info.sku_id = pssav.sku_id
    where info.spu_id=#{spuId};
</select>
image-20220802195353614
image-20220802195353614
9、测试

gulimall-product模块的com.atguigu.gulimall.product.GulimallProductApplicationTests测试类里添加如下代码。然后执行测试

@Autowired
SkuSaleAttrValueService skuSaleAttrValueService;
@Test
public void skuSaleAttrValueServiceTest(){
   System.out.println(skuSaleAttrValueService.getSaleAttrsBySpuId(1L));
}

控制台已正确输出如下数据:

[SkuItemVo.SkuItemSaleAttrVo(attrId=4, attrName=颜色, attrValues=[星河银, 亮黑色, 翡冷翠, 罗兰紫]), SkuItemVo.SkuItemSaleAttrVo(attrId=11, attrName=版本, attrValues=[8GB+128GB, 8GB+256GB])]
image-20220802112111881
image-20220802112111881
10、在Model里添加数据

修改gulimall-product模块的com.atguigu.gulimall.product.web.ItemController类的skuItem方法

/**
 * 展示sku的详情
 * @param skuId
 * @return
 */
@GetMapping("/{skuId}.html")
public String skuItem(@PathVariable("skuId") Long skuId, Model model){

    //System.out.println("准备查询" + skuId + "的详情");
    SkuItemVo vo = skuInfoService.item(skuId);
    model.addAttribute("item",vo);
    return "item";
}
image-20220802112435047
image-20220802112435047

3、展示数据

1、修改标题、价格、大图

gulimall-product模块的src/main/resources/templates/item.html文件里的<html>标签上添加xmlns:th="http://www.thymeleaf.org"属性,引入thymeleaf

<html xmlns:th="http://www.thymeleaf.org">
image-20220802150449195
image-20220802150449195

http://item.gulimall.com/9.html页面里,打开控制台,定位到华为 HUAWEI Mate 10 6GB+128GB 亮黑色 移动联通电信4G手机 双卡双待,复制华为 HUAWEI Mate 10 6GB+128GB 亮黑色 移动联通电信4G手机 双卡双待

image-20220804205800473
image-20220804205800473

gulimall-product模块的src/main/resources/templates/item.html文件里搜索华为 HUAWEI Mate 10 6GB+128GB 亮黑色 移动联通电信4G手机 双卡双待,在class="box-name"<div>里添加th:text="${item.info.skuTitle}"属性,在class="box-hide"<div>里添加 th:text="${item.info.skuSubtitle}"属性,删除class="box-hide"<div><u></u>里的内容

修改主标题副标题,删除优惠信息跳转

<div class="box-name" th:text="${item.info.skuTitle}">
   华为 HUAWEI Mate 10 6GB+128GB 亮黑色 移动联通电信4G手机 双卡双待
</div>
<div class="box-hide" th:text="${item.info.skuSubtitle}">预订用户预计11月30日左右陆续发货!麒麟970芯片!AI智能拍照!
   <a href=""><u></u></a>
</div>
image-20220802150741326
image-20220802150741326

http://item.gulimall.com/9.html页面里,打开控制台,定位到大图,复制probox

image-20220802150947928
image-20220802150947928

gulimall-product模块的src/main/resources/templates/item.html文件里搜索probox,在 class="probox"<div>里的<img>标签上添加th:src="${item.info.skuDefaultImg}"属性,在class="showbox"<div>里的<img>标签上添加th:src="${item.info.skuDefaultImg}"属性

<div class="imgbox">
   <div class="probox">
      <!--商品大图图片-->
      <img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
      <div class="hoverbox"></div>
   </div>
   <div class="showbox">
      <!--大图放大后的图片 -->
      <img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
   </div>
</div>
image-20220802151259345
image-20220802151259345

http://item.gulimall.com/9.html页面里,打开控制台,定位到4499.00,复制4499.00

image-20220802151427909
image-20220802151427909

gulimall-product模块的src/main/resources/templates/item.html文件里搜索4499.00,在该<span>标签里添加th:text="${item.info.price}"属性

<span th:text="${item.info.price}">4499.00</span>
image-20220802153343528
image-20220802153343528

重启GulimallProductApplication服务,刷新http://item.gulimall.com/9.html页面,可以看到主标题副标题价格大图都已经动态显示出来了,但是价格显示了小数点后6位

image-20220802153454603
image-20220802153454603

http://search.gulimall.com/list.html页面里搜索华为,随便点击一个商品,可以看到在http://item.gulimall.com/1.html页面里已动态刷新出这些数据,但是价格精确到了小数点后三位

GIF 2022-8-2 15-36-59
GIF 2022-8-2 15-36-59

thymeleaf里,有一个#numbers.formatDecimal(final Number target, final Integer minIntegerDigits, final Integer decimalDigits)方法,可以指定最小整数位数小数个数

将刚刚的<span th:text="${item.info.price}">4499.00</span>修改为如下代码

<span th:text="${#numbers.formatDecimal(item.info.price,1,2)}">4499.00</span>
image-20220802154113758
image-20220802154113758

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

打开http://item.gulimall.com/7.html页面,可以看到价格已只显示小数点后两位了

image-20220802154150273
image-20220802154150273
2、修改销售属性、小图

http://item.gulimall.com/7.html页面里,,打开控制台,定位到无货,此商品暂时售完,复制无货,此商品暂时售完

image-20220802154434635
image-20220802154434635

gulimall-product模块的com.atguigu.gulimall.product.vo.SkuItemVo类里添加hasStock字段,用来指明是否有货

/**
 * 是否有货
 */
boolean hasStock = true;
image-20220802154552565
image-20220802154552565

gulimall-product模块的src/main/resources/templates/item.html文件里搜索无货,此商品暂时售完,在此<span>标签里添加th:text属性

<li>
   <span th:text="${item.hasStock?'有货':'无货,此商品暂时售完'}">有货</span>
</li>
image-20220802155031124
image-20220802155031124

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

刷新http://item.gulimall.com/7.html页面,可以看到已经显示有货

image-20220802155138616
image-20220802155138616

http://item.gulimall.com/7.html页面里,,打开控制台,定位到选择颜色,复制选择颜色

image-20220802155237628
image-20220802155237628

gulimall-product模块的src/main/resources/templates/item.html文件里搜索选择颜色,只保留一个class="box-attr clear"<div>,删掉其余同级<div>

image-20220802155328193
image-20220802155328193

将其修改为如下代码

<div class="box-attr-3">
   <div class="box-attr clear" th:each="attr : ${item.saleAttr}">
      <dl>
         <dt>选择[[${attr.attrName}]]</dt>
         <dd th:each="val : ${attr.attrValues}">
            <a href="#">
            <!--<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" />-->
               [[${val}]]
            </a>
         </dd>
      </dl>
   </div>
</div>
image-20220802162459901
image-20220802162459901

http://item.gulimall.com/3.html页面里,,打开控制台,定位到小图片,复制box-lh-one

image-20220813093708919
image-20220813093708919

gulimall-product模块的src/main/resources/templates/item.html文件里搜索box-lh-one,只保留一个class="box-attr clear"<div>

<div class="box-lh-one">
   <ul>
      <li th:each="image : ${item.images}"><img th:src="${image.imgUrl}"/></li>
   </ul>
</div>
image-20220802160025226
image-20220802160025226

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

image-20220802160751563
image-20220802160751563

但图片有可能为空,因此需要加一个判断

<div class="box-lh-one">
   <ul>
      <li th:each="image : ${item.images}" th:if="${!#strings.isEmpty(image.imgUrl)}">
         <img th:src="${image.imgUrl}"/>
      </li>
   </ul>
</div>
image-20220802160526179
image-20220802160526179
3、修改商品介绍、规格与包装

http://item.gulimall.com/3.html页面里,,打开控制台,定位到商品详情的图片,复制xiaoguo

image-20220802162621276
image-20220802162621276

class="xiaoguo"<img>标签的上面<p>标签和<table>标签注释掉

<p>
    <a href="#">品牌:华为(HUAWEI)</a>
</p>

class="xiaoguo"<img>标签修改为如下代码

<img class="xiaoguo" th:each="desp : ${#strings.listSplit(item.desp.decript,',')}" th:src="${desp}" />
image-20220802163541409
image-20220802163541409

http://item.gulimall.com/3.html页面里,,打开控制台,定位到规格与包装,复制规格与包装

image-20220802164153238
image-20220802164153238

规格与包装所在的<a>标签的href="##"属性删掉,并删掉同级其他<li>标签里的<a>标签里的href="#"属性

<ul class="shopjieshao">
   <li class="jieshoa" style="background: #e4393c;">
      <a style="color: white;">商品介绍</a>
   </li>
   <li class="baozhuang">
      <a >规格与包装</a>
   </li>
   <li class="baozhang">
      <a >售后保障</a>
   </li>
   <li class="pingjia">
      <a >商品评价(4万+)</a>
   </li>
   <li class="shuoming">
      <a>预约说明</a>
   </li>

</ul>
image-20220802164419137
image-20220802164419137

重启GulimallProductApplication服务,刷新http://item.gulimall.com/3.html页面,可以看到点击商品介绍规格与包装售后保障商品评价(4万+)预约说明都没有什么问题,就是鼠标样式有点问题

GIF 2022-8-2 16-45-37
GIF 2022-8-2 16-45-37

gulimall-product模块的src/main/resources/templates/item.html文件的<head>标签里修改鼠标样式

<style  type="text/css">
   a:hover{
      cursor: pointer;
   }
</style>
image-20220802165345938
image-20220802165345938

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

刷新http://item.gulimall.com/3.html页面,可以看到鼠标样式已经正常了

GIF 2022-8-2 16-54-19
GIF 2022-8-2 16-54-19

http://item.gulimall.com/3.html页面里,打开控制台,定位到商品介绍最下面的规格介绍,复制guiGebox guiGebox1

image-20220802170417765
image-20220802170417765

gulimall-product模块的src/main/resources/templates/item.html文件里搜索guiGebox guiGebox1,只保留里面第一个class="guiGe"<div>,删掉其他的class="guiGe"<div>

image-20220802170547221
image-20220802170547221

将其修改为如下样式

<div class="guiGebox guiGebox1">
   <div class="guiGe" th:each="guige : ${item.groupAttrs}">
      <h3 th:text="${guige.groupName}" style="text-align: left">主体</h3>
      <dl>
         <div th:each="attr:${guige.attrs}">
            <dt th:text="${attr.attrName}" style="text-align: left">品牌</dt>
            <dd th:text="${attr.attrValue}" style="text-align: left">华为(HUAWEI)</dd>
         </div>
      </dl>
   </div>
   <div class="package-list">
      <h3>包装清单</h3>
      <p>手机(含内置电池) X 1、5A大电流华为SuperCharge充电器X 1、5A USB数据线 X 1、半入耳式线控耳机 X 1、快速指南X 1、三包凭证 X 1、取卡针 X 1、保护壳 X 1</p>
   </div>
</div>
image-20220802172130594
image-20220802172130594

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

刷新http://item.gulimall.com/3.html页面,可以看到商品介绍里的销售属性已经显示出来了

image-20220802172158517
image-20220802172158517

http://item.gulimall.com/3.html页面里,打开控制台,定位到规格与包装里的销售属性,复制guiGebox

image-20220802163735252
image-20220802163735252

gulimall-product模块的src/main/resources/templates/item.html文件里搜索guiGebox,只保留里面第一个class="guiGe"<div>,删掉其他的class="guiGe"<div>

image-20220802185536572
image-20220802185536572

将其修改为如下样式

<div class="guiGebox">
   <div class="guiGe" th:each="guige : ${item.groupAttrs}">
      <h3 th:text="${guige.groupName}" style="text-align: left">主体</h3>
      <dl>
         <div th:each="attr:${guige.attrs}">
            <dt th:text="${attr.attrName}" style="text-align: left">品牌</dt>
            <dd th:text="${attr.attrValue}" style="text-align: left">华为(HUAWEI)</dd>
         </div>
      </dl>
   </div>
   <div class="package-list">
      <h3>包装清单</h3>
      <p>手机(含内置电池) X 1、5A大电流华为SuperCharge充电器X 1、5A USB数据线 X 1、半入耳式线控耳机 X 1、快速指南X 1、三包凭证 X 1、取卡针 X 1、保护壳 X 1</p>
   </div>
</div>
image-20220802190210444
image-20220802190210444

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

刷新http://item.gulimall.com/3.html页面,可以看到规格与包装里的销售属性已经显示出来了

image-20220802190954869
image-20220802190954869

但是此时的商品介绍里的销售属性竟然显示了两次一模一样的数据

GIF 2022-8-2 19-12-48
GIF 2022-8-2 19-12-48

gulimall-product模块的src/main/resources/templates/item.html文件里搜索guiGebox guiGebox1,注释掉该class="guiGebox guiGebox1"<div>标签

image-20220802191412216
image-20220802191412216

点击Build -> Recompile 'item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

刷新http://item.gulimall.com/3.html页面,可以看到这次就只显示一次了

GIF 2022-8-2 19-15-17
GIF 2022-8-2 19-15-17

4、点击可选参数跳转到指定sku页面

1、编写sql

当点击某一个具体的颜色内存容量等时,可以查询哪些sku有这个具体的颜色内存容量。修改上次写的查询该sku可选参数的sql,修改为如下sql,多查询一个pssav.attr_name

select pssav.sku_id,pssav.attr_id attr_id,pssav.attr_name,pssav.attr_value attr_value
from pms_sku_info info
left join pms_sku_sale_attr_value pssav on info.sku_id = pssav.sku_id
where info.spu_id=1;
image-20220802192144097
image-20220802192144097

可以根据spu_id查询该商品的所有可选参数的所有具体属性值,然后判断哪些sku有该属性值。当点击某一个可选参数的具体属性值时,修改该可选参数的属性值,其他可选参数的具体属性值不变,再查找交集,判断要选择的sku,要显示的sku就是满足所有可选参数的某一具体属性值的sku交集。(eg: 当前sku的可选参数信息为:颜色=亮黑色(具有该可选参数的属性值的sku3,4)、版本=8GB+128GB(具有该可选参数的属性值的sku1,3,5,7)。因此如果点击颜色=罗兰紫就需要判断属性具有颜色=罗兰紫(sku=7,8)和版本=8GB+128GB(sku=1,3,5,7)的sku ,将满足这些属性值的sku求交集就为7,因此应该跳转到sku_id=7的商品页)

select pssav.attr_id attr_id,pssav.attr_name attr_name,pssav.attr_value attr_value,
       group_concat(distinct pssav.sku_id) sku_id
from pms_sku_info info
         left join pms_sku_sale_attr_value pssav on info.sku_id = pssav.sku_id
where info.spu_id=1
group by pssav.attr_id,pssav.attr_name,pssav.attr_value;
image-20220802193044714
image-20220802193044714
2、修改代码

gulimall-product模块的com.atguigu.gulimall.product.vo.SkuItemVo类里的private List<String> attrValues;修改为private List<AttrValueWithSkuIdVo> attrValues;

再新建AttrValueWithSkuIdVo匿名内部类

@Data
public static class AttrValueWithSkuIdVo{
    /**
     * 属性值(如翡冷翠、8GB+128GB、8GB+256GB等)
     */
    private String attrValue;
    /**
     * 具有该属性值的skuId集合
     */
    private String skuIds;
}
image-20220802194718825
image-20220802194718825

修改gulimall-product模块的src/main/resources/mapper/product/SkuSaleAttrValueDao.xml文件的id为getSaleAttrsBySpuId的查询语句

<resultMap id="skuItemSaleAttrVo" type="com.atguigu.gulimall.product.vo.SkuItemVo$SkuItemSaleAttrVo">
    <id property="attrId" column="attr_id"/>
    <result property="attrName" column="attr_name"/>
    <collection property="attrValues" ofType="com.atguigu.gulimall.product.vo.SkuItemVo$AttrValueWithSkuIdVo">
        <result property="attrValue" column="attr_value"/>
        <result property="skuIds" column="sku_id"/>
    </collection>
</resultMap>

<select id="getSaleAttrsBySpuId" resultMap="skuItemSaleAttrVo">
    select pssav.attr_id attr_id,pssav.attr_name attr_name,pssav.attr_value attr_value,
           group_concat(distinct pssav.sku_id) sku_id
    from gulimall_pms.pms_sku_info info
             left join gulimall_pms.pms_sku_sale_attr_value pssav on info.sku_id = pssav.sku_id
    where info.spu_id=1
    group by pssav.attr_id,pssav.attr_name,pssav.attr_value;
</select>
image-20220802195721440
image-20220802195721440
3、修改页面

修改gulimall-product模块的src/main/resources/templates/item.html class="box-attr-3"<div>

<div class="box-attr-3">
   <div class="box-attr clear" th:each="attr : ${item.saleAttr}">
      <dl>
         <dt>选择[[${attr.attrName}]]</dt>
         <dd th:each="val : ${attr.attrValues}">
            <a href="#" th:attr="skus=${val.skuIds}">
            <!--<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" />-->
               [[${val.attrValue}]]
            </a>
         </dd>
      </dl>
   </div>
</div>
image-20220802200349661
image-20220802200349661

重启GulimallProductApplication模块,在http://item.gulimall.com/3.html页面里,打开控制台,定位到选择颜色里的星河银8GB+256GB

<a href="#" skus="1,2">星河银</a>
<a href="#" skus="2,4,6,8">8GB+256GB</a>

可以看到星河银8GB+256GBskus集合的∩交集只有2,这就是想要的sku(边框颜色有问题)

image-20220802200202794
image-20220802200202794

gulimall-product模块的src/main/resources/templates/item.html文件里搜索box-attr clear,跳转到可选参数这里,

class="box-attr clear"<div>里的<a>标签里的href="#"删掉,替换为 th:class="${#lists.contains(#strings.listSplit(val.skuIds,','),item.info.skuId.toString())?'sku_attr_value checked':'sku_attr_value'}",如果选择了该属性值就class属性里加上checked

<div class="box-attr-3">
   <div class="box-attr clear" th:each="attr : ${item.saleAttr}">
      <dl>
         <dt>选择[[${attr.attrName}]]</dt>
         <dd th:each="val : ${attr.attrValues}">
            <a th:class="${#lists.contains(#strings.listSplit(val.skuIds,','),item.info.skuId.toString())?'sku_attr_value checked':'sku_attr_value'}"
               th:attr="skus=${val.skuIds}">
            <!--<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" />-->
               [[${val.attrValue}]]
            </a>
         </dd>
      </dl>
   </div>
</div>
image-20220802204646306
image-20220802204646306

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

可以看到当前3sku,里,attrValue 的skus属性里包含该3号的sku的标签class都为sku_attr_value checked,其他attrValueskus属性里包不含该3号的sku的标签class都为sku_attr_value(边框颜色有问题)

image-20220802204605054
image-20220802204605054

gulimall-product模块的src/main/resources/templates/item.html文件里的<script>里添加如下方法,去掉所有class里含有sku_attr_value<a>标签的父标签的红色边框,并将class='sku_attr_value checked'<a>标签的父标签添加红色边框

// 页面加载时自动调用该方法
$(function () {
   $(".sku_attr_value").parent().css({"border":"solid 1px #ccc"});
   $("a[class='sku_attr_value checked']").parent().css({"border":"solid 1px red"});
})
image-20220802205731494
image-20220802205731494

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

可以看到当前3sku,里,attrValue 的skus属性里包含该3号的sku的标签class都为sku_attr_value checked,其他attrValueskus属性里包不含该3号的sku的标签class都为sku_attr_value,并且都应用上了红色边框样式

image-20220802205829833
image-20220802205829833

gulimall-product模块的src/main/resources/templates/item.html文件里的<script>里添加如下方法,给class里含有sku_attr_value的标签添加点击事件,判断点击后可能要跳转的sku的集合

$(".sku_attr_value").click(function () {
   //点击的元素先添加上自定义的属性。为了识别我们是刚才被点击的
   var skus = new Array();
   $(this).addClass("clicked");
   var curr = $(this).attr("skus").split(",");
   //当前被点击的所有sku组合数组放进去
   skus.push(curr);
   //去掉同一行的所有的checked
   $(this).parent().parent().find(".sku_attr_value").removeClass("checked");
   //其他行的被选中的skuIds也放到skus里
   $("a[class='sku_attr_value checked']").each(function(){
      skus.push($(this).attr("skus").split(","));
   });
   console.log(skus);
});
image-20220802212245851
image-20220802212245851

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

http://item.gulimall.com/2.html页面里,如果点击选择颜色里的星河银,此时已求出符合满足所有可选参数的某个属性值的sku的集合

image-20220802212145502
image-20220802212145502

gulimall-product模块的src/main/resources/templates/item.html文件里的<script>里在.sku_attr_value的点击事件方法里继续添加代码

$(".sku_attr_value").click(function () {
   //点击的元素先添加上自定义的属性。为了识别我们是刚才被点击的
   var skus = new Array();
   $(this).addClass("clicked");
   var curr = $(this).attr("skus").split(",");
   //当前被点击的所有sku组合数组放进去
   skus.push(curr);
   //去掉同一行的所有的checked
   $(this).parent().parent().find(".sku_attr_value").removeClass("checked");
   //其他行的被选中的skuIds也放到skus里
   $("a[class='sku_attr_value checked']").each(function(){
      skus.push($(this).attr("skus").split(","));
   });
   console.log(skus);
   //取出他们的交集,得到skuId (skus[0]为刚刚点击的attrValue)
   var filterEle = skus[0];
   for(var i = 1;i<skus.length;i++){
      //使用 $(filterEle) 把 js对象 转为 Jquery对象
      filterEle = $(filterEle).filter(skus[i]);
   }
   //JQuery对象
   console.log(filterEle);
   //第0个元素即为想要获取的值
   console.log(filterEle[0]);
});
image-20220802213251687
image-20220802213251687

http://item.gulimall.com/2.html页面里,亮黑色和8GB+128GB都有的是3

image-20220802212809885
image-20220802212809885

http://item.gulimall.com/1.html页面里,在此页面点击选择颜色里的亮黑色,可以看到控制台输出3

image-20220802213018996
image-20220802213018996

gulimall-product模块的src/main/resources/templates/item.html文件里的<script>里在.sku_attr_value的点击事件方法里继续添加代码

$(".sku_attr_value").click(function () {
   //点击的元素先添加上自定义的属性。为了识别我们是刚才被点击的
   var skus = new Array();
   $(this).addClass("clicked");
   var curr = $(this).attr("skus").split(",");
   //当前被点击的所有sku组合数组放进去
   skus.push(curr);
   //去掉同一行的所有的checked
   $(this).parent().parent().find(".sku_attr_value").removeClass("checked");
   //其他行的被选中的skuIds也放到skus里
   $("a[class='sku_attr_value checked']").each(function(){
      skus.push($(this).attr("skus").split(","));
   });
   console.log(skus);
   //取出他们的交集,得到skuId (skus[0]为刚刚点击的attrValue)
   var filterEle = skus[0];
   for(var i = 1;i<skus.length;i++){
      //使用 $(filterEle) 把 js对象 转为 Jquery对象
      filterEle = $(filterEle).filter(skus[i]);
   }
   //JQuery对象
   console.log(filterEle);
   //第0个元素即为想要获取的值
   console.log(filterEle[0]);
   //4、跳转
   location.href = "http://item.gulimall.com/"+filterEle[0]+".html";
});
image-20220802213149530
image-20220802213149530

点击Build -> Recompile "item.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

http://item.gulimall.com/1.html页面里点击选择颜色里的亮黑色,可以看到正确跳转到了http://item.gulimall.com/3.html页面

GIF 2022-8-2 21-34-19
GIF 2022-8-2 21-34-19

5、异步编排

1、配置线程池

gulimall-product模块的com.atguigu.gulimall.product.config包里新建MyThreadConfig配置类,在这里面配置线程池,但这种固定配置的写法不是很好

package com.atguigu.gulimall.product.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author 无名氏
 * @date 2022/8/3
 * @Description: 线程池
 */
@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(){
        return new ThreadPoolExecutor(20,
                200, 10,
                TimeUnit.SECONDS, new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new  ThreadPoolExecutor.AbortPolicy());
    }
}
image-20220803092004898
image-20220803092004898

gulimall-product模块的com.atguigu.gulimall.product.config包里新建ThreadPollConfigProperties类,在该类里定义线程池可能的参数,并和配置文件的gulimall.thread绑定

package com.atguigu.gulimall.product.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author 无名氏
 * @date 2022/8/3
 * @Description:
 */
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPollConfigProperties {

    private Integer corePoolSize;
    private Integer maximumPoolSize;
    private Integer keepAliveTime;
}
image-20220803092437609
image-20220803092437609

可以根据提示添加如下依赖,启动该模块后,写自定义的配置可以有提示

image-20220803092755205
image-20220803092755205

gulimall-product模块的pom.xml里添加如下依赖

<!--添加注释处理器(使用idea添加自定义配置会有提示)-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
image-20220803093240135
image-20220803093240135

gulimall-product模块的src/main/resources/application.properties文件里添加如下配置

gulimall.thread.core-pool-size=20
gulimall.thread.maximum-pool-size=200
gulimall.thread.keep-alive-time=10
image-20220803093858347
image-20220803093858347

修改gulimall-product模块的com.atguigu.gulimall.product.config.MyThreadConfig配置类,改为从配置文件里动态取值

package com.atguigu.gulimall.product.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author 无名氏
 * @date 2022/8/3
 * @Description: 线程池
 */
//@EnableConfigurationProperties(ThreadPollConfigProperties.class)
@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPollConfigProperties poll){
        return new ThreadPoolExecutor(poll.getCorePoolSize(),
                poll.getMaximumPoolSize(), poll.getKeepAliveTime(),
                TimeUnit.SECONDS, new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new  ThreadPoolExecutor.AbortPolicy());
    }
}
image-20220803093723240
image-20220803093723240
2、使用异步编排

修改gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl类的item方法,改用异步编排来获取信息

@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
    SkuItemVo skuItemVo = new SkuItemVo();

    //1、sku基本信息获取pms_sku_info
    CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
        SkuInfoEntity skuInfoEntity = this.getById(skuId);
        skuItemVo.setInfo(skuInfoEntity);
        return skuInfoEntity;
    }, executor);
    //3、获取spu的销售属性组合。
    CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((info) -> {
        List<SkuItemVo.SkuItemSaleAttrVo> skuItemSaleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(info.getSpuId());
        skuItemVo.setSaleAttr(skuItemSaleAttrVos);
    },executor);
    //4、获取spu的介绍 pms_spu_info_desc
    CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((info) -> {
        SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(info.getSpuId());
        skuItemVo.setDesp(spuInfoDescEntity);
    }, executor);
    //5、获取spu的规格参数信息。
    CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((info) -> {
        List<SkuItemVo.SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuIdAndCatalogId(info.getSpuId(), info.getCatalogId());
        skuItemVo.setGroupAttrs(attrGroupVos);
    }, executor);

    //2、sku的图片信息pms_sku_images
    CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
        List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);
    },executor);

    //不用添加infoFuture,因为 saleAttrFuture,descFuture,baseAttrFuture能完成在其之前的infoFuture必然能完成
    CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();
    return skuItemVo;
}
image-20220803095329617
image-20220803095329617

鼠标放在 CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();get这里,按alt+enter快捷键,选择Add exceptions to method signature,再点击yes,给本类和本类的接口的item方法抛出异常

GIF 2022-8-3 10-11-03
GIF 2022-8-3 10-11-03

此时gulimall-product模块的com.atguigu.gulimall.product.service.SkuInfoService接口的item方法变为如下代码

SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException;
image-20220813152500357
image-20220813152500357

gulimall-product模块的com.atguigu.gulimall.product.web.ItemController类的skuItem方法变为如下代码

/**
 * 展示sku的详情
 * @param skuId
 * @return
 */
@GetMapping("/{skuId}.html")
public String skuItem(@PathVariable("skuId") Long skuId, Model model) throws ExecutionException, InterruptedException {

    System.out.println("准备查询" + skuId + "的详情");
    SkuItemVo vo = skuInfoService.item(skuId);
    model.addAttribute("item",vo);
    return "item";
}
image-20220813152705939
image-20220813152705939

http://item.gulimall.com页面里随便输一个关键词,然后点击搜索按钮,在http://search.gulimall.com/list.html?keyword=华为页面里,随便点一个商品,出现了异常

GIF 2022-8-3 10-02-11
GIF 2022-8-3 10-02-11

查看GulimallProductApplication服务的控制台,报了如下异常,原因说的很清除Prohibited package name: java.util.concurrent,包名不能以java开头

java.lang.SecurityException: Prohibited package name: java.util.concurrent
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655) ~[na:1.8.0_301]
    at java.lang.ClassLoader.defineClass(ClassLoader.java:754) ~[na:1.8.0_301]
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[na:1.8.0_301]
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468) ~[na:1.8.0_301]
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74) ~[na:1.8.0_301]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369) ~[na:1.8.0_301]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363) ~[na:1.8.0_301]
    at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_301]
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362) ~[na:1.8.0_301]
    at org.springframework.boot.devtools.restart.classloader.RestartClassLoader.findClass(RestartClassLoader.java:159) ~[spring-boot-devtools-2.1.8.RELEASE.jar:2.1.8.RELEASE]
    at org.springframework.boot.devtools.restart.classloader.RestartClassLoader.loadClass(RestartClassLoader.java:141) ~[spring-boot-devtools-2.1.8.RELEASE.jar:2.1.8.RELEASE]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ~[na:1.8.0_301]
    at com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl.item(SkuInfoServiceImpl.java:133) ~[classes/:na]
    at com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl$$FastClassBySpringCGLIB$$f4bab3b6.invoke(<generated>) ~[classes/:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.9.RELEASE.jar:5.1.9.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]
    at com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl$$EnhancerBySpringCGLIB$$204d922f.item(<generated>) ~[classes/:na]
    at com.atguigu.gulimall.product.web.ItemController.skuItem(ItemController.java:34) ~[classes/:na]
image-20220803100300583
image-20220803100300583

经过查找发现gulimall-common模块不知道怎么多了java.util.concurrent.CompletableFuturejava.util.concurrent.FutureTask,删掉重启gulimall-product模块就好了

image-20220804113632736
image-20220804113632736
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.0.0-alpha.8