Tworzenie deklaratywnych klientów HTTP z wykorzystaniem Feign

Michał Świeży

Istnieje wiele powodów, dla których architektura oparta na mikrousługach cieszy się coraz większym zainteresowaniem, takich jak separacja domen, łatwość utrzymania, gotowość wdrożenia do „chmury”, testowalność itp. Istnieją również pewne wyzwania związane z tym sposobem tworzenia oprogramowania, takie jak komunikacja między rozproszonymi mikroserwisami. Jednym z najpopularniejszych sposobów na umożliwienie im interakcji ze sobą jest nadal dobre, stare i proste zapytanie HTTP. W takim przypadku musimy wybrać, jak to zrobić.

Jest tu kilka opcji – zaczynając od starego zwykłego HttpUrlConnection, poprzez ApacheHttpClient, odświeżony HttpClient, OkHttp, Spring’s RestTemplate lub nowszy asynchroniczny WebClient – zwykle im nowsze rozwiązanie, tym wygodniejsze jest w użyciu, ukrywając i automatyzując część kodu powtarzalnego.

Imperatywnie vs Deklaratywnie

Wymienione wyżej implementacje klientów są wciąż rozwiązaniami imperatywnymi, przez co nadal musimy określić JAK nasze zapytanie zostanie wykonane. Spójrzmy na przykład (OkHttp):

public class UsersAPI {
    // (...)
    public List<User> getUsers() throws Exception {
        final ObjectMapper mapper = new ObjectMapper();
        final OkHttpClient client = new OkHttpClient();
        final Request request = new Request.Builder()
                .url("http://localhost:8080/")
                .get()
                .build();
        final TypeReference<List<User>> collectionType = new TypeReference<List<User>>() {};
        try (Response response = client.newCall(request).execute()) {
            return response.body() != null ? mapper.readValue(response.body().byteStream(), collectionType) : null;
        }
    }

    public User getUser(final Long userId) {
        // ...
    }
    // (...)
}

Od razu rzuca się w oczy, że powyższy kod jest powtarzalny i będzie wyglądał podobnie w pozostałych operacjach CRUD. Aby przestrzegać zasady DRY z pewnością moglibyśmy znaleźć część wspólną i ją wyodrębnić, ale nadal byłoby to trochę kłopotliwe. Spójrzmy natomiast na to:

public interface UsersAPI {
    // (...)
    @RequestLine("GET")
    List<User> getUsers();

    @RequestLine("GET /{userId}")
    User getUser(@Param("userId") final Long userId);
    // (...)
}

W tym wypadków mówimy jedynie CO powinno zostać zrobione – jest to deklaratywny kod, jak w przypadku Springowego JPA repository.

Feign

No dobra, chwileczkę, jest fajnie, ale to tylko interfejs, musimy jeszcze napisać implementację! Ponieważ ten interfejs jest dobrze opisany, nie musimy jej pisać sami. Aby nasz klient był gotowy do współpracy, wystarczy, że pozwolimy bibliotece Feign stworzyć dla nas implementację:

final UsersAPI usersAPIClient = Feign.builder()
    .client(new OkHttpClient())
    .encoder(new JacksonEncoder())
    .decoder(new JacksonDecoder())
    .logger(new Slf4jLogger(UsersRestClient.class))
    .target(User.class, "http://localhost:8080");

Abyśmy byli w stanie to zrobić potrzebujemy kilka zależności:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-slf4j</artifactId>
</dependency>

Jak widać, podpowiadamy Feignowi, czego ma użyć, aby stworzyć dla nas implementację. W tym przykładzie, pod maską, Feign użyje tego samego klienta OkHttp, którego używaliśmy wcześniej ręcznie, a także zdekoduje treść odpowiedzi za pomocą Jacksona. Następnie utworzona implementacja jest gotowa do użycia o tak:

final List<User> allUsers = usersAPIClient.getUsers();

Możemy wybrać, co ma być używane przez Feign jako klient HTTP, body encoder/dekoder czy nawet logger, ale musimy pamiętać o posiadaniu niezbędnych zależności. Dodatkowo możemy nawet sami napisać odpowiednie implementacje – Feign jest w tym aspekcie bardzo elastyczny.

A co ze Springiem?

