Spring Cloud Alibaba Sentinel 整合 Feign 的设计实现

简介: 前段时间 Hystrix 宣布不再维护之后(Hystrix 停止开发。。。Spring Cloud 何去何从?),Feign 作为一个跟 Hystrix 强依赖的组件,必然会有所担心后续的使用。

image.png前段时间 Hystrix 宣布不再维护之后(Hystrix 停止开发。。。Spring Cloud 何去何从?),Feign 作为一个跟 Hystrix 强依赖的组件,必然会有所担心后续的使用。


作为 Spring Cloud Alibaba 体系中的熔断器 Sentinel,Sentinel 目前整合了 Feign,本文对整合过程做一次总结,欢迎大家讨论和使用。


Feign 是什么?

Feign 是一个 Java 实现的 Http 客户端,用于简化 Restful 调用。


Feign 跟 OkHttp、HttpClient 这种客户端实现理念不一样。Feign 强调接口的定义,接口中的一个方法对应一个 Http 请求,调用方法即发送一个 Http 请求;OkHttp 或 HttpClient 以过程式的方式发送 Http 请求。Feign 底层发送请求的实现可以跟 OkHttp 或 HttpClient 整合。


要想整合 Feign,首先要了解 Feign 的使用以及执行过程,然后看 Sentinel 如何整合进去。


Feign 的使用

需要两个步骤:


1、使用 @EnableFeignClients 注解开启 Feign 功能

@SpringBootApplication
@EnableFeignClients // 开启 Feign 功能
public class MyApplication {
  ...
}

@EnableFeignClients 属性介绍:


value:String[] 包路径。比如 org.my.pkg,会扫描这个包路径下带有 @FeignClient 注解的类并处理;


basePackages:String[] 跟 value 属性作用一致;


basePackageClasses:Class[] 跟 basePackages 作用一致,basePackages 是个 String 数组,而 basePackageClasses 是个 Class 数组,用于扫描这些类对应的 package;


defaultConfiguration:Class[] 默认的配置类,对于所有的 Feign Client,这些配置类里的配置都会对它们生效,可以在配置类里构造 feign.codec.Decoder, feign.codec.Encoder 或 feign.Contract 等bean;


clients:Class[] 表示 @FeignClient; 注解修饰的类集合,如果指定了该属性,那么扫描功能相关的属性就是失效。比如 value、basePackages 和 basePackageClasses;


2、使用 @FeignClient 注解修饰接口,这样会基于跟接口生成代理类

@FeignClient(name = "service-provider")
public interface EchoService {
  @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
  String echo(@PathVariable("str") String str);
}

只要确保这个被 @FeignClient 注解修饰到的接口能被 @EnableFeignClients 注解扫描到,就会基于 java.lang.reflect.Proxy 根据这个接口生成一个代理类。


生成代理类之后,会被注入到 ApplicationContext 中,直接 AutoWired 就能使用,使用的时候调用 echo 方法就相当于是发起一个 Restful 请求。


@FeignClient 属性介绍:


value:String 服务名。比如 service-provider, http://service-provider。比如 EchoService 中如果配置了 value=service-provider,那么调用 echo 方法的 url 为 http://service-provider/echo;如果配置了 value=https://service-provider,那么调用 echo 方法的 url 为 https://service-provider/divide


serviceId:String 该属性已过期,但还能用。作用跟 value 一致

name:String 跟 value 属性作用一致


qualifier:String 给 FeignClient 设置 @Qualifier 注解


url:String 绝对路径,用于替换服务名。优先级比服务名高。比如 EchoService 中如果配置了 url=aaa,那么调用 echo 方法的 url 为 http://aaa/echo;如果配置了 url=https://aaa,那么调用 echo 方法的 url 为 https://aaa/divide


decode404:boolean 默认是 false,表示对于一个 http status code 为 404 的请求是否需要进行 decode,默认不进行 decode,当成一个异常处理。设置为true之后,遇到 404 的 response 还是会解析 body


configuration:Class[] 跟 @EnableFeignClients 注解的 defaultConfiguration 属性作用一致,但是这个对于单个 FeignClient 的配置,而 @EnableFeignClients 里的 defaultConfiguration 属性是作用域全局的,针对所有的 FeignClient


fallback:Class 默认值是 void.class,表示 fallback 类,需要实现 FeignClient 对应的接口,当调用方法发生异常的时候会调用这个 Fallback 类对应的 FeignClient 接口方法。


如果配置了 fallback 属性,那么会把这个 Fallback 类包装在一个默认的 FallbackFactory 实现类 FallbackFactory.Default 上,而不使用 fallbackFactory 属性对应的 FallbackFactory 实现类


fallbackFactory:Class 默认值是 void.class,表示生产 fallback 类的 Factory,可以实现 feign.hystrix.FallbackFactory 接口,FallbackFactory 内部会针对一个 Throwable 异常返回一个 Fallback 类进行 fallback 操作


path:String 请求路径。 在服务名或 url 与 requestPath 之间


primary:boolean 默认是 true,表示当前这个 FeignClient 生成的 bean 是否是 primary。


所以如果在 ApplicationContext中存在一个实现 EchoService 接口的 Bean,但是注入的时候并不会使用该Bean,因为 FeignClient 生成的 Bean 是 primary


Feign 的执行过程

了解了 Feign 的使用之后,接下来我们来看 Feign 构造一个 Client 的过程。


从 @EnableFeignClients 注解可以看到,入口在该注解上的 FeignClientsRegistrar 类上,整个链路是这样的:




从这个链路上我们可以得到几个信息:


1.@FeignClient 注解修饰的接口最终会被转换成 FeignClientFactoryBean 这个 FactoryBean,FactoryBean内部的 getObject 方法最终会返回一个 Proxy


2.在构造 Proxy 的过程中会根据 org.springframework.cloud.openfeign.Targeter 接口的 target 方法去构造。如果启动了hystrix开关(feign.hystrix.enabled=true),会使用 HystrixTargeter,否则使用默认的 DefaultTargeter


3.Targeter 内部构造 Proxy 的过程中会使用 feign.Feign.Builder 去调用它的 build 方法构造 feign.Feign 实例(默认只有一个子类 ReflectiveFeign)。


如果启动了 hystrix 开关(feign.hystrix.enabled=true),会使用 feign.hystrix.HystrixFeign.Builder,否则使用默认的feign.Feign.Builder


4.构造出 feign.Feign 实例之后,调用 newInstance 方法返回一个 Proxy


简单看下这个 newInstance 方法内部的逻辑:

public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if(Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    // 使用 InvocationHandlerFactory 根据接口的方法信息和 target 对象构造 InvocationHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
    // 构造代理
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
    for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

这里的 InvocationHandlerFactory 是通过构造 Feign 的时候传入的:


使用原生的 DefaultTargeter: 那么会使用 feign.InvocationHandlerFactory.Default 这个 factory,并且构造出来的 InvocationHandler 是 feign.ReflectiveFeign.FeignInvocationHandler


使用 hystrix 的 HystrixTargeter: 那么会在feign.hystrix.HystrixFeign.Builder#build(feign.hystrix.FallbackFactory) 方法中调用父类的 invocationHandlerFactory 方法传入一个匿名的 InvocationHandlerFactory 实现类,该类内部构造出的 InvocationHandler 为 HystrixInvocationHandler


Sentinel 整合 Feign

理解了 Feign 的执行过程之后,Sentinel 想要整合 Feign,可以参考 Hystrix 的实现:


1.❌ 实现 Targeter 接口 SentinelTargeter。 很不幸,Targeter 这个接口属于包级别的接口,在外部包中无法使用,这个 Targeter 无法使用。没关系,我们可以沿用默认的HystrixTargeter(实际上会用 DefaultTargeter,下文 Note 有解释)


2.✅ FeignClientFactoryBean 内部构造 Targeter、feign.Feign.Builder 的时候,都会从 FeignContext 中获取。所以我们沿用默认的 DefaultTargeter 的时候,内部使用的 feign.Feign.Builder 可控,而且这个 Builder 不是包级别的类,可在外部使用


创建 SentinelFeign.Builder 继承 feign.Feign.Builder ,用来构造 Feign


SentinelFeign.Builder 内部需要获取 FeignClientFactoryBean 中的属性进行处理,比如获取 fallback, name, fallbackFactory。


很不幸,FeignClientFactoryBean 这个类也是包级别的类。没关系,我们知道它存在在 ApplicationContext 中的 beanName, 拿到 bean 之后根据反射获取属性就行(该过程在初始化的时候进行,不会在调用的时候进行,所以不会影响性能)


SentinelFeign.Builder 调用 build 方法构造 Feign 的过程中,我们不需要实现一个新的 Feign,跟 hystrix 一样沿用 ReflectiveFeign 即可,在沿用的过程中调用父类 feign.Feign.Builder 的一些方法进行改造即可,比如 invocationHandlerFactory 方法设置 InvocationHandlerFactory ,contract 的调用

3.✅ 跟 hystrix 一样实现自定义的 InvocationHandler 接口 SentinelInvocationHandler 用来处理方法的调用


4.✅ SentinelInvocationHandler 内部使用 Sentinel 进行保护,这个时候涉及到资源名的获取。SentinelInvocationHandler 内部的 feign.Target 能获取服务名信息,feign.InvocationHandlerFactory.MethodHandler 的实现类 feign.SynchronousMethodHandler 能拿到对应的请求路径信息。


很不幸,feign.SynchronousMethodHandler 这个类也是包级别的类。没关系,我们可以自定义一个 feign.Contract 的实现类 SentinelContractHolder 在处理 MethodMetadata 的过程把这些 metadata 保存下来(feign.Contract 这个接口在 Builder 构造 Feign 的过程中会对方法进行解析并验证)。


在 SentinelFeign.Builder 中调用 contract 进行设置,SentinelContractHolder 内部保存一个 Contract 使用委托方式不影响原先的 Contract 过程


Note: spring-cloud-starter-openfeign 依赖内部包含了 feign-hystrix。所以是说默认使用 HystrixTargeter 这个 Targeter ,进入 HystrixTargeter 的 target 方法内部一看,发现有段逻辑这么写的:

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
                    Target.HardCodedTarget<T> target) {
  if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
    // 如果 Builder 不是 feign.hystrix.HystrixFeign.Builder,使用这个 Builder 进行处理
    // 我们默认构造了 SentinelFeign.Builder 这个 Builder,默认使用 feign-hystrix 依赖也没有什么问题
    return feign.target(target);
  }
  feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
  ...
}

SentinelInvocationHandler 内部我们对资源名的处理策略是: http方法:protocol://服务名/请求路径跟参数

比如这个 TestService:

@FeignClient(name = "test-service")
public interface TestService {
  @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
  String echo(@PathVariable("str") String str);
  @RequestMapping(value = "/divide", method = RequestMethod.GET)
  String divide(@RequestParam("a") Integer a, @RequestParam("b") Integer b);
}

echo 方法对应的资源名:GET:http://test-service/echo/{str}

divide 方法对应的资源名:GET:http://test-service/divide

总结

1.Feign 的内部很多类都是 package 级别的,外部 package 无法引用某些类,这个时候只能想办法绕过去,比如使用反射


2.目前这种实现有风险,万一哪天 starter 内部使用的 Feign 相关类变成了 package 级别,那么会改造代码。所以把 Sentinel 的实现放到 Feign 里并给 Feign 官方提 pr 可能更加合适


3.Feign的处理流程还是比较清晰的,只要能够理解其设计原理,我们就能容易地整合进去


欢迎大家对整合方案进行讨论,并能给出不合理的地方,当然能提pr解决不合理的地方就更好了。


Sentinel Starter 整合 Feign 的代码目前已经在 github 仓库上,但是没未发版。预计月底发版,如果现在就想使用,可以在 pom 中引入 Spring SNAPSHOT 的 repository 或自行下载源码进行编译。


最后再附上一个使用 Nacos 做服务发现和 Sentinel 做限流的 Feign 例子。


https://github.com/spring-cloud-incubator/spring-cloud-alibaba/tree/master/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example


相关文章
|
19天前
|
JSON SpringCloudAlibaba Java
Springcloud Alibaba + jdk17+nacos 项目实践
本文基于 `Springcloud Alibaba + JDK17 + Nacos2.x` 介绍了一个微服务项目的搭建过程,包括项目依赖、配置文件、开发实践中的新特性(如文本块、NPE增强、模式匹配)以及常见的问题和解决方案。通过本文,读者可以了解如何高效地搭建和开发微服务项目,并解决一些常见的开发难题。项目代码已上传至 Gitee,欢迎交流学习。
Springcloud Alibaba + jdk17+nacos 项目实践
|
6天前
|
消息中间件 自然语言处理 Java
知识科普:Spring Cloud Alibaba基本介绍
知识科普:Spring Cloud Alibaba基本介绍
25 2
|
13天前
|
人工智能 开发框架 Java
总计 30 万奖金,Spring AI Alibaba 应用框架挑战赛开赛
Spring AI Alibaba 应用框架挑战赛邀请广大开发者参与开源项目的共建,助力项目快速发展,掌握 AI 应用开发模式。大赛分为《支持 Spring AI Alibaba 应用可视化调试与追踪本地工具》和《基于 Flow 的 AI 编排机制设计与实现》两个赛道,总计 30 万奖金。
|
14天前
|
Dubbo Java 应用服务中间件
Dubbo学习圣经:从入门到精通 Dubbo3.0 + SpringCloud Alibaba 微服务基础框架
尼恩团队的15大技术圣经,旨在帮助开发者系统化、体系化地掌握核心技术,提升技术实力,从而在面试和工作中脱颖而出。本文介绍了如何使用Dubbo3.0与Spring Cloud Gateway进行整合,解决传统Dubbo架构缺乏HTTP入口的问题,实现高性能的微服务网关。
|
14天前
|
人工智能 Java API
阿里云开源 AI 应用开发框架:Spring AI Alibaba
近期,阿里云重磅发布了首款面向 Java 开发者的开源 AI 应用开发框架:Spring AI Alibaba(项目 Github 仓库地址:alibaba/spring-ai-alibaba),Spring AI Alibaba 项目基于 Spring AI 构建,是阿里云通义系列模型及服务在 Java AI 应用开发领域的最佳实践,提供高层次的 AI API 抽象与云原生基础设施集成方案,帮助开发者快速构建 AI 应用。本文将详细介绍 Spring AI Alibaba 的核心特性,并通过「智能机票助手」的示例直观的展示 Spring AI Alibaba 开发 AI 应用的便利性。示例源
|
19天前
|
人工智能 Java API
阿里云开源 AI 应用开发框架:Spring AI Alibaba
阿里云开源 Spring AI Alibaba,旨在帮助 Java 开发者快速构建 AI 应用,共同构建物理新世界。
|
14天前
|
负载均衡 算法 Java
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
40岁老架构师尼恩分享了关于SpringCloud核心组件的底层原理,特别是针对蚂蚁集团面试中常见的面试题进行了详细解析。内容涵盖了Nacos注册中心的AP/CP模式、Distro和Raft分布式协议、Sentinel的高可用组件、负载均衡组件的实现原理等。尼恩强调了系统化学习的重要性,推荐了《尼恩Java面试宝典PDF》等资料,帮助读者更好地准备面试,提高技术实力,最终实现“offer自由”。更多技术资料和指导,可关注公众号【技术自由圈】获取。
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
|
2月前
|
Java 数据中心 Sentinel
spring boot sentinel 的使用
要实现Spring Boot集成Sentinel的熔断降级,需引入Sentinel依赖并配置Dashboard地址;使用`@SentinelResource`注解定义受保护资源及blockHandler处理降级逻辑;通过Sentinel Dashboard配置熔断规则,如异常比例或响应时间。启动应用后,测试熔断功能,确保Sentinel正常工作。建议动态调整规则以提升系统稳定性。
|
2月前
|
人工智能 前端开发 Java
Spring Cloud Alibaba AI,阿里AI这不得玩一下
🏀闪亮主角: 大家好,我是JavaDog程序狗。今天分享Spring Cloud Alibaba AI,基于Spring AI并提供阿里云通义大模型的Java AI应用。本狗用SpringBoot+uniapp+uview2对接Spring Cloud Alibaba AI,带你打造聊天小AI。 📘故事背景: 🎁获取源码: 关注公众号“JavaDog程序狗”,发送“alibaba-ai”即可获取源码。 🎯主要目标:
61 0
|
3月前
|
Java UED Sentinel
微服务守护神:Spring Cloud Sentinel,让你的系统在流量洪峰中稳如磐石!
【8月更文挑战第29天】Spring Cloud Sentinel结合了阿里巴巴Sentinel的流控、降级、熔断和热点规则等特性,为微服务架构下的应用提供了一套完整的流量控制解决方案。它能够有效应对突发流量,保护服务稳定性,避免雪崩效应,确保系统在高并发下健康运行。通过简单的配置和注解即可实现高效流量控制,适用于高并发场景、依赖服务不稳定及资源保护等多种情况,显著提升系统健壮性和用户体验。
76 1