Feignクライアントによるマイクロサービス呼び出しとサーキットブレーカーの内部メカニズム

Feignは宣言型のHTTPクライアントとして設計されており、マイクロサービス間通信を簡略化します。Spring CloudではRibbonとEurekaとの統合により、負荷分散を実現したHTTPクライアントとして利用可能です。

環境設定:

<spring-boot.version>2.3.2.RELEASE</spring-boot.version><br></br><spring-cloud.version>Hoxton.SR9</spring-cloud.version><br></br><spring-cloud-openfeign.version>2.2.6.RELEASE</spring-cloud-openfeign.version>

本記事ではNacosをサービスディスカバリ、Sentinelをサーキットブレーカーとして使用します。 事前準備については「SpringCloudのサーキットブレーカー実装(第6章)」をご参照ください。 主要アノテーションは@EnableFeignClientsとFeignClientです。

マイクロサービス呼び出し時に@EnableFeignClientsを設定し、FeignClientで具体的なインターフェースを定義します。

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class NacosConsumerApplication {
    ......
    public static void main(String[] args) {
        SpringApplication.run(NacosConsumerApplication.class, args);
    }
    ......
}
@FeignClient(value = "service-provider", fallback = ServiceClientFailback.class)
public interface ServiceClient {
    @GetMapping("/echo/{param}")
    String echo(@PathVariable("param") String param);

    @GetMapping("/echo1/{param}")
    String echo1(@PathVariable("param") String param);
}

@EnableFeignClientsの定義にはFeignClientsRegistrarがインポートされています。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    ......
}

FeignClientsRegistrarはImportBeanDefinitionRegistrarを継承しており、初期化時にregisterFeignClientsメソッドが呼ばれます。このメソッドはFeignClientアノテーション付きインターフェースをスキャンし、コンテナに登録します。生成オブジェクトのファクトリとしてFeignClientFactoryBeanが使用されます。

public void registerFeignClients(AnnotationMetadata metadata,
        BeanDefinitionRegistry registry) {
    ......
    if (clients == null || clients.length == 0) {
        //FeignClientアノテーション付きインターフェースのスキャン
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<String> basePackages = getBasePackages(metadata);
        for (String basePackage : basePackages) {
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }
    ......

    for (BeanDefinition candidateComponent : candidateComponents) {
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            ......
            //Beanファクトリの設定
            registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

private void registerFeignClient(BeanDefinitionRegistry registry,
        AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    //FeignClientFactoryBeanを生成オブジェクトのファクトリとして指定
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
            .genericBeanDefinition(FeignClientFactoryBean.class);
    ......
}

Spring Cloud FeignClientはSpringのプロキシファクトリを利用して動的に代理クラスを生成します。ここではすべてのFeignClientのBeanDefinitionをFeignClientFactoryBean型に設定しています。このクラスはFactoryBeanを継承しており、代理オブジェクトの生成を担います。スキャン処理はここで終了します。

プロキシオブジェクトの生成はSpringコンテナの初期化時に発生します。アプリケーションのサービスインスタンス化時にFeignClientが検出されると、プロキシBeanとして処理され、FeignClientFactoryBean.getObject()メソッドが呼ばれて代理オブジェクトが生成されます。

FeignClientFactoryBeanの内部ロジックを分析すると、getTarget()メソッドが呼ばれます。

<T> T getTarget() {
    FeignContext context = applicationContext.getBean(FeignContext.class);
    //デフォルトはHystrixFeign.Builder、Sentinel使用時はSentinelFeign.Builder
    Feign.Builder builder = feign(context);

    if (!StringUtils.hasText(url)) {
        if (!name.startsWith("http")) {
            url = "http://" + name;
        }
        else {
            url = name;
        }
        url += cleanPath();
        //プロキシオブジェクトの返却
        return (T) loadBalance(builder, context,
                new HardCodedTarget<>(type, name, url));
    }
    ......
}

Feign.Builderの取得処理:

protected Feign.Builder feign(FeignContext context) {
    FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
    Logger logger = loggerFactory.create(type);

    // @formatter:off
    //SentinelFeignAutoConfigurationでFeign.Builderが注入される
    Feign.Builder builder = get(context, Feign.Builder.class)
            // 必須コンポーネントの設定
            .logger(logger)
            .encoder(get(context, Encoder.class))
            .decoder(get(context, Decoder.class))
            .contract(get(context, Contract.class));
    // @formatter:on

    configureFeign(context, builder);

    return builder;
}

SentinelFeign.Builderの注入はapplication.propertiesの設定に依存します。

feign.sentinel.enabled=true

SentinelFeignAutoConfigurationでの注入処理:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class, Feign.class })
public class SentinelFeignAutoConfiguration {

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    @ConditionalOnProperty(name = "feign.sentinel.enabled")
    public Feign.Builder feignSentinelBuilder() {
        return SentinelFeign.builder();
    }

}

loadBalance処理の続き:

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
        HardCodedTarget<T> target) {
    //デフォルトはClient.Default、OkHttpClientやApacheHttpClientも設定可能
    Client client = getOptional(context, Client.class);
    if (client != null) {
        builder.client(client);
        //HystrixTargeterの使用
        Targeter targeter = get(context, Targeter.class);
        return targeter.target(this, builder, context, target);
    }
    ......
}

