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