This project provides two reusable Spring Boot starter modules for integrating external HTTP services. It offers
standard abstraction layers for both Spring Cloud OpenFeign and Spring 6 RestClient, both backed by a robust,
dynamically configured Apache HTTP Client.
- Dynamic Property Binding: Standardized
application.ymlconfiguration (Connection pooling, SSL, Proxy, Basic Auth) mapped via Spring'sBinder. - Apache HTTP Client Factory: Centralized creation of
CloseableHttpClientwith advanced pooling (PoolingHttpClientConnectionManagerBuilder) and lifecycle management. - Observability: Out-of-the-box integration with Micrometer
ObservationRegistryfor metrics and distributed tracing. - MDC & Headers: Built-in interceptors for JSON content types, authentication, and context propagation (e.g.,
X-Request-Id). - Template Method Pattern: Abstract base classes allow developers to focus on service-specific logic while inheriting robust boilerplate.
A module for creating declarative HTTP clients using Spring Cloud OpenFeign.
<dependency>
<groupId>com.tosan.client.http</groupId>
<artifactId>tosan-httpclient-spring-boot-feignclient-starter</artifactId>
<version>latest</version>
</dependency>-
Create the Feign Interface:
public interface CustomServerRestController { String PATH = "/api/v1/resource"; @GetMapping("/{id}") MyResourceDto getResource(@PathVariable("id") String id); }
-
Extend
AbstractFeignConfiguration: Note: Use@Import(FeignClientsConfiguration.class)to provide default OpenFeign beans.@Configuration @Import(FeignClientsConfiguration.class) public class CustomServerFeignConfig extends AbstractFeignConfiguration<HttpClientProperties> { public static final String SERVICE_NAME = "custom-web-service"; public CustomServerFeignConfig( ObservationRegistry observationRegistry, JsonReplaceHelperDecider jacksonReplaceHelper, ObjectProvider<feign.Feign.Builder> builderProvider, Encoder encoder, Decoder decoder, Contract contract) { super(SERVICE_NAME, HttpClientProperties.class, observationRegistry, jacksonReplaceHelper, builderProvider, encoder, decoder, contract); } @Bean(SERVICE_NAME) public ExternalServiceInvoker<CustomServerRestController> serviceInvokerBean(Environment environment) { return createServiceInvoker(environment, CustomServerRestController.PATH, CustomServerRestController.class); } @Override public CustomErrorDecoderConfig createCustomErrorDecoderConfig(ObjectMapper objectMapper) { CustomErrorDecoderConfig config = new CustomErrorDecoderConfig(); config.getScanPackageList().add("com.example.api.exception"); config.setExceptionExtractType(ExceptionExtractType.EXCEPTION_IDENTIFIER_FIELDS); config.setCheckedExceptionClass(CustomServerException.class); config.setUncheckedExceptionClass(CustomRuntimeException.class); config.setObjectMapper(objectMapper); return config; } }
-
Inject and Use:
@Service public class MyBusinessService { private final CustomServerRestController client; public MyBusinessService(@Qualifier(CustomServerFeignConfig.SERVICE_NAME) ExternalServiceInvoker<CustomServerRestController> invoker) { this.client = invoker.getServiceApi(); } }
A module for creating fluent, modern HTTP clients using Spring Framework 6's RestClient.
<dependency>
<groupId>com.tosan.client.http</groupId>
<artifactId>tosan-httpclient-spring-boot-restclient-starter</artifactId>
<version>latest</version>
</dependency>-
Extend
AbstractRestClientConfiguration:@Configuration public class CustomServerRestClientConfig extends AbstractRestClientConfiguration<HttpClientProperties> { public static final String SERVICE_NAME = "custom-web-service"; public CustomServerRestClientConfig( ObservationRegistry observationRegistry, JsonReplaceHelperDecider jacksonReplaceHelper) { super(SERVICE_NAME, HttpClientProperties.class, observationRegistry, jacksonReplaceHelper); } @Bean(SERVICE_NAME) public RestClient customServerRestClient(Environment environment) { return super.getInstance(environment); } // Optional: Override hooks to customize behavior @Override protected void customizeRestClient(RestClient.Builder builder) { builder.defaultHeader("X-Custom-Header", "Value"); } @Override protected List<ClientHttpRequestInterceptor> createInterceptors() { List<ClientHttpRequestInterceptor> interceptors = super.createInterceptors(); // Add custom interceptors here return interceptors; } }
-
Inject and Use:
@Service public class MyBusinessService { private final RestClient restClient; public MyBusinessService(@Qualifier(CustomServerRestClientConfig.SERVICE_NAME) RestClient restClient) { this.restClient = restClient; } public MyResourceDto fetchData(String id) { return restClient.get() .uri("/api/v1/resource/{id}", id) .retrieve() .body(MyResourceDto.class); } }
Both modules utilize the same properties structure mapped via dynamic configuration binding. Define the properties in
your application.yml using the configured service name prefix (e.g., [service-name].client).
Both starters use the same class to configure settings.
| Config | Description | Default | Example |
|---|---|---|---|
| baseServiceUrl | Base service url (required) | localhost:8080/example |
|
| sslContext | SSL Version (optional) | TLSv1.2 |
TLSv1.1 |
| connection.connectionTimeout (optional) | Connection Timeout in ms | 5000 | 2000 |
| connection.socketTimeout (optional) | Socket Timeout in ms | 10000 | 5000 |
| connection.maxConnections (optional) | max number of connections | 200 | 100 |
| connection.maxConnectionsPerRoute (optional) | max number of connections per route | 50 | 10 |
| connection.timeToLive (optional) | time to live | 900 | 300 |
| connection.timeToLiveUnit (optional) | time to live unit | SECONDS | MINUTES |
| connection.followRedirects (optional) | following redirects | true | true or false |
| connection.connectionTimerRepeat (optional) | connection timer repeat | 3000 | 1000 |
| connection.cookieSpecPolicy (optional) | Standard cookie specifications | ignore |
relaxed,strict,ignore |
| proxy (optional) | Configuration for used proxy servers | ||
| proxy.enable | Proxy enable (optional) | false | true or false |
| proxy.host | Hostname or IP of the Proxy | 192.168.7.130 or corp-proxy.domain |
|
| proxy.port | Port of the Proxy (optional) | 100 |
|
| proxy.user | Proxy user name (optional) | testUser |
|
| proxy.password | Proxy password (optional) | testPassword |
|
| ssl (optional) | Configuration for ssl | ||
| ssl.enable | SSL Enable (optional) | false | true or false |
| ssl.context | SSL Version (optional) | TLSv1.2 |
TLSv1.1 |
| ssl.check-validity | Check SSL validity (optional) | false | true or false |
| ssl.keystore.path | Keystore file path | classpath:keystore.jks |
|
| ssl.keystore.password | Keystore password | changeit |
|
| ssl.keystore.type | Keystore type (optional) | JKS |
PKCS12 |
| ssl.truststore.path | Truststore file path | classpath:truststore.jks |
|
| ssl.truststore.password | Truststore password | changeit |
|
| ssl.truststore.type | Truststore type (optional) | JKS |
PKCS12 |
| authorization.enable | Authorization enable (optional) | false | true or false |
| authorization.username | Authorization user name (optional) | testUser |
|
| authorization.password | Authorization password (optional) | testPassword |
Example:
custom-web-service:
client:
base-service-url: "https://api.external-service.com"
connection:
max-connections: 200
max-connections-per-route: 50
time-to-live: 900000
connection-timeout: 5000
socket-timeout: 10000
connection-timer-repeat: 3000
follow-redirects: true
authorization:
enable: true
username: "my-user"
password: "my-password"
proxy:
enable: false
host: "127.0.0.1"
port: "8080"
ssl:
enable: true
context: "TLSv1.2"
check-validity: true
keystore:
path: "classpath:keystore.jks"
password: "keystore-pass"
truststore:
path: "classpath:truststore.jks"
password: "truststore-pass"