Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。 使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,并和Eureka结合
名称 | 解释 |
---|---|
@RequestLine | 方法上 定义HttpMethod 和 UriTemplate. UriTemplate 中使用{} 包裹的表达式,可以通过在方法参数上使用 |
@Param | 自动注入 @Param 方法参数 定义模板变量,模板变量的值可以使用名称的方式使用模板注入解析 |
@Headers | 类上或者方法上 定义头部模板变量,使用@Param 注解提供参数值的注入。如果该注解添加在接口类上,则所有的请求都会携带对应的Header信息;如果在方法上,则只会添加到对应的方法请求上 |
@QueryMap | 方法上 定义一个键值对或者 pojo,参数值将会被转换成URL上的 query 字符串上 |
@HeaderMap | 方法上 定义一个HeaderMap, 与 UrlTemplate 和HeaderTemplate 类型,可以使用@Param 注解提供参数值 |
@FeignClient | 注解指定调用的微服务名称,封装了调用 USER-API 的过程,作为消费方调用模板。 |
@EnableFeignClients | 扫描声明它们是假装客户端的接口(@FeignClient )。配置组件扫描指令以供使用(@Configuration )类。 |
@SpringQueryMap | Spring MVC相当于OpenFeign的{@QueryMap} 。 |
@FeignClient详解
名称 | 解释 |
---|---|
name、value和serviceId | 从源码可以得知,name是value的别名,value也是name的别名。两者的作用是一致的,name指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。其中,serviceId和value的作用一样,用于指定服务ID,已经废弃。 |
qualifier | 该属性用来指定@Qualifier注解的值,该值是该FeignClient的限定词,可以使用改值进行引用。 |
url | url属性一般用于调试程序,允许我们手动指定@FeignClient调用的地址。 |
decode404 | 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException |
configuration | Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract。 |
fallback | 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口 |
fallbackFactory | 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。 |
path | path属性定义当前FeignClient的统一前缀。 |
primary | 是否将伪代理标记为主Bean,默认为true。 |
@EnableFeignClients
名称 | 解释 |
---|---|
value | 为basePackages属性的别名,允许使用更简洁的书写方式。 |
basePackage | 设置自动扫描带有@FeignClient注解的基础包路径。 |
basePackageClasses | 该属性是basePackages属性的安全替代属性。该属性将扫描指定的每个类所在的包下面的所有被@FeignClient修饰的类;这需要考虑在每个包中创建一个特殊的标记类或接口,该类或接口除了被该属性引用外,没有其他用途。 |
defaultConfiguration | 该属性用来自定义所有Feign客户端的配置,使用@Configuration进行配置。 |
clients | 设置由@FeignClient注解修饰的类列表。如果clients不是空数组,则不通过类路径自动扫描功能来加载FeignClient。 |
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); @RequestLine("POST /repos/{owner}/{repo}/issues") void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo); } public class MyApp { public static void main(String... args) { GitHub github = Feign.builder().decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); } } 复制代码
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> 复制代码
@SpringBootApplication @EnableFeignClients public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @FeignClient("stores") public interface StoreClient { @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json") Store update(@PathVariable("storeId") Long storeId, Store store); } 复制代码
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency> 复制代码
# 配置 feign.httpclient.enabled = true 复制代码
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency> 复制代码
# 配置 feign.okhttp.enabled = true 复制代码
名称 | 解释 |
---|---|
Logger.Level loggerLevel | 日志级别,默认Logger.Level.NONE |
Integer connectTimeout | 连接超时时间 java.net.HttpURLConnection#getConnectTimeout(),如果使用Hystrix,该配置无效连接超时时间 |
Integer readTimeout | 读取超时时间 java.net.HttpURLConnection#getReadTimeout(),如果使用Hystrix,该配置无效 |
Class retryer | 重试接口实现类,默认实现 Retryer.NEVER_RETRY |
Class errorDecoder | 错误编码,默认ErrorDecoder.Default() |
List<Class>requestInterceptors | 请求拦截器 |
Boolean decode404 | 是否开启404编码 |
Class decoder | 解码器, 将一个http响应转换成一个对象,Spring Cloud Feign 使用 ResponseEntityDecoder |
Class encoder | 编码器,将一个对象转换成http请求体中, Spring Cloud Feign 使用 SpringEncoder |
Class contract | 处理Feign接口注解,Spring Cloud Feign 使用SpringMvcContract 实现,处理Spring mvc 注解,也就是我们为什么可以用Springmvc 注解的原因。 |
配置示例
feign: client: default-config: my-config config: my-config: error-decoder: com.example.feign.MyErrorDecoder logger-level: full 复制代码
配置项源码解析
// 配置类-FeignClientFactoryBean#configureFeign protected void configureFeign(FeignContext context, Feign.Builder builder) { //配置文件,以feign.client开头 FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class); if (properties != null) { if (properties.isDefaultToProperties()) { //使用java config 配置 configureUsingConfiguration(context, builder); configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder); configureUsingProperties(properties.getConfig().get(this.name), builder); } else { configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder); configureUsingProperties(properties.getConfig().get(this.name), builder); configureUsingConfiguration(context, builder); } } else { configureUsingConfiguration(context, builder); // Feign默认配置 } } 复制代码
<span>复制代码</span>
第一种:配置文件无配置
使用 java config 配置,优先级有低到高进行单个配置覆盖 1、FeignClientsConfiguration Spring Cloud Feign 全局默认配置。 2、@EnableFeignClients#defaultConfiguration 自定义全局默认配置。 3、FeignClient#configuration 单个Feign接口局部配置。
第二种:feign.client.default-to-properties=true(默认true)
java config 和application.properties(.yml)配置,优先级有低到高进行单个配置覆盖 1、FeignClientsConfiguration Spring Cloud Feign 全局默认配置。 2、@EnableFeignClients#defaultConfiguration 自定义全局默认配置。 3、FeignClient#configuration 单个Feign接口局部配置。 4、application.properties(.yml)配置文件全局默认配置,配置属性feign.client.default-config指定默认值(defult),如何使用,在application.properties(.yml)配置文件应用小节讲解 5、application.properties(.yml)配置文件局部配置,指定@FeignClient#name局部配置。
第三种:feign.client.default-to-properties=false(默认true)
java config 和application.properties(.yml)配置,优先级有低到高进行单个配置覆盖 1、application.properties(.yml)配置文件全局默认配置,配置属性feign.client.default-config指定默认值(defult),如何使用,在application.properties(.yml)配置文件应用小节讲解 2、application.properties(.yml)配置文件局部配置,指定@FeignClient#name局部配置。 3、FeignClientsConfiguration Spring Cloud Feign 全局默认配置。 4、@EnableFeignClients#defaultConfiguration 自定义全局默认配置。 5、FeignClient#configuration 单个Feign接口局部配置。
复制代码
<span>复制代码</span>
主要流程步骤:
一、注册FeignClient配置类和FeignClient BeanDefinition
二、实例化Feign上下文对象FeignContext
三、创建 Feign.builder 对象
四、生成负载均衡代理类
五、生成默认代理类
六、注入到spring容器
Feign调用流程图
一、从启动类注开始,来看下@EnableFeignClients注解:@Import(FeignClientsRegistrar.class) 二、我们分解别来看一下上面registerBeanDefinitions中的两个方法: 1) 注册默认配置方法:registerDefaultConfiguration: 上述方法为读取启动类上面@EnableFeignClients注解中声明feign相关配置类,默认name为default,一般情况下无需配置。用默认的FeignAutoConfiguration 即可。该方法中有比较重要的方法:注册配置registerClientConfiguration,启动流程一共有两处读取feign的配置类,第一处将bean配置类包装成FeignClientSpecification ,注入到容器。该对象非常重要,包含FeignClient需要的重试策略,超时策略,日志等配置,如果某个服务没有设置,则读取默认的配置。 2)扫描FeignClient: registerFeignClients方法主要是扫描类路径,对所有的FeignClient生成对应的BeanDefinition,这里第二处调了用registerClientConfiguration 注册配置的方法,这里主要是将扫描的目录下,每个项目的配置类加载的容器当中。SpringCloud FeignClient其实是利用了spring的代理工厂来生成代理类,所以这里将所有的feignClient的描述信息BeanDefinition设定为FeignClientFactoryBean类型,该类又继承FactoryBean,很明显,这是一个代理类。在spring中,FactoryBean是一个工厂bean,用作创建代理bean,所以得出结论,feign将所有的feignClient bean包装成FeignClientFactoryBean。扫描方法到此结束。 三、代理类什么时候会触发生成呢? 在spring刷新容器时,当实例化我们的业务service时,如果发现注册了FeignClient,spring就会去实例化该FeignClient,同时会进行判断是否是代理bean,如果为代理bean,则调用FeignClientFactoryBean的getObject()方法生成代理bean。 复制代码
主要流程步骤: 一、实例化FeignContex,实例化Feign上下文对象FeignContext 二、可以看到feign的配置类设置到feign的容器当中,而集合中的元素 正是上面我们提到的两处调用registerClientConfiguration方法添加进去的,前后呼应。 然而,当我们引入了sleuth之后,获取的feignContext确是TraceFeignClientAutoConfiguration中配置的实例sleuthFeignContext:可以看到上面创建了一个TraceFeignContext实例,因为该对象继承FeignContext,同时通过FeignContextBeanPostProcessor后处理器的postProcessAfterInitialization方法返回TraceFeignContext ,所以在上面第2步中通过类型获取: applicationContext.getBean(FeignContext.class),最终拿到的是TraceFeignContext。 复制代码
1、Feign.Builder builder = get(context, Feign.Builder.class) ,又会有以下三种情况
1)单独使用Feign,通过加载FeignClientsConfiguration的配置创建Feign .Builder 2)引入了hystrix,没有引入sleuth,通过加载FeignClientsConfiguration的配置创建HystrixFeign .Builder 3)同时引入hystrix 和 sleuth,加载TraceFeignClientAutoConfiguration的配置创建HystrixFeign.Builder 复制代码
2、设置重试策略,log等组件
1)Feign.builder在获取之后又分别指定了重试策略,日志级别,错误代码code等 2)将FeignContext做了缓存,每个服务对应一个FeignContext,服务名作为key。 复制代码
3、加载配置的顺序为:先加载每个服务的配置类,然后加载启动类注解上的配置类,最后加载默认的配置类。这样做有什么好处?
spring刷新容器的方法也是对所有的bean进行了缓存,如果已经创建,则不再实例化。 所以优先选取每个FeignClient的配置类,最后默认的配置类兜底。 复制代码
有个重要判断:判断FeignClient声明的url是否为空,来判断具体要生成的代理类
1)如果为空,则默认走Ribbon代理,也就是这个入口,会有加载ribbon的处理。 @FeignClient("MyFeignClient") 2)如果不为空,指定url,则走默认生成代理类的方式,也就是所谓的硬编码。 @FeignClient(value = "MyFeignClient",url = "http://localhost:8081") 复制代码
这样处理方便开发人员进行测试,无需关注注册中心,直接http调用,是个不错的开发小技巧 接下来Client client = getOptional(context, Client.class);这里会从FeignContext上下文中获取Client对象 这里又会有三种情况:
1)没有整合ribbon、sleuth:获取默认的Client:Default实例。 2)整合了ribbon,没有整合sleuth: 获取LoadBalanceFeignClient实例。 3)整合了ribbon 和 sleuth: 会获取TraceFeignClient实例 复制代码
feign初始化结构为动态代理的整个过程
1、初始化Feign.Builder传入参数,构造ReflectiveFeign 2、ReflectiveFeign通过内部类ParseHandlersByName的Contract属性,解析接口生成MethodMetadata 3、ParseHandlersByName根据MethodMetadata生成RequestTemplate工厂 4、ParseHandlersByName创建SynchronousMethodHandler,传入MethodMetadata、RequestTemplate工厂和Feign.Builder相关参数 5、ReflectiveFeign创建FeignInvocationHandler,传入参数SynchronousMethodHandler,绑定DefaultMethodHandler 6、ReflectiveFeign根据FeignInvocationHandler创建Proxy 复制代码
接口调用过程:
1、接口的动态代理Proxy调用接口方法会执行的FeignInvocationHandler 2、FeignInvocationHandler通过方法签名在属性Map<Method, MethodHandler> dispatch中找到SynchronousMethodHandler,调用invoke方法 3、SynchronousMethodHandler的invoke方法根据传入的方法参数,通过自身属性工厂对象RequestTemplate.Factory创建RequestTemplate,工厂里面会用根据需要进行Encode 4、SynchronousMethodHandler遍历自身属性RequestInterceptor列表,对RequestTemplate进行改造 4、SynchronousMethodHandler调用自身Target属性的apply方法,将RequestTemplate转换为Request对象 5、SynchronousMethodHandler调用自身Client的execute方法,传入Request对象 6、Client将Request转换为http请求,发送后将http响应转换为Response对象 7、SynchronousMethodHandler调用Decoder的方法对Response对象解码后返回 8、返回的对象最后返回到Proxy 复制代码
理解了第四步的逻辑,生成默认代理类就很容易理解了,唯一不同点就是client的实现类为loadBalanceClient。 注意:不管是哪种代理类,最终发起请求还是由Feign.Default中的execute方法完成,默认使用HttpUrlConnection实现。
总结:通过spring refresh()方法,触发FeignClientFactoryBean.getObject()方法获得了代理类,然后完成注入spring容器的过程。该实现方式同Dubbo的实现方式类似,有兴趣的可以自行研究噢。
名称 | 解释 |
---|---|
Contract | Contract的作用是解析接口方法,生成Rest定义。feign默认使用自己的定义的注解,还提供了JAXRSContract javax.ws.rs注解接口实现SpringContract是spring cloud提供SpringMVC注解实现方式 |
Client | Client.Default 使用java api的HttpClientConnection发送http请求; ApacheHttpClient 使用apache的Http客户端发送请求; OkHttpClient 使用OKHttp客户端发送请求;RibbonClient 使用Ribbon进行客户端路由 |
Decoder | json解码器 GsonDecoder、JacksonDecoder;XML解码器 JAXBDecoder;Stream流解码器 StreamDecoder |
Encoder | 默认编码器,只能处理String和byte[];json编码器GsonEncoder、JacksonEncoder;XML编码器JAXBEncoder |
Target | HardCodedTarget 默认Target,不做任何处理。LoadBalancingTarget 使用Ribbon进行客户端路由 |
Retryer | 默认的策略是Retryer.Default,包含3个参数:间隔、最大间隔和重试次数,第一次失败重试前会sleep输入的间隔时间的,后面每次重试sleep时间是前一次的1.5倍,超过最大时间或者最大重试次数就失败 |
RequestInterceptor | 调用客户端发请求前,修改RequestTemplate,比如为所有请求添加Header就可以用拦截器实现 |
InvocationHandler | 通过InvocationHandlerFactory注入到Feign.Builder中,feign提供了Hystrix的扩展,实现Hystrix接入 |
1.扩展性好,能有很多自定义的扩展点,如rxjava,hystrix等 2.轻量代码量少易学习 3.springclound默认使用的组件 4.api使用简单,简化了http调用api 复制代码
1.组件很多默认的实现有一些坑,需要注意避免 复制代码
本文使用 mdnice 排版