导入黑马商城项目
创建Mysql服务
由于已有相关项目则要关闭DockerComponent中的已开启的项目
[root@server02 ~]# docker compose down
WARN[0000] /root/docker-compose.yml: `version` is obsolete
[+] Running 4/4
✔ Container nginx Removed 0.2s
✔ Container hmall Removed 0.4s
✔ Container mysql Removed 1.5s
✔ Network hmall Removed 0.1s
[root@server02 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@server02 ~]#
创建网络用来连接项目容器
[root@server02 ~]# docker network create hm-net
025bba52632ae4d789c1281fb0114fd10f58fe9b0cf7c998a32c660b2eb889e9
[root@server02 ~]#
创建并运行Mysql容器
[root@server02 ~]# docker run -d \
> --name mysql \
> -p 3306:3306 \
> -e TZ=Asia/Shanghai \
> -e MYSQL_ROOT_PASSWORD=123 \
> -v /root/mysql/data:/var/lib/mysql \
> -v /root/mysql/conf:/etc/mysql/conf.d \
> -v /root/mysql/init:/docker-entrypoint-initdb.d \
> --network hm-net\
> mysql
a88243bf07574804e6a580e66dbf5b885c73c4d72e4a33b288c4d1f1f7541bea
[root@server02 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a88243bf0757 mysql "docker-entrypoint.s…" 6 seconds ago Up 5 seconds 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp mysql
[root@server02 ~]#
导入后端代码
打开准备的后端代码
导入前端代码
将提供的hmail-nginx文件迁移到英文目录下,使用命令行启动
start nginx.exe
单体结构
微服务项目
SpringCloud
微服务的技术栈。
微服务拆分
熟悉黑马商城
黑马商城的组成
拆分原则
服务拆分
黑马商城采用Maven聚合来进行微服务。
案例:拆分服务
拆分商品相关微服务:
远程调用
由于启动类也是一个配置类,因此在启动类添加Bean容器
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
修改后的service文件
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final RestTemplate restTemplate;
@Override
public void addItem2Cart(CartFormDTO cartFormDTO) {
// 1.获取登录用户
Long userId = UserContext.getUser();
// 2.判断是否已经存在
if (checkItemExists(cartFormDTO.getItemId(), userId)) {
// 2.1.存在,则更新数量
baseMapper.updateNum(cartFormDTO.getItemId(), userId);
return;
}
// 2.2.不存在,判断是否超过购物车数量
checkCartsFull(userId);
// 3.新增购物车条目
// 3.1.转换PO
Cart cart = BeanUtils.copyBean(cartFormDTO, Cart.class);
// 3.2.保存当前用户
cart.setUserId(userId);
// 3.3.保存到数据库
save(cart);
}
@Override
public List<CartVO> queryMyCarts() {
// 1.查询我的购物车列表
List<Cart> carts = lambdaQuery().eq(Cart::getUserId, 1L /*TODO UserContext.getUser()*/).list();
if (CollUtils.isEmpty(carts)) {
return CollUtils.emptyList();
}
// 2.转换VO
List<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);
// 3.处理VO中的商品信息
handleCartItems(vos);
// 4.返回
return vos;
}
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id TODO 处理商品信息
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
//通过restTemplate查询商品信息
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
CollUtil.join(itemIds, ",")
);
List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
throw new BadRequestException("购物车中商品不存在!");
}
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
@Override
public void removeByItemIds(Collection<Long> itemIds) {
// 1.构建删除条件,userId和itemId
QueryWrapper<Cart> queryWrapper = new QueryWrapper<Cart>();
queryWrapper.lambda()
.eq(Cart::getUserId, UserContext.getUser())
.in(Cart::getItemId, itemIds);
// 2.删除
remove(queryWrapper);
}
private void checkCartsFull(Long userId) {
int count = Math.toIntExact(lambdaQuery().eq(Cart::getUserId, userId).count());
if (count >= 10) {
throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", 10));
}
}
private boolean checkItemExists(Long itemId, Long userId) {
int count = Math.toIntExact(lambdaQuery()
.eq(Cart::getUserId, userId)
.eq(Cart::getItemId, itemId)
.count());
return count > 0;
}
}
注入RestTemplate来进行信息交互。
private final RestTemplate restTemplate;
由于springboot不推荐@Autoword注入因此使用无参构造进行注入Lombok中的@RequiredArgsConstructor注解可以将类中所有必要的参数加入无参构造(final),以此进行注入
获取商品信息并返回:
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id TODO 处理商品信息
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
//通过restTemplate查询商品信息
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
CollUtil.join(itemIds, ",")
);
List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
throw new BadRequestException("购物车中商品不存在!");
}
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
修改位置:
// 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
//通过restTemplate查询商品信息
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",//要访问的接口位置
HttpMethod.GET,//访问的类型
null,//requestEnitiy设置为空
new ParameterizedTypeReference<List<ItemDTO>>() {
},//用以将List<ItemDTO>作为泛型传入
CollUtil.join(itemIds, ",")//要携带的属性
);
List<ItemDTO> items = response.getBody();//获取数据
服务治理
注册中心原理
Nacos注册中心
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
1、创建nacos:
在数据库中导入nacos的数据库
2、在docker中创建nacos
- 将配置文件导入虚拟机
- 将nacos的tar包导入虚拟机,nacos.tar
- 上传nacos包到本地镜像
docker load -i nacos.tar
- 创建nacos容器
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
0
[root@server02 ~]# docker load -i nacos.tar
9c1b6dd6c1e6: Loading layer 83.9MB/83.9MB
13a34b6fff78: Loading layer 5.177MB/5.177MB
8373e87e0617: Loading layer 3.584kB/3.584kB
dc68e54a0b44: Loading layer 109.3MB/109.3MB
445489eb2a14: Loading layer 2.048kB/2.048kB
cb4f3f38c79e: Loading layer 127.6MB/127.6MB
91e3449baf4f: Loading layer 7.68kB/7.68kB
a28384a784e0: Loading layer 5.632kB/5.632kB
8fa6b76bdc24: Loading layer 3.072kB/3.072kB
5f70bf18a086: Loading layer 1.024kB/1.024kB
Loaded image: nacos/nacos-server:v2.1.0-slim
[root@server02 ~]#
[root@server02 ~]#
[root@server02 ~]# dis
REPOSITORY TAG IMAGE ID CREATED SIZE
root-hmall latest 3e78751a9f66 42 hours ago 370MB
docker-demo 1.0 9e95eddcf939 43 hours ago 319MB
mysql latest be960704dfac 2 weeks ago 602MB
nacos/nacos-server v2.1.0-slim 49addbd025a1 2 years ago 322MB
openjdk 11.0-jre-buster 57925f2e4cff 2 years ago 301MB
nginx latest 3f8a4339aadd 6 years ago 108MB
[root@server02 ~]# docker run -d \
> --name nacos \
> --env-file ./nacos/custom.env \
> -p 8848:8848 \
> -p 9848:9848 \
> -p 9849:9849 \
> --restart=always \
> docker ps -a^C
[root@server02 ~]#
[root@server02 ~]# docker run -d \
> --name nacos \
> --env-file ./nacos/custom.env \
> -p 8848:8848 \
> -p 9848:9848 \
> -p 9849:9849 \
> --restart=always \
> nacos/nacos-server:v2.1.0-slim
a46373d69c1dcaf7a8183a21b81aeec3c790e3016d25607e90930e94e08119a6
[root@server02 ~]# dps
CONTAINER ID IMAGE PORTS STATUS NAMES
a46373d69c1d nacos/nacos-server:v2.1.0-slim 0.0.0.0:8848->8848/tcp, :::8848->8848/tcp, 0.0.0.0:9848-9849->9848-9849/tcp, :::9848-9849->9848-9849/tcp Up 21 seconds nacos
dc43e5e833fe mysql 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp Up 13 minutes mysql
[root@server02 ~]#
服务注册
服务发现
引入依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置RestTemplate类
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
引入nacos的地址:
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
修改代码:
private final RestTemplate restTemplate;
//引入 服务发现 客户端,使用该客户端获取对应的服务列表
private final DiscoveryClient discoveryClient;
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id TODO 处理商品信息
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
//获取服务列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
//进行负载均衡(随机负载均衡)
ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));
//获取该服务的URL
URI uri = serviceInstance.getUri();
//通过restTemplate查询商品信息
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
uri+"/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
CollUtil.join(itemIds, ",")
);
List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
throw new BadRequestException("购物车中商品不存在!");
}
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
OpenFeign
引入依赖:
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
在启动项添加注解:
@EnableFeignClients //启用openfeign
@EnableFeignClients //启用openfeign
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
创建接口:
import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Collection;
import java.util.List;
@FeignClient("item-service")
public interface ItemClient {
// 获取商品详情
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
修改代码:
//注入bean
private final ItemClient itemClient;
// private final RestTemplate restTemplate;
//
// //引入 服务发现 客户端,使用该客户端获取对应的服务列表
// private final DiscoveryClient discoveryClient;
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id TODO 处理商品信息
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// // 2.查询商品
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
// //获取服务列表
// List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
//
// //进行负载均衡(随机负载均衡)
// ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));
// //获取该服务的URL
// URI uri = serviceInstance.getUri();
//
// //通过restTemplate查询商品信息
// ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
// uri+"/items?ids={ids}",
// HttpMethod.GET,
// null,
// new ParameterizedTypeReference<List<ItemDTO>>() {
// },
// CollUtil.join(itemIds, ",")
// );
// List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
throw new BadRequestException("购物车中商品不存在!");
}
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
连接池
引入okhttp的依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
修改配置文件,开启连接池
feign:
okhttp:
enabled: true # 开启OKHttp功能
最佳实践
采用方法二进行改造
- 创建 hm-api 模块。
- 在该模块下创建 com.hmall.api.client包和com.hmall.api.dto和com.hmall.api.config包
- 在cart-servicer启动项 添加扫描包
- @EnableFeignClients(basePackages = "com.hmall.api.client")
- 引入模块cart-servicer模块中的代码
@FeignClient("item-service")
public interface ItemClient {
// 获取商品详情
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
@Data
@ApiModel(description = "商品实体")
public class ItemDTO {
@ApiModelProperty("商品id")
private Long id;
@ApiModelProperty("SKU名称")
private String name;
@ApiModelProperty("价格(分)")
private Integer price;
@ApiModelProperty("库存数量")
private Integer stock;
@ApiModelProperty("商品图片")
private String image;
@ApiModelProperty("类目名称")
private String category;
@ApiModelProperty("品牌名称")
private String brand;
@ApiModelProperty("规格")
private String spec;
@ApiModelProperty("销量")
private Integer sold;
@ApiModelProperty("评论数")
private Integer commentCount;
@ApiModelProperty("是否是推广广告,true/false")
private Boolean isAD;
@ApiModelProperty("商品状态 1-正常,2-下架,3-删除")
private Integer status;
}