This guide provides a comprehensive Java implementation for integrating with JUHE API using Spring Boot, featuring enterprise-grade error handling, resilience patterns, and best practices.
Prerequisites
- Java 11 or higher
- Maven or Gradle build tool
- Basic understanding of Spring Boot
- IDE such as IntelliJ IDEA or Eclipse
Project Setup
Maven Dependencies
Add the following dependencies to your pom.xml
:
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Jackson for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Resilience4j for circuit breaking -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.1</version>
</dependency>
<!-- Caffeine Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Lombok to reduce boilerplate -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Gradle Dependencies
Or if you're using Gradle, add to your build.gradle
:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.httpcomponents:httpclient:4.5.13'
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
implementation 'com.github.ben-manes.caffeine:caffeine'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
API Client Implementation
Model Classes
First, let's create the model classes for the responses:
// Location.java
package com.example.juhe.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class Location {
private String name;
private String country;
@JsonProperty("lat")
private double latitude;
@JsonProperty("lon")
private double longitude;
}
// WeatherCondition.java
package com.example.juhe.model;
import lombok.Data;
@Data
public class WeatherCondition {
private String text;
private String icon;
}
// CurrentWeather.java
package com.example.juhe.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class CurrentWeather {
@JsonProperty("temp_c")
private double temperatureCelsius;
@JsonProperty("temp_f")
private double temperatureFahrenheit;
private WeatherCondition condition;
private int humidity;
@JsonProperty("wind_kph")
private double windSpeedKph;
@JsonProperty("wind_mph")
private double windSpeedMph;
}
// WeatherResponse.java
package com.example.juhe.model;
import lombok.Data;
@Data
public class WeatherResponse {
private String status;
private WeatherData data;
}
@Data
class WeatherData {
private Location location;
private CurrentWeather current;
}
// ForecastDay.java
package com.example.juhe.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class ForecastDay {
private String date;
@JsonProperty("max_temp_c")
private double maxTemperatureCelsius;
@JsonProperty("min_temp_c")
private double minTemperatureCelsius;
private String condition;
@JsonProperty("chance_of_rain")
private int chanceOfRain;
}
// ForecastResponse.java
package com.example.juhe.model;
import lombok.Data;
import java.util.List;
@Data
public class ForecastResponse {
private String status;
private ForecastData data;
}
@Data
class ForecastData {
private Location location;
private List<ForecastDay> forecast;
}
API Exception Handling
Create a custom exception for API errors:
// JuheApiException.java
package com.example.juhe.exception;
import lombok.Getter;
@Getter
public class JuheApiException extends RuntimeException {
private final String errorCode;
private final int statusCode;
public JuheApiException(String message, String errorCode, int statusCode) {
super(message);
this.errorCode = errorCode;
this.statusCode = statusCode;
}
public JuheApiException(String message, String errorCode, int statusCode, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.statusCode = statusCode;
}
}
API Client
Now, let's implement the robust API client:
// JuheApiClient.java
package com.example.juhe.client;
import com.example.juhe.exception.JuheApiException;
import com.example.juhe.model.ForecastResponse;
import com.example.juhe.model.WeatherResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
@Component
@Slf4j
public class JuheApiClient {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final String apiKey;
private final String baseUrl;
public JuheApiClient(
ObjectMapper objectMapper,
@Value("${juhe.api.key}") String apiKey,
@Value("${juhe.api.base-url:<https://hub.juheapi.com>}") String baseUrl) {
this.objectMapper = objectMapper;
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.restTemplate = createRestTemplate();
}
private RestTemplate createRestTemplate() {
// Configure connection pooling
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(20);
// Configure timeouts
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(10000)
.build();
// Build HttpClient
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
.build();
// Create request factory with the HttpClient
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(requestFactory);
}
/**
* Make a GET request to the JUHE API.
*
* @param endpoint API endpoint
* @param params Query parameters
* @param responseType Response class
* @param <T> Response type
* @return API response
*/
@CircuitBreaker(name = "juheApi")
@Retry(name = "juheApi")
public <T> T get(String endpoint, Map<String, Object> params, Class<T> responseType) {
try {
// Add API key to parameters
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl + endpoint);
// Add query parameters
if (params != null) {
params.forEach(builder::queryParam);
}
// Add API key
builder.queryParam("key", apiKey);
// Set up headers
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
// Make the request
ResponseEntity<String> response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
new HttpEntity<>(headers),
String.class
);
// Check for error in response
String responseBody = response.getBody();
JsonNode root = objectMapper.readTree(responseBody);
if ("error".equals(root.path("status").asText())) {
String errorCode = root.path("code").asText("UNKNOWN_ERROR");
String errorMessage = root.path("message").asText("Unknown error occurred");
throw new JuheApiException(errorMessage, errorCode, response.getStatusCodeValue());
}
// Parse successful response
return objectMapper.readValue(responseBody, responseType);
} catch (HttpStatusCodeException e) {
// Handle HTTP errors
try {
JsonNode errorBody = objectMapper.readTree(e.getResponseBodyAsString());
String errorCode = errorBody.path("code").asText("HTTP_ERROR");
String errorMessage = errorBody.path("message").asText(e.getStatusText());
throw new JuheApiException(errorMessage, errorCode, e.getRawStatusCode(), e);
} catch (IOException ioException) {
throw new JuheApiException(e.getStatusText(), "HTTP_ERROR", e.getRawStatusCode(), e);
}
} catch (IOException e) {
// Handle JSON parsing errors
throw new JuheApiException("Error parsing API response", "PARSE_ERROR", 500, e);
} catch (JuheApiException e) {
// Rethrow API exceptions
throw e;
} catch (Exception e) {
// Handle other exceptions
throw new JuheApiException("Unexpected error: " + e.getMessage(), "UNEXPECTED_ERROR", 500, e);
}
}
/**
* Get current weather for a location.
*
* @param location Location name
* @return Weather response
*/
@Cacheable(value = "weatherCache", key = "#location")
public WeatherResponse getCurrentWeather(String location) {
log.info("Fetching current weather for location: {}", location);
return get("/weather/current", Collections.singletonMap("location", location), WeatherResponse.class);
}
/**
* Get weather forecast for a location.
*
* @param location Location name
* @param days Number of days (default: 5)
* @return Forecast response
*/
@Cacheable(value = "forecastCache", key = "#location + '-' + #days")
public ForecastResponse getWeatherForecast(String location, int days) {
log.info("Fetching {}-day forecast for location: {}", days, location);
Map<String, Object> params = Map.of(
"location", location,
"days", days
);
return get("/weather/forecast", params, ForecastResponse.class);
}
}
Service Layer
Let's create a service layer that uses our API client:
// WeatherService.java
package com.example.juhe.service;
import com.example.juhe.client.JuheApiClient;
import com.example.juhe.exception.JuheApiException;
import com.example.juhe.model.CurrentWeather;
import com.example.juhe.model.ForecastDay;
import com.example.juhe.model.ForecastResponse;
import com.example.juhe.model.WeatherResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class WeatherService {
private final JuheApiClient apiClient;
/**
* Get current weather for a location.
*
* @param location Location name
* @return Current weather data
*/
public CurrentWeather getCurrentWeather(String location) {
try {
WeatherResponse response = apiClient.getCurrentWeather(location);
return response.getData().getCurrent();
} catch (JuheApiException e) {
log.error("Error fetching current weather for {}: {} - {}",
location, e.getErrorCode(), e.getMessage());
throw e;
}
}
/**
* Get weather forecast for a location.
*
* @param location Location name
* @param days Number of forecast days
* @return List of forecast days
*/
public List<ForecastDay> getWeatherForecast(String location, int days) {
try {
ForecastResponse response = apiClient.getWeatherForecast(location, days);
return response.getData().getForecast();
} catch (JuheApiException e) {
log.error("Error fetching forecast for {}: {} - {}",
location, e.getErrorCode(), e.getMessage());
throw e;
}
}
}
Controller Layer
Now, let's create REST endpoints that use our service:
// WeatherController.java
package com.example.juhe.controller;
import com.example.juhe.exception.JuheApiException;
import com.example.juhe.model.CurrentWeather;
import com.example.juhe.model.ForecastDay;
import com.example.juhe.service.WeatherService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/weather")
@RequiredArgsConstructor
public class WeatherController {
private final WeatherService weatherService;
@GetMapping("/{location}")
public ResponseEntity<Object> getCurrentWeather(@PathVariable String location) {
try {
CurrentWeather weather = weatherService.getCurrentWeather(location);
return ResponseEntity.ok(Map.of(
"success", true,
"data", weather
));
} catch (JuheApiException e) {
return handleApiException(e);
}
}
@GetMapping("/{location}/forecast")
public ResponseEntity<Object> getWeatherForecast(
@PathVariable String location,
@RequestParam(defaultValue = "5") int days) {
try {
List<ForecastDay> forecast = weatherService.getWeatherForecast(location, days);
return ResponseEntity.ok(Map.of(
"success", true,
"data", forecast
));
} catch (JuheApiException e) {
return handleApiException(e);
}
}
private ResponseEntity<Object> handleApiException(JuheApiException e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("error", Map.of(
"code", e.getErrorCode(),
"message", e.getMessage()
));
return ResponseEntity
.status(e.getStatusCode() < 500 ? e.getStatusCode() : HttpStatus.INTERNAL_SERVER_ERROR)
.body(error);
}
}
Application Configuration
Let's configure the application:
// Application.java
package com.example.juhe;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class JuheApiApplication {
public static void main(String[] args) {
SpringApplication.run(JuheApiApplication.class, args);
}
}
// AppConfig.java
package com.example.juhe.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class AppConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(100));
return cacheManager;
}
}
Application Properties
Create an application.properties
file:
# API Configuration
juhe.api.key=${JUHE_API_KEY}
juhe.api.base-url=https://hub.juheapi.com
# Resilience4j Circuit Breaker Configuration
resilience4j.circuitbreaker.instances.juheApi.failureRateThreshold=50
resilience4j.circuitbreaker.instances.juheApi.minimumNumberOfCalls=5
resilience4j.circuitbreaker.instances.juheApi.automaticTransitionFromOpenToHalfOpenEnabled=true
resilience4j.circuitbreaker.instances.juheApi.waitDurationInOpenState=10s
# Resilience4j Retry Configuration
resilience4j.retry.instances.juheApi.maxAttempts=3
resilience4j.retry.instances.juheApi.waitDuration=1s
resilience4j.retry.instances.juheApi.retryExceptions=org.springframework.web.client.ResourceAccessException
Error Handling
Let's add a global exception handler:
// GlobalExceptionHandler.java
package com.example.juhe.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.Map;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(JuheApiException.class)
public ResponseEntity<Object> handleApiException(JuheApiException e) {
log.error("API Exception: {} - {}", e.getErrorCode(), e.getMessage());
return ResponseEntity
.status(determineHttpStatus(e))
.body(Map.of(
"success", false,
"error", Map.of(
"code", e.getErrorCode(),
"message", e.getMessage()
)
));
}
private HttpStatus determineHttpStatus(JuheApiException e) {
if (e.getStatusCode() >= 400 && e.getStatusCode() < 600) {
return HttpStatus.valueOf(e.getStatusCode());
}
// Map common error codes to appropriate HTTP statuses
return switch (e.getErrorCode()) {
case "INVALID_API_KEY" -> HttpStatus.UNAUTHORIZED;
case "RATE_LIMIT_EXCEEDED" -> HttpStatus.TOO_MANY_REQUESTS;
case "INVALID_PARAMETER" -> HttpStatus.BAD_REQUEST;
case "PARSE_ERROR" -> HttpStatus.UNPROCESSABLE_ENTITY;
default -> HttpStatus.INTERNAL_SERVER_ERROR;
};
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleGenericException(Exception e) {
log.error("Unexpected error: {}", e.getMessage(), e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"success", false,
"error", Map.of(
"code", "INTERNAL_ERROR",
"message", "An unexpected error occurred"
)
));
}
}
Running the Application
To run your application, set the JUHE_API_KEY
environment variable and start Spring Boot:
export JUHE_API_KEY=your_api_key
./mvnw spring-boot:run
Or with Gradle:
export JUHE_API_KEY=your_api_key
./gradlew bootRun
Testing the API
Let's create a simple test to verify our client works:
// JuheApiClientTest.java
package com.example.juhe.client;
import com.example.juhe.exception.JuheApiException;
import com.example.juhe.model.WeatherResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
public class JuheApiClientTest {
@Autowired
private JuheApiClient apiClient;
@Test
public void getCurrentWeather_validLocation_returnsWeatherData() {
// Given
String location = "London";
// When
WeatherResponse response = apiClient.getCurrentWeather(location);
// Then
assertNotNull(response);
assertEquals("success", response.getStatus());
assertNotNull(response.getData());
assertNotNull(response.getData().getLocation());
assertNotNull(response.getData().getCurrent());
assertEquals("London", response.getData().getLocation().getName());
}
@Test
public void getCurrentWeather_invalidLocation_throwsException() {
// Given
String invalidLocation = "NonExistentLocation123456789";
// When & Then
JuheApiException exception = assertThrows(
JuheApiException.class,
() -> apiClient.getCurrentWeather(invalidLocation)
);
assertEquals("LOCATION_NOT_FOUND", exception.getErrorCode());
}
}
Best Practices
- Connection Pooling: Efficient HTTP connection reuse
- Caching: Reduce API calls with appropriate caching
- Circuit Breaking: Prevent cascading failures
- Retry Logic: Automatically retry transient failures
- Error Handling: Comprehensive exception handling
- Logging: Detailed logging for debugging
- Configuration Externalization: API keys and URLs in properties
- Type Safety: Strongly typed DTOs for API responses
- Service Layer: Business logic separated from API calls
- Testing: Verify client behavior with unit tests
Next Steps
- Learn about JUHE API Features for more capabilities
- Explore Python Integration examples
- Visit Best Practices for more optimization tips