简介
使用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如何使用。
首先要引入OpenFeign的依赖。
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>启动类上面添加
@EnableFeignClients
注解,该注解表示当程序启动时,会扫描所有feign客户端(即带@FeignClient
注解的接口)并创建代理对象。1
2
3
4
5
6
7
8
9
10
public class OrderServiceBootstrap {
public static void main(String[] args) {
SpringApplication.run(OrderServiceBootstrap.class, args);
}
}在应用中使用
@FeignClient
声明一个Feign客户端,为了方便,找个网上现成的开放接口,比如新浪财经的股票查询接口:http://hq.sinajs.cn/, 接口有一个参数,参数名为list。1
2
3
4
5
6
7"hq-sinajs-client", url = "http://hq.sinajs.cn/") (name =
public interface SinaApiClient {
//使用get方式调用 (method = RequestMethod.GET)
String getByCode(@RequestParam("list") String code);
}在单元测试方法中调用 SinaApiClient 接口的
getByCode()
方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16(SpringRunner.class)
(classes = OrderServiceBootstrap.class)
public class MainTest {
private SinaApiClient sinaApiClient;
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。
用户服务代码
引入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>启动类
1
2
3
4
5
6
7
8
9
10
//注册到Eurka注册中心
public class UserServiceBootstrap {
public static void main(String[] args) {
SpringApplication.run(UserServiceBootstrap.class, args);
}
}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
274j
"/user", method = {RequestMethod.GET, RequestMethod.POST}) (value =
public class UserController {
private UserService userService;
"/getById") (
public ResponseData getById(@RequestParam Long id) {
User user = userService.getById(id);
log.info("用户|查询用户|id={}|{}", id, user);
return ResponseData.data(user);
}
"/getCoupon") (
public ResponseData getCoupon(@RequestParam Long userId, @RequestParam String type) {
log.info("用户|查询用户的优惠券|userId={},type={}", userId, type);
return ResponseData.success();
}
}service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}配置文件application.yml
1
2
3
4
5
6
7
8
9
10
11server:
port: 8001
spring:
application:
name: biz-user-service
eureka:
client:
service-url:
defaultZone: http://localhost:9110/eureka
logging:
config: classpath:logback.xml
订单服务
订单服务也需要注册到和用户服务相同的注册中心(比如注册中心是eureka,启动类上要加上
@EnableEurekaClient
注解,并在application.yml中配置注册中心地址以及服务名等)1
2
3
4
5
6
7
8
9
10
11
public class OrderServiceBootstrap {
public static void main(String[] args) {
SpringApplication.run(OrderServiceBootstrap.class, args);
}
}调用用户服务接口则可以写成如下形式:
1
2
3
4
5
6"biz-user-service") (name =
public interface UserServiceClient {
"/user/getById") (value =
ResponseData getById(@RequestParam("id") int id);
}配置文件application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17server:
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单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16(SpringRunner.class)
(classes = OrderServiceBootstrap.class)
public class MainTest {
private UserServiceClient userServiceClient;
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"biz-user-service", url = "http://localhost:8001") (name =
public interface UserServiceClient {
"/user/getById") (value =
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"biz-user-service", url = "http://localhost:8001", path = "/user") (name =
public interface UserServiceClient {
"/getById") (value =
public User getById(@RequestParam("id") int id);
"/getCoupon") (value =
public User getCoupon(@RequestParam("id") int id, @RequestParam("type") String type);
}
fallback
定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,但是无法知道具体的错误信息。
fallback指定的类必须实现@FeignClient标注的接口。
示例如下,先定义一个fallback类UserServiceClientFallback:1
2
3
4
5
6
7
8
public class UserServiceClientFallback implements UserServiceClient {
public User getUserById(int id) {
return new User(0, "默认fallback");
}
}
然后在@FeignClient注解中配置fallback为UserServiceClientFallback:1
2
3
4
5
6
7"biz-user-service", fallback = UserServiceClientFallback.class) (value =
public interface UserServiceClient {
"/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
public class UserServiceClientFallbackFactory implements FallbackFactory<UserServiceClient> {
private Logger logger = LoggerFactory.getLogger(UserServiceClientFallbackFactory.class);
public UserServiceClient create(Throwable cause) {
return new UserServiceClient() {
public User getById(int id) {
logger.error("UserRemoteClient.getById", cause);
return new User(0, "默认");
}
};
}
}
然后在@FeignClient注解中配置 fallbackFactory UserServiceClientFallbackFactory:1
2
3
4
5
6
7"biz-user-service", fallbackFactory = UserServiceClientFallbackFactory.class) (value =
public interface UserServiceClient {
"/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"biz-user-service", qualifier = "myUserServiceClient", primary = false) (name =
public interface UserServiceClient {
"/user/getById") (value =
public User getById(@RequestParam("id") int id);
}
那么在使用@Qualifier
自动注入时就可以myUserServiceClient这个值。1
2
3
4
5
6
7
8
9 (SpringRunner.class)
(classes = OrderServiceBootstrap.class)
public class MainTest {
"myUserServiceClient") (
private UserServiceClient userServiceClient;
}