Spring Cloud微服务之Feign——声明式服务间调用

目录

写在前面

服务间调用常见的方案

方案一:HttpClient

方案二:RestTemplate

方案三:Feign

框架版本问题及可能遇到的坑

搭建服务注册中心

搭建服务1——service-a

搭建服务2——service-b

在service-b中通过Feign调用service-a

进阶:Feign服务间调用—传递各种参数

再次进阶:引入Hystrix熔断器

总结


写在前面

在之前的 Spring Cloud微服务入门级教程(零基础,最详细,可运行) 文章中,我们搭建了一套基于Spring Cloud的微服务项目。在那个项目中,我们有两个相同的服务提供者和一个网关(Gateway),然后客户端直接访问我们的网关,由网关转发到两个相同的服务节点。这种情况其实只适用于所有API接口都混合在同一个服务下的项目,我们只是提供了多个服务实例来达到负载均衡,并没有真正的按照功能进行拆分。

在真正的微服务项目开发中,我们绝对不只是提供多个服务实例去负载均衡,要不然直接用Nginx不就行了吗?大部分情况是根据业务维度进行拆分,所以在特定的业务场景下就会出现服务间相互调用的需求。比如服务A需要调用服务B的接口,那么我们应该如何实现呢?

服务间调用常见的方案

方案一:HttpClient

这里说的HttpClient,指的是一类产品或方案,比如还有OkHttp系列的。这种方案,是最暴力的实现方式,直接通过Java代码模拟浏览器去调用接口,也是在最早的分布式系统中所使用的,因为那时候没有服务治理框架,只能这样去调用。但是这种方式,暴力不代表不合理。在当下,依赖第三方接口的开发只能用这种方式去调用,比如微信公众号开发中的相关API接口。

这种方式呢,调用一个接口比较繁琐,大体的流程一致:先获取一个client对象,然后要构建request对象、接收response对象,如果有请求头还需要Header对象,如果是https的还要处理SSL。传参的话也不是很灵活,get请求还好说,post请求简直就是噩梦,特别是写到请求体里的body传参方式……

方案二:RestTemplate

在微服务的架构下,我们有服务治理中心,当然不会通过上面的方式去调用啦……

RestTemplate是Spring Cloud提供的一种服务间调用解决方案,它虽然利用了服务治理中心来调取服务,但是我感觉它就是穿着华丽外衣的HttpClient,看一下它的调用方式:

它在调用接口的时候,依然会让你传入一个url,只不过通过HttpClient调用接口时传入的url是一个可以外网访问的唯一http地址,而RestTemplate传入的url,是把域名换成了服务名,然后根据服务名去注册中心找对应的服务。

什么意思呢?下面看一个例子。

假如我们有一个服务,在注册中心的服务名叫做service-a,这个service-a服务下面有一个接口,地址是/student/list,假设域名www.test.com可以直接定位到service-a服务。同时注册中心有service-b,需要调用service-a的这个接口,那么如果我们通过HttpClient去调用这个服务接口,那传入的url就是http://www.test.com/student/list,这样显然没有通过我们的注册中心来获取服务;如果我们通过RestTemplate去调用,那就是http://service-a/student/list。

上面的例子,RestTemplate就会根据服务名称去注册中心寻找对应的服务,将请求转发过去。如果运用了Ribbon的话,还会实现负载均衡的效果:注册中心将所有此名称的服务实例清单返回,然后从这些服务清单中任选一个服务进行请求转发。

RestTemplate的传参、请求体的设置、请求头的设置,和HttpClient的流程如出一辙,比较繁琐,不够优雅,区别就是可以利用服务治理中心来达到了负载均衡。

方案三:Feign

接下来说说我们今天的主角,Feign。它是对RestTemplate和Ribbon的封装,让我们的服务间调用更加方便、更加简洁,实现声明式的服务调用,具体什么意思呢?

我们在使用Feign进行服务间调用时,只需要在调用方建一个interface接口,并且加上@FeignClient注解将其声明为一个服务调用客户端,并且进行相关的注解赋值,就可以调用其他服务了!

实践出真知,下面我们就来代码实战。

框架版本问题及可能遇到的坑

笔者使用的Spring Boot版本是2.2.2.RELEASE,Spring Cloud版本是Hoxton.RELEASE,JDK版本是10,不同版本的框架所使用的maven依赖可能是不同的,如果你的版本不对,可能会导致依赖下载不下来。

另外笔者用的是IDEA自带的maven,不是自己安装配置的,所以如果你也要使用的话,请在如下地方进行设置:

