商品详情异步与首页

5264 字
26 分钟
商品详情异步与首页

商品详情的异步#

问题的引出#

到目前位置,我们已经完成了获取商品详情的功能,代码如下:

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的执行”

image-20240519161150253
image-20240519161150253

如上图所示,这里描绘的是一个业务接口的流程,其中包括CF1\CF2\CF3\CF4\CF5\CF6 共6个步骤,并描绘了这些步骤之间的依赖关系,每个步骤可以是一次数据库操作或者是一次本地方法调用,或者是一次服务间的调用等,在使用CompletableFuture进行异步化编程时,图中的每个步骤都会产生一个CompletableFuture对象,最终结果也会用一个CompletableFuture来进行表示。

根据CompletableFuture依赖数量,可以分为以下几类:零依赖、一元依赖、二元依赖和多元依赖。

零依赖#

零依赖,其实也就是CompletableFuture对象的创建,我们可以先来看看如何不依赖其他的CompletableFuture对象来创建新的CompletableFuture。

image-20240519161519164
image-20240519161519164

如上图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的结果是null
String result = CompletableFuture.get();
  • 关于CompletableFuture.get()方法,是一个阻塞方法,会阻塞调用线程,直到CompletableFuture对象所代表的的异步任务执行结束。

一元依赖#

image-20240519162415395
image-20240519162415395

如上图所示,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);

二元依赖#

image-20240519201159136
image-20240519201159136

如上图红色链路所示,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)
@FunctionalInterface
public 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();

多元依赖#

image-20240519202449036
image-20240519202449036

如上图红色链路所示,整个流程的结束依赖于三个步骤CF3、CF4、CF5,这种多元依赖可以通过allOfanyOf方法来实现,区别是当需要多个依赖全部完成时使用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 查询skuInfo
CompletableFuture<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();
@Data
public class FirstLevelCategoryNodeDTO {
// 一级目录id
Long categoryId;
// 一级目录名称
String categoryName;
// 一级目录的位序
Integer index;
// 一级目录所属的二级目录列表
List<SecondLevelCategoryNodeDTO> categoryChild;
}
@Data
public 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;
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

商品详情异步与首页
https://firefly-mu-weld.vercel.app/posts/microservice-14-product-detail-async-home/
作者
Daisy
发布于
2026-06-14
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Daisy
Hello, I'm Daisy.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签

文章目录