文章目录
1. 搭建商品详情微服务
当用户搜索到商品后,如果想要了解商品的更多信息,就需要进入商品详情页。
由于商品详情浏览量比较大,所以我们会创建一个微服务,用来展示商品详情。我们的商品详情页会采用 Thymeleaf 模板引擎渲染后,再返回到客户端。
1.1 创建工程
-
右键 leyou 项目 --> New Module --> Maven --> Next
-
填写项目信息 --> Next
-
填写保存的位置 --> Finish
-
添加依赖
<?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</artifactId> <groupId>com.leyou.parent</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.parent</groupId> <artifactId>leyou-goods-web</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <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>com.leyou.common</groupId> <artifactId>leyou-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.leyou.item</groupId> <artifactId>leyou-item-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> </project>
-
编写配置文件 application.yaml
server: port: 8084 spring: application: name: goods-web-service thymeleaf: cache: false eureka: client: service-url: defaultZone: http://localhost:10086/eureka instance: lease-renewal-interval-in-seconds: 5 lease-expiration-duration-in-seconds: 10
-
编写启动类
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class LeyouGoodsWebApplication { public static void main(String[] args) { SpringApplication.run(LeyouGoodsWebApplication.class, args); } }
1.2 创建页面模板
-
从 leyou-portal 项目中复制 item.html 模板到当前项目 resource 目录下的 templates 中。
-
把 HTML 的名称空间改成
xmlns:th="http://www.thymeleaf.org"
,这样页面就由 Thymeleaf 的引擎解析了。
1.3 页面跳转
1.3.1 修改页面跳转路径
当我们点击某个商品图片时,应该携带该商品的 SpuId 跳转到商品详情页。
例如:
http://www.leyou.com/item/2314123.html
我们打开 search.html,修改其中的商品路径:
1.3.2 Nginx 反向代理
接下来,我们要把这个地址指向我们的 leyou-goods-web
服务,其端口为 8084。
我们在 nginx.conf 中添加配置,并重启 Nginx
1.3.3 编写 Controller
在 leyou-goods-web 中编写 Controller,接收请求,并跳转到商品详情页
@Controller
@RequestMapping("/item")
public class GoodsController {
/**
* 跳转到商品详情页
*
* @param model
* @param id
* @return
*/
@GetMapping("/{id}.html")
public String toItemPage(Model model, @PathVariable("id") Long id) {
return "item";
}
}
1.3.4 测试
-
启动 leyou-goods-web 工程
-
点击一个搜索到的商品,成功跳转到商品详情页
1.4 后台提供接口
1.4.1 分析模型数据
首先我们一起来分析一下,在这个页面中需要哪些数据。
我们已知的条件是传递来的 Spu 的 id,我们需要根据 Spu 的 id 查询到下面的数据:
- Spu 信息
- Spu 详情
- Spu 下的所有 Sku
- 品牌
- 商品三级分类
- 规格参数组
- 规格参数
1.4.2 商品微服务提供接口
为了查询到上面的数据,我们需要在商品微服务中提供一些接口。
通过 Spu 的 id 查询 Spu
-
在 SpuApi 接口中添加方法 querySpuById
/** * 通过 spuId 查询 Spu * @param spuId * @return */ @GetMapping("{spuId}") public Spu querySpuById(@PathVariable("spuId") Long spuId);
-
在 SpuController 中添加方法 querySpuById
/** * 通过 spuId 查询 Spu * @param spuId * @return */ @GetMapping("{spuId}") public ResponseEntity<Spu> querySpuById(@PathVariable("spuId") Long spuId){ Spu spu = spuService.querySpuById(spuId); if (spu == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(spu); }
-
在 SpuService 中添加方法 querySpuById
/** * 通过 spuId 查询 Spu * @param spuId * @return */ public Spu querySpuById(Long spuId) { Spu spu = spuMapper.selectByPrimaryKey(spuId); return spu; }
通过分类 id 查询规格参数组
商品详情页需要展示商品的规格参数组,以及其下的规格参数。所以我们需要提供一个接口,通过 Spu 的 id 查询规格参数组,并将规格参数封装其中。
-
在 SpecificationApi 接口中添加方法 queryGroupWithCid
/** * 通过分类 id 查询规格参数组 * @param cid * @return */ @GetMapping("/group/param/{cid}") public List<SpecGroup> queryGroupWithCid(@PathVariable("cid") Long cid);
-
在 SpecificationController 中添加方法 queryGroupsWithParam
/** * 通过分类 id 查询规格参数组 * * @param cid * @return */ @GetMapping("/group/param/{cid}") public ResponseEntity<List<SpecGroup>> queryGroupsWithParam(@PathVariable("cid") Long cid) { List<SpecGroup> specGroups = specificationService.queryGroupsWithParam(cid); if (CollectionUtils.isEmpty(specGroups)) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(specGroups); }
-
在 SpecificationService 中添加方法 queryGroupsWithParam
/** * 通过分类 id 查询规格参数组 * * @param cid * @return */ public List<SpecGroup> queryGroupsWithParam(Long cid) { List<SpecGroup> specGroups = querySpecGroupsByCid(cid); for (SpecGroup specGroup : specGroups) { List<SpecParam> params = querySpecParams(specGroup.getId(), null, null, null); specGroup.setParams(params); } return specGroups; }
1.4.3 创建 FeignClient
我们在 leyou-goods-web 服务中,创建 FeignClient
@FeignClient(value = "item-service")
public interface BrandClient extends BrandApi {
}
@FeignClient(value = "item-service")
public interface SpuClient extends SpuApi {
}
@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {
}
@FeignClient(value = "item-service")
public interface SpecificationClient extends SpecificationApi {
}
1.4.4 商品详情微服务提供接口
再来回顾一下商品详情页需要的数据,如下:
- Spu 信息
- Spu 详情
- Spu 下的所有 Sku
- 品牌
- 商品三级分类
- 规格参数组
- 规格参数
我们可以使用 Map<String, Object>
的数据结构封装这些数据,第一个参数为数据名称,第二个参数为数据。
-
在 GoodsController 中查询到所需数据,并放入 model
@Controller @RequestMapping("/item") public class GoodsController { @Autowired private GoodsService goodsService; /** * 通过 spuId 查询所需数据 * * @param model * @param id * @return */ @GetMapping("/{id}.html") public String toItemPage(Model model, @PathVariable("id") Long id) { // 通过 spuId 查询所需数据 Map<String, Object> modelMap = this.goodsService.loadData(id); // 放入模型 model.addAllAttributes(modelMap); return "item"; } }
-
在 GoodsService 中添加方法 loadData
@Service public class GoodsService { @Autowired private BrandClient brandClient; @Autowired private CategoryClient categoryClient; @Autowired private SpuClient spuClient; @Autowired private SpecificationClient specificationClient; /** * 通过 spuId 查询所需数据 * @param spuId * @return */ public Map<String, Object> loadData(Long spuId) { Map<String, Object> map = new HashMap<>(); // 查询 Spu Spu spu = this.spuClient.querySpuById(spuId); // 查询 SpuDetail SpuDetail spuDetail = this.spuClient.querySpuDetailBySpuId(spuId); // 查询 Sku 集合 List<Sku> skus = this.spuClient.querySkusBySpuId(spuId); // 查询分类 List<Long> cids = Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()); List<String> names = this.categoryClient.queryNamesByIds(cids); List<Map<String, Object>> categories = new ArrayList<>(); for (int i = 0; i < cids.size(); i++) { Map<String, Object> categoryMap = new HashMap<>(); categoryMap.put("id", cids.get(i)); categoryMap.put("name", names.get(i)); categories.add(categoryMap); } // 查询品牌 Brand brand = this.brandClient.queryBrandById(spu.getBrandId()); // 查询规格参数组 List<SpecGroup> groups = this.specificationClient.queryGroupsWithParam(spu.getCid3()); // 查询特殊的规格参数 List<SpecParam> params = this.specificationClient.querySpecParams(null, spu.getCid3(), false, null); Map<Long, String> paramMap = new HashMap<>(); params.forEach(param -> { paramMap.put(param.getId(), param.getName()); }); // 封装 Spu map.put("spu", spu); // 封装 SpuDetail map.put("spuDetail", spuDetail); // 封装 Sku 集合 map.put("skus", skus); // 封装分类 map.put("categories", categories); // 封装品牌 map.put("brand", brand); // 封装规格参数组 map.put("groups", groups); // 封装特殊规格参数 map.put("paramMap", paramMap); return map; } }
1.4.5 测试
-
在 item.html 页面写一段 JS 代码,把模型中的数据取出观察
<script th:inline="javascript"> const a = /*[[${groups}]]*/ []; const b = /*[[${params}]]*/ []; const c = /*[[${categories}]]*/ []; const d = /*[[${spu}]]*/ {}; const e = /*[[${spuDetail}]]*/ {}; const f = /*[[${skus}]]*/ []; const g = /*[[${brand}]]*/ {}; </script>
-
重启 leyou-item 和 leyou-goods-web 工程
-
点击一个商品详情页,查看网页源码,成功查到数据
1.5 渲染页面
略,交给前端吧。最终效果如下:
2. 页面静态化
2.1 问题分析
现在我们的商品详情页会采用 Thymeleaf 模板引擎渲染后,再返回到客户端。
但这样在后台需要做大量的数据查询,而后渲染得到 HTML 页面。会对数据库造成压力,并且请求的响应时间过长,并发能力不高。
有没有办法解决这些问题呢?
- 首先,我们能想到的就是缓存技术。比如使用 Redis 缓存,不过 Redis 适合数据规模比较小的情况。假如数据量比较大,比如我们的商品详情页,每个页面如果 10 kb,100 万商品,就是 10 GB 空间,对内存占用比较大,此时就给缓存系统带来极大压力,如果缓存崩溃,接下来倒霉的就是数据库了。
- 其次,可以使用静态化技术。静态化是指把动态生成的 HTML 页面变为静态内容保存,以后用户的请求到来,直接访问静态页面,不再经过服务器的渲染。而静态的 HTML 页面可以部署在 Nginx 中,从而大大提高并发能力。
2.2 如何实现静态化
原来,我们商品详情页通过 Thymeleaf 模板引擎生成后,直接就返回给客户端了。
现在,我们生成商品详情页后,将它先部署一份在 Nginx 中,再返回给客户端。下一次在访问这个页面时,就直接访问 Niginx 中的静态页面。
2.3 实现页面静态化
-
在 leyou-goods-web 工程中的 service 包下创建 GoodsHtmlService
@Service public class GoodsHtmlService { @Autowired private GoodsService goodsService; @Autowired private TemplateEngine templateEngine; /** * 创建 HTML 静态页面 * * @param spuId * @throws Exception */ public void createHtml(Long spuId) { PrintWriter writer = null; try { // 获取页面数据 Map<String, Object> spuMap = this.goodsService.loadData(spuId); // 创建 Thymeleaf 上下文对象 Context context = new Context(); // 把数据放入上下文对象 context.setVariables(spuMap); // 创建输出流 File file = new File("D:\\nginx-1.14.0\\html\\item\\" + spuId + ".html"); writer = new PrintWriter(file); // 执行页面静态化方法 templateEngine.process("item", context, writer); } catch (Exception e) { e.printStackTrace(); } finally { if (writer != null) { writer.close(); } } } }
-
在 GoodsController 中调用页面静态化方法
/** * 通过 spuId 查询所需数据 * * @param model * @param id * @return */ @GetMapping("/{id}.html") public String toItemPage(Model model, @PathVariable("id") Long id) { // 通过 spuId 查询所需数据 Map<String, Object> modelMap = this.goodsService.loadData(id); // 放入模型 model.addAllAttributes(modelMap); // 页面静态化 goodsHtmlService.createHtml(id); return "item"; }
-
修改 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 html; if (!-f $request_filename) { #请求的文件不存在,就反向代理 proxy_pass http://127.0.0.1:8084; break; } } location / { proxy_pass http://127.0.0.1:9002; proxy_connect_timeout 600; proxy_read_timeout 600; } }
2.4 测试
-
重启商品详情微服务
-
重启 Nginx
-
访问一个商品详情页后,成功生成静态页面
-
再次访问,速度得到极大提升