# spring-cloud-feign ## 一、feign 简介 在上一个用例中,我们使用ribbon+restTemplate 实现服务之间的远程调用,实际上每一个调用都是模板化的内容,所以spring cloud Feign 在此基础上进行了进一步的封装。我们只需要定义一个接口并使用feign注解的方式来进行配置,同时采用springMvc 注解进行参数绑定就可以完成服务的调用。feign同时还内置实现了负载均衡、服务容错等功能。 ## 二、项目结构 + common: 公共的接口和实体类; + consumer: 服务的消费者,采用feign调用产品服务; + producer:服务的提供者; + eureka: 注册中心。
## 三、服务提供者的实现
#### 3.1 产品服务由`ProductService`提供,并通过`ProducerController`将服务暴露给外部调用。 ProductService.java: ```java /** * @author : heibaiying * @description : 产品提供接口实现类 */ @Service public class ProductService implements IProductService, ApplicationListener { private static List productList = new ArrayList<>(); public Product queryProductById(int id) { return productList.stream().filter(p->p.getId()==id).collect(Collectors.toList()).get(0); } public List queryAllProducts() { return productList; } @Override public void saveProduct(Product product) { productList.add(product); } @Override public void onApplicationEvent(WebServerInitializedEvent event) { int port = event.getWebServer().getPort(); for (long i = 0; i < 20; i++) { productList.add(new Product(i, port + "产品" + i, i / 2 == 0, new Date(), 66.66f * i)); } } } ``` ProducerController.java: ```java @RestController public class ProducerController implements ProductFeign { @Autowired private IProductService productService; @GetMapping("products") public List productList() { return productService.queryAllProducts(); } @GetMapping("product/{id}") public Product productDetail(@PathVariable int id) { return productService.queryProductById(id); } @PostMapping("product") public void save(@RequestBody Product product) { productService.saveProduct(product); } } ``` #### 3.2 指定注册中心地址,并在启动类上开启自动注册@EnableDiscoveryClient ```java server: port: 8020 # 指定服务命名 spring: application: name: producer # 指定注册中心地址 eureka: client: serviceUrl: defaultZone: http://localhost:8010/eureka/ ``` ```java @SpringBootApplication @EnableDiscoveryClient public class ProducerApplication { public static void main(String[] args) { SpringApplication.run(ProducerApplication.class, args); } } ``` ## 四、服务消费者的实现
#### 4.1 导入openfeign依赖 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` #### 4.2 指定注册中心地址,并在启动类上添加注解@EnableDiscoveryClient和@EnableFeignClients @EnableFeignClients 会去扫描工程中所有用 @FeignClient 声明的 feign 客户端。 ```java server: port: 8080 # 指定服务命名 spring: application: name: consumer # 指定注册中心地址 eureka: client: serviceUrl: defaultZone: http://localhost:8010/eureka/ ``` ```java @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } } ``` #### 4.3 创建服务调用公共接口 ```java /** * @author : heibaiying * @description : 声明式服务调用 */ public interface ProductFeign { @GetMapping("products") List productList(); /** * 这是需要强调的是使用feign时候@PathVariable一定要用value指明参数, * 不然会抛出.IllegalStateException: PathVariable annotation was empty on param 异常 */ @GetMapping("product/{id}") Product productDetail(@PathVariable(value = "id") int id); @PostMapping("product") void save(@RequestBody Product product); } ``` 按照官方对于服务最佳化的推荐,这里我们的服务调用接口放在公共模块中,因为在实际的开发中,同一个服务调用接口可能被多个模块所使用。
#### 4.4 继承公共接口,创建CProductFeign, 用@FeignClient声明为feign客户端 ```java /** * @author : heibaiying * @description : 声明式接口调用 */ @FeignClient(value = "producer",configuration = FeignConfig.class) public interface CProductFeign extends ProductFeign { } ``` #### 4.5 注入使用 feign 服务调用接口 ```java @Controller @RequestMapping("sell") public class SellController { @Autowired private CProductFeign cproductFeign; @GetMapping("products") public String productList(Model model) { List products = cproductFeign.productList(); model.addAttribute("products", products); return "products"; } @GetMapping("product/{id}") public String productDetail(@PathVariable int id, Model model) { Product product = cproductFeign.productDetail(id); model.addAttribute("product", product); return "product"; } @PostMapping("product") public String save(@RequestParam String productName) { long id = Math.round(Math.random() * 100); Product product = new Product(id, productName, false, new Date(), 88); cproductFeign.save(product); return "redirect:products"; } } ``` ## 五、启动测试 #### 5.1 启动一个Eureka服务、三个producer服务(注意区分端口)、和一个消费者服务 feign 的依赖中导入了spring-cloud-starter-netflix-ribbon依赖,并且在内部实现了基于ribbon的客户端负载均衡,所以我们这里启动三个producer实例来观察负载均衡的情况。
**服务注册中心:**
#### 5.2 访问http://localhost:8080/sell/products 查看负载均衡的调用结果
## 六、 feign 的服务容错 #### 6.1 feign 的依赖中默认导入了hystrix 的相关依赖,我们不需要额外导入,只需要开启相关配置即可
#### 6.2 在application.yml 中开启hystrix ```yml feign: hystrix: # 如果为true,则OpenFign客户端将使用Hystrix断路器进行封装 默认为false enabled: true ``` #### 6.3 创建`CProductFeignImpl`,继承feign接口(CProductFeign),定义熔断时候的回退处理 ```java /** * @author : heibaiying * @description : 定义发生熔断时候的回退处理。除了继承自CProductFeign,还需要用@Component声明为spring的组件 */ @Component public class CProductFeignImpl implements CProductFeign { // 发生熔断时候,返回空集合,前端页面会做容错显示 @Override public List productList() { return new ArrayList<>(); } @Override public Product productDetail(int id) { return null; } @Override public void save(Product product) { } } ``` 页面的简单容错处理: ```html 产品列表

产品列表:点击查看详情

    <#if (products?size>0) > <#list products as product>
  • ${product.name}
  • <#else>

    当前排队人数过多,请之后再购买!

``` #### 6.4 在 @FeignClient 注解中,用fallback参数指定熔断时候的回退处理 ```java /** * @author : heibaiying * @description : 声明式接口调用 */ @FeignClient(value = "producer",configuration = FeignConfig.class,fallback = CProductFeignImpl.class) public interface CProductFeign extends ProductFeign { } ``` #### 6.5 测试熔断处理 hystrix 默认调用超时时间为2s ,这里我们使用线程休眠的方式来模拟超时熔断。 ```java public List queryAllProducts() { /*用于测试 hystrix 超时熔断 try { int i = new Random().nextInt(2500); Thread.sleep(i); } catch (InterruptedException e) { e.printStackTrace(); }*/ return productList; } ``` 测试结果: