第7章-CompletableFuture异步编排.md 39 KB

第7章-CompletableFuture异步编排

学习目标:

  • CompletableFuture异步任务应用场景
  • 掌握CompletableFuture相关API的应用
  • 基于CompletableFuture+自定义线程池实现优化商品数据接口调用
  • 基于CompletableFuture实现首页商品分类

1、CompletableFuture异步编排

问题:查询商品详情页的逻辑非常复杂,数据的获取都需要远程调用,必然需要花费更多的时间。

假如商品详情页的每个查询,需要如下标注的时间才能完成

  1. 获取sku的基本信息+sku的图片信息 1s
  2. 获取商品所属三级分类 0.5s
  3. 获取spu的所有销售属性 1s
  4. 商品sku价格 0.5s
  5. 获取商品海报列表 0.5s
  6. 获取商品Sku平台属性以及值 0.5s
  7. ......

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

1.1 CompletableFuture介绍

Future是Java 5添加的接口,用来描述一个异步计算的结果。你可以使用isDone方法检查计算是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。

在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。

CompletableFuture类实现了Future接口,所以你还是可以像以前一样通过get方法阻塞或者轮询的方式获得结果,但是这种方式不推荐使用。

CompletableFuture和FutureTask同属于Future接口的实现类,都可以获取线程的执行结果。

img

1.2 创建异步对象

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

img

没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。

  • runAsync方法不支持返回值。
  • supplyAsync可以支持返回值。

    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.TimeoutException;
    
    /**
    * @author: atguigu
    * @create: 2023-06-14 09:32
    */
    public class CompletableFutureTest {
    
    
    /**
     * 创建异步任务对象
     * @param args
     */
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        //1.创建异步任务-不需线程执行结果 CompletableFuture对象创建后 创建线程执行(默认自带线程池或者自定义线程池)
        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 不需要线程计算结果任务执行...");
        });
        //1.1 保证异步任务被执行,调用下面两个方法 join() 或者 get()方法
        future1.join();
        //future1.get();
    
    
        //2.创建异步任务-需线程执行结果
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "\t 需要获取线程计算结果.任务执行...");
                TimeUnit.SECONDS.sleep(5);
                return "atguigu";
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        //2.1 获取子线程执行结果
        //String s = future2.get();  //一致等待获取到线程结果
        //System.out.println(s);
        String s1 = future2.get(4, TimeUnit.SECONDS);  //一致等待获取到线程结果 ,超时抛出异常:TimeoutException
        System.out.println(s1);
    }
    
    }
    

1.3 计算完成时回调方法

当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:

img

  • whenComplete可以处理正常或异常的计算结果

  • exceptionally处理异常情况。BiConsumer<? super T,? super Throwable>可以定义处理业务

whenComplete 和 whenCompleteAsync 的区别

whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。

whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。

方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

代码示例:

/**
 * 异步任务完成后,继续执行回调方法
 *
 * @param args
 * @throws ExecutionException
 * @throws InterruptedException
 * @throws TimeoutException
 */
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
    //2.创建异步任务-需线程执行结果
    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        int i = 1 / 0;
        System.out.println(Thread.currentThread().getName() + "\t 需要获取线程计算结果.任务执行...");
        return "atguigu";
    });
    //whenComplete((上个任务执行结果,异常信息))
    //CompletableFuture<String> future3 = future2.whenCompleteAsync((t, u) -> {
    //    System.out.println(Thread.currentThread().getName() + "\t" + t);
    //    System.out.println(u);
    //});
    //exceptionally((上个任务异常信息))
    CompletableFuture<String> future3 = future2.exceptionally((t) -> {
        System.out.println(Thread.currentThread().getName() + "\t" + t);
        return "itguigu";
    });

    String s = future3.get();
    System.out.println(s);
}

1.4 线程串行化与并行化方法

thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。

img

thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。

img

thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行 thenRun的后续操作

img

带有Async默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。

Function<? super T,? extends U>

T:上一个任务返回结果的类型

U:当前任务的返回值类型

代码演示:

/**
 * 多组异步任务执行
 *  thenApply 当前任务依赖于上一个任务执行结果,当前任务返回结果(返回任务类型)
 * @param args
 */
