一、OpenFeign介紹 OpenFeign是⼀種聲明式,模版化的HTTP客戶端。使⽤OpenFeign進⾏遠程調⽤時,開發者完全感知不到這是在進⾏遠程調⽤,⽽是像在調⽤本地⽅法⼀樣。使⽤⽅式是註解+接⼝形式,把需要調⽤的遠程接⼝封裝到接⼝當中,映射地址為遠程接⼝的地址。在啟動SpringClou ...
一、OpenFeign介紹
OpenFeign是⼀種聲明式,模版化的HTTP客戶端。使⽤OpenFeign進⾏遠程調⽤時,開發者完全感知不到這是在進⾏遠程調⽤,⽽是像在調⽤本地⽅法⼀樣。使⽤⽅式是註解+接⼝形式,把需要調⽤的遠程接⼝封裝到接⼝當中,映射地址為遠程接⼝的地址。在啟動SpringCloud應⽤時,Feign會掃描標有@FeignClient註解的接⼝,⽣成代理並且註冊到Spring容器當中。⽣成代理時Feign會為每個接⼝⽅法創建⼀個RequestTemplate對象,該對象封裝HTTP請求需要的全部信息,請求參數名、請求⽅法等信息都是在這個過程中確定的,模版化就體現在這⾥。
二、OpenFeign的使用
- 搭建前置環境,在pom.xml文件中引入依賴,可以選擇使用註冊中心或者配置中心
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 配置中⼼依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<!-- 註冊中⼼依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 健康檢查,將服務註冊到consul需要 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- openfeign,在需要遠程調⽤的服務中引⼊ -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
1.使用註冊中心
- 使⽤註冊中⼼,將服務註冊到consul(nacos),調⽤者拿到被調⽤服務的地址端⼝進⾏調⽤
spring.cloud.consul.host=192.168.137.1 #consul地址
spring.cloud.consul.port=8080 #端⼝號
spring.cloud.consul.discovery.service-name=service-test-01 #服務名稱
spring.cloud.consul.discovery.health-check-interval=1m #健康檢查間隔時間
server.port=10000 #服務端⼝號
- 在配置類上開啟服務發現以及允許遠程調⽤
@EnableDiscoveryClient //開啟服務發現
@EnableFeignClients //開啟服務調⽤,只需要在調⽤⽅開啟即可
- 服務運⾏之後可以在consul的UI界⾯看到運⾏的服務,consul會定時檢查服務的健康狀態
- 創建遠程調用介面
@FeignClient("serviceName")
public interface Service2Remote {
/** 這⾥有⾃定義解碼器對遠程調⽤的結果進⾏解析,拿到真正的返回類型,所以接⼝返回值類型和遠程接⼝返回類型保持⼀致 **/
@PostMapping("/page")
List<QuestionResp> pageQuestion(PageQuestionReq req);
}
- 簡單使用
@RestController
@RequestMapping("/service/remote")
public class RemoteController {
@Autowired
private Service2Remote service2Remote;
@PostMapping("/getQuestionList")
public List<QuestionResp> getQuestionList(@RequestBody PageQuestionReq req){
List<QuestionResp> result = service2Remote.pageQuestion(req);
//對拿到的數據進⾏處理...
return result;
}
}
2.使用配置中心
- 將請求的URL寫在配置中⼼進⾏讀取修改配置⽂件
spring.cloud.consul.config.format=KEY_VALUE #consul⽀持yaml格式和Key-value形式
spring.cloud.consul.config.enabled=true #開啟配置
spring.cloud.consul.config.prefixes=glab/plat/wt/application/test #consul配置存放的外層⽂件夾⽬錄
spring.cloud.consul.config.default-context=config #⽗級⽂件夾
spring.cloud.consul.config.watch.delay=1000 #輪詢時間
spring.cloud.consul.discovery.enabled=false #關閉註冊
remote.url=www.baidu.com #請求地址
- 創建遠程調用介面
@FeignClient(name = "service2RemoteByUrl",url = "${remote.url}") //name需要配置,URL從配置中⼼讀取
public interface Service2RemoteByUrl {
@PostMapping("/page")
List<QuestionResp> pageQuestion(PageQuestionReq req);
}
3.自定義解碼器(編碼器)
//⾃定義解碼器實現Decoder接⼝,重寫decode⽅法即可,根據具體需求進⾏編寫
//如果是⾃定義編碼器,需要實現Encoder接⼝,重寫encode⽅法
public class FeignDecoder implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException,DecodeException, FeignException {
if (response.body() == null){
throw new DecodeException(ErrorEnum.EXECUTE_ERR.getErrno(),"沒有獲取到有效結果值",response.request());
}
// 拿到值
String result = Util.toString(response.body().asReader(Util.UTF_8));
Map<String,Object> resMap = null;
try {
resMap = JSON.parseObject(result, Map.class);
} catch (Exception e) {
//返回結果是字元串
return result;
}
}
4.遠程調用攜帶Cookie
- 由於feign調⽤是新創建⼀個Request,因此在請求時不會攜帶⼀些原本就有的信息,例如Cookie,因此需要⾃定義RequestInterceptor對Request進⾏額外設置,⼀般情況下,寫⼊Cookie是⽐較常⻅的做法,如下設置
@Configuration
public class BeanConfig {
@Bean
public RequestInterceptor requestInterceptor(){
return template -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//此處可以根據業務⽽具體定製攜帶規則
String data = request.getParameter("data");
String code = null;
try {
//這⾥需要轉碼,否則會報錯
code = URLEncoder.encode(data, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
template.query("data",code);
//請求頭中攜帶Cookie
String cookie = request.getHeader("Cookie");
template.header("Cookie",cookie);
};
}
@Bean
public Decoder decoder(){
return new FeignDecoder();
}
}
三、調用流程解析
//在使⽤EnableFeignClients開啟feign功能時,點擊進⼊會看到該註解是通過ImportFeignClientsRegistrar類⽣效的,其中有個⽅法
//registerBeanDefinitions執⾏兩條語句
registerDefaultConfiguration(metadata, registry); //載入預設配置信息
registerFeignClients(metadata, registry); //註冊掃描標有FeignClient的接⼝
//關註registerFeignClients⽅法
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); //在basePackage路徑下掃描並添加標有FeignClient的接⼝
}
for (BeanDefinition candidateComponent : candidateComponents) { //遍歷
if (candidateComponent instanceof AnnotatedBeanDefinition) {
registerClientConfiguration(registry, name, attributes.get("configuration")); //
registerFeignClient(registry, annotationMetadata, attributes); //註冊到Spring容器當中,⽅法詳細在FeignClientsRegistrar類當中
}
}
//在對feign調⽤時進⾏斷點調試
//在⽣成Feign遠程接⼝的代理類時,調⽤處理器是Feign提供的FeignInvocationHandler
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
//equals,hashCode,toString三個⽅法直接本地執⾏
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
//執⾏⽅法對應的⽅法處理器MethodHandler,這個接⼝是Feign提供的,與InvocationHandler⽆任何關係,只有⼀個invoke⽅法
return dispatch.get(method).invoke(args);
}
//點進上⾯的invoke⽅法
public Object invoke(Object[] argv) throws Throwable {
//創建⼀個request模版
RequestTemplate template = buildTemplateFromArgs.create(argv);
while (true) {
try {
return executeAndDecode(template, options); //創建request執⾏並且解碼
}
}
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template); //創建Request並增強
Response response = client.execute(request, options); //執⾏調用請求,不再繼續分析了
response = response.toBuilder().request(request).requestTemplate(template).build();
//如果有重寫解碼器,使⽤⾃定義的解碼器,feign預設使⽤SpringEncoder
if (decoder != null)
return decoder.decode(response, metadata.returnType());
}
Request targetRequest(RequestTemplate template) {
//如果⾃定義了RequestInterceptor,在這⾥可以對Request進⾏增強
for (RequestInterceptor interceptor : requestInterceptors) {
//執⾏⾃定義的apply⽅法
interceptor.apply(template);
}
//創建Request
return target.apply(template);
}
四、補充
- 關於Client接⼝的實現類,使⽤註冊中⼼和使⽤配置中⼼其流程稍有區別
//使⽤配置中⼼拿url⽅式進⾏調⽤,使⽤的是Client的預設內部實現類 Default ,其中Default使⽤的是HttpURLConnection進⾏Http請求的
HttpURLConnection connection = convertAndSend(request, options);
//如果使⽤的是服務發現,使⽤的使⽤Client的實現類FeignBlockingLoadBalancerClient,它會去根據配置的服務名去註冊中⼼查找服務的IP地址和端⼝號,執⾏使⽤的仍然是預設實現類Default,通過HttpURLConnection請求
//FeignBlockingLoadBalancerClient,根據服務名稱查找服務IP地址、端⼝ 88⾏
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
//具體實現⽅法,BlockingLoadBalancerClient類中 145⾏
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
//還有其他實現Client接⼝的客戶端,例如ApacheHttpClient,ApacheHttpClient帶有連接池功能,具有優秀的HTTP連接復⽤能⼒,需要通過引⼊依賴來使⽤