Skip to content

Instantly share code, notes, and snippets.

@gabrieldewes
Created April 14, 2020 20:29
Show Gist options
  • Save gabrieldewes/aae5b0be3a3c8d6d783fceb3becac540 to your computer and use it in GitHub Desktop.
Save gabrieldewes/aae5b0be3a3c8d6d783fceb3becac540 to your computer and use it in GitHub Desktop.
WebClient abstraction for property-based configuration
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