Spring Cloud微服务之Feign——声明式服务间调用_第1张图片

如果大家在搭建过程中遇到其他问题,请在下方进行评论留言或私信,我会及时查看。

搭建服务注册中心

开发工具是IDEA,怎样创建项目我就不说了,在Spring Cloud微服务入门级教程(零基础,最详细,可运行)里写的很详细。需要注意的是,在创建项目的时候要勾选上eureka依赖:

Spring Cloud微服务之Feign——声明式服务间调用_第2张图片

项目创建好之后,目录结构如下:

Spring Cloud微服务之Feign——声明式服务间调用_第3张图片

然后我们在启动类上面添加注解@EnableEurekaServer,表明这是一个服务注册中心:

package com.demo.feign.register;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@SpringBootApplication
@EnableEurekaServer
public class DemoRegisterApplication {

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

}

然后在配置文件填写如下配置(注意我的后缀是yml,如果你的是properties请修改格式):

# 端口8080
server:
  port: 8080

# 禁止注册自己
eureka:
  client:
    fetch-registry: false
    register-with-eureka: false
    service-url:
      defaultZone: http://localhost:8080/eureka/

搭建服务1——service-a

这里我们将service-a作为一个服务,待会建一个service-b来调用service-a。

这里我们选择的依赖有Web、eureka-client、lombok(需单独在IDEA进行安装),对应的依赖如下:

Spring Cloud微服务之Feign——声明式服务间调用_第4张图片

然后我们需要在启动类添加注解,表明这是一个服务客户端:

package com.cloud.feign.servicea;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@EnableEurekaClient
@SpringBootApplication
public class ServiceAApplication {

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

}

然后在配置文件进行如下配置:

# 端口 8081
server:
  port: 8081

# 服务名称 service-a
spring:
  application:
    name: service-a

# 注册到服务中心
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka/

为了良好的工程习惯,我们新建一个back包,并且在包下新建Back.java类,用于统一返回数据格式:

package com.cloud.feign.servicea.back;
 
 
import lombok.*;
import lombok.experimental.Accessors;
 
import java.io.Serializable;
 
/**
 *  

* 统一返回数据 *

* * @author 秋枫艳梦 * @date 2020-01-11 * */ @Builder @ToString @Accessors(chain = true) @AllArgsConstructor public class Back implements Serializable { private static final long serialVersionUID = 1L; /** * 运行成功 */ final String RUN_SUCCESS = "0"; /** * 运行失败 */ final String RUN_ERROR = "1"; /** * 业务失败 */ final Boolean BACK_ERROR = false; /** * 业务成功 */ final Boolean BACK_SUCCESS = true; @Getter @Setter private String code = RUN_SUCCESS; @Getter @Setter private String msg = "success"; @Getter @Setter private Boolean state = BACK_SUCCESS; @Getter @Setter private Long count = null; @Getter @Setter private T data; public Back() { super(); } public Back(T data) { super(); this.data = data; } public Back error(String msg) { this.state = BACK_ERROR; this.msg = msg; return this; } public Back msg(String msg) { this.msg = msg; return this; } }

然后我们新建entity包,并且在此包中新建Student.java实体类:

package com.cloud.feign.servicea.entity;

import lombok.Data;

