Refactor to use SpEL in function properties

* Clean up per review
This commit is contained in:
David Turanski
2020-08-12 15:39:56 -04:00
committed by GitHub
parent 23aabb1d63
commit eee707da00
14 changed files with 279 additions and 544 deletions

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.fn.http.request;
import java.time.Duration;
import java.util.Map;
import java.util.function.Function;
import reactor.core.publisher.Flux;
@@ -25,6 +26,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.Message;
import org.springframework.web.reactive.function.client.WebClient;
@@ -50,7 +53,7 @@ public class HttpRequestFunctionConfiguration {
}
@Bean
public HttpRequestFunction httpRequestFunction(WebClient webClient, HttpRequestProperties properties) {
public HttpRequestFunction httpRequestFunction(WebClient webClient, HttpRequestFunctionProperties properties) {
return new HttpRequestFunction(webClient, properties);
}
@@ -58,28 +61,57 @@ public class HttpRequestFunctionConfiguration {
* Function that accepts a {@code Flux<Message<?>>} containing body and headers and
* returns a {@code Flux<ResponseEntity<?>>}.
*/
public static class HttpRequestFunction implements Function<Flux<Message<?>>, Flux<ResponseEntity<?>>> {
public static class HttpRequestFunction implements Function<Flux<Message<?>>, Flux<?>> {
private final WebClient webClient;
private final UriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory();
private final HttpRequestProperties properties;
private final HttpRequestFunctionProperties properties;
public HttpRequestFunction(WebClient webClient, HttpRequestProperties properties) {
public HttpRequestFunction(WebClient webClient, HttpRequestFunctionProperties properties) {
this.webClient = webClient;
this.properties = properties;
}
@Override
public Flux<ResponseEntity<?>> apply(Flux<Message<?>> messageFlux) {
public Flux<?> apply(Flux<Message<?>> messageFlux) {
return messageFlux.flatMap(message -> this.webClient
.method(properties.getHttpMethod())
.uri(uriBuilderFactory.uriString(properties.getUrl()).build())
.bodyValue(properties.getBody() == null ? message.getPayload() : properties.getBody())
.headers(httpHeaders -> httpHeaders.addAll(properties.getHeaders()))
.method(resolveHttpMethod(message))
.uri(uriBuilderFactory.uriString(resolveUrl(message)).build())
.bodyValue(resolveBody(message))
.headers(httpHeaders -> httpHeaders.addAll(resolveHeaders(message)))
.retrieve()
.toEntity(properties.getExpectedResponseType())
.map(responseEntity -> properties.getReplyExpression().getValue(responseEntity))
.timeout(Duration.ofMillis(properties.getTimeout())));
}
private String resolveUrl(Message<?> message) {
return properties.getUrlExpression().getValue(message, String.class);
}
private HttpMethod resolveHttpMethod(Message<?> message) {
return properties.getHttpMethodExpression().getValue(message, HttpMethod.class);
}
private Object resolveBody(Message<?> message) {
return properties.getBodyExpression() != null ? properties.getBodyExpression().getValue(message)
: message.getPayload();
}
private HttpHeaders resolveHeaders(Message<?> message) {
HttpHeaders headers = new HttpHeaders();
if (properties.getHeadersExpression() != null) {
Map<?, ?> headersMap = properties.getHeadersExpression().getValue(message, Map.class);
for (Map.Entry<?, ?> header : headersMap.entrySet()) {
if (header.getKey() != null && header.getValue() != null) {
headers.add(header.getKey().toString(),
header.getValue().toString());
}
}
}
return headers;
}
}
}

View File

@@ -16,12 +16,14 @@
package org.springframework.cloud.fn.http.request;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.expression.Expression;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.validation.annotation.Validated;
/**
@@ -35,25 +37,9 @@ import org.springframework.validation.annotation.Validated;
*/
@Validated
@ConfigurationProperties("http.request")
public class HttpRequestFunctionProperties implements HttpRequestProperties {
private static final HttpMethod DEFAULT_HTTP_METHOD = HttpMethod.GET;
public class HttpRequestFunctionProperties {
private static final Class<?> DEFAULT_RESPONSE_TYPE = String.class;
/**
* The URL to issue an http request to, as a static value.
*/
private String url;
/**
* The (static) request body; if neither this nor bodyExpression is provided, the payload
* will be used.
*/
private Object body;
/**
* The kind of http method to use.
*/
private HttpMethod httpMethod = DEFAULT_HTTP_METHOD;
/**
* The type used to interpret the response.
@@ -66,33 +52,49 @@ public class HttpRequestFunctionProperties implements HttpRequestProperties {
private long timeout = 30_000;
/**
* A Map of HTTP request headers.
* A SpEL expression against incoming message to determine the URL to use.
*/
private HttpHeaders headers = new HttpHeaders();
private Expression urlExpression;
@NotEmpty
@Override
public String getUrl() {
return this.url;
/**
* A SpEL expression to derive the request method from the incoming message.
*/
private Expression httpMethodExpression = new ValueExpression(HttpMethod.GET);
/**
* A SpEL expression to derive the request body from the incoming message.
*/
private Expression bodyExpression;
/**
* A SpEL expression used to derive the http headers map to use.
*/
private Expression headersExpression;
/**
* A SpEL expression used to compute the final result, applied against the whole http
* {@link org.springframework.http.ResponseEntity}.
*/
private Expression replyExpression = new FunctionExpression<ResponseEntity>(ResponseEntity::getBody);
@NotNull
public Expression getUrlExpression() {
return urlExpression;
}
public void setUrl(String url) {
this.url = url;
public void setUrlExpression(Expression urlExpression) {
this.urlExpression = urlExpression;
}
public Expression getHttpMethodExpression() {
return httpMethodExpression;
}
public void setHttpMethodExpression(Expression httpMethodExpression) {
this.httpMethodExpression = httpMethodExpression;
}
@NotNull
@Override
public HttpMethod getHttpMethod() {
return httpMethod;
}
public void setHttpMethod(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
}
@NotNull
@Override
public Class<?> getExpectedResponseType() {
return expectedResponseType;
}
@@ -101,16 +103,6 @@ public class HttpRequestFunctionProperties implements HttpRequestProperties {
this.expectedResponseType = expectedResponseType;
}
@Override
public Object getBody() {
return body;
}
public void setBody(Object body) {
this.body = body;
}
@Override
public long getTimeout() {
return this.timeout;
}
@@ -119,13 +111,29 @@ public class HttpRequestFunctionProperties implements HttpRequestProperties {
this.timeout = timeout;
}
@Override
public HttpHeaders getHeaders() {
return headers;
public Expression getBodyExpression() {
return bodyExpression;
}
public void setHeaders(HttpHeaders headers) {
this.headers = headers;
public void setBodyExpression(Expression bodyExpression) {
this.bodyExpression = bodyExpression;
}
public Expression getHeadersExpression() {
return headersExpression;
}
public void setHeadersExpression(Expression headersExpression) {
this.headersExpression = headersExpression;
}
@NotNull
public Expression getReplyExpression() {
return replyExpression;
}
public void setReplyExpression(Expression replyExpression) {
this.replyExpression = replyExpression;
}
}

View File

@@ -1,29 +0,0 @@
/*
* Copyright 2020-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.fn.http.request;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
public interface HttpRequestProperties {
String getUrl();
Object getBody();
long getTimeout();
HttpHeaders getHeaders();
Class<?> getExpectedResponseType();
HttpMethod getHttpMethod();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018-2020 the original author or authors.
* Copyright 2020-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@ import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
@@ -45,22 +44,17 @@ import org.springframework.messaging.support.MessageBuilder;
import static org.assertj.core.api.Assertions.assertThat;
public class HttpRequestFunctionTestApplicationTests {
private MockWebServer server;
private MockWebServer server = new MockWebServer();
private ApplicationContextRunner runner;
@BeforeEach
void setup() {
this.server = new MockWebServer();
this.runner = new ApplicationContextRunner()
.withUserConfiguration(HttpRequestFunctionTestApplication.class)
.withPropertyValues(
"http.request.url=" + url());
}
@AfterEach
void shutdown() throws IOException {
this.server.shutdown();
"http.request.reply-expression=#root",
"http.request.url-expression='" + url() + "'");
}
@Test
@@ -70,11 +64,12 @@ public class HttpRequestFunctionTestApplicationTests {
.setResponseCode(HttpStatus.OK.value())
.setBody("hello"));
runner.run(context -> {
runner.withPropertyValues("http.request.http-method-expression='POST'").run(context -> {
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
Message<?> message = MessageBuilder.withPayload("").build();
StepVerifier.create(httpRequestFunction.apply(Flux.just(message)))
.assertNext((ResponseEntity r) -> {
.assertNext(o -> {
ResponseEntity r = (ResponseEntity) o;
assertThat(r.getBody()).isEqualTo("hello");
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
})
@@ -96,22 +91,24 @@ public class HttpRequestFunctionTestApplicationTests {
}
});
runner.withPropertyValues("http.request.http-method=POST", "http.request.headers[Content-Type]=application/json").run(context -> {
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
String json = "{\"hello\":\"world\"}";
Message<?> message = MessageBuilder.withPayload(json)
.build();
StepVerifier.create(httpRequestFunction.apply(Flux.just(message)))
.assertNext((ResponseEntity r) -> {
assertThat(r.getBody()).isEqualTo(json);
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
assertThat(r.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
})
.expectComplete()
.verify();
RecordedRequest request = server.takeRequest(100, TimeUnit.MILLISECONDS);
assertThat(request.getMethod()).isEqualTo("POST");
});
runner.withPropertyValues("http.request.http-method-expression='POST'",
"http.request.headers-expression={'Content-Type':'application/json'}").run(context -> {
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
String json = "{\"hello\":\"world\"}";
Message<?> message = MessageBuilder.withPayload(json)
.build();
StepVerifier.create(httpRequestFunction.apply(Flux.just(message)))
.assertNext(o -> {
ResponseEntity r = (ResponseEntity) o;
assertThat(r.getBody()).isEqualTo(json);
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
assertThat(r.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
})
.expectComplete()
.verify();
RecordedRequest request = server.takeRequest(100, TimeUnit.MILLISECONDS);
assertThat(request.getMethod()).isEqualTo("POST");
});
}
@Test
@@ -127,15 +124,15 @@ public class HttpRequestFunctionTestApplicationTests {
}
});
runner.withPropertyValues("http.request.http-method=POST",
"http.request.headers[Content-Type]=application/json",
runner.withPropertyValues("http.request.http-method-expression='POST'",
"http.request.expected-response-type=" + Map.class.getName()).run(context -> {
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
Map<String, String> json = Collections.singletonMap("hello", "world");
Message<?> message = MessageBuilder.withPayload(json)
.build();
StepVerifier.create(httpRequestFunction.apply(Flux.just(message)))
.assertNext((ResponseEntity r) -> {
.assertNext(o -> {
ResponseEntity r = (ResponseEntity) o;
assertThat(r.getBody()).isEqualTo(json);
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
assertThat(r.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
@@ -149,7 +146,6 @@ public class HttpRequestFunctionTestApplicationTests {
@Test
void shouldDelete() {
server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
@@ -160,13 +156,14 @@ public class HttpRequestFunctionTestApplicationTests {
}
});
runner.withPropertyValues("http.request.http-method=DELETE",
runner.withPropertyValues("http.request.http-method-expression='DELETE'",
"http.request.expected-response-type=" + Void.class.getName()).run(context -> {
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
Message<?> message = MessageBuilder.withPayload("")
.build();
StepVerifier.create(httpRequestFunction.apply(Flux.just(message)))
.assertNext((ResponseEntity r) -> {
.assertNext(o -> {
ResponseEntity r = (ResponseEntity) o;
assertThat(r.getBody()).isNull();
assertThat(r.getStatusCode().is2xxSuccessful()).isTrue();
})
@@ -188,6 +185,106 @@ public class HttpRequestFunctionTestApplicationTests {
});
}
@Test
void requestUsingExpressions() {
server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE,
recordedRequest.getHeader(HttpHeaders.ACCEPT))
.setBody(recordedRequest.getBody())
.setResponseCode(HttpStatus.OK.value());
}
});
runner.withPropertyValues(
"http.request.url-expression=headers['url']",
"http.request.http-method-expression=headers['method']",
"http.request.body-expression=headers['body']",
"http.request.headers-expression={Accept:'application/json'}")
.run(context -> {
Message<?> message = MessageBuilder.withPayload("")
.setHeader("url", url())
.setHeader("method", "POST")
.setHeader("body", "{\"hello\":\"world\"}")
.build();
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
StepVerifier.create(httpRequestFunction.apply(Flux.just(message)))
.assertNext(o -> {
ResponseEntity responseEntity = (ResponseEntity) o;
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isEqualTo(message.getHeaders().get("body"));
assertThat(responseEntity.getHeaders().getContentType())
.isEqualTo(MediaType.APPLICATION_JSON);
})
.expectComplete()
.verify();
});
}
@Test
void requestUsingReturnType() {
server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE,
recordedRequest.getHeader(HttpHeaders.CONTENT_TYPE))
.setBody(recordedRequest.getBody())
.setResponseCode(HttpStatus.OK.value());
}
});
runner.withPropertyValues(
"http.request..http-method-expression='POST'",
"http.request.headers-expression={'Content-Type':'application/octet-stream'}",
"http.request.expected-response-type=byte[]")
.run(context -> {
Message<?> message = MessageBuilder.withPayload("hello")
.build();
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
StepVerifier.create(httpRequestFunction.apply(Flux.just(message)))
.assertNext(o -> {
ResponseEntity responseEntity = (ResponseEntity) o;
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isEqualTo("hello".getBytes());
assertThat(responseEntity.getHeaders().getContentType())
.isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
})
.expectComplete()
.verify();
});
}
@Test
void requestUsingJsonPathMethodExpression() {
server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
return new MockResponse()
.setBody(recordedRequest.getBody())
.setHeader("method", recordedRequest.getMethod())
.setResponseCode(HttpStatus.OK.value());
}
});
runner.withPropertyValues(
"http.request.http-method-expression=#jsonPath(payload,'$.myMethod')")
.run(context -> {
Message<?> message = MessageBuilder
.withPayload("{\"name\":\"Fred\",\"age\":41, \"myMethod\":\"POST\"}")
.build();
HttpRequestFunction httpRequestFunction = context.getBean(HttpRequestFunction.class);
StepVerifier.create(httpRequestFunction.apply(Flux.just(message)))
.assertNext(o -> {
ResponseEntity responseEntity = (ResponseEntity) o;
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isEqualTo(message.getPayload());
})
.expectComplete()
.verify();
});
}
private String url() {
return String.format("http://localhost:%d", server.getPort());
}