目录
Service中的实现impl所继承的接口IService(各种方法):
[1. 创建新module - maven模块,并引入依赖](#1. 创建新module - maven模块,并引入依赖)
[2. 新建包com.hmall.xx(业务名),添加和修改启动类,新建mapper包、domain包 - service包 - controller包](#2. 新建包com.hmall.xx(业务名),添加和修改启动类,新建mapper包、domain包 - service包 - controller包)
[3. 拷贝并修改yaml配置文件到resources中,分别修改 端口号、服务名称、datasource(需创建sql datebase)、swagger接口文档说明与controller扫描包](#3. 拷贝并修改yaml配置文件到resources中,分别修改 端口号、服务名称、datasource(需创建sql datebase)、swagger接口文档说明与controller扫描包)
4.domain,mapper,service,controller包代码
[5. 刷新maven,添加该业务模块启动项到Services中,并把Active profiles 修改为 local](#5. 刷新maven,添加该业务模块启动项到Services中,并把Active profiles 修改为 local)
[6. 运行,在访问地址后面添加doc.html访问swagger接口文档,进行调试](#6. 运行,在访问地址后面添加doc.html访问swagger接口文档,进行调试)
2、启动服务,访问http://192.168.150.101:8848/nacos/控制台,可以发现服务注册成功:
[1、 添加依赖,配置Nacos](#1、 添加依赖,配置Nacos)
一、业务流程
1、登录
Controller中的接口:
java
@Api(tags = "用户相关接口")
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final IUserService userService;
@ApiOperation("用户登录接口")
@PostMapping("login")
public UserLoginVO login(@RequestBody @Validated LoginFormDTO loginFormDTO){
return userService.login(loginFormDTO);
}
}
Service中的实现impl:
java
public interface IUserService extends IService<User> {
UserLoginVO login(LoginFormDTO loginFormDTO);
void deductMoney(String pw, Integer totalFee);
}
Service中的实现impl所继承的接口IService(各种方法):
java
public interface IService<T> {
int DEFAULT_BATCH_SIZE = 1000;
default boolean save(T entity) {
return SqlHelper.retBool(this.getBaseMapper().insert(entity));
}
@Transactional(
rollbackFor = {Exception.class}
)
default boolean saveBatch(Collection<T> entityList) {
return this.saveBatch(entityList, 1000);
}
boolean saveBatch(Collection<T> entityList, int batchSize);
@Transactional(
rollbackFor = {Exception.class}
)
default boolean saveOrUpdateBatch(Collection<T> entityList) {
return this.saveOrUpdateBatch(entityList, 1000);
}
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);
}
VO:
java
@Data
public class UserLoginVO {
private String token;
private Long userId;
private String username;
private Integer balance;
}
DTO:
java
@Data
@ApiModel(description = "登录表单实体")
public class LoginFormDTO {
@ApiModelProperty(value = "用户名", required = true)
@NotNull(message = "用户名不能为空")
private String username;
@NotNull(message = "密码不能为空")
@ApiModelProperty(value = "用户名", required = true)
private String password;
@ApiModelProperty(value = "是否记住我", required = false)
private Boolean rememberMe = false;
}
2、搜索商品
在首页搜索框输入关键字手机,点击搜索即可进入搜索列表页面:
Controller中的接口:
java
@Api(tags = "搜索相关接口")
@RestController
@RequestMapping("/search")
@RequiredArgsConstructor
public class SearchController {
private final IItemService itemService;
@ApiOperation("搜索商品")
@GetMapping("/list")
public PageDTO<ItemDTO> search(ItemPageQuery query) {
// 分页查询
Page<Item> result = itemService.lambdaQuery()
.like(StrUtil.isNotBlank(query.getKey()), Item::getName, query.getKey())
.eq(StrUtil.isNotBlank(query.getBrand()), Item::getBrand, query.getBrand())
.eq(StrUtil.isNotBlank(query.getCategory()), Item::getCategory, query.getCategory())
.eq(Item::getStatus, 1)
.between(query.getMaxPrice() != null, Item::getPrice, query.getMinPrice(), query.getMaxPrice())
.page(query.toMpPage("update_time", false));
// 封装并返回
return PageDTO.of(result, ItemDTO.class);
}
}
3、购物车
在搜索到的商品列表中,点击按钮加入购物车
,即可将商品加入购物车:
加入成功后即可进入购物车列表页,查看自己购物车商品列表:
Controller中的接口:
java
@Api(tags = "购物车相关接口")
@RestController
@RequestMapping("/carts")
@RequiredArgsConstructor
public class CartController {
private final ICartService cartService;
@ApiOperation("添加商品到购物车")
@PostMapping
public void addItem2Cart(@Valid @RequestBody CartFormDTO cartFormDTO){
cartService.addItem2Cart(cartFormDTO);
}
@ApiOperation("更新购物车数据")
@PutMapping
public void updateCart(@RequestBody Cart cart){
cartService.updateById(cart);
}
@ApiOperation("删除购物车中商品")
@DeleteMapping("{id}")
public void deleteCartItem(@Param ("购物车条目id")@PathVariable("id") Long id){
cartService.removeById(id);
}
@ApiOperation("查询购物车列表")
@GetMapping
public List<CartVO> queryMyCarts(){
return cartService.queryMyCarts();
}
@ApiOperation("批量删除购物车中商品")
@ApiImplicitParam(name = "ids", value = "购物车条目id集合")
@DeleteMapping
public void deleteCartItemByIds(@RequestParam("ids") List<Long> ids){
cartService.removeByItemIds(ids);
}
}
其中,查询购物车列表时,由于要判断商品最新的价格和状态,所以还需要查询商品信息,业务流程如下:
二、拆分商品服务
1. 创建新module - maven模块,并引入依赖
创建新模块
选择maven模块,并设定JDK版本为11:
添加依赖
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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.heima</groupId>
<artifactId>hmall</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.qingshui</groupId>
<artifactId>item-service</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>3.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 新建包com.hmall.xx(业务名),添加和修改启动类,新建mapper包、domain包 - service包 - controller包
新建包com.hmall.item:
编写启动类ItemApplication:
java
@MapperScan("com.hmall.item.mapper")
@SpringBootApplication
public class ItemApplication {
public static void main(String[] args) {
SpringApplication.run(ItemApplication.class, args);
}
}
新建mapper包、domain包 - service包 - controller包:
3. 拷贝并修改yaml配置文件到resources中,分别修改 端口号、服务名称、datasource(需创建sql datebase)、swagger接口文档说明与controller扫描包
从hm-service中拷贝:
修改application.yaml为:
Matlab
server:
port: 8081
spring:
application:
name: item-service
profiles:
active: dev
datasource:
url: jdbc:mysql://${hm.db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: ${hm.db.pw}
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
update-strategy: not_null
id-type: auto
logging:
level:
com.hmall: debug
pattern:
dateformat: HH:mm:ss:SSS
file:
path: "logs/${spring.application.name}"
knife4j:
enable: true
openapi:
title: 商品服务接口文档
description: "信息"
email: zhanghuyi@itcast.cn
concat: 虎哥
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.hmall.item.controller
创建该模块的数据库:
4.domain,mapper,service,controller包代码
【1】domain包代码:dto、po、vo、(query)
dto:数据传输对象
OrderDetailDTO:
java
@ApiModel(description = "订单明细条目")
@Data
@Accessors(chain = true)
public class OrderDetailDTO {
@ApiModelProperty("商品id")
private Long itemId;
@ApiModelProperty("商品购买数量")
private Integer num;
}
ItemDTO:
java
@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;
}
po:实体
java
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("item")
public class Item implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 商品id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* SKU名称
*/
private String name;
/**
* 价格(分)
*/
private Integer price;
/**
* 库存数量
*/
private Integer stock;
/**
* 商品图片
*/
private String image;
/**
* 类目名称
*/
private String category;
/**
* 品牌名称
*/
private String brand;
/**
* 规格
*/
private String spec;
/**
* 销量
*/
private Integer sold;
/**
* 评论数
*/
private Integer commentCount;
/**
* 是否是推广广告,true/false
*/
@TableField("isAD")
private Boolean isAD;
/**
* 商品状态 1-正常,2-下架,3-删除
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 创建人
*/
private Long creater;
/**
* 修改人
*/
private Long updater;
}
vo:视图对象
在hm-service模块中没有与商品有关的代码。
query:分页查询
java
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "商品分页查询条件")
public class ItemPageQuery extends PageQuery {
@ApiModelProperty("搜索关键字")
private String key;
@ApiModelProperty("商品分类")
private String category;
@ApiModelProperty("商品品牌")
private String brand;
@ApiModelProperty("价格最小值")
private Integer minPrice;
@ApiModelProperty("价格最大值")
private Integer maxPrice;
}
【2】mapper包代码 :mapper接口 及mapper.xml文件
java
public interface ItemMapper extends BaseMapper<Item> {
@Update("UPDATE item SET stock = stock - #{num} WHERE id = #{itemId}")
void updateStock(OrderDetailDTO orderDetail);
}
【3】 service包:service接口及实现类
service接口:
java
public interface IItemService extends IService<Item> {
void deductStock(List<OrderDetailDTO> items);
List<ItemDTO> queryItemByIds(Collection<Long> ids);
}
修改sqlStatement后的实现类:
java
@Service
public class ItemServiceImpl extends ServiceImpl<ItemMapper, Item> implements IItemService {
@Override
public void deductStock(List<OrderDetailDTO> items) {
String sqlStatement = "com.hmall.mapper.item.ItemMapper.updateStock";
boolean r = false;
try {
r = executeBatch(items, (sqlSession, entity) -> sqlSession.update(sqlStatement, entity));
} catch (Exception e) {
throw new BizIllegalException("更新库存异常,可能是库存不足!", e);
}
if (!r) {
throw new BizIllegalException("库存不足!");
}
}
@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
return BeanUtils.copyList(listByIds(ids), ItemDTO.class);
}
}
【4】controller包
java
@Api(tags = "商品管理相关接口")
@RestController
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {
private final IItemService itemService;
@ApiOperation("分页查询商品")
@GetMapping("/page")
public PageDTO<ItemDTO> queryItemByPage(PageQuery query) {
// 1.分页查询
Page<Item> result = itemService.page(query.toMpPage("update_time", false));
// 2.封装并返回
return PageDTO.of(result, ItemDTO.class);
}
@ApiOperation("根据id批量查询商品")
@GetMapping
public List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids){
return itemService.queryItemByIds(ids);
}
@ApiOperation("根据id查询商品")
@GetMapping("{id}")
public ItemDTO queryItemById(@PathVariable("id") Long id) {
return BeanUtils.copyBean(itemService.getById(id), ItemDTO.class);
}
@ApiOperation("新增商品")
@PostMapping
public void saveItem(@RequestBody ItemDTO item) {
// 新增
itemService.save(BeanUtils.copyBean(item, Item.class));
}
@ApiOperation("更新商品状态")
@PutMapping("/status/{id}/{status}")
public void updateItemStatus(@PathVariable("id") Long id, @PathVariable("status") Integer status){
Item item = new Item();
item.setId(id);
item.setStatus(status);
itemService.updateById(item);
}
@ApiOperation("更新商品")
@PutMapping
public void updateItem(@RequestBody ItemDTO item) {
// 不允许修改商品状态,所以强制设置为null,更新时,就会忽略该字段
item.setStatus(null);
// 更新
itemService.updateById(BeanUtils.copyBean(item, Item.class));
}
@ApiOperation("根据id删除商品")
@DeleteMapping("{id}")
public void deleteItemById(@PathVariable("id") Long id) {
itemService.removeById(id);
}
@ApiOperation("批量扣减库存")
@PutMapping("/stock/deduct")
public void deductStock(@RequestBody List<OrderDetailDTO> items){
itemService.deductStock(items);
}
}
5. 刷新maven,添加该业务模块启动项到Services中,并把Active profiles 修改为 local
6. 运行,在访问地址后面添加doc.html访问swagger接口文档,进行调试
三、拆分购物车服务
同上
四、服务调用(RPC)
背景:
在拆分的时候,我们发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了
item-service
服务,导致我们无法查询。(也就是一个模块要进行查询但是查询代码都在另外一个模块)那么问题来了:我们该如何跨服务调用,准确的说,如何在
cart-service
中获取item-service
服务中的提供的商品数据呢?
因此,现在查询购物车列表的流程变成了这样:
假如我们在cart-service中能模拟浏览器,发送http请求到item-service,是不是就实现了跨微服务的远程调用了呢(也就是用Java代码发送Http请求)
RestTemplate
Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送。
其中提供了大量的方法,方便我们发送Http请求,例如:
我们在cart-service
服务中定义一个配置类
先将RestTemplate注册为一个Bean:
java
package com.hmall.cart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RemoteCallConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
远程直接调用
在这个过程中,item-service
提供了查询接口,cart-service
利用Http请求调用该接口。因此item-service
可以称为服务的提供者,而cart-service
则称为服务的消费者或服务调用者。
五、Nacos注册中心
背景:
RPC通过Http请求实现了跨微服务的远程调用,这种手动发送Http请求的方式存在一些问题。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:
此时,每个item-service
的实例其IP或端口不同,问题来了:
-
item-service这么多实例,cart-service如何知道每一个实例的地址?
-
http请求要写url地址,
cart-service
服务到底该调用哪个实例呢? -
如果在运行过程中,某一个
item-service
实例宕机,cart-service
依然在调用该怎么办? -
如果并发太高,
item-service
临时多部署了N台实例,cart-service
如何知道新实例的地址?
这时我们需要注册中心 (其实也是建立一个连接池)
流程如下:
-
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
-
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
-
调用者自己对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
-
服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
-
当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
-
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
-
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
服务注册
将模块(微服务)注册到Nacos
1、添加依赖,配置Nacos
在item-service
的pom.xml
中添加依赖:
XML
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
在item-service
的application.yml
中添加nacos地址配置:
将nacos地址修改为自己的虚拟机地址
XML
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
2、启动服务,访问http://192.168.150.101:8848/nacos/控制台,可以发现服务注册成功:
服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现 。
1、 添加依赖,配置Nacos
服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
我们在cart-service
中的pom.xml
中添加下面的依赖:
XML
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
在cart-service
的application.yml
中添加nacos地址配置:
XML
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
2、根据负载均衡算法发现并调用方法
接下来,服务调用者cart-service
就可以去订阅item-service
服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
-
随机
-
轮询
-
IP的hash
-
最近最少访问
-
...
这里我们可以选择最简单的随机负载均衡。
另外,服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口:
但现在不需要了,我们通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用: