learn.lianglianglee.com/专栏/深入理解 Sentinel(完)/17 Sentinel 主流框架适配.md.html
2022-05-11 19:04:14 +08:00

947 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>17 Sentinel 主流框架适配.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/深入理解 Sentinel/01 开篇词:一次服务雪崩问题排查经历.md.html">01 开篇词:一次服务雪崩问题排查经历.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/02 为什么需要服务降级以及常见的几种降级方式.md.html">02 为什么需要服务降级以及常见的几种降级方式.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/03 为什么选择 SentinelSentinel 与 Hystrix 的对比.md.html">03 为什么选择 SentinelSentinel 与 Hystrix 的对比.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/04 Sentinel 基于滑动窗口的实时指标数据统计.md.html">04 Sentinel 基于滑动窗口的实时指标数据统计.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/05 Sentinel 的一些概念与核心类介绍.md.html">05 Sentinel 的一些概念与核心类介绍.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/06 Sentinel 中的责任链模式与 Sentinel 的整体工作流程.md.html">06 Sentinel 中的责任链模式与 Sentinel 的整体工作流程.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/07 Java SPI 及 SPI 在 Sentinel 中的应用.md.html">07 Java SPI 及 SPI 在 Sentinel 中的应用.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/08 资源指标数据统计的实现全解析(上).md.html">08 资源指标数据统计的实现全解析(上).md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/09 资源指标数据统计的实现全解析(下).md.html">09 资源指标数据统计的实现全解析(下).md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/10 限流降级与流量效果控制器(上).md.html">10 限流降级与流量效果控制器(上).md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/11 限流降级与流量效果控制器(中).md.html">11 限流降级与流量效果控制器(中).md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/12 限流降级与流量效果控制器(下).md.html">12 限流降级与流量效果控制器(下).md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/13 熔断降级与系统自适应限流.md.html">13 熔断降级与系统自适应限流.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/14 黑白名单限流与热点参数限流.md.html">14 黑白名单限流与热点参数限流.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/15 自定义 ProcessorSlot 实现开关降级.md.html">15 自定义 ProcessorSlot 实现开关降级.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/16 Sentinel 动态数据源:规则动态配置.md.html">16 Sentinel 动态数据源:规则动态配置.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/深入理解 Sentinel/17 Sentinel 主流框架适配.md.html">17 Sentinel 主流框架适配.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/18 Sentinel 集群限流的实现(上).md.html">18 Sentinel 集群限流的实现(上).md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/19 Sentinel 集群限流的实现(下).md.html">19 Sentinel 集群限流的实现(下).md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/20 结束语Sentinel 对应用的性能影响如何?.md.html">20 结束语Sentinel 对应用的性能影响如何?.md.html</a>
</li>
<li>
<a href="/专栏/深入理解 Sentinel/21 番外篇Sentinel 1.8.0 熔断降级新特性解读.md.html">21 番外篇Sentinel 1.8.0 熔断降级新特性解读.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>17 Sentinel 主流框架适配</h1>
<p>使用 Sentinel 需要用 try-catch-finally 将需要保护的资源(方法或者代码块)包装起来,在目标方法或者代码块执行之前,调用 ContextUtil#enter 方法以及 SphU#entry 方法,在抛出异常时,如果非 BlockException 异常需要调用 Tracer#trace 记录异常,修改异常指标数据,在 finally 中需要调用 Entry#exit 方法,以及 ContextUtil#exit 方法。</p>
<p>为了节省这些步骤Sentinel 提供了对主流框架的适配,如适配 Spring MVC、Webflux、Dubbo、Api Gateway 等框架。当然,对于 Sentinel 未适配的框架,我们也可以自己实现适配器。在 Sentinel 源码之外alibaba 的 spring-cloud-starter-alibaba-sentinel 也为 Sentinel 提供与 OpenFeign 框架整合的支持。</p>
<p><img src="assets/ae715de0-ed3b-11ea-be9c-f7616f01fc23" alt="17-01-sentinel-adapter" /></p>
<h3>Spring MVC 适配器</h3>
<p>Sentinel 借助 Spring MVC 框架的 HandlerInterceptor 适配 Spring MVC但也需要我们借助 WebMvcConfigurer 将 SentinelWebInterceptor 注册到 Spring MVC 框架。</p>
<h4><strong>使用步骤</strong></h4>
<p>第一步:在项目中添加 Spring MVC 适配模块的依赖。</p>
<pre><code>&lt;dependency&gt;
&lt;groupId&gt;com.alibaba.csp&lt;/groupId&gt;
&lt;artifactId&gt;sentinel-spring-webmvc-adapter&lt;/artifactId&gt;
&lt;version&gt;${version}&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>第二步:编写 WebMvcConfigurer在 addInterceptors 方法中注入 SentinelWebInterceptor。</p>
<pre><code class="language-java">@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
config.setBlockExceptionHandler(new DefaultBlockExceptionHandler());
config.setHttpMethodSpecify(true);
config.setOriginParser(request -&gt; request.getHeader(&quot;S-user&quot;));
// SentinelWebInterceptor 拦截所有接口(&quot;/**&quot;
registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns(&quot;/**&quot;);
}
}
</code></pre>
<p>在创建 SentinelWebInterceptor 时,可为 SentinelWebInterceptor 添加配置,使用 SentinelWebMvcConfig 封装这些配置:</p>
<ul>
<li>setBlockExceptionHandler配置 BlockException 异常处理器,如果不想配置 BlockException 异常处理器,也可在 Spring MVC 的全局异常处理器中处理 BlockException 异常。</li>
<li>setOriginParser注册调用来源origin解析器例如从请求头中获取“S-user”参数的值作为调用来源名称上游服务在发起请求时就可在请求头写入“S-user”参数告知自己的身份。</li>
<li>setHttpMethodSpecify是否需要给资源名称加上 HttpMethod 前缀,例如 GET 接口“/hello”如果 httpMethodSpecify 配置为 false则资源名称为“/hello”否则资源名称为“GET:/hell”。</li>
</ul>
<h4><strong>适配原理</strong></h4>
<p>Spring MVC 框架的方法拦截器HandlerInterceptor的定义如下</p>
<pre><code class="language-java">public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
</code></pre>
<p>HandlerInterceptor 在 DispatcherServlet#doDispatch 方法中被调用,每个方法的调用时机如下:</p>
<ul>
<li>preHandle在调用接口方法之前调用</li>
<li>postHandle在接口方法执行完成返回 ModelAndView 时被调用;</li>
<li>afterCompletion在接口方法执行完成时被调用无论成功或异常都会被调用</li>
</ul>
<p>因此Sentinel 可借助 HandlerInterceptor 与 Spring MVC 框架整合,在 HandlerInterceptor#preHandle 方法中调用 ContextUtil#enter 方法以及 SphU#entry 方法,在 afterCompletion 方法中根据方法参数 ex 是否为空处理异常情况,并且完成 Entry#exit 方法、ContextUtil#exit 方法的调用。</p>
<p>SentinelWebInterceptor 是 AbstractSentinelInterceptor 的子类preHandle 与 afterCompletion 方法在父类中实现,自身只实现父类定义的一个获取资源名称的抽象方法,其源码如下:</p>
<pre><code class="language-java"> @Override
protected String getResourceName(HttpServletRequest request) {
// 1
Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (resourceNameObject == null || !(resourceNameObject instanceof String)) {
return null;
}
String resourceName = (String) resourceNameObject;
// 2
UrlCleaner urlCleaner = config.getUrlCleaner();
if (urlCleaner != null) {
resourceName = urlCleaner.clean(resourceName);
}
// 3
if (StringUtil.isNotEmpty(resourceName) &amp;&amp; config.isHttpMethodSpecify()) {
resourceName = request.getMethod().toUpperCase() + &quot;:&quot; + resourceName;
}
return resourceName;
}
</code></pre>
<p>资源名称生成过程如下。</p>
<p>\1. 从 HttpServletRequest 的属性中获取 HandlerMapping 匹配的 URL。</p>
<p>因为有些接口是这样的:“/hello/{name}”,如果直接从 HttpServletRequest 获取请求路径,那么每个请求获取到的 URL 就可能会不同。</p>
<p>\2. 如果 UrlCleaner 不为空,则调用 UrlCleaner 的 clean 方法。</p>
<p>UrlCleaner 用于实现将多个接口合并为一个,例如接口:“/user/create”、“/user/del”、“/user/update”借助 UrlCleaner 修改资源名称将这几个接口都改为“/user/**”即可实现三个接口使用同一个限流规则。</p>
<p>\3. 根据 SentinelWebMvcConfig 配置对象判断是否需要添加 HttpMethod 前缀,如果需要则给资源名称拼接前缀。</p>
<p>一般来说,不建议使用,因为如果接口使用 @RequestMapping 声明,那么想对该接口限流就需要配置多个限流规则,而一般旧项目多是使用 @RequestMapping 声明接口方法。例如接口“/user/create”你可能需要针对“GET:/user/create”、“POST:/user/create”等多个资源配置限流规则。</p>
<p>由于 AbstractSentinelInterceptor 的源码较多,我们分几个步骤分析。</p>
<p>AbstractSentinelInterceptor#preHandle 方法源码如下:</p>
<pre><code class="language-java"> @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
try {
//1
String resourceName = getResourceName(request);
if (StringUtil.isNotEmpty(resourceName)) {
//2
String origin = parseOrigin(request);
//3
ContextUtil.enter(SENTINEL_SPRING_WEB_CONTEXT_NAME, origin);
//4
Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
//5
setEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName(), entry);
}
return true;
} catch (BlockException e) {
// 6
handleBlockException(request, response, e);
return false;
}
}
</code></pre>
<ol>
<li>获取资源名称;</li>
<li>调用 OriginParser#parseOrigin 方法解析调用来源,例如从请求头获取&quot;S-user&quot;参数的值;</li>
<li>调用 ContextUtil#enter 方法Context 名称为“sentinel_spring_web_context”</li>
<li>调用 SphU#entry 方法,资源类型为 COMMON_WEB流量类型为 IN</li>
<li>将 SphU#entry 方法返回的 Entry 放入 HttpServletRequest 的属性表中,方便在 afterCompletion 中取出;</li>
<li>如果抛出 BlockException 异常,说明当前请求被拒绝,需调用 handleBlockException 方法处理 BlockException 异常。</li>
</ol>
<p>AbstractSentinelInterceptor#handleBlockException 方法源码如下:</p>
<pre><code class="language-java">protected void handleBlockException(HttpServletRequest request, HttpServletResponse response, BlockException e)
throws Exception {
if (baseWebMvcConfig.getBlockExceptionHandler() != null) {
baseWebMvcConfig.getBlockExceptionHandler().handle(request, response, e);
} else {
throw e;
}
}
</code></pre>
<p>如果我们给 SentinelWebMvcConfig 配置了 BlockExceptionHandler则调用 BlockExceptionHandler#handle 方法处理 BlockException 异常,否则将异常抛出,由全局处理器处理。</p>
<p>AbstractSentinelInterceptor#afterCompletion 方法源码如下:</p>
<pre><code class="language-java"> @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
//1
Entry entry = getEntryInRequest(request, baseWebMvcConfig.getRequestAttributeName());
if (entry != null) {
//2
traceExceptionAndExit(entry, ex);
removeEntryInRequest(request);
}
//3
ContextUtil.exit();
}
</code></pre>
<ol>
<li>从 HttpServletRequest 的属性表中获取 preHandle 方法放入的 Entry</li>
<li>调用 traceExceptionAndExit 方法,记录异常和调用 Entry#exit 方法;</li>
<li>调用 ContextUtil#exit 方法,如果当前 CtEntry 为空,则从 ThreadLocal 中移除 Context。</li>
</ol>
<p>AbstractSentinelInterceptor#traceExceptionAndExit 方法源码如下:</p>
<pre><code class="language-java"> protected void traceExceptionAndExit(Entry entry, Exception ex) {
if (entry != null) {
if (ex != null) {
Tracer.traceEntry(ex, entry);
}
entry.exit();
}
}
</code></pre>
<p>当方法执行抛出异常时,调用 Tracer#traceEntry 方法记录异常,更新异常指标数据。</p>
<h3>OpenFeign 适配器</h3>
<p>Sentinel 整合 OpenFeign 主要用于实现熔断降级,因此,关于 OpenFeign 的 Sentinel 适配器的使用介绍基于服务消费端。</p>
<h4><strong>使用步骤</strong></h4>
<p><strong>1. 引入依赖</strong></p>
<p>借助 spring-cloud-starter-alibaba-sentinel 实现与 OpenFeign 整合,添加依赖配置如下:</p>
<pre><code class="language-xml">&lt;dependency&gt;
&lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-starter-alibaba-sentinel&lt;/artifactId&gt;
&lt;version&gt;2.2.1.RELEASE&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p><strong>2. 启用 OpenFeign 整合 Sentinel 的自动配置</strong></p>
<p>在 application.yaml 配置文件中添加如下配置,启用 Sentinel 与 OpenFeign 整合适配器。</p>
<pre><code class="language-yaml">feign:
sentinel:
enabled: true
</code></pre>
<p><strong>3. 熔断降级规则配置</strong></p>
<p>可基于动态数据源实现,也可直接调用 DegradeRuleManager 的 loadRules API 硬编码实现,可参考上一篇。</p>
<p><strong>4. 给 @FeignClient 注解配置异常回调</strong></p>
<p>给接口上的 @FeignClient 注解配置 fallback 属性,实现请求被拒绝后的处理。</p>
<pre><code class="language-java">@FeignClient(
//.....
// 这里配置
fallback = ServiceDegradeFallback.class)
public interface DemoService {
@PostMapping(&quot;/services&quot;)
ListGenericResponse&lt;DemoDto&gt; getServices();
}
</code></pre>
<p>fallback 属性要求配置一个类,该类必须实现相同的接口,所以 ServiceDegradeFallback 必须实现 DemoService 接口。</p>
<pre><code class="language-java">public class ServiceDegradeFallback implements DemoService {
@Override
public ListGenericResponse&lt;DemoDto&gt; getServices() {
ListGenericResponse response = new ListGenericResponse&lt;DemoDto&gt;();
response.setCode(ResultCode.SERVICE_DEGRAD.getCode())
.setMessage(&quot;服务降级&quot;);
return response;
}
}
</code></pre>
<p>ServiceDegradeFallback 类中处理接口降级逻辑,例如,响应一个状态码告知消费端由于服务降级本次接口调用失败。</p>
<p>最后还需要将该 ServiceDegradeFallback 注册到 Feign 的 Clinet 环境隔离的容器中。</p>
<p>编写配置类 SentinelFeignConfig在 SentinelFeignConfig 中注册 ServiceDegradeFallback。</p>
<pre><code class="language-java">public class SentinelFeignConfig {
@Bean
public ServiceDegradeFallback degradeMockYcpayService() {
return new ServiceDegradeFallback();
}
}
</code></pre>
<p>将 SentinelFeignConfig 配置类添加到 @FeignClient 注解的 configuration 属性,如下:</p>
<pre><code class="language-java">@FeignClient(
// .....
configuration = {
// 这里配置
SentinelFeignConfig.class
},
// 这里配置
fallback = ServiceDegradeFallback.class)
public interface DemoService {
@PostMapping(&quot;/services&quot;)
ListGenericResponse&lt;DemoDto&gt; getServices();
}
</code></pre>
<p>当满足熔断条件时Sentinel 会抛出一个 DegradeException 异常,如果配置了 fallback那么 Sentinel 会从 Bean 工厂中根据 fallback 属性配置的类型取一个 Bean 并调用接口方法。</p>
<h4><strong>Sentinel 与 OpenFeign 整合实现原理</strong></h4>
<p>当 Sentinel 与 OpenFeign、Ribbon 整合时,客户端向服务端发起一次请求的过程如下图所示。</p>
<p><img src="assets/7b545840-ed3b-11ea-9210-e5c7b119b96e" alt="17-02-openfeign-sentinel" /></p>
<ol>
<li>当调用@FeignClient 接口的方法时,由 Sentinel 提供的方法调用拦截器SentinelInvocationHandler拦截方法的执行根据接口方法上注解的 url 生成资源名称,然后调用 Sentinel 的 SphU#entry 方法(完成所有 ProcessorSlot#entry 方法的调用),判断当前发起的请求是否需要熔断;</li>
<li>非熔断降级情况下,继续将请求交给 OpenFeign 的 MethodHandler 处理;</li>
<li>OpenFeign 从 Ribbon 获取一个服务提供者节点;</li>
<li>OpenFeign 使用 HttpClient 发起 HTTP 请求;</li>
<li>OpenFeign 请求成功或者异常(已经经过重试)时,调用 Sentinel 的 Entry#exit 方法(完成所有 ProcessorSlot#exit 方法的调用)更新当前时间窗口的请求成功总数、异常总数等指标数据。</li>
</ol>
<p>可见Sentinel 处在接口调用的最前端,因此 Sentinel 统计的指标数据即不会受 Ribbon 的重试影响也不会受 OpenFeign 的重试影响。</p>
<p>Sentinel 通过自己提供 InvocationHandler 替换 OpenFeign 的 InvocationHandler 实现请求拦截。SentinelInvocationHandler 源码调试如下图所示。</p>
<p><img src="assets/d7b506c0-ed3b-11ea-816b-87f82de6664d" alt="17-02-openfeign-sentinel02" /></p>
<p>InvocationHandler 是 OpenFeign 为接口生成 JDK 动态代理类时所需要的是接口的方法拦截处理器Sentinel 通过替换 OpenFeign 的 InvocationHandler 拦截方法的执行,在 OpenFeign 处理接口调用之前完成熔断降级的检查。</p>
<p>那么Sentinel 是如何将原本的 FeignInvocationHandler 替换为 SentinelInvocationHandler 的呢?</p>
<p>OpenFeign 通过 Feign.Builder 类创建接口的代理类,所以 Sentinel 直接将 Feign.Builder 也替换成了 SentinelFeign.Builder由 SentinelFeignAutoConfiguration 自动配置类向 Spring 的 Bean 容器注入 SentinelFeign.Builder代码如下</p>
<pre><code class="language-java">@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class, Feign.class })
public class SentinelFeignAutoConfiguration {
@Bean
@Scope(&quot;prototype&quot;)
@ConditionalOnMissingBean
@ConditionalOnProperty(name = &quot;feign.sentinel.enabled&quot;)
public Feign.Builder feignSentinelBuilder() {
return SentinelFeign.builder();
}
}
</code></pre>
<p>SentinelFeign.Builder 继承 Feign.Builder 并重写 build 方法SentinelFeign.Builder#build 方法源码如下:</p>
<pre><code class="language-java">public final class SentinelFeign {
public static Builder builder() {
return new Builder();
}
public static final class Builder extends Feign.Builder
implements ApplicationContextAware {
// .....
@Override
public Feign build() {
super.invocationHandlerFactory(new InvocationHandlerFactory() {
@Override
public InvocationHandler create(Target target,
Map&lt;Method, MethodHandler&gt; dispatch) {
// 创建 SentinelInvocationHandler
}
});
super.contract(new SentinelContractHolder(contract));
return super.build();
}
// .....
}
}
</code></pre>
<p>SentinelFeign.Builder#build 偷天换日,替换了 InvocationHandlerFactory所以 OpenFeign 调用 InvocationHandlerFactory#create 方法创建的 InvocationHandler 就变成了 SentinelInvocationHandler。</p>
<p>看 InvocationHandlerFactory#create 方法的返回值类型我们也能知道,该方法负责创建 SentinelInvocationHandler。create 方法部分源码如下:</p>
<pre><code class="language-java">Class fallback = (Class) getFieldValue(feignClientFactoryBean,
&quot;fallback&quot;);
Object fallbackInstance = getFromContext(beanName, &quot;fallback&quot;, fallback,
target.type());
return new SentinelInvocationHandler(target, dispatch,
new FallbackFactory.Default(fallbackInstance));
</code></pre>
<p>在创建 SentinelInvocationHandler 之前,通过反射从 FeignClientFactoryBean 拿到 @FeignClient 注解的 fallback 属性值,然后根据 fallback 类型从 Bean 工厂取得 fallback 实例,将 fallback 实例传递给 SentinelInvocationHandler。当触发熔断时SentinelInvocationHandler 就能取得 fallback 实例并调用。</p>
<h3>总结</h3>
<p>本篇我们分析了 Sentinel 适配 Spring MVC 框架的实现原理,以及 Sentinel 适配 Spring Cloud OpenFeign 框架的实现原理。适配各种主流框架,无非就是通过框架提供的方法拦截器注入 Sentinel或者通过拦截主流框架的入口方法注入 Sentinel。了解原理之后如果我们项目中使用的框架 Sentinel 并未适配,那么我们也可以自己实现适配器。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/深入理解 Sentinel/16 Sentinel 动态数据源:规则动态配置.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/深入理解 Sentinel/18 Sentinel 集群限流的实现(上).md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997b2b2a2c3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>