From d46fb3a92e559d32501ca4c65c7f12cb008841db Mon Sep 17 00:00:00 2001 From: Momo Date: Thu, 6 Dec 2018 15:26:36 -0500 Subject: [PATCH] Add support for feign's QueryMap annotation for Object mapping (#79) * Add QueryMapParameterProcessor * Add license to QueryMapParameterProcessor * Use SpringQueryMap instead of QueryMap. Add test. * Add documentation and license to SpringQueryMap * SpringQueryMap docs * Fix typos --- .../main/asciidoc/spring-cloud-openfeign.adoc | 34 ++++++++++++ .../cloud/openfeign/SpringQueryMap.java | 53 +++++++++++++++++++ .../QueryMapParameterProcessor.java | 51 ++++++++++++++++++ .../openfeign/support/SpringMvcContract.java | 5 +- .../support/SpringMvcContractTests.java | 23 +++++++- 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/annotation/QueryMapParameterProcessor.java diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 28f26e97..62560d9f 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -434,3 +434,37 @@ via discovery, and all requests are executed in a <>, so failures will show up in Hystrix metrics, and once the circuit is open the proxy will not try to contact the service. + +=== Feign `@QueryMap` support + +The OpenFeign `@QueryMap` annotation provides support for POJOs to be used as +GET parameter maps. Unfortunately, the default OpenFeign QueryMap annotation is +incompatible with Spring because it lacks a `value` property. + +Spring Cloud OpenFeign provides an equivalent `@SpringQueryMap` annotation, which +is used to annotate a POJO or Map parameter as a query parameter map. + +For example, the `Params` class defines parameters `param1` and `param2`: + +[source,java,indent=0] +---- +// Params.java +public class Params { + private String param1; + private String param2; + + // [Getters and setters omitted for brevity] +} +---- + +The following feign client uses the `Params` class by using the `@SpringQueryMap` annotation: + +[source,java,indent=0] +---- +@FeignClient("demo") +public class DemoTemplate { + + @GetMapping(path = "/demo") + String demoEndpoint(@SpringQueryMap Params params); +} +---- \ No newline at end of file diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java new file mode 100644 index 00000000..6d919076 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 org.springframework.cloud.openfeign; + +import feign.QueryMap; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Spring MVC equivalent of OpenFeign's {@link feign.QueryMap} parameter annotation. + * + * @author Aram Peres + * @see feign.QueryMap + * @see org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface SpringQueryMap { + + /** + * Alias for {@link #encoded()}. + * + * @see QueryMap#encoded() + */ + @AliasFor("encoded") + boolean value() default false; + + /** + * Specifies whether parameter names and values are already encoded. + * + * @see QueryMap#encoded() + */ + @AliasFor("value") + boolean encoded() default false; +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/annotation/QueryMapParameterProcessor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/annotation/QueryMapParameterProcessor.java new file mode 100644 index 00000000..e06bff80 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/annotation/QueryMapParameterProcessor.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 org.springframework.cloud.openfeign.annotation; + +import feign.MethodMetadata; +import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; +import org.springframework.cloud.openfeign.SpringQueryMap; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +/** + * {@link SpringQueryMap} parameter processor. + * + * @author Aram Peres + * @see AnnotatedParameterProcessor + */ +public class QueryMapParameterProcessor implements AnnotatedParameterProcessor { + + private static final Class ANNOTATION = SpringQueryMap.class; + + @Override + public Class getAnnotationType() { + return ANNOTATION; + } + + @Override + public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { + int paramIndex = context.getParameterIndex(); + MethodMetadata metadata = context.getMethodMetadata(); + if (metadata.queryMapIndex() == null) { + metadata.queryMapIndex(paramIndex); + metadata.queryMapEncoded(SpringQueryMap.class.cast(annotation).encoded()); + } + return true; + } +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java index e7d9c050..67500944 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import java.util.Map; import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.cloud.openfeign.annotation.PathVariableParameterProcessor; +import org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor; import org.springframework.cloud.openfeign.annotation.RequestHeaderParameterProcessor; import org.springframework.cloud.openfeign.annotation.RequestParamParameterProcessor; import org.springframework.context.ConfigurableApplicationContext; @@ -62,6 +63,7 @@ import feign.Param; * @author Spencer Gibb * @author Abhijit Sarkar * @author Halvdan Hoem Grelland + * @author Aram Peres */ public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware { @@ -332,6 +334,7 @@ public class SpringMvcContract extends Contract.BaseContract annotatedArgumentResolvers.add(new PathVariableParameterProcessor()); annotatedArgumentResolvers.add(new RequestParamParameterProcessor()); annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor()); + annotatedArgumentResolvers.add(new QueryMapParameterProcessor()); return annotatedArgumentResolvers; } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java index ac21d65f..57f511f6 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import feign.Param; import org.junit.Before; import org.junit.Test; +import org.springframework.cloud.openfeign.SpringQueryMap; import org.springframework.core.convert.ConversionService; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.NumberFormat; @@ -61,6 +62,7 @@ import feign.MethodMetadata; /** * @author chadjaros * @author Halvdan Hoem Grelland + * @author Aram Peres */ public class SpringMvcContractTests { private static final Class EXECUTABLE_TYPE; @@ -488,6 +490,20 @@ public class SpringMvcContractTests { assertEquals("{aParam}", params.get("aParam").iterator().next()); } + @Test + public void testProcessQueryMapObject() throws Exception { + Method method = TestTemplate_QueryMap.class.getDeclaredMethod("queryMapObject", + TestObject.class, String.class); + MethodMetadata data = this.contract + .parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertEquals("/queryMapObject", data.template().url()); + assertEquals("GET", data.template().method()); + assertEquals(0, data.queryMapIndex().intValue()); + Map> params = data.template().queries(); + assertEquals("{aParam}", params.get("aParam").iterator().next()); + } + @Test(expected = IllegalStateException.class) public void testProcessQueryMapMoreThanOnce() throws Exception { Method method = TestTemplate_QueryMap.class.getDeclaredMethod( @@ -573,6 +589,11 @@ public class SpringMvcContractTests { String queryMapMoreThanOnce( @RequestParam MultiValueMap queryMap1, @RequestParam MultiValueMap queryMap2); + + @RequestMapping(path = "/queryMapObject") + String queryMapObject( + @SpringQueryMap TestObject queryMap, + @RequestParam(name = "aParam") String aParam); } @JsonAutoDetect