HystrixTargeter#targetの処理:

public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
        FeignContext context, Target.HardCodedTarget<T> target) {
    if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
        //SentinelFeign.Builder使用時はここが実行される
        return feign.target(target);
    }
    ......
}

ReflectiveFeign#newInstanceの処理:

  public <T> T newInstance(Target<T> target) {
    //メソッドとハンドラーのマップ生成
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    
    ......
    
    //SentinelInvocationHandlerの生成
    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;
  }

SentinelInvocationHandlerの生成処理:

public Feign build() {
    super.invocationHandlerFactory(new InvocationHandlerFactory() {
        @Override
        public InvocationHandler create(Target target,
                Map<Method, MethodHandler> dispatch) {
            ......

            //エラーハンドリングの設定、FallbackFactoryを使用
            Object failbackInstance;
            FallbackFactory failbackFactoryInstance;
            if (void.class != fallback) {
                failbackInstance = getFromContext(beanName, "fallback", fallback,
                        target.type());
                return new SentinelInvocationHandler(target, dispatch,
                        new FallbackFactory.Default(failbackInstance));
            }
            if (void.class != fallbackFactory) {
                failbackFactoryInstance = (FallbackFactory) getFromContext(
                        beanName, "fallbackFactory", fallbackFactory,
                        FallbackFactory.class);
                return new SentinelInvocationHandler(target, dispatch,
                        failbackFactoryInstance);
            }
            return new SentinelInvocationHandler(target, dispatch);
        }

        ......
    });

    super.contract(new SentinelContractHolder(contract));
    return super.build();
}

最終的に生成されたプロキシオブジェクトはSentinelInvocationHandlerを介して動作します。

invokeメソッドの処理:

public Object invoke(final Object proxy, final Method method, final Object[] args)
        throws Throwable {
    ......

    Object result;
    //メソッドハンドラーの取得
    MethodHandler methodHandler = this.dispatch.get(method);
    if (target instanceof Target.HardCodedTarget) {
        Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target;
        MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
                .get(hardCodedTarget.type().getName()
                        + Feign.configKey(hardCodedTarget.type(), method));
        if (methodMetadata == null) {
            result = methodHandler.invoke(args);
        }
        else {
            String resourceName = methodMetadata.template().method().toUpperCase()
                    + ":" + hardCodedTarget.url() + methodMetadata.template().path();
            Entry entry = null;
            try {
                ContextUtil.enter(resourceName);
                entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
                //メソッド実行
                result = methodHandler.invoke(args);
            }
            catch (Throwable ex) {
                //サーキットブレーカー処理
                if (!BlockException.isBlockException(ex)) {
                    Tracer.trace(ex);
                }
                //FeignClientのfallback設定を元にハンドリング
                if (fallbackFactory != null) {
                    try {
                        Object fallbackResult = fallbackMethodMap.get(method)
                                .invoke(fallbackFactory.create(ex), args);
                        return fallbackResult;
                    }
                    catch (IllegalAccessException e) {
                        throw new AssertionError(e);
                    }
                    catch (InvocationTargetException e) {
                        throw new AssertionError(e.getCause());
                    }
                }
                ......
            }
            ......
        }
    }
    else {
        result = methodHandler.invoke(args);
    }

    return result;
}

要約

FeignClientによるマイクロサービス呼び出しは、自動的に生成されたプロキシオブジェクトを通じて実現され、アクセス方法や例外処理のカスタマイズが可能です。

タグ: SpringCloud Feign マイクロサービス サーキットブレーカー Sentinel

5月22日 12:11 投稿