学习目标:
问题:查询商品详情页的逻辑非常复杂,数据的获取都需要远程调用,必然需要花费更多的时间。
假如商品详情页的每个查询,需要如下标注的时间才能完成
那么,用户需要4s后才能看到商品详情页的内容。很显然是不能接受的。如果有多个线程同时完成这4步操作,也许只需要1.5s即可完成响应。
Future是Java 5添加的接口,用来描述一个异步计算的结果。你可以使用isDone方法检查计算是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。
在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。
CompletableFuture类实现了Future接口,所以你还是可以像以前一样通过get方法阻塞或者轮询的方式获得结果,但是这种方式不推荐使用。
CompletableFuture和FutureTask同属于Future接口的实现类,都可以获取线程的执行结果。
CompletableFuture 提供了四个静态方法来创建一个异步操作。
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。
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);
}
}
当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:
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);
}
thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行 thenRun的后续操作
带有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();
}
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs);
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs);
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();
}
好处:
在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);
}
}
对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;
}
}
Springboot线程池实现关键类:ThreadPoolTaskExecutor默认线程池实现,为什么不采用Springboot默认线程池?
private int maxPoolSize = Integer.MAX_VALUE;
private int queueCapacity = Integer.MAX_VALUE;
原因:最大线程数跟阻塞队列长度Integer最大值,存在OOM风险.
一般Springboot应用采用自定义线程池:配置自定义线程池
application.yml
定义线程池相关变量:
# 线程池配置参数
task:
pool:
corePoolSize: 10 # 设置核心线程数
maxPoolSize: 20 # 设置最大线程数
keepAliveTime: 300 # 设置空闲线程存活时间(秒)
queueCapacity: 100 # 设置队列容量
threadNamePrefix: "gmall-item-" # 设置线程名称前缀
awaitTerminationSeconds: 60 # 设置线程池等待终止时间(秒)
spring:
main:
allow-bean-definition-overriding: true
定义属性类读取配置信息
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;
}
注册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;
}
}
异步方法上加注解 @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";
}
}
前面做了商品详情,我们现在来做首页分类,我先看看京东的首页分类效果,我们如何实现类似效果:
思路:
1,首页属于并发量比较高的访问页面,我看可以采取页面静态化方式实现,或者把数据放在缓存中实现
2,我们把生成的静态文件可以放在nginx访问或者放在web-index模块访问
在web-all
模块中新增商品服务依赖
<dependency>
<groupId>com.atguigu.gmall</groupId>
<artifactId>service-product-client</artifactId>
<version>1.0</version>
</dependency>
由于商品分类信息在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
}
]
},
//..{}其他的一级分类
]
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();
}
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();
}
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);
}
}
}
在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;
}
第一种缓存渲染方式:
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做静态代理方式:
生成静态文件
@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);
}
}
解压课后资料中nginx压缩 不要中文空格
将静态文件拷贝到nginx/html目录下 包含js,css等文件夹
启动Nginx服务
访问首页
Nginx反向代理配置-了解
启动nginx,nginx目录下打开命令行
start nginx
关闭nginx
nginx -s stop
重新加载nginx配置文件
nginx -s reload
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;
}
}
后端的网关服务要搭建集群