Feign
Feign WHAT WHY HOW maven依賴 自動裝配 編寫接口 調用接口 注意事項 原理
WHAT
Feign的GitHub描述如下:
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.
簡單的說,Feign是一套Http客戶端"綁定器"。個人理解,這個"綁定"有點像ORM。ORM是把數據庫字段和代碼中的實體"綁定"起來;Feign提供的基本功能就是方便、簡單地把Http的Request/Response和代碼中的實體"綁定"起來。
舉個例子,在我們系統調用時,我們是這樣寫的:
@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>了。
WHY
與其它的Http調用方式,例如URLConnection、HttpClient、RestTemplate相比,Feign有哪些優勢呢?
最核心的一點在於,Feign的抽象層次比其它幾個工具、框架都更高。
首先,一般來說抽象層次越高,其中包含的功能也就越多。
此外,抽象層次越高,使用起來就越簡便。例如,上面這個例子中,把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代碼、查工單等重複勞動中解脫出來。怎樣“以不變應萬變”呢?提高系統設計的抽象層次就是一個不錯的辦法。
HOW
Feign有好幾種用法:既可以在代碼中直接使用FeignBuilder來構建客戶端、也可以使用Feign自帶的註解、還可以使用SpringMVC的註解。這裏只介紹下使用SpringMVC註解的方式。
maven依賴
我們系統引入的依賴是這樣的:
<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不能與本系統中SpringMVC的配置衝突
Feign接口上定義RequestMapping地址與本系統中Controller定義的地址不能有衝突。例如:
@Controller
public class Con{
@PostMapping("/test")
public void test(){}
}
@FeignClient(name="testClient")
public interface Fei{
@PostMapping("/test")
public void test();
}
上面這種情況下,Feign解析會報錯。
自定義configuration不能被裝載到SpringContext中
通過@FeignClient註解中configuration字段指定的自定義配置類,不能被SpringIoC掃描、裝載進來,否則可能會有問題。
一般的文檔都是這麼寫的,但是我們系統在調用時的自定義配置是會被SpringIOC掃描裝載的,並沒有遇到什麼問題。
與SpringMVC配合使用時,需要單獨聲明HttpMessageConverters
需要指定一個這樣的bean,否則在裝配Feign時會出現循環依賴的問題:
@Bean
public HttpMessageConverters HttpMessageConverters() {
return new HttpMessageConverters();
}
使用@RequestParam註解時,必須指定name字段
在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;
}
}
// 後續略