Sentinel學習(七) —— API、註解、整合Feign

Sentinel API

這裏介紹三個重要的API。

  • ContextUtil
  • Tracer
  • SphU
    @GetMapping("/test-sentinel-api")
    public String testSentinelApi(@RequestParam(required = false) String a) {
        //定義一個sentinel受保護的資源,名稱是test-sentinel-api
        String resourceName = "test-sentinel-api";
        //
        ContextUtil.enter(resourceName, "test-wfw");
        Entry entry = null;
        try {
            entry = SphU.entry(resourceName);
            //被保護的邏輯
            if (StringUtils.isEmpty(a)) {
                throw new IllegalArgumentException("a is not null");
            }
            return a;
        } catch (BlockException be) {
            //如果受保護的資源被限流或者降級了 就會拋BlockException
            log.warn("限流或者降級了", be);
            return "限流或者降級了";
        } catch (IllegalArgumentException ie) {
            //統計 IllegalArgumentException 發生的次數、佔比。。。
            Tracer.trace(ie);
            return "a is not null";
        } finally {
            if(entry != null) {
                //退出entry
                entry.exit();
            }
            ContextUtil.exit();
        }

    }

Sentinel 註解

屬性 作用 是否必須
value 資源名稱
entryType entry類型,標記流量的方向,取值IN/OUT,默認是OUT
blockHandler 處理BlockException的函數名稱。函數要求:1.必須是 public 2.返回類型與原方法一致 3.參數類型需要和原方法相匹配,並在最後加 BlockException 類型的參數。4.默認需和原方法在同一個類中。若希望使用其他類的函數,可配置 blockHandlerClass ,並指定blockHandlerClass裏面的方法。
blockHandlerClass 存放blockHandler的類。對應的處理函數必須static修飾,否則無法解析,其他要求:同blockHandler。
fallback 用於在拋出異常的時候提供fallback處理邏輯。fallback函數可以針對所有類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理。函數要求:1. 返回類型與原方法一致 2. 參數類型需要和原方法相匹配,Sentinel 1.6開始,也可在方法最後加 Throwable 類型的參數。3.默認需和原方法在同一個類中。若希望使用其他類的函數,可配置 fallbackClass ,並指定fallbackClass裏面的方法。
fallbackClass【1.6】 存放fallback的類。對應的處理函數必須static修飾,否則無法解析,其他要求:同fallback。
defaultFallback【1.6】 用於通用的 fallback 邏輯。默認fallback函數可以針對所有類型的異常(除了 exceptionsToIgnore 裏面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,以fallback爲準。函數要求:1. 返回類型與原方法一致 2. 方法參數列表爲空,或者有一個 Throwable 類型的參數。3. 默認需要和原方法在同一個類中。若希望使用其他類的函數,可配置 fallbackClass ,並指定 fallbackClass 裏面的方法。
exceptionsToIgnore【1.6】 指定排除掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣拋出。
exceptionsToTrace 需要trace的異常 Throwable

限流處理的方法。

    @GetMapping("/test-sentinel-resource")
    @SentinelResource(value = "test-sentinel-resource",
            blockHandlerClass = TestBlock.class,
            blockHandler = "block",
            fallbackClass = TestFallBack.class,
            fallback = "fallBack"
    )
    public String testSentinelResource(@RequestParam(required = false) String a) {
        if (StringUtils.isEmpty(a)) {
            throw new IllegalArgumentException("a is not null");
        }
        return a;
    }

限流處理類。

@Slf4j
@Component
public class TestBlock {
    public static String block(String a, BlockException e) {
        log.warn("限流 或者 降級 block a:{}", a, e);
        return "限流 或者 降級 block";
    }
}

降級處理類。

@Slf4j
@Component
public class TestFallBack {
    public static String fallBack(String a, Throwable e) {
        log.warn("限流 或者 降級 fall a:{}", a, e);
        return "限流 或者 降級 fall";
    }
}

TIPS

  • 1.6.0 之前的版本 fallback 函數只針對降級異常(DegradeException)進行處理,不能針對業務異常進行處理。
  • 若 blockHandler 和 fallback 都進行了配置,則被限流降級而拋出 BlockException 時只會進入 blockHandler 處理邏輯。若未配置 blockHandler、fallback 和 defaultFallback,則被限流降級時會將 BlockException 直接拋出。
  • 從 1.4.0 版本開始,註解方式定義資源支持自動統計業務異常,無需手動調用 Tracer.trace(ex) 來記錄業務異常。Sentinel 1.4.0 以前的版本需要自行調用 Tracer.trace(ex) 來記錄業務異常。
源碼分析

SentinelResourceAspect 是對 @SentinelResource的處理類

@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {

    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }

    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
        // 獲取當前訪問的方法
        Method originMethod = resolveMethod(pjp);
        // 獲取方法上的SentinelResource註解
        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            // Should not go through here.
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        }
        // 獲取資源名
        String resourceName = getResourceName(annotation.value(), originMethod);
        EntryType entryType = annotation.entryType();
        Entry entry = null;
        try {
            entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs());
            Object result = pjp.proceed();
            return result;
        } catch (BlockException ex) {
            // 處理被限制的異常,回調事先配置的異常處理方法
            return handleBlockException(pjp, annotation, ex);
        } catch (Throwable ex) {
            Tracer.trace(ex);
            throw ex;
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
    }
}
  • 使用aspect的around攔截,攔截標註有SentinelResource的註解
  • 進入方法之前調用SphU.entry(resourceName, entryType),結束之後調用entry.exit();
  • 異常的時候調用handleBlockException方法

handleBlockException

通過反射獲取到註解上配置的fallback方法

    private Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource annotation, BlockException ex)
        throws Exception {
        // Execute fallback for degrading if configured.
        Object[] originArgs = pjp.getArgs();
        if (isDegradeFailure(ex)) {
            Method method = extractFallbackMethod(pjp, annotation.fallback());
            if (method != null) {
                return method.invoke(pjp.getTarget(), originArgs);
            }
        }
        // Execute block handler if configured.
        Method blockHandler = extractBlockHandlerMethod(pjp, annotation.blockHandler(), annotation.blockHandlerClass());
        if (blockHandler != null) {
            // Construct args.
            Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
            args[args.length - 1] = ex;
            if (isStatic(blockHandler)) {
                return blockHandler.invoke(null, args);
            }
            return blockHandler.invoke(pjp.getTarget(), args);
        }
        // If no block handler is present, then directly throw the exception.
        throw ex;
    }

整合 Feign

  • 增加配置

    feign:
    sentinel:
    enabled: true
    
  • 創建限流處理類

    @Slf4j
    @Component
    public class UserFallBackFactoryFeign implements FallbackFactory<UserFeign> {
        @Override
        public UserFeign create(Throwable throwable) {
            return id -> {
                log.warn("限流或降級 id:{}", id, throwable);
                User user = new User();
                user.setName("默認用戶");
                return user;
            };
        }
    }
    
    @Slf4j
    @Component
    public class UserFallBackFeign implements UserFeign {
    
        @Override
        public User getUserById(Long id) {
            User user = new User();
            user.setName("默認用戶");
            return user;
        }
    }
    
  • feign 註解修改

    需要注意的是 fallbackFactory 和 fallback 只能存在一個。

    @FeignClient(value = "user",
            //fallbackFactory = UserFallBackFactoryFeign.class,
            fallback = UserFallBackFeign.class
    )
    public interface UserFeign {
        @GetMapping("/user/{id}")
        User getUserById(@PathVariable Long id);
    }
    
源碼分析

我們先分析下Feign的整個構造流程。

  • @EnableFeignClients 註解可以看到,入口在該註解上的 FeignClientsRegistrar 類上。

  • FeignClientsRegistrar實現了ImportBeanDefinitionRegistrar接口,用來了動態加載@FeignClient註解的接口並最終會被轉換成 FeignClientFactoryBean 這個 FactoryBeanFactoryBean內部的 getObject 方法最終會返回一個 Proxy。

  • 在構造 Proxy 的過程中會根據 org.springframework.cloud.openfeign.Targeter 接口的 target方法去構造。如果啓動了hystrix開關(feign.hystrix.enabled=true),會使用 HystrixTargeter,否則使用默認的 DefaultTargeter

  • Targeter 內部構造 Proxy 的過程中會使用 feign.Feign.Builder 去調用它的 build 方法構造 feign.Feign 實例(默認只有一個子類 ReflectiveFeign)。如果啓動了 hystrix 開關(feign.hystrix.enabled=true),會使用 feign.hystrix.HystrixFeign.Builder,否則使用默認的feign.Feign.Builder

  • 構造出 feign.Feign 實例之後,調用 newInstance 方法返回一個 Proxy

  • 簡單看下這個 newInstance 方法內部的邏輯:

        public <T> T newInstance(Target<T> target) {
            Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
            Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
            List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
            Method[] var5 = target.type().getMethods();
            int var6 = var5.length;
    
            for(int var7 = 0; var7 < var6; ++var7) {
                Method method = var5[var7];
                if (method.getDeclaringClass() != Object.class) {
                    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 = this.factory.create(target, methodToHandler);
            // 構造代理
            T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
            Iterator var12 = defaultMethodHandlers.iterator();
    
            while(var12.hasNext()) {
                DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
                defaultMethodHandler.bindTo(proxy);
            }
    
            return proxy;
        }
    

    這裏的 InvocationHandlerFactory 是通過構造 Feign 的時候傳入的:

    1)使用原生的 DefaultTargeter: 那麼會使用 feign.InvocationHandlerFactory.Default 這個 factory,並且構造出來的 InvocationHandlerfeign.ReflectiveFeign.FeignInvocationHandler

    2)使用 hystrixHystrixTargeter: 那麼會在feign.hystrix.HystrixFeign.Builder#build(feign.hystrix.FallbackFactory) 方法中調用父類的 invocationHandlerFactory方法傳入一個匿名的 InvocationHandlerFactory 實現類,該類內部構造出的 InvocationHandlerHystrixInvocationHandler

理解了 Feign 的執行過程之後,Sentinel 想要整合 Feign,可以參考 Hystrix 的實現:

  • 實現 Targeter 接口 SentinelTargeter。 很不幸,Targeter 這個接口屬於包級別的接口,在外部包中無法使用,這個 Targeter 無法使用。沒關係,我們可以沿用默認的HystrixTargeter(實際上會用DefaultTargeter)。
  • FeignClientFactoryBean 內部構造 Targeterfeign.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 方法設置 InvocationHandlerFactorycontract 的調用。
    • 跟 hystrix 一樣實現自定義的 InvocationHandler 接口 SentinelInvocationHandler 用來處理方法的調用。
    • 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 過程。

總結

  1. Feign 的內部很多類都是 package 級別的,外部 package 無法引用某些類,這個時候只能想辦法繞過去,比如使用反射

  2. 目前這種實現有風險,萬一哪天 starter 內部使用的 Feign 相關類變成了 package 級別,那麼會改造代碼。所以把 Sentinel 的實現放到 Feign 裏並給 Feign 官方提 pr 可能更加合適

  3. Feign的處理流程還是比較清晰的,只要能夠理解其設計原理,我們就能容易地整合進去

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章