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()));
@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
Viaopenapi-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);
}
}