/**
 *  

* 学生实体 *

* * @author 秋枫艳梦 * @date 2020-02-25 * */ @Data public class Student { //姓名 private String name; //年龄 private Integer age; }

然后我们新建controller包,并在包下面新建StudentController.java类:

package com.cloud.feign.servicea.controller;

import com.cloud.feign.servicea.back.Back;
import com.cloud.feign.servicea.entity.Student;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 *  

* 学生模块,控制器 *

* * @author 秋枫艳梦 * @date 2020-02-25 * */ @RestController @RequestMapping(value = "/student") public class StudentController { /** * 简单的查询,不带任何参数 * @return 响应数据 * */ @GetMapping(value = "/get") public Back get () { Student student = new Student(); student.setName("张三"); student.setAge(18); return new Back<>(student).msg("success"); } }

最终的项目结构:

Spring Cloud微服务之Feign——声明式服务间调用_第5张图片

搭建服务2——service-b

现在我们搭建另一个服务service-b,在此服务中将通过Feign服务间调用的方式去调用我们在service-a当中编写的接口。

新建项目时除了service-a的那些依赖,因为我们要用到Feign,所以要勾选OpenFeign依赖,所有的依赖如下:

Spring Cloud微服务之Feign——声明式服务间调用_第6张图片

接下来在启动类添加@EnableEurekaClient和@EnableFeignClients,前者 表明这是一个服务客户端,后者表明此客户端需要通过Feign的方式调用其他的服务,通过此注解开启对Feign功能的支持。

package com.cloud.feign.serviceb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableEurekaClient
@EnableFeignClients
@SpringBootApplication
public class ServiceBApplication {

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

}

然后我们在配置文件进行如下配置:

server:
  port: 8082

spring:
  application:
    name: service-b

eureka:
  client:
    service-url:
      defaultZone : http://localhost:8080/eureka/

至此,基本的环境就搭建好了,我们马上开始进行调用。

在service-b中通过Feign调用service-a

首先我们先在service-b项目中建一个包,叫service,并且在里面新建一个StudentService接口,用于调用service-a服务:

package com.cloud.feign.serviceb.service;

import com.cloud.feign.serviceb.fallback.StudentBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 *  

* 调用其他服务的声明式接口、Feign客户端 * 我们使用@FeignClient注解将此接口声明为一个Feign客户端,并且通过 * value属性指定此接口要请求的服务名称,这里即是service-a *

* * @author 秋枫艳梦 * @date 2020-02-27 * */ @FeignClient(value = "service-a") public interface StudentService { /** * 使用@GetMapping注解,表明此方法要请求service-a服务下的/student/get接口, * 相当于RestTemplate中的 http://service-a/student/get * * 由于此处我们并没有处理service-a服务返回的结果,只是做了一下类似于请求转发, * 返回结果是service-a决定的,所以在这里指定为Object * */ @GetMapping(value = "/student/get") Object get (); }

上面的代码注释,笔者已经解释得很清楚了,当我们在service-b中的其他类(比如控制器)中调用StudentService的get()方法,那么Feign就会请求service-a服务下的/student/get接口,并将结果返回给调用者。

接下来,我们在service-b中新建一个包controller,并且在里面新建一个控制器,作为我们测试的入口:

package com.cloud.feign.serviceb.controller;

import com.cloud.feign.serviceb.service.StudentService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

/**
 *  

* 测试入口 *

* * @author 秋枫艳梦 * @date 2020-02-27 * */ @RestController @RequestMapping(value = "/test") @AllArgsConstructor public class TestController { //装配Feign服务间调用组件 private StudentService service; /** * 获取数据,调用StudentService的get()方法 * */ @GetMapping(value = "/get") public Object get () { return service.get(); } }

 

现在让我们将服务注册中心、service-a、service-b全部启动,可以看到注册中心已经将服务注册进来:

Spring Cloud微服务之Feign——声明式服务间调用_第7张图片

我们访问在service-b中的接口,即localhost:8082/test/get ,如果没问题的话应该会把service-a中的接口数据返回给我们,看一下效果:

Spring Cloud微服务之Feign——声明式服务间调用_第8张图片

可以看到我们已经通过Feign实现了简单的服务间调用,而且是不是比RestTemplate简单好多? 而且,Feign也集成来Ribbon,默认是支持负载均衡的,如果你有多个service-a实例,并且让这些实例返回不同的数据,你就会看到效果。

进阶:Feign服务间调用—传递各种参数

上面我们只是实现了最简单的get请求,而且什么参数都没有传,真正的项目环境中可要复杂的多,我们的service-b服务在调用service-a服务的时候可能要传递各种参数,下面我们就实现各种传参的调用示例。

先总结一下我们常用的传参方式:

  • RequestParam,也是最基本的传参,直接跟在路径后面,如http://www.xxx.com/list?id=1&name=张三
  • RequestBody,经常用于POST或PUT请求,新增或编辑数据时使用,是一种JSON请求体的参数
  • PathVariable,在RESTful风格的路径中比较常见,比如删除id为1的学生:http://www.xxx.com/student/del/1
  • RequestHeader,即将参数写到请求头中,多用于token、身份验证数据或敏感数据

Feign中各个服务之间传递参数的方式,跟平常的controller层入参基本一样,所以特别浅显易懂。接下来,笔者将带领大家实现这四种传参方式,基本可以满足开发中95%以上的使用场景。

首先,我们在service-a中的controller层扩展几个方法,分别涵盖上面的四种参数传递方式:

注意这里的代码是service-a中的StudentController.java!!!

package com.cloud.feign.servicea.controller;

import com.cloud.feign.servicea.back.Back;
import com.cloud.feign.servicea.entity.Student;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 *  

* 学生模块,控制器 *

* * @author 秋枫艳梦 * @date 2020-02-25 * */ @RestController @RequestMapping(value = "/student") public class StudentController { /** * 简单的查询,不带任何参数 * @return 响应数据 * */ @GetMapping(value = "/get") public Back get () { Student student = new Student(); student.setName("张三"); student.setAge(18); return new Back<>(student).msg("success"); } /** * RequestParam参数 * @param id 接收到的id * @return 响应数据 * */ @GetMapping(value = "/get/by/id") public Back getById (@RequestParam(value = "id") Integer id) { //为了方便测试观察,直接将id返回去 return new Back<>(id).msg("根据ID查询成功"); } /** * RequestBody请求体参数 * @param student 学生实体 * @return 响应数据 * */ @PostMapping(value = "/save") public Back save (@RequestBody Student student) { //为了方便测试观察,直接将实体返回去,你也可以在控制台进行打印输出 return new Back<>(student).msg("保存成功"); } /** * PathVariable参数 * @param id 接收到的id * @return 响应数据 * */ @DeleteMapping(value = "/delete/{id}") public Back delete (@PathVariable Integer id) { return new Back<>(id).msg("删除成功"); } /** * 请求头参数,从请求头中获取相应的参数 * @param request 请求对象 * @return 响应数据 * */ @GetMapping(value = "/token") public Back header (HttpServletRequest request) { return new Back<>(request.getHeader("token")).msg("success"); } }

接下来我们在service-b中的StudentService.java,也扩展一下,和上面的对应上,注意看注释:

这里需要将service-a中的实体Student,拿到service-b中一份

package com.cloud.feign.serviceb.service;

import com.cloud.feign.serviceb.entity.Student;
import com.cloud.feign.serviceb.fallback.StudentBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 *  

* 调用其他服务的声明式接口、Feign客户端 * 我们使用@FeignClient注解将此接口声明为一个Feign客户端,并且通过 * value属性指定此接口要请求的服务名称,这里即是service-a *

* * @author 秋枫艳梦 * @date 2020-02-27 * */ @FeignClient(value = "service-a") public interface StudentService { /** * 使用@GetMapping注解,表明此方法要请求service-a服务下的/student/get接口, * 相当于RestTemplate中的 http://service-a/student/get * * 由于此处我们并没有处理service-a服务返回的结果,只是做了一下类似于请求转发, * 返回结果是service-a决定的,所以在这里指定为Object * */ @GetMapping(value = "/student/get") Object get (); /** * Feign调用其他服务时,@RequestParam必须显式声明参数名,否则会报错。 * 我们在参数前面加上注解,就像Controller层接收参数一样,这样Feign在 * 调用服务时就会以对应的注解方式进行传参,假设id为1,这里的效果等价于: * http://service-a/student/get/by/id?id=1 * * @param id ID * */ @GetMapping(value = "/student/get/by/id") Object getById (@RequestParam(value = "id") Integer id); /** * 由于我们此处用的注解是@PostMapping,那么这个方法被调用时,其实就是以 * POST的形式调用了 http://service-a/student/save 接口,并且将接收到的 * 实体以JSON请求体的方式传递过去 * * @param student 数据实体 * */ @PostMapping(value = "/student/save") Object save (@RequestBody Student student); /** * 同上,Feign会先识别出此处的方法注解是@DeleteMapping,就会以delete方式 * 请求 http://service-a/student/delete/接口,又因为我们使用了@PathVariable * 注解,所以假设当参数为1时,等价于: * http://service-a/student/delete/1 * * @param id ID * */ @DeleteMapping(value = "/student/delete/{id}") Object delete (@PathVariable Integer id); /** * 由于此处使用了@RequestHeader注解,Feign在调用/student/token接口 * 时会把token写到请求头中,并且被调用者可以通过HttpServletRequest对 * 象获取到这个参数 * * @param token token * */ @GetMapping(value = "/student/token") Object headers (@RequestHeader(value = "token") String token); }

然后我们继续修改service-b中的TestController.java:

package com.cloud.feign.serviceb.controller;

import com.cloud.feign.serviceb.entity.Student;
import com.cloud.feign.serviceb.service.StudentService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 *  

* 测试入口 *

* * @author 秋枫艳梦 * @date 2020-02-27 * */ @RestController @RequestMapping(value = "/test") @AllArgsConstructor public class TestController { //装配Feign服务间调用组件 private StudentService service; /** * 获取数据,调用StudentService的get()方法 * */ @GetMapping(value = "/get") public Object get () { return service.get(); } @GetMapping(value = "/get/by/id") public Object getById (@RequestParam(value = "id") Integer id) { return service.getById(id); } @PostMapping(value = "/save") public Object save (@RequestBody Student student) { return service.save(student); } @DeleteMapping(value = "/delete/{id}") public Object delete (@PathVariable Integer id) { return service.delete(id); } @GetMapping(value = "/token") public Object header (HttpServletRequest request) { return service.headers(request.getHeader("token")); } }

现在让我们开始使用Postman进行测试各个接口:

Spring Cloud微服务之Feign——声明式服务间调用_第9张图片

Spring Cloud微服务之Feign——声明式服务间调用_第10张图片 

Spring Cloud微服务之Feign——声明式服务间调用_第11张图片 

Spring Cloud微服务之Feign——声明式服务间调用_第12张图片 

通过上面的结果,我们已经大功告成啦!

再次进阶:引入Hystrix熔断器

我们已经通过Feign实现了服务间的调用,但是服务和服务之间的这种依赖关系,可能一个服务挂了,另一个依赖它的服务也会受影响。我们要有一种机制,如果在调用某个服务的时候请求出错或者超时,要及时熔断,避免大量请求被阻塞,同时我们可能希望记录一下日志,或者将数据回滚,就像try-catch那样。

在之前的文章中笔者介绍过Gateway网关配合Hystrix的使用,但是我们这里没有走网关,走的是服务间内部调用,那该怎么办呢?强大的Feign对Hystrix也有很好的支持,就拿本文的例子来说:

我们可以声明一个类,比如叫StudentBack,用来执行service-a中的student相关接口异常时的回滚操作,我们只需要用这个类实现我们的Feign客户端StudentService接口,并且在注解上通过fallback指定StudentBack.class,那么出现异常或者超时,将会执行StudentBack中的相关方法。看一个例子!

我们在service-b中引入Back类,用于返回信息,同时建一个fallback包,然后建一个处理类StudentBack.java:

package com.cloud.feign.serviceb.fallback;

import com.cloud.feign.serviceb.back.Back;
import com.cloud.feign.serviceb.entity.Student;
import com.cloud.feign.serviceb.service.StudentService;
import org.springframework.stereotype.Component;

/**
 *  

* 调用service-a失败时的熔断处理方法 * 使用@Component注解声明这个类为Bean *

* * @author 秋枫艳梦 * @date 2020-02-27 * */ @Component public class StudentBack implements StudentService { /** * 调用http://service-a/student/get失败时的处理方法 * * */ @Override public Object get() { return new Back<>().error("内部系统异常"); } @Override public Object getById(Integer id) { return null; } @Override public Object save(Student student) { return null; } @Override public Object delete(Integer id) { return null; } @Override public Object headers(String token) { return null; } }

上面只以一个get()方法为例。因为StudentService是一个interface接口,而这里的StudentBack是一个实现了StudentService接口的类,所以每当一个StudentService中的方法执行失败时(也就是调用其他服务的接口失败时),都会执行StudentBack中相应的实现方法,这样我们就可以为每一个请求的失败进行定制化处理,记录日志、重试、数据回滚……

然后不要忘了在我们的StudentService中的注解上指定fallback:

Spring Cloud微服务之Feign——声明式服务间调用_第13张图片

最后还需要在配置文件中开启Feign对Hystrix的支持:

Spring Cloud微服务之Feign——声明式服务间调用_第14张图片


然后我们在service-a中的接口,制造一个异常:

Spring Cloud微服务之Feign——声明式服务间调用_第15张图片

 重启服务,进行测试,访问service-b中的接口:

Spring Cloud微服务之Feign——声明式服务间调用_第16张图片

 以上,我们就实现了异常情况下的处理。

如果要配置超时熔断,则将service-b的配置文件修改如下:

server:
  port: 8082

spring:
  application:
    name: service-b

eureka:
  client:
    service-url:
      defaultZone : http://localhost:8080/eureka/

feign:
  hystrix:
    enabled: true

# ribbon级超时配置
ribbon:
  ReadTimeout: 3000
  ConnectTimeout: 3000

# hystrix级超时配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000

以上就是关于超时的配置,如果service-a的controller中睡眠8秒,就可以看到效果,关于这里的超时配置,参考的是下面的这篇文章,这个博主说的很好:

https://blog.csdn.net/east123321/article/details/82385816

总结

今天的文章到这里就结束了,希望可以帮助到大家。如果有什么问题,可以给我评论留言或者私信,我会及时查看,欢迎大家多来交流。

另外,给大家贴出来Spring Cloud Feign的官方文档,里面有一些新的玩法大家可以研究一下:

https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html

你可能感兴趣的:(企业级开发框架)