Używanie Feign ze Springiem jest jeszcze łatwiejsze, ponieważ jego wersja Spring Cloud dodaje automatyczną konfigurację Bean’ów i obsługę standardowych adnotacji Spring MVC. Aby skorzystać z tych udogodnień, musimy dodać zależność od Spring Cloud OpenFeign:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Teraz nasz interfejs klienta HTTP będzie wyglądał następująco:

@FeignClient(name = "user-server", url = "${user-server.url:http://localhost:8080}")
public interface UsersAPI {
    // (...)
    @GetMapping
    List<User> getUsers();

    @GetMapping("/{userId}")
    User getUser(@PathVariable("userId") Long userId);
    // (...)
}

Musimy również pamiętać o włączeniu wsparcia interfejsów Feign w naszej Springowej aplikacji:

@SpringBootApplication
@EnableFeignClients(basePackages = "pl.jlabs.example.feign.client")
public class UserThinWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserThinWebApplication.class, args);
    }
}

Chociaż używamy standardowych adnotacji Spring MVC, naprawdę wygodnie jest używać interfejsu z adnotacjami @FeignClient jako wspólnego interfejsu zarówno dla kontrolera po stronie serwera, jak i klienta generowanego automatycznie przez OpenFeign, aby zapewnić ich spójność.

Personalizacja OpenFeign

Aby być tak zwięzłym i łatwym w użyciu, jak to tylko możliwe, OpenFeign w aplikacjach Springowych tworzy klientów przy użyciu niektórych domyślnych Beanów, ale oczywiście istnieje możliwość ich dostosowania. OpenFeign jest tak elastyczny i istnieje tak wiele możliwych sposobów na dostosowanie jego zachowania, że nawet próba wymienienia ich wszystkich wykraczałaby poza zakres tego krótkiego artykułu – dokumentacja OpenFeign jest kompleksowym źródłem informacji na ten temat.

Czy może być asynchroniczny?

Obecnie (stan na kwiecień 2023 r.) Feign obsługuje tylko synchroniczne zapytania http, więc jeśli chcesz wykorzystać korzyści płynące z asynchroniczności, musisz użyć czegoś innego (lub ręcznie opakować to w coś takiego jak CompletableFuture, co okaże się mniej zwięzłe). Na szczęście istnieją alternatywy – można użyć np. ReactiveFeign lub nowiutkiego @HttpExchange ze Springa 6. Rzućmy okiem na Reactive Feign, który jest bardzo podobny do swojego synchronicznego poprzednika. Najpierw pobierz zależności:

<dependency>
    <groupId>com.playtika.reactivefeign</groupId>
    <artifactId>feign-reactor-spring-cloud-starter</artifactId>
</dependency>

nasz klient będzie wyglądał następująco:

@ReactiveFeignClient(name = "user-server", url = "${user-server.url:http://localhost:8080}")
public interface UsersAPI {
    // (...)
    @GetMapping
    Flux<User> getUsers();

    @GetMapping("/{userId}")
    Mono<User> getUser(@PathVariable("userId") final Long userId);
    // (...)
}

Tutaj również musimy pamiętać o włączeniu wsparcia interfejsów, tym razem Reactive Feign w naszej Springowej aplikacji:

@SpringBootApplication
@EnableReactiveFeignClients
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Teraz jesteśmy gotowi aby używać deklaratywnego asynchronicznego klienta HTTP!

Integracja

Spring Cloud OpenFeign bardzo dobrze integruje się z innymi komponentami Spring Cloud, takimi jak CircuitBreaker, ServiceDiscovery czy LoadBalancer. Więcej szczegółów na temat możliwych integracji można znaleźć w dokumentacji Spring Cloud.

Podsumowanie

Dzięki programowaniu deklaratywnemu jesteśmy w stanie wyabstrahować powtarzające się, szablonowe implementacje, a w rezultacie uzyskać czystszą i bardziej zwięzłą bazę kodu – po prostu łatwiejszą do rozwoju i utrzymania. W przypadku operacji bazodanowych Spring Data JPA jest obecny od lat i już udowodnił swoją skuteczność. Moim skromnym zdaniem, deklaratywni klienci HTTP mogą pójść tą samą drogą – nawet współtwórcy Springa wprowadzili deklaratywny @HttpExchange wraz ze Springiem 6. OpenFeign jest dojrzałą i dobrze udokumentowaną implementacją deklaratywnego klienta HTTP i myślę, że warto spróbować go w swoich obecnych i przyszłych projektach.

Kompletny przykład użycia OpenFeign można znaleźć na stronie Github.

Odniesienia

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami