此次跟随教程完成项目,主要目的是学习一些新的技术,掌握一些开发工具的使用。因此,并不会对事务逻辑进行细致记录。此篇博客仅用于博主本人日后搭建项目时的参考,并非教程
开始制作商品详情页:页面渲染有两种方法,第一种是先加载静态页面,然后通过ajax异步加载数据后进行渲染。第二种是向服务端发起请求,获取数据后页面在后端完成渲染后直接返回。这里我们采取第二种方式,但是最后要做页面静态化处理。
在SpringBoot中已经不推荐使用JSP了,所以我们会使用模板引擎技术thymeleaf来渲染页面。
创建一个新的工程专门用来处理静态页:ly-page
pom.xml文件如下:
<?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.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.service</groupId>
<artifactId>ly-page</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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.service</groupId>
<artifactId>ly-item-interface</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
这里前缀会被默认配置为"classpath:/templates/",后缀为".html"。因此我们在resources文件夹下创建一个名为templates的文件夹。
添加启动类如下:
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LyPageApplication {
public static void main(String[] args) {
SpringApplication.run(LyPageApplication.class);
}
}
我们在开发阶段,不希望有页面缓存,所以配置文件如下:
server:
port: 8084
spring:
application:
name: page-service
thymeleaf:
cache: false #禁用thymeleaf的缓存
rabbitmq:
host: 192.168.114.129
username: leyou
password: leyou
virtual-host: /leyou
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
加了配置文件后,我们发现更改后页面仍然不会刷新,这是因为idea的原因。我么在更新页面后需要使用Ctrl+Shift+F9来rebuild一下页面才会刷新。现在将准备好的页面copy到templates目录下。Thymeleaf的语法就不在这里详细记录了,只记录几条小要点:
- 更改html的名称空间如下:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 添加启动类后在html中写表达式时才会出现相应提示
- 表达式应写在属性中,记得和el表达式进行区分(user为后台传回的自定义对象):
<h2>
<p>Name: <span th:text="${user.name}">Jack</span>.</p>
<p>Age: <span th:text="${user.age}">21</span>.</p>
<p>friend: <span th:text="${user.friend.name}">Rose</span>.</p>
</h2>
- 通过js代码取值方法:
<script th:inline="javascript">
const user = /*[[${user}]]*/ {};
const age = /*[[${user.age}]]*/ 20;
console.log(user);
console.log(age)
</script>
因为我们的请求路径为www.leyou.com/item/id.html,所以我们如果希望把此请求交由page工程处理,需要在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 / {
proxy_pass http://192.168.1.13:9002;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
#新添加的内容,此路径的请求均交给8084端口处理
location/item {
proxy_pass http://192.168.1.13:8084;
}
}
重启nginx:nginx -s reload
为了处理请求,我们需要创建web层,代码如下:
package com.leyou.page.web;
import com.leyou.page.service.PageService;
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.PathVariable;
import java.util.Map;
@Controller
public class PageController {
@Autowired
private PageService pageService;
@GetMapping("item/{id}.html")
public String toItemPage(@PathVariable("id") Long spuId, Model model) {
//查询数据模型
Map<String,Object> attributes = pageService.loadModel(spuId);
// 准备模型数据
model.addAllAttributes(attributes);
// 返回视图
return "item";
}
}
loadModel方法用于返回一个Map,这里面通过key-value方式封装了所有的attribute,避免了语句重复,addAllAttributes方法直接传入map即可。service中的loadModel方法如下:
package com.leyou.page.service;
import com.leyou.item.pojo.*;
import com.leyou.page.client.BrandClient;
import com.leyou.page.client.CategoryClient;
import com.leyou.page.client.GoodsClient;
import com.leyou.page.client.SpecificationClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import java.io.File;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class PageService {
@Autowired
private BrandClient brandClient;
@Autowired
private CategoryClient categoryClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specificationClient;
public Map<String, Object> loadModel(Long spuId) {
Map<String,Object> model = new HashMap<>();
// 查询spu
Spu spu = goodsClient.querySpuById(spuId);
// 查询skus
List<Sku> skus = spu.getSkus();
// 查询详情
SpuDetail spuDetail = spu.getSpuDetail();
// 查询brand
Brand brand = brandClient.queryBrandById(spu.getBrandId());
// 查询商品分类
List<Category> categories = categoryClient.queryCategoryByIds(
Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
// 查询规格参数
List<SpecGroup> specs = specificationClient.queryGroupByCid(spu.getCid3());
model.put("title",spu.getTitle());
model.put("subTitle",spu.getSubTitle());
model.put("skus",skus);
model.put("detail",spuDetail);
model.put("brand",brand);
model.put("categories",categories);
model.put("specs",specs);
return model;
}
}
这样我们就实现了商品详情页的基本功能,现在来实现静态化。我们将模板引擎的输出目的地进行改写,使其不再写入response而是写到本地文件。springboot中已经自动配置了模板引擎,直接使用即可。在service层中注入模板引擎并添加创建静态页面和删除页面的方法如下:
@Autowired
private TemplateEngine templateEngine;
public void createHtml(Long spuId) {
// 上下文
Context context = new Context();
context.setVariables(loadModel(spuId));
// 输出流
File dest = new File("E:/html", spuId + ".html");
if(dest.exists()) {
dest.delete();
}
try(PrintWriter writer = new PrintWriter(dest, "UTF-8")) {
// 生成html
templateEngine.process("item", context,writer);
}catch (Exception e) {
log.error("[静态页服务] 生成静态页服务异常!",e);
}
}
public void deleteHtml(Long spuId) {
File dest = new File("E:/html", spuId + ".html");
if(dest.exists()) {
dest.delete();
}
}
在执行此方法后,我们可以在相应的位置找到html文件。静态资源应交给nginx进行管理,但是在此次开发中,我们的nginx安装在虚拟机上,所以暂时只能手动上传,我们可以在部署项目时,将路径进行修改,然后将page项目与nginx部署在一起,这样可以直接将html文件写到nginx的相关路径下。现在,我们将生成的静态页面上传,然后修改nginx的配置文件,当用户发起请求时,nginx会先到本地进行查询,如果找不到,才交由page项目处理。修改nginx配置文件如下:
location /item {
# 先找本地
root html;
if (!-f $request_filename) { #请求的文件不存在,就反向代理
proxy_pass http://相应的ip:8084;
break;
}
}
记得重启nginx:nginx -s reload,静态化处理完成。
当我们以管理者身份对商品进行增删改操作时,仅仅只是改变了数据库,然而我们的索引库和商品静态页实际上也需要改变。但是如果由搜索微服务和静态页微服务提供相应接口,再由商品微服务在增删改业务中进行调用,就违背了设计思想也不便于后期维护。所以我们采用消息队列RabbitMQ来解决这个问题。
首先上传安装所需的文件,在/home/leyou下创建文件夹名为mq。将文件传到mq下。首先安装erlang的虚拟机,这个需要联网安装。
yum install esl-erlang_17.3-1~centos~6_amd64.rpm -y
yum install esl-erlang-compat-R14B-1.el6.noarch.rpm -y
虚拟机安装完成,开始安装rabbitmq。
rpm -ivh rabbitmq-server-3.4.1-1.noarch.rpm
安装完成,但是想要运行的话,我们需要把配置文件安装到/etc下,执行下列命令行:
cp /usr/share/doc/rabbitmq-server-3.4.1/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
进入配置文件对其进行修改:
vim /etc/rabbitmq/rabbitmq.config
通过输入/loopback_进行搜索。
删除前面的%%和最后面的逗号:
设置开机启动的命令:
chkconfig rabbitmq-server on
手动开启,关闭,重启的命令:
service rabbitmq-server start
service rabbitmq-server stop
service rabbitmq-server restart
通过命令开启web管理插件后重启:
rabbitmq-plugins enable rabbitmq_management
service rabbitmq-server restart
现在我们可以在浏览器中访问了,端口号为15672,初始账号和密码都为guest。
Spring已经支持了AMQP,我们直接对项目进行改造。商品微服务为消息的生产者,首先添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
改写配置文件:
spring:
rabbitmq:
host: 192.168.114.129
username: leyou
password: leyou
virtual-host: /leyou
template:
retry:
enabled: true #开启重试
initial-interval: 10000ms #初始重试间隔时间
max-interval: 30000ms #最大重试间隔时间
multiplier: 2 #重试时间倍数
exchange: ly.item.exchange #默认的交换机
publisher-confirms: true #生产者确认,如果消息发送失败会重试
在goodsService中注入template,并在代码中添加发送消息的语句。
@Autowired
private AmqpTemplate amqpTemplate;
//发送MQ消息
amqpTemplate.convertAndSend("item.update",spu.getId());
template中已经提供了发送消息的方法,我么已经在配置文件中配置了交换机,因此在这里我们可以不用再次指定。
改造好商品微服务后,我们改造静态页微服务和搜索微服务。
静态页微服务:
同样添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
更改配置文件:
pring:
rabbitmq:
host: 192.168.114.129
username: leyou
password: leyou
virtual-host: /leyou
添加配置类,监听消息并在得到相应的消息后调用方法实现删除页面或添加更新页面:
package com.leyou.page.mq;
import com.leyou.page.service.PageService;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ItemListener {
@Autowired
private PageService pageService;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "page.item.insert.queue",durable = "true"),
exchange = @Exchange(name = "ly.item.exchange",type = ExchangeTypes.TOPIC),
key = {"item.insert","item.update"}
))
public void listenInsertOrUpdate(Long spuId) {
if(spuId == null) {
return;
}
pageService.createHtml(spuId);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "page.item.delete.queue",durable = "true"),
exchange = @Exchange(value = "ly.item.exchange",type = ExchangeTypes.TOPIC),
key = {"item.delete"}
))
public void listenDelete(Long spuId) {
if (spuId == null) {
return;
}
pageService.deleteHtml(spuId);
}
}
完成!搜索微服务的改造方式和静态页微服务类似,就不再记录。
第四部分到此为止