開始 Feign在Spring Cloud體系中被整合進來作為web service客戶端,使用HTTP請求遠程服務時能就像調用本地方法,可見在未來一段時間內,大多數Spring Cloud架構的微服務之間調用都會使用Feign來完成。 所以準備完整解讀一遍Feign的源碼,讀源碼,我個人覺得一方面 ...
開始
Feign在Spring Cloud體系中被整合進來作為web service客戶端,使用HTTP請求遠程服務時能就像調用本地方法,可見在未來一段時間內,大多數Spring Cloud架構的微服務之間調用都會使用Feign來完成。
所以準備完整解讀一遍Feign的源碼,讀源碼,我個人覺得一方面,可以在使用的基礎上對內部實現的細節的瞭解,提高使用時對組件功能的信心,另一方面,開源組件的代碼質量一般都比較高,對代碼結構組織一般比較優秀,還有,內部實現的一些細節可能優秀開發的思考所得,值得仔細揣摩。我對後兩個好處比較感興趣,雖然現如今寫的代碼好與壞,其實不會太多的影響平時的工作,不過如果內心是真的愛代碼,也會不斷追求細節的極致。
因為是Spring Cloud體系下使用Feign,必然會涉及到:服務註冊(Euraka),負載均衡(Rinbon),熔斷器(Hystrix)等方面的整合知識。
另外,能思考的高度和廣度必然有限,但是源碼閱讀學習又難以共同參與,所以剛好你也在這個位置,有自己的思路或想法,不吝留言。
內容
1,EnableFeignClients註解
大流程上,就是掃描FeignClient註解的介面,將介面方法動態代理成http客戶端的介面請求操作就完成了Feign的目的。所以一個FeignClient註解對應一個客戶端。
- EnableFeignClients這個註解可以配置掃描FeignClient註解的路徑。可以通過value屬性或basePackages屬性來制定掃描的包路徑。
- basePackageClasses屬性並不是精準掃描哪幾個Class,而是指定這些指定的class在的package會被掃描。所以註釋中推薦寫一個空介面來標記這個package要被掃描的方式來關聯。
- defaultConfiguration屬性是可以定義全局Feign配置的類,預設使用FeignClientsConfiguration類。想要自定義需要好好確認下FeignClientsConfiguration定義了那一些bean。當然如果只是想覆蓋部分bean,完全不用這個,直接在Configuration定義對應bean即可。
- clients屬性才是精準指定Class掃描,與package掃描互斥。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
/**
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
* declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of
* {@code @ComponentScan(basePackages="org.my.pkg")}.
* @return the array of 'basePackages'.
*/
String[] value() default {};
/**
* Base packages to scan for annotated components.
* <p>
* {@link #value()} is an alias for (and mutually exclusive with) this attribute.
* <p>
* Use {@link #basePackageClasses()} for a type-safe alternative to String-based
* package names.
*
* @return the array of 'basePackages'.
*/
String[] basePackages() default {};
/**
* Type-safe alternative to {@link #basePackages()} for specifying the packages to
* scan for annotated components. The package of each class specified will be scanned.
* <p>
* Consider creating a special no-op marker class or interface in each package that
* serves no purpose other than being referenced by this attribute.
*
* @return the array of 'basePackageClasses'.
*/
Class<?>[] basePackageClasses() default {};
/**
* A custom <code>@Configuration</code> for all feign clients. Can contain override
* <code>@Bean</code> definition for the pieces that make up the client, for instance
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
*
* @see FeignClientsConfiguration for the defaults
*/
Class<?>[] defaultConfiguration() default {};
/**
* List of classes annotated with @FeignClient. If not empty, disables classpath scanning.
* @return
*/
Class<?>[] clients() default {};
}
從EnableFeignClients註解的屬性看,我們可以瞭解到,在解析這個註解屬性的時候,需要利用配置的掃描的package或Class,掃描FeignClient註解,進而解析那些FeignClient註解的配置屬性。並且我們還可以配置全局的Feign相關的配置。
回頭我們再看一下EnableFeignClients定義的元數據,@Import註解的使用值得學習一下。
關於這個註解,我們可以理解成導入
@Import註解導入的類 FeignClientsRegistrar 是繼承 ImportBeanDefinitionRegistrar 的,ImportBeanDefinitionRegistrar的方法一般實現動態註冊bean使用,在由@Import註解導入後,Spring容器啟動時會執行registerBeanDefinitions方法。
所以一般@Import註解和ImportBeanDefinitionRegistrar實現動態註冊bean而配合使用。
前面提到大流程,這篇文章的思路基本描述了:掃描+動態代理介面+http請求,其中也對@Import和ImportBeanDefinitionRegistrar使用場景進行瞭解釋,可以做參考學習。
2,FeignClient註解
每個FeignClient代表一個http客戶端,定義的每一個方法對應這個一個介面。
- value和name用於定義http客戶端服務的名稱,在spring cloud為服務之間調用服務總要有負載均衡的,比如Rinbon。所以這裡定義的會是服務提供方的應用名(serviceId)。
- qualifier屬性在spring容器中定義FeignClient的bean時,配置名稱,在裝配bean的時候可以用這個名稱裝配。使用spring的註解:Qualifier。
- url屬性用來定義請求的絕對URL。
- decode404屬性,在客戶端返回404時是進行decode操作還是拋出異常的標記。
- configuration屬性,自定義配置類,可以定義Decoder, Encoder,Contract來覆蓋預設的配置,可以參考預設的配置類:FeignAutoConfiguration
- fallback屬性 使用fallback機制時可以配置的類屬性,繼承客戶端介面,實現fallback邏輯。如果要使用fallback機制需要配合Hystrix一起,所以需要開啟Hystrix。
- fallbackFactory屬性 生產fallback實例,生產的自然是繼承客戶端介面的實例。
- path屬性 每個介面url的統一首碼
- primary屬性 標記在spring容器中為primary bean
/**
* Annotation for interfaces declaring that a REST client with that interface should be
* created (e.g. for autowiring into another component). If ribbon is available it will be
* used to load balance the backend requests, and the load balancer can be configured
* using a <code>@RibbonClient</code> with the same name (i.e. value) as the feign client.
*
* @author Spencer Gibb
* @author Venil Noronha
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
/**
* The name of the service with optional protocol prefix. Synonym for {@link #name()
* name}. A name must be specified for all clients, whether or not a url is provided.
* Can be specified as property key, eg: ${propertyKey}.
*/
@AliasFor("name")
String value() default "";
/**
* The service id with optional protocol prefix. Synonym for {@link #value() value}.
*
* @deprecated use {@link #name() name} instead
*/
@Deprecated
String serviceId() default "";
/**
* The service id with optional protocol prefix. Synonym for {@link #value() value}.
*/
@AliasFor("value")
String name() default "";
/**
* Sets the <code>@Qualifier</code> value for the feign client.
*/
String qualifier() default "";
/**
* An absolute URL or resolvable hostname (the protocol is optional).
*/
String url() default "";
/**
* Whether 404s should be decoded instead of throwing FeignExceptions
*/
boolean decode404() default false;
/**
* A custom <code>@Configuration</code> for the feign client. Can contain override
* <code>@Bean</code> definition for the pieces that make up the client, for instance
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
*
* @see FeignClientsConfiguration for the defaults
*/
Class<?>[] configuration() default {};
/**
* Fallback class for the specified Feign client interface. The fallback class must
* implement the interface annotated by this annotation and be a valid spring bean.
*/
Class<?> fallback() default void.class;
/**
* Define a fallback factory for the specified Feign client interface. The fallback
* factory must produce instances of fallback classes that implement the interface
* annotated by {@link FeignClient}. The fallback factory must be a valid spring
* bean.
*
* @see feign.hystrix.FallbackFactory for details.
*/
Class<?> fallbackFactory() default void.class;
/**
* Path prefix to be used by all method-level mappings. Can be used with or without
* <code>@RibbonClient</code>.
*/
String path() default "";
/**
* Whether to mark the feign proxy as a primary bean. Defaults to true.
*/
boolean primary() default true;
}
通過FeignClient註解的屬性,可以看到針對單個Feign客戶端可以做自定義的配置。
3,定義客戶端介面的註解
在Feign中需要定義http介面的辦法,註解是個好解決方案。這裡就看到Contract的介面,解析這些註解用的,下麵是抽象類BaseContract,它有預設實現,即Contract.Default,解析了自定義註解:feign.Headers,feign.RequestLine,feign.Body,feign.Param,feign.QueryMap,feign.HeaderMap,這些註解都是用來定義描述http客戶端提供的介面信息的。
但是因為這裡預設將Feign和Spring Cloud體系中使用,而提供了SpringMvcContract類來解析使用的註解,而這個註解就是RequestMapping。這個註解使用過spring mvc的同學必然非常熟悉,這裡就是利用了這個註解的定義進行解析,只是功能上並不是和spring保持完全一致,畢竟它這裡只需要考慮將介面信息定義出來即可。
在SpringMvcContract的代碼里,可以看到解析RequestMapping註解屬性的邏輯代碼,如此在使用中可以直接使用RequestMapping來定義介面。
- value屬性和path屬性定義介面路徑
- method屬性配置HTTP請求方法
- params屬性在feign中不支持
- headers屬性配置http頭信息
- consumes屬性配置http頭信息,只解析使用配置了 Content-Type 屬性的值
- produces屬性配置http頭信息,只解析使用配置了 Accept 屬性的值
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
/**
* Assign a name to this mapping.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used on both levels, a combined name is derived by concatenation
* with "#" as separator.
* @see org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder
* @see org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy
*/
String name() default "";
/**
* The primary mapping expressed by this annotation.
* <p>In a Servlet environment this is an alias for {@link #path}.
* For example {@code @RequestMapping("/foo")} is equivalent to
* {@code @RequestMapping(path="/foo")}.
* <p>In a Portlet environment this is the mapped portlet modes
* (i.e. "EDIT", "VIEW", "HELP" or any custom modes).
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
*/
@AliasFor("path")
String[] value() default {};
/**
* In a Servlet environment only: the path mapping URIs (e.g. "/myPath.do").
* Ant-style path patterns are also supported (e.g. "/myPath/*.do").
* At the method level, relative paths (e.g. "edit.do") are supported within
* the primary mapping expressed at the type level. Path mapping URIs may
* contain placeholders (e.g. "/${connect}")
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
* @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
* @since 4.2
*/
@AliasFor("value")
String[] path() default {};
/**
* The HTTP request methods to map to, narrowing the primary mapping:
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this HTTP method restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* <p>Supported for Servlet environments as well as Portlet 2.0 environments.
*/
RequestMethod[] method() default {};
/**
* The parameters of the mapped request, narrowing the primary mapping.
* <p>Same format for any environment: a sequence of "myParam=myValue" style
* expressions, with a request only mapped if each such parameter is found
* to have the given value. Expressions can be negated by using the "!=" operator,
* as in "myParam!=myValue". "myParam" style expressions are also supported,
* with such parameters having to be present in the request (allowed to have
* any value). Finally, "!myParam" style expressions indicate that the
* specified parameter is <i>not</i> supposed to be present in the request.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this parameter restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* <p>In a Servlet environment, parameter mappings are considered as restrictions
* that are enforced at the type level. The primary path mapping (i.e. the
* specified URI value) still has to uniquely identify the target handler, with
* parameter mappings simply expressing preconditions for invoking the handler.
* <p>In a Portlet environment, parameters are taken into account as mapping
* differentiators, i.e. the primary portlet mode mapping plus the parameter
* conditions uniquely identify the target handler. Different handlers may be
* mapped onto the same portlet mode, as long as their parameter mappings differ.
*/
String[] params() default {};
/**
* The headers of the mapped request, narrowing the primary mapping.
* <p>Same format for any environment: a sequence of "My-Header=myValue" style
* expressions, with a request only mapped if each such header is found
* to have the given value. Expressions can be negated by using the "!=" operator,
* as in "My-Header!=myValue". "My-Header" style expressions are also supported,
* with such headers having to be present in the request (allowed to have
* any value). Finally, "!My-Header" style expressions indicate that the
* specified header is <i>not</i> supposed to be present in the request.
* <p>Also supports media type wildcards (*), for headers such as Accept
* and Content-Type. For instance,
* <pre class="code">
* @RequestMapping(value = "/something", headers = "content-type=text/*")
* </pre>
* will match requests with a Content-Type of "text/html", "text/plain", etc.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this header restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* <p>Maps against HttpServletRequest headers in a Servlet environment,
* and against PortletRequest properties in a Portlet 2.0 environment.
* @see org.springframework.http.MediaType
*/
String[] headers() default {};
/**
* The consumable media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Content-Type} matches one of these media types.
* Examples:
* <pre class="code">
* consumes = "text/plain"
* consumes = {"text/plain", "application/*"}
* </pre>
* Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Content-Type} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this consumes restriction.
* @see org.springframework.http.MediaType
* @see javax.servlet.http.HttpServletRequest#getContentType()
*/
String[] consumes() default {};
/**
* The producible media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Accept} matches one of these media types.
* Examples:
* <pre class="code">
* produces = "text/plain"
* produces = {"text/plain", "application/*"}
* produces = "application/json; charset=UTF-8"
* </pre>
* <p>It affects the actual content type written, for example to produce a JSON response
* with UTF-8 encoding, {@code "application/json; charset=UTF-8"} should be used.
* <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Accept} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this produces restriction.
* @see org.springframework.http.MediaType
*/
String[] produces() default {};
}
和註解RequestMapping組合使用在傳參的註解目前包含:PathVariable,RequestHeader,RequestParam。
PathVariable:url占位符參數綁定
RequestHeader:可以設置業務header
RequestParam:將傳參映射到http請求的參數,get/post請求都支持
關於RequestParam,前面有文章涉及到細節:鏈接
結束
先看一眼將涉及到的註解,通過這些註解,我們可以大致瞭解到Feign能提供的能力範圍和實現機制,而對應這些註解的源碼在後續文章中也將一一學習到。