服务调用
在 SpringCloud
中服务间调用方式主要是使用 http restful
方式进行服务间调用。
所谓的服务键调用,简单来说,就是我可以调用其他模块的接口。
比如:
在服务A中有一个接口。
1 2 3 4 5 6 7 8
| @RestController public AController{ @GetMapping("/msga") public String GetAMeg(){ return "AController"; } }
|
服务B想要直接调用这个接口。需要通过什么方式或什么组件可以调用这个功能。
一、负载均衡
服务间调用还需要考虑一个问题:负载均衡。
负载均衡在系统架构中是非常重要的。因为负载均衡对系统的高可用,网络压力的缓解和处理能力扩容的重要手段之一。
1、客户端负载均衡
在客户端负载均衡中,客户端知道所有服务端的详细信息,当需要调用服务端上的接口时,客户端从服务端列表中,根据负载均衡策略,自己挑选一个服务端来调用,此时客户端知道自己调用的是哪一个服务端。
2、负载均衡策略
二、基于 RestTemplate 服务调用
spring框架提供的RestTemplate
类可用于在应用中调用rest服务,我们只需要传入url及返回值类型即可。
1、环境搭建
新建两个client实例,注册到consul中。
1 2
| 服务A Client-A 端口号 8801 服务B Client-B 端口号 8802
|

为服务A添加一个接口
我们希望在服务B中调用服务A的接口
1 2 3 4 5 6 7
| @RestController public class MsgController { @GetMapping("/getmsg") public String getMsg(){ return "这是服务A中的接口"; } }
|
调用localhost:8801/getmsg

2、RestTemplate调用
在服务B中编写Controller
1 2 3 4 5 6 7 8 9 10 11
| @RestController public class ClientBController {
@GetMapping("/getmsg") public String getMsg(){ RestTemplate restTemplate = new RestTemplate(); String result = restTemplate.getForObject("http://localhost:8801/getmsg",String.class);
return result; } }
|

3、基于RestTemplate调用存在的问题
核心代码
1
| restTemplate.getForObject("http://localhost:8801/getmsg",String.class);
|
没有经过注册中心。
从这行代码可以看出,是直接使用的url,没有从注册中心中获取服务A地址。
没有实现负载均衡
因此直接写上服务A的url,如果这个服务A宕机,那么程序会出现错误。
不利于维护
直接将服务A地址写死,无法高效修改或服务剔除。
三、基于Ribbon的服务调用
1、什么是Ribbon
Ribbon是Netflixfa
发布的一个负载均衡器,有助于控制HTTP和TCP客户端行为。
Ribbon提供客户端负载均衡的功能,Ribbon利用从注册中心读取的服务信息,在调用服务结点提供的服务时,会合理的进行负载。
2、开发环境配置
重新配置开发环境
创建两个服务并注册到Consul注册中心中,并希望用户服务去调用产品服务中的接口:
1 2
| user服务 端口号 8881 用户服务 products服务 端口号 8882 产品服务
|
并且引入Ribbon依赖:
1 2 3 4 5 6
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>2.2.1.RELEASE</version> </dependency>
|
两个服务

在商品服务中实现接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @RestController public class ProductsController {
@Value("${server.port}") private int port;
@GetMapping("/product/findAll") public Map<String,Object> findAll(){ Map<String,Object> map = new HashMap<>(); map.put("msg","服务调用成功,服务提供的端口号是:" + port);
return map; } }
|

3、基于Ribbon服务调用
方法简述
1 2 3
| 1. 使用discovery client 进行调用 2. 使用loadBalanceClient 进行调用 3. 使用@loadBalanced 进行调用
|
3.1、使用 discovery client
核心代码:
1
| discoveryClient.getInstances("服务提供者");
|
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
| @RestController @Slf4j public class UserController {
@Autowired private DiscoveryClient discoveryClient;
private ServiceInstance getInstance(){ List<ServiceInstance> serviceInstances = discoveryClient.getInstances("product"); return serviceInstances.get(0);
} @GetMapping("/user/findAll") public Map<String,Object> findAll(){ RestTemplate restTemplate = new RestTemplate();
HashMap<String,Object> map = new HashMap<>(); String url = "http://" + getInstance().getHost() + ":" + getInstance().getPort() + "/product/findAll"; map = restTemplate.getForObject(url, HashMap.class);
return map; } }
|

3.2、Load Balance Client
通过DiscoveryClient
可以从注册中心中将所有的服务提供者列举出来。
我们从中选择一个服务提供者,为服务的消费者提供服务。
在DiscoveryClient
中,我们并没有添加负载均衡。只是简单的从提供者中选择了一项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @RestController @Slf4j public class UserController {
@Autowired private LoadBalancerClient loadBalancerClient;
@GetMapping("/user/findAll") public Map<String,Object> findAll(){ RestTemplate restTemplate = new RestTemplate();
HashMap<String,Object> map = new HashMap<>();
ServiceInstance product = loadBalancerClient.choose("product"); String url = "http://" + product.getHost() + ":" + product.getPort() + "/product/findAll"; log.info("==============" + url); map = restTemplate.getForObject(url, HashMap.class);
return map; } }
|

