İçeriğe geç

Feign Client — Kolaylığın Gizlediği Şeyler

Orta 10 dk Çok sık karşılaşılır

Önce şunu oku: BFF — Backend for Frontend

30 saniyede özet

Feign, başka bir servise yapılan HTTP çağrısını sıradan bir metot çağrısı gibi gösterir. Kolaylık gerçek, ama görünmeyen üç şey var: gecikme, yarım kalan hatalar ve çoğu zaman yanlış gelen varsayılan ayarlar.

Başka bir servisi çağırmak normalde otuz satır tekrar eden kod ister. Feign ile yalnızca bir arayüz yazarsın, çağrıyı yapan kodu Spring üretir. RestTemplate ile yazacağın otuz satırlık tekrar kaybolur.

  1. Bayt: Feign ile başka bir servisi çağırmak tek satır: priceClient.getPrice(id). Otuz satır gitti!

  2. Sen: Harika, yerel bir metot gibi.

  3. Bayt: Ama fiyat servisi yavaşlayınca bizim servis de kilitlendi...

  4. Bayt: Sekretere şunu sor demek kolay. Hat kopunca ne olacağını da bilmek gerekiyor.

Feign ne verir, ne vermez?

Tüm iş bu arayüzde
@FeignClient(name = "pricing")
interface PricingClient {
@GetMapping("/prices/{id}")
Price get(@PathVariable String id);
}

Gövde yazmazsın; Spring bu arayüzün uygulamasını üretir ve bean olarak verir. Kazanç tekrarın silinmesi: URL kurma, serileştirme, yanıt okuma tek yerde.

Vermediği şeyler de aynı ölçüde net: timeout, retry politikası, hata çevirimi, devre kesici. Bunların hiçbiri Feign’i eklemekle gelmez — hepsi ayrı ve açık kararlardır.

Kafam karıştı, daha basit anlat

Feign, başka bir servisi aramayı bir arayüz yazmak kadar kolaylaştırır. Adresi kurmak, isteği göndermek, cevabı okumak senin yerine yapılır.

Hızlı kontrolBaşlangıç

Feign client ne yapar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Feign ile `WebClient` arasındaki temel fark nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Yerel çağrı gibi görünür, değildir

pricing.get(id) satırına bak. Bir List.get(id) çağrısından hiçbir farkı yok.

Oysa arkasında servis keşfi, bağlantı kurma, ağ üzerinden istek, serileştirme ve karşı tarafın yükü var. İmza bunların hiçbirini söylemiyor — ve söylemediği için kod incelemesinde kimse sormuyor.

Kafam karıştı, daha basit anlat

Satır yan odaya seslenmek gibi görünür, ama aslında başka şehri telefonla aramaktır. Hat meşgul olabilir, geç cevap gelebilir. Kod bunu sana söylemez, sen hatırlamalısın.

Hızlı kontrolOrta

Feign'in kolaylığının getirdiği asıl risk nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Satır satır: aynı arayüz, üç sürpriz

Fiyat servisi bu id için 404 dönerse, senin API'n çağırana ne döner? Cevabı göster

Varsayılan hâlde 500. FeignException.NotFound bir RuntimeException’dır ve hiçbir yerde yakalanmazsa controller’dan dışarı sızar — çağıran “sunucu hatası” görür, oysa gerçek “bulunamadı”.

Metot gibi görünen şey aslında bir telefon hattı.
Adım adım oku
  1. priceClient.getPrice(id) kodda sıradan bir metot çağrısı gibi durur.
  2. Arkasında ise ağ üzerinden giden bir HTTP isteği, yani bir telefon hattı vardır.
  3. Hat takılabilir, karşı taraf meşgul çalabilir ya da bulunamadı diyebilir.
  4. Zaman aşımını, yalnızca güvenli çağrılar için yeniden denemeyi ve hataların doğru çevrilmesini sen ayarlarsın.

Varsayılanlarla ne oluyor?

ProductController.java
1@FeignClient(name = "pricing")
2interface PricingClient {
3 @GetMapping("/prices/{id}")
şu an çalışan satır Price get(@PathVariable String id);
5}
6
7@GetMapping("/products/{id}")
8ProductDto product(@PathVariable String id) {
9 return new ProductDto(catalogue.find(id), pricing.get(id));
10}

Debug

Adım 1/6

üretim Spring başlangıçta bu arayüzün uygulamasını üretir. Yazmadığın kod, hata ayıklarken de göremediğin kod.

yazdığın satır
= 1
çalışan katman
= keşif + HTTP + serileştirme
Java 21UTF-8LF4:1

Sol/sağ ok tuşlarıyla da gezebilirsin.

Son adım en kritik olanı: retry katmanlar boyunca toplanmaz, çarpılır. Zorlanan bir servise en çok yüklenen taraf, onu kurtarmaya çalışan çağıran olur.

Hızlı kontrolOrta

Üst servis 404 dönüyor ve senin API'n de 404 dönsün istiyorsun. Ne yaparsın?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Her davranışı, Feign'i eklemekle geldiğine mi yoksa açıkça yapılandırman gerektiğine göre ayır.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Feign ile gelir

Arayüzü yazınca hazır

    Açıkça yapılandırılır

    Yapmazsan varsayılan davranışa kalırsın

      Kendin gör

      Fiyat servisinin davranışını, metodu, timeout’ları, retry’ı ve dismiss404’ü değiştir. Üstte ayarların kod hâli, altta her denemenin thread’i ne kadar tuttuğu var.

      Feign çağrısı — varsayılan ayarlar neye mal oluyor?

      Tohum 856798
      PricingClient.java
      1@FeignClient(name = "pricing", configuration = PricingClientConfig.class)
      2interface PricingClient {
      3 @GetMapping("/prices/{id}")
      4 Optional<Price> get(@PathVariable String id);
      5}
      6
      7class PricingClientConfig {
      8 @Bean Request.Options options() {
      9 return new Request.Options(10000, MILLISECONDS, 60000, MILLISECONDS, true);
      10 }
      11 @Bean Retryer retryer() { return Retryer.NEVER_RETRY; }
      12}
      Java 21UTF-8LF

      Oynat veya adımla: pricing çağrısı başlayacak.

      Hız
      Adım 0

      Şu an ne oldu?

      GET çağrısı başlıyor

      Kod tek satır: pricing.get(id). Adımla ve bu tek satırın thread'i ne kadar tuttuğunu gör.

      Görevler0/4

      • Tek bir isteğin bir thread'i en az 60 saniye tutmasını sağlaaçık

        İpucu

        Varsayılan ayarlarla yavaş servisi çağır. Read timeout'u değiştirme.

      • Yavaş servise karşı 3 saniye içinde hata dönaçık

        İpucu

        Read timeout'u düşür ve retry'ı kapalı tut. Hızlı başarısızlık, yavaş başarısızlıktan iyidir.

      • 404'ü istisna değil Optional.empty() olarak alaçık

        İpucu

        Servisi "Bu id için 404" yap. Hangi ayar 404'ü boş bir değere çevirir?

      • Retry ile aynı POST'tan birden fazla rezervasyon oluşturaçık

        İpucu

        POST seç, Retryer.Default'u aç ve yavaş servisi kısa bir read timeout ile çağır.

      Olay günlüğü (0)

      Henüz olay yok. Oynat veya adımla.

      1. Varsayılanlarla oynat. Fiyat servisi yavaş, retry yok. Tek satırlık pricing.get(id) bir thread’i tam 60 saniye tutuyor ve sonunda istisna atıyor.
      2. Retryer.Default’u aç. Beş deneme ve aralarındaki beklemeler: aynı çağrı artık beş dakikadan uzun sürüyor. Spring Cloud OpenFeign’ın retry’ı varsayılan olarak kapatmasının sebebi bu.
      3. Read timeout’u 2 saniyeye indir, retry’ı kapat. Çağrı iki saniyede başarısız oluyor. fail fastBaşarısız olacağı belli olan bir işlemi beklemeden reddetmek. Breaker açıkken çağrının ağa hiç çıkmaması budur — çağıranın thread'i timeout boyunca tutulmaz.Sözlükte gör →, yavaş başarısızlıktan iyidir.
      4. POST seç ve retry’ı yeniden aç. Yavaş sunucu vazgeçilen her isteği yine de işliyor: tek bir rezervasyon isteği beş kayıt bırakıyor.
      5. Servisi 404’e çevir. Retry açık olsa da tek deneme yapılıyor, çünkü 404 bir ağ hatası değil. dismiss404 ile istisna yerine Optional.empty() dönüyor.
      Kafam karıştı, daha basit anlat

      Simülatörde bir çağrının thread’i ne kadar tuttuğuna bak. Timeout yoksa, yavaş bir servis senin thread’lerini de yavaş yavaş tüketir.

      Hızlı kontrolOrta

      Simülatörde fiyat servisi yavaş, read timeout varsayılan 60 sn ve Retryer.Default açık. Tek bir GET çağrısı thread'i ne kadar tutar?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Tuzaklar

      Sessiz fallback. Devre kesici fallback’i boş liste döndürüyorsa kullanıcı hatasız ama yanlış bir sayfa görür ve kimse alarm almaz. Fallback bir gizleme aracı değil, bilinçli bir azaltılmış hizmet kararıdır — ve her zaman metriğe yazılmalıdır.

      Retry’ı idempotent olmayan çağrıya koymak. POST /orders timeout aldığında siparişin oluşup oluşmadığını bilemezsin. idempotencyAynı işlemin bir kez de on kez de çalışsa aynı sonucu vermesi. Dağıtık sistemde timeout aldığında işlemin yapılıp yapılmadığını bilemediğin için zorunludur.Sözlükte gör → anahtarı yoksa retry, ikinci bir sipariş demektir.

      Sağlayıcının yayınladığı ortak client kütüphanesi kolay görünür: fiyat ekibi PricingClient’ı paketleyip yayınlar. Bedeli, tüketicilerin sağlayıcının sürüm takvimine bağlanması ve arayüzün sağlayıcının modeline göre şekillenmesidir. Arayüzü tüketici tanımlarsa bağımlılık oku doğru yöne bakar.

      Timeout bütçesini hesaba katmamak. timeout bütçesiBir zincirdeki timeout değerlerinin içten dışa artacak şekilde bölünmesi. Dıştaki içtekinden küçükse çağıran vazgeçer ama iş devam eder.Sözlükte gör → aşağı indikçe küçülmeli ve retry’lı toplam süre dıştaki bütçeyi aşmamalı. Aşarsa çağıran vazgeçer, senin çağrın devam eder.

      Aşağıdaki örnek bir bankanın transfer servisinden; hesap servisine Feign ile soruyor. Feign’in vermediği dört şey açıkça ekleniyor: istemci başına zaman aşımı, bilinçli retry kararı, hata çevirimi ve Feign’i domain’den saklayan bir adaptör. Test gerçek HTTP’yi WireMock ile taklit ediyor.

      Derinleş · Hesap servisi istemcisi: Feign'in etrafında ne var? 6 dosya · ~87 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/com/bank/transfer/accounts/ AccountClient.java Arayüz: tekrarı siler; ağın gerçeklerini (gecikme, hata) saklar.

      src/main/java/com/bank/transfer/accounts/AccountClient.java
      // The transfer service asks the account service: does this IBAN exist, is it open, whose is it?
      @FeignClient(name = "accounts", url = "${accounts.url}", configuration = AccountFeignConfig.class)
      interface AccountClient {
      @GetMapping("/accounts/{iban}")
      AccountDto get(@PathVariable String iban); // looks local; it is a network call
      }

      src/main/resources/ application.yml İstemci başına timeout ve log seviyesi. Retry kapalı; retry kararı tek bir katmanda verilir.

      src/main/resources/application.yml
      accounts:
      url: http://account-service
      spring:
      cloud:
      openfeign:
      client:
      config:
      accounts:
      connect-timeout: 500 # ms
      read-timeout: 1500 # ms, below the caller's own budget
      logger-level: basic
      dismiss404: true # 404 -> empty body instead of an exception

      src/main/java/com/bank/transfer/accounts/ AccountFeignConfig.java HTTP durumlarını anlamlı exception'lara çeviren ErrorDecoder.

      src/main/java/com/bank/transfer/accounts/AccountFeignConfig.java
      class AccountFeignConfig {
      // Not annotated @Configuration: it applies to this client only, not to every Feign client.
      @Bean
      Retryer retryer() {
      return Retryer.NEVER_RETRY; // retries live in one place (resilience4j), never in two
      }
      @Bean
      ErrorDecoder accountErrorDecoder() {
      return (methodKey, response) -> switch (response.status()) {
      case 400 -> new InvalidIbanException(methodKey);
      case 429, 503 -> new AccountsUnavailableException("accounts busy: " + response.status());
      default -> new ErrorDecoder.Default().decode(methodKey, response);
      };
      }
      }

      src/main/java/com/bank/transfer/accounts/ AccountAdapter.java Adaptör: domain Feign'i görmez; bilinmeyen IBAN (404) Optional.empty olur, kesinti anlamlı bir hataya.

      src/main/java/com/bank/transfer/accounts/AccountAdapter.java
      @Component
      class AccountAdapter implements AccountLookup { // AccountLookup is the domain's own port
      private final AccountClient client;
      AccountAdapter(AccountClient client) {
      this.client = client;
      }
      @Override
      public Optional<Beneficiary> beneficiaryOf(Iban iban) {
      try {
      AccountDto dto = client.get(iban.value()); // null body when 404 (dismiss404)
      return Optional.ofNullable(dto).map(AccountDto::toBeneficiary);
      } catch (RetryableException ex) { // connect/read timeout
      throw new AccountsUnavailableException("accounts timed out", ex);
      }
      }
      }

      src/main/java/com/bank/transfer/ TransferApplication.java Feign istemcilerini açan satır.

      src/main/java/com/bank/transfer/TransferApplication.java
      @SpringBootApplication
      @EnableFeignClients
      public class TransferApplication {
      public static void main(String[] args) {
      SpringApplication.run(TransferApplication.class, args);
      }
      }

      src/test/java/com/bank/transfer/accounts/ AccountAdapterTest.java WireMock: yavaş ve hatalı sunucuyu testte üretmek.

      src/test/java/com/bank/transfer/accounts/AccountAdapterTest.java
      @SpringBootTest(properties = "accounts.url=http://localhost:${wiremock.server.port}")
      @AutoConfigureWireMock(port = 0)
      class AccountAdapterTest {
      private static final String IBAN = "TR330006100519786457841326";
      @Autowired AccountLookup accounts;
      @Test
      void unknownIbanIsEmpty() {
      stubFor(get("/accounts/" + IBAN).willReturn(notFound()));
      assertThat(accounts.beneficiaryOf(new Iban(IBAN))).isEmpty();
      }
      @Test
      void slowServerFailsFastWithADomainError() {
      stubFor(get("/accounts/" + IBAN)
      .willReturn(okJson("{\"name\":\"Ayşe Yılmaz\",\"status\":\"OPEN\"}").withFixedDelay(3_000)));
      assertThatThrownBy(() -> accounts.beneficiaryOf(new Iban(IBAN)))
      .isInstanceOf(AccountsUnavailableException.class); // after 1.5 s, not 3 s
      }
      }

      Kendini sına

      Şimşek turu1/5

      Feign çağrısı arka planda bir HTTP isteğidir.

      Soru 1/4Orta

      Fiyat servisi bu id için 404 dönüyor. Senin API'n çağırana ne döner?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
      ProductController.java
      1@FeignClient(name = "pricing")
      2interface PricingClient {
      3 @GetMapping("/prices/{id}")
      4 Price get(@PathVariable String id);
      5}
      6
      7@GetMapping("/products/{id}")
      8ProductDto product(@PathVariable String id) {
      9 return new ProductDto(catalogue.find(id), pricing.get(id));
      10}
      11
      12// pricing servisi bu id icin 404 donuyor
      Java 21UTF-8LF

      Aklında kalacak üç şey

      1. 1 Feign'in tek işi tekrarı silmektir: arayüzü sen yazarsın, çağrıyı yapan kodu Spring üretir. Sağlamlıkla ilgili hiçbir şey getirmez.
      2. 2 Asıl risk görünüşte: pricing.get(id) yerel bir çağrıya benzediği için gecikme, zaman aşımı ve hata ihtimali gözden kaçar.
      3. 3 Tekrar denemeler katmanlar boyunca çarpılır. Feign, yük dengeleyici ve resilience4j ayrı ayrı denerse tek bir istek düzinelerce isteğe dönüşür.
      Sonraki kapı Bir servisin kopyalarından biri ölünce trafiği kim, nasıl öteki kopyalara yönlendiriyor? API Gateway ve Service Discovery — İstek Doğru Kapıyı Nasıl Bulur · 9 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.