申明式接口调用Feign,极大的简化了我们接口之间的调用。只需要通过注解就可以实现我们系统之间接口的调用。
关于分布式我们之前主要集中讨论了服务治理。eureka、consul、zookeeper我们分别从三个角度不同程度的学习了这三个框架的原理及区别。这些作为前期springcloud的重要组成部分是我们学习分布式不容忽视的章节。至于现在springcloud alibaba我们这里重头菜要留到最后。对springcloud alibaba感兴趣还请关注我后续会更新相关内容
简介
[openfeign源码] github.com/spring-cloud/spring-cloud-openfeign
[springcloud官网] spring.io/projects/spring-cloud-openfeign/
- Feign是一个申明式web接口调用的客户端,他基于注解式开发极大简化我们开发成本。
使用
- 他的到来是真的简化我们,在springcloud中与他整合也是非常的方便
pom引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 我们只需要引入openfeign,但是它依赖于服务注册中间件。我们这里选择springcloud初期推出服务治理也是我们第一课讨论的中间件-eureka。所以这里除了openfeign意外我们还引入了eureka。关于eureka的说明不了解可以到我的首页中查找。
启动类注入
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
-
这是个标准的springboot启动程序,
-
①、
@SpringBootApplication
: springboot程序标识启动注解 -
②、
@EnableEurekaClient
: 前面我们也介绍了,开启eureka的相关配置 -
@EnableFeignClients
: 开启OpenFeign的相关配置
新建interface
-
在我们之前的案例中,我们有payment、order两个模块。OpenFeign使用在客户端上。所以这里我们在eureka章节的项目继续使用。
-
先启动eureka服务和payment服务,为了后面测试负载均衡我们这里也启动两个payment服务。
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface OrderPaymentService {
@RequestMapping(value = "/payment/create" , method = RequestMethod.POST)
public ResultInfo createByOrder(Payment payment);
}
-
FeignClient中的内容是payment注册到eureka的服务名。这里需要注意下。
-
接口里我们只需要写对应方法。方法名不需要保持一致。只需要在@RequestMapping注解中请求接口和请求方式需要和payment中保持一致。
-
接口中的方法名不需要处理但是入参类型和payment需要保持一致。
调用
-
剩下就是我们在使用的地方,通过@Autowired等注解注入
OrderPaymentService
。然后就是普通的java方法调用。为了演示出负载均衡的效果。我们在payment方法中携带出端口信息。 -
效果读者可以自行测试,可以发现order服务的保存订单会负载均衡调用两个payment服务。和之前我们ribbon结合restTemplate调用效果是一样的。
-
OpenFeign依赖eureka服务发现。借助ribbon实现负载均衡,借助resttemplate进行接口调用。说到底还是我们常规的操作。
超时控制
-
为了保证调用方系统可用性,我们肯定不能让OpenFeign一直在等待提供方返回数据,向我们基于eureka实现的服务治理如果eureka给我们提供的地址因为网络问题卡顿,那么我么一直等待的话会造成使用效果降低。所以我们需要有一个超时控制。在常规的前后端开发调用接口也是有超时控制的。
-
我们在payment中新增一个timeout接口并在接口内部进行休眠5s.
- 然后在order端进行feign接口开发
-
然后我们调用order端的接口就会发现出现报错。并且报错信息就是超时错误。在feign中默认超时时间是1S 。
-
我们只需要在配置文件中配置ribbon的超时时间就可以了。
-
只加
ribbon.ReadTimeout
属性发现超时就可以生效。但是需要注意这里的超时时间尽量设置比接口真实超时时间大一点。因为中间还有网络延迟时间。如下图所示ribbon.ReadTimeout=6000,那么在接口中我们休眠时间建议在4S以下。
- 因为openfeign在构建的时候是基于Hystrix构建的。内部是有降级思想的。如果我们想开启hystrix我们可以通过
feign.hystrix.enabled=true
来开启hystrix。
-
在ribbon中内置了hystrix的。hystrix是用来做服务熔断降级操作的。hystrix默认超时时间1S。ribbon的默认连接超时1S、默认操作请求1S。在第一次请求到服务端的时候Ribbon是需要进行连接验证的。所以在设置中
-
$$
hystrix.timeout>2\times(ribbon.connectTimeout+ribbon.ReadTimeout)
$$
- 如果开启了hystrix那我们就需要注意超时的控制了。hystrix的超时会被ribbon影响到。上面的公式建议hystrix的超时设置大于ribbon的两个超时。hystrix设置太大也没有意义因为会被ribbon首先限制。
feign雪崩处理熔断降级
- 上面feign超时我们已经提及了feign内部是内置的hystrix的。而hystrix作用是用来服务熔断降级限流的。那么我么feign自然也就具备了响应的功能。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
@AliasFor("name")
String value() default "";
@Deprecated
String serviceId() default "";
String contextId() default "";
@AliasFor("value")
String name() default "";
String qualifier() default "";
String url() default "";
boolean decode404() default false;
Class<?>[] configuration() default {};
Class<?> fallback() default void.class;
Class<?> fallbackFactory() default void.class;
String path() default "";
boolean primary() default true;
}
-
我们可以看出除了value配置服务提供者在eureka中注册的服务名外,还有两个参数使我们本次需要的fallback()、fallbackFactory()
-
这两个就是配置我们的服务熔断降级处理的方案。我们已实现fallback为例展示下代码的配置
@Component
public class PaymentServiceFallbackImpl implements OrderPaymentService {
@Override
public ResultInfo createByOrder(Payment payment) {
ResultInfo resultInfo = new ResultInfo();
resultInfo.setMsg("我被熔断了createByOrder");
return resultInfo;
}
@Override
public ResultInfo getTimeOut(Long id) {
ResultInfo resultInfo = new ResultInfo();
resultInfo.setMsg("我被熔断了getTimeOut");
return resultInfo;
}
}
@FeignClient(value = "CLOUD-PAYMENT-SERVICE" ,fallback = PaymentServiceFallbackImpl.class)
public interface OrderPaymentService {
@RequestMapping(value = "/payment/create" , method = RequestMethod.POST)
public ResultInfo createByOrder(Payment payment);
@RequestMapping(value = "/payment/getTimeOut/{id}" , method = RequestMethod.GET)
public ResultInfo getTimeOut(@PathVariable("id") Long id);
}
- 我们是依据上面代码开始展开的。所以其他的配置这里就不展开了需要在配置文件中设置feign.hystrix.enable=true因为feign默认关闭hystrix的。只需要上面两处修改。同时我将ribbon的超时时间改小点 。模拟出服务超时的现象。之前我们是直接报错。Ribbon负载的错误因为超时。现在我们再看看超时会出现什么现象吧。
- 除了fallback还要一个fallbackfactory。那么他们两个有什么作用呢。两个都是事先熔断之后的逻辑。但是fallback没有记录日志的功能。而fallbackfactory中我们可以记录。具体的可以操作源码查看PaymentServiceFallbackFactoryImpl 。 通过Throwable对象可以获取到错误日志。
@Component
@Slf4j
public class PaymentServiceFallbackFactoryImpl implements FallbackFactory<OrderPaymentService> {
@Override
public OrderPaymentService create(Throwable cause) {
return new OrderPaymentService() {
@Override
public ResultInfo createByOrder(Payment payment) {
ResultInfo resultInfo = new ResultInfo();
resultInfo.setMsg("我被工厂熔断方式。。。createByOrder");
return resultInfo;
}
@Override
public ResultInfo getTimeOut(Long id) {
ResultInfo resultInfo = new ResultInfo();
resultInfo.setMsg("我被工厂熔断方式。。。getTimeOut");
return resultInfo;
}
};
}
}
日志打印
- 既然OpenFeign是帮助我们调用接口,那么我们肯定需要了解接口调用的入参、出参等信息。欢句换说我们需要知道OpenFeign调用Http的细节。
|级别|作用|
|—|---|
|NONE|默认,没有日志|
|BASIC|请求方法、URL、响应状态|
|HEADERS|BASIC、请求、响应信息|
|FULL|完整数据|
- 配置也很简单,我们只需要注册一个bean并开启日志就可以了
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
- 然后在配置文件中配置我们需要拦截的路径就可以了。
logging:
level:
com.zxhtom.cloud.order: debug
原理篇
openfeign原理前提知识准备(功底深厚直接跳过)
AnnotationMetadata是什么
- 字面意思是注解的元数据。其实就是对注解的一种封装对象。通过他我们可以获取到注解里的属性数据。
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
- 上述就是获取
EnableFeignClients
注解的属性内容。
Class#getEnclosingClass
- 该方法是用来获取该class对象的封闭类的。什么叫封闭类呢。
@Data
public class Parent {
private String group;
@Data
class Child {
private String name;
}
}
-
上面是我们常用的内部类。不知道细心的你有没有发现对于内部类的创建不能像普通类一样在别处创建。此处的Parent类就是Child类的封闭类。
-
而内部类除了在自己的封闭类中可以直接new意外,在其他地方都是不可以直接new的。
public static void main(String[] args) {
Parent parent = new Parent();
parent.setGroup("zxhgroup");
//下面new Child首先编译都不会成功的。
Parent.Child child = new Parent.Child();
//下面通过自己的封闭类进行new则是可以的
Parent.Child realChild = parent.new Child();
}
- 现在我们在回到Class#getEnclosingClass这个话题上。他将返回当前类的封闭类。即如果是Child的class对象调用的则返回的是Parent的Class对象。如果没有封闭类的话则返回null
- 如上图所示,我们最终打印的是Parent的Class对象信息。
spring注册bean
- 相信大家都知道spring注册bean是通过
BeanDefinition
作为载体的。而真正将BeanDefinition
解析成springbean的是BeanDefinitionRegistry
。 这里直接看看下面我手动注册bean的代码吧。
//首先获取容器上下文
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
//生成java类对应的BeanDefinitionBuilder
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(Student.class);
//将BeanDefinition注册到该spring容器上
context.registerBeanDefinition("student",builder.getBeanDefinition());
//尝试获取
Object orderController = context.getBean("student");
System.out.println(orderController);
ClassPathScanningCandidateComponentProvider
ClassPathScanningCandidateComponentProvider
我们平时开发很少会接触到的,但是这个东西在spring源码中确实不可忽略的一个角色。他主要是用来获取spring容器下指定Class的BeanDefinition
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(Student.class);
context.registerBeanDefinition("student",builder.getBeanDefinition());
Object orderController = context.getBean("student");
System.out.println(orderController);
ClassPathScanningCandidateComponentProvider classPathScanningCandidateComponentProvider = new ClassPathScanningCandidateComponentProvider(false);
classPathScanningCandidateComponentProvider.addIncludeFilter(new AnnotationTypeFilter(ComponentScan.class));
Set<BeanDefinition> candidateComponents = classPathScanningCandidateComponentProvider.findCandidateComponents("com.zxhtom.cloud.order.spring");
for (BeanDefinition candidateComponent : candidateComponents) {
System.out.println(candidateComponent.getBeanClassName());
}
- 上面我们就可以获取到
com.zxhtom.cloud.order.spring
包下带有@ComponentScan
注解的BeanDefinition
; 实际上就是获取到了Config
对应的BeanDefinition
。
FeignClientFactoryBean
-
细心的开发者应该知道spring容器管理的bean是通过
BeanDefinition
创建的。bean和java的对象是一脉相承的。java 对象是Class表示的。但是不知道你有没有发现FeignClient开发的实际上是个interface 。 但是我们在使用的时候却是正常的通过@Autowired
注入的。这个就违反了spring设计理念。于此类似的还有Mybatis中的Mapper开发。 -
上面的情况不知道大家有没有思考过。spring容器bean都是java对象产生的。为什么Feign或者Mybatis这些框架中确实已接口存在的。而如果我们自己在接口上添加
@Component
等注解想spring容器注册时确实失败的。 -
对,没错像Feign能够将接口注册进spring里完全是因为
FeignClientFactoryBean
这个类。这个类实现了FactoryBean
。而FactoryBean
的作用就是创建Bean,并将Bean注册到Spring容器里同时也会将自己注册进spring容器。换句话说FactoryBean
会注册两个bean到spring容器中。FactoryBean
自己注册进去的名字是&xxx。 -
关于
FactoryBean
的具体使用及它与BeanFactory
的区别我们后续章节在展开讨论。为了防止找不到我,还请关注我获取实时更新
- 这个接口需要实现两个方法,一个返回bean的类型。另外一个就是返回bean对象。很明显
FeignClientFactoryBean#getObject
方法就是产生@FeignClient
注解的真实对象也叫作代理对象。
OpenFeign原理解析源码直入
- 还记得上面我们是如何配置Feign的吗,我们是直接在OrderApplication启动类上添加的。实际上就是直接在spring容器中添加次注解。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
- 而关于
EnableFeignClients
注解也很简单。里面有五个属性。值得注意的是该注解有导入了FeignClientsRegistrar.class
。不难理解重头戏肯定在FeignClientsRegistrar
里面。
FeignClientsRegistrar
实现了资源管理器、环境管理器、注册bean管理器。前面两个很好理解就是对资源、环境数据的操作。而注册bean实际上就是让FeignClientRegistrar
拥有了注册bean的能力。我们知道spring想容器中注册是通过BeanDefinition
。所以在Feign的源码追踪专题中ImportBeanDefinitionRegistrar
这个接口肯定是重中之重了。
ImportBeanDefinitionRegistrar
这个接口重点实现就是registerBeanDefinitions
这个方法。我们现在去FeignClientsRegistrar
中查看这个方法。
regisDefaultConfiguration
- 什么叫注册默认配置?这个默认配置其实就是
EnableFeignClients
中配置的spring配置类。
-
在上一章节我们已经分析了
hasEnclosingClass
的作用了。这里我们简单理解判断EnableFeignClient
是否注解在内部类上。我们可以看到实际regisDefaultConfiguration
方法中最终调用的是registerClientCOnfiguration
。而registerClientConfiguration
中实际上就是将FeignClientSpecification
注册到spring容器中。 -
而
FeignClientSpecification
实现了NamedContextFactory.Specification
接口。 -
NamedContextFactory.Specification
作用是用来管理spring容器中所有的Specification
。 这个类的作用就是让AnnotationConfigApplicationContext
根据不同的name管理对应的Config。 这就是regisDefaultConfiguration
里的逻辑。在我们第一章节的OpenFeign
的使用中,我们在@EnableFeignClients
注解中是没有配置任何东西的。后面我们在扩展篇继续摸索一下
registerFeignClients
- 下面我们开始遨游下
registerFeignClients
这个方法的逻辑。
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
- 首先开场的也是我们上面前置储备知识章节提到的
ClassPathScanningCandidateComponentProvider
这个类。这里就是创建扫描对象。方便后面扫描FeignClient
注解类进行解析
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
- 接下来是元数据
MetaData
。 这个元数据是OrderApplication启动类产生的元数据。因为这个是启动类上的EnableFeignClients
注解进入的。这个步骤是获取EnableFeignClients
这个注解的属性值。这里和registerDefaultConfiguration
哪里是一样的获取相关配置。
- 后面就是根据
EnableFeignClients
注解属性进行配置。会去获取clients这个属性。根据下面if判断可以推断出这个clients属性是用来扫描Client所在包的路径。并且添加ClassPathScanningCandidateComponentProvider
过滤器。
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
- 接下里就是获取
BeanDefinition
了。通过findCandidateComponents
来获取指定包路径下带有EnableFeignClients
注解的类对应的BeanDefinition
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
- 然后根据
FeignClient
注解上的属性凭借注册bean的name,然后通过BeanDefinition
注册到spring容器中。
-
最后就是将
FeignClientFactoryBean
中的object注册到spring容器去。而FeignClientFactoryBean
产生的对象就是@FeignClient
注解的类的信息。通过元数据信息在通过FeignClientFactoryBean
产生对象注册进去。 -
上面储备章节我们说过了
FeignClientFactoryBean
是产生FeignClient
注解的接口的代理对象。当我们@Autowired
注入的对象实际上就是这个代理对象。这个代理对象会基于注解信息解析出真实服务集合然后基于负载均衡进行接口调用。
总结
gitee.com/zxhTom/cloud-framework-root/tree/feature%2Fcloud-eureka/
-
openfeign极大的简化我们接口调用的耦合。我们主需要在接口中配置相关信息。然后就是本地化调用方法。
-
但是openfeign的实现很值得我们需要。里面涉及了spring的bean注册。bean拦截。动态代理等逻辑。
-
由于时间篇幅及能力的问题,本章节针对openfeign的动态代理只是点到为止。