场景
当使用多服务时,经常会遇到服务之间的相互调用。
一个服务如果要调用另一个服务的接口,需要:
① 定义一个请求,并设置目标地址。
② 为这个请求设置参数。
③ 为这个请求设置请求头等属性。
④ 发送请求并接收结果。
⑤ 将结果转换为本地对象。
以上流程非常繁琐,即使借助RestTemplate
这样的辅助类,每次调用接口都要写这样一堆代码,非常不友好。
Feign流程
为了解决上述场景的问题,现在要将这个流程封装成请求工具类,从而方便地进行服务间的接口调用。
于是设计可简化为:
① 为工具类设置目标地址和请求参数。
② 工具类负责构造请求并发送。
③ 工具类将收到的返回自动转换为本地对象。
这就是Feign的思路。
① 在本地定义一个service
,在该service
上使用注解声明远程目标接口。
② 使用@FeignClient
标记该service
。
③ 在本地的逻辑中引入该service
,并直接使用其接口。
原理
Feign实现的原理核心是动态代理。
使用@FeignClient
注解会令Feign创建动态代理。当有其他服务需要调用该被注解的类或接口时,Feign就会根据注解的情况动态构造请求并发送,然后接收结果并返回。
即:A工程提供接口,B工程使用Feign来调用A工程的接口。
A工程接口可通过浏览器或postman等常规方式调用。整个过程中A并不关心B是怎么来调用自身接口的。
使用
Feign可用于微服务中多个服务间的交互,或者多个不同项目间的交互。
依然引用上述A工程和B工程的例子。A工程使用8080端口,B工程使用8081端口。
A工程正常实现即可,不需要引入Feign。B工程作为请求的发出者,需引入Feign及其相关依赖。
设A工程提供了一个接口:
127.0.0.1:8080/test/t1
无参数。
其返回值为:this is project A.
。
依赖
在B工程的pom.xml中引入依赖。
通常,Feign使用cloud版本,因此需要依赖spring-cloud:
<!-- spring-cloud 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- feign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
注意spring-cloud的版本需要与SpringBoot的版本相匹配,否则可能会报*Error creating bean with name ‘configurationPropertiesBeans’*的错误。
开启Feign功能
在B工程的Application文件前增加注解@EnableFeignClients
:
@SpringBootApplication
@EnableFeignClients
public class TestBApplication {
public static void main(String[] args) {
SpringApplication.run(TestBApplication.class, args);
}
}
定义请求service
在B工程中定义一个service
,在前面添加@FeignClient
注解。
@FeignClient(name = "feign-test",
url = "http://localhost:8080")
public interface FeignService {
@GetMapping(value = "/test/t1")
String feignTest();
}
其中url
属性指明了请求地址前缀。@GetMapping
中指明了具体接口。
其他关于Fegin的参数见下文。
引用请求service
在B工程中的测试controller中引入service
并使用:
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private FeignService feignService;
@GetMapping("/doFeignTest")
public String doFeignTest() {
return feignService.feignTest();
}
}
调用
在浏览器中调用:
127.0.0.1:8081/test/doFeignTest
浏览器会显示返回内容:this is project A.
传参
A工程的接口需要传入参数:
@GetMapping(value = "/test/t2")
String feignTest2(@RequestParam String s);
则B工程在service
中也需要传入参数:
public interface FeignService {
@GetMapping(value = "/test/t2")
String feignTest2(@RequestParam String s);
}
由于是get
请求,且参数唯一,这里使用了@RequestParam
的方式传参。
这样即可将参数传给A工程。
返回对象
A工程接口/test/t3
的返回是一个对象:
@Data
public class AStudent {
String name;
int age;
}
但对于B工程来说,接收到的返回就是一组数据流。B工程可定义自己的对象来进行接收:
@Data
public class BPerson {
String name;
int age;
}
使用该对象接收返回即可:
@FeignClient(...)
public interface FeignService {
@GetMapping(value = "/test/t3")
BPerson feignTest3();
}
熔断
所谓熔断,就是防止调用异常时程序无法继续执行。
在Fegin中,可以设置一个本地类作为熔断触发的回调。一旦调用出现异常,Fegin就会改为调用本地类的对应方法,从而获取一个替代的返回结果。这样可以让程序继续顺利执行。
开启熔断
首先要在配置文件application.properties中开启熔断:
feign.hystrix.enabled=true
定义熔断回调类
前面定义的Feign service类是一个interface
:
@FeignClient(...)
public interface FeignService {
@GetMapping(value = "/test/t1")
String feignTest();
}
其下有一个接口feignTest()
。
熔断回调类需对该service
进行派生,并重载feignTest()
:
@Component
public class FeignServiceCallback implements FeignService {
@Override
public String feignTest() {
return "调用失败,返回熔断结果";
}
}
设置熔断回调
在Feign service类中,为@FeignClient
添加fallback
参数:
@FeignClient(name = "feign-test",
url = "http://localhost:8080",
fallback = FeignServiceCallback.class)
public interface FeignService {
...
}
使用
这样,当Feign调用A工程的接口时,若出现问题,Fegin会调用FeignServiceCallback.feignTest()
,返回:“调用失败,返回熔断结果”。
多服务下的使用方式
多服务下,会出现这样一种现象:A提供接口,B和C都需要调用该接口。那么按上述方式,B和C就都需要添加一个interface FeignService
。若B和C调用的接口相同,那么B和C添加的interface FeignService
也是相同的。那么,若还有D、E、F……等服务都需要调用A提供的接口,那每个服务都要添加一个interface FeignService
,且都是相同的实现。
于是,考虑对interface FeignService
进行复用。如果可以定义一个公共的interface FeignService
,那么每个需要调用A接口的服务直接将interface FeignService
引入即可使用。
基于该思路,将interface FeignService
放在A中。A定义一个针对自身接口的interface FeignService
,其他服务只要引入这个interface FeignService
即可调用。
若A、B、C等都处于同一个工程下,那么B、C等可直接进行引入;但若处于不同的工程下,要引入,就得使用其他方式,例如打包为jar后复制过来。
实例:
① A所在的工程是一个多服务工程,A是其中一个子服务。现在添加一个新的子服务,称之为A_Api。在其中添加interface FeignService
。
② 对A_Api子服务进行打包,得到一个jar,称为A_Api.jar。
③ B是另一个工程中的子服务。将A_Api.jar引入到B中,这样B就有了interface FeignService
。于是B可以调用A的接口了。
在上述流程③中,B调用的interface FeignService
是打包好的,那么意味着请求的url地址也打包在其中了。而实际部署环境可能会变。于是不在interface FeignService
中配置url,仅配置服务名。然后借助nacos这样的中间件通过服务名来获取A服务的url。
关于B中引入A_Api.jar,直接将interface FeignService
打包为jar然后复制到B中是一种方式。若有条件,更推荐搭建一个Maven,A将interface FeignService
打包并上传到Maven,然后B在pom.mxl中引入。
interface FeignService
实现方式
@FeignClient(name = "service-a",
fallback = FeignServiceCallback.class)
public interface FeignService {
@GetMapping(value = "/test")
String feignTest();
}
里面定义了一个方法,会访问service-a
的/test
接口。
那这意味着A服务service-a
必须提供一个对应的/test
接口。
通常,接口都是由controller来提供。但这里也可以采用另一种方式:
在service-a
中定义一个class FeignServiceImpl implements FeignService
:
@Controller
public class FeignServiceImpl implements FeignService {
@Resource
private IAService aService;
@Override
@GetMapping(value = "/test")
public String feignTest() {
return aService.doTest();
}
}
其中:
FeignServiceImpl
从FeignService
派生,因此必须实现其所有接口。这一点是java语法决定的。当然,这也刚好满足了与FeignService
的接口一一对应的要求。FeignServiceImpl
位于A服务文件夹下,因此可引入并使用A服务的任意类。就像上面,引入了private IAService aService;
。FeignServiceImpl
为了能接收FeignService
中Feign的调用,需要使用@GetMapping
来为每个方法添加接口路径。在类前也添加了@Controller
来充当controller的作用。于是对于FeignService
来说,FeignServiceImpl
就是个controller而已。
该方式相当于是提供了一种强制的规范。但从本质来说,FeignServiceImpl
从FeignService
派生并非是必要的。定义一个controller也能实现相同的作用。
@FeignClient
常用属性
@FeignClient
注解可包含多个属性,其常用的有:
- value/name: 这两个属性作用相同,表示别名。多服务下注册到服务注册中心时使用的服务名,用于服务发现。
- url: 指定@FeignClient调用的地址。
- configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract。
- fallback: 指定熔断的回调类。当调用远程接口失败或超时,会调用对应接口。fallback指定的类必须实现@FeignClient标记的接口。
- fallbackFactory: 工厂类,用于生成fallback类实例。通过该属性可实现每个接口通用的调用失败处理逻辑,减少重复的代码。
- path: 定义当前FeignClient的统一前缀。