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能提供的能力范围和实现机制,而对应这些注解的源码在后续文章中也将一一学习到。