简介 使用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 @SpringBootApplication @EnableFeignClients 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 @FeignClient(name = "hq-sinajs-client", url = "http://hq.sinajs.cn/") public interface SinaApiClient { @RequestMapping(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 @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。
用户服务代码
引入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 @SpringBootApplication @EnableEurekaClient 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 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(); } }
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; } }
配置文件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
订单服务
订单服务也需要注册到和用户服务相同的注册中心(比如注册中心是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); } }
调用用户服务接口则可以写成如下形式:
1 2 3 4 5 6 @FeignClient(name = "biz-user-service") public interface UserServiceClient { @GetMapping(value = "/user/getById") ResponseData getById (@RequestParam("id") int id) ; }
配置文件application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
单元测试
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 8 @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 16 @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; }