开始
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 而配合使用.
前面提到大流程, 这篇文章 https://zhuanlan.zhihu.com/p/30123517 的思路基本描述了: 扫描 + 动态代理接口 + 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 能提供的能力范围和实现机制, 而对应这些注解的源码在后续文章中也将一一学习到.
来源: https://www.cnblogs.com/killbug/p/10389530.html