From 790e502afc12a3a03998094c3b9a879dfe08c18e Mon Sep 17 00:00:00 2001 From: lony2003 Date: Tue, 1 Apr 2025 21:58:23 +0800 Subject: [PATCH 01/16] feat(springboot openfeign): An OpenFeign Client for Dapr Invokes A OpenFeign Client and it's AutoConfiguration using Dapr Invoke APIs. Splited into two librarys, dapr-openfeign-client is the pure client of OpenFeign, and dapr-spring-openfeign is the plugin of Spring Cloud OpenFeign. Currently only HTTP Invoke Method and Invoke Binding supported, and Invoke Binding is not always recommend to use this client, depends on what binding component is. Signed-off-by: lony2003 --- dapr-spring/dapr-openfeign-client/pom.xml | 36 ++ .../spotbugs-exclude.xml | 11 + .../io/dapr/feign/DaprInvokeFeignClient.java | 385 ++++++++++++++++++ .../feign/DaprFeignClientBindingTest.java | 65 +++ .../dapr/feign/DaprFeignClientMethodTest.java | 66 +++ .../dapr-spring-boot-starter/pom.xml | 6 + dapr-spring/dapr-spring-openfeign/pom.xml | 56 +++ .../openfeign/annotation/UseDaprClient.java | 29 ++ .../DaprFeignClientAutoConfiguration.java | 44 ++ .../DaprFeignClientProperties.java | 37 ++ .../targeter/DaprClientTargeter.java | 65 +++ ...itional-spring-configuration-metadata.json | 25 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../DaprOpenFeignClientTestApplication.java | 13 + .../openfeign/DaprOpenFeignClientTests.java | 48 +++ .../client/DaprInvokeBindingClient.java | 13 + .../client/DaprInvokeMethodClient.java | 12 + .../openfeign/client/DemoFeignClient.java | 7 + .../src/test/resources/application.properties | 1 + dapr-spring/pom.xml | 2 + sdk-tests/pom.xml | 18 + .../io/dapr/it/spring/feign/DaprFeignIT.java | 74 ++++ .../feign/DaprFeignTestApplication.java | 13 + .../it/spring/feign/PostgreBindingClient.java | 12 + .../java/io/dapr/it/spring/feign/Result.java | 13 + .../it/spring/feign/TestMethodClient.java | 22 + .../it/spring/feign/TestRestController.java | 25 ++ 27 files changed, 1099 insertions(+) create mode 100644 dapr-spring/dapr-openfeign-client/pom.xml create mode 100644 dapr-spring/dapr-openfeign-client/spotbugs-exclude.xml create mode 100644 dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java create mode 100644 dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java create mode 100644 dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java create mode 100644 dapr-spring/dapr-spring-openfeign/pom.xml create mode 100644 dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/annotation/UseDaprClient.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeter.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTestApplication.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTests.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeBindingClient.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeMethodClient.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DemoFeignClient.java create mode 100644 dapr-spring/dapr-spring-openfeign/src/test/resources/application.properties create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignTestApplication.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java diff --git a/dapr-spring/dapr-openfeign-client/pom.xml b/dapr-spring/dapr-openfeign-client/pom.xml new file mode 100644 index 0000000000..5bc42bd6ed --- /dev/null +++ b/dapr-spring/dapr-openfeign-client/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + io.dapr.spring + dapr-spring-parent + 0.15.0-SNAPSHOT + + + dapr-openfeign-client + + + 17 + 17 + UTF-8 + ./spotbugs-exclude.xml + + + + + io.github.openfeign + feign-core + 13.2.1 + compile + + + io.github.openfeign + feign-jaxrs + 13.5 + test + + + + \ No newline at end of file diff --git a/dapr-spring/dapr-openfeign-client/spotbugs-exclude.xml b/dapr-spring/dapr-openfeign-client/spotbugs-exclude.xml new file mode 100644 index 0000000000..f47dce3e23 --- /dev/null +++ b/dapr-spring/dapr-openfeign-client/spotbugs-exclude.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java b/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java new file mode 100644 index 0000000000..2582a2f619 --- /dev/null +++ b/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java @@ -0,0 +1,385 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.feign; + +import feign.Client; +import feign.Request; +import feign.Response; +import feign.Util; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.client.domain.HttpExtension; +import io.dapr.client.domain.InvokeBindingRequest; +import io.dapr.client.domain.InvokeMethodRequest; +import io.dapr.utils.TypeRef; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * This module directs Feign's requests to Dapr, which is a microservice framework. Ex. + * + *

Currently, Dapr supports two ways to invoke operations, which is invokeBinding (Output Binding) and + * invokeMethod, so this Client supports two schemas: http://binding.xxx or http://method.xxx
+ * You don't have to mind why there is a http schema at start, it's just a trick to hack Spring Boot Openfeign. + * + *

For invokeMethod, there are two types of information in the url, which is very similar to an HTTP URL, except that + * the host in the HTTP URL is converted to appId, and the path (excluding "/") is converted to methodName.
+ * For example, if you have a method which appid is "myApp", and methodName is "getAll", then the url of this request is + * "http://method.myApp/getAll".
+ * You can also set HTTP Headers if you like, the Client will handle them.
+ * Currently only HTTP invokes are supported, but I think we will support grpc invokes in the future, may be the url + * will be "http://method_grpc.myApp/getAll" or something. + * + *

For invokeBinding, there are also two types of information in the url, the host is the bindingName, and the path + * is the operation.
+ * Note: different bindings support different operations, so you have to check + * the documentation of + * Dapr for that.
+ * For example, if you have a binding which bindingName is "myBinding", and the operation supported is "create", and + * then the url of this request is "http://binding.myBinding/create"
+ * You can put some metadata into the Header of your Feign Request, the Client will handle them. + * + *

As for response, the result code is always 200 OK, and if client have met any error, it will throw an IOException + * for that.
+ * Currently, we have no method to gain metadata from server as Dapr Client doesn't have methods to do that, so headers + * will be blank. If Accept header has set in request, a fake Content-Type header will be created in response, and it + * will be the first value of Accept header. + * + *

+ * MyAppData response = Feign.builder().client(new DaprFeignClient()).target(MyAppData.class,
+ * "http://binding.myBinding/create");
+ * 
+ */ +public class DaprInvokeFeignClient implements Client { + + private static final String DOT = "\\."; + + private final int retry; + private final int timeout; + + private final DaprClient daprClient; + + /** + * Default Client creation with no arguments. + * + *

+ * retry 5 tries with 2000ms waiting and default DaprClient. + *

+ */ + public DaprInvokeFeignClient() { + daprClient = new DaprClientBuilder().build(); + retry = 2000; + timeout = 3; + } + + /** + * Client creation with DaprClient Specified. + * + *

+ * retry 5 tries with 2000ms waiting. + *

+ * + * @param daprClient client sepcified + */ + public DaprInvokeFeignClient(DaprClient daprClient) { + this.daprClient = daprClient; + retry = 2000; + timeout = 5; + } + + /** + * Client creation with DaprClient, wait time, retry time Specified. + * + * @param daprClient client sepcified + * @param timeout wait time (ms) + * @param retry retry times + */ + public DaprInvokeFeignClient(DaprClient daprClient, int timeout, int retry) { + this.daprClient = daprClient; + this.timeout = timeout; + this.retry = retry; + } + + @Override + public Response execute(Request request, Request.Options options) throws IOException { + URI uri; + try { + uri = new URI(request.url()); + } catch (URISyntaxException e) { + throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); + } + + String schemaAndHost = uri.getHost(); + + String[] splitSchemaAndHost = schemaAndHost.split(DOT); + String schema; + + if (splitSchemaAndHost.length >= 2) { + schema = splitSchemaAndHost[0]; + } else { + throw new IOException("Host '" + schemaAndHost + "' is not supported"); + } + + Mono result = null; + + switch (schema) { + case "method": + result = getResultFromInvokeHTTPMethodRequest(uri, request); + break; + case "binding": + result = getResultFromInvokeBindingRequest(uri, request); + break; + default: + throw new IOException("Schema '" + schema + "' is not supported"); + } + + Map> headerMap = new HashMap<>(); + + if (request.headers().containsKey("Accept")) { + headerMap.put("Content-Type", List.of(request.headers().get("Accept") + .stream().findFirst() + .orElseThrow(() -> new IOException("Accept header can not be null")))); + } + + return Response.builder() + .status(200) + .reason("OK") + .request(request) + .headers(headerMap) + .body(toResponseBody(result, options)) + .build(); + } + + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + private Mono getResultFromInvokeHTTPMethodRequest(URI uri, Request request) throws IOException { + String[] splitSchemaAndHost = uri.getHost().split(DOT); + + List hostList = new ArrayList<>(List.of(splitSchemaAndHost)); + hostList.remove(0); + + InvokeMethodRequest invokeMethodRequest = null; + try { + invokeMethodRequest = toInvokeMethodHTTPRequest(new URI("method", + uri.getUserInfo(), + String.join(".", hostList), + uri.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment()), request); + } catch (URISyntaxException e) { + throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); + } + + return daprClient.invokeMethod(invokeMethodRequest, TypeRef.BYTE_ARRAY); + + } + + private Mono getResultFromInvokeBindingRequest(URI uri, Request request) throws IOException { + String[] splitSchemaAndHost = uri.getHost().split(DOT); + + List hostList = new ArrayList<>(List.of(splitSchemaAndHost)); + hostList.remove(0); + + InvokeBindingRequest invokeBindingRequest = null; + try { + invokeBindingRequest = toInvokeBindingRequest(new URI("binding", + uri.getUserInfo(), + String.join(".", hostList), + uri.getPort(), + uri.getPath(), + uri.getQuery(), + uri.getFragment()), request); + } catch (URISyntaxException e) { + throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); + } + + return daprClient.invokeBinding(invokeBindingRequest, TypeRef.BYTE_ARRAY); + } + + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + private InvokeMethodRequest toInvokeMethodHTTPRequest(URI uri, Request request) throws IOException { + + String path = uri.getPath(); + if (path.startsWith("/")) { + path = path.substring(1); + } + + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 2); + } + + InvokeMethodRequest invokeMethodRequest = new InvokeMethodRequest(uri.getHost(), path); + invokeMethodRequest.setMetadata(toHeader(request.headers())); + + if (request.body() != null) { + invokeMethodRequest.setBody(request.body()); + } + + invokeMethodRequest.setContentType(getContentType(request)); + invokeMethodRequest.setHttpExtension(toHttpExtension(request.httpMethod().name().toLowerCase())); + + return invokeMethodRequest; + } + + private HttpExtension toHttpExtension(String method) throws IOException { + switch (method) { + case "none": + return HttpExtension.NONE; + case "put": + return HttpExtension.PUT; + case "post": + return HttpExtension.POST; + case "delete": + return HttpExtension.DELETE; + case "head": + return HttpExtension.HEAD; + case "connect": + return HttpExtension.CONNECT; + case "options": + return HttpExtension.OPTIONS; + case "trace": + return HttpExtension.TRACE; + case "get": + return HttpExtension.GET; + default: + throw new IOException("Method '" + method + "' is not supported"); + } + } + + private String getContentType(Request request) { + String contentType = null; + for (Map.Entry> entry : request.headers().entrySet()) { + if (entry.getKey().equalsIgnoreCase("Content-Type")) { + Collection values = entry.getValue(); + if (values != null && !values.isEmpty()) { + contentType = values.iterator().next(); + break; + } + } + } + return contentType; + } + + private InvokeBindingRequest toInvokeBindingRequest(URI uri, Request request) throws IOException { + String path = uri.getPath(); + if (path.startsWith("/")) { + path = path.substring(1); + } + + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 2); + } + + if (path.split("/").length > 1) { + throw new IOException("Binding Operation '" + path + "' is not reconigzed"); + } + + InvokeBindingRequest invokeBindingRequest = new InvokeBindingRequest(uri.getHost(), path); + invokeBindingRequest.setMetadata(toHeader(request.headers())); + + if (request.body() != null) { + invokeBindingRequest.setData(request.body()); + } + + return invokeBindingRequest; + } + + private Map toHeader(Map> header) { + Map headerMap = new HashMap<>(); + + // request headers + for (Map.Entry> headerEntry : header.entrySet()) { + String headerName = headerEntry.getKey(); + + if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) { + continue; + } + + for (String headerValue : headerEntry.getValue()) { + headerMap.put(headerName, headerValue); + } + } + + return headerMap; + } + + private Response.Body toResponseBody(Mono response, Request.Options options) throws IOException { + byte[] result; + + try { + result = response.retry(retry).block(Duration.of(timeout, + TimeUnit.MILLISECONDS.toChronoUnit())); + + if (result == null) { + result = new byte[0]; + } + + } catch (RuntimeException e) { + throw new IOException("Can not get Response", e); + } + + + byte[] finalResult = result; + return new Response.Body() { + @Override + public Integer length() { + return finalResult.length; + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public InputStream asInputStream() throws IOException { + return new ByteArrayInputStream(finalResult); + } + + @SuppressWarnings("deprecation") + @Override + public Reader asReader() throws IOException { + return new InputStreamReader(asInputStream(), UTF_8); + } + + @Override + public Reader asReader(Charset charset) throws IOException { + return new InputStreamReader(asInputStream(), charset); + } + + @Override + public void close() throws IOException { + + } + }; + } + +} diff --git a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java new file mode 100644 index 0000000000..6e10b7a672 --- /dev/null +++ b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.feign; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import feign.Feign; +import feign.Headers; +import feign.RequestLine; +import feign.Response; +import io.dapr.client.DaprClient; +import io.dapr.client.domain.InvokeBindingRequest; +import io.dapr.utils.TypeRef; +import reactor.core.publisher.Mono; + +@ExtendWith(MockitoExtension.class) +public class DaprFeignClientBindingTest { + + @Mock + DaprClient daprClient; + + @Test + void DaprFeignClient_testMockBindingInvoke() { + DaprFeignClientTestInterface repository = + newBuilder().target(DaprFeignClientTestInterface.class, "http://binding.myBinding"); + + assertEquals(200, repository.getWithContentType().status()); + assertEquals(0, repository.get().body().length()); + } + + public Feign.Builder newBuilder() { + Mockito.when(daprClient.invokeBinding(Mockito.any(InvokeBindingRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) + .thenReturn(Mono.just(new byte[0])); + + return Feign.builder().client(new DaprInvokeFeignClient(daprClient)); + } + + public interface DaprFeignClientTestInterface { + + @RequestLine("GET /create") + @Headers({"Accept: text/plain", "Content-Type: text/plain"}) + Response getWithContentType(); + + @RequestLine("GET /get") + Response get(); + } +} diff --git a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java new file mode 100644 index 0000000000..249bda4119 --- /dev/null +++ b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.feign; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import feign.Feign; +import feign.Headers; +import feign.RequestLine; +import feign.Response; +import io.dapr.client.DaprClient; +import io.dapr.client.domain.InvokeMethodRequest; +import io.dapr.utils.TypeRef; +import reactor.core.publisher.Mono; + +@ExtendWith(MockitoExtension.class) +public class DaprFeignClientMethodTest { + + @Mock + DaprClient daprClient; + + @Test + void DaprFeignClient_testMockMethodInvoke() { + DaprFeignClientTestInterface repository = + newBuilder().target(DaprFeignClientTestInterface.class, "http://method.myApp/"); + + assertEquals(12, repository.getWithContentType().body().length()); + } + + public Feign.Builder newBuilder() { + Mockito.when(daprClient.invokeMethod(Mockito.any(InvokeMethodRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) + .thenReturn(Mono.just("hello world!".getBytes(StandardCharsets.UTF_8))); + + return Feign.builder().client(new DaprInvokeFeignClient(daprClient)); + } + + public interface DaprFeignClientTestInterface { + + @RequestLine("GET /getAll") + @Headers({"Accept: text/plain", "Content-Type: text/plain"}) + Response getWithContentType(); + + @RequestLine("GET /abc") + Response get(); + } +} diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml index 623040b378..87ce7edadf 100644 --- a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml @@ -45,6 +45,12 @@ dapr-spring-workflows ${project.parent.version} + + io.dapr.spring + dapr-spring-openfeign + ${project.parent.version} + true + diff --git a/dapr-spring/dapr-spring-openfeign/pom.xml b/dapr-spring/dapr-spring-openfeign/pom.xml new file mode 100644 index 0000000000..4922c11ac9 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + io.dapr.spring + dapr-spring-parent + 0.15.0-SNAPSHOT + + + dapr-spring-openfeign + dapr-spring-openfeign + Dapr Spring OpenFeign + jar + + + 17 + 17 + UTF-8 + + + + + org.springframework.cloud + spring-cloud-openfeign-core + 4.1.4 + compile + + + io.dapr.spring + dapr-openfeign-client + ${project.parent.version} + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.cloud + spring-cloud-starter-openfeign + 4.1.4 + test + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + + \ No newline at end of file diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/annotation/UseDaprClient.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/annotation/UseDaprClient.java new file mode 100644 index 0000000000..2827073d6d --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/annotation/UseDaprClient.java @@ -0,0 +1,29 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Let OpenFeign create Client Proxy using Dapr Client to handle requests. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface UseDaprClient { +} diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java new file mode 100644 index 0000000000..558b05f381 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign.autoconfigure; + +import io.dapr.client.DaprClient; +import io.dapr.feign.DaprInvokeFeignClient; +import io.dapr.spring.openfeign.targeter.DaprClientTargeter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.cloud.openfeign.Targeter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(DaprFeignClientProperties.class) +@ConditionalOnProperty(name = "dapr.feign.enabled", matchIfMissing = true) +@ConditionalOnClass(FeignAutoConfiguration.class) +public class DaprFeignClientAutoConfiguration { + + @Bean + public Targeter targeter(DaprInvokeFeignClient daprInvokeFeignClient) { + return new DaprClientTargeter(daprInvokeFeignClient); + } + + @Bean + @ConditionalOnMissingBean + public DaprInvokeFeignClient daprInvokeFeignClient(DaprClient daprClient) { + return new DaprInvokeFeignClient(daprClient); + } +} diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java new file mode 100644 index 0000000000..fbf50568c4 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java @@ -0,0 +1,37 @@ +package io.dapr.spring.openfeign.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(DaprFeignClientProperties.PROPERTY_PREFIX) +public class DaprFeignClientProperties { + + public static final String PROPERTY_PREFIX = "dapr.feign"; + + private boolean enabled; + private Integer timeout; + private Integer retries; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Integer getTimeout() { + return timeout; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public Integer getRetries() { + return retries; + } + + public void setRetries(Integer retries) { + this.retries = retries; + } +} diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeter.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeter.java new file mode 100644 index 0000000000..bad5edb1ce --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeter.java @@ -0,0 +1,65 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign.targeter; + +import feign.Client; +import feign.Feign; +import feign.Target; +import io.dapr.feign.DaprInvokeFeignClient; +import io.dapr.spring.openfeign.annotation.UseDaprClient; +import org.springframework.cloud.openfeign.FeignClientFactory; +import org.springframework.cloud.openfeign.FeignClientFactoryBean; +import org.springframework.cloud.openfeign.Targeter; + +import java.lang.reflect.Field; + +public class DaprClientTargeter implements Targeter { + + private final DaprInvokeFeignClient daprInvokeFeignClient; + + public DaprClientTargeter(DaprInvokeFeignClient daprInvokeFeignClient) { + this.daprInvokeFeignClient = daprInvokeFeignClient; + } + + @Override + public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context, + Target.HardCodedTarget target) { + Class builderClass = Feign.Builder.class; + + Client defaultClient = null; + + try { + Field clientField = builderClass.getDeclaredField("client"); + clientField.setAccessible(true); + + defaultClient = (Client) clientField.get(feign); + + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + Class classOfT = target.type(); + UseDaprClient useDaprClient = classOfT.getAnnotation(UseDaprClient.class); + + if (useDaprClient != null) { + feign.client(daprInvokeFeignClient); + } + + T targetInstance = feign.target(target); + + feign.client(defaultClient); + + return targetInstance; + } +} diff --git a/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000000..a1501c23e8 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,25 @@ +{ + "properties": [ + { + "name": "dapr.feign.enabled", + "type": "java.lang.Boolean", + "defaultValue": true, + "sourceType": "io.dapr.spring.openfeign.autoconfigure.DaprFeignClientProperties", + "description": "enable dapr openfeign or not." + }, + { + "name": "dapr.feign.timeout", + "type": "java.lang.Integer", + "defaultValue": 2000, + "sourceType": "io.dapr.spring.openfeign.autoconfigure.DaprFeignClientProperties", + "description": "timeout for dapr invoke." + }, + { + "name": "dapr.feign.retries", + "type": "java.lang.Integer", + "defaultValue": 3, + "sourceType": "io.dapr.spring.openfeign.autoconfigure.DaprFeignClientProperties", + "description": "retries for invoke fails." + } + ] +} \ No newline at end of file diff --git a/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..995fe8a402 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +io.dapr.spring.openfeign.autoconfigure.DaprFeignClientAutoConfiguration \ No newline at end of file diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTestApplication.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTestApplication.java new file mode 100644 index 0000000000..cac447fd27 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTestApplication.java @@ -0,0 +1,13 @@ +package io.dapr.spring.openfeign; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@EnableFeignClients +@SpringBootApplication +public class DaprOpenFeignClientTestApplication { + public static void main(String[] args) { + SpringApplication.run(DaprOpenFeignClientTestApplication.class, args); + } +} diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTests.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTests.java new file mode 100644 index 0000000000..8650b84f6d --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTests.java @@ -0,0 +1,48 @@ +package io.dapr.spring.openfeign; + +import io.dapr.client.DaprClient; +import io.dapr.client.domain.InvokeBindingRequest; +import io.dapr.client.domain.InvokeMethodRequest; +import io.dapr.spring.openfeign.client.DaprInvokeBindingClient; +import io.dapr.spring.openfeign.client.DaprInvokeMethodClient; +import io.dapr.utils.TypeRef; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +public class DaprOpenFeignClientTests { + + @MockBean + DaprClient daprClient; + + @Autowired + DaprInvokeBindingClient invokeBindingClient; + + @Autowired + DaprInvokeMethodClient invokeMethodClient; + + @Test + public void daprInvokeMethodTest() { + Mockito.when(daprClient.invokeMethod(Mockito.any(InvokeMethodRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) + .thenReturn(Mono.just("Hello World!".getBytes(StandardCharsets.UTF_8))); + + assertEquals("Hello World!", invokeMethodClient.getQuery()); + } + + @Test + public void daprInvokeBindingTest() { + Mockito.when(daprClient.invokeBinding(Mockito.any(InvokeBindingRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) + .thenReturn(Mono.just("Hello World!".getBytes(StandardCharsets.UTF_8))); + + assertEquals("Hello World!", invokeBindingClient.getQuery()); + } + +} diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeBindingClient.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeBindingClient.java new file mode 100644 index 0000000000..8255436e77 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeBindingClient.java @@ -0,0 +1,13 @@ +package io.dapr.spring.openfeign.client; + +import io.dapr.spring.openfeign.annotation.UseDaprClient; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + + +@FeignClient(name = "invoke-binding", url = "http://binding.democlient/") +@UseDaprClient +public interface DaprInvokeBindingClient { + @GetMapping(value = "/create", produces = "text/plain;charset=utf-8") + String getQuery(); +} diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeMethodClient.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeMethodClient.java new file mode 100644 index 0000000000..0087325d21 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeMethodClient.java @@ -0,0 +1,12 @@ +package io.dapr.spring.openfeign.client; + +import io.dapr.spring.openfeign.annotation.UseDaprClient; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient(name = "invoke-method", url = "http://method.democlient/") +@UseDaprClient +public interface DaprInvokeMethodClient { + @GetMapping(value = "/hello", produces = "text/plain;charset=utf-8") + String getQuery(); +} diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DemoFeignClient.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DemoFeignClient.java new file mode 100644 index 0000000000..a0ae9faf5b --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DemoFeignClient.java @@ -0,0 +1,7 @@ +package io.dapr.spring.openfeign.client; + +import org.springframework.cloud.openfeign.FeignClient; + +@FeignClient(name = "demo-client", url = "http://dapr.io") +public interface DemoFeignClient { +} diff --git a/dapr-spring/dapr-spring-openfeign/src/test/resources/application.properties b/dapr-spring/dapr-spring-openfeign/src/test/resources/application.properties new file mode 100644 index 0000000000..a14ffccb40 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/test/resources/application.properties @@ -0,0 +1 @@ +dapr.feign.enabled=true \ No newline at end of file diff --git a/dapr-spring/pom.xml b/dapr-spring/pom.xml index fe4ebaa172..5a78237ccf 100644 --- a/dapr-spring/pom.xml +++ b/dapr-spring/pom.xml @@ -25,6 +25,8 @@ dapr-spring-boot-tests dapr-spring-boot-starters/dapr-spring-boot-starter dapr-spring-boot-starters/dapr-spring-boot-starter-test + dapr-openfeign-client + dapr-spring-openfeign diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index c1ffacad0e..ba43ba6425 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -30,6 +30,7 @@ 3.25.5 1.41.0 3.4.3 + 2024.0.0 1.5.16 3.9.1 1.20.0 @@ -50,6 +51,13 @@ pom import + + org.springframework.cloud + spring-cloud-dependencies + ${springcloud.version} + pom + import + org.junit.platform junit-platform-commons @@ -260,6 +268,16 @@ junit-platform-engine test + + org.springframework.cloud + spring-cloud-starter-openfeign + test + + + io.dapr.spring + dapr-spring-openfeign + 0.15.0-SNAPSHOT + diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java new file mode 100644 index 0000000000..fa7f7e8497 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -0,0 +1,74 @@ +package io.dapr.it.spring.feign; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.HashMap; +import java.util.Map; + +import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME; +import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = {"dapr.feign.enable=true"}) +@Testcontainers +@Tag("testcontainers") +public class DaprFeignIT { + private static final String CONNECTION_STRING = + "host=postgres-repository user=postgres password=password port=5432 connect_timeout=10 database=dapr_db_repository"; + + private static final Map BINDING_PROPERTIES = Map.of("connectionString", CONNECTION_STRING); + + private static final Network DAPR_NETWORK = Network.newNetwork(); + + public static final String BINDING_NAME = "postgresbinding"; + + @Container + private static final PostgreSQLContainer POSTGRE_SQL_CONTAINER = new PostgreSQLContainer<>("postgres:16-alpine") + .withNetworkAliases("postgres-repository") + .withDatabaseName("dapr_db_repository") + .withUsername("postgres") + .withPassword("password") + .withNetwork(DAPR_NETWORK); + + @Container + @ServiceConnection + private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG) + .withAppName("dapr-feign-test") + .withNetwork(DAPR_NETWORK) + .withComponent(new Component(BINDING_NAME, "bindings.postgresql", "v1", BINDING_PROPERTIES)) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .dependsOn(POSTGRE_SQL_CONTAINER); + + @Autowired + PostgreBindingClient postgreBindingClient; + + @Autowired + TestMethodClient testMethodClient; + + @Test + public void invokeBindingTest() { + + } + + @Test + public void invokeMethodTest() { + assertEquals("hello", testMethodClient.hello()); + assertEquals("hello", testMethodClient.echo("hello")); + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignTestApplication.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignTestApplication.java new file mode 100644 index 0000000000..81d250bae9 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignTestApplication.java @@ -0,0 +1,13 @@ +package io.dapr.it.spring.feign; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableFeignClients +public class DaprFeignTestApplication { + public static void main(String[] args) { + SpringApplication.run(DaprFeignTestApplication.class, args); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java new file mode 100644 index 0000000000..39eef20753 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java @@ -0,0 +1,12 @@ +package io.dapr.it.spring.feign; + +import io.dapr.spring.openfeign.annotation.UseDaprClient; +import org.springframework.cloud.openfeign.FeignClient; + +import static io.dapr.it.spring.feign.DaprFeignIT.BINDING_NAME; + +@FeignClient(value = "postgres-binding", url = "http://binding." + BINDING_NAME) +@UseDaprClient +public interface PostgreBindingClient { + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java new file mode 100644 index 0000000000..88d803c4f4 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java @@ -0,0 +1,13 @@ +package io.dapr.it.spring.feign; + +public class Result { + private final String message; + + public Result(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java new file mode 100644 index 0000000000..2c9ec79a9d --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java @@ -0,0 +1,22 @@ +package io.dapr.it.spring.feign; + +import io.dapr.spring.openfeign.annotation.UseDaprClient; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(value = "test-method", url = "http://method.dapr-feign-test/") +@UseDaprClient +public interface TestMethodClient { + + @GetMapping("/hello") + String hello(); + + @PostMapping("/echo") + String echo(@RequestBody String input); + + @PostMapping(value = "/echoj", consumes = "text/plain", produces = "application/json") + Result echoJson(@RequestBody String input); + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java new file mode 100644 index 0000000000..bfec6730c7 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java @@ -0,0 +1,25 @@ +package io.dapr.it.spring.feign; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestRestController { + + @GetMapping("/hello") + public String hello() { + return "hello"; + } + + @PostMapping("/echo") + public String echo(@RequestBody String input) { + return input; + } + + @PostMapping("/echoj") + public Result echoJson(@RequestBody String input) { + return new Result(input); + } +} From 5e0e1100dbd84e5523175213a0156a3b428371d8 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Tue, 1 Apr 2025 21:58:23 +0800 Subject: [PATCH 02/16] feat(springboot openfeign): An OpenFeign Client for Dapr Invokes A OpenFeign Client and it's AutoConfiguration using Dapr Invoke APIs. Splited into two librarys, dapr-openfeign-client is the pure client of OpenFeign, and dapr-spring-openfeign is the plugin of Spring Cloud OpenFeign. Currently only HTTP Invoke Method and Invoke Binding supported, and Invoke Binding is not always recommend to use this client, depends on what binding component is. Signed-off-by: lony2003 --- .../io/dapr/feign/DaprInvokeFeignClient.java | 23 +++++---- .../io/dapr/it/spring/feign/DaprFeignIT.java | 49 +++++++++++++++++-- .../java/io/dapr/it/spring/feign/Result.java | 8 ++- .../it/spring/feign/TestMethodClient.java | 5 +- .../it/spring/feign/TestRestController.java | 10 ++++ 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java b/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java index 2582a2f619..cb74529b79 100644 --- a/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java +++ b/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java @@ -332,18 +332,23 @@ private Map toHeader(Map> header) { } private Response.Body toResponseBody(Mono response, Request.Options options) throws IOException { - byte[] result; + byte[] result = new byte[0]; - try { - result = response.retry(retry).block(Duration.of(timeout, - TimeUnit.MILLISECONDS.toChronoUnit())); + for (int count = 0; count < retry; count++) { + try { + result = response.block(Duration.of(timeout, + TimeUnit.MILLISECONDS.toChronoUnit())); - if (result == null) { - result = new byte[0]; - } + if (result == null) { + result = new byte[0]; + } - } catch (RuntimeException e) { - throw new IOException("Can not get Response", e); + break; + } catch (RuntimeException e) { + if (retry == count + 1) { + throw new IOException("Can not get Response", e); + } + } } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java index fa7f7e8497..00419797b8 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -1,8 +1,13 @@ package io.dapr.it.spring.feign; +import io.dapr.client.DaprClient; +import io.dapr.client.domain.HttpExtension; import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; +import io.dapr.utils.TypeRef; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,9 +17,12 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.testcontainers.containers.Network; import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -22,8 +30,16 @@ import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG; import static org.junit.jupiter.api.Assertions.assertEquals; -@ExtendWith(SpringExtension.class) -@SpringBootTest(properties = {"dapr.feign.enable=true"}) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, + classes = { + DaprFeignTestApplication.class + }, + properties = { + "dapr.feign.enabled=true", + "dapr.feign.retries=1" + } +) @Testcontainers @Tag("testcontainers") public class DaprFeignIT { @@ -36,6 +52,9 @@ public class DaprFeignIT { public static final String BINDING_NAME = "postgresbinding"; + private static final int APP_PORT = 8080; + private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*"; + @Container private static final PostgreSQLContainer POSTGRE_SQL_CONTAINER = new PostgreSQLContainer<>("postgres:16-alpine") .withNetworkAliases("postgres-repository") @@ -49,11 +68,26 @@ public class DaprFeignIT { private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG) .withAppName("dapr-feign-test") .withNetwork(DAPR_NETWORK) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withComponent(new Component(BINDING_NAME, "bindings.postgresql", "v1", BINDING_PROPERTIES)) .withDaprLogLevel(DaprLogLevel.DEBUG) + .withAppPort(APP_PORT) + .withAppHealthCheckPath("/ready") + .withAppChannelAddress("host.testcontainers.internal") .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .dependsOn(POSTGRE_SQL_CONTAINER); + @BeforeAll + public static void beforeAll(){ + org.testcontainers.Testcontainers.exposeHostPorts(APP_PORT); + } + + @BeforeEach + public void beforeEach() { + // Ensure the subscriptions are registered + Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(DAPR_CONTAINER); + } + @Autowired PostgreBindingClient postgreBindingClient; @@ -66,9 +100,18 @@ public void invokeBindingTest() { } @Test - public void invokeMethodTest() { + public void invokeSimpleGetMethodTest() { assertEquals("hello", testMethodClient.hello()); + } + + @Test + public void invokeSimplePostMethodTest() { assertEquals("hello", testMethodClient.echo("hello")); } + @Test + public void invokeJsonMethodTest() { + assertEquals("hello", testMethodClient.echoJson("hello").getMessage()); + } + } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java index 88d803c4f4..c5d8b655c4 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java @@ -1,12 +1,18 @@ package io.dapr.it.spring.feign; public class Result { - private final String message; + private String message; + + public Result() {} public Result(String message) { this.message = message; } + public void setMessage(String message) { + this.message = message; + } + public String getMessage() { return message; } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java index 2c9ec79a9d..1ea2e8042f 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java @@ -1,5 +1,6 @@ package io.dapr.it.spring.feign; +import io.dapr.Topic; import io.dapr.spring.openfeign.annotation.UseDaprClient; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @@ -10,13 +11,13 @@ @UseDaprClient public interface TestMethodClient { - @GetMapping("/hello") + @GetMapping(value = "/hello") String hello(); @PostMapping("/echo") String echo(@RequestBody String input); - @PostMapping(value = "/echoj", consumes = "text/plain", produces = "application/json") + @PostMapping(value = "/echoj", produces = "application/json;charset=utf-8") Result echoJson(@RequestBody String input); } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java index bfec6730c7..29c1bb578e 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java @@ -1,5 +1,6 @@ package io.dapr.it.spring.feign; +import io.dapr.Topic; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -8,11 +9,20 @@ @RestController public class TestRestController { + public static final String pubSubName = "pubsub"; + public static final String topicName = "mockTopic"; + + @GetMapping("/ready") + public String ok() { + return "OK"; + } + @GetMapping("/hello") public String hello() { return "hello"; } + @Topic(name = topicName, pubsubName = pubSubName) @PostMapping("/echo") public String echo(@RequestBody String input) { return input; From 2e7d1d0ba0b46d899361c7b32f666c5057d45530 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Tue, 1 Apr 2025 21:58:23 +0800 Subject: [PATCH 03/16] feat(springboot openfeign): An OpenFeign Client for Dapr Invokes A OpenFeign Client and it's AutoConfiguration using Dapr Invoke APIs. Splited into two librarys, dapr-openfeign-client is the pure client of OpenFeign, and dapr-spring-openfeign is the plugin of Spring Cloud OpenFeign. Currently only HTTP Invoke Method and Invoke Binding supported, and Invoke Binding is not always recommend to use this client, depends on what binding component is. Signed-off-by: lony2003 --- .../io/dapr/feign/DaprInvokeFeignClient.java | 55 +++++++++++-------- .../feign/DaprFeignClientBindingTest.java | 8 ++- .../dapr/feign/DaprFeignClientMethodTest.java | 7 ++- .../DaprFeignClientAutoConfiguration.java | 10 +++- .../DaprFeignClientProperties.java | 6 +- 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java b/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java index cb74529b79..5e9000f575 100644 --- a/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java +++ b/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java @@ -80,6 +80,8 @@ */ public class DaprInvokeFeignClient implements Client { + private static final Map httpExtensionMap = generateHttpExtensionMap(); + private static final String DOT = "\\."; private final int retry; @@ -87,6 +89,31 @@ public class DaprInvokeFeignClient implements Client { private final DaprClient daprClient; + private static Map generateHttpExtensionMap() { + Map tempHttpExtensionMap = new HashMap<>(); + + tempHttpExtensionMap.put("none", + HttpExtension.NONE); + tempHttpExtensionMap.put("put", + HttpExtension.PUT); + tempHttpExtensionMap.put("post", + HttpExtension.POST); + tempHttpExtensionMap.put("delete", + HttpExtension.DELETE); + tempHttpExtensionMap.put("head", + HttpExtension.HEAD); + tempHttpExtensionMap.put("connect", + HttpExtension.CONNECT); + tempHttpExtensionMap.put("options", + HttpExtension.OPTIONS); + tempHttpExtensionMap.put("trace", + HttpExtension.TRACE); + tempHttpExtensionMap.put("get", + HttpExtension.GET); + + return tempHttpExtensionMap; + } + /** * Default Client creation with no arguments. * @@ -119,8 +146,8 @@ public DaprInvokeFeignClient(DaprClient daprClient) { * Client creation with DaprClient, wait time, retry time Specified. * * @param daprClient client sepcified - * @param timeout wait time (ms) - * @param retry retry times + * @param timeout wait time (ms) + * @param retry retry times */ public DaprInvokeFeignClient(DaprClient daprClient, int timeout, int retry) { this.daprClient = daprClient; @@ -250,28 +277,10 @@ private InvokeMethodRequest toInvokeMethodHTTPRequest(URI uri, Request request) } private HttpExtension toHttpExtension(String method) throws IOException { - switch (method) { - case "none": - return HttpExtension.NONE; - case "put": - return HttpExtension.PUT; - case "post": - return HttpExtension.POST; - case "delete": - return HttpExtension.DELETE; - case "head": - return HttpExtension.HEAD; - case "connect": - return HttpExtension.CONNECT; - case "options": - return HttpExtension.OPTIONS; - case "trace": - return HttpExtension.TRACE; - case "get": - return HttpExtension.GET; - default: - throw new IOException("Method '" + method + "' is not supported"); + if (!httpExtensionMap.containsKey(method)) { + throw new IOException("Method '" + method + "' is not supported"); } + return httpExtensionMap.get(method); } private String getContentType(Request request) { diff --git a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java index 6e10b7a672..d8d8f3082a 100644 --- a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java +++ b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import feign.Body; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -43,7 +44,7 @@ void DaprFeignClient_testMockBindingInvoke() { newBuilder().target(DaprFeignClientTestInterface.class, "http://binding.myBinding"); assertEquals(200, repository.getWithContentType().status()); - assertEquals(0, repository.get().body().length()); + assertEquals(0, repository.post().body().length()); } public Feign.Builder newBuilder() { @@ -59,7 +60,8 @@ public interface DaprFeignClientTestInterface { @Headers({"Accept: text/plain", "Content-Type: text/plain"}) Response getWithContentType(); - @RequestLine("GET /get") - Response get(); + @RequestLine("POST /get") + @Body("test") + Response post(); } } diff --git a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java index 249bda4119..be4164a49b 100644 --- a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java +++ b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java @@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets; +import feign.Body; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -45,6 +46,7 @@ void DaprFeignClient_testMockMethodInvoke() { newBuilder().target(DaprFeignClientTestInterface.class, "http://method.myApp/"); assertEquals(12, repository.getWithContentType().body().length()); + assertEquals(200, repository.post().status()); } public Feign.Builder newBuilder() { @@ -60,7 +62,8 @@ public interface DaprFeignClientTestInterface { @Headers({"Accept: text/plain", "Content-Type: text/plain"}) Response getWithContentType(); - @RequestLine("GET /abc") - Response get(); + @RequestLine("POST /abc/") + @Body("test") + Response post(); } } diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java index 558b05f381..b4843e28bf 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java @@ -36,9 +36,15 @@ public Targeter targeter(DaprInvokeFeignClient daprInvokeFeignClient) { return new DaprClientTargeter(daprInvokeFeignClient); } + @SuppressWarnings("checkstyle:MissingJavadocMethod") @Bean @ConditionalOnMissingBean - public DaprInvokeFeignClient daprInvokeFeignClient(DaprClient daprClient) { - return new DaprInvokeFeignClient(daprClient); + public DaprInvokeFeignClient daprInvokeFeignClient(DaprClient daprClient, + DaprFeignClientProperties daprFeignClientProperties) { + return new DaprInvokeFeignClient( + daprClient, + daprFeignClientProperties.getTimeout(), + daprFeignClientProperties.getRetries() + ); } } diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java index fbf50568c4..2926a9d74d 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java @@ -7,9 +7,9 @@ public class DaprFeignClientProperties { public static final String PROPERTY_PREFIX = "dapr.feign"; - private boolean enabled; - private Integer timeout; - private Integer retries; + private boolean enabled = true; + private Integer timeout = 2000; + private Integer retries = 3; public boolean isEnabled() { return enabled; From 665c29937cc029fd60f4106a1c02aa4427101fa2 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Wed, 2 Apr 2025 20:46:23 +0800 Subject: [PATCH 04/16] docs(springboot openfeign): add an example to springboot-examples this example can redirect order request to producer-app. It needs producer-app to run properly. Signed-off-by: lony2003 --- .../it/spring/feign/PostgreBindingClient.java | 12 +++ spring-boot-examples/README.md | 47 +++++++++- spring-boot-examples/kubernetes/README.md | 27 +++++- .../kubernetes/openfeign-app.yaml | 45 ++++++++++ spring-boot-examples/openfgien-app/pom.xml | 61 +++++++++++++ .../openfeign/OpenFeignApplication.java | 28 ++++++ .../springboot/examples/openfeign/Order.java | 52 +++++++++++ .../ProducerClientRestController.java | 34 ++++++++ .../openfeign/client/ProducerClient.java | 23 +++++ .../src/main/resources/application.properties | 2 + .../openfeign/DaprTestContainersConfig.java | 86 ++++++++++++++++++ .../examples/openfeign/OpenFeignAppTests.java | 87 +++++++++++++++++++ .../openfeign/TestConsumerApplication.java | 32 +++++++ spring-boot-examples/pom.xml | 9 ++ 14 files changed, 541 insertions(+), 4 deletions(-) create mode 100644 spring-boot-examples/kubernetes/openfeign-app.yaml create mode 100644 spring-boot-examples/openfgien-app/pom.xml create mode 100644 spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java create mode 100644 spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/Order.java create mode 100644 spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java create mode 100644 spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java create mode 100644 spring-boot-examples/openfgien-app/src/main/resources/application.properties create mode 100644 spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java create mode 100644 spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java create mode 100644 spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java index 39eef20753..65e1253cfc 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java @@ -2,6 +2,10 @@ import io.dapr.spring.openfeign.annotation.UseDaprClient; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.List; import static io.dapr.it.spring.feign.DaprFeignIT.BINDING_NAME; @@ -9,4 +13,12 @@ @UseDaprClient public interface PostgreBindingClient { + @PostMapping("/exec") + void exec(@RequestHeader("sql") String sql, @RequestHeader("params") List params); + + @PostMapping("/exec") + void exec(@RequestHeader("sql") String sql, @RequestHeader("params") String params); + + @PostMapping("/query") + String query(@RequestHeader("sql") String sql, @RequestHeader("params") List params); } diff --git a/spring-boot-examples/README.md b/spring-boot-examples/README.md index 3cc88610de..cda11afcf0 100644 --- a/spring-boot-examples/README.md +++ b/spring-boot-examples/README.md @@ -1,12 +1,14 @@ # Dapr Spring Boot and Testcontainers integration Example -This example consists of two applications: +This example consists of three applications: - Producer App: - Publish messages using a Spring Messaging approach - Store and retrieve information using Spring Data CrudRepository - Implements a Workflow with Dapr Workflows - Consumer App: - Subscribe to messages +- OpenFeign App: + - A proxy to Producer App Order API ## Running these examples from source code @@ -68,6 +70,29 @@ cd consumer-app/ The `consumer-app` starts in port `8081` by default. +And then run in a different terminal: + + + + +```sh +cd openfeign-app/ +../../mvnw -Dspring-boot.run.arguments="--reuse=true" spring-boot:test-run +``` + + +The `openfeign-app` starts in port `8083` by default. + ## Interacting with the applications Now that both applications are up you can place an order by sending a POST request to `:8080/orders/` @@ -92,6 +117,26 @@ curl -X POST localhost:8080/orders -H 'Content-Type: application/json' -d '{ "it +You can also send POST request to the `openfeign-app`, which redirect the request to the `producer-app`: + + + + +```sh +curl -X POST localhost:8083/rpc/producer/orders -H 'Content-Type: application/json' -d '{ "item": "the mars volta EP", "amount": 1 }' +``` + + + If you check the `producer-app` logs you should see the following lines: diff --git a/spring-boot-examples/kubernetes/README.md b/spring-boot-examples/kubernetes/README.md index 3bb5da421e..e2a0e13bcb 100644 --- a/spring-boot-examples/kubernetes/README.md +++ b/spring-boot-examples/kubernetes/README.md @@ -1,7 +1,7 @@ # Running this example on Kubernetes To run this example on Kubernetes, you can use any Kubernetes distribution. -We install Dapr on a Kubernetes cluster and then we will deploy both the `producer-app` and `consumer-app`. +We install Dapr on a Kubernetes cluster and then we will deploy the `producer-app`, `consumer-app` and `openfeign-app`. ## Creating a cluster and installing Dapr @@ -77,6 +77,24 @@ docker push localhost:5001/sb-consumer-app podman push localhost:5001/sb-consumer-app --tls-verify=false ``` +From inside the `spring-boot-examples/openfeign-app` directory you can run the following command to create a container: +```bash +mvn spring-boot:build-image +``` + +Once we have the container image created, we need to tag and push to the local registry, so the image can be used from our local cluster. +Alternatively, you can push the images to a public registry and update the Kubernetes manifests accordingly. + +```bash +docker tag openfeign-app:0.15.0-SNAPSHOT localhost:5001/sb-openfeign-app +docker push localhost:5001/sb-openfeign-app +``` + +**Note**: for Podman you need to run: +``` +podman push localhost:5001/sb-openfeign-app --tls-verify=false +``` + Now we are ready to install our application into the cluster. ## Installing and interacting with the application @@ -107,7 +125,7 @@ Next you need to use `kubectl port-forward` to be able to send requests to the a kubectl port-forward svc/producer-app 8080:8080 ``` -In a different terminals you can check the logs of the `producer-app` and `consumer-app`: +In a different terminals you can check the logs of the `producer-app`, `consumer-app` and `openfeign-app`: ```bash kubectl logs -f producer-app- @@ -117,5 +135,8 @@ and ```bash kubectl logs -f consumer-app- ``` +and - +```bash +kubectl logs -f openfeign-app- +``` diff --git a/spring-boot-examples/kubernetes/openfeign-app.yaml b/spring-boot-examples/kubernetes/openfeign-app.yaml new file mode 100644 index 0000000000..40dff61ff2 --- /dev/null +++ b/spring-boot-examples/kubernetes/openfeign-app.yaml @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: openfeign-app + name: openfeign-app +spec: + type: NodePort + ports: + - name: "openfeign-app" + port: 8083 + targetPort: 8083 + nodePort: 31003 + selector: + app: openfeign-app + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: openfeign-app + name: openfeign-app +spec: + replicas: 1 + selector: + matchLabels: + app: openfeign-app + template: + metadata: + annotations: + dapr.io/app-id: openfeign-app + dapr.io/app-port: "8083" + dapr.io/enabled: "true" + labels: + app: openfeign-app + spec: + containers: + - image: localhost:5001/sb-openfeign-app + name: openfeign-app + imagePullPolicy: Always + ports: + - containerPort: 8083 + name: openfeign-app diff --git a/spring-boot-examples/openfgien-app/pom.xml b/spring-boot-examples/openfgien-app/pom.xml new file mode 100644 index 0000000000..a0dd753e12 --- /dev/null +++ b/spring-boot-examples/openfgien-app/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + io.dapr + spring-boot-examples + 0.15.0-SNAPSHOT + + + openfgien-app + openfgien-app + Spring Boot, Testcontainers and Dapr Integration Examples :: OpenFeign App + + + 17 + 17 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + io.dapr.spring + dapr-spring-boot-starter + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + io.dapr.spring + dapr-spring-openfeign + ${dapr.sdk.alpha.version} + + + io.dapr.spring + dapr-spring-boot-starter-test + test + + + org.testcontainers + junit-jupiter + test + + + io.rest-assured + rest-assured + test + + + + \ No newline at end of file diff --git a/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java b/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java new file mode 100644 index 0000000000..0dafc1af14 --- /dev/null +++ b/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.springboot.examples.openfeign; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; + +@SpringBootApplication +@EnableFeignClients +public class OpenFeignApplication { + + public static void main(String[] args) { + SpringApplication.run(OpenFeignApplication.class, args); + } + +} diff --git a/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/Order.java b/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/Order.java new file mode 100644 index 0000000000..7917bbb605 --- /dev/null +++ b/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/Order.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.springboot.examples.openfeign; + +public class Order { + private String id; + private String item; + private Integer amount; + + public Order() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getItem() { + return item; + } + + public void setItem(String item) { + this.item = item; + } + + public Integer getAmount() { + return amount; + } + + public void setAmount(Integer amount) { + this.amount = amount; + } + + @Override + public String toString() { + return "Order{" + "id='" + id + '\'' + ", item='" + item + '\'' + ", amount=" + amount + '}'; + } +} diff --git a/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java b/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java new file mode 100644 index 0000000000..321d598d1a --- /dev/null +++ b/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java @@ -0,0 +1,34 @@ +package io.dapr.springboot.examples.openfeign; + +import io.dapr.springboot.examples.openfeign.client.ProducerClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/rpc/producer") +public class ProducerClientRestController { + private final ProducerClient producerClient; + + public ProducerClientRestController(ProducerClient producerClient) { + this.producerClient = producerClient; + } + + @PostMapping("/orders") + public String storeOrder(@RequestBody Order order) { + return producerClient.storeOrder(order); + } + + @GetMapping("/orders") + public Iterable getAll() { + return producerClient.getAll(); + } + + @GetMapping("/orders/byItem/") + public Iterable getAllByItem(@RequestParam("item") String item) { + return producerClient.getAllByItem(item); + } +} diff --git a/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java b/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java new file mode 100644 index 0000000000..7e371780d8 --- /dev/null +++ b/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java @@ -0,0 +1,23 @@ +package io.dapr.springboot.examples.openfeign.client; + +import io.dapr.spring.openfeign.annotation.UseDaprClient; +import io.dapr.springboot.examples.openfeign.Order; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(value = "producer-client", url = "http://method.producer-app/") +@UseDaprClient +public interface ProducerClient { + + @PostMapping("/orders") + String storeOrder(@RequestBody Order order); + + @GetMapping(value = "/orders", produces = "application/json") + Iterable getAll(); + + @GetMapping(value = "/orders/byItem/", produces = "application/json") + Iterable getAllByItem(@RequestParam("item") String item); +} diff --git a/spring-boot-examples/openfgien-app/src/main/resources/application.properties b/spring-boot-examples/openfgien-app/src/main/resources/application.properties new file mode 100644 index 0000000000..0ff707082e --- /dev/null +++ b/spring-boot-examples/openfgien-app/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.application.name=openfeign-app +server.port=8083 diff --git a/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java b/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java new file mode 100644 index 0000000000..f0e3529868 --- /dev/null +++ b/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java @@ -0,0 +1,86 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.springboot.examples.openfeign; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.Network; +import org.testcontainers.utility.DockerImageName; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@TestConfiguration(proxyBeanMethods = false) +public class DaprTestContainersConfig { + + @Bean + public Network getDaprNetwork(Environment env) { + boolean reuse = env.getProperty("reuse", Boolean.class, false); + if (reuse) { + Network defaultDaprNetwork = new Network() { + @Override + public String getId() { + return "dapr-network"; + } + + @Override + public void close() { + + } + + @Override + public Statement apply(Statement base, Description description) { + return null; + } + }; + + List networks = DockerClientFactory.instance().client().listNetworksCmd() + .withNameFilter("dapr-network").exec(); + if (networks.isEmpty()) { + Network.builder().createNetworkCmdModifier(cmd -> cmd.withName("dapr-network")).build().getId(); + return defaultDaprNetwork; + } else { + return defaultDaprNetwork; + } + } else { + return Network.newNetwork(); + } + } + + + @Bean + @ServiceConnection + public DaprContainer daprContainer(Network daprNetwork, Environment env) { + boolean reuse = env.getProperty("reuse", Boolean.class, false); + + return new DaprContainer("daprio/daprd:1.14.4") + .withAppName("openfeign-app") + .withNetwork(daprNetwork) + .withDaprLogLevel(DaprLogLevel.INFO) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withAppPort(8083).withAppChannelAddress("host.testcontainers.internal") + .withReusablePlacement(reuse) + .withAppHealthCheckPath("/actuator/health"); + } + +} diff --git a/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java b/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java new file mode 100644 index 0000000000..3db232d691 --- /dev/null +++ b/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.springboot.examples.openfeign; + +import io.dapr.client.DaprClient; +import io.dapr.client.domain.InvokeMethodRequest; +import io.dapr.springboot.DaprAutoConfiguration; +import io.dapr.springboot.examples.openfeign.client.ProducerClient; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.utils.TypeRef; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.testcontainers.containers.wait.strategy.Wait; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +@SpringBootTest(classes = {TestConsumerApplication.class, DaprTestContainersConfig.class, + DaprAutoConfiguration.class}, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class OpenFeignAppTests { + + @MockitoBean + private DaprClient daprClient; + + + @BeforeAll + public static void setup() { + org.testcontainers.Testcontainers.exposeHostPorts(8083); + } + + @BeforeEach + void setUp() { + RestAssured.baseURI = "http://localhost:" + 8083; + Mockito.when(daprClient.invokeMethod(Mockito.any(InvokeMethodRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) + .thenReturn(Mono.just("[]".getBytes(StandardCharsets.UTF_8))); + } + + @Test + public void demoClientTest() { + given().contentType(ContentType.JSON) + .body("{ \"id\": \"abc-123\",\"item\": \"the mars volta LP\",\"amount\": 1}") + .when() + .post("/rpc/producer/orders") + .then() + .statusCode(200); + + given().contentType(ContentType.JSON) + .when() + .get("/rpc/producer/orders") + .then() + .statusCode(200).body("size()", is(0)); + + given().contentType(ContentType.JSON) + .when() + .queryParam("item", "the mars volta LP") + .get("/rpc/producer/orders/byItem/") + .then() + .statusCode(200).body("size()", is(0)); + } + + +} diff --git a/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java b/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java new file mode 100644 index 0000000000..b4caed399e --- /dev/null +++ b/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.springboot.examples.openfeign; + +import io.dapr.springboot.examples.openfeign.DaprTestContainersConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class TestConsumerApplication { + + public static void main(String[] args) { + SpringApplication.from(OpenFeignApplication::main) + .with(DaprTestContainersConfig.class) + .run(args); + org.testcontainers.Testcontainers.exposeHostPorts(8083); + } + + +} diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index 75a32364f7..4fa1969066 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -16,11 +16,13 @@ true 3.4.3 + 2024.0.0 producer-app consumer-app + openfgien-app @@ -32,6 +34,13 @@ pom import + + org.springframework.cloud + spring-cloud-dependencies + ${springcloud.version} + pom + import + From d7831b0df09dd581e4fd2c52af9bb4efbeaecfa1 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Wed, 2 Apr 2025 20:47:27 +0800 Subject: [PATCH 05/16] test(springboot openfeign): A new test that tests PostgreSQL bindings. this test tests the PostgreSQL binding of dapr. Signed-off-by: lony2003 --- .../java/io/dapr/it/spring/feign/DaprFeignIT.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java index 00419797b8..9e467045af 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -2,6 +2,7 @@ import io.dapr.client.DaprClient; import io.dapr.client.domain.HttpExtension; +import io.dapr.it.spring.data.TestDaprSpringDataConfiguration; import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; @@ -14,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.testcontainers.containers.Network; import org.testcontainers.containers.PostgreSQLContainer; @@ -24,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME; @@ -96,7 +99,15 @@ public void beforeEach() { @Test public void invokeBindingTest() { + postgreBindingClient.exec("CREATE TABLE \"demodata\" (\n" + + "\t\"id\" serial NOT NULL UNIQUE,\n" + + "\t\"name\" varchar(255) NOT NULL,\n" + + "\tPRIMARY KEY(\"id\")\n" + + ");", List.of()); + postgreBindingClient.exec("INSERT INTO demodata (id, name) VALUES ($1, $2)", "[1, \"hello\"]"); + + assertEquals("[[1,\"hello\"]]", postgreBindingClient.query("SELECT * FROM demodata", List.of())); } @Test From 6498a09a9c5e4ea6a6aa5ea2e63868e2bcdb4dba Mon Sep 17 00:00:00 2001 From: lony2003 Date: Wed, 2 Apr 2025 20:48:03 +0800 Subject: [PATCH 06/16] docs(springboot openfeign): Update doc for Dapr OpenFeign Signed-off-by: lony2003 --- .../en/java-sdk-docs/spring-boot/_index.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/daprdocs/content/en/java-sdk-docs/spring-boot/_index.md b/daprdocs/content/en/java-sdk-docs/spring-boot/_index.md index 9819f8ef81..10a93cb05a 100644 --- a/daprdocs/content/en/java-sdk-docs/spring-boot/_index.md +++ b/daprdocs/content/en/java-sdk-docs/spring-boot/_index.md @@ -40,6 +40,47 @@ By adding these dependencies you can: - Use the Spring Data and Messaging abstractions and programming model that uses the Dapr APIs under the hood - Improve your inner-development loop by relying on [Testcontainers](https://testcontainers.com/) to bootstrap Dapr Control plane services and default components +___(Optional)___ And if you want to enable openfeign support, you will also need to add the dependencies to your project: + +``` + + org.springframework.cloud + spring-cloud-starter-openfeign + + + io.dapr.spring + dapr-spring-openfeign + 0.15.0-SNAPSHOT + +``` + +By adding these dependencies you can: +- Invoke Method and Bindings with OpenFeign, just like other HTTP endpoints. + +___Note that Spring Cloud dependencies will require a different dependencyManagement setup from normal SpringBoot Application, +please check the [Official Documentation](https://spring.io/projects/spring-cloud) for more information.___ + +___(Optional)___ If you want to use OpenFeign with Dapr from a non-SpringBoot project, you can add this dependency to your project: + +``` + + io.dapr.spring + dapr-openfeign-client + 0.15.0-SNAPSHOT + +``` + +It mainly provides a Client for OpenFeign to receive OpenFeign requests and send them using Dapr. + +You can use the client like this: + +```java +MyAppData response = Feign.builder().client(new DaprFeignClient()).target(MyAppData.class, + "http://binding.myBinding/create"); +``` + +___Note that you don't have to add this dependency to your SpringBoot project directly, `dapr-spring-openfeign` has already included it.___ + Once these dependencies are in your application, you can rely on Spring Boot autoconfiguration to autowire a `DaprClient` instance: ```java @@ -323,6 +364,55 @@ daprWorkflowClient.raiseEvent(instanceId, "MyEvenet", event); Check the [Dapr Workflow documentation](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/) for more information about how to work with Dapr Workflows. +## Invoke Methods and Bindings registered in Dapr with Spring Cloud OpenFeign + +First you should follow the official Spring Cloud OpenFeign steps to enable FeignClient features, +mainly by adding a `@UseFeignClient` annotation in your SpringBoot Application of Configurations. + +Define a FeignClient using DaprClient is very easy, you can just define a regular FeignClient, and add a `@UseFeignClient` to the interface, just like that: + +```java +@FeignClient(value = "producer-client", url = "http://method.producer-app/") +@UseDaprClient +public interface ProducerClient { + + @PostMapping("/orders") + String storeOrder(@RequestBody Order order); + + @GetMapping(value = "/orders", produces = "application/json") + Iterable getAll(); + + @GetMapping(value = "/orders/byItem/", produces = "application/json") + Iterable getAllByItem(@RequestParam("item") String item); +} +``` + +There you go! now when you call the ProducerClient methods, it will call the DaprClient to handle that. + +>___Note: because of the design of DaprClient, you won't get any headers from Dapr.___ +> +>___So you need to add `produces = "application/json"` +to your RequestMapping in order to parse the response body which return type is other than `String`.___ +> +> ___The `produces` field will generate an `Accept` header to the request, +the client will read it and create a fake `Content-Type` header to the response, +and Spring Cloud Openfeign will read the `Content-Type` header of the response to parse values.___ + +You may have noticed that the `url` field of `@FeignClient` is strange, here is the schema of it: + +The following content is from the Java Doc of DaprInvokeFeignClient. + +> Dapr currently supports two methods of invocation: invokeBinding (output binding) and invokeMethod. This client supports two modes: http://binding.xxx or http://method.xxx. The http scheme at the beginning is just to make Spring Boot Openfeign work properly. +> +> For invokeMethod, the URL contains two types of information, similar to the format of an HTTP URL. The difference lies in the conversion of the host in the HTTP URL to appId, and the path (excluding “/”) to methodName. For example, if you have a method with the appId “myApp” and the methodName “getAll/demo”, then the URL for this request would be http://method.myApp/getAll/demo. You can also set HTTP headers if you wish, and the client will handle them. Currently, only HTTP calls are supported, but grpc calls may be supported in the future, with possible URLs like http://method_grpc.myApp/getAll or similar. +> +> For invokeBinding, the URL also contains two types of information: the host is the bindingName, and the path is the operation. Note that different bindings support different operations, so you must consult the Dapr documentation. For example, if you have a binding with the bindingName “myBinding” and the supported operation is “create”, then the URL for this request would be http://binding.myBinding/create. You can put some metadata in the headers of the Feign request, and the client will handle them. +> +> As for the response, the result code is always 200 OK. If the client encounters any errors, it will throw an IOException. +> +> Currently, we have no method to gain metadata from server as Dapr Client doesn’t have methods to do that, so headers will be blank. If Accept header has set in request, a fake Content-Type header will be created in response, and it will be the first value of Accept header. + +___Note that not all bindings are recommended to use FeignClient to query directly, you can try `dapr-spring-data` for databases, or `dapr-spring-messaging` for pubsubs___ ## Next steps From fd89e6fc53e80e9cded8615eb253a6a11abe93a3 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Thu, 3 Apr 2025 14:10:10 +0800 Subject: [PATCH 07/16] docs(springboot openfeign): A fix of example docs. Fix the example doc to make openfeign app works in mm.py Signed-off-by: lony2003 --- spring-boot-examples/README.md | 2 +- spring-boot-examples/{openfgien-app => openfeign-app}/pom.xml | 2 +- .../springboot/examples/openfeign/OpenFeignApplication.java | 0 .../main/java/io/dapr/springboot/examples/openfeign/Order.java | 0 .../examples/openfeign/ProducerClientRestController.java | 0 .../springboot/examples/openfeign/client/ProducerClient.java | 0 .../src/main/resources/application.properties | 0 .../springboot/examples/openfeign/DaprTestContainersConfig.java | 0 .../dapr/springboot/examples/openfeign/OpenFeignAppTests.java | 0 .../springboot/examples/openfeign/TestConsumerApplication.java | 0 spring-boot-examples/pom.xml | 2 +- 11 files changed, 3 insertions(+), 3 deletions(-) rename spring-boot-examples/{openfgien-app => openfeign-app}/pom.xml (98%) rename spring-boot-examples/{openfgien-app => openfeign-app}/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java (100%) rename spring-boot-examples/{openfgien-app => openfeign-app}/src/main/java/io/dapr/springboot/examples/openfeign/Order.java (100%) rename spring-boot-examples/{openfgien-app => openfeign-app}/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java (100%) rename spring-boot-examples/{openfgien-app => openfeign-app}/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java (100%) rename spring-boot-examples/{openfgien-app => openfeign-app}/src/main/resources/application.properties (100%) rename spring-boot-examples/{openfgien-app => openfeign-app}/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java (100%) rename spring-boot-examples/{openfgien-app => openfeign-app}/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java (100%) rename spring-boot-examples/{openfgien-app => openfeign-app}/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java (100%) diff --git a/spring-boot-examples/README.md b/spring-boot-examples/README.md index cda11afcf0..a6ba771502 100644 --- a/spring-boot-examples/README.md +++ b/spring-boot-examples/README.md @@ -29,7 +29,7 @@ expected_stdout_lines: background: true expected_return_code: 143 sleep: 30 -timeout_seconds: 45 +timeout_seconds: 75 --> diff --git a/spring-boot-examples/openfgien-app/pom.xml b/spring-boot-examples/openfeign-app/pom.xml similarity index 98% rename from spring-boot-examples/openfgien-app/pom.xml rename to spring-boot-examples/openfeign-app/pom.xml index a0dd753e12..2fdec559dc 100644 --- a/spring-boot-examples/openfgien-app/pom.xml +++ b/spring-boot-examples/openfeign-app/pom.xml @@ -9,7 +9,7 @@ 0.15.0-SNAPSHOT - openfgien-app + openfeign-app openfgien-app Spring Boot, Testcontainers and Dapr Integration Examples :: OpenFeign App diff --git a/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java b/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java similarity index 100% rename from spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java rename to spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/OpenFeignApplication.java diff --git a/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/Order.java b/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/Order.java similarity index 100% rename from spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/Order.java rename to spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/Order.java diff --git a/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java b/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java similarity index 100% rename from spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java rename to spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java diff --git a/spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java b/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java similarity index 100% rename from spring-boot-examples/openfgien-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java rename to spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java diff --git a/spring-boot-examples/openfgien-app/src/main/resources/application.properties b/spring-boot-examples/openfeign-app/src/main/resources/application.properties similarity index 100% rename from spring-boot-examples/openfgien-app/src/main/resources/application.properties rename to spring-boot-examples/openfeign-app/src/main/resources/application.properties diff --git a/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java similarity index 100% rename from spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java rename to spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java diff --git a/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java similarity index 100% rename from spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java rename to spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java diff --git a/spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java similarity index 100% rename from spring-boot-examples/openfgien-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java rename to spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index 4fa1969066..7ad23c618b 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -22,7 +22,7 @@ producer-app consumer-app - openfgien-app + openfeign-app From 46a9f65d9360c6507ebd4384a607bc918496848a Mon Sep 17 00:00:00 2001 From: lony2003 Date: Mon, 7 Apr 2025 18:26:59 +0800 Subject: [PATCH 08/16] refactor(springboot openfeign): Use BeanPostProcessor to modify the original Targeter beans instead of override them. Signed-off-by: lony2003 --- .../DaprFeignClientAutoConfiguration.java | 10 +++---- .../targeter/DaprClientTargeter.java | 23 +++++++++++----- .../DaprClientTargeterBeanPostProcessor.java | 27 +++++++++++++++++++ 3 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeterBeanPostProcessor.java diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java index b4843e28bf..8c89ba39ae 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java @@ -15,27 +15,23 @@ import io.dapr.client.DaprClient; import io.dapr.feign.DaprInvokeFeignClient; -import io.dapr.spring.openfeign.targeter.DaprClientTargeter; +import io.dapr.spring.openfeign.targeter.DaprClientTargeterBeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.openfeign.FeignAutoConfiguration; -import org.springframework.cloud.openfeign.Targeter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(DaprFeignClientProperties.class) @ConditionalOnProperty(name = "dapr.feign.enabled", matchIfMissing = true) @ConditionalOnClass(FeignAutoConfiguration.class) +@Import(DaprClientTargeterBeanPostProcessor.class) public class DaprFeignClientAutoConfiguration { - @Bean - public Targeter targeter(DaprInvokeFeignClient daprInvokeFeignClient) { - return new DaprClientTargeter(daprInvokeFeignClient); - } - @SuppressWarnings("checkstyle:MissingJavadocMethod") @Bean @ConditionalOnMissingBean diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeter.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeter.java index bad5edb1ce..520c5ece61 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeter.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeter.java @@ -27,14 +27,28 @@ public class DaprClientTargeter implements Targeter { private final DaprInvokeFeignClient daprInvokeFeignClient; + private final Targeter targeter; - public DaprClientTargeter(DaprInvokeFeignClient daprInvokeFeignClient) { + public DaprClientTargeter(DaprInvokeFeignClient daprInvokeFeignClient, Targeter targeter) { this.daprInvokeFeignClient = daprInvokeFeignClient; + this.targeter = targeter; } @Override public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context, Target.HardCodedTarget target) { + Class classOfT = target.type(); + UseDaprClient useDaprClient = classOfT.getAnnotation(UseDaprClient.class); + + if (useDaprClient == null) { + return targeter.target( + factory, + feign, + context, + target + ); + } + Class builderClass = Feign.Builder.class; Client defaultClient = null; @@ -49,12 +63,7 @@ public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignCl throw new RuntimeException(e); } - Class classOfT = target.type(); - UseDaprClient useDaprClient = classOfT.getAnnotation(UseDaprClient.class); - - if (useDaprClient != null) { - feign.client(daprInvokeFeignClient); - } + feign.client(daprInvokeFeignClient); T targetInstance = feign.target(target); diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeterBeanPostProcessor.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeterBeanPostProcessor.java new file mode 100644 index 0000000000..0e2986a709 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeterBeanPostProcessor.java @@ -0,0 +1,27 @@ +package io.dapr.spring.openfeign.targeter; + +import io.dapr.feign.DaprInvokeFeignClient; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.cloud.openfeign.Targeter; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnBean(Targeter.class) +public class DaprClientTargeterBeanPostProcessor implements BeanPostProcessor { + + private final DaprInvokeFeignClient daprInvokeFeignClient; + + public DaprClientTargeterBeanPostProcessor(DaprInvokeFeignClient daprInvokeFeignClient) { + this.daprInvokeFeignClient = daprInvokeFeignClient; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof Targeter) { + return new DaprClientTargeter(daprInvokeFeignClient, (Targeter) bean); + } + return bean; + } +} From 2877e86edd7bb18c0cb6647a36d45df1cca4e623 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Tue, 8 Apr 2025 11:16:36 +0800 Subject: [PATCH 09/16] style(springboot openfeign): add headers, clean import, refresh style Signed-off-by: lony2003 --- dapr-spring/dapr-openfeign-client/pom.xml | 4 +- .../io/dapr/feign/DaprInvokeFeignClient.java | 50 +++++++-------- .../feign/DaprFeignClientBindingTest.java | 59 +++++++++-------- .../dapr/feign/DaprFeignClientMethodTest.java | 63 +++++++++---------- dapr-spring/dapr-spring-openfeign/pom.xml | 4 +- .../DaprFeignClientProperties.java | 14 +++++ .../DaprClientTargeterBeanPostProcessor.java | 14 +++++ .../DaprOpenFeignClientTestApplication.java | 14 +++++ .../openfeign/DaprOpenFeignClientTests.java | 14 +++++ .../client/DaprInvokeBindingClient.java | 14 +++++ .../client/DaprInvokeMethodClient.java | 14 +++++ .../openfeign/client/DemoFeignClient.java | 14 +++++ .../io/dapr/it/spring/feign/DaprFeignIT.java | 28 +++++---- .../feign/DaprFeignTestApplication.java | 13 ++++ .../it/spring/feign/PostgreBindingClient.java | 13 ++++ .../java/io/dapr/it/spring/feign/Result.java | 24 +++++-- .../it/spring/feign/TestMethodClient.java | 14 ++++- .../it/spring/feign/TestRestController.java | 14 ++++- spring-boot-examples/openfeign-app/pom.xml | 4 +- .../ProducerClientRestController.java | 13 ++++ .../openfeign/client/ProducerClient.java | 13 ++++ .../openfeign/DaprTestContainersConfig.java | 4 -- .../examples/openfeign/OpenFeignAppTests.java | 10 +-- .../openfeign/TestConsumerApplication.java | 5 +- 24 files changed, 305 insertions(+), 128 deletions(-) diff --git a/dapr-spring/dapr-openfeign-client/pom.xml b/dapr-spring/dapr-openfeign-client/pom.xml index 5bc42bd6ed..0bfad1bca1 100644 --- a/dapr-spring/dapr-openfeign-client/pom.xml +++ b/dapr-spring/dapr-openfeign-client/pom.xml @@ -1,6 +1,6 @@ - 4.0.0 diff --git a/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java b/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java index 5e9000f575..b7c8e9e909 100644 --- a/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java +++ b/dapr-spring/dapr-openfeign-client/src/main/java/io/dapr/feign/DaprInvokeFeignClient.java @@ -89,31 +89,6 @@ public class DaprInvokeFeignClient implements Client { private final DaprClient daprClient; - private static Map generateHttpExtensionMap() { - Map tempHttpExtensionMap = new HashMap<>(); - - tempHttpExtensionMap.put("none", - HttpExtension.NONE); - tempHttpExtensionMap.put("put", - HttpExtension.PUT); - tempHttpExtensionMap.put("post", - HttpExtension.POST); - tempHttpExtensionMap.put("delete", - HttpExtension.DELETE); - tempHttpExtensionMap.put("head", - HttpExtension.HEAD); - tempHttpExtensionMap.put("connect", - HttpExtension.CONNECT); - tempHttpExtensionMap.put("options", - HttpExtension.OPTIONS); - tempHttpExtensionMap.put("trace", - HttpExtension.TRACE); - tempHttpExtensionMap.put("get", - HttpExtension.GET); - - return tempHttpExtensionMap; - } - /** * Default Client creation with no arguments. * @@ -155,6 +130,31 @@ public DaprInvokeFeignClient(DaprClient daprClient, int timeout, int retry) { this.retry = retry; } + private static Map generateHttpExtensionMap() { + Map tempHttpExtensionMap = new HashMap<>(); + + tempHttpExtensionMap.put("none", + HttpExtension.NONE); + tempHttpExtensionMap.put("put", + HttpExtension.PUT); + tempHttpExtensionMap.put("post", + HttpExtension.POST); + tempHttpExtensionMap.put("delete", + HttpExtension.DELETE); + tempHttpExtensionMap.put("head", + HttpExtension.HEAD); + tempHttpExtensionMap.put("connect", + HttpExtension.CONNECT); + tempHttpExtensionMap.put("options", + HttpExtension.OPTIONS); + tempHttpExtensionMap.put("trace", + HttpExtension.TRACE); + tempHttpExtensionMap.put("get", + HttpExtension.GET); + + return tempHttpExtensionMap; + } + @Override public Response execute(Request request, Request.Options options) throws IOException { URI uri; diff --git a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java index d8d8f3082a..5348ff12ed 100644 --- a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java +++ b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientBindingTest.java @@ -14,15 +14,7 @@ package io.dapr.feign; -import static org.junit.jupiter.api.Assertions.assertEquals; - import feign.Body; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - import feign.Feign; import feign.Headers; import feign.RequestLine; @@ -30,38 +22,45 @@ import io.dapr.client.DaprClient; import io.dapr.client.domain.InvokeBindingRequest; import io.dapr.utils.TypeRef; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; +import static org.junit.jupiter.api.Assertions.assertEquals; + @ExtendWith(MockitoExtension.class) public class DaprFeignClientBindingTest { - @Mock - DaprClient daprClient; + @Mock + DaprClient daprClient; - @Test - void DaprFeignClient_testMockBindingInvoke() { - DaprFeignClientTestInterface repository = - newBuilder().target(DaprFeignClientTestInterface.class, "http://binding.myBinding"); + @Test + void DaprFeignClient_testMockBindingInvoke() { + DaprFeignClientTestInterface repository = + newBuilder().target(DaprFeignClientTestInterface.class, "http://binding.myBinding"); - assertEquals(200, repository.getWithContentType().status()); - assertEquals(0, repository.post().body().length()); - } + assertEquals(200, repository.getWithContentType().status()); + assertEquals(0, repository.post().body().length()); + } - public Feign.Builder newBuilder() { - Mockito.when(daprClient.invokeBinding(Mockito.any(InvokeBindingRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) - .thenReturn(Mono.just(new byte[0])); + public Feign.Builder newBuilder() { + Mockito.when(daprClient.invokeBinding(Mockito.any(InvokeBindingRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) + .thenReturn(Mono.just(new byte[0])); - return Feign.builder().client(new DaprInvokeFeignClient(daprClient)); - } + return Feign.builder().client(new DaprInvokeFeignClient(daprClient)); + } - public interface DaprFeignClientTestInterface { + public interface DaprFeignClientTestInterface { - @RequestLine("GET /create") - @Headers({"Accept: text/plain", "Content-Type: text/plain"}) - Response getWithContentType(); + @RequestLine("GET /create") + @Headers({"Accept: text/plain", "Content-Type: text/plain"}) + Response getWithContentType(); - @RequestLine("POST /get") - @Body("test") - Response post(); - } + @RequestLine("POST /get") + @Body("test") + Response post(); + } } diff --git a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java index be4164a49b..ac7d4e38ed 100644 --- a/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java +++ b/dapr-spring/dapr-openfeign-client/src/test/java/io/dapr/feign/DaprFeignClientMethodTest.java @@ -14,17 +14,7 @@ package io.dapr.feign; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.charset.StandardCharsets; - import feign.Body; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - import feign.Feign; import feign.Headers; import feign.RequestLine; @@ -32,38 +22,47 @@ import io.dapr.client.DaprClient; import io.dapr.client.domain.InvokeMethodRequest; import io.dapr.utils.TypeRef; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + @ExtendWith(MockitoExtension.class) public class DaprFeignClientMethodTest { - @Mock - DaprClient daprClient; + @Mock + DaprClient daprClient; - @Test - void DaprFeignClient_testMockMethodInvoke() { - DaprFeignClientTestInterface repository = - newBuilder().target(DaprFeignClientTestInterface.class, "http://method.myApp/"); + @Test + void DaprFeignClient_testMockMethodInvoke() { + DaprFeignClientTestInterface repository = + newBuilder().target(DaprFeignClientTestInterface.class, "http://method.myApp/"); - assertEquals(12, repository.getWithContentType().body().length()); - assertEquals(200, repository.post().status()); - } + assertEquals(12, repository.getWithContentType().body().length()); + assertEquals(200, repository.post().status()); + } - public Feign.Builder newBuilder() { - Mockito.when(daprClient.invokeMethod(Mockito.any(InvokeMethodRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) - .thenReturn(Mono.just("hello world!".getBytes(StandardCharsets.UTF_8))); + public Feign.Builder newBuilder() { + Mockito.when(daprClient.invokeMethod(Mockito.any(InvokeMethodRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) + .thenReturn(Mono.just("hello world!".getBytes(StandardCharsets.UTF_8))); - return Feign.builder().client(new DaprInvokeFeignClient(daprClient)); - } + return Feign.builder().client(new DaprInvokeFeignClient(daprClient)); + } - public interface DaprFeignClientTestInterface { + public interface DaprFeignClientTestInterface { - @RequestLine("GET /getAll") - @Headers({"Accept: text/plain", "Content-Type: text/plain"}) - Response getWithContentType(); + @RequestLine("GET /getAll") + @Headers({"Accept: text/plain", "Content-Type: text/plain"}) + Response getWithContentType(); - @RequestLine("POST /abc/") - @Body("test") - Response post(); - } + @RequestLine("POST /abc/") + @Body("test") + Response post(); + } } diff --git a/dapr-spring/dapr-spring-openfeign/pom.xml b/dapr-spring/dapr-spring-openfeign/pom.xml index 4922c11ac9..64e85cfa1f 100644 --- a/dapr-spring/dapr-spring-openfeign/pom.xml +++ b/dapr-spring/dapr-spring-openfeign/pom.xml @@ -1,6 +1,6 @@ - 4.0.0 diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java index 2926a9d74d..373a120891 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java @@ -1,3 +1,17 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign.autoconfigure; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeterBeanPostProcessor.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeterBeanPostProcessor.java index 0e2986a709..c2d334f853 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeterBeanPostProcessor.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/targeter/DaprClientTargeterBeanPostProcessor.java @@ -1,3 +1,17 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign.targeter; import io.dapr.feign.DaprInvokeFeignClient; diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTestApplication.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTestApplication.java index cac447fd27..a5e5aa9e6c 100644 --- a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTestApplication.java +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTestApplication.java @@ -1,3 +1,17 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign; import org.springframework.boot.SpringApplication; diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTests.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTests.java index 8650b84f6d..4e9edc2226 100644 --- a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTests.java +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/DaprOpenFeignClientTests.java @@ -1,3 +1,17 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign; import io.dapr.client.DaprClient; diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeBindingClient.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeBindingClient.java index 8255436e77..94665e3a15 100644 --- a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeBindingClient.java +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeBindingClient.java @@ -1,3 +1,17 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign.client; import io.dapr.spring.openfeign.annotation.UseDaprClient; diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeMethodClient.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeMethodClient.java index 0087325d21..b12f7fa7e9 100644 --- a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeMethodClient.java +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DaprInvokeMethodClient.java @@ -1,3 +1,17 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign.client; import io.dapr.spring.openfeign.annotation.UseDaprClient; diff --git a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DemoFeignClient.java b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DemoFeignClient.java index a0ae9faf5b..90b4f56471 100644 --- a/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DemoFeignClient.java +++ b/dapr-spring/dapr-spring-openfeign/src/test/java/io/dapr/spring/openfeign/client/DemoFeignClient.java @@ -1,3 +1,17 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.spring.openfeign.client; import org.springframework.cloud.openfeign.FeignClient; diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java index 9e467045af..4812345aad 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -1,42 +1,44 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.it.spring.feign; -import io.dapr.client.DaprClient; -import io.dapr.client.domain.HttpExtension; -import io.dapr.it.spring.data.TestDaprSpringDataConfiguration; import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; -import io.dapr.utils.TypeRef; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; import org.testcontainers.containers.Network; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; -import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME; import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { - DaprFeignTestApplication.class + DaprFeignTestApplication.class }, properties = { "dapr.feign.enabled=true", @@ -81,7 +83,7 @@ public class DaprFeignIT { .dependsOn(POSTGRE_SQL_CONTAINER); @BeforeAll - public static void beforeAll(){ + public static void beforeAll() { org.testcontainers.Testcontainers.exposeHostPorts(APP_PORT); } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignTestApplication.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignTestApplication.java index 81d250bae9..75ced791d9 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignTestApplication.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignTestApplication.java @@ -1,3 +1,16 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.it.spring.feign; import org.springframework.boot.SpringApplication; diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java index 65e1253cfc..3ca6ba38ed 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/PostgreBindingClient.java @@ -1,3 +1,16 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.it.spring.feign; import io.dapr.spring.openfeign.annotation.UseDaprClient; diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java index c5d8b655c4..4150e616b8 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/Result.java @@ -1,19 +1,33 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.it.spring.feign; public class Result { private String message; - public Result() {} - - public Result(String message) { - this.message = message; + public Result() { } - public void setMessage(String message) { + public Result(String message) { this.message = message; } public String getMessage() { return message; } + + public void setMessage(String message) { + this.message = message; + } } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java index 1ea2e8042f..25656a47b6 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestMethodClient.java @@ -1,6 +1,18 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.it.spring.feign; -import io.dapr.Topic; import io.dapr.spring.openfeign.annotation.UseDaprClient; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java index 29c1bb578e..cf23406889 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java @@ -1,6 +1,18 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.it.spring.feign; -import io.dapr.Topic; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; diff --git a/spring-boot-examples/openfeign-app/pom.xml b/spring-boot-examples/openfeign-app/pom.xml index 2fdec559dc..a5d6070e4a 100644 --- a/spring-boot-examples/openfeign-app/pom.xml +++ b/spring-boot-examples/openfeign-app/pom.xml @@ -1,6 +1,6 @@ - 4.0.0 diff --git a/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java b/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java index 321d598d1a..2eafdb9fa2 100644 --- a/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java +++ b/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/ProducerClientRestController.java @@ -1,3 +1,16 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.springboot.examples.openfeign; import io.dapr.springboot.examples.openfeign.client.ProducerClient; diff --git a/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java b/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java index 7e371780d8..1c97e8d123 100644 --- a/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java +++ b/spring-boot-examples/openfeign-app/src/main/java/io/dapr/springboot/examples/openfeign/client/ProducerClient.java @@ -1,3 +1,16 @@ +/* + * Copyright 2025 The Dapr 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 + * http://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 io.dapr.springboot.examples.openfeign.client; import io.dapr.spring.openfeign.annotation.UseDaprClient; diff --git a/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java index f0e3529868..7e448cbd8e 100644 --- a/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java +++ b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java @@ -13,7 +13,6 @@ package io.dapr.springboot.examples.openfeign; -import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; import org.junit.runner.Description; @@ -24,11 +23,8 @@ import org.springframework.core.env.Environment; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.Network; -import org.testcontainers.utility.DockerImageName; -import java.util.HashMap; import java.util.List; -import java.util.Map; @TestConfiguration(proxyBeanMethods = false) public class DaprTestContainersConfig { diff --git a/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java index 3db232d691..d9533b9626 100644 --- a/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java +++ b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/OpenFeignAppTests.java @@ -16,8 +16,6 @@ import io.dapr.client.DaprClient; import io.dapr.client.domain.InvokeMethodRequest; import io.dapr.springboot.DaprAutoConfiguration; -import io.dapr.springboot.examples.openfeign.client.ProducerClient; -import io.dapr.testcontainers.DaprContainer; import io.dapr.utils.TypeRef; import io.restassured.RestAssured; import io.restassured.http.ContentType; @@ -25,23 +23,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.testcontainers.containers.wait.strategy.Wait; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import static io.restassured.RestAssured.given; -import static org.awaitility.Awaitility.await; import static org.hamcrest.CoreMatchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest(classes = {TestConsumerApplication.class, DaprTestContainersConfig.class, DaprAutoConfiguration.class}, - webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class OpenFeignAppTests { @MockitoBean @@ -57,7 +51,7 @@ public static void setup() { void setUp() { RestAssured.baseURI = "http://localhost:" + 8083; Mockito.when(daprClient.invokeMethod(Mockito.any(InvokeMethodRequest.class), Mockito.eq(TypeRef.BYTE_ARRAY))) - .thenReturn(Mono.just("[]".getBytes(StandardCharsets.UTF_8))); + .thenReturn(Mono.just("[]".getBytes(StandardCharsets.UTF_8))); } @Test diff --git a/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java index b4caed399e..41f0bb9dcb 100644 --- a/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java +++ b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/TestConsumerApplication.java @@ -13,7 +13,6 @@ package io.dapr.springboot.examples.openfeign; -import io.dapr.springboot.examples.openfeign.DaprTestContainersConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -23,8 +22,8 @@ public class TestConsumerApplication { public static void main(String[] args) { SpringApplication.from(OpenFeignApplication::main) - .with(DaprTestContainersConfig.class) - .run(args); + .with(DaprTestContainersConfig.class) + .run(args); org.testcontainers.Testcontainers.exposeHostPorts(8083); } From ee6f761c81f92d3b4b67c15a93e2f96e0c3d2c85 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Tue, 8 Apr 2025 11:17:46 +0800 Subject: [PATCH 10/16] fix(springboot openfeign): delete pubsub subscription to make DaprSpringMessagingIT works Signed-off-by: lony2003 --- .../io/dapr/it/spring/feign/DaprFeignIT.java | 19 ++++++------------- .../it/spring/feign/TestRestController.java | 1 - 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java index 4812345aad..50ac2aec05 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -48,17 +48,13 @@ @Testcontainers @Tag("testcontainers") public class DaprFeignIT { + public static final String BINDING_NAME = "postgresbinding"; private static final String CONNECTION_STRING = "host=postgres-repository user=postgres password=password port=5432 connect_timeout=10 database=dapr_db_repository"; - private static final Map BINDING_PROPERTIES = Map.of("connectionString", CONNECTION_STRING); - private static final Network DAPR_NETWORK = Network.newNetwork(); - - public static final String BINDING_NAME = "postgresbinding"; - private static final int APP_PORT = 8080; - private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*"; + private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*App entered healthy status.*"; @Container private static final PostgreSQLContainer POSTGRE_SQL_CONTAINER = new PostgreSQLContainer<>("postgres:16-alpine") @@ -73,7 +69,6 @@ public class DaprFeignIT { private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG) .withAppName("dapr-feign-test") .withNetwork(DAPR_NETWORK) - .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withComponent(new Component(BINDING_NAME, "bindings.postgresql", "v1", BINDING_PROPERTIES)) .withDaprLogLevel(DaprLogLevel.DEBUG) .withAppPort(APP_PORT) @@ -81,6 +76,10 @@ public class DaprFeignIT { .withAppChannelAddress("host.testcontainers.internal") .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .dependsOn(POSTGRE_SQL_CONTAINER); + @Autowired + PostgreBindingClient postgreBindingClient; + @Autowired + TestMethodClient testMethodClient; @BeforeAll public static void beforeAll() { @@ -93,12 +92,6 @@ public void beforeEach() { Wait.forLogMessage(SUBSCRIPTION_MESSAGE_PATTERN, 1).waitUntilReady(DAPR_CONTAINER); } - @Autowired - PostgreBindingClient postgreBindingClient; - - @Autowired - TestMethodClient testMethodClient; - @Test public void invokeBindingTest() { postgreBindingClient.exec("CREATE TABLE \"demodata\" (\n" + diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java index cf23406889..a3a6ba007c 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/TestRestController.java @@ -34,7 +34,6 @@ public String hello() { return "hello"; } - @Topic(name = topicName, pubsubName = pubSubName) @PostMapping("/echo") public String echo(@RequestBody String input) { return input; From bb9e2febe8190617cd26dd12f4899196e6a59108 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Tue, 8 Apr 2025 11:26:02 +0800 Subject: [PATCH 11/16] fix(springboot openfeign): fix spring cloud version error for springboot 3.3.x Signed-off-by: lony2003 --- .github/workflows/build.yml | 4 +++- sdk-tests/pom.xml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 021e6f5f19..f8efa20f3d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,11 +25,13 @@ jobs: matrix: java: [ 17 ] spring-boot-version: [ 3.4.3 ] + spring-cloud-version: [ 2024.0.1 ] spring-boot-display-version: [ 3.4.x ] experimental: [ false ] include: - java: 17 spring-boot-version: 3.3.9 + spring-cloud-version: 2023.0.5 spring-boot-display-version: 3.3.x experimental: false env: @@ -126,7 +128,7 @@ jobs: run: ./mvnw install -q -B -DskipTests - name: Integration tests using spring boot version ${{ matrix.spring-boot-version }} id: integration_tests - run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -Pintegration-tests verify + run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} PRODUCT_SPRING_CLOUD_VERSION=${{ matrix.spring-cloud-version }} ./mvnw -B -Pintegration-tests verify env: DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} - name: Upload test report for sdk diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index ba43ba6425..b50c022318 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -400,6 +400,7 @@ ${env.PRODUCT_SPRING_BOOT_VERSION} + ${env.PRODUCT_SPRING_CLOUD_VERSION} From 475835cbe1ea55ab7fb3e9da132333050c6bca98 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Tue, 8 Apr 2025 12:01:16 +0800 Subject: [PATCH 12/16] fix(springboot openfeign): change IT ports to make DaprSpringMassagingIT works Signed-off-by: lony2003 --- .../src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java index 50ac2aec05..d0613106ba 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -42,7 +42,8 @@ }, properties = { "dapr.feign.enabled=true", - "dapr.feign.retries=1" + "dapr.feign.retries=1", + "server.port=" + DaprFeignIT.APP_PORT } ) @Testcontainers @@ -53,7 +54,7 @@ public class DaprFeignIT { "host=postgres-repository user=postgres password=password port=5432 connect_timeout=10 database=dapr_db_repository"; private static final Map BINDING_PROPERTIES = Map.of("connectionString", CONNECTION_STRING); private static final Network DAPR_NETWORK = Network.newNetwork(); - private static final int APP_PORT = 8080; + protected static final int APP_PORT = 8081; private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*App entered healthy status.*"; @Container From a330ef2d22e5a137ed5344f0ed609fea32f6c9ad Mon Sep 17 00:00:00 2001 From: lony2003 Date: Wed, 23 Apr 2025 09:21:21 +0800 Subject: [PATCH 13/16] fix(springcloud openfeign): fix IT failed because of reflactor of DAPR_RUNTIME_IMAGE_TAG Signed-off-by: lony2003 --- .../src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java | 4 ++-- .../examples/openfeign/DaprTestContainersConfig.java | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java index d0613106ba..29af4ccce0 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -32,7 +32,7 @@ import java.util.List; import java.util.Map; -import static io.dapr.it.testcontainers.DaprContainerConstants.IMAGE_TAG; +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest( @@ -67,7 +67,7 @@ public class DaprFeignIT { @Container @ServiceConnection - private static final DaprContainer DAPR_CONTAINER = new DaprContainer(IMAGE_TAG) + private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) .withAppName("dapr-feign-test") .withNetwork(DAPR_NETWORK) .withComponent(new Component(BINDING_NAME, "bindings.postgresql", "v1", BINDING_PROPERTIES)) diff --git a/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java index 7e448cbd8e..37385db7c6 100644 --- a/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java +++ b/spring-boot-examples/openfeign-app/src/test/java/io/dapr/springboot/examples/openfeign/DaprTestContainersConfig.java @@ -26,6 +26,8 @@ import java.util.List; +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; + @TestConfiguration(proxyBeanMethods = false) public class DaprTestContainersConfig { @@ -69,7 +71,7 @@ public Statement apply(Statement base, Description description) { public DaprContainer daprContainer(Network daprNetwork, Environment env) { boolean reuse = env.getProperty("reuse", Boolean.class, false); - return new DaprContainer("daprio/daprd:1.14.4") + return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) .withAppName("openfeign-app") .withNetwork(daprNetwork) .withDaprLogLevel(DaprLogLevel.INFO) From c381e44da4a2aea7c2d710cd2ccd6ee598a85fb1 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Wed, 23 Apr 2025 12:38:16 +0800 Subject: [PATCH 14/16] fix(springboot openfeign): fix IT failed for tests that have two dapr client bean autowired Job SDK ITs have a daprPreviewClient bean defined, as SpringBoot bean search will first get beans that impl the interface and get all beans that matches the bean's class, when openfeign autowire DaprClient, there is a conflict between daprClient and daprPreviewClient. Change the AutoConfiguration behavior to make a more strict rule for making AutoConfiguration run Signed-off-by: lony2003 --- dapr-spring/dapr-spring-openfeign/pom.xml | 1 + .../spotbugs-exclude.xml | 16 +++++++++++++++ .../DaprFeignClientAutoConfiguration.java | 3 ++- .../DaprFeignClientProperties.java | 9 --------- .../FeignClientAnnoationEnabledCondition.java | 20 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 7 ------- .../io/dapr/it/spring/feign/DaprFeignIT.java | 1 - 7 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 dapr-spring/dapr-spring-openfeign/spotbugs-exclude.xml create mode 100644 dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/FeignClientAnnoationEnabledCondition.java diff --git a/dapr-spring/dapr-spring-openfeign/pom.xml b/dapr-spring/dapr-spring-openfeign/pom.xml index 64e85cfa1f..15682a2f3d 100644 --- a/dapr-spring/dapr-spring-openfeign/pom.xml +++ b/dapr-spring/dapr-spring-openfeign/pom.xml @@ -18,6 +18,7 @@ 17 17 UTF-8 + ./spotbugs-exclude.xml diff --git a/dapr-spring/dapr-spring-openfeign/spotbugs-exclude.xml b/dapr-spring/dapr-spring-openfeign/spotbugs-exclude.xml new file mode 100644 index 0000000000..74ebf5708a --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/spotbugs-exclude.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java index 8c89ba39ae..f6d9be8161 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientAutoConfiguration.java @@ -22,12 +22,13 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.openfeign.FeignAutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(DaprFeignClientProperties.class) -@ConditionalOnProperty(name = "dapr.feign.enabled", matchIfMissing = true) +@Conditional(FeignClientAnnoationEnabledCondition.class) @ConditionalOnClass(FeignAutoConfiguration.class) @Import(DaprClientTargeterBeanPostProcessor.class) public class DaprFeignClientAutoConfiguration { diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java index 373a120891..ebad216268 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/DaprFeignClientProperties.java @@ -21,18 +21,9 @@ public class DaprFeignClientProperties { public static final String PROPERTY_PREFIX = "dapr.feign"; - private boolean enabled = true; private Integer timeout = 2000; private Integer retries = 3; - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - public Integer getTimeout() { return timeout; } diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/FeignClientAnnoationEnabledCondition.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/FeignClientAnnoationEnabledCondition.java new file mode 100644 index 0000000000..6d6a30b983 --- /dev/null +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/FeignClientAnnoationEnabledCondition.java @@ -0,0 +1,20 @@ +package io.dapr.spring.openfeign.autoconfigure; + +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class FeignClientAnnoationEnabledCondition implements Condition { + @Override + public boolean matches(@NotNull ConditionContext context, @NotNull AnnotatedTypeMetadata metadata) { + if (context.getBeanFactory() == null) { + return false; // Return false if context or BeanFactory is null + } + + String[] beanNames = context.getBeanFactory().getBeanNamesForAnnotation(EnableFeignClients.class); + return beanNames != null && beanNames.length > 0; // Check for null and non-empty array + } +} diff --git a/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/additional-spring-configuration-metadata.json index a1501c23e8..7a71528312 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/dapr-spring/dapr-spring-openfeign/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,12 +1,5 @@ { "properties": [ - { - "name": "dapr.feign.enabled", - "type": "java.lang.Boolean", - "defaultValue": true, - "sourceType": "io.dapr.spring.openfeign.autoconfigure.DaprFeignClientProperties", - "description": "enable dapr openfeign or not." - }, { "name": "dapr.feign.timeout", "type": "java.lang.Integer", diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java index 29af4ccce0..abd8c7c405 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -41,7 +41,6 @@ DaprFeignTestApplication.class }, properties = { - "dapr.feign.enabled=true", "dapr.feign.retries=1", "server.port=" + DaprFeignIT.APP_PORT } From bc6d98ccea7b77ade0336267ed4993d430961895 Mon Sep 17 00:00:00 2001 From: lony2003 Date: Wed, 23 Apr 2025 13:42:18 +0800 Subject: [PATCH 15/16] fix(springboot openfeign): fix IT failed for tests that have two dapr client bean autowired Job SDK ITs have a daprPreviewClient bean defined, as SpringBoot bean search will first get beans that impl the interface and get all beans that matches the bean's class, when openfeign autowire DaprClient, there is a conflict between daprClient and daprPreviewClient. Change the AutoConfiguration behavior to make a more strict rule for making AutoConfiguration run Signed-off-by: lony2003 --- dapr-spring/dapr-spring-openfeign/pom.xml | 1 - .../dapr-spring-openfeign/spotbugs-exclude.xml | 16 ---------------- .../FeignClientAnnoationEnabledCondition.java | 14 +++++++++----- dapr-spring/spotbugs-exclude.xml | 5 +++++ 4 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 dapr-spring/dapr-spring-openfeign/spotbugs-exclude.xml diff --git a/dapr-spring/dapr-spring-openfeign/pom.xml b/dapr-spring/dapr-spring-openfeign/pom.xml index 15682a2f3d..64e85cfa1f 100644 --- a/dapr-spring/dapr-spring-openfeign/pom.xml +++ b/dapr-spring/dapr-spring-openfeign/pom.xml @@ -18,7 +18,6 @@ 17 17 UTF-8 - ./spotbugs-exclude.xml diff --git a/dapr-spring/dapr-spring-openfeign/spotbugs-exclude.xml b/dapr-spring/dapr-spring-openfeign/spotbugs-exclude.xml deleted file mode 100644 index 74ebf5708a..0000000000 --- a/dapr-spring/dapr-spring-openfeign/spotbugs-exclude.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/FeignClientAnnoationEnabledCondition.java b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/FeignClientAnnoationEnabledCondition.java index 6d6a30b983..8603dbf160 100644 --- a/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/FeignClientAnnoationEnabledCondition.java +++ b/dapr-spring/dapr-spring-openfeign/src/main/java/io/dapr/spring/openfeign/autoconfigure/FeignClientAnnoationEnabledCondition.java @@ -7,14 +7,18 @@ import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; +import java.util.Objects; + public class FeignClientAnnoationEnabledCondition implements Condition { @Override + @SuppressWarnings("null") public boolean matches(@NotNull ConditionContext context, @NotNull AnnotatedTypeMetadata metadata) { - if (context.getBeanFactory() == null) { - return false; // Return false if context or BeanFactory is null + try { + ConfigurableListableBeanFactory factory = Objects.requireNonNull(context.getBeanFactory()); + String[] beanNames = factory.getBeanNamesForAnnotation(EnableFeignClients.class); + return beanNames != null && beanNames.length > 0; + } catch (Exception e) { + return false; } - - String[] beanNames = context.getBeanFactory().getBeanNamesForAnnotation(EnableFeignClients.class); - return beanNames != null && beanNames.length > 0; // Check for null and non-empty array } } diff --git a/dapr-spring/spotbugs-exclude.xml b/dapr-spring/spotbugs-exclude.xml index fc80592a14..74ebf5708a 100644 --- a/dapr-spring/spotbugs-exclude.xml +++ b/dapr-spring/spotbugs-exclude.xml @@ -8,4 +8,9 @@ + + + + + From e8a35ccc0f86ac739fc569c80a240ef9fa4fc4cc Mon Sep 17 00:00:00 2001 From: lony2003 Date: Wed, 23 Apr 2025 14:42:11 +0800 Subject: [PATCH 16/16] fix(springboot openfeign): fix IT failure because of APP_PORT change APP_PORT to 8082 Signed-off-by: lony2003 --- .../src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java index abd8c7c405..3f59b7d28d 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/feign/DaprFeignIT.java @@ -53,7 +53,7 @@ public class DaprFeignIT { "host=postgres-repository user=postgres password=password port=5432 connect_timeout=10 database=dapr_db_repository"; private static final Map BINDING_PROPERTIES = Map.of("connectionString", CONNECTION_STRING); private static final Network DAPR_NETWORK = Network.newNetwork(); - protected static final int APP_PORT = 8081; + protected static final int APP_PORT = 8082; private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*App entered healthy status.*"; @Container