public static void main(String[] args) throws ExecutionException, InterruptedException {
    //1.创建异步任务A 带返回结果异步任务对象
    CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
        log.info("模拟A线程执行业务.....");
        return "A";
    });
    //2.1 闯异步任务B 依赖异步A的结果 并且返回 B线程结果
    CompletableFuture<Long> futureB = futureA.thenApply((rA) -> {
        log.info("模拟B线程执行业务中需要获取A任务结果:{}", rA);
        //return rA + ":B";
        return 666L;
    });
    //2.2 闯异步任务C 依赖异步A的结果 并且返回 C线程结果
    CompletableFuture<String> futureC = futureA.thenApply(rA -> {
        log.info("模拟C线程执行业务中需要获取A任务结果:{}", rA);
        return rA + "C";
    });

    //3. 保证任务都执行调用异步任务对象方法 get 阻塞等待当前异步任务结果
    Long b = futureB.get();
    log.info("B线程执行结果:{}", b);
    String c = futureC.get();
    log.info("C线程执行结果:{}", c);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    //1.创建异步任务A 带返回结果异步任务对象
    CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
        log.info("模拟A线程执行业务.....");
        return "A";
    });
    //2.创建异步任务B 依赖A任务执行结果, B 无返回值
    CompletableFuture<Void> futureB = futureA.thenAccept(rA -> {
        log.info("模拟B线程执行业务中需要获取A任务结果:{}", rA);
    });
    //2.创建异步任务C 依赖A任务执行结果, C 无返回值
    CompletableFuture<Void> futureC = futureA.thenAccept(rA -> {
        log.info("模拟C线程执行业务中需要获取A任务结果:{}", rA);
    });

    CompletableFuture<Void> futureD = futureA.thenRun(() -> {
        log.info("模拟D线程(无法获取上一个任务结果,本任务不返回结果)执行...");
    });

    futureB.get();
    futureC.get();
    futureD.get();
}

1.5 多任务组合

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs);
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs);
  • allOf:等待所有任务完成
  • anyOf:只要有一个任务完成 注意:组合后要调用.join()方法

    /**
    * 多个异步任务组合+自定义线程池
    *
    * @param args
    * @throws ExecutionException
    * @throws InterruptedException
    * @throws TimeoutException
    */
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
    int processorsCount = Runtime.getRuntime().availableProcessors();
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            processorsCount * 2,
            processorsCount * 2,
            0,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(200),
            Executors.defaultThreadFactory(),
            //new ThreadPoolExecutor.CallerRunsPolicy()
            (runnable, executor)->{
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }
                executor.submit(runnable);
            }
    );
    
    //1.创建异步任务-需线程执行结果
    CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
        System.out.println(Thread.currentThread().getName() + "\t futureA任务执行...");
        return "atguigu";
    }, threadPoolExecutor);
    //2.创建异步任务B,依赖于任务A的执行结果,无结果返回
    CompletableFuture<Void> futureB = futureA.thenAcceptAsync((futureAResult) -> {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "\t futureB 任务执行...futureA结果:" + futureAResult);
    }, threadPoolExecutor);
    
    //3.创建异步任务C,依赖于任务A的执行结果,无结果返回
    CompletableFuture<Void> futureC = futureA.thenAcceptAsync((futureAResult) -> {
        System.out.println(Thread.currentThread().getName() + "\t futureC 任务执行...futureA结果:" + futureAResult);
    }, threadPoolExecutor);
    
    //所有任务全部执行完毕 主线程继续执行
    //CompletableFuture.allOf(futureA, futureB, futureC).join();
    //System.out.println("end");
    
    CompletableFuture.anyOf(futureA, futureB, futureC).join();
    System.out.println("end");
    
    threadPoolExecutor.shutdown();
    }
    

1.6 优化商品详情页

1.6.1. 自定义线程池

好处:

  • 效率高,提交任务,不需要等待线程创建,直接使用线程池中线程执行任务
  • 减少资源消耗,线程是稀缺资源
  • 统一管理线程,方便监控

service-item模块中新建包名:com.atguigu.gmall.item.config 新增线程池配置类:ThreadPoolConfig

package com.atguigu.gmall.common.config;

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

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

/**
 * @author: atguigu
 * @create: 2023-06-14 11:18
 */
@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        int processorsCount = Runtime.getRuntime().availableProcessors();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                processorsCount * 2,
                processorsCount * 2,
                0,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(200),
                Executors.defaultThreadFactory(),
                //new ThreadPoolExecutor.CallerRunsPolicy()
                (runnable, executor) -> {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                    }
                    executor.submit(runnable);
                }
        );
        //线程池创建,核心线程同时创建
        threadPoolExecutor.prestartCoreThread();
        //threadPoolExecutor.prestartAllCoreThreads();
        return threadPoolExecutor;
    }
}

经过测试实际注入的线程池对象为链路追踪自带的线程池,故需要再启动类上排除掉链路追踪自动装配类AsyncDefaultAutoConfiguration

package com.atguigu.gmall;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.sleuth.instrument.async.AsyncDefaultAutoConfiguration;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, AsyncDefaultAutoConfiguration.class})//取消数据源自动配置
@EnableDiscoveryClient
@EnableFeignClients  //默认扫描Feign接口包 启动类所在类所在包
public class ItemApp {
    public static void main(String[] args) {
        SpringApplication.run(ItemApp.class, args);
    }
}

1.6.2. 优化商品详情数据

service-item模块中ItemServiceImpl类中的getBySkuId方法进行优化

package com.atguigu.gmall.item.service.impl;

import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.item.service.ItemService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author: atguigu
 * @create: 2023-09-02 16:15
 */
@Slf4j
@Service
public class ItemServiceImpl implements ItemService {

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    /**
     * 汇总渲染商品详情页面所需要数据模型
     * 远程调用商品服务获取七项商品相关信息
     * ${categoryView}-三级分类
     * ${skuInfo}-商品sku信息
     * ${price}-商品价格
     * ${spuPosterList} 海报图片
     * ${skuAttrList} sku平台属性信息
     * ${spuSaleAttrList} 销售属性列表
     * ${valuesSkuJson} 切换商品SKU
     *
     * @param skuId
     * @return
     */
    @Override
    public Map<String, Object> getSkuInfo(Long skuId) {

        //0.查询布隆过滤器中是否包含查询商品skuId
        RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
        boolean exists = bloomFilter.contains(skuId);
        if (!exists) {
            log.error("[详情服务]访问商品不存在:{}", skuId);
            throw new RuntimeException("访问商品不存在:" + skuId);
        }

        //Map<String, Object> mapResult = new HashMap<>();  多线程并发写hashMap导致key被覆盖
        ConcurrentHashMap<String, Object> mapResult = new ConcurrentHashMap<>();
        //1.根据商品SkuId查询商品信息 skuinfo信息异步任务需要返回结果,提供给其他异步任务使用
        CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
            SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
            if (skuInfo == null) {
                throw new RuntimeException("远程调用商品不存在!");
            }
            mapResult.put("skuInfo", skuInfo);
            return skuInfo;
        }, threadPoolExecutor);


        //2.根据商品分类ID查询分类信息 获取商品信息异步任务结果,当前任务不需要返回结果
        CompletableFuture<Void> categoryViewCOmCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
            BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
            if (categoryView != null) {
                mapResult.put("categoryView", categoryView);
            }
        }, threadPoolExecutor);

        //3.根据商品SkuId查询商品价格
        CompletableFuture<Void> priceCompletableFuture = CompletableFuture.runAsync(() -> {
            BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
            if (skuPrice != null) {
                mapResult.put("price", skuPrice);
            }
        }, threadPoolExecutor);

        //4.根据商品SpuId查询海报图片列表
        CompletableFuture<Void> spuPosterListCOmCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
            List<SpuPoster> spuPosterList = productFeignClient.getSpuPosterBySpuId(skuInfo.getSpuId());
            if (!CollectionUtils.isEmpty(spuPosterList)) {
                mapResult.put("spuPosterList", spuPosterList);
            }
        }, threadPoolExecutor);

        //5.根据skuId查询商品平台属性列表
        CompletableFuture<Void> skuAttrListCompletableFuture = CompletableFuture.runAsync(() -> {
            List<BaseAttrInfo> baseAttrInfoList = productFeignClient.getAttrListBySkuId(skuId);
            if (!CollectionUtils.isEmpty(baseAttrInfoList)) {
                mapResult.put("skuAttrList", baseAttrInfoList);

            }
        }, threadPoolExecutor);

        //6.根据skuId,spuId获取所有销售属性,带选中当前sku销售属性值
        CompletableFuture<Void> spuSaleAttrListCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
            List<SpuSaleAttr> spuSaleAttrListCheckBySku = productFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
            if (!CollectionUtils.isEmpty(spuSaleAttrListCheckBySku)) {
                mapResult.put("spuSaleAttrList", spuSaleAttrListCheckBySku);
            }
        }, threadPoolExecutor);

        //7.切换商品SKU字符串
        CompletableFuture<Void> valuesSkuJsonCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
            String changeSkuValueIdsString = productFeignClient.getChangeSkuValueIdsMap(skuInfo.getSpuId());
            if (StringUtils.isNotBlank(changeSkuValueIdsString)) {
                mapResult.put("valuesSkuJson", changeSkuValueIdsString);
            }
        }, threadPoolExecutor);

        //x.组合以上七个异步任务
        CompletableFuture.allOf(
                skuInfoCompletableFuture,
                categoryViewCOmCompletableFuture,
                spuPosterListCOmCompletableFuture,
                spuSaleAttrListCompletableFuture,
                valuesSkuJsonCompletableFuture,
                priceCompletableFuture,
                skuAttrListCompletableFuture
        ).join();
        return mapResult;
    }
}

1.7 SpringBoot线程池(了解)

Springboot线程池实现关键类:ThreadPoolTaskExecutor默认线程池实现,为什么不采用Springboot默认线程池?

private int maxPoolSize = Integer.MAX_VALUE;
private int queueCapacity = Integer.MAX_VALUE;

原因:最大线程数跟阻塞队列长度Integer最大值,存在OOM风险.

一般Springboot应用采用自定义线程池:配置自定义线程池

  1. application.yml 定义线程池相关变量:

    # 线程池配置参数
    task:
    pool:
    corePoolSize: 10 # 设置核心线程数
    maxPoolSize: 20  # 设置最大线程数
    keepAliveTime: 300 # 设置空闲线程存活时间(秒)
    queueCapacity: 100 # 设置队列容量
    threadNamePrefix: "gmall-item-" # 设置线程名称前缀
    awaitTerminationSeconds: 60 #  设置线程池等待终止时间(秒)
    spring:
    main:
    allow-bean-definition-overriding: true
    
  2. 定义属性类读取配置信息

    package com.atguigu.gmall.item.config;
       
       
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
       
    /**
    * 线程配置属性类
    **/
    @Data
    @ConfigurationProperties(prefix = "task.pool")
    public class TaskThreadPoolConfig {
       private int corePoolSize;
       private int maxPoolSize;
       private int keepAliveSeconds;
       private int queueCapacity;
       private int awaitTerminationSeconds;
       private String threadNamePrefix;
    }
    
  3. 注册springboot线程池对象

    package com.atguigu.gmall.item.config;
       
       
       
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import org.springframework.stereotype.Component;
       
    import java.util.concurrent.Executor;
    import java.util.concurrent.ThreadPoolExecutor;
       
    /**
    * 创建自定义线程池配置类
    *
    * @Author
    * @Date 2022-04-05 17:26
    **/
    @EnableAsync
    @Configuration
    @EnableConfigurationProperties({TaskThreadPoolConfig.class})//开启配置属性支持
    public class AsyncScheduledTaskConfig {
       
       @Autowired
       private TaskThreadPoolConfig config;
       
       /**
        * 1.这种形式的线程池配置是需要在使用的方法上面添加@Async("customAsyncThreadPool")注解的
        * 2。如果在使用的方法上不添加该注解,那么spring就会使用默认的线程池
        * 3.所以如果添加@Async注解但是不指定使用的线程池,又想自己自定义线程池,那么就可以重写spring默认的线程池
        * 4.所以第二个方法就是重写spring默认的线程池
        *
        * @return
        */
       @Bean("customAsyncThreadPool")
       public Executor customAsyncThreadPool() {
           ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
           //最大线程数
           executor.setMaxPoolSize(config.getMaxPoolSize());
           //核心线程数
           executor.setCorePoolSize(config.getCorePoolSize());
           //任务队列的大小
           executor.setQueueCapacity(config.getQueueCapacity());
           //线程池名的前缀
           executor.setThreadNamePrefix(config.getThreadNamePrefix());
           //允许线程的空闲时间30秒
           executor.setKeepAliveSeconds(config.getKeepAliveSeconds());
           //设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
           executor.setWaitForTasksToCompleteOnShutdown(true);
           //设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住
           executor.setAwaitTerminationSeconds(config.getAwaitTerminationSeconds());
       
           /**
            * 拒绝处理策略
            * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
            * AbortPolicy():直接抛出异常。
            * DiscardPolicy():直接丢弃。
            * DiscardOldestPolicy():丢弃队列中最老的任务。
            */
           /**
            * 特殊说明:
            * 1. 这里演示环境,拒绝策略咱们采用抛出异常
            * 2.真实业务场景会把缓存队列的大小会设置大一些,
            * 如果,提交的任务数量超过最大线程数量或将任务环缓存到本地、redis、mysql中,保证消息不丢失
            * 3.如果项目比较大的话,异步通知种类很多的话,建议采用MQ做异步通知方案
            */
           executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
           //线程初始化
           executor.initialize();
           return executor;
       }
    }
    
  4. 异步方法上加注解 @Async

    package com.atguigu.gmall.item.service;
    
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Service;
    
    /**
    * @author: atguigu
    * @create: 2023-06-14 11:48
    */
    @Service
    public class TestAsyncService {
    /**
     * 调用其他业务方法,单个简单异步任务
     *
     * @return
     */
    @Async("customAsyncThreadPool")
    public String async() {
        System.out.println(Thread.currentThread().getName() + "\t异步方法执行了");
        return "a";
    }
    }
    

2、首页商品分类实现

img

前面做了商品详情,我们现在来做首页分类,我先看看京东的首页分类效果,我们如何实现类似效果:

img

思路:

1,首页属于并发量比较高的访问页面,我看可以采取页面静态化方式实现,或者把数据放在缓存中实现

2,我们把生成的静态文件可以放在nginx访问或者放在web-index模块访问

2.1 修改pom.xml

web-all模块中新增商品服务依赖

<dependency>
    <groupId>com.atguigu.gmall</groupId>
    <artifactId>service-product-client</artifactId>
    <version>1.0</version>
</dependency>

2.2 封装数据接口

由于商品分类信息在service-product模块,我们在该模块封装数据,数据结构为父子层级

商品分类保存在base_category1、base_category2和base_category3表中,由于需要静态化页面,我们需要一次性加载所有数据,前面我们使用了一个视图base_category_view,所有我从视图里面获取数据,然后封装为父子层级

数据结构如下:json 数据结构

[
    {
        "index":1,     #序号
        "categoryName":"图书、音像、电子书刊",   #一级分类名称
        "categoryId":1,                       #一级分类ID
        "categoryChild":[                     #当前一级分类包含的二级分类集合
            {
                "categoryName":"电子书刊",     #二级分类名称
                "categoryId":1,               #二级分类ID
                "categoryChild":[             #当前二级分类包含的三级分类集合
                    {
                        "categoryName":"电子书",#三级分类名称
                        "categoryId":1         #三级分类ID
                    },
                    {
                        "categoryName":"网络原创",
                        "categoryId":2
                    }
                ]
            },
            //当前一级分类下 其他二级分类对象
        ]
    },
    {
        "index":2,
        "categoryName":"手机",
        "categoryId":2,
        "categoryChild":[
            {
                "categoryName":"手机通讯",
                "categoryId":13,
                "categoryChild":[
                    {
                        "categoryName":"手机",
                        "categoryId":61
                    }
                ]
            },
            {
                "categoryName":"运营商",
                "categoryId":14
            },
            {
                "categoryName":"手机配件",
                "categoryId":15
            }
        ]
    },
  //..{}其他的一级分类
]

2.2.1 控制器

YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/651

service-product模块中ProductApiController

/**
 * 查询首页所有分类集合-包含1,2,3级分类
 *
 * @return
 */
@ApiOperation("查询首页所有分类集合-包含1,2,3级分类")
@GetMapping("/inner/getBaseCategoryList")
public List<JSONObject> getBaseCategoryList() {
    return baseCategoryViewService.getBaseCategoryList();
}

2.2.2 BaseCategoryViewService接口

package com.atguigu.gmall.product.service;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
 * VIEW 业务接口类
 * @author atguigu
 * @since 2023-09-02
 */
public interface BaseCategoryViewService extends IService<BaseCategoryView> {

    /**
     * 查询首页所有分类集合-包含1,2,3级分类
     *
     * @return
     */
    List<JSONObject> getBaseCategoryList();
}

2.2.3 BaseCategoryViewServiceImpl实现类

package com.atguigu.gmall.product.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.common.redis.GmallCache;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.mapper.BaseCategoryViewMapper;
import com.atguigu.gmall.product.service.BaseCategoryViewService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * VIEW 业务实现类
 *
 * @author atguigu
 * @since 2023-09-02
 */
@Service
public class BaseCategoryViewServiceImpl extends ServiceImpl<BaseCategoryViewMapper, BaseCategoryView> implements BaseCategoryViewService {

    /**
     * 查询首页所有分类集合-包含1,2,3级分类
     *
     * @return
     */
    @Override
    @GmallCache(prefix = "baseCategoryList:")
    public List<JSONObject> getBaseCategoryList() {
        List<JSONObject> listResult = new ArrayList<>();
        try {
            //1.查询数据库中"分类视图-封装所有分类数据"得到所有分类集合
            //1.1 调用分类视图业务层查询到所有910条分类数据
            List<BaseCategoryView> allCategoryList = this.list();

            if (!CollectionUtils.isEmpty(allCategoryList)) {
                //1.2 根据分类对象中一级分类ID进行分组,得到所有一级分类  分组后Map的Key:一级分类ID  分组后Map的Value:当前一级分类集合  Map长度就是一级分类数量
                Map<Long, List<BaseCategoryView>> category1ListMap =
                        allCategoryList.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory1Id));

                //1.3 遍历一级分类Map 每遍历一次就处理一个一级分类
                for (Map.Entry<Long, List<BaseCategoryView>> entry : category1ListMap.entrySet()) {
                    //2.处理集合获取所有一级分类
                    //2.1 新创建1级分类对象 分组1级分类对应 categoryId,categoryName,categoryChild
                    JSONObject category1 = new JSONObject();
                    //2.2 获取1级分类ID-Map的Key,为1级分类对象分类ID赋值
                    Long category1Id = entry.getKey();
                    category1.put("categoryId", category1Id);

                    //2.3 获取1级分类集合-Map中Value,为1级分类对象分类名称赋值
                    String category1Name = entry.getValue().get(0).getCategory1Name();
                    category1.put("categoryName", category1Name);

                    //3.在一级分类集合中处理二级分类-将二级分类集合放入一级分类对象中categoryChild属性中
                    //3.1 将以及分类集合按照二级分类ID进行分组 得到包含二级分类信息Map集合
                    Map<Long, List<BaseCategoryView>> category2ListMap = entry.getValue().stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory2Id));
                    //3.2 遍历包含二级分类信息Map集合
                    List<JSONObject> category2List = new ArrayList<>();
                    for (Map.Entry<Long, List<BaseCategoryView>> entry2 : category2ListMap.entrySet()) {
                        //3.2.1 构建二级分类对象
                        JSONObject category2 = new JSONObject();
                        //3.2.2 封装二级分类ID
                        Long category2Id = entry2.getKey();
                        category2.put("categoryId", category2Id);
                        //3.2.3 封装二级分类名称
                        String category2Name = entry2.getValue().get(0).getCategory2Name();
                        category2.put("categoryName", category2Name);
                        //4.处理三级分类-将三级分类集合放入二级分类对象中categoryChild属性中
                        //4.1 遍历二级分类集合 获取分类对象中三级分类ID跟名称
                        List<JSONObject> category3List = new ArrayList<>();
                        for (BaseCategoryView baseCategoryView : entry2.getValue()) {
                            JSONObject category3 = new JSONObject();
                            category3.put("categoryId", baseCategoryView.getCategory3Id());
                            category3.put("categoryName", baseCategoryView.getCategory3Name());
                            category3List.add(category3);
                        }
                        category2.put("categoryChild", category3List);

                        //3.3 将二级分类装入二级分类集合中
                        category2List.add(category2);

                    }
                    //3.4 将二级分类集合放入一级分类对象中
                    category1.put("categoryChild", category2List);
                    listResult.add(category1);
                }
            }
            //5.返回经过处理所有一级分类集合
            return listResult;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

2.3 service-product-client添加接口

service-product-client模块中ProductFeignClient,提供远程调用FeignAPI接口以及服务降级方法

/**
 * 查询首页所有分类集合-包含1,2,3级分类
 *
 * @return
 */
@ApiOperation("查询首页所有分类集合-包含1,2,3级分类")
@GetMapping("/inner/getBaseCategoryList")
public List<JSONObject> getBaseCategoryList();
@Override
public List<JSONObject> getBaseCategoryList() {
    log.error("[商品服务],getBaseCategoryList业务远程调用失败,执行了服务降级");
    return null;
}

2.4 页面渲染

第一种缓存渲染方式

web-all模块中编写控制器

package com.atguigu.gmall.web.controller;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.product.client.ProductFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

/**
 * @author: atguigu
 * @create: 2023-09-08 11:48
 */
@Controller
public class IndexHtmlController {

    @Autowired
    private ProductFeignClient productFeignClient;

    /**
     * 渲染门户首页
     *
     * @param model
     * @return
     */
    @GetMapping({"/", "/index.html"})
    public String indexHtml(Model model) {
        //1.TODO 首页中间轮播图广告
        //2.TODO 秒杀商品列表(部分)
        //3.TODO 频道广场(购买商品入口)
        //4.TODO 推荐商品列表
        //5.TODO 热点商品关键词
        //6.商城分类数据(一、二、三级分类) 如果存在多项业务数据远程获取,采用CompletableFuture+线程池优化 为并行
        List<JSONObject> list = productFeignClient.getBaseCategoryList();
        model.addAttribute("list", list);
        return "/index/index";
    }

}

第二种方式nginx做静态代理方式:

  1. 生成静态文件

    @Autowired
    private TemplateEngine templateEngine;
    
    
    /**
    * 将首页进行保存html文件到磁盘
    */
    @GetMapping("createIndexHtml")
    @ResponseBody
    public void createIndexHtml() {
    List<JSONObject> list = productFeignClient.getBaseCategoryList();
    //1.模板文件名称
    String indexName = "/index/index";
    //2.封装模板所属数据
    Context context = new Context();
    context.setVariable("list", list);
    //3.写文件对象
    FileWriter fileWriter = null;
    try {
        fileWriter = new FileWriter("D:\\tmp\\index.html");
        //x.将模板业务所需要数据跟模板进行合并产生html文件,将文件保存--
        templateEngine.process(indexName, context, fileWriter);
        fileWriter.close();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    }
    
  2. 解压课后资料中nginx压缩 不要中文空格

  3. 将静态文件拷贝到nginx/html目录下 包含js,css等文件夹

image-20230104161748678

  1. 启动Nginx服务

  2. 访问首页

image-20230104161906057

Nginx反向代理配置-了解

  1. 启动nginx,nginx目录下打开命令行

    start nginx
    
  2. 关闭nginx

    nginx -s stop
    
  3. 重新加载nginx配置文件

    nginx -s reload
    
  4. nginx.conf配置文件

    	#配置集群列表 默认负载均衡策略为轮询
    	upstream gatewayUpstream {
    	   server 127.0.0.1:80 weight=3;
    	   server 127.0.0.1:81 weight=1;
    	   server 127.0.0.1:82 weight=1;
    	}
       	
       server {
           listen       88;
           server_name  localhost;
       
           #charset koi8-r;
       
           #access_log  logs/host.access.log  main;
    		#配置监听的请求路径
           location / {
               #root   html;
               #index  index.html index.htm;
               #将请求地址以"/"开头的全部反向代理到网关集群服务列表
    			proxy_pass http://gatewayUpstream;
           }
    }
    
  5. 后端的网关服务要搭建集群

image-20230425161313653

  1. 通过Nginx访问服务测试 http://localhost:88/api/product/inner/getBaseCategoryList

    image-20230104162327323