Created
April 14, 2020 20:29
-
-
Save gabrieldewes/aae5b0be3a3c8d6d783fceb3becac540 to your computer and use it in GitHub Desktop.
WebClient abstraction for property-based configuration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import io.netty.channel.ChannelException; | |
import io.netty.channel.ChannelOption; | |
import io.netty.handler.timeout.ReadTimeoutHandler; | |
import io.netty.handler.timeout.WriteTimeoutHandler; | |
import lombok.Getter; | |
import lombok.Setter; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.boot.context.properties.ConfigurationProperties; | |
import org.springframework.http.HttpHeaders; | |
import org.springframework.http.HttpStatus; | |
import org.springframework.http.client.reactive.ReactorClientHttpConnector; | |
import org.springframework.stereotype.Component; | |
import org.springframework.util.LinkedMultiValueMap; | |
import org.springframework.util.MultiValueMap; | |
import org.springframework.web.reactive.function.BodyInserters; | |
import org.springframework.web.reactive.function.client.ClientResponse; | |
import org.springframework.web.reactive.function.client.WebClient; | |
import org.springframework.web.util.UriBuilder; | |
import reactor.core.Exceptions; | |
import reactor.core.publisher.Mono; | |
import reactor.netty.http.client.HttpClient; | |
import reactor.netty.tcp.TcpClient; | |
import reactor.retry.Retry; | |
import java.io.IOException; | |
import java.net.URI; | |
import java.nio.charset.Charset; | |
import java.time.Duration; | |
import java.util.Objects; | |
import java.util.concurrent.TimeUnit; | |
import java.util.function.Consumer; | |
import java.util.function.Function; | |
//@Component | |
public class MyWebClient { | |
private static final Logger log = LoggerFactory.getLogger(MyWebClient.class); | |
private WebClient.Builder builder; | |
private WebClient webClient; | |
private MyWebClient.Properties properties; | |
public MyWebClient(WebClient.Builder builder, MyWebClient.Properties properties) { | |
this.builder = builder; | |
this.properties = properties; | |
} | |
public MyWebClient(MyWebClient.Properties properties) { | |
this.builder = WebClient.builder(); | |
this.properties = properties; | |
} | |
public void initialize() { | |
this.webClient = this.builder | |
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient()))) | |
.baseUrl(properties.getBaseUrl()) | |
.defaultHeaders(this.defaultHeaders()).build(); | |
} | |
public void insecureInitialize() throws SSLException { | |
if (this.properties.insecure) { | |
// Never use this TrustManagerFactory in production. | |
// It is purely for testing purposes, and thus it is very insecure. | |
SslContext sslContext = SslContextBuilder | |
.forClient() | |
.trustManager(InsecureTrustManagerFactory.INSTANCE) | |
.build(); | |
this.webClient = this.builder | |
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient()) | |
.secure(sslContextSpec -> sslContextSpec.sslContext(sslContext)))) | |
.baseUrl(properties.getBaseUrl()) | |
.defaultHeaders(this.defaultHeaders()).build(); | |
} | |
} | |
private TcpClient tcpClient() { | |
return TcpClient.create() | |
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeout()) | |
.doOnConnected(connection -> { | |
connection.addHandlerLast(new ReadTimeoutHandler(properties.getReadTimeout(), TimeUnit.MILLISECONDS)); | |
connection.addHandlerLast(new WriteTimeoutHandler(properties.getWriteTimeout(), TimeUnit.MILLISECONDS)); | |
}); | |
} | |
public Mono<ClientResponse> get(final String uri, Consumer<HttpHeaders> httpHeadersConsumer, Object... uriVariables) { | |
return this.get(uri, new LinkedMultiValueMap<>(), httpHeadersConsumer, uriVariables); | |
} | |
public Mono<ClientResponse> get(final String uri, Object... uriVariables) { | |
return this.get(uri, new LinkedMultiValueMap<>(), uriVariables); | |
} | |
public Mono<String> getString(final String uri, Consumer<HttpHeaders> httpHeadersConsumer, Object... uriVariables) { | |
return this.get(uri, new LinkedMultiValueMap<>(), httpHeadersConsumer, uriVariables) | |
.filter(clientResponse -> clientResponse.statusCode().series().equals(HttpStatus.Series.SUCCESSFUL)) | |
.flatMap(clientResponse -> clientResponse.bodyToMono(String.class)); | |
} | |
public Mono<String> getString(final String uri, Object... uriVariables) { | |
return this.get(uri, new LinkedMultiValueMap<>(), uriVariables) | |
.filter(clientResponse -> clientResponse.statusCode().series().equals(HttpStatus.Series.SUCCESSFUL)) | |
.flatMap(clientResponse -> clientResponse.bodyToMono(String.class)); | |
} | |
public Mono<ClientResponse> get(final String uri, | |
final MultiValueMap<String, String> queryParams, | |
final Object... uriVariables) { | |
return this.get(uri, queryParams, null, uriVariables); | |
} | |
public Mono<ClientResponse> get(final String uri, | |
final MultiValueMap<String, String> queryParams, | |
final Consumer<HttpHeaders> httpHeadersConsumer, | |
final Object... uriVariables) { | |
return this.webClient.get() | |
.uri(this.buildUri(uri, queryParams, uriVariables)) | |
.acceptCharset(Charset.forName("UTF-8")) | |
.headers(httpHeadersConsumer) | |
.exchange() | |
.flatMap(this::withRetry); | |
} | |
public Mono<String> postForString(final String uri, final Object body, Object... uriVariables) { | |
return this.post(uri, body, uriVariables) | |
.filter(clientResponse -> clientResponse.statusCode().series().equals(HttpStatus.Series.SUCCESSFUL)) | |
.flatMap(clientResponse -> clientResponse.bodyToMono(String.class)); | |
} | |
public Mono<ClientResponse> post(final String uri, final Object body, final Object... uriVariables) { | |
return this.webClient.post() | |
.uri(this.buildUri(uri, null, uriVariables)) | |
.acceptCharset(Charset.forName("UTF-8")) | |
.body(BodyInserters.fromValue(body)) | |
.exchange() | |
.flatMap(this::withRetry); | |
} | |
private Function<UriBuilder, URI> buildUri(final String uri, | |
final MultiValueMap<String, String> queryParams, | |
final Object... uriVariables) { | |
return (uriBuilder -> { | |
uriBuilder.path(uri); | |
uriBuilder.queryParams(queryParams); | |
return uriBuilder.build(uriVariables); | |
}); | |
} | |
private Consumer<HttpHeaders> defaultHeaders() { | |
return (httpHeaders) -> { | |
httpHeaders.set(HttpHeaders.ACCEPT, properties.getAccept()); | |
httpHeaders.set(HttpHeaders.CONTENT_TYPE, properties.getContentType()); | |
if (properties.getAuthorization() != null) { | |
SigaWebClient.Properties.Authorization auth = properties.getAuthorization(); | |
if (auth.getBasic() != null && auth.getBearer() != null) { | |
throw new RuntimeException("Mismatch MyWebClient.Properties.Authorization configuration!"); | |
} | |
if (auth.getBasic() != null) { | |
httpHeaders.setBasicAuth(auth.getBasic().getUsername(), auth.getBasic().getPassword()); | |
} | |
if (auth.getBearer() != null) { | |
httpHeaders.setBearerAuth(auth.getBearer().getToken()); | |
} | |
} | |
if (properties.getHeaders() != null && properties.getHeaders().length > 0) { | |
for (MyWebClient.Properties.Header h : properties.getHeaders()) { | |
httpHeaders.set(h.getName(), h.getValue()); | |
} | |
} | |
}; | |
} | |
private Mono<ClientResponse> withRetry(ClientResponse originalResponse) { | |
if (properties.getRetry().isEnabled()) { | |
MyWebClient.Properties.Retry retry = properties.getRetry(); | |
return Mono.just(originalResponse) | |
.doOnError(ChannelException.class, e -> { | |
log.error("Retry attempt due to ChannelException error: {}", e.getMessage()); | |
throw Exceptions.propagate(e); | |
}) | |
.doOnError(IOException.class, e -> { | |
log.error("Retry attempt due to IOException error: {}", e.getMessage()); | |
throw Exceptions.propagate(e); | |
}) | |
.map(clientResponse -> { | |
if (retry.getStatusCodes() != null) { | |
for (int code : retry.getStatusCodes()) { | |
if (clientResponse.rawStatusCode() == code) { | |
log.error("Retry attempt due to {} error", clientResponse.rawStatusCode()); | |
throw Exceptions.propagate(Objects.requireNonNull(clientResponse.createException().block())); | |
} | |
} | |
} | |
return clientResponse; | |
}) | |
.retryWhen(Retry.any() | |
.fixedBackoff(Duration.ofMillis(retry.getBackoff())) | |
.retryMax(retry.getMaxAttempts())); | |
} | |
return Mono.just(originalResponse); | |
} | |
/** | |
* #// Default MyWebClient Properties "resources/application.properties" | |
* com.yourpackage.web.connect-timeout=3000 | |
* com.yourpackage.web.read-timeout=5000 | |
* com.yourpackage.web.write-timeout=5000 | |
* com.yourpackage.web.insecure=true | |
* com.yourpackage.web.base-url=http://localhost:80 | |
* com.yourpackage.web.accept=application/json;charset=utf-8 | |
* com.yourpackage.web.content-type=application/json;charset=utf-8 | |
* #// Authorization Basic | |
* com.yourpackage.web.authorization.basic.username=user | |
* com.yourpackage.web.authorization.basic.password=pass | |
* #// Fixed or long live Bearer tokens | |
* com.yourpackage.web.authorization.bearer.token=token | |
* #// Request retry when fail | |
* com.yourpackage.web.retry.enabled=true | |
* com.yourpackage.web.retry.status-codes=422,429,500,503 | |
* com.yourpackage.web.retry.max-attempts=3 | |
* com.yourpackage.web.retry.backoff=2000 | |
* #// Custom default headers | |
* com.yourpackage.web.headers[0].name=X-Test-Header | |
* com.yourpackage.web.headers[0].value=HeaderValue | |
* com.yourpackage.web.headers[1].name=X-Test-Header-1 | |
* com.yourpackage.web.headers[1].value=HeaderValue1 | |
*/ | |
@ConfigurationProperties(prefix = "com.yourpackage.web") | |
@Getter | |
@Setter | |
public static class Properties { | |
private Integer connectTimeout = 3000; | |
private Integer readTimeout = 5000; | |
private Integer writeTimeout = 5000; | |
private boolean insecure = false; | |
private String baseUrl = "http://localhost"; | |
private String accept = "application/json"; | |
private String contentType = "application/json"; | |
private Properties.Authorization authorization = new Properties.Authorization(); | |
private Properties.Retry retry = new Properties.Retry(); | |
private Properties.Header[] headers = new Properties.Header[0]; | |
@Getter | |
@Setter | |
public static class Authorization { | |
private SigaWebClient.Properties.Authorization.Basic basic; | |
private SigaWebClient.Properties.Authorization.Bearer bearer; | |
@Getter | |
@Setter | |
public static class Basic { | |
private String username; | |
private String password; | |
} | |
@Getter | |
@Setter | |
public static class Bearer { | |
private String token; | |
} | |
} | |
@Getter | |
@Setter | |
public static class Retry { | |
private boolean enabled = false; | |
private int[] statusCodes; | |
private int maxAttempts; | |
private int backoff; | |
} | |
@Getter | |
@Setter | |
public static class Header { | |
private String name; | |
private String value; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment