学习目标:
问题:查询商品详情页的逻辑非常复杂,数据的获取都需要远程调用,必然需要花费更多的时间。
假如商品详情页的每个查询,需要如下标注的时间才能完成
那么,用户需要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 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.ThreadPoolExecutor;
/**
* @author: atguigu
* @create: 2023-07-28 16:31
*/
@Service
@SuppressWarnings("all") //去除警告抑制
public class ItemServiceImpl implements ItemService {
@Autowired
private ProductFeignClient productFeignClient;
@Autowired
private RedissonClient redissonClient;
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
/**
* 远程调用商品服务,汇总渲染详情页面所需要数据模型
* 1. ${skuInfo} 商品SKU基本信息 包含:名称,默认图片,重量,商品SKU图片列表
* 2. ${categoryView} 商品SKU所属分类 属性:category1Id|name,category2Id|name,category3Id|name
* 3. ${price} 商品价格-实时
* 4. ${spuPosterList} 商品海报图片列表
* 5. ${skuAttrList} 商品SKU所有平台属性以及值
* 6. ${spuSaleAttrList} 商品Spu所有销售属性,以及销售属性值,带选中效果 根据skuId查销售属性
* 7. ${valuesSkuJson} 选中一组销售属性实现完成切换SKU详情页面JSON
*
* @param skuId
* @return
*/
@Override
public Map<String, Object> getItemInfo(Long skuId) {
//查询布隆过滤器,验证用户访问商品ID是否存在 TODO 暂时注释掉
/*RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
boolean contains = bloomFilter.contains(skuId);
if (!contains) {
return null;
}*/
Map<String, Object> mapResult = new HashMap<>();
//1. ${skuInfo} 商品SKU基本信息 包含:名称,默认图片,重量,商品SKU图片列表
CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
// 商品信息是需要被其他四个异步任务锁需要的,故商品信息异步任务必须有返回值
SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
if (skuInfo != null) {
mapResult.put("skuInfo", skuInfo);
}
return skuInfo;
}, threadPoolExecutor);
//2. ${categoryView} 商品SKU所属分类 属性:category1Id|name,category2Id|name,category3Id|name
CompletableFuture<Void> categoryViewCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
// 商品分类异步任务需要依赖商品信息异步任务的执行结果,当前异步任务无需返回结果
BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
if (categoryView != null) {
mapResult.put("categoryView", categoryView);
}
}, threadPoolExecutor);
//3. ${price} 商品价格-实时
CompletableFuture<Void> priceCompletableFuture = CompletableFuture.runAsync(() -> {
// 商品价格异步任务独立跟商品信息异步任务并行执行
BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
if (skuPrice != null) {
mapResult.put("price", skuPrice);
}
}, threadPoolExecutor);
//4. ${spuPosterList} 商品海报图片列表
CompletableFuture<Void> spuPosterListCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
//海报列表异步任务需要依赖商品信息异步任务的执行结果,当前异步任务无需返回结果
List<SpuPoster> spuPosterList = productFeignClient.getSpuPosterBySpuId(skuInfo.getSpuId());
if (!CollectionUtils.isEmpty(spuPosterList)) {
mapResult.put("spuPosterList", spuPosterList);
}
}, threadPoolExecutor);
//5. ${skuAttrList} 商品SKU所有平台属性以及值
CompletableFuture<Void> skuAttrListCompletableFuture = CompletableFuture.runAsync(() -> {
// 平台属性列表异步任务独立跟商品信息异步任务并行执行
List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
if (!CollectionUtils.isEmpty(attrList)) {
mapResult.put("skuAttrList", attrList);
}
}, threadPoolExecutor);
//6. ${spuSaleAttrList} 商品Spu所有销售属性,以及销售属性值,带选中效果 根据skuId查销售属性
CompletableFuture<Void> spuSaleAttrListCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
// 销售属性异步任务需要依赖商品信息异步任务的执行结果,当前异步任务无需返回结果
List<SpuSaleAttr> spuSaleAttrList = productFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
if (!CollectionUtils.isEmpty(spuSaleAttrList)) {
mapResult.put("spuSaleAttrList", spuSaleAttrList);
}
}, threadPoolExecutor);
//7. ${valuesSkuJson} 选中一组销售属性实现完成切换SKU详情页面JSON
CompletableFuture<Void> valuesSkuJsonCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
//切换SKU异步任务需要依赖商品信息异步任务的执行结果,当前异步任务无需返回结果
String skuValueIdsMap = productFeignClient.getSkuValueIdsMap(skuInfo.getSpuId());
if (StringUtils.isNotBlank(skuValueIdsMap)) {
mapResult.put("valuesSkuJson", skuValueIdsMap);
}
}, threadPoolExecutor);
//8.将以上所有异步任务进行组合 必须所有异步任务都执行完毕
CompletableFuture.allOf(
skuInfoCompletableFuture,
priceCompletableFuture,
skuAttrListCompletableFuture,
categoryViewCompletableFuture,
spuPosterListCompletableFuture,
spuSaleAttrListCompletableFuture,
valuesSkuJsonCompletableFuture
).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
/**
* 查询所有分类,{一级分类:{二级分类:{三级分类}}}
*
* @return
*/
@ApiOperation("查询所有分类,{一级分类:{二级分类:{三级分类}}}")
@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;
/**
* @author: atguigu
* @create: 2023-02-25 10:14
*/
public interface BaseCategoryViewService extends IService<BaseCategoryView> {
/**
* 查询所有分类列表 分类嵌套结果:一级分类分类对象中包含二级分类集合;在二级分类对象中包含三级分类集合
* @return
*/
List<JSONObject> getBaseCategoryList();
}
package com.atguigu.gmall.product.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.common.cache.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-07-28
*/
@Service
public class BaseCategoryViewServiceImpl extends ServiceImpl<BaseCategoryViewMapper, BaseCategoryView> implements BaseCategoryViewService {
/**
* 查询所有分类,{一级分类:{二级分类:{三级分类}}}
* 1.查询分类视图得到所有一二三级分类
* 2.将分类数据放入缓存
*
* @return
*/
@Override
@GmallCache(prefix = "baseCategoryList:") //baseCategoryList:none:info
public List<JSONObject> getBaseCategoryList() {
List<JSONObject> list = new ArrayList<>();
//1.查询分类视图得到所有分类数据
List<BaseCategoryView> allList = this.list();
//2.处理一级分类
if (!CollectionUtils.isEmpty(allList)) {
//2.1 对所有分类集合进行分组:按照一级分类ID进行分组 采用stream流进行分组 Map中Key:一级分类ID Map中Val:当前所有一级分类列表
Map<Long, List<BaseCategoryView>> category1MapList =
allList.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory1Id));
//2.2 封装一级分类对象-封装一级分类ID以及一级分类名称 既要获取上面Map的key 还要获取val 采用entrySet进行遍历
for (Map.Entry<Long, List<BaseCategoryView>> category1Entry : category1MapList.entrySet()) {
//2.2.1 获取一级分类ID以及名称
Long category1Id = category1Entry.getKey();
String category1Name = category1Entry.getValue().get(0).getCategory1Name();
//2.2.2 按照要求构建一级分类对象
JSONObject category1 = new JSONObject();
category1.put("categoryId", category1Id);
category1.put("categoryName", category1Name);
//3.处理二级分类
//3.1 将当前一级分类所有列表按照二级分类ID进行分组 Map中Key:二级分类ID MapValue:当前分类下所有二级分类集合
Map<Long, List<BaseCategoryView>> category2MapList =
category1Entry.getValue().stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory2Id));
//3.2 遍历二级分类Map集合
List<JSONObject> category2List = new ArrayList<>();
for (Map.Entry<Long, List<BaseCategoryView>> category2Entry : category2MapList.entrySet()) {
//3.2.1 获取二级分类ID跟名称
Long category2Id = category2Entry.getKey();
String category2Name = category2Entry.getValue().get(0).getCategory2Name();
//3.2.2 按照要求构建二级分类对象
JSONObject category2 = new JSONObject();
category2.put("categoryId", category2Id);
category2.put("categoryName", category2Name);
//4.处理三级分类
//4.1 二级分类集合中包含三级分类 只需要遍历二级分类得到三级分类
List<JSONObject> category3List = new ArrayList<>();
for (BaseCategoryView baseCategoryView : category2Entry.getValue()) {
//4.1.1 遍历构建三级分类对象
JSONObject category3 = new JSONObject();
category3.put("categoryId", baseCategoryView.getCategory3Id());
category3.put("categoryName", baseCategoryView.getCategory3Name());
category3List.add(category3);
}
//4.2 将当前二级分类下三级分类集合放入二级分类对象中“categoryChild”属性
category2.put("categoryChild", category3List);
category2List.add(category2);
}
//3.3 将当前一级分类下二级分类集合 放入到一级分类中“categoryChild”属性中
category1.put("categoryChild", category2List);
list.add(category1);
}
}
return list;
}
}
在service-product-client
模块中ProductFeignClient,提供远程调用FeignAPI接口以及服务降级方法
/**
* 查询所有分类列表 分类嵌套结果:一级分类分类对象中包含二级分类集合;在二级分类对象中包含三级分类集合
* @return
*/
@GetMapping("/api/product/inner/getBaseCategoryList")
public List<JSONObject> getBaseCategoryList();
@Override
public List<JSONObject> getBaseCategoryList() {
return null;
}
第一种缓存渲染方式:
web-all
模块中编写控制器
package com.atguigu.gmall.web.controller;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.common.result.Result;
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-08-03 10:24
*/
@Controller
public class IndexController {
@Autowired
private ProductFeignClient productFeignClient;
/***
* 渲染首页
* @param model
* @return
*/
@GetMapping({"/", ""})
public String indexHtml(Model model) {
//1.远程调用商品服务获取首页需要分类数据
List<JSONObject> list = productFeignClient.getBaseCategoryList();
model.addAttribute("list", list);
//2.todo 调用广告服务获取广告列表
//3.todo 调用推荐服务获取推荐商品
//4.todo 调用运营服务获取热卖商品/新品等 如果有多项数据获取改串行为并行提交效率
return "/index/index";
}
}
第二种方式nginx做静态代理方式:
生成静态文件
@Autowired
private TemplateEngine templateEngine;
/**
* 生成首页静态文件
*
* @return
*/
@ResponseBody
@GetMapping("/create")
public Result createIndexHtmlFile() {
try {
//1.远程调用商品服务获取首页需要分类数据
List<JSONObject> list = productFeignClient.getBaseCategoryList();
//2.创建上下文对象封装页面数据模型
Context context = new Context();
context.setVariable("list", list);
//3.创建写文件对象
String filePath = "D:\\tmp\\index.html";
FileWriter fileWriter = new FileWriter(filePath);
//4.调用模板引擎方法生成静态文件
templateEngine.process("/index/index", context, fileWriter);
} catch (Exception e) {
throw new RuntimeException(e);
}
return Result.ok();
}
解压课后资料中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;
}
}
后端的网关服务要搭建集群
通过Nginx访问服务测试 http://localhost:88/api/product/inner/getBaseCategoryList
网关中配置全局过滤器可以确定访问的是哪个服务
package com.atguigu.gmall.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author: atguigu
* @create: 2023-08-03 10:46
*/
@Slf4j
@Component
public class AccessFilter implements GlobalFilter {
@Value("${server.port}")
private Integer port;
/**
* 验证请求是否经过该节点
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[网关被访问],当前节点端口:{}", port);
return chain.filter(exchange);
}
}