Tworzenie deklaratywnych klientów HTTP z wykorzystaniem Feign

Michał Świeży

Jest wiele powodów, dla których architektura oparta na mikrousługach cieszy się coraz większym zainteresowaniem. Są to m.in. możliwość separacji domen, łatwość utrzymania, gotowość wdrożenia do chmury czy testowalność. Z tym sposobem tworzenia oprogramowania wiążą się jednak również pewne wyzwania, takie jak komunikacja między rozproszonymi mikroserwisami. Jak umożliwić im interakcję? Jednym z najpopularniejszych sposobów jest stare, ale nadal dobre i proste zapytanie HTTP.

Jakie rozwiązanie wybrać wykorzystując zapytanie HTTP?

Jest kilka opcji, aby wykorzystać zapytanie HTTP – zaczynając od starego HttpUrlConnection, poprzez ApacheHttpClient czy odświeżony HttpClient, OkHttp, Spring’s RestTemplate, aż po nowszy asynchroniczny WebClient. Zwykle im świeższe rozwiązanie, tym jest ono wygodniejsze w użyciu, ponieważ ukrywa i automatyzuje część kodu powtarzalnego.

Imperatywnie vs. deklaratywnie

Wymienione wyżej implementacje klientów są wciąż rozwiązaniami imperatywnymi. Oznacza to, że nadal musimy określić, jak zostanie wykonane nasze zapytanie. Spójrz 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ć. Nadal byłoby to jednak trochę kłopotliwe. Spójrz natomiast na to:

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

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

W tym przypadku mówimy jedynie, co powinno zostać zrobione. Jest to deklaratywny kod jak w przypadku springowego JPA repository.

Feign

No dobrze, jest fajnie, ale to tylko interfejs. Musimy jeszcze napisać implementację! Ponieważ jest on dobrze opisany, nie musimy jej tworzyć sami. Aby nasz klient był gotowy do współpracy, wystarczy, że pozwolimy działać bibliotece Feign. Zobacz:

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 było to możliwe, potrzebujemy kilku 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ć, Feign uzyskuje podpowiedź, czego ma użyć, aby stworzyć dla nas implementację. W tym przykładzie, pod maską, wykorzysta on tego samego klienta OkHttp, którego używaliśmy wcześniej ręcznie. Zdekoduje on też treść odpowiedzi z pomocą Jacksona. Utworzona implementacja jest gotowa do użycia. Jak? O tak:

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

Możesz wybrać, czego Feign ma użyć jako klienta HTTP, body encodera/dekodera czy nawet loggera. Musisz jednak pamiętać o posiadaniu niezbędnych zależności. Dodatkowo możesz nawet samodzielnie napisać odpowiednie implementacje, ponieważ Feign jest w tym aspekcie bardzo elastyczny.

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ń, musisz dodać zależność od Spring Cloud OpenFeign. Jak to zrobić? Spójrz:

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

Teraz 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);
    // (...)
}

Musisz również pamiętać o włączeniu wsparcia interfejsów Feign w 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ż korzystamy ze 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. Pozwoli to 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. Oczywiście istnieje też możliwość ich dostosowania. OpenFeign jest tak elastyczny i istnieje tak wiele sposobów na dostosowanie jego zachowania, że nawet próba wymienienia ich wszystkich wykraczałaby poza zakres tego krótkiego artykułu. Kompleksowym źródłem informacji na ten temat jest dokumentacja OpenFeign.

Czy może być asynchroniczny?

Obecnie (stan na kwiecień 2023 r.) Feign obsługuje tylko synchroniczne zapytania HTTP. Jeśli więc 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żesz 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ż musisz 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);
    }
}

Wszystko gotowe, 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żesz znaleźć w dokumentacji Spring Cloud.

Podsumowanie

Dzięki programowaniu deklaratywnemu jesteśmy w stanie wyabstrahować powtarzające się, szablonowe implementacje. W rezultacie uzyskuje się 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 wypróbować go w swoich obecnych i przyszłych projektach.

Kompletny przykład użycia OpenFeign znajdziesz na stronie Github.

Źródła

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

Skontaktuj się z nami