Dlaczego warto tworzyć własne Spring Boot Startery?
W enterprise środowiskach często używamy tych samych bibliotek i konfiguracji w wielu projektach. Bez własnych starterów programiści muszą kopiować te same konfiguracje, co prowadzi do duplikacji kodu i błędów. Spring Boot Starter rozwiązuje ten problem poprzez enkapsulację konfiguracji w reuzywalnym module.
Własny starter pozwala zespołom na standaryzację sposobu integracji z zewnętrznymi systemami, bazami danych czy narzędziami monitoringu. Zamiast dokumentować „jak skonfigurować bibliotekę X”, po prostu dodajesz dependency i wszystko działa out-of-the-box.
Co się nauczysz:
- Jak stworzyć strukturę Spring Boot Starter od podstaw
- Implementację auto-konfiguracji z warunkami
- Zarządzanie properties i zewnętrzną konfiguracją
- Best practices dla API design starterów
- Testowanie i publikowanie własnych starterów
Anatomia Spring Boot Starter
Typowy starter składa się z:
**Auto-Configuration** – klasy `@Configuration` z warunkami określającymi kiedy konfiguracja ma być aktywna
**Properties** – klasy `@ConfigurationProperties` do externalizacji konfiguracji
**META-INF/spring.factories** – plik rejestrujący auto-konfigurację w Spring Boot
**Dependencies** – transitive dependencies potrzebne do działania
Tworzenie Custom Starter – Praktyczny Przykład
Stworzymy starter dla integracji z zewnętrznym API klientów. Nazwijmy go `customer-api-spring-boot-starter`.
1. Struktura projektu
customer-api-spring-boot-starter/ ├── pom.xml ├── src/main/java/ │ └── com/company/starter/customerapi/ │ ├── CustomerApiAutoConfiguration.java │ ├── CustomerApiProperties.java │ ├── CustomerApiClient.java │ └── CustomerApiHealthIndicator.java ├── src/main/resources/ │ └── META-INF/ │ └── spring.factories └── src/test/java/ └── com/company/starter/customerapi/ └── CustomerApiAutoConfigurationTest.java
2. Pom.xml – Dependencies
4.0.0 com.company.starter customer-api-spring-boot-starter 1.0.0 jar Customer API Spring Boot Starter Spring Boot Starter for Customer API integration 1.8 2.1.1.RELEASE org.springframework.boot spring-boot-autoconfigure ${spring-boot.version} org.springframework.boot spring-boot-configuration-processor ${spring-boot.version} true org.springframework spring-web 5.1.3.RELEASE com.fasterxml.jackson.core jackson-databind 2.9.7 org.springframework.boot spring-boot-actuator ${spring-boot.version} true org.springframework.boot spring-boot-starter-test ${spring-boot.version} test
3. Configuration Properties
@ConfigurationProperties(prefix = "customer.api") public class CustomerApiProperties { /** * Base URL for Customer API */ private String baseUrl = "https://api.customers.com"; /** * API key for authentication */ private String apiKey; /** * Connection timeout in milliseconds */ private int connectTimeout = 5000; /** * Read timeout in milliseconds */ private int readTimeout = 10000; /** * Enable retry mechanism */ private boolean retryEnabled = true; /** * Maximum number of retry attempts */ private int maxRetries = 3; /** * Health check configuration */ private HealthCheck healthCheck = new HealthCheck(); public static class HealthCheck { /** * Enable health check endpoint */ private boolean enabled = true; /** * Health check endpoint path */ private String endpoint = "/health"; // getters and setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getEndpoint() { return endpoint; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } } // getters and setters public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public int getConnectTimeout() { return connectTimeout; } public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; } public int getReadTimeout() { return readTimeout; } public void setReadTimeout(int readTimeout) { this.readTimeout = readTimeout; } public boolean isRetryEnabled() { return retryEnabled; } public void setRetryEnabled(boolean retryEnabled) { this.retryEnabled = retryEnabled; } public int getMaxRetries() { return maxRetries; } public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } public HealthCheck getHealthCheck() { return healthCheck; } public void setHealthCheck(HealthCheck healthCheck) { this.healthCheck = healthCheck; } }
4. API Client Implementation
public class CustomerApiClient { private final RestTemplate restTemplate; private final CustomerApiProperties properties; private final ObjectMapper objectMapper; public CustomerApiClient(CustomerApiProperties properties) { this.properties = properties; this.objectMapper = new ObjectMapper(); this.restTemplate = createRestTemplate(); } private RestTemplate createRestTemplate() { RestTemplate template = new RestTemplate(); // Configure timeouts HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setConnectTimeout(properties.getConnectTimeout()); factory.setReadTimeout(properties.getReadTimeout()); template.setRequestFactory(factory); // Add API key interceptor template.getInterceptors().add((request, body, execution) -> { request.getHeaders().add("X-API-Key", properties.getApiKey()); return execution.execute(request, body); }); return template; } public Customer getCustomer(Long customerId) { String url = properties.getBaseUrl() + "/customers/" + customerId; try { ResponseEntityresponse = restTemplate.getForEntity(url, Customer.class); return response.getBody(); } catch (Exception e) { if (properties.isRetryEnabled()) { return retryGetCustomer(customerId, 0); } throw new CustomerApiException("Failed to get customer: " + customerId, e); } } private Customer retryGetCustomer(Long customerId, int attempt) { if (attempt >= properties.getMaxRetries()) { throw new CustomerApiException("Max retries exceeded for customer: " + customerId); } try { Thread.sleep(1000 * (attempt + 1)); // Exponential backoff return getCustomer(customerId); } catch (Exception e) { return retryGetCustomer(customerId, attempt + 1); } } public List searchCustomers(String query) { String url = properties.getBaseUrl() + "/customers/search?q=" + query; try { ResponseEntity response = restTemplate.getForEntity(url, Customer[].class); return Arrays.asList(response.getBody()); } catch (Exception e) { throw new CustomerApiException("Failed to search customers: " + query, e); } } public boolean isHealthy() { try { String healthUrl = properties.getBaseUrl() + properties.getHealthCheck().getEndpoint(); ResponseEntity response = restTemplate.getForEntity(healthUrl, String.class); return response.getStatusCode().is2xxSuccessful(); } catch (Exception e) { return false; } } }
5. Auto-Configuration
@Configuration @EnableConfigurationProperties(CustomerApiProperties.class) @ConditionalOnClass({RestTemplate.class, CustomerApiClient.class}) @ConditionalOnProperty(prefix = "customer.api", name = "enabled", havingValue = "true", matchIfMissing = true) public class CustomerApiAutoConfiguration { private final CustomerApiProperties properties; public CustomerApiAutoConfiguration(CustomerApiProperties properties) { this.properties = properties; } @Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = "customer.api", name = "api-key") public CustomerApiClient customerApiClient() { return new CustomerApiClient(properties); } @Bean @ConditionalOnClass(HealthIndicator.class) @ConditionalOnBean(CustomerApiClient.class) @ConditionalOnProperty(prefix = "customer.api.health-check", name = "enabled", havingValue = "true", matchIfMissing = true) public CustomerApiHealthIndicator customerApiHealthIndicator(CustomerApiClient client) { return new CustomerApiHealthIndicator(client); } @Bean @ConditionalOnMissingBean @ConditionalOnBean(CustomerApiClient.class) public CustomerService customerService(CustomerApiClient client) { return new CustomerService(client); } }
6. Health Indicator
public class CustomerApiHealthIndicator implements HealthIndicator { private final CustomerApiClient customerApiClient; public CustomerApiHealthIndicator(CustomerApiClient customerApiClient) { this.customerApiClient = customerApiClient; } @Override public Health health() { try { boolean isHealthy = customerApiClient.isHealthy(); if (isHealthy) { return Health.up() .withDetail("status", "Customer API is accessible") .withDetail("timestamp", Instant.now()) .build(); } else { return Health.down() .withDetail("status", "Customer API is not responding") .withDetail("timestamp", Instant.now()) .build(); } } catch (Exception e) { return Health.down() .withDetail("status", "Customer API health check failed") .withDetail("error", e.getMessage()) .withDetail("timestamp", Instant.now()) .build(); } } }
7. META-INF/spring.factories
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.company.starter.customerapi.CustomerApiAutoConfiguration
Testowanie Auto-Configuration
@RunWith(SpringRunner.class) @SpringBootTest public class CustomerApiAutoConfigurationTest { @Autowired(required = false) private CustomerApiClient customerApiClient; @Test public void contextLoads() { // Test passes if context loads without errors } @Test @TestPropertySource(properties = { "customer.api.api-key=test-key", "customer.api.base-url=http://localhost:8080" }) public void shouldCreateCustomerApiClientWhenPropertiesProvided() { assertThat(customerApiClient).isNotNull(); } @Test @TestPropertySource(properties = { "customer.api.enabled=false" }) public void shouldNotCreateCustomerApiClientWhenDisabled() { assertThat(customerApiClient).isNull(); } } @TestConfiguration public class CustomerApiAutoConfigurationTestConfiguration { @Bean @Primary public CustomerApiClient mockCustomerApiClient() { return Mockito.mock(CustomerApiClient.class); } }
Używanie Custom Starter
1. Dodanie dependency
com.company.starter customer-api-spring-boot-starter 1.0.0
2. Konfiguracja w application.yml
customer: api: api-key: ${CUSTOMER_API_KEY} base-url: https://prod-api.customers.com connect-timeout: 3000 read-timeout: 15000 retry-enabled: true max-retries: 5 health-check: enabled: true endpoint: /status
3. Użycie w aplikacji
@RestController public class CustomerController { private final CustomerService customerService; public CustomerController(CustomerService customerService) { this.customerService = customerService; } @GetMapping("/customers/{id}") public Customer getCustomer(@PathVariable Long id) { return customerService.getCustomer(id); } @GetMapping("/customers/search") public ListsearchCustomers(@RequestParam String query) { return customerService.searchCustomers(query); } }
Starter automatycznie konfiguruje beans i dependencies gdy zostanie dodany do classpath. Zwykła biblioteka wymaga manualnej konfiguracji. Starter = biblioteka + auto-konfiguracja + sensible defaults.
Gdy ta sama konfiguracja jest potrzebna w wielu projektach, gdy integrujesz się z external service często używanym w firmie, lub gdy chcesz standaryzować sposób używania konkretnej biblioteki.
Tak, starter może definiować transitive dependencies. Używaj `
Użyj `–debug` flag lub `logging.level.org.springframework.boot.autoconfigure=DEBUG`. Sprawdź warunki w `@ConditionalOn*` adnotacjach i czy wymagane properties są ustawione.
Tak, używaj `@ConditionalOnMissingBean` w auto-konfiguracji. Dzięki temu użytkownicy mogą definiować własne implementacje które będą miały pierwszeństwo nad starter beans.
Używaj semantic versioning. Major version dla breaking changes, minor dla nowych features, patch dla bugfixes. Dokumentuj breaking changes i migration guides.
Używaj lazy initialization gdzie to możliwe, minimalizuj liczbę beans tworzonych podczas startup, używaj `@ConditionalOn*` żeby unikać niepotrzebnych konfiguracji.
🚀 Zadanie dla Ciebie
Stwórz starter dla integracji z Redis cache. Dodaj auto-konfigurację RedisTemplate, health indicator, metrics collector i configuration properties dla connection pool. Uwzględnij profile-specific configuration (dev/prod) i fallback gdy Redis nie jest dostępny. Napisz comprehensive testy i dokumentację.
Przydatne zasoby:
- Spring Boot Auto-Configuration Reference
- Configuration Metadata
- Condition Annotations
- Official Spring Boot Starters Source
Jakie własne startery stworzyłeś w swoich projektach? Które biblioteki najczęściej konfigurujesz i mogłyby skorzystać z własnego starter? Podziel się doświadczeniami w komentarzach!