[TOC]
1.Thymeleaf介绍
概念
Thymeleaf 是一个跟 FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较与其他的模板引擎,它有如下特点:
- 动静结合:Thymeleaf 在有网络和无网络的环境下皆可运行,无网络显示静态内容,有网络用后台得到数据替换静态内容
- 与SpringBoot完美整合,springboot默认整合thymeleaf
之前springboot课程中已经讲解过thymeleaf,这里不再重复
2.商品详情页
2.1 需求分析
当系统审核完成商品,需要将商品详情页进行展示,那么采用静态页面生成的方式生成,并部署到高性能的web服务器中进行访问是比较合适的。所以,开发流程如下图所示:
执行步骤解释:
- 系统管理员(商家运维人员)修改或者审核商品的时候,会触发canal监控数据
- canal微服务获取修改数据后,调用静态页微服务的方法进行生成静态页
- 静态页微服务只负责使用thymeleaf的模板技术生成静态页
2.2 商品静态化微服务创建
2.2.1 需求分析
该微服务只用于生成商品静态页,不做其他事情。
2.2.2 搭建项目
(1)创建legou-page工程
(2)legou-page中添加起步依赖,如下
<?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>legou-parent</artifactId>
<groupId>com.lxs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>legou-page</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</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.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--商品微服务-->
<dependency>
<groupId>com.lxs</groupId>
<artifactId>legou-item-instance</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.lxs</groupId>
<artifactId>legou-common</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
(3)配置文件
legou-page/src/main/resources/bootstrap.yml
spring:
application:
name: page-service
# 生成静态页的位置
pagepath: d:\temp
config-repo/page-service.yml
server:
port: 9008
logging:
#file: demo.log
pattern:
console: "%d - %msg%n"
level:
org.springframework.web: debug
com.lxs: debug
security:
oauth2:
resource:
jwt:
key-uri: http://localhost:9098/oauth/token_key #如果使用JWT,可以获取公钥用于 token 的验签
(4)创建系统启动类
package com.lxs.legou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class PageApplication {
public static void main(String[] args) {
SpringApplication.run(PageApplication.class, args);
}
}
2.3 生成静态页
thymleaf模板和静态资源css,js,图片等直接从资料拷贝即可,这部分内容有前端人员提供
2.3.1 需求分析
页面发送请求,传递要生成的静态页的的商品的SpuID.后台controller 接收请求,调用thyemleaf的原生API生成商品静态页。
上图是要生成的商品详情页,从图片上可以看出需要查询SPU的3个分类作为面包屑显示,同时还需要查询SKU和SPU信息。
2.3.2 Feign创建
一会儿需要查询SPU和SKU以及Category,所以我们需要先创建Feign,创建CategoryClient代码如下
legou-page/src/main/java/com/lxs/legou/page/client/CategoryClient.java
package com.lxs.legou.page.client;
import com.lxs.legou.item.api.CategoryApi;
import com.lxs.legou.item.po.Category;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(name = "item-service", contextId = "p1", fallback = CategoryClient.CategoryClientFallback.class)
public interface CategoryClient extends CategoryApi {
@Component
@RequestMapping("/category-fallback2") //这个可以避免容器中requestMapping重复
class CategoryClientFallback implements CategoryClient {
private static final Logger LOGGER = LoggerFactory.getLogger(CategoryClientFallback.class);
@Override
public List<String> queryNameByIds(List<Long> ids) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
@Override
public List<Category> list(Category category) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
@Override
public Category edit(Long id) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
}
}
FeignClient中的contextId的应用
比如我们有个item-service服务,但item-service服务中有很多个接口,我们不想将所有的调用接口都定义在一个类中,比如:
@FeignClient(name = "item-service", contextId = "p1")
@RequestMapping("/item/category")
public interface CategoryClient1 {
@Override
@PostMapping("/query-name-by-ids")
public List<String> queryNameByIds(List<Long> ids) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
}
@FeignClient(name = "item-service", contextId = "p2")
@RequestMapping("/item/category")
public interface CategoryClient2 {
@Override
@PostMapping("/query-name-by-ids")
public List<String> queryNameByIds(List<Long> ids) {
LOGGER.info("异常发生,进入fallback方法");
return null;
}
}
这里使用contextId手动指定一个名称,或者可以做如下配置
spring:
main:
allow-bean-definition-overriding: true
legou-page/src/main/java/com/lxs/legou/page/client/SpuClient.java
package com.lxs.legou.page.client;
import com.lxs.legou.item.api.SpuApi;
import com.lxs.legou.item.po.Spu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(name = "item-service", contextId = "p3", fallback = SpuClient.SpuClientFallback.class)
public interface SpuClient extends SpuApi {
@Component
@RequestMapping("/spu-fallback2") //这个可以避免容器中requestMapping重复
class SpuClientFallback implements SpuClient {
private static final Logger LOGGER = LoggerFactory.getLogger(SpuClientFallback.class);
@Override
public List<Spu> selectAll() {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
@Override
public Spu edit(Long id) {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
}
}
legou-page/src/main/java/com/lxs/legou/page/client/SpuDetailClient.java
package com.lxs.legou.page.client;
import com.lxs.legou.item.api.SpuDetailApi;
import com.lxs.legou.item.po.SpuDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(name = "item-service", contextId = "p4", fallback = SpuDetailClient.SpuDetailFallback.class)
public interface SpuDetailClient extends SpuDetailApi {
@Component
@RequestMapping("/spu-detail-fallback2") //这个可以避免容器中requestMapping重复
class SpuDetailFallback implements SpuDetailClient {
private static final Logger LOGGER = LoggerFactory.getLogger(SpuDetailFallback.class);
@Override
public SpuDetail edit(Long id) {
System.out.println("异常发生,进入fallback方法");
return null;
}
}
}
legou-page/src/main/java/com/lxs/legou/page/client/SkuClient.java
package com.lxs.legou.page.client;
import com.lxs.legou.item.api.SkuApi;
import com.lxs.legou.item.po.Sku;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@FeignClient(name = "item-service", contextId = "p2", fallback = SkuClient.SkuClientFallback.class)
public interface SkuClient extends SkuApi {
@Component
@RequestMapping("/sku-fallback2")
//这个可以避免容器中requestMapping重复
class SkuClientFallback implements SkuClient {
private static final Logger LOGGER = LoggerFactory.getLogger(SkuClientFallback.class);
@Override
public List<Sku> selectSkusBySpuId(Long spuId) {
LOGGER.error("异常发生,进入fallback方法");
return null;
}
}
}
2.3.3 静态页生成代码
(1)创建Controller
legou-page/src/main/java/com/lxs/legou/page/controller/PageController.java
package com.lxs.legou.page.controller;
import com.lxs.legou.page.service.PageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/page")
public class PageController {
@Autowired
private PageService pageService;
/**
* 生成静态页面
* @param id SPU的ID
* @return
*/
@RequestMapping("/createHtml/{id}")
public ResponseEntity<String> createHtml(@PathVariable(name="id") Long id){
pageService.createPageHtml(id);
return ResponseEntity.ok("生成成功");
}
}
(2)创建service
接口:
package com.lxs.legou.page.service;
public interface PageService {
//生成静态页
void createPageHtml(Long id);
}
实现类:
上图代码如下:
package com.lxs.legou.page.service.impl;
import com.lxs.legou.common.utils.JsonUtils;
import com.lxs.legou.core.json.JSON;
import com.lxs.legou.item.po.Category;
import com.lxs.legou.item.po.Sku;
import com.lxs.legou.item.po.Spu;
import com.lxs.legou.item.po.SpuDetail;
import com.lxs.legou.page.client.CategoryClient;
import com.lxs.legou.page.client.SkuClient;
import com.lxs.legou.page.client.SpuClient;
import com.lxs.legou.page.client.SpuDetailClient;
import com.lxs.legou.page.service.PageService;
import com.netflix.discovery.converters.Auto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.io.File;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class PageServiceImpl implements PageService {
@Autowired
private SpuClient spuClient;
@Autowired
private CategoryClient categoryClient;
@Autowired
private SkuClient skuClient;
@Autowired
private SpuDetailClient spuDetailClient;
@Autowired
private TemplateEngine templateEngine;
//生成静态文件路径
@Value("${pagepath}")
private String pagepath;
/**
* 构建数据模型
* @param spuId
* @return
*/
private Map<String,Object> buildDataModel(Long spuId){
//构建数据模型
Map<String,Object> dataMap = new HashMap<>();
//获取spu 和SKU列表
Spu spu = spuClient.edit(spuId);
Category c1 = categoryClient.edit(spu.getCid1());
//获取分类信息
dataMap.put("category1", categoryClient.edit(spu.getCid1()));
dataMap.put("category2", categoryClient.edit(spu.getCid2()));
dataMap.put("category3", categoryClient.edit(spu.getCid3()));
List<Sku> skus = skuClient.selectSkusBySpuId(spu.getId());
List<String> images = new ArrayList<>();
for (Sku sku : skus) {
images.add(sku.getImages());
}
dataMap.put("imageList", images);
SpuDetail spuDetail = spuDetailClient.edit(spu.getId());
// Map<String, String> genericMap = JsonUtils.parseMap(spuDetail.getGenericSpec(), String.class, String.class);
Map<String, Object> genericMap = JsonUtils.parseMap(spuDetail.getSpecialSpec(), String.class, Object.class);
// dataMap.put("specificationList", JSON.parseObject(spu.getSpecItems(),Map.class));
dataMap.put("specificationList", genericMap);
dataMap.put("spu",spu);
dataMap.put("spuDetail",spuDetail);
//根据spuId查询Sku集合
dataMap.put("skuList", skus);
return dataMap;
}
/***
* 生成静态页
* @param spuId
*/
@Override
public void createPageHtml(Long spuId) {
// 1.上下文 模板 + 数据集 =html
Context context = new Context();
Map<String, Object> dataModel = buildDataModel(spuId);
context.setVariables(dataModel);//model.addtribute()
// 2.准备文件
File dir = new File(pagepath);
if (!dir.exists()) {
dir.mkdirs();
}
File dest = new File(dir, spuId + ".html");
// 3.生成页面
try (PrintWriter writer = new PrintWriter(dest, "UTF-8")) {
//模板的文件
templateEngine.process("item", context, writer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.2.4 模板填充
(1)面包屑数据
修改item.html,填充三个分类数据作为面包屑,代码如下:
(2)商品图片
修改item.html,将商品图片信息输出,在真实工作中需要做空判断,代码如下:
(3)规格输出
(4)默认SKU显示
静态页生成后,需要显示默认的Sku,我们这里默认显示第1个Sku即可,这里可以结合着Vue一起实现。可以先定义一个集合,再定义一个spec和sku,用来存储当前选中的Sku信息和Sku的规格,代码如下:
页面显示默认的Sku信息
(5)记录选中的Sku
在当前Spu的所有Sku中spec值是唯一的,我们可以根据spec来判断用户选中的是哪个Sku,我们可以在Vue中添加代码来实现,代码如下:
添加规格点击事件
(6)样式切换
点击不同规格后,实现样式选中,我们可以根据每个规格判断该规格是否在当前选中的Sku规格中,如果在,则返回true添加selected样式,否则返回false不添加selected样式。
Vue添加代码:
页面添加样式绑定,代码如下:
2.2.5 静态资源过滤
生成的静态页我们可以先放到legou-page工程中,后面项目实战的时候可以挪出来放到Nginx指定发布目录。一会儿我们将生成的静态页放到resources/templates/items目录下,所以请求该目录下的静态页需要直接到该目录查找即可。
我们创建一个EnableMvcConfig类,开启静态资源过滤,代码如下:
@ControllerAdvice
@Configuration
public class EnableMvcConfig implements WebMvcConfigurer{
/***
* 静态资源放行
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/items/**").addResourceLocations("classpath:/templates/items/");
}
}
5.4.6 启动测试
启动eurekea服务端
启动商品微服务
启动静态化微服务 legou-page
将静态资源导入到legou-page中,如下图:
生成静态页地址 http://localhost:18085/page/createHtml/1087918019151269888
静态页生成后访问地址 http://localhost:9008/items/1087918019151269888.html
3 canal监听生成静态页(作业)
监听到数据的变化,直接调用feign 生成静态页即可.
3.1 需求分析
当商品微服务审核商品之后,应当发送消息,这里采用了Canal监控数据变化,数据变化后,调用feign实现生成静态页
3.2 Feign创建
(1)Feign创建
在legou-canal中创建PageFeign,代码如下:
@FeignClient(name="item")
@RequestMapping("/page")
public interface PageFeign {
/***
* 根据SpuID生成静态页
* @param id
* @return
*/
@RequestMapping("/createHtml/{id}")
Result createHtml(@PathVariable(name="id") Long id);
}
3.3 canal监听数据变化
监听类中,监听商品数据库的tb_spu的数据变化,当数据变化的时候生成静态页或者删除静态页
在原来的监听类中添加如下代码即可,
@Autowired
private PageFeign pageFeign;
@ListenPoint(destination = "example",
schema = "legou",
table = {
"spu"},
eventType = {
CanalEntry.EventType.UPDATE, CanalEntry.EventType.INSERT, CanalEntry.EventType.DELETE})
public void onEventCustomSpu(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
//判断操作类型
if (eventType == CanalEntry.EventType.DELETE) {
String spuId = "";
List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
for (CanalEntry.Column column : beforeColumnsList) {
if (column.getName().equals("id")) {
spuId = column.getValue();//spuid
break;
}
}
//todo 删除静态页
}else{
//新增 或者 更新
List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
String spuId = "";
for (CanalEntry.Column column : afterColumnsList) {
if (column.getName().equals("id")) {
spuId = column.getValue();
break;
}
}
//更新 生成静态页
pageFeign.createHtml(Long.valueOf(spuId));
}
}