一、OpenFeign介紹
OpenFeign是 種聲明式,模版化的HTTP客戶端。使 OpenFeign進 遠程調 時,開發(fā)者完全感知不到這是在進 遠程調 , 是像在調 本地 法 樣。使 式是注解+接 形式,把需要調 的遠程接 封裝到接 當中,映射地址為遠程接 的地址。在啟動SpringCloud應 時,Feign會掃描標有@FeignClient注解的接 , 成代理并且注冊到Spring容器當中。 成代理時Feign會為每個接 法創(chuàng)建 個RequestTemplate對象,該對象封裝HTTP請求需要的全部信息,請求參數名、請求 法等信息都是在這個過程中確定的,模版化就體現在這 。
二、OpenFeign的使用
- 搭建前置環(huán)境,在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 org.springframework.boot spring-boot-starter-actuator 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#服務端 號
- 在配置類上開啟服務發(fā)現以及允許遠程調
@EnableDiscoveryClient //開啟服務發(fā)現@EnableFeignClients //開啟服務調 ,只需要在調 開啟即可
- 服務運 之后可以在consul的UI界 看到運 的服務,consul會定時檢查服務的健康狀態(tài)
- 創(chuàng)建遠程調用接口
@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#請求地址
- 創(chuàng)建遠程調用接口
@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調 是新創(chuàng)建 個Request,因此在請求時不會攜帶 些原本就有的信息,例如Cookie,因此需要 定義RequestInterceptor對Request進 額外設置, 般情況下,寫 Cookie是 較常 的做法,如下設置
@Configurationpublic class BeanConfig { @Bean public RequestInterceptor requestInterceptor(){ return template -> { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //此處可以根據業(yè)務 具體定制攜帶規(guī)則 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執(zhí) 兩條語句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三個 法直接本地執(zhí) } else if (“hashCode”.equals(method.getName())) { return hashCode(); } else if (“toString”.equals(method.getName())) { return toString(); } //執(zhí) 法對應的 法處理器MethodHandler,這個接 是Feign提供的,與InvocationHandler 任何關系,只有 個invoke 法 return dispatch.get(method).invoke(args);}//點進上 的invoke 法public Object invoke(Object[] argv) throws Throwable { //創(chuàng)建 個request模版 RequestTemplate template = buildTemplateFromArgs.create(argv); while (true) { try { return executeAndDecode(template, options); //創(chuàng)建request執(zhí) 并且解碼 } }}Object executeAndDecode(RequestTemplate template, Options options) throws Throwable { Request request = targetRequest(template); //創(chuàng)建Request并增強 Response response = client.execute(request, options); //執(zhí) 調用請求,不再繼續(xù)分析了 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) { //執(zhí) 定義的apply 法 interceptor.apply(template); } //創(chuàng)建Request return target.apply(template);}
四、補充
- 關于Client接 的實現類,使 注冊中 和使 配置中 其流程稍有區(qū)別
//使 配置中 拿url 式進 調 ,使 的是Client的默認內部實現類 Default ,其中Default使 的是HttpURLConnection進 Http請求的HttpURLConnection connection = convertAndSend(request, options);//如果使 的是服務發(fā)現,使 的使 Client的實現類FeignBlockingLoadBalancerClient,它會去根據配置的服務名去注冊中 查找服務的IP地址和端 號,執(zhí) 使 的仍然是默認實現類Default,通過HttpURLConnection請求//FeignBlockingLoadBalancerClient,根據服務名稱查找服務IP地址、端 88 ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);//具體實現 法,BlockingLoadBalancerClient類中 145 Response loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();//還有其他實現Client接 的客戶端,例如ApacheHttpClient,ApacheHttpClient帶有連接池功能,具有優(yōu)秀的HTTP連接復 能 ,需要通過引 依賴來使