随着项目的使用者越来越多,项目承担的压力也会越来越大,为了让我们的项目能服务更多的使用者,我们不得不需要把我们的单体项目拆分成多个微服务,就比如把一个商城系统拆分成用户系统,商品系统,订单系统,购物车系统,支付系统等等,每个微服务都对应着自己的数据库,别的服务不能访问,别的微服务需要通过本微服务的service层方法才能获取。把这些微服务分配到多个Tomcat上,使其能承担更多的压力。
但问题也来了,在原来的单体架构中,我们把所有系统的mapper写在同一个包中,service写在同一个包中,还有po、controller都各写在一个包中,这样某个微服务想获取别的微服务的数据库中的数据直接引入对应的service层方法就可以了。但现在各个微服务分开了,写成了多个module或project,不能通过直接导入service层方法获取其他微服务的数据库数据了。
我就拿查询购物车列表时,购物车微服务还需要调用商品微服务的service方法查询购物车中每个商品的详细信息。(购物车微服务的数据库中只会存商品的基本信息)
这是单体架构时的代码:
@Service
@RequiredArgsConstructor //只对加上final的必须初始化的成员变量写入构造函数,不想写入构造函数的不写final就行
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {private final IItemService itemService;@Overridepublic List<CartVO> queryMyCarts() {// 1.查询我的购物车列表List<Cart> carts = lambdaQuery().eq(Cart::getUserId, 1L/* UserContext.getUser()*/).list();if (CollUtils.isEmpty(carts)) {return CollUtils.emptyList();}// 2.转换VOList<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);// 3.处理VO中的商品信息handleCartItems(vos);// 4.返回return vos;}private void handleCartItems(List<CartVO> vos) {
// 1.获取商品idSet<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());// 2.查询商品List<ItemDTO> items = itemService.queryItemByIds(itemIds);if (CollUtils.isEmpty(items)) {return;}// 3.转为 id 到 item的mapMap<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));// 4.写入vofor (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());}
原代码中只有handleCartItems()方法中有对itemService的引用,所以咱们只需要该handleCartItems方法中的itemService业务就可以了。
所以这时就需要微服务直接的远程调用,远程调用有3种方法:
方法一:利用restTemplate(不推荐)
首先需要在启动类中声明resttemplate的bean
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartServiceApplication {public static void main(String[] args) {SpringApplication.run(CartServiceApplication.class, args);}@Beanpublic RestTemplate restTemplate(){return new RestTemplate();}
}
然后在需要改写的方法所在的类中,也就是CartServiceImpl类中注入该bean,我用的构造器注入。
@Service
@RequiredArgsConstructor //只对加上final的必须初始化的成员变量写入构造函数,不想写入构造函数的不写final就行
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {//private final IItemService itemService;private final RestTemplate restTemplate;}
然后改写handleCartItems方法
private void handleCartItems(List<CartVO> vos) {// 1.获取商品idSet<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());// 2.查询商品//List<ItemDTO> items = itemService.queryItemByIds(itemIds);//2.1 利用restTemplate发送http请求HashMap<String,Object> map=new HashMap<>();map.put("ids", itemIds.stream().map(String::valueOf).collect(Collectors.joining(",")));ResponseEntity<List<ItemDTO>> response = restTemplate.exchange("http://localhost:8081/items?ids={ids}", //请求路径HttpMethod.GET, //请求方式null, //请求体,可以为空//ItemDTO.class, //返回体的类型,应该返回ItemDTO的集合,字节码不能使用泛型,所以不能直接传list集合new ParameterizedTypeReference<List<ItemDTO>>() {}, //这里传的是个对象,可以用泛型//Map.of("ids",CollUtils.join(itemIds,","))map);//2.2 解析响应if (!response.getStatusCode().is2xxSuccessful()) {//查询失败return;}List<ItemDTO> items = response.getBody();if (CollUtils.isEmpty(items)) {return;}// 3.转为 id 到 item的mapMap<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));// 4.写入vofor (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());}
注意,restTemplate.exchange()方法中的最后一个map参数,如果你用的是jdk11及以上,就可以直接写Map.of()。我用的jdk8,没有Map.of方法所以我提前声明一个hashMap,并用stream流把itemIds转换成String形式并用逗号隔开。
由代码可知,这中方式访问其他微服务的路径是写死的,所以又有问题了,我们在开发微服务时,会有生产环境、测试环境等等,而且微服务一般都是集群部署,一个微服务会部署很多个端口后,然后有nginx负载均衡转发给其中的一个 ,并且如果其中一个端口号对应的服务宕机了,应该是不需要重启服务,就自动有本集群中其他端口号的服务立刻顶上,由这些原因可见,restTemplate是很不方便的。
方法二:nacos
购物车微服务的name为cart-service,商品微服务的name为item-service
nacos是个注册中心,我简单说一下nacos在远程调用过程中起到的作用:每个微服务在启动时都会向nacos注册本微服务的信息(包括端口号,本服务的名字等等),然后当购物车微服务想查询商品详细信息时,会根据微服务名字(item-service)从nacos中拉取商品微服务的相关信息(包括端口号等等),当商品微服务在nacos中注册了多个端口号的服务时,nacos会根据负载均衡原则帮你选出一个来给你提高服务。
首先需要引入nacos的依赖
<!-- nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
然后在application.yaml文件中写入nacos的地址(地址看你启动nacos的IP): 别忘了开启nacos
spring:cloud:nacos:server-addr: 127.0.0.1:8848
然后代码
@Service
@RequiredArgsConstructor //只对加上final的必须初始化的成员变量写入构造函数,不想写入构造函数的不写final就行
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {//private final IItemService itemService;private final RestTemplate restTemplate;private final DiscoveryClient discoveryClient;private void handleCartItems(List<CartVO> vos) {//1.获取商品idSet<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());// 2.查询商品//2.1根据服务名称获取服务的实例列表List<ServiceInstance> instances = discoveryClient.getInstances("item-service");if (CollUtil.isEmpty(instances)){return;}//2.2手写负载均衡,从实例列表中挑一个ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));//2.3 利用restTemplate发送http请求HashMap<String,Object> map=new HashMap<>();map.put("ids", itemIds.stream().map(String::valueOf).collect(Collectors.joining(",")));ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(serviceInstance.getUri()+"/items?ids={ids}", //请求路径HttpMethod.GET, //请求方式null, //请求体,可以为空//ItemDTO.class, //返回体的类型,应该返回ItemDTO的集合,字节码不能使用泛型,所以不能直接传list集合new ParameterizedTypeReference<List<ItemDTO>>() {}, //这里传的是个对象,可以用泛型//Map.of("ids",CollUtils.join(itemIds,","))map);//2.4 解析响应if (!response.getStatusCode().is2xxSuccessful()) {//查询失败return;}List<ItemDTO> items = response.getBody();if (CollUtils.isEmpty(items)) {return;}// 3.转为 id 到 item的mapMap<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));// 4.写入vofor (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());}
}
这样我们的路径就不是写死的了,但是原来本是
List<ItemDTO> items = itemService.queryItemByIds(itemIds); 这一行代码的事,现在呢,写了10多行代码才解决。如果不同微服务之间都需要互相调用,那岂不是每个方法都需要这么写,繁琐死,所以我们有了简明一点的第三种方法
第三种方法:openFeign
feign是一个声明式的http客户端,其作用是帮助我们优雅的实现http请求的发送。
声明式,意思是我们只需要把发请求所需要的信息声明好,剩下的我们不需要去管,全交给Feign去做。
步骤:
首先我们需要创建一个与购物车系统和商品系统同层次的系统,名字随便命名,我用的是hm-api。
然后在pom文件中引入相关依赖:
<!-- 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><!-- 引入okhttp连接池,提高feign的性能--><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId></dependency><dependency><groupId>io.swagger</groupId><artifactId>swagger-annotations</artifactId><version>1.6.6</version></dependency>
然后在hm-api项目下创建client,dto,config包。client包中写的是远程调用时调用的方法,dto是远程调用方法需要的实体类,config包是对Feign的配置。
我们查看需要远程调用的代码部分
List<ItemDTO> items = itemService.queryItemByIds(itemIds);
需要用到itemService.queryItemByIds方法,我们先去找它的controller中对应的方法
@RestController
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {private final IItemService itemService;@GetMappingpublic Collection<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids){return itemService.queryItemByIds(ids);}
}
然后把该controller中对应的方法抽取出来写入hm-api中的client包中
package com.hmall.hmapi.client;import com.hmall.hmapi.dto.ItemDTO;
import com.hmall.hmapi.dto.OrderDetailDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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);}
@FeignClient()注解中写要调用的微服务的name。
然后把代码中用到的ItemDto实体类拷贝到hm-api中的dto包中。
然后代码
@Service
@RequiredArgsConstructor //只对加上final的必须初始化的成员变量写入构造函数,不想写入构造函数的不写final就行
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {//private final IItemService itemService;//private final RestTemplate restTemplate;//private final DiscoveryClient discoveryClient;private final ItemClient itemClient;private void handleCartItems(List<CartVO> vos) {//openFeignSet<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());List<ItemDTO> items = itemClient.queryItemByIds(itemIds);if (CollUtils.isEmpty(items)) {return;}// 3.转为 id 到 item的mapMap<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));// 4.写入vofor (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());}}
}
现在代码就简短很多了。
因为每次远程调用访问数据库,都需要建立一次连接,损耗比较大,所以我们建议引入一个连接池。连接池的依赖我们前面已经引入了,现在需要做的只有开启连接池了,现在让我们去调用远程调用的application.yaml文件中,也就是cart-service服务中的application.yaml中,写上以下几行配置就可以了。
feign:okhttp:enabled: true # 打开feign连接池配置