课程文档
目录
一、业务流程
1、登录
Controller中的接口:
Service中的实现impl:
Service中的实现impl所继承的接口IService(各种方法):
VO:
DTO:
2、搜索商品
Controller中的接口:
3、购物车
Controller中的接口:
二、拆分商品服务
1. 创建新module - maven模块,并引入依赖
2. 新建包com.hmall.xx(业务名),添加和修改启动类,新建mapper包、domain包 - service包 - controller包
3. 拷贝并修改yaml配置文件到resources中,分别修改 端口号、服务名称、datasource(需创建sql datebase)、swagger接口文档说明与controller扫描包
4.domain,mapper,service,controller包代码
5. 刷新maven,添加该业务模块启动项到Services中,并把Active profiles 修改为 local
6. 运行,在访问地址后面添加doc.html访问swagger接口文档,进行调试
三、拆分购物车服务
四、服务调用(RPC)
RestTemplate
远程直接调用
五、Nacos注册中心
服务注册
1、添加依赖,配置Nacos
2、启动服务,访问http://192.168.150.101:8848/nacos/控制台,可以发现服务注册成功:
服务发现
1、 添加依赖,配置Nacos
2、根据负载均衡算法发现并调用方法
一、业务流程
1、登录
Controller中的接口:
@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:
public interface IUserService extends IService<User> {UserLoginVO login(LoginFormDTO loginFormDTO);void deductMoney(String pw, Integer totalFee);
}
Service中的实现impl所继承的接口IService(各种方法):
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:
@Data
public class UserLoginVO {private String token;private Long userId;private String username;private Integer balance;
}
DTO:
@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中的接口:
@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中的接口:
@Api(tags = "购物车相关接口")
@RestController
@RequestMapping("/carts")
@RequiredArgsConstructor
public class CartController {private final ICartService cartService;@ApiOperation("添加商品到购物车")@PostMappingpublic void addItem2Cart(@Valid @RequestBody CartFormDTO cartFormDTO){cartService.addItem2Cart(cartFormDTO);}@ApiOperation("更新购物车数据")@PutMappingpublic void updateCart(@RequestBody Cart cart){cartService.updateById(cart);}@ApiOperation("删除购物车中商品")@DeleteMapping("{id}")public void deleteCartItem(@Param ("购物车条目id")@PathVariable("id") Long id){cartService.removeById(id);}@ApiOperation("查询购物车列表")@GetMappingpublic List<CartVO> queryMyCarts(){return cartService.queryMyCarts();}@ApiOperation("批量删除购物车中商品")@ApiImplicitParam(name = "ids", value = "购物车条目id集合")@DeleteMappingpublic void deleteCartItemByIds(@RequestParam("ids") List<Long> ids){cartService.removeByItemIds(ids);}
}
其中,查询购物车列表时,由于要判断商品最新的价格和状态,所以还需要查询商品信息,业务流程如下:
二、拆分商品服务
1. 创建新module - maven模块,并引入依赖
创建新模块
选择maven模块,并设定JDK版本为11:
添加依赖
<?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:
@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为:
server:port: 8081
spring:application:name: item-serviceprofiles:active: devdatasource:url: jdbc:mysql://${hm.db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: ${hm.db.pw}
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandlerglobal-config:db-config:update-strategy: not_nullid-type: auto
logging:level:com.hmall: debugpattern:dateformat: HH:mm:ss:SSSfile:path: "logs/${spring.application.name}"
knife4j:enable: trueopenapi:title: 商品服务接口文档description: "信息"email: zhanghuyi@itcast.cnconcat: 虎哥url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.hmall.item.controller
创建该模块的数据库:
4.domain,mapper,service,controller包代码
【1】domain包代码:dto、po、vo、(query)
dto:数据传输对象
OrderDetailDTO:
@ApiModel(description = "订单明细条目")
@Data
@Accessors(chain = true)
public class OrderDetailDTO {@ApiModelProperty("商品id")private Long itemId;@ApiModelProperty("商品购买数量")private Integer num;
}
ItemDTO:
@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:实体
@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:分页查询
@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文件
public interface ItemMapper extends BaseMapper<Item> {@Update("UPDATE item SET stock = stock - #{num} WHERE id = #{itemId}")void updateStock(OrderDetailDTO orderDetail);
}
【3】 service包:service接口及实现类
service接口:
public interface IItemService extends IService<Item> {void deductStock(List<OrderDetailDTO> items);List<ItemDTO> queryItemByIds(Collection<Long> ids);
}
修改sqlStatement后的实现类:
@Service
public class ItemServiceImpl extends ServiceImpl<ItemMapper, Item> implements IItemService {@Overridepublic 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("库存不足!");}}@Overridepublic List<ItemDTO> queryItemByIds(Collection<Long> ids) {return BeanUtils.copyList(listByIds(ids), ItemDTO.class);}
}
【4】controller包
@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批量查询商品")@GetMappingpublic 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("新增商品")@PostMappingpublic 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("更新商品")@PutMappingpublic 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:
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 {@Beanpublic 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
中添加依赖:
<!--nacos 服务注册发现-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
在item-service
的application.yml
中添加nacos地址配置:
将nacos地址修改为自己的虚拟机地址
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
中添加下面的依赖:
<!--nacos 服务注册发现-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
在cart-service
的application.yml
中添加nacos地址配置:
spring:cloud:nacos:server-addr: 192.168.150.101:8848
2、根据负载均衡算法发现并调用方法
接下来,服务调用者cart-service
就可以去订阅item-service
服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
-
随机
-
轮询
-
IP的hash
-
最近最少访问
-
...
这里我们可以选择最简单的随机负载均衡。
另外,服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口:
但现在不需要了,我们通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用: