申明式接口调用Feign,极大的简化了我们接口之间的调用。只需要通过注解就可以实现我们系统之间接口的调用。
关于分布式我们之前主要集中讨论了服务治理。eureka、consul、zookeeper我们分别从三个角度不同程度的学习了这三个框架的原理及区别。这些作为前期springcloud的重要组成部分是我们学习分布式不容忽视的章节。至于现在springcloud alibaba我们这里重头菜要留到最后。对springcloud alibaba感兴趣还请关注我后续会更新相关内容
简介
==
使用
==
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>
启动类注入
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
@SpringBootApplication
: springboot程序标识启动注解@EnableEurekaClient
: 前面我们也介绍了,开启eureka的相关配置@EnableFeignClients
: 开启OpenFeign的相关配置新建interface
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface OrderPaymentService {
@RequestMapping(value = "/payment/create" , method = RequestMethod.POST)
public ResultInfo createByOrder(Payment payment);
}
调用
--
OrderPaymentService
。然后就是普通的java方法调用。为了演示出负载均衡的效果。我们在payment方法中携带出端口信息。超时控制
ribbon.ReadTimeout
属性发现超时就可以生效。但是需要注意这里的超时时间尽量设置比接口真实超时时间大一点。因为中间还有网络延迟时间。如下图所示ribbon.ReadTimeout=6000,那么在接口中我们休眠时间建议在4S以下。
feign.hystrix.enabled=true
来开启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;
}
@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);
}
@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;
}
};
}
}
日志打印
级别
作用
NONE
默认,没有日志
BASIC
请求方法、URL、响应状态
HEADERS
BASIC、请求、响应信息
FULL
完整数据
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
logging:
level:
com.zxhtom.cloud.order: debug
原理篇
===
openfeign原理前提知识准备(功底深厚直接跳过)
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
EnableFeignClients
注解的属性内容。@Data
public class Parent {
private String group;
@Data
class Child {
private String name;
}
}
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();
}
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
我们平时开发很少会接触到的,但是这个东西在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
。BeanDefinition
创建的。bean和java的对象是一脉相承的。java 对象是Class表示的。但是不知道你有没有发现FeignClient开发的实际上是个interface 。 但是我们在使用的时候却是正常的通过@Autowired
注入的。这个就违反了spring设计理念。于此类似的还有Mybatis中的Mapper开发。@Component
等注解想spring容器注册时确实失败的。FeignClientFactoryBean
这个类。这个类实现了FactoryBean
。而FactoryBean
的作用就是创建Bean,并将Bean注册到Spring容器里同时也会将自己注册进spring容器。换句话说FactoryBean
会注册两个bean到spring容器中。FactoryBean
自己注册进去的名字是&xxx。FactoryBean
的具体使用及它与BeanFactory
的区别我们后续章节在展开讨论。为了防止找不到我,还请关注我获取实时更新FeignClientFactoryBean#getObject
方法就是产生@FeignClient
注解的真实对象也叫作代理对象。OpenFeign原理解析源码直入
@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
中查看这个方法。EnableFeignClients
中配置的spring配置类。hasEnclosingClass
的作用了。这里我们简单理解判断EnableFeignClient
是否注解在内部类上。我们可以看到实际regisDefaultConfiguration
方法中最终调用的是registerClientCOnfiguration
。而registerClientConfiguration
中实际上就是将FeignClientSpecification
注册到spring容器中。FeignClientSpecification
实现了NamedContextFactory.Specification
接口。NamedContextFactory.Specification
作用是用来管理spring容器中所有的Specification
。 这个类的作用就是让AnnotationConfigApplicationContext
根据不同的name管理对应的Config。 这就是regisDefaultConfiguration
里的逻辑。在我们第一章节的OpenFeign
的使用中,我们在@EnableFeignClients
注解中是没有配置任何东西的。后面我们在扩展篇继续摸索一下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
注入的对象实际上就是这个代理对象。这个代理对象会基于注解信息解析出真实服务集合然后基于负载均衡进行接口调用。总结
==
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。