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">
    * &#064;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能提供的能力範圍和實現機制,而對應這些註解的源碼在後續文章中也將一一學習到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章