乐趣区

关于springcloud:分布式系列接口调用openfeign小试牛刀

申明式接口调用 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
@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 的区别咱们后续章节在展开讨论。为了避免找不到我,<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 的动静代理只是点到为止。

退出移动版