Feign WHAT WHY HOW maven依賴 自動裝配 編寫接口 調用接口 注意事項 原理html
Feign的GitHub描述以下:java
Feign is a Java to Http client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to Http APIs regardless of ReSTfulness.git
簡單的說,Feign是一套Http客戶端"綁定器"。我的理解,這個"綁定"有點像ORM。ORM是把數據庫字段和代碼中的實體"綁定"起來;Feign提供的基本功能就是方便、簡單地把Http的Request/Response和代碼中的實體"綁定"起來。github
舉個例子,在咱們系統調用時,咱們是這樣寫的:web
@FeignClient(url = "${feign.url.user}", name = "UserInfoService",
configuration = FeignConfiguration.UserInfoFeignConfiguration.class)
public interface UserInfoService {
/**
* 查詢用戶數據
*
* @param userInfo 用戶信息
* @return 用戶信息
*/
@PostMapping(path = "/user/getUserInfoRequest")
BaseResult<UserInfoBean> queryUserInfo(Body4UserInfo userInfo);
}
// 使用時
BaseResult<UserInfoBean> response = UserInfoService.queryUserInfo(Body4UserInfo.of(userBean.getId()));
上面這段代碼裏,咱們只須要建立一個Body4UserInfo,而後像調用本地方法那樣,就能夠拿到返回對象BaseResult<UserInfoBean>了。spring
與其它的Http調用方式,例如URLConnection、HttpClient、RestTemplate相比,Feign有哪些優點呢?數據庫
最核心的一點在於,Feign的抽象層次比其它幾個工具、框架都更高。json
首先,通常來講抽象層次越高,其中包含的功能也就越多。
api
此外,抽象層次越高,使用起來就越簡便。例如,上面這個例子中,把Body4UserInfo轉換爲HttpRequest、把HttpResponse轉換爲BaseResult<UserInfoBean>的操做,就不須要咱們操心了。併發
固然,單純從這一個例子中,看不出Feign提供了多大的幫助。可是能夠想一下:若是咱們調用的接口,有些參數要用RequestBody傳、有些要用RequestParam傳;有些要求加特殊的header;有些要求Content-Type是application/json、有些要求是application/x-www-form-urlencoded、還有些要求application/octet-stream呢?若是這些接口的返回值有些是applicaion/json、有些是text/html,有些是application/pdf呢?不一樣的請求和響應對應不一樣的處理邏輯。咱們若是本身寫,可能每次都要從新寫一套代碼。而使用Feign,則只須要在對應的接口上加幾個配置就能夠。寫代碼和加配置,顯而後者更方便。
此外,抽象層次越高,代碼可替代性就越好。若是嘗試過Apache的HttpClient3.x升級到4.x,就知道這種接口不兼容的升級改造是多麼痛苦。若是要從Apache的HttpClient轉到OkHttp上,因爲使用了不一樣的API,更要費一番周折。而使用Feign,咱們只須要修改幾行配置就能夠了。即便要從Feign轉向其它組件,我只須要給UserInfoService提供一個新的實現類便可,調用方代碼甚至一行都不用改。若是咱們升級一個框架、重構一個組件,須要改的代碼成百上千行,那誰也不敢亂動代碼。代碼的可替代性越好,咱們就越能放心、順利的對系統作重構和優化。
並且,抽象層次越高,代碼的可擴展性就越高。若是咱們使用的仍是URLConnection,那麼連Http鏈接池都很難實現。若是咱們使用的是HttpClient或者RESTTemplate,那麼作異步請求、合併請求都須要咱們本身寫不少代碼。可是,使用Feign時,咱們能夠輕鬆地擴展Feign的功能:異步請求、併發控制、合併請求、負載均衡、熔斷降級、鏈路追蹤、流式處理、Reactive……,還能夠經過實現feign.Client接口或自定義Configuration來擴展其它自定義的功能。
放眼Java世界的各大框架、組件,不管是URLConnection、HttpClient、RESTTemplate和Feign,Servlet、Struts1.0/2.0和SpringMVC,仍是JDBCConnection、myBatis/Hibernate和Spring-Data JPA,Redis、Jedis和Redisson,越新、越好用的框架,其抽象層級一般都更高。這對咱們一樣也是一個啓示:咱們須要去學習、瞭解和掌握技術的底層原理;可是在設計和使用時,咱們應該從底層跳出來、站在更高的抽象層級上去設計和開發。尤爲是對業務開發來講,頻繁的需求變動是難以免的,咱們只有作出可以「以不變應萬變」、「以系統的少許變動應對需求的大量變動」,才能從無謂的加班、copy代碼、查工單等重複勞動中解脫出來。怎樣「以不變應萬變」呢?提升系統設計的抽象層次就是一個不錯的辦法。
Feign有好幾種用法:既能夠在代碼中直接使用FeignBuilder來構建客戶端、也可使用Feign自帶的註解、還可使用SpringMVC的註解。這裏只介紹下使用SpringMVC註解的方式。
咱們系統引入的依賴是這樣的:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.1.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>spring-web</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okHttp</artifactId>
<version>10.1.0</version>
</dependency>
直接引入spring-cloud-starter-openfeign,是由於這個包內有feign的自動裝配相關代碼,不須要咱們再本身手寫。
另外,這裏之因此是openfeign、而不是原生的feign,是由於原生的Feign只支持原生的註解,openfeign是SpringCloud項目加入了對SpringMVC註解的支持以後的版本。
引入feign-okHttp則是爲了在底層使用okHttp客戶端。默認狀況下,feign會直接使用URLConnection;若是系統中引入了Apache的HttpClient包,則OpenFeign會自動把HttpClient裝配進來。若是要使用OkHttpClient,首先須要引入對應的依賴,而後修改一點配置。
若是使用了SpringBoot,那麼直接用@EnableFeignClient就能夠自動裝配了。若是沒有使用SpringBoot,則須要本身導入一下其中的AutoConfiguration類:
/**
* 非SpringBoot的系統須要增長這個類,並保證Spring Context啓動時加載到這個類
*/
@Configuration
@ImportAutoConfiguration({FeignAutoConfiguration.class})
@EnableFeignClients(basePackages = "com.test.test.feign")
public class FeignConfiguration {
}
上面這個類能夠沒有具體的實現,可是必須有幾個註解。
@Configuration
使用這個註解是爲了讓Spring Conetxt啓動時裝載這個類。在xml文件裏配<context:component-scan base-package="com.test.user">,或者使用@Component能夠起到相同的做用。
@ImportAutoConfiguration({FeignAutoConfiguration.class})
使用這個註解是爲了導入FeignAutoConfiguration中自動裝配的bean。這些bean是feign發揮做用所必須的一些基礎類,例如feignContext、feignFeature、feignClient等等。
@EnableFeignClients(basePackages = "com.test.user.feign")
使用這個註解是爲了掃描具體的feign接口上的@FeignClient註解。這個註解的用法到後面再說。
爲了使用okHttp、而不是Apache的HttpClient,咱們還須要在系統中增長兩行配置:
# 使用properties文件配置
feign.okHttp.enabled=true
feign.Httpclient.enabled=false
這兩行配置也能夠用yml格式配置,只要能被SpringContext解析到配置就行。配置好之後,FeignAutoConfiguration就會按照OkHttpFeignConfiguration的代碼來把okHttp3.OkHttpClient裝配到FeignClient裏去了。
@Configuration
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class,
FeignHttpClientProperties.class })
public class FeignAutoConfiguration {
// 其它略
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.Httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration {
// 其它略
}
@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(okHttp3.OkHttpClient.class)
@ConditionalOnProperty("feign.okHttp.enabled")
protected static class OkHttpFeignConfiguration {
// 其它略
}
除了這兩個配置以外,FeignClientProperties和FeignHttpClientProperties裏面還有不少其它配置,你們能夠關注下。
依賴和配置都弄好以後,就能夠寫一個Fiegn的客戶端接口了:
@FeignClient(url = "${feign.url.user}", name = "UserInfoService",
configuration = FeignConfiguration.UserFeignConfiguration.class)
public interface UserInfoService {
@PostMapping(path = "/user/getUserInfoRequest")
BaseResult<UserInfoBean> queryUserInfo(Body4UserInfo userInfo);
首先,咱們只須要寫一個接口,並在接口上加上@FeignClient註解、接口方法上加上@RequestMapping(或者@PostMapping、@GetMappping等對應註解)。Feign會根據@EnableFeignClients(basePackages = "com.test.user.feign")的配置,掃描到@FeignClient註解,併爲註解類生成動態代理。所以,咱們不須要寫具體的實現類。
而後,配置好@FeignClient和@PostMapping中的各個字段。@PostMapping註解字段比較簡單,和咱們寫@Controller時的配置方式基本同樣。@FeignClient註解字段有下面這幾個:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
@AliasFor("name")
String value() default "";
@Deprecated
String serviceId() default "";
String contextId() default "";
@AliasFor("value")
String name() default "";
String qualifier() default "";
String url() default "";
boolean decode404() default false;
Class<?>[] configuration() default {};
Class<?> fallback() default void.class;
Class<?> fallbackFactory() default void.class;
String path() default "";
boolean primary() default true;
}
每一個字段的配置含義你們能夠參考GitHub上的文檔,或者看這個類的javadoc。經常使用的大概就是name、url、configuration這幾個。
name字段有兩種含義。若是是配合SpringCloud一塊兒使用,而且沒有配置url字段的狀況下,那麼name字段就是服務提供方在Eureka上註冊的服務名。Feign會根據name字段到Eureka上找到服務提供方的url。若是沒有與SpringCloud一塊兒使用,name字段會用作url、contextId等字段的備選:若是沒有配置後者,那麼就拿name字段值當作後者來使用。
url字段用來指定服務方的地址。這個地址能夠不帶協議前綴(Http://,feign默認是Http,若是要用Https須要增長配置),例如咱們配置了「ka.test.idc/」,實際調用時則是「Http://ka.test.idc/」。
configuration字段用來爲當前接口指定自定義配置。有些接口調用須要在feign通用配置以外增長一些自定義配置,例如調用百度api須要走代理、調用接口須要傳一些額外字段等。這些自定義配置就能夠經過configuration字段來指定。不過configuration字段只能指定三類自定義配置:Encoder、Decoder和Contract。Encoder和Decoder分別負責處理對象到HttpRequest和HttpResponse到對象的轉換;Contract則定義瞭如何解析這個接口和方法上的註解(SpringCloud就是經過Contract接口的一個子類SpringMvcContract來解析方法上的SpringMVC註解的)。
定義好了上面的接口後,咱們使用起來就很簡單了:
@Service("UserInfoBusiness")
public class UserInfoBusinessImpl implements UserInfoBusiness {
@Resource
private UserInfoService UserInfoService;
@Override
public UserInfoBean getUserInfo(String id) {
//feign鏈接
BaseResult<UserInfoVo> response = UserInfoService.queryUserInfoRequest(UserInfoService.Body4UserInfo.of(id));
// 其它略
}
能夠看到這裏的代碼,和咱們使用其它的bean的方式是同樣的。
使用Feign客戶端須要注意幾個事情。
Feign接口上定義RequestMapping地址與本系統中Controller定義的地址不能有衝突。例如:
@Controller
public class Con{
@PostMapping("/test")
public void test(){}
}
@FeignClient(name="testClient")
public interface Fei{
@PostMapping("/test")
public void test();
}
上面這種狀況下,Feign解析會報錯。
經過@FeignClient註解中configuration字段指定的自定義配置類,不能被SpringIoC掃描、裝載進來,不然可能會有問題。
通常的文檔都是這麼寫的,可是咱們系統在調用時的自定義配置是會被SpringIOC掃描裝載的,並無遇到什麼問題。
須要指定一個這樣的bean,不然在裝配Feign時會出現循環依賴的問題:
@Bean
public HttpMessageConverters HttpMessageConverters() {
return new HttpMessageConverters();
}
在SpringMVC中,@RequestParam註解若是不指定name字段,那麼會以變量名做爲queryString的參數名;可是在FeignClient中使用@RequestParam時,則必須指定name字段,不然會沒法解析參數。
@Controller
public class Con{
/**這裏的@RequestParam不用指定name,調用時會根據變量名自動解析爲 test=? */
@PostMapping("/test")
public void test(@RequestParam String test){}
}
@FeignClient(name="test")
public interface Fei{
/**這裏的@RequestParam必須指定name,不然調用時會報錯 */
@GetMapping("/test")
public String test(@RequestParam(name="test") String test);
}
提及來其實很簡單,和其它使用註解的框架同樣,Feign是經過動態代理來動態實現@FeignClient的接口的。
詳細一點來講,Feign經過FeignClientBuilder來動態構建被代理對象。在構建動態代理時,經過FeignClientFactoryBean和Feign.Builder來把@FeignClient接口、Feign相關的Configuration組裝在一塊兒。
public class FeignClientBuilder{
public static final class Builder<T> {
private FeignClientFactoryBean feignClientFactoryBean;
/**
* @param <T> the target type of the Feign client to be created
* @return the created Feign client
*/
public <T> T build() {
return this.feignClientFactoryBean.getTarget();
}
}
// 其它略
}
class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("Http")) {
this.url = "Http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("Http")) {
this.url = "Http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
// 在這個裏面生成一個代理
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
// 其它略
}
// 中間跳轉略
public class ReflectiveFeign extends Feign {
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
// 在這裏生成動態代理。
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
}
// 後續略