3.3、@LoadBalance
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Configuration public class UserConfig {
@Bean @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @RestController @Slf4j public class UserController {
private String serverName = "product";
@Autowired private RestTemplate restTemplate;
@GetMapping("/user/findAll") public Map<String,Object> findAll(){ HashMap<String,Object> map = new HashMap<>(); map = restTemplate.getForObject("http://"+ serverName +"/product/findAll", HashMap.class); return map; } }
|
四、基于OpenFeign的服务调用
1、OpenFeign简介
前面的 RestTemplate + Ribbon 去进行服务间的调用,核心代码是:
1
| restTemplate.getForObject(url,Class);
|
可以看到,我们使用的是拼接字符串的方式构造URL的。
这样直接在代码中写出你要调用的服务和资源路径,这样系统的耦合度太高了。如果资源路径修改,就必须在代码上修改。
OpenFeign是Netflflix
开发的声明式,模板化的HTTP客户端。
Feign可帮助我们更加便捷,优雅的调用HTTP API。
2、OpenFeign实例
1、引入依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
|
2、入口类加入注解,开启支持OpenFeign
1 2 3 4 5 6 7 8 9
| @SpringBootApplication @FeignClient public class User8881Application {
public static void main(String[] args) { SpringApplication.run(User8881Application.class, args); }
}
|
3、创建客户端调用接口
1 2 3 4 5 6
| @FeignClient("PRODUCT") public interface ProductClient { @GetMapping("/product/findAll") String findAll(); }
|
4、使用feignClient
客户端对象调用服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @RestController @Slf4j public class UserController {
@Autowired private ProductClient productClient; @GetMapping("/user/findAllByFeign") public Map<String, Object> findAll(){ log.info("通过使用OpenFeign组件调用服务"); Map<String,Object> map = productClient.findAll();
return map; } }
|

3、使用OpenFeign的细节问题
1、传参
服务提供者添加方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestController public class ProductsController {
@Value("${server.port}") private int port;
@GetMapping("/product/findOne") public Map<String,Object> findOne(String id){ HashMap<String, Object> map = new HashMap<>(); map.put("port","服务提供者的port:" + port); map.put("id","参数id" + id);
return map; } }
|
接口
1 2 3 4 5 6
| @FeignClient("PRODUCT") public interface ProductClient { @GetMapping("/product/findOne") Map<String,Object> findOne(@RequestParam("id") String id);
}
|
添加方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @Slf4j public class UserController { @Autowired private ProductClient productClient;
@GetMapping("/user/findOne") public Map<String,Object> findOne(String id){ log.info("通过使用OpenFeign组件调用服务"); Map<String,Object> map = productClient.findOne("1"); return map; } }
|

如果传递的是对象
1 2 3 4 5 6 7 8
| @Data @NoArgsConstructor @AllArgsConstructor @ToString public class Student { private String id; private String name; }
|
Product Post请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Slf4j @RestController public class PostController {
@RequestMapping(value = "/product/save",method = RequestMethod.POST) public Map<String,Object> save(@RequestBody Student student){
log.info("====================="); HashMap<String, Object> map = new HashMap<>();
map.put("stu",student); map.put("status",200);
return map; } }
|
在调用方
1 2 3 4 5
| @FeignClient("PRODUCT") public interface ProductClient { @PostMapping("/product/save") Map<String,Object> save( @RequestBody Student student); }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @RestController @Slf4j public class UserController { @PostMapping("/user/save") public Map<String,Object> save(Student student){
log.info("==========" + student);
Map<String,Object> map = productClient.save(student); return map; } }
|

2、超时设置
默认情况下,OpenFeign在进行服务调用时,要求服务提供方处理业务逻辑时间必须在1 S内返回,如果超过1 S没有返回则OpenFeign会直接报错,不会等待服务执行。
但是由于复杂的业务逻辑,可能会超过1 S,因此需要修改OpenFeign的默认服务调用超时事件。
1 2 3 4 5 6 7 8 9 10
| @GetMapping("/product/findAll") public Map<String,Object> findAll() throws InterruptedException { Thread.sleep(2000); Map<String,Object> map = new HashMap<>(); map.put("msg","服务调用成功,生产服务提供的端口号是:" + port);
return map; }
|

修改默认超时时间
1 2
| feign.client.config.product.connectTimeout=5000 #配置指定服务连接超时 feign.client.config.product.readTimeout=5000 #配置指定服务等待超时
|
可能服务的调用者会同时调用多个服务,写单个服务超时时间麻烦,可以简写默认的超时时间:
1 2
| feign.client.config.default.connectTimeout=5000 #配置所有服务连接超时 feign.client.config.default.readTimeout=5000 #配置所有服务等待超时
|
3、日志设置
默认OpenFeign在调用是并不是最详细日志输出,因此在调试程序时应该开启feign的详细日志展示。
feign对日志的处理非常灵活可为每个feign客户端指定日志记录策略,每个客户端都会创建一个logger。
我们可以为feign客户端配置各自的logger.level
对象,告诉feign记录那些日志logger.lever
有以下的几种值
1 2 3 4
| NONE 不记录任何日志 BASIC 仅仅记录请求方法,url,响应状态代码及执行时间 HEADERS 记录Basic级别的基础上,记录请求和响应的header FULL 记录请求和响应的header,body和元数据
|
开始日志配置
1 2 3 4
| feign.client.config.product.loggerLevel=full #开启指定服务日志展示
logging.level.com.jiang.feignclients=debug #指定feign调用客户端对象所在包,必须是debug级别
|