服务间调用
Sprint提供了RestTemplate工具,可以实现服务间调用
注入IOC的方式
Spring推荐使用构造函数注入,而不是简单的Autoware注入
1 2 3 4
| private RestTemplate restTemplate; public CartServiceImpl(RestTemplate restTemplate) { this.restTemplate = restTemplate; }
|
我们可以通过搭配使用lombok来实现注解注入构造函数
@AllArgsConstructor注解可以为所有成员变量生成构造函数,但这种方式并不是我们想要的
因为有一些变量是无需加入构造函数的(也无需使用IOC容器注入)
用final来修饰的变量必须有初值,所以我们可以搭配@RequiredArgsConstructor注释,实现给必要的变量生成构造函数,其最终效果为:为使用final修饰的属性加构造函数
1 2 3 4 5 6
| @RequiredArgsConstructor public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService { private final RestTemplate restTemplate; …… }
|
RestTemplate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| public class YourService {
private final RestTemplate restTemplate;
public YourService(RestTemplate restTemplate) { this.restTemplate = restTemplate; }
public void processCarts(List<Long> itemIds) { String idsParam = CollUtil.join(itemIds, ",");
ResponseEntity<List<ItemDTO>> re = restTemplate.exchange( "http://localhost:8081/items?ids={ids}", HttpMethod.GET, null, new ParameterizedTypeReference<List<ItemDTO>>() {}, Map.of("ids", idsParam) );
if (!re.getStatusCode().is2xxSuccessful()){ return; }
List<ItemDTO> items = re.getBody();
if (CollUtil.isEmpty(items)) { return; } } }
|
注册中心
为了解决HttpTemplate的一系列服务间调用问题,我们使用服务中心来进行最佳实践
很多框架中都提供了注册中心的服务,我们来学习经典的一款——nacos
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| services: nacos: image: nacos/nacos-server:v2.1.0-slim container_name: nacos-server env_file: - ./custom.env ports: - "8848:8848" - "9848:9848" - "9849:9849" networks: - my_app_network restart: no
networks: my_app_network: external: true
|
1 2 3 4 5 6 7 8 9
| PREFER_HOST_MODE=hostname MODE=standalone SPRING_DATASOURCE_PLATFORM=mysql MYSQL_SERVICE_HOST=mysql MYSQL_SERVICE_DB_NAME=nacos MYSQL_SERVICE_PORT=3306 MYSQL_SERVICE_USER=root MYSQL_SERVICE_PASSWORD=Zhuwenxue2002 MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
|
注:这里使用docker网络管理的最佳实现,即自定义网络之后,让相关联服务都加入此网络,可直接通过服务名称代替主机名访问服务
服务注册
引入nacos discovery依赖
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
配置nacos地址
1 2 3 4 5 6
| spring: application: name: item-service cloud: nacos: server-addr: localhost:8848
|
配置好服务后,nacos的web管理界面http://localhost:8848/nacos/index.html的服务列表中就可以发现服务
请注意,这里的服务名就是yaml的配置项spring-application-name,当配置多台服务时,我们只要保证服务名相同,即达成同一服务的多个实例配置
服务依赖
同样的,我们先来引入依赖(与服务注册相同的依赖)
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
配置nacos地址(也相同)
1 2 3 4 5 6
| spring: application: name: item-service cloud: nacos: server-addr: localhost:8848
|
DiscoveryClient接口是SpringCloud定义的标准接口,通过DI注入来获取到Nacos针对此接口的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| List<ServiceInstance> instances = discoveryClient.getInstances("item-service"); if (CollUtils.isEmpty(instances)) { return; } ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
ResponseEntity<List<ItemDTO>> re = restTemplate.exchange(instance.getUri() + "/items?ids={ids}", HttpMethod.GET, null, new ParameterizedTypeReference<List<ItemDTO>>() { }, Map.of("ids", CollUtils.join(itemIds, ",")));
if (!re.getStatusCode().is2xxSuccessful()){ return; } List<ItemDTO> items = re.getBody();
if (CollUtils.isEmpty(items)) { return; }
|
这样,我们通过动态Nacos获取到服务的URI,即可直接访问服务
- 在服务的时候,可以实现负载均衡的访问
- 在某一个服务挂掉的时候,仍然可以正常通过其他服务访问
OpenFeign
注入依赖
1 2 3 4 5 6 7 8 9 10
| <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
1 2 3 4
| @MapperScan("com.hmall.cart.mapper") @SpringBootApplication @EnableFeignClients public class CartApplication {
|
编写FeignClient,将服务间的API写成类Controller的形式
1 2 3 4 5
| @FeignClient("item-service") public interface ItemClient { @GetMapping List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids ); }
|
OKHttp
OpenFiegn对Http请求做了优雅的封装,其底层默认使用HttpURLConection的发送方式(JDK自带)
性能很低且不支持连接池,但OpenFiegn对于底层的连接方式做了可插拔式设计
我们可以通过简单配置为其更换其他底层连接方式(支持连接池):
首先引入okhttp的依赖
1 2 3 4
| <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
|
开启feign-okhttp的支持
1 2 3
| feign: okhttp: enabled: true
|
至此,OpenFiegn的底层已经是使用OkHttp进行发送请求,且支持连接池
最佳实践
方案一:重构服务
将一个微服务重构,每一个微服务再拆成三个模块:
- api:存放api供外部调用
- dto:api所需传输实体
- biz:属于微服务本身的业务逻辑
如果有需求,就将api和dto两个模块作为依赖引入,就可以直接实现服务间调用
方案二:抽取API
将所有的API以及DTO抽取出到一个模块中,所有项目都依赖此模块
将API独立模块之后,Feign会找不到API包模块,需要指定FeignClient位置才行
方式一:指定所在包
@EnableFeignClients(basePackages = “com.hmall.api.clients”)
方式二:指定字节码文件
@EnableFeignClients(clients = {UserClient.class})
日志管理
默认情况下OpenFeign不提供任何的日志记录,一般情况下也不需要配置日志,只有调试时需要
创建一个配置类,但不要加配置类注解
1 2 3 4 5 6
| public class DefaultFeignConfig { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
|
为单个client配置日志
1 2 3 4 5
| @FeignClient(value = "item-service",configuration = DefaultFeignConfig.class) public interface ItemClient { @GetMapping("/items") List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids ); }
|
为该服务整体配置日志
1 2 3 4 5 6 7 8
| @MapperScan("com.hmall.cart.mapper") @SpringBootApplication @EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class) public class CartApplication { public static void main(String[] args) { SpringApplication.run(CartApplication.class, args); } }
|