SpringCloud—OpenFeign

简介

使用Spring Cloud搭建各种微服务之后,不同的各个服务之间会需要相互调用http接口,或者要调用外部的http接口,Spring Cloud OpenFeign优雅解决了这一需求,使得http调用就像调用本地方法一样。
Spring Cloud OpenFeign是Spring Cloud团队将原生的Feign结合到Spring Cloud中的产物,主要是基于Feign扩展了对Spring MVC注解的支持,同时还整合了Ribbon和注册中心(例如Eureka)来提供均衡负载的http客户端实现。

使用示例一

现有一个订单模块服务(biz-order-service),我们就以该服务为基础演示OpenFeign如何使用。

  1. 首先要引入OpenFeign的依赖。

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 启动类上面添加@EnableFeignClients注解,该注解表示当程序启动时,会扫描所有feign客户端(即带@FeignClient注解的接口)并创建代理对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @SpringBootApplication
    @EnableFeignClients
    public class OrderServiceBootstrap {


    public static void main(String[] args) {
    SpringApplication.run(OrderServiceBootstrap.class, args);
    }

    }
  3. 在应用中使用@FeignClient声明一个Feign客户端,为了方便,找个网上现成的开放接口,比如新浪财经的股票查询接口:http://hq.sinajs.cn/, 接口有一个参数,参数名为list

    1
    2
    3
    4
    5
    6
    7
    @FeignClient(name = "hq-sinajs-client", url = "http://hq.sinajs.cn/")
    public interface SinaApiClient {

    @RequestMapping(method = RequestMethod.GET) //使用get方式调用
    String getByCode(@RequestParam("list") String code);

    }
  4. 在单元测试方法中调用 SinaApiClient 接口的getByCode()方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = OrderServiceBootstrap.class)
    public class MainTest {


    @Autowired
    private SinaApiClient sinaApiClient;


    @Test
    public void testSinaApiClient() {
    String resp = sinaApiClient.getByCode("sh601006");
    System.out.println(resp);
    }

    }

打印结果:

1
var hq_str_sh601006="大秦铁路,6.540,6.530,6.530,6.570,6.530,6.530,6.550,6665811,43640317.000,524700,6.530,646000,6.520,734000,6.510,1029100,6.500,250000,6.490,598100,6.550,334100,6.560,541400,6.570,699112,6.580,333200,6.590,2021-02-25,10:10:39,00,";

PS:上面的String getByCode(@RequestParam("list") String code)方法中,list表示的是新浪的接口中的参数名,如果没有通过@RequestParam注解指定,取方法的参数名,比如这个方法中是code

使用示例二

上面的示例是通过url调用外部接口,我们知道微服务架构中,各个服务是会注册到注册中心的,所以OpenFeign也可以根据注册的服务名来请求接口,比如另外还有一个用户服务注册的服务名是:biz-user-service。

用户服务代码

  1. 引入SpringBoot和Eureka依赖。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
    </dependency>
  2. 启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @SpringBootApplication
    @EnableEurekaClient //注册到Eurka注册中心
    public class UserServiceBootstrap {


    public static void main(String[] args) {
    SpringApplication.run(UserServiceBootstrap.class, args);
    }

    }
  3. controller

    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
    @Slf4j
    @RestController
    @RequestMapping(value = "/user", method = {RequestMethod.GET, RequestMethod.POST})
    public class UserController {


    @Autowired
    private UserService userService;


    @RequestMapping("/getById")
    public ResponseData getById(@RequestParam Long id) {
    User user = userService.getById(id);
    log.info("用户|查询用户|id={}|{}", id, user);
    return ResponseData.data(user);
    }



    @RequestMapping("/getCoupon")
    public ResponseData getCoupon(@RequestParam Long userId, @RequestParam String type) {
    log.info("用户|查询用户的优惠券|userId={},type={}", userId, type);
    return ResponseData.success();
    }


    }
  4. service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Service
    public class UserService {


    public User getById(Long id) {
    if (id == null || id <= 0) {
    return null;
    }
    User user = new User();
    user.setId(id);
    user.setNickname("zhangsan");
    user.setLoginName("zhangsan@123.com");
    return user;
    }

    }
  5. 配置文件application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    server:
    port: 8001
    spring:
    application:
    name: biz-user-service
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:9110/eureka
    logging:
    config: classpath:logback.xml

订单服务

  1. 订单服务也需要注册到和用户服务相同的注册中心(比如注册中心是eureka,启动类上要加上@EnableEurekaClient注解,并在application.yml中配置注册中心地址以及服务名等)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @SpringBootApplication
    @EnableFeignClients
    @EnableEurekaClient
    public class OrderServiceBootstrap {


    public static void main(String[] args) {
    SpringApplication.run(OrderServiceBootstrap.class, args);
    }

    }
  2. 调用用户服务接口则可以写成如下形式:

    1
    2
    3
    4
    5
    6
    @FeignClient(name = "biz-user-service")
    public interface UserServiceClient {

    @GetMapping(value = "/user/getById")
    ResponseData getById(@RequestParam("id") int id);
    }
  3. 配置文件application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    server:
    port: 8002

    spring:
    application:
    name: biz-order-service
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:9110/eureka

    logging:
    level:
    root: info
    feign:
    okhttp:
    enabled: true
  4. 单元测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = OrderServiceBootstrap.class)
    public class MainTest {


    @Autowired
    private UserServiceClient userServiceClient;


    @Test
    public void getUserByIdTest() {
    ResponseData resp = userServiceClient.getById(2);
    System.out.println(resp);
    }

    }

