「Feign」OpenFeign入門以及遠端呼叫

一、OpenFeign介紹

OpenFeign是⼀種宣告式,模版化的HTTP客戶端。使⽤OpenFeign進⾏遠端調⽤時,開發者完全感知不到這是在進⾏遠端調⽤,⽽是像在調⽤本地⽅法⼀樣。使⽤⽅式是註解+接⼝形式,把需要調⽤的遠端接⼝封裝到接⼝當中,對映地址為遠端接⼝的地址。在啟動SpringCloud應⽤時,Feign會掃描標有@FeignClient註解的接⼝,⽣成代理並且註冊到Spring容器當中。⽣成代理時Feign會為每個接⼝⽅法建立⼀個RequestTemplate物件,該物件封裝HTTP請求需要的全部資訊,請求引數名、請求⽅法等資訊都是在這個過程中確定的,模版化就體現在這⾥。

二、OpenFeign的使用

搭建前置環境,在pom。xml檔案中引入依賴,可以選擇使用註冊中心或者配置中心

org。springframework。cloud spring-cloud-dependencies 2020。0。3 pom import <!—— 配置中⼼依賴——> org。springframework。cloud spring-cloud-starter-consul-config <!—— 註冊中⼼依賴 ——> org。springframework。cloud spring-cloud-starter-consul-discovery <!—— 健康檢查,將服務註冊到consul需要 ——> org。springframework。boot spring-boot-starter-actuator <!—— openfeign,在需要遠端調⽤的服務中引⼊ ——> org。springframework。cloud spring-cloud-starter-openfeign

1。使用註冊中心

使⽤註冊中⼼,將服務註冊到consul(nacos),調⽤者拿到被調⽤服務的地址端⼝進⾏調⽤

spring。cloud。consul。host=192。168。0。124 #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 pageQuestion(PageQuestionReq req);}

簡單使用

@RestController@RequestMapping(“/service/remote”)public class RemoteController { @Autowired private Service2Remote service2Remote; @PostMapping(“/getQuestionList”) public List getQuestionList(@RequestBody PageQuestionReq req){ List 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 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 resMap = null; try { resMap = JSON。parseObject(result, Map。class); } catch (Exception e) { //返回結果是字串 return result; }}

4。遠端呼叫攜帶Cookie

由於feign調⽤是新建立⼀個Request,因此在請求時不會攜帶⼀些原本就有的資訊,例如Cookie,因此需要⾃定義RequestInterceptor對Request進⾏額外設定,⼀般情況下,寫⼊Cookie是⽐較常⻅的做法,如下設定

@Configurationpublic 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提供的FeignInvocationHandlerpublic 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 loadBalancerResponse = Mono。from(loadBalancer。choose(request))。block();//還有其他實現Client接⼝的客戶端,例如ApacheHttpClient,ApacheHttpClient帶有連線池功能,具有優秀的HTTP連線復⽤能⼒,需要透過引⼊依賴來使⽤