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的处理流程还是比较清晰的,只要能够理解其设计原理,我们就能容易地整合进去

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