申明式接口调用Feign,极大的简化了咱们接口之间的调用。只须要通过注解就能够实现咱们零碎之间接口的调用。
对于分布式咱们之前次要集中探讨了服务治理。eureka、consul、zookeeper咱们别离从三个角度不同水平的学习了这三个框架的原理及区别。这些作为后期springcloud的重要组成部分是咱们学习分布式不容忽视的章节。至于当初springcloud alibaba咱们这里重头菜要留到最初。对springcloud alibaba感兴趣还请关注我后续会更新相干内容
简介
openfeign源码
springcloud官网
- 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@EnableFeignClientspublic 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)@Documentedpublic @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为例展现下代码的配置
@Componentpublic 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@Slf4jpublic 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并开启日志就能够了
@Configurationpublic 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对象的关闭类的。什么叫关闭类呢。
@Datapublic 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类对应的BeanDefinitionBuilderBeanDefinitionBuilder 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
的区别咱们后续章节在展开讨论。为了避免找不到我,<red>还请关注我获取实时更新</red>
- 这个接口须要实现两个办法,一个返回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
注入的对象实际上就是这个代理对象。这个代理对象会基于注解信息解析出实在服务汇合而后基于负载平衡进行接口调用。
总结
点我看源码
- openfeign极大的简化咱们接口调用的耦合。咱们主须要在接口中配置相干信息。而后就是本地化调用办法。
- 然而openfeign的实现很值得咱们须要。外面波及了spring的bean注册。bean拦挡。动静代理等逻辑。
- 因为工夫篇幅及能力的问题,本章节针对openfeign的动静代理只是点到为止。