商品详情异步与首页
商品详情的异步
问题的引出
到目前位置,我们已经完成了获取商品详情的功能,代码如下:
public ProductDetailDTO getItemBySkuId(Long skuId) {
ProductDetailDTO productDetailDTO = new ProductDetailDTO(); // 1. 获取sku基本信息 + 图片列表 SkuInfoDTO skuInfo = skuService.getSkuInfo(skuId); if (skuInfo.getId() == null) { return productDetailDTO; } productDetailDTO.setSkuInfo(skuInfo);
// 2. 获取SKU所属的SPU中包含的销售属性及销售属性值(包含目标sku的销售属性值) List<SpuSaleAttributeInfoDTO> spuSaleAttrListCheckBySku = skuService.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId()); productDetailDTO.setSpuSaleAttrList(spuSaleAttrListCheckBySku);
// 3. 获取 销售属性值组合 与 skuId对应关系的 Json字符串 Map<String, Long> skuValueIdsMap = spuService.getSkuValueIdsMap(skuInfo.getSpuId()); String skuValueIdsJson = JSON.toJSONString(skuValueIdsMap); productDetailDTO.setValuesSkuJson(skuValueIdsJson);
// 4. 获取商品价格 BigDecimal skuPrice = skuService.getSkuPrice(skuId); productDetailDTO.setPrice(skuPrice);
// 5. 获取sku对应三级类目 CategoryHierarchyDTO categoryView = categoryService.getCategoryViewByCategoryId(skuInfo.getThirdLevelCategoryId()); productDetailDTO.setCategoryHierarchy(categoryView);
// 6. 查询海报集合 List<SpuPosterDTO> spuPosterBySpuId = spuService.findSpuPosterBySpuId(skuInfo.getSpuId()); productDetailDTO.setSpuPosterList(spuPosterBySpuId);
// 7. 查询SKU商品的规格参数(平台属性 对应的那个 平台属性值) List<PlatformAttributeInfoDTO> platformAttrInfoBySku = skuService.getPlatformAttrInfoBySku(skuId); List<SkuSpecification> skuSpecifications = platformAttrInfoBySku.stream().map(platformAttributeInfoDTO -> { SkuSpecification skuSpecification = new SkuSpecification(); // 获取平台属性名称 String attrName = platformAttributeInfoDTO.getAttrName(); // 设置平台属性名称 skuSpecification.setAttrName(attrName); // 获取平台属性值的名称 String valueName = platformAttributeInfoDTO.getAttrValueList().get(0).getValueName();
// 设置平台属性值的名称 skuSpecification.setAttrValue(valueName); return skuSpecification; }).collect(Collectors.toList()); productDetailDTO.setSkuAttrList(skuSpecifications);
return productDetailDTO; }稍微思考以下,从执行效率上来看,以上的代码是否还有优化的空间呢?回答当然是肯定的
sku基本信息,spu销售属性值集合,sku_id与其对应的销售属性值组合,…,这些商品详情页中的各项数据,目前我们是依次先后获取的。但是,如果可以可以分别同时获取,那么获取数据的效率肯定会大大提升。于是,我们马上就想到了线程池,代码改造如下:
// 创建线程池ExecutorService executor = ...;
// 向线程池中提交异步任务executor.submit(获取skuInfo的异步任务);executor.submit(获取sku对应的spu销售属性值集合的异步任务);....看起来似乎非常的简单,但是仔细一想,又有两个难以解决的问题:
- 获取详情页数据的这些异步任务之间,并非完全没有关系,它们是有明显的逻辑顺序的

- 我们需要等待所有的异步任务都执行完毕,将数据封装到ProductDetailDTO对象中,从而给前端返回完整的详情页数据

// 创建线程池ExecutorService executor = ...;
// 向线程池中提交异步任务executor.submit(获取skuInfo的异步任务);executor.submit(获取sku对应的spu销售属性值集合的异步任务);....// 即使不考虑异步任务之间的先后逻辑关系,也需要在最后等待所有异步任务执行完毕,返回ProductDetailDTO对象以上两个问题,我们如何解决呢?这就涉及到了异步任务的编排问题了,通过JDK8引入的CompletableFuture就可以帮我们实现异步任务的编排。
CompletableFuture
一个CompletableFuture对象主要有两个功能:
- 表示一个Future对象,即可以表示一个异步任务的执行结果
- 可以实现多个异步任务之间的功能依赖,对多个异步任务之间进行”组合”。
什么是组合呢?比如”异步任务A需要在异步任务B之后执行,那么可以通过CompletableFuture实现任务A执行完毕后,自动触发任务B的执行”

如上图所示,这里描绘的是一个业务接口的流程,其中包括CF1\CF2\CF3\CF4\CF5\CF6 共6个步骤,并描绘了这些步骤之间的依赖关系,每个步骤可以是一次数据库操作或者是一次本地方法调用,或者是一次服务间的调用等,在使用CompletableFuture进行异步化编程时,图中的每个步骤都会产生一个CompletableFuture对象,最终结果也会用一个CompletableFuture来进行表示。
根据CompletableFuture依赖数量,可以分为以下几类:零依赖、一元依赖、二元依赖和多元依赖。
零依赖
零依赖,其实也就是CompletableFuture对象的创建,我们可以先来看看如何不依赖其他的CompletableFuture对象来创建新的CompletableFuture。

如上图CF1和CF2的创建,其实就是直接创建CompletableFuture对象。
runAsync-无返回值
第一种方式是获取一个表示无返回值异步任务执行结果的CompletableFuture对象
接口
// 获取一个异步任务CompletableFuture对象// 这里没有指定线程池,默认会把当前任务交给CompletableFuture内部的线程池来执行static CompletableFuture<Void> runAsync(Runnable runnable)
// 在指定的线程池中,执行下一个异步任务static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)使用案例
// runAsync案例CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> System.out.println("hello,world"));
// 创建线程池Executor executor = ...CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> System.out.println("hello,world"), executor);注意事项
- CompletableFuture
,泛型类型为Void,表示异步任务没有执行结果 - 一个异步任务虽然没有返回值,但是会有执行状态,而这个执行状态,也被封装在CompletableFuture
对象中,因此我们可以做如下操作
// 异步任务是否被取消了boolean isCancelled = CompletableFuture.isCancelled();// 是否在执行过程中出现了异常boolean isExceptional = CompletableFuture.isCompletedExceptionally();// 是否正常执行完毕了boolean done = CompletableFuture.isDone();supplyAsync-有返回值
第二种方式是获取一个表示有返回值的异步任务执行结果的CompletableFuture对象、
接口
/* Supplier<U> supplier 提供者接口,产生一个结果 CompletableFuture<U> 表示supplier在子线程中执行的结果,包含U类型的对象*/static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 在指定的线程池中,执行下一个异步任务static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)public interface Supplier<T> { T get();}使用案例
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> "hello,world");// 在当前线程中,获取异步任务执行的结果String result = completableFuture.get();
Executor executor = ...CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> "hello,world", executor);
// 在当前线程中,获取异步任务执行的结果String result = completableFuture.get();注意事项
- CompletableFuture
, 泛型类型为String,表示异步任务最终返回的结果是一个String对象 - CompletableFuture
中除了封装异步任务执行的状态,还封装了异步任务的执行结果,可以通过如下方式获取
// CompletableFuture的泛型是什么类型,get方法获取的就是什么类型的值,若为Void,则get的结果是nullString result = CompletableFuture.get();- 关于CompletableFuture.get()方法,是一个阻塞方法,会阻塞调用线程,直到CompletableFuture对象所代表的的异步任务执行结束。
一元依赖

如上图所示,CF3和CF5都需要依赖于前一个CompletableFuture对象。
thenRun & thenRunAsync
接口
// thenRun方法:在当前异步任务结束后,触发下一个异步任务(thenRun方法传递的参数)无参且没有返回值的异步任务CompletableFuture<Void> thenRun(Runnable action)
// thenRunAync方法:方法参数和执行结果同thenRun,区别是thenRunAsync方法传递的异步任务所执行的线程是一个新的线程.// thenRunAsync方法还有一个重载方法,executor指定下一个异步任务执行所使用的线程池CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)使用案例
// 异步执行第一个异步任务,得到代表第一个异步任务执行结果的CompletableFuture<Void>对象CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> System.out.println("first"));
// 指定在当前任务(completableFuture代表当前的异步任务)执行完毕后,执行下一个异步任务CompletableFuture thenCompletableFuture = completableFuture.thenRun(() -> System.out.println("second"));
// 在当前线程中等待执行完毕(join并非必须, 只是上课演示的需要)thenCompletableFuture.join();或者我们可以使用链式调用的方式,重新书写以上的代码(效果等效),实际开发中我们通常习惯使用链式调用方式
CompletableFuture.runAsync(() -> System.out.println("first")) // 指定下一个要执行的异步任务 .thenRun(() -> System.out.println("second")).join();thenAccept & thenAcceptAsync
接口
thenAccept方法: 在当前异步任务执行结束后,触发下一个异步任务的执行,且下一个异步任务,接收当前异步任务的结果作为参数执行,下一个异步任务无返回值
/* 参数: action的accept方法的参数表示下一个异步任务所接受的当前异步任务执行的结果 返回值: CompletableFuture<Void>表示下一个异步任务的执行结果无返回值*/CompletableFuture<Void> thenAccept(Consumer<? super T> action)
// thenAcceptAsync方法: 方法参数和执行结果同thenAccept,区别是thenAcceptAsync方法传递的异步任务所执行的线程是一个新的线程CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
// thenAcceptAsync还有一个重载方法,executor指定下一个异步任务执行所使用的线程池CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)public interface Consumer<T> {
void accept(T t);}CompletableFuture.supplyAsync(() -> "hello,world") // 下一个异步任务接收当前异步任务的结果并处理 .thenAccept(r -> System.out.println(r.length()));使用案例
Executor executor = ...
CompletableFuture.supplyAsync(() -> { // 输出线程名称 System.out.println(Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } return "hello,world"; }, executor).thenAcceptAsync(s -> { // 输出线程名称 System.out.println(Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(s.length()); }, executor).join();thenApply & thenApplyAsync
接口
thenApply方法: 在当前异步任务执行结束后,触发下一个异步任务的执行,下一个异步任务接收当前异步任务的结果作为参数,且下一个异步任务有自己的返回值
thenApplyAsync方法:方法参数和执行结果同thenApply,区别是thenApplyAsync方法传递的异步任务所执行的线程是一个新的线程
/* 参数: Function的apply方法第一个参数表示接收的当前异步的结果 Function的apply方法返回值表示下一个异步任务的结果 返回值: CompletableFuture<U>表示下一个异步任务的执行结果包含下一个异步任务的返回值*/<U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
// 把异步任务交给默认的线程池来执行<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
// 把异步任务交给指定的线程池来执行CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)public interface Function<T, R> { // 将一个参数值转化为另一个类型的值并返回 R apply(T t);}使用案例
// thenApply案例CompletableFuture<String> completableFuture = CompletableFuture .supplyAsync(() -> new Random().nextInt(1000)) // 下一个异步任务接收当前异步任务的结果并返回处理结果 .thenApply((r) -> r % 2 == 0 ? "奇数" : "偶数");// 通过completableFuture获取异步任务返回的结果String result = completableFuture.get();// 输出结果System.out.println(result);
// thenApplyAsync案例CompletableFuture<String> completableFutureResult = CompletableFuture.supplyAsync(() -> { try { //休眠三秒中 TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } // 返回随机数 return new Random().nextInt(1000);})// 下一个异步任务接收当前异步任务的结果并返回处理结果.thenApplyAsync((r) -> r % 2 == 0 ? "奇数" : "偶数");String result = completableFuture.get();System.out.println(result);
// thenApplyAsync案例 Executor executor = ...;
CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(() -> { // 输出线程名称 System.out.println(Thread.currentThread().getName()); try { //休眠三秒中 TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } int i = new Random().nextInt(1000); System.out.println(i); // 返回随机数 return i;}, executor) // 下一个异步任务接收当前异步任务的结果并返回处理结果 .thenApplyAsync(v -> { // 输出线程名称 System.out.println(Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } return v % 2 == 0 ? "偶数" : "奇数"; }, executor);
String s = stringCompletableFuture.get();System.out.println(s);二元依赖

如上图红色链路所示,CF4同时依赖于两个CF1和CF2,这种二元依赖可以通过thenCombine等方法来实现,如下代码所示
thenCombine & thenCombineAsync
接口
/* 参数: other: 待合并的另一个异步任务 T: 第一个任务的返回结果 U: 第二个任务的返回结果 fn: 组合之后的返回类型 返回值: CompletableFuture<U>表示下一个异步任务的执行结果包含下一个异步任务的返回值*/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)@FunctionalInterfacepublic interface BiFunction<T, U, R> { // T 第一个任务的返回结果 // U 第二个任务的返回结果 // R 组合之后的返回类型 R apply(T t, U u);}使用
CompletableFuture<String> firstCompletableFuture = CompletableFuture.supplyAsync(() -> "first"); //创建第一个异步任务CompletableFuture<String> secondCompletableFuture = CompletableFuture.supplyAsync(() -> "second"); // 创建第二个异步任务
// 组合第一个和第二个异步任务的结果CompletableFuture<String> combineCompletableFuture = firstCompletableFuture.thenCombine(secondCompletableFuture, (firstRet, secondRet) -> firstRet + secondRet);
// 获得结果String ret = combineCompletableFuture.get();多元依赖

如上图红色链路所示,整个流程的结束依赖于三个步骤CF3、CF4、CF5,这种多元依赖可以通过allOf或anyOf方法来实现,区别是当需要多个依赖全部完成时使用allOf,当多个依赖中的任意一个完成即可时使用anyOf。
allOf
allOf方法: 可以将多个异步任务合并为一个新的异步任务,当且仅当所有的异步任务都执行完,新的异步任务才算执行完
/* 参数表示待合并的多个异步任务 返回值为CompletableFuture<Void>类型,说明合并之后的异步任务不包含任何返回值*/CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)User user = new User();
// 第一个异步任务CompletableFuture<Void> cf1 = CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } user.setId(1001);});
// 第二个异步任务CompletableFuture<Void> cf2 = CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } user.setName("张三");});
// 第三个异步任务CompletableFuture<Void> cf3 = CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } user.setAge(30);});
// 组合,等待所有的异步任务执行完CompletableFuture<Void> cfAll = CompletableFuture.allOf(cf1, cf2, cf3);// 可以调用join方法阻塞到所有任务都执行完毕cfAll.join();System.out.println(user);anyOf (了解)
allOf方法: 可以将多个异步任务合并为一个新的异步任务,其中只要有一个任务执行完,新的异步任务就算执行完,且会将该任务的执行结果,作为新的异步任务的结果
/* 参数表示待合并的多个异步任务 返回值为CompletableFuture<Object> 合并之后的异步任务的结果,同先执行完的那个异步任务的结果*/CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)注意:如果这些待合并的异步任务中,同时有多个异步任务执行完毕,那么合并之后的新的异步任务的执行结果就是不确定的,具有很大的不确定性,所以实际开发中很少用。
异常处理
CompletableFuture还给我们提供了一个专门用来处理异常的方法:exceptionally
/* 注意,该方法当且仅当 当前异步任务出现异常的时候才会执行 作用: 通过执行fn,将当前异步任务的异常转化为某一个默认值返回 返回值:异常处理的结果,即当发生异常时所返回的默认值,默认值的数据类型同当前异步任务的返回值类型*/CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)CompletableFuture<int[]> exceptionally = CompletableFuture.supplyAsync(() -> { int[] a = null; // 数组越界异常 System.out.println(a[3]); return a; })// 当前异步任务返回数组出现了异常,则就返回长度为0的数组.exceptionally(e -> new int[0]);
int[] ints = exceptionally.get();System.out.println(ints.length);项目中的使用
在1.1的问题引出中,我们所遇到的问题,其实都可以通过CompletableFuture的异步编排来实现。
ProductDetailDTO productDetailDTO = new ProductDetailDTO();
// 通过skuId 查询skuInfoCompletableFuture<SkuInfoDTO> skuCompletableFuture = CompletableFuture.supplyAsync(() -> { SkuInfoDTO skuInfo = skuService.getSkuInfo(skuId); // 保存skuInfo productDetailDTO.setSkuInfo(skuInfo); return skuInfo;}, threadPoolExecutor);
// 销售属性-销售属性值回显并锁定 thenAcceptAsync 串行化CompletableFuture<Void> spuSaleAttrCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuInfo -> { List<SpuSaleAttributeInfoDTO> spuSaleAttrListCheckBySku = skuService.getSpuSaleAttrListCheckBySku(skuInfo.getId(), skuInfo.getSpuId());
// 保存数据 productDetailDTO.setSpuSaleAttrList(spuSaleAttrListCheckBySku);}, threadPoolExecutor);
//根据spuId 查询map 集合属性// 销售属性-销售属性值回显并锁定 thenAcceptAsync 串行化CompletableFuture<Void> skuValueIdsMapCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuInfo -> { Map<String, Long> skuValueIdsMap = spuService.getSkuValueIdsMap(skuInfo.getSpuId());
String valuesSkuJson = JSON.toJSONString(skuValueIdsMap); // 保存valuesSkuJson productDetailDTO.setValuesSkuJson(valuesSkuJson);
}, threadPoolExecutor);
//获取商品最新价格CompletableFuture<Void> skuPriceCompletableFuture = CompletableFuture.runAsync(() -> { BigDecimal skuPrice = skuService.getSkuPrice(skuId); productDetailDTO.setPrice(skuPrice);
}, threadPoolExecutor);
//获取分类信息 thenAcceptAsync 串行化CompletableFuture<Void> categoryViewCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuInfo -> { CategoryHierarchyDTO categoryViewByCategory = categoryService.getCategoryViewByCategoryId(skuInfo.getThirdLevelCategoryId()); //分类信息 productDetailDTO.setCategoryHierarchy(categoryViewByCategory);
}, threadPoolExecutor);
// 获取海报数据 thenAcceptAsync 串行化CompletableFuture<Void> spuPosterListCompletableFuture = skuCompletableFuture.thenAcceptAsync(skuInfo -> { // spu海报数据 List<SpuPosterDTO> spuPosterBySpuId = spuService.findSpuPosterBySpuId(skuInfo.getSpuId()); productDetailDTO.setSpuPosterList(spuPosterBySpuId);
}, threadPoolExecutor);
// 获取sku平台属性,即规格数据CompletableFuture<Void> skuAttrListCompletableFuture = CompletableFuture.runAsync(() -> { List<PlatformAttributeInfoDTO> platformAttrInfoBySku = skuService.getPlatformAttrInfoBySku(skuId); List<SkuSpecification> skuAttrList = platformAttrInfoBySku.stream().map((baseAttrInfo) -> { // SkuSpecification skuSpecification = new SkuSpecification(); skuSpecification.setAttrName(baseAttrInfo.getAttrName()); skuSpecification.setAttrValue(baseAttrInfo.getAttrValueList().get(0).getValueName()); return skuSpecification; }).collect(Collectors.toList()); productDetailDTO.setSkuAttrList(skuAttrList);}, threadPoolExecutor);
// 等待所有异步任务执行完毕CompletableFuture.allOf(skuCompletableFuture, spuSaleAttrCompletableFuture, skuValueIdsMapCompletableFuture, skuPriceCompletableFuture, categoryViewCompletableFuture, spuPosterListCompletableFuture, skuAttrListCompletableFuture).join();return productDetailDTO;通过CompletableFuture的异步编排功能,既可以实现异步,还可以保证异步任务的执行流程,在保证正确性的前提下,提升商品详情页数据的获取效率。
首页功能

在电商页面首页,我们会展示电商网站中所有的一级商品类目,一级商品类目对应的二级商品类目,二级商品类目对应的三级商品类目。

所以我们必须在访问首页时,查询到所有商品类目,一级对应的层级关系,具体的请求链路如下:

首页返回的完整的三级类目及层级关系的json数据
[ { "categoryId": 1, "categoryName": "图书、音像、电子书刊", "categoryChild": [{ "categoryId": 1, "categoryName": "电子书刊", "categoryChild": [{ "categoryId": 1, "categoryName": "电子书" }, { "categoryId": 2, "categoryName": "网络原创" }, { "categoryId": 3, "categoryName": "数字杂志" }, { "categoryId": 4, "categoryName": "多媒体图书" }] }, ... ] }, { "categoryId": 2, "categoryName": "手机", "categoryChild": [{ "categoryId": 13, "categoryName": "手机通讯", "categoryChild": [{ "categoryId": 61, "categoryName": "手机" }, { "categoryId": 62, "categoryName": "对讲机" }] }, ... ] } ....]在service-product中,我们需要定义Controller,来接收处理获取首页分类数据请求的Controller
/** * 获取全部分类信息 * @return */ @GetMapping("/index") public Result getBaseCategoryList(){ List<FirstLevelCategoryNodeDTO> categoryTreeList = categoryService.getCategoryTreeList(); return Result.ok(categoryTreeList); }还要定义业务层接口,以及业务层实现类
/* 获取完整的三级类目信息 */ List<FirstLevelCategoryNodeDTO> getCategoryTreeList();@Datapublic class FirstLevelCategoryNodeDTO { // 一级目录id Long categoryId;
// 一级目录名称 String categoryName;
// 一级目录的位序 Integer index;
// 一级目录所属的二级目录列表 List<SecondLevelCategoryNodeDTO> categoryChild;
}@Datapublic class SecondLevelCategoryNodeDTO {
// 二级目录id Long categoryId;
// 二级目录名称 String categoryName;
// 二级目录所包含的三级目录列表 List<ThirdLevelCategoryNodeDTO> categoryChild;}public class ThirdLevelCategoryNodeDTO { // 三级目录id private Long categoryId; // 三级目录名称 private String categoryName;}在实现代码之前我们先梳理写思路:
- 先查询出所有的一二三级类目的关联关系,得到 List
select c1.id as first_level_category_id, c1.name as first_level_category_name, c2.id as second_level_category_id, c2.name as second_level_category_name, c3.id as third_level_category_id, c3.name as third_level_category_name from first_level_category c1 inner join second_level_category c2 on c2.first_level_category_id = c1.id inner join third_level_category c3 on c3.second_level_category_id = c2.id <where> <if test="thirdLevelCategoryId != null"> c3.id=#{thirdLevelCategoryId} </if> </where>
- 将上述表示所有一二三级关联关系的List,根据一级类目Id分组得到Map<Long, List
,Map中的key表示一级类目Id,value表示一级类目所包含的二级类目信息集合。 - 针对Map中的每一个key,以及Map的value,我们可以构造出表示一级类目的FirstLevelCategoryNodeDTO对象,以及构造出属于该一级类目的二级类目集合,即List
- 同理,二级类目还包含多个三级类目,如何得到二级类目包含的三级类目集合呢?继续针对属于同一个一级类目的List
,继续根据二级类目Id分组,分组之后,又得到一个Map<Long, List >,Map中的key表示二级类目Id,value表示二级目录所包含的三级类目信息集合 - 针对该Map中的每一个Key,以及Map中的value,我们可以构造出表示二级类目的SecondLevelCategoryNodeDTO对象,以及二级类目所属的三级类目集合,即List
- 这样一来,一级类目FirstLevelCategoryNodeDTO,一级类目包含的二级类目集合List
,以及每一个二级类目SecondLevelCategoryNodeDTO,二级类目包含的三级类目集合List 就都有了
@Override @RedisCache(prefix = "category") public List<FirstLevelCategoryNodeDTO> getCategoryTreeList() { // 声明几个json 集合 ArrayList<FirstLevelCategoryNodeDTO> firstLevelCategoryTreeNodes = new ArrayList<>(); // 声明获取所有分类数据集合 List<CategoryHierarchy> categoryHierarchyList = categoryHierarchyMapper.selectCategoryHierarchy(null); // 循环上面的集合并安一级分类Id 进行分组 Map<Long, List<CategoryHierarchy>> firstLevelCategoryMap = categoryHierarchyList.stream().collect(Collectors.groupingBy(CategoryHierarchy::getFirstLevelCategoryId)); int index = 1; // 获取一级分类下所有数据 for (Map.Entry<Long, List<CategoryHierarchy>> firstLevelEntry : firstLevelCategoryMap.entrySet()) { // 获取一级分类Id Long firstLevelCategoryId = firstLevelEntry.getKey(); // 获取一级分类下面的所有集合 List<CategoryHierarchy> firstLevelCategories = firstLevelEntry.getValue(); // FirstLevelCategoryNodeDTO firstLevelCategoryNode = new FirstLevelCategoryNodeDTO(); firstLevelCategoryNode.setIndex(index); firstLevelCategoryNode.setCategoryId(firstLevelCategoryId); firstLevelCategoryNode.setCategoryName(firstLevelCategories.get(0).getFirstLevelCategoryName()); // 变量迭代 index++; List<SecondLevelCategoryNodeDTO> secondLevelCategoryNodes = buildSecondLevelCategoryNodeDTOs(firstLevelCategories);
// 将二级数据放入一级里面 firstLevelCategoryNode.setCategoryChild(secondLevelCategoryNodes); // 将一级类目放入最终结果集 firstLevelCategoryTreeNodes.add(firstLevelCategoryNode); } return firstLevelCategoryTreeNodes; } private List<SecondLevelCategoryNodeDTO> buildSecondLevelCategoryNodeDTOs(List<CategoryHierarchy> firstLevelCategories) {
// 声明二级分类对象集合 List<SecondLevelCategoryNodeDTO> secondLevelCategoryNodes = new ArrayList<>();
// 循环获取二级分类数据 Map<Long, List<CategoryHierarchy>> firstLevelCategoryChildrenMap = firstLevelCategories.stream() .collect(Collectors.groupingBy(CategoryHierarchy::getSecondLevelCategoryId));
// 循环遍历 for (Map.Entry<Long, List<CategoryHierarchy>> secondLevelEntry : firstLevelCategoryChildrenMap.entrySet()) { // 获取二级分类Id Long secondLevelCategoryId = secondLevelEntry.getKey(); // 获取二级分类下的所有集合 List<CategoryHierarchy> secondLevelCategories = secondLevelEntry.getValue(); // 声明二级分类对象 SecondLevelCategoryNodeDTO secondLevelCategoryNode = new SecondLevelCategoryNodeDTO(); secondLevelCategoryNode.setCategoryId(secondLevelCategoryId); secondLevelCategoryNode.setCategoryName(secondLevelCategories.get(0).getSecondLevelCategoryName());
List<ThirdLevelCategoryNodeDTO> thirdLevelCategoryNodes = buildThirdLevelCategoryNodes(secondLevelCategories); // 将三级类目列表放入二级类目中 secondLevelCategoryNode.setCategoryChild(thirdLevelCategoryNodes);
// 添加到二级分类集合 secondLevelCategoryNodes.add(secondLevelCategoryNode); } return secondLevelCategoryNodes; } private List<ThirdLevelCategoryNodeDTO> buildThirdLevelCategoryNodes(List<CategoryHierarchy> secondLevelCategories) { // 循环三级分类数据, 封装为ThirdLevelCategoryNodeDTO List<ThirdLevelCategoryNodeDTO> thirdLevelCategoryNodeDTOs = secondLevelCategories.stream().map(categoryHierarchy -> { ThirdLevelCategoryNodeDTO thirdLevelCategoryNode = new ThirdLevelCategoryNodeDTO(); thirdLevelCategoryNode.setCategoryId(categoryHierarchy.getThirdLevelCategoryId()); thirdLevelCategoryNode.setCategoryName(categoryHierarchy.getThirdLevelCategoryName()); return thirdLevelCategoryNode; }).collect(Collectors.toList());
return thirdLevelCategoryNodeDTOs; }文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!