I'm using the following code (from this answer) to configure headers to be logged on WebClient requests:
ExchangeStrategies exchangeStrategies = ExchangeStrategies.withDefaults();
exchangeStrategies
.messageWriters().stream()
.filter(LoggingCodecSupport.class::isInstance)
.forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true));
client = WebClient.builder()
.exchangeStrategies(exchangeStrategies)
This works, but causes my Jackson configuration to be lost. In my application.properties I have:
spring.jackson.default-property-inclusion=non-null
spring.jackson.deserialization.accept-empty-string-as-null-object=true
which gets overwritten by the above code. Here is my workaround:
#Autowired ObjectMapper objectMapper;
#Bean
WebClientCustomizer webClientCustomizer() {
return (WebClient.Builder builder) -> {
builder
.exchangeStrategies(createExchangeStrategiesWhichLogHeaders())
};
}
private ExchangeStrategies createExchangeStrategiesWhichLogHeaders() {
ExchangeStrategies exchangeStrategies =
ExchangeStrategies.builder()
.codecs(
clientDefaultCodecsConfigurer -> {
clientDefaultCodecsConfigurer
.defaultCodecs()
.jackson2JsonEncoder(
new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
clientDefaultCodecsConfigurer
.defaultCodecs()
.jackson2JsonDecoder(
new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
})
.build();
exchangeStrategies
.messageWriters()
.stream()
.filter(LoggingCodecSupport.class::isInstance)
.forEach(writer -> ((LoggingCodecSupport) writer).setEnableLoggingRequestDetails(true));
return exchangeStrategies;
}
This works, but feels a bit strange. The question is: do I need to include the jackson/objectMapper configuration like this, or is there a simpler way to avoid the Spring objectMapper configuration being overwritten?
As of Spring Boot 2.1.0, you can achieve this by enabling the following property:
spring.http.log-request-details=true
If you're on a previous Spring Boot version, you should be able to customize this without overwriting or rebuilding the whole configuration, like this:
#Configuration
static class LoggingCodecConfig {
#Bean
#Order(0)
public CodecCustomizer loggingCodecCustomizer() {
return (configurer) -> configurer.defaultCodecs()
.enableLoggingRequestDetails(true);
}
}
Related
I have created below webclient and using it inside of my service to make HTTP third party calls.
#Configuration
public class WebclientConfig {
#Bean
public WebClient webClient() {
// custom client connector with connection pool disabled is being used as by default the connection pooling is done and connection reset happens after some idle time.
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(HttpClient.newConnection()))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
and in my service, I am calling the third party service like below.
private Flux<BusinessObject> getBusinessObjects(String serviceURL) {
return this.webClient.get()
.uri(serviceURL)
.retrieve()
.bodyToFlux(BusinessObject.class) //code below this, do I have to copy for each webclient request to configure the retry, even if the values are same
.retryWhen(Retry.backoff(3, Duration.of(2, ChronoUnit.SECONDS))
.doBeforeRetry((value) -> log.info("Retrying request " + value))
.filter(error -> error instanceof WebClientRequestException)
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) ->
new RuntimeException(retrySignal.failure().getMessage())));
}
My question is as in comment in above code.
I have multiple webclient calls, but I want to configure the retry backoff configuration at a single place. How can I do that? so that my code should look like below
private Flux<BusinessObject> getBusinessObjects(String serviceURL) {
return this.webClient.get()
.uri(serviceURL)
.retrieve()
.bodyToFlux(BusinessObject.class)
.somCommonRetryCodeWrappingTheRetryLogic();
}
You can use transform operator for this purpose:
private Flux<BusinessObject> getBusinessObjects(String serviceURL) {
return this.webClient.get()
.uri(serviceURL)
.retrieve()
.bodyToFlux(BusinessObject.class)
.transform(originalFlux -> wrapWithRetry(originalFlux));
}
private <T> Flux<T> wrapWithRetry(Flux<T> originalFlux) {
return originalFlux
.retryWhen(Retry.backoff(3, Duration.of(2, ChronoUnit.SECONDS))
.doBeforeRetry((value) -> log.info("Retrying request " + value))
.filter(error -> error instanceof WebClientRequestException)
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) ->
new RuntimeException(retrySignal.failure().getMessage())));
}
Only drawback is that if you have Mono use cases as well then you need to implement it twice.
If that is still too much copy-paste, you can consider defining an ExchangeFilterFunction to apply retry for every WebClient call automatically. Something like this:
WebClient.builder()
// ...
.filter((request, next) -> next.exchange(request).retry()) // add full retry config here
.build();
I have to migrate an existing OAuth2RestTemplate based OAuth2 microservice to Spring Security / WebClient because OAuth2RestTemplate is deprecated. However, the new implementation is unable to access the OAuth2 server through a proxy server. I've read various articles on this topic, including Spring WebClient is not using Proxy for the latest version of spring-security-oauth2-client-5.3.4.RELEASE, and tried to include the logic described in these articles in my implementation. Without success unfortunately. My implementation configures the underlying ClientHttpConnector used by WebClient with the appropriate proxy settings. See the code snippets below from WebClientConfig (the complete codebase including WebClientConfigand unit tests can be found here: github):
#Bean
public ReactiveClientRegistrationRepository clientRegistrationRepository() {
ClientRegistration clientRegistration = ClientRegistration
.withRegistrationId(REGISTRATION_ID)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.clientAuthenticationMethod(ClientAuthenticationMethod.POST)
.userInfoAuthenticationMethod(AuthenticationMethod.FORM)
.tokenUri(restServiceProperties.getOauth2Url())
.build();
return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
}
#Bean
public ReactiveOAuth2AuthorizedClientService authorizedClientService(ReactiveClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
}
#Bean
public ClientHttpConnector clientHttpConnector() {
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
if (Strings.isNotEmpty(restServiceProperties.getProxyUrl())) {
try {
HttpHost proxy = HttpHost.create(restServiceProperties.getProxyUrl());
clientBuilder.setRoutePlanner(new DefaultProxyRoutePlanner(proxy));
} catch (URISyntaxException e) {
log.error("Error: {}", e.getMessage());
throw new RuntimeException(e);
}
}
CloseableHttpAsyncClient client = clientBuilder.build();
return new HttpComponentsClientHttpConnector(client);
}
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService, ClientHttpConnector clientHttpConnector, WebClient.Builder webClientBuilder) {
WebClient webClient = webClientBuilder
.clientConnector(clientHttpConnector)
.filter(logRequest())
.build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(createAuthorizedClientProvider(webClient));
authorizedClientManager.setContextAttributesMapper(request -> {
Map<String, Object> contextAttributes = new HashMap<>();
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, USER);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, PASSWORD);
return Mono.just(contextAttributes);
});
return authorizedClientManager;
}
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager,
ClientHttpConnector clientHttpConnector, WebClient.Builder webClientBuilder) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2FilterFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2FilterFunction.setDefaultClientRegistrationId(REGISTRATION_ID);
return webClientBuilder
.clientConnector(clientHttpConnector)
.filters(exchangeFilterFunctions -> {
exchangeFilterFunctions.add(oauth2FilterFunction);
exchangeFilterFunctions.add(logRequest());
})
.build();
}
private ReactiveOAuth2AuthorizedClientProvider createAuthorizedClientProvider(WebClient webClient) {
WebClientReactiveClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
clientCredentialsTokenResponseClient.setWebClient(webClient);
return ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(builder -> builder.accessTokenResponseClient(clientCredentialsTokenResponseClient))
.password()
.refreshToken()
.build();
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
log.info("Request: method: {} URL: {}", clientRequest.method(), clientRequest.url());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> log.info("Request header: name: {} value: {}", name, value)));
return Mono.just(clientRequest);
});
}
I also tried clientBuilder.setProxy(proxy) rather than clientBuilder.setRoutePlanner(new DefaultProxyRoutePlanner(proxy)), but that didn't solve the issue.
Regardig the unit tests: NoProxyServerTest shows that the OAuth2 server and downstream server can be accessed when no proxy server is configured. ProxyServerTest shows that the OAuth2 server cannot be accessed when a proxy server is configured. DirectProxyServerInvocationTest shows that the MockProxyServer is actually working. And DownstreamServerProxyServerInvocationTest shows that the downstream server can accessed through a proxy server. WebClient is configured in WebClientConfig. This is potentially the best configuration so far, but I've tried many different configurations to solve this proxy server issue (including the use of https.proxyPort, https.proxyHost, http.proxyPortand http.proxyHost).
A similar test approached is being used to test the deprecated OAuth2RestTemplate based implementation. And all four tests pass against that implementation.
I am new to the Spring Integration project, now I need to create a flow with Java DSL and test it.
I came up with these flows. First one should run by cron and invoke second one, which invokes HTTP endpoint and translates XML response to POJO:
#Bean
IntegrationFlow pollerFlow() {
return IntegrationFlows
.from(() -> new GenericMessage<>(""),
e -> e.poller(p -> p.cron(this.cron)))
.channel("pollingChannel")
.get();
}
#Bean
IntegrationFlow flow(HttpMessageHandlerSpec bulkEndpoint) {
return IntegrationFlows
.from("pollingChannel")
.enrichHeaders(authorizationHeaderEnricher(user, password))
.handle(bulkEndpoint)
.transform(xmlTransformer())
.channel("httpResponseChannel")
.get();
}
#Bean
HttpMessageHandlerSpec bulkEndpoint() {
return Http
.outboundGateway(uri)
.httpMethod(HttpMethod.POST)
.expectedResponseType(String.class)
.errorHandler(new DefaultResponseErrorHandler());
}
Now I want to test flow and mock HTTP call, but struggling to mock HTTP handler, I tried to do it like that:
#ExtendWith(SpringExtension.class)
#SpringIntegrationTest(noAutoStartup = {"pollerFlow"})
#ContextConfiguration(classes = FlowConfiguration.class)
public class FlowTests {
#Autowired
private MockIntegrationContext mockIntegrationContext;
#Autowired
public DirectChannel httpResponseChannel;
#Autowired
public DirectChannel pollingChannel;
#Test
void test() {
final MockMessageHandler mockHandler = MockIntegration.mockMessageHandler()
.handleNextAndReply(message -> new GenericMessage<>(xml, message.getHeaders()));
mockIntegrationContext.substituteMessageHandlerFor("bulkEndpoint", mockHandler);
httpResponseChannel.subscribe(message -> {
assertThat(message.getPayload(), is(notNullValue()));
assertThat(message.getPayload(), instanceOf(PartsSalesOpenRootElement.class));
});
pollingChannel.send(new GenericMessage<>(""));
}
}
But I am always getting an error, that on line:
mockIntegrationContext.substituteMessageHandlerFor("bulkEndpoint", mockHandler);
org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'bulkEndpoint' is expected to be of type 'org.springframework.integration.endpoint.IntegrationConsumer' but was actually of type 'org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler'
Am I doing something wrong here? I am assuming I have a problem with IntegrationFlow itself, or maybe my testing approach is a problem.
The error is correct. The bulkEndpoint is not an endpoint by itself. It is really a MessageHandler. The endpoint is created from the .handle(bulkEndpoint).
See docs: https://docs.spring.io/spring-integration/docs/current/reference/html/overview.html#finding-class-names-for-java-and-dsl-configuration and https://docs.spring.io/spring-integration/docs/current/reference/html/testing.html#testing-mocks.
So, to make it working you need to do something like this:
.handle(bulkEndpoint, e -> e.id("actualEndpoint"))
And then in the test:
mockIntegrationContext.substituteMessageHandlerFor("actualEndpoint", mockHandler);
You also probably need to think to not have that pollerFlow to be started when you test it sine you send the message into pollingChannel manually. So, there is no conflicts with what you'd like to test. For this reason you also add a id() into your e.poller(p -> p.cron(this.cron)) and use #SpringIntegrationTest(noAutoStartup) to have it stopped before your test. I see you try noAutoStartup = {"pollerFlow"}, but this is not going to help for static flows. You indeed need to have stopped an actual endpoint in this case.
While sending a file I receive an array of bytes. I always have a problem with webflux to receive an array.
the error thrown as below :
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
at org.springframework.core.io.buffer.LimitedDataBufferList.raiseLimitException(LimitedDataBufferList.java:101)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException
Do you now how to resolve that in webflux ?
This worked for me:
Create a #Bean in one of your configuration classes or the main SpringBootApplication class:
#Bean
public WebClient webClient() {
final int size = 16 * 1024 * 1024;
final ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(size))
.build();
return WebClient.builder()
.exchangeStrategies(strategies)
.build();
}
Next, go to your desired class where you want to use the WebClient:
#Service
public class TestService {
#Autowired
private WebClient webClient;
public void test() {
String out = webClient
.get()
.uri("/my/api/endpoint")
.retrieve()
.bodyToMono(String.class)
.block();
System.out.println(out);
}
}
I suppose this issue is about adding a new spring.codec.max-in-memory-size configuration property in Spring Boot. Add it to the application.yml file like:
spring:
codec:
max-in-memory-size: 10MB
Set the maximum bytes (in megabytes) in your Spring Boot application.properties configuration file like below:
spring.codec.max-in-memory-size=20MB
i was getting this error for a simple RestController (i post a large json string).
here is how i successfully changed the maxInMemorySize
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.ResourceHandlerRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
#Configuration
public class WebfluxConfig implements WebFluxConfigurer {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui.html**")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
#Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024);
}
}
this was surprisingly hard to find
worked for me
webTestClient.mutate()
.codecs(configurer -> configurer
.defaultCodecs()
.maxInMemorySize(16 * 1024 * 1024))
.build().get()
.uri("/u/r/l")
.exchange()
.expectStatus()
.isOk()
Instead of retrieving data at once, you can stream:
Mono<String> string = webClient.get()
.uri("end point of an API")
.retrieve()
.bodyToFlux(DataBuffer.class)
.map(buffer -> {
String string = buffer.toString(Charset.forName("UTF-8"));
DataBufferUtils.release(buffer);
return string;
});
Alternatively convert to stream:
.map(b -> b.asInputStream(true))
.reduce(SequenceInputStream::new)
.map(stream -> {
// consume stream
stream.close();
return string;
});
In most cases you don't want to really aggregate the stream, rather than processing it directly. The need to load huge amount of data in memory is mostly a sign to change the approach to more reactive one. JSON- and XML-Parsers have streaming interfaces.
This worked for me
val exchangeStrategies = ExchangeStrategies.builder()
.codecs { configurer: ClientCodecConfigurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024) }.build()
return WebClient.builder().exchangeStrategies(exchangeStrategies).build()
Another alternative could be creating a custom CodecCustomizer, which is going to be applied to both WebFlux and WebClient at the same time:
#Configuration
class MyAppConfiguration {
companion object {
private const val MAX_MEMORY_SIZE = 50 * 1024 * 1024 // 50 MB
}
#Bean
fun codecCustomizer(): CodecCustomizer {
return CodecCustomizer {
it.defaultCodecs()
.maxInMemorySize(MAX_MEMORY_SIZE)
}
}
}
As of Spring Boot 2.3.0, there is now a dedicated configuration property for the Reactive Elasticsearch REST client.
You can use the following configuration property to set a specific memory limit for the client.
spring.data.elasticsearch.client.reactive.max-in-memory-size=
The already existing spring.codec.max-in-memory-size property is separate and only affects other WebClient instances in the application.
For those who had no luck with the myriad of beans, customizers, and properties that could be added to solve this problem, check whether you have defined a bean extending WebFluxConfigurationSupport. If you have, it will disable the autoconfiguration version of the same bean (my personal experience, Boot 2.7.2), somewhere under which Spring loads properties such as the suggested spring.codec.max-in-memory-size. For this solution to work you need to have also configured this property correctly.
To test if this is the cause of your problems, remove your WebFluxConfigurationSupport implementation temporarily. The long term fix that worked for me was to use configuration beans to override attributes for the autoconfigured bean. In my case, WebFluxConfigurer had all of the same methods available and was a drop-in replacement for WebFluxConfigurationSupport. Large JSON messages are now decoding for me as configured.
If you dont want to change the default settings for webClient globally, you can use the following approach to manually merge multiple DataBuffers
webClient
.method(GET)
.uri("<uri>")
.exchangeToMono(response -> {
return response.bodyToFlux(DataBuffer.class)
.switchOnFirst((firstBufferSignal, responseBody$) -> {
assert firstBufferSignal.isOnNext();
return responseBody$
.collect(() -> requireNonNull(firstBufferSignal.get()).factory().allocateBuffer(), (accumulator, curr) -> {
accumulator.ensureCapacity(curr.readableByteCount());
accumulator.write(curr);
DataBufferUtils.release(curr);
})
.map(accumulator -> {
final var responseBodyAsStr = accumulator.toString(UTF_8);
DataBufferUtils.release(accumulator);
return responseBodyAsStr;
});
})
.single();
});
The above code aggregates all the DataBuffers into a single DataBuffer & then converts the final DataBuffer into a string. Please note that this answer wont work as DataBuffers received might not have all the bytes to construct a character (incase of UTF-8 characters, each character can take upto 4 bytes). So we cant convert intermediate DataBuffers into String as the bytes towards
the end of buffer might have only part of the bytes required to construct a valid character
Note that this loads all the response DataBuffers into memory but unlike changing global settings for the webClient across the whole application. You can use this option to read complete response only where you want i.e you can narrow down & pick this option only where you expect large responses.
As of Spring boot 2.7.x we should use below property to set the memory size to webclient which is used internally in reactive ElasticSearch
spring.elasticsearch.webclient.max-in-memory-size=512MB
Just add below code in your springboot main class.
#Bean
public WebClient getWebClient() {
return WebClient.builder()
.baseUrl("Your_SERVICE_URL")
.codecs(configurer -> configurer
.defaultCodecs()
.maxInMemorySize(16 * 1024 * 1024))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
This work for me.
I have a spring integration IntegrationFlow that's defined like this:
IntegrationFlows.from(Amqp.inboundAdapter(connectionFactory, "queueName")
.id("id")
.autoStartup(autoStartup)
.concurrentConsumers(2)
.maxConcurrentConsumers(3)
.messageConverter(messageConverter()))
.aggregate(a -> ...)
.handle(serviceActivatorBean)
.get();
And serviceActivatorBean looks like this:
#Component
#Transactional
public class ServiceActivator {
#ServiceActivator
public void myMethod(Collection<MyEvent> events) {
....
}
}
If myMethod throws an exception it will be logged but no retry will happen. I've tried to change the IntegrationFlow to this:
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
RetryTemplate retryTemplate = new RetryTemplate();
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(5);
retryTemplate.setRetryPolicy(retryPolicy);
advice.setRetryTemplate(retryTemplate);
IntegrationFlows.from(Amqp.inboundAdapter(connectionFactory, "queueName")
.id("id")
.autoStartup(autoStartup)
.adviceChain(advice)
.concurrentConsumers(2)
.maxConcurrentConsumers(3)
.messageConverter(messageConverter()))
.aggregate(a -> ...)
.handle(serviceActivatorBean)
.get();
But then I a log message like this (an retries won't happen):
2017-06-30 13:18:10.611 WARN 88706 --- [erContainer#1-2]
o.s.i.h.a.RequestHandlerRetryAdvice : This advice
org.springframework.integration.handler.advice.RequestHandlerRetryAdvice
can only be used for MessageHandlers; an attempt to advise method
'invokeListener' in
'org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$1'
is ignored
How can I configure this IntegrationFlow to behave the same way a RabbitListener would? I.e. let RabbitMQ publish the messages again.
Use a retry interceptor in the adapter's advice chain instead of the RequestHandlerRetryAdvice - that is for consuming endpoints, as the message says.