Feign的使用实例

在这个部分,先做一个快速入门的演示来认识 Feign,然后结合 Spring MVC,说明 Feign 能扩展 Spring MVC,本节重点是理解参数传递的不同与相同点。

Feign演示实例

按照惯例,仍然先演示一个实例,让我们对 Feign 有一个直观的印象。实例中需要启动三个项目,服务注册中心、服务提供者、服务消费者。

在服务调用之前,做准备工作。首先启动服务注册中心,此处还是选择在第 10 章使用的服务注册中心,下面是服务注册中心的配置。

server.port=8764
eureka.client.service-url.defaultZone=http://localhost:8764/eureka/
eureka.client.register-with-eureka=false

然后,启动服务提供者。在服务提供者中,有一段服务程序将被暴露出来,在后面的服务消费者中,将会调用这段代码。为了不翻看前面的代码,这里展示一下将会被调用的服务的程序。代码如下所示。

package com.cloudtest.eurekaprovider.controller;
@RestController
public class HelloController {
   @Autowired
   private DiscoveryClient client;
   @Autowired
   private Registration registration; // 服务注册
   private final Logger logger = LoggerFactory.getLogger(HelloController.class);
   @GetMapping("/hello")
   public String hello(){
      ServiceInstance instance = serviceInstance();
      String result = "host:port=" + instance.getUri() + ", "
             + "service_id:" + instance.getServiceId();
      logger.info(result);
      return "hello eureka!";
   }
   public ServiceInstance serviceInstance() {
   List<ServiceInstance> list = client.getInstances(registration.getServiceId());
      if (list != null && list.size() > 0) {
             for(ServiceInstance itm : list){
               if(itm.getPort()==8091){
                    return itm;
               }
             }
      }
      return null;
   }
}

在上面的代码中,可以看到提供的 Rest 接口是 “/hello”。在服务提供者中,我们的配置文件也会用到,代码如下所示。

server.port=8091
spring.application.name=helloService
eureka.client.service-url.defaultZone=http://localhost:8764/eureka/

现在,开始写服务消费者。为了方便重现程序,我们先看实例的程序结构,如图12.1所示。

image 2024 04 01 10 45 31 513
Figure 1. 图12.1 程序结构

在上面的程序结构中,演示实例主要位于 feign 包,当然启动类也需要修改。首先,给客户端添加 Feign 依赖,代码如下所示。

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

然后,修改启动类,给启动类添加 @EnableFeignClients 注解,其功能是开启 Spring Cloud Feign。代码如下所示。

package com.cloudtest.eurekaconsumer;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class EurekaconsumerApplication {
   public static void main(String[] args) {
      SpringApplication.run(EurekaconsumerApplication.class, args);
   }
   @Bean
   @LoadBalanced
   RestTemplate restTemplate(){
      return new RestTemplate();
   }
}

最后,定义一个接口,该接口是内部要调用的接口,代码如下所示。

package com.cloudtest.eurekaconsumer.feign;
/**
  * 定义需要调用的接口
  */
@FeignClient(name="helloService")
@Component
public interface HelloInterface {
   @GetMapping("/hello")
   public String getHelloMsg();
}

在上面的代码中,我们需要添加 @FeignClient 注解绑定应用服务,并使用属性 name 制定应用服务名,这个服务名需要和服务注册中心的应用名相同,不过不区分大小写。在上面代码中,制定的应用服务名为 helloService。可以发现 helloService 与前面服务提供者中的配置文件中的应用名是相同的。

当知道应用服务名后,还需要指定调用的 Rest 接口。在上面的代码中,getHelloMsg 方法调用的接口是 “/hello”,这个 hello 接口就是指定的服务提供者的 “/hello” 接口。

然后就是如何使用。新建一个控制类,在浏览器上进行调用,代码如下所示。

package com.cloudtest.eurekaconsumer.feign.controller;
@RestController
public class HelloMsgController {
   @Autowired
   HelloInterface helloInterface;
   @GetMapping("/helloTest")
   public String helloMsg(){
      String msg=helloInterface.getHelloMsg();
      System.out.println("+++"+msg+"+++");
      return "success";
   }
}

在上面的代码中,使用注解 @Autowired 引进接口。然后在代码中,直接使用接口中的方法,就像在调用本地类中的方法。

在这里,将返回的内容放入变量 msg 中,然后输出,最后运行、检查效果。进入链接 http://localhost:8070/helloTest 。浏览器效果如图12.2所示。

image 2024 04 01 10 48 46 330
Figure 2. 图12.2 浏览器效果

这个效果最明显,说明程序成功运行。控制台如图12.3所示。

image 2024 04 01 10 49 11 237
Figure 3. 图12.3 控制台

这个效果是说明服务调用之后,返回的值是 “hello eureka”。我们看服务提供者的 “/hello”,可以发现代码的最后是 “return "hello eureka!"”,说明这里返回结果是正确的,同时说明可以使用这种方式进行服务间的调用。

查看日志。为什么还要查看日志?这里主要看服务提供者的控制台上是否有日志输出。进入服务提供者的控制台,我们发现有如下日志。

2019-01-21 22:31:35.315 INFO 17088 --- [nio-8091-exec-9]
c.c.e.controller.HelloController :
host:port=http://DESKTOP-TQ4OD79:8091, service_id:HELLOSERVICE

Feign与Spring MVC

在12.1.1节中演示的实例程序,可以帮助我们快速理解在 Spring Cloud 项目中 Feign 是如何使用的。虽然在客户端写 Restful 风格的接口和以前有些不同,但是在 Eureka 中,客户端也是服务端的概念下,理解起来也不困难。

上一个章节中,我们直接简单地调用服务,不涉及参数的传递。如果读者认真看过11.2节的 RestTemplate 的介绍后,这里就很好理解,因为两者使用方式的注解相同。

(1)服务提供者

我们对服务提供者程序进行修改,为了方便重现程序,先看程序结构,如图12.4所示。

image 2024 04 01 10 51 22 000
Figure 4. 图12.4 服务提供者程序结构

新建一个参数请求控制类 ParamHelloController,代码如下所示。

package com.cloudtest.eurekaprovider.controller;
@RestController
public class ParamHelloController {
   @GetMapping("/getUserName")
   public String hello(@RequestParam String name){
      return "get:" + "name=" +name;
   }
@GetMapping("/getUser")
public String hello(@RequestHeader String name,@RequestHeader String age){
   return "get:" + "name=" +name+" age=" +age;
}
@PostMapping("/postUser")
public String hello(@RequestBody User user){
   return "post:" + user.getName()+", "+user.getAge();
}
}

在上面的代码中列举了三种场景,即两种 GET 方式和一种 POST 方式的场景。在代码中,引用了 User 类,代码如下所示。

package com.cloudtest.eurekaprovider.bean;
public class User {
   private String name;
   private String age;
   //其他省略
}

(2)服务消费者

在开始程序前,依旧先展示程序结构,如图12.5所示。

image 2024 04 01 10 53 07 367
Figure 5. 图12.5 服务消费者程序结构

首先,复制一份服务提供者的 User 对象,在这里需要保证两个程序相同,因为若在服务提供者的 User 对象中添加参数,需要在服务消费者处使用 User 中的参数。

然后定义一个接口, ParamHelloInterface 接口的代码如下所示。

package com.cloudtest.eurekaconsumer.feign;
@FeignClient(name="helloService")
public interface ParamHelloInterface {
    @GetMapping("/getUserName")
    public String hello(@RequestParam("name") String name);

    @GetMapping("/getUser")
    public String hello(@RequestHeader("name") String name,@RequestHeader("age") String age);

    @PostMapping("/postUser")
    public String hello(@RequestBody User user);
}

在上面的代码中需要注意,在 Spring MVC 中,@RequestParam 注解中不需要指定参数名,因为默认的值就是参数名,但是在这里,如果不指定参数名则不能通过编译。当然,@RequestHeader 注解也需要指定参数名。然后进行服务调用,代码如下所示。

package com.cloudtest.eurekaconsumer.feign.controller;
@RestController
public class ParamHelloMsgController {
   @Autowired
   ParamHelloInterface paramHelloInterface;
   @GetMapping("/testParamHello")
   public String test(){
      String msg1=paramHelloInterface.hello("tom1");
      System.out.println("msg1:"+msg1);
      String msg2=paramHelloInterface.hello("tom2","18");
      System.out.println("msg2:"+msg2);
        String msg3=paramHelloInterface.hello(new User( "tom3" ,"19"));
      System.out.println("msg3:"+msg3);
      return "param test success";
   }
}

其实,我们还需要修改一些东西。因为这个代码是直接在实例的基础上添加的,但是定义接口时,另外定义了一个接口。因此,如果不修改,程序将会报错,下面是日志。

Description:
The bean 'helloService.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.

这里的意思是,我们不能有 @FeignClient(相同服务名)这种情况,因为不允许,所以要注释与本实例无关的接口与控制类。

2019-01-21 23:58:59.706 INFO 18272 --- [nio-8070-exec-2] c.n.l. DynamicServerListLoadBalancer: DynamicServerListLoadBalancer for client helloService initialized:DynamicServerListLoadBalancer:{NFLoadBalancer:name=helloService,current list of Servers=[DESKTOP-TQ4OD79:8091],Load balancer stats=Zone stats:{defaultzone=[Zone:defaultzone; Instance count:1; A c t i v e c o n n e c t i o n s count: 0;
Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:DESKTOP-TQ4OD79:8091; Zone:defaultZone;
Total Requests:0;Successive connection failure:0; Total blackout seconds:0;Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0;total failure count in last (1000) msecs:0; average resp time:0.0;90 percentile resp time:0.0; 95 percentile resp time:0.0;min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.Domain ExtractingServerList@7c17214e
msg1:get:name=tom1
msg2:get:name=tom2age=18
msg3:post:tom3,19

对于上面的日志,主要指加粗的部分。第一处,说明使用的是 Ribbon 负载均衡;第二处,说明使用的是服务提供者,具体信息可以在服务注册中心的信息面板上看;第三处,就是我们调用服务返回的信息。