商品详情页goods-web
you-goods-web`
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou-parent</artifactId>
<groupId>com.leyou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.goods</groupId>
<artifactId>leyou-goods-web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.item</groupId>
<artifactId>leyou-item-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LeyouGoodsWebApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouGoodsWebApplication.class, args);
}
}
pei zhi
server:
port: 7004
spring:
application:
name: goods-web
thymeleaf:
cache: false
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka
instance:
lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
商品微服务中提供这个接口:根据id查询spu --- GoodsApi
/**
* 根据spu的id查询spu
* @param id
* @return
*/
@GetMapping("/spu/{id}")
public Spu querySpuById(@PathVariable("id") Long id);
GoodsController
@GetMapping("/spu/{id}")
public ResponseEntity<Spu> querySpuById(@PathVariable("id") Long id){
Spu spu = this.goodsService.querySpuById(id);
if(spu == null){
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(spu);
}
GoodsService
public Spu querySpuById(Long id) {
return this.spuMapper.selectByPrimaryKey(id);
}
leyou-item-service中提供一个接口,查询规格组,同时查询规格组内的所有参数。
SpecificationAPI:
@GetMapping("/{cid}")
public List<SpecGroup> querySpecsByCid(@PathVariable Long cid);
SpecificationController:
@GetMapping("/{cid}")
public ResponseEntity<List<SpecGroup>> querySpecsByCid(@PathVariable Long cid) {
List<SpecGroup> specGroups = this.specificationService.querySpecsByCid(cid);
if (CollectionUtils.isEmpty(specGroups)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(specGroups);
}
SpecificationService
@Override
public List<SpecGroup> querySpecsByCid(Long cid) {
//查询所有的规格参数组
List<SpecGroup> specGroups = this.querySpecGroupsByCid(cid);
specGroups.forEach(specGroup -> {
//查询该规格参组下的所有规格参数
List<SpecParam> specParams = this.queryParams(specGroup.getId(), null, null, null);
//封装到规格参数组中
specGroup.setParams(specParams);
});
return specGroups;
}
创建FeignClient
我们在leyou-goods-web
服务中,创建FeignClient:
[图片上传失败...(image-88592f-1592833403265)]
将之前搜索服务中的复制过来
封装数据模型
我们创建一个GoodsService,在里面来封装数据模型。
这里要查询的数据:
-
SPU
-
SpuDetail
-
SKU集合
-
商品分类
- 这里值需要分类的id和name就够了,因此我们查询到以后自己需要封装数据
-
品牌对象
-
规格组
- 查询规格组的时候,把规格组下所有的参数也一并查出,上面提供的接口中已经实现该功能,我们直接调
-
sku的特有规格参数
有了规格组,为什么这里还要查询?
因为在SpuDetail中的SpecialSpec中,是以id作为规格参数的key,如图:
[图片上传失败...(image-6570d0-1592833403265)]
但是,在页面渲染时,需要知道参数的名称,如图:
[图片上传失败...(image-a6d876-1592833403265)]
我们就需要把id和name一一对应起来,因此需要额外查询sku的特有规格参数,然后变成一个id:name的键值对格式。也就是一个Map,方便将来根据id查找!
Service代码
@Override
public Map<String, Object> loadData(Long spuId) {
//根据商品id查询商品(spu)
Spu spu = this.goodsClient.querySpuById(spuId);
//查询商品详情(spuDetail)
SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spuId);
//根据spuId查询其下的所有sku
List<Sku> skus = this.goodsClient.querySkuBySpuId(spuId);
// 查询分类
List<Category> categories = this.categoryClient.queryAllByCid3(spu.getCid3());
//只需要分类id和分类名称
List<Map<String, Object>> categoryData = categories.stream().map(category -> {
Map<String, Object> map = new HashMap<>();
map.put("id", category.getId());
map.put("name", category.getName());
return map;
}).collect(Collectors.toList());
// 查询品牌
Brand brand = this.brandClient.queryBrandByid(spu.getBrandId());
// 查询规格参数组和组下规格参数
List<SpecGroup> groups = this.specificationClient.querySpecsByCid(spu.getCid3());
//查询特殊的规格参数获取 id,name
List<SpecParam> specialParams = this.specificationClient.querySpecParams(null, spu.getCid3(), false, null);
Map<Long, String> params = new HashMap<>();
specialParams.forEach(specParam -> {
params.put(specParam.getId(), specParam.getName());
});
//封装商品信息
Map<String, Object> goodsInfo = new HashMap<>();
// 封装spu
goodsInfo.put("spu", spu);
// 封装spuDetail
goodsInfo.put("spuDetail", spuDetail);
// 封装sku集合
goodsInfo.put("skus", skus);
// 分类
goodsInfo.put("categories", categories);
// 品牌
goodsInfo.put("brand", brand);
// 规格参数组
goodsInfo.put("groups", groups);
// 查询特殊规格参数
goodsInfo.put("params", params);
return goodsInfo;
}
然后在controller中把数据放入model:
@Controller
@RequestMapping("item")
public class GoodsController {
@Autowired
private GoodsService goodsService;
@GetMapping("/{id}.html")
public String toItemPage(Model model, @PathVariable("id") Long id) {
Map<String, Object> map = goodsService.loadData(id);
model.addAllAttributes(map);
return "item";
}
}
Thymeleaf实现静态化
概念
先说下Thymeleaf中的几个概念:
- Context:运行上下文
- TemplateResolver:模板解析器
- TemplateEngine:模板引擎
Context
上下文: 用来保存模型数据,当模板引擎渲染时,可以从Context上下文中获取数据用于渲染。
当与SpringBoot结合使用时,我们放入Model的数据就会被处理到Context,作为模板渲染的数据使用。
TemplateResolver
模板解析器:用来读取模板相关的配置,例如:模板存放的位置信息,模板文件名称,模板文件的类型等等。
当与SpringBoot结合时,TemplateResolver已经由其创建完成,并且各种配置也都有默认值,比如模板存放位置,其默认值就是:templates。比如模板文件类型,其默认值就是html。
TemplateEngine
模板引擎:用来解析模板的引擎,需要使用到上下文、模板解析器。分别从两者中获取模板中需要的数据,模板文件。然后利用内置的语法规则解析,从而输出解析后的文件。来看下模板引擎进行处理的函数:
templateEngine.process("模板名", context, writer);
三个参数:
- 模板名称
- 上下文:里面包含模型数据
- writer:输出目的地的流
在输出时,我们可以指定输出的目的地,如果目的地是Response的流,那就是网络响应。如果目的地是本地文件,那就实现静态化了。
而在SpringBoot中已经自动配置了模板引擎,因此我们不需要关心这个。现在我们做静态化,就是把输出的目的地改成本地文件即可!
具体实现
Service代码:
@Service
public class GoodsHtmlServiceImpl implements GoodsHtmlService {
@Autowired
private GoodsService goodsService;
@Autowired
private TemplateEngine templateEngine;
private final Logger LOGGER = LoggerFactory.getLogger(getClass());
@Override
public void createHtml(Map<String, Object> model) {
//创建thymeleaf上下文对象
Context context = new Context();
//把数据放入上下文
context.setVariables(model);
//获取spu
Spu spu = (Spu) model.get("spu");
Writer writer = null;
try {
writer = new PrintWriter(new File("/home/cloudlandboy/Project/leyou/html/item/" + spu.getId() + ".html"));
// 执行页面静态化方法
templateEngine.process("item", context, writer);
} catch (FileNotFoundException e) {
LOGGER.error("页面静态化出错:{}," + e, model);
} finally {
IOUtils.closeQuietly(writer);
}
}
@Override
public void asyncExcuteCreateHtml(Map<String, Object> model) {
ThreadUtils.execute(() -> createHtml(model));
}
}
线程工具类:
public class ThreadUtils {
private static final ExecutorService es = Executors.newFixedThreadPool(10);
public static void execute(Runnable runnable) {
es.submit(runnable);
}
}
什么时候创建静态文件
我们编写好了创建静态文件的service,那么问题来了:什么时候去调用它呢
想想这样的场景:
假如大部分的商品都有了静态页面。那么用户的请求都会被nginx拦截下来,根本不会到达我们的leyou-goods-web
服务。只有那些还没有页面的请求,才可能会到达这里。
因此,如果请求到达了这里,我们除了返回页面视图外,还应该创建一个静态页面,那么下次就不会再来麻烦我们了。
所以,我们在GoodsController中添加逻辑,去生成静态html文件:
@Controller
@RequestMapping("item")
public class GoodsController {
@Autowired
private GoodsService goodsService;
@Autowired
private GoodsHtmlService goodsHtmlService;
/**
* 跳转到商品详情页
*
* @param model
* @param id
* @return
*/
@GetMapping("/{id}.html")
public String toItemPage(Model model, @PathVariable("id") Long id) {
Map<String, Object> map = goodsService.loadData(id);
model.addAllAttributes(map);
//页面静态化
goodsHtmlService.asyncExcuteCreateHtml(map);
return "item";
}
}
注意:生成html 的代码不能对用户请求产生影响,所以这里我们使用额外的线程进行异步创建。
nginx代理静态页面
接下来,我们修改nginx,让它对商品请求进行监听,指向本地静态页面,如果本地没找到,才进行反向代理:
server {
listen 80;
server_name www.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /item {
# 先找本地
root /home/cloudlandboy/Project/leyou/html;
# 请求的文件不存在,就反向代理
if (!-f $request_filename){
proxy_pass http://127.0.0.1:7004;
}
}
location / {
proxy_pass http://127.0.0.1:9002;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
!> 注意if
和后面的括号之间要有一个空格
重启测试:
发现请求速度得到了极大提升:
[图片上传失败...(image-f0200a-1592833403265)]