Skip to main content

Snippets Java / Spring Boot

Exemples pour intégrer l’API THE HIVE depuis un backend Java 21 / Spring Boot 3.x.

Flow général côté client Java

1. Configuration

# application.yml
hive:
  api:
    base-url: https://api.wethehivers.com/v1/api
    timeout-ms: 10000
  credentials:
    email: ${HIVE_EMAIL}
    password: ${HIVE_PASSWORD}
@ConfigurationProperties(prefix = "hive.api")
public record HiveApiProperties(String baseUrl, int timeoutMs) {}

2. RestTemplate (synchrone, simple)

@Configuration
public class HiveRestConfig {

    @Bean
    public RestTemplate hiveRestTemplate(HiveApiProperties props) {
        var factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(props.timeoutMs());
        factory.setReadTimeout(props.timeoutMs());
        return new RestTemplateBuilder()
            .rootUri(props.baseUrl())
            .requestFactory(() -> factory)
            .defaultHeader("Content-Type", "application/json")
            .build();
    }
}

Login

public record LoginRequest(String email, String password) {}
public record TokenPair(String accessToken, String refreshToken) {}

TokenPair tokens = restTemplate.postForObject(
    "/auth/login",
    new LoginRequest(email, password),
    TokenPair.class
);

Recherche d’offres

public record OffreSummary(Long id, String titre, String slug) {}
public record Page<T>(List<T> content, long totalElements, int totalPages) {}

ResponseEntity<Page<OffreSummary>> resp = restTemplate.exchange(
    "/offres/search?q={q}&typeContrat={t}&page=0&size=20",
    HttpMethod.GET,
    null,
    new ParameterizedTypeReference<>() {},
    "java", "CDI"
);

3. WebClient (réactif, non bloquant)

@Bean
public WebClient hiveWebClient(HiveApiProperties props) {
    var http = HttpClient.create()
        .responseTimeout(Duration.ofMillis(props.timeoutMs()));
    return WebClient.builder()
        .baseUrl(props.baseUrl())
        .clientConnector(new ReactorClientHttpConnector(http))
        .defaultHeader("Content-Type", "application/json")
        .build();
}

Appel authentifié

Mono<Page<OffreSummary>> page = webClient.get()
    .uri(uri -> uri.path("/offres/search")
        .queryParam("q", "java")
        .queryParam("page", 0)
        .queryParam("size", 20)
        .build())
    .header("Authorization", "Bearer " + token)
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError, r ->
        r.bodyToMono(ApiError.class).flatMap(e -> Mono.error(new HiveClientException(e))))
    .bodyToMono(new ParameterizedTypeReference<Page<OffreSummary>>() {});

4. TokenManager avec auto-refresh

@Component
public class HiveTokenManager {

    private final WebClient client;
    private volatile String accessToken;
    private volatile String refreshToken;
    private final Object lock = new Object();

    public HiveTokenManager(WebClient hiveWebClient) {
        this.client = hiveWebClient;
    }

    public String getValidAccess() {
        if (accessToken != null && !isExpired(accessToken)) return accessToken;
        synchronized (lock) {
            if (accessToken != null && !isExpired(accessToken)) return accessToken;
            return refresh();
        }
    }

    private String refresh() {
        var pair = client.post()
            .uri("/auth/refresh-token")
            .bodyValue(Map.of("refreshToken", refreshToken))
            .retrieve()
            .bodyToMono(TokenPair.class)
            .blockOptional(Duration.ofSeconds(10))
            .orElseThrow(() -> new IllegalStateException("Refresh failed"));
        this.accessToken = pair.accessToken();
        this.refreshToken = pair.refreshToken();
        return pair.accessToken();
    }

    private boolean isExpired(String jwt) {
        var parts = jwt.split("\\.");
        var payload = new String(Base64.getUrlDecoder().decode(parts[1]));
        var exp = JsonPath.read(payload, "$.exp");
        return Instant.now().getEpochSecond() >= ((Number) exp).longValue() - 30;
    }
}

5. Intercepteur WebClient (Authorization auto)

@Bean
public WebClient authedHiveWebClient(HiveApiProperties props, HiveTokenManager tm) {
    return WebClient.builder()
        .baseUrl(props.baseUrl())
        .filter((req, next) -> {
            var authed = ClientRequest.from(req)
                .header("Authorization", "Bearer " + tm.getValidAccess())
                .build();
            return next.exchange(authed);
        })
        .filter((req, next) -> next.exchange(req)
            .flatMap(resp -> {
                if (resp.statusCode().value() == 401) {
                    tm.forceRefresh();
                    var retry = ClientRequest.from(req)
                        .header("Authorization", "Bearer " + tm.getValidAccess())
                        .build();
                    return next.exchange(retry);
                }
                return Mono.just(resp);
            }))
        .build();
}

6. Retry avec backoff (rate limit 429)

import reactor.util.retry.Retry;

Mono<OffreDetail> offre = webClient.get()
    .uri("/offres/{id}", id)
    .retrieve()
    .bodyToMono(OffreDetail.class)
    .retryWhen(Retry.backoff(3, Duration.ofSeconds(2))
        .filter(ex -> ex instanceof WebClientResponseException.TooManyRequests)
        .onRetryExhaustedThrow((spec, signal) -> signal.failure()));
Version synchrone avec @Retryable :
@Retryable(
    retryFor = HttpClientErrorException.TooManyRequests.class,
    maxAttempts = 4,
    backoff = @Backoff(delay = 2000, multiplier = 2)
)
public OffreDetail fetchOffre(long id) {
    return restTemplate.getForObject("/offres/{id}", OffreDetail.class, id);
}

7. Upload multipart (CV)

public Long uploadCv(Path pdf) throws IOException {
    var body = new MultiValueMapAdapter<String, Object>(new LinkedHashMap<>());
    body.add("file", new FileSystemResource(pdf.toFile()));

    var headers = new HttpHeaders();
    headers.setContentType(MediaType.MULTIPART_FORM_DATA);
    headers.setBearerAuth(tm.getValidAccess());

    ResponseEntity<CvUploadResponse> resp = restTemplate.exchange(
        "/candidats/me/cv",
        HttpMethod.POST,
        new HttpEntity<>(body, headers),
        CvUploadResponse.class
    );
    return Objects.requireNonNull(resp.getBody()).cvId();
}

8. Types générés depuis OpenAPI

Via openapi-generator-maven-plugin :
<plugin>
  <groupId>org.openapitools</groupId>
  <artifactId>openapi-generator-maven-plugin</artifactId>
  <version>7.5.0</version>
  <executions>
    <execution>
      <goals><goal>generate</goal></goals>
      <configuration>
        <inputSpec>https://api.wethehivers.com/v3/api-docs</inputSpec>
        <generatorName>java</generatorName>
        <library>webclient</library>
        <apiPackage>com.example.hive.api</apiPackage>
        <modelPackage>com.example.hive.model</modelPackage>
        <configOptions>
          <useJakartaEe>true</useJakartaEe>
          <useBeanValidation>true</useBeanValidation>
          <openApiNullable>false</openApiNullable>
        </configOptions>
      </configuration>
    </execution>
  </executions>
</plugin>

9. Gestion d’erreurs typées

public record ApiError(int status, String message, Map<String, String> errors) {}

public class HiveClientException extends RuntimeException {
    private final ApiError error;
    public HiveClientException(ApiError e) {
        super("[" + e.status() + "] " + e.message());
        this.error = e;
    }
    public ApiError error() { return error; }
}

10. Circuit breaker (Resilience4j)

@CircuitBreaker(name = "hiveApi", fallbackMethod = "fallbackSearch")
@TimeLimiter(name = "hiveApi")
public CompletableFuture<Page<OffreSummary>> searchOffres(String q) {
    return webClient.get()
        .uri(b -> b.path("/offres/search").queryParam("q", q).build())
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<Page<OffreSummary>>() {})
        .toFuture();
}

public CompletableFuture<Page<OffreSummary>> fallbackSearch(String q, Throwable t) {
    return CompletableFuture.completedFuture(new Page<>(List.of(), 0, 0));
}
resilience4j:
  circuitbreaker:
    instances:
      hiveApi:
        slidingWindowSize: 20
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
  timelimiter:
    instances:
      hiveApi:
        timeoutDuration: 5s

11. Cache local (Caffeine)

@Cacheable(value = "offresSearch", key = "#q + ':' + #page")
public Page<OffreSummary> search(String q, int page) {
    return restTemplate.getForObject(
        "/offres/search?q={q}&page={p}&size=20",
        new ParameterizedTypeReference<Page<OffreSummary>>() {}.getClass(),
        q, page
    );
}
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=500,expireAfterWrite=30s

12. Test d’intégration (WireMock)

@SpringBootTest
@AutoConfigureWireMock(port = 0)
class HiveClientTest {

    @Autowired RestTemplate hiveRestTemplate;

    @Test
    void search_ok() {
        stubFor(get(urlPathEqualTo("/offres/search"))
            .willReturn(okJson("""
                {"content":[{"id":1,"titre":"Dev Java","slug":"dev-java"}],
                 "totalElements":1,"totalPages":1}
                """)));
        var page = hiveRestTemplate.getForObject(
            "/offres/search?q=java", Page.class);
        assertThat(page.totalElements()).isEqualTo(1);
    }
}

Voir aussi