@FeignClient使用介绍

FeignClient注解被@Target(ElementType.TYPE)修饰,说明是标注在类上的注解。
下面说明一下@FeignClient注解的常用属性。

value, name,serviceId

这三个属性是同一个含义,其中serviceId已经被废弃。如果没有配置url属性,则value或者name配置的值将作为服务名称,用于服务发现;如果配置了url属性,则该值仅作为一个名称。
value或者name属性是一个必须配置项。

url

url用于配置指定服务的地址,相当于直接请求这个服务,不经过Ribbon的服务选择。像调试等场景可以使用。
示例:

1
2
3
4
5
6
@FeignClient(name = "biz-user-service", url = "http://localhost:8001")
public interface UserServiceClient {

@GetMapping(value = "/user/getById")
public User getById(@RequestParam("id") int id);
}

decode404

当调用请求发生404错误时,如果decode404的值为true,那么会执行decoder解码,否则抛出 FeignException 异常。

path

path属性用来定义当前FeignClient访问接口时的统一前缀,可以简化该FeignClient在@RequestMapping注解中配置的value值。比如有一个用户服务,存在一系列用户管理的接口:
/user/getUserById
/user/modifyNickname
如果每次都在@RequestMapping中都写上全路径就有点繁琐,所以可以把公共的部分配置到path属性。

1
2
3
4
5
6
7
8
9
10
@FeignClient(name = "biz-user-service", url = "http://localhost:8001", path = "/user")
public interface UserServiceClient {

@GetMapping(value = "/getById")
public User getById(@RequestParam("id") int id);


@GetMapping(value = "/getCoupon")
public User getCoupon(@RequestParam("id") int id, @RequestParam("type") String type);
}

fallback

定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,但是无法知道具体的错误信息。
fallback指定的类必须实现@FeignClient标注的接口。
示例如下,先定义一个fallback类UserServiceClientFallback:

1
2
3
4
5
6
7
8
@Component
public class UserServiceClientFallback implements UserServiceClient {
@Override
public User getUserById(int id) {
return new User(0, "默认fallback");
}

}

然后在@FeignClient注解中配置fallback为UserServiceClientFallback:

1
2
3
4
5
6
7
@FeignClient(value = "biz-user-service", fallback = UserServiceClientFallback.class)
public interface UserServiceClient {

@GetMapping("/user/getById")
public User getById(@RequestParam("id")int id);

}

ps:通常fallback中通常返回服务访问失败等错误信息。

fallbackFactory

定义容错的处理类的工厂类,用于生成多个用于生成fallback类,可以获取到具体的错误信息。
示例如下,定义一个fallbackFactory类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class UserServiceClientFallbackFactory implements FallbackFactory<UserServiceClient> {
private Logger logger = LoggerFactory.getLogger(UserServiceClientFallbackFactory.class);

@Override
public UserServiceClient create(Throwable cause) {
return new UserServiceClient() {
@Override
public User getById(int id) {
logger.error("UserRemoteClient.getById", cause);
return new User(0, "默认");
}
};
}
}

然后在@FeignClient注解中配置 fallbackFactory UserServiceClientFallbackFactory:

1
2
3
4
5
6
7
@FeignClient(value = "biz-user-service", fallbackFactory = UserServiceClientFallbackFactory.class)
public interface UserServiceClient {

@GetMapping("/user/getById")
public User getById(@RequestParam("id")int id);

}

qualifier

qualifier对应的是@Qualifier注解,对于qualifier指定的值,可以在@Qualifier注解中使用。

比如上面我们的Feign Client有fallback实现,默认@FeignClient注解的primary=true, 意味着我们使用@Autowired注入是没有问题的,会优先注入你的Feign Client(比如上面的 UserServiceClient 和 UserServiceClientFallback ,如果自动注入UserServiceClient实例的话,会自动注入UserServiceClient接口的代理对象,而不是UserServiceClientFallback)。但如果@FeignClient注解的primary=false,IOC容器不知道要注入哪一个对象,所以需要通过@Qualifier注解指定。

示例如下,@FeignClient注解中,配置qualifier = “myUserServiceClient”。

1
2
3
4
5
6
@FeignClient(name = "biz-user-service", qualifier = "myUserServiceClient", primary = false)
public interface UserServiceClient {

@GetMapping(value = "/user/getById")
public User getById(@RequestParam("id") int id);
}

那么在使用@Qualifier自动注入时就可以myUserServiceClient这个值。

1
2
3
4
5
6
7
8
9
@RunWith(SpringRunner.class)
@SpringBootTest(classes = OrderServiceBootstrap.class)
public class MainTest {


@Qualifier("myUserServiceClient")
private UserServiceClient userServiceClient;

}

------ 本文完 ------