SpringMvcContract support parse params (#1016)

This commit is contained in:
Puppy4C
2024-05-13 20:45:57 +08:00
committed by GitHub
parent 596f9b391d
commit c276f1c6d4
3 changed files with 177 additions and 3 deletions

View File

@@ -45,7 +45,7 @@ public interface StoreClient {
@GetMapping("/stores")
Page<Store> getStores(Pageable pageable);
@PostMapping(value = "/stores/{storeId}", consumes = "application/json")
@PostMapping(value = "/stores/{storeId}", consumes = "application/json", params = "mode=upsert")
Store update(@PathVariable("storeId") Long storeId, Store store);
@DeleteMapping("/stores/{storeId:\\d+}")
@@ -701,6 +701,36 @@ public interface DemoClient {
You can also disable the feature via property `spring.cloud.openfeign.cache.enabled=false`.
[[spring-requestmapping-support]]
=== Spring @RequestMapping Support
Spring Cloud OpenFeign provides support for the Spring `@RequestMapping` annotation and its derived annotations (such as `@GetMapping`, `@PostMapping`, and others) support.
The attributes on the `@RequestMapping` annotation (including `value`, `method`, `params`, `headers`, `consumes`, and `produces`) are parsed by `SpringMvcContract` as the content of the request.
Consider the following example:
Define an interface using the `params` attribute.
[source,java,indent=0]
----
@FeignClient("demo")
public interface DemoTemplate {
@PostMapping(value = "/stores/{storeId}", params = "mode=upsert")
Store update(@PathVariable("storeId") Long storeId, Store store);
}
----
In the above example, the request url is resolved to `/stores/{storeId}?mode=upsert`. +
The params attribute also supports the use of multiple `key=value` or only one `key`: +
- When `params = { "key1=v1", "key2=v2" }`, the request url is parsed as `/stores/{storeId}?key1=v1&key2=v2`.
- When `params = "key"`, the request url is parsed as `/stores/{storeId}?key`.
[[feign-querymap-support]]
=== Feign @QueryMap support

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* Copyright 2013-2024 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.
@@ -87,6 +87,7 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.findMerg
* @author Darren Foong
* @author Ram Anaswara
* @author Sam Kruglov
* @author Tang Xiong
*/
public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
@@ -244,6 +245,9 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
// headers
parseHeaders(data, method, methodMapping);
// params
parseParams(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<>());
}
@@ -354,6 +358,22 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
}
}
private void parseParams(MethodMetadata data, Method method, RequestMapping methodMapping) {
String[] params = methodMapping.params();
if (params == null || params.length == 0) {
return;
}
for (String param : params) {
NameValueResolver nameValueResolver = new NameValueResolver(param);
if (!nameValueResolver.isNegated()) {
data.template().query(resolve(nameValueResolver.getName()), resolve(nameValueResolver.getValue()));
}
else {
throw new IllegalArgumentException("Negated params are not supported: " + param);
}
}
}
private Map<Class<? extends Annotation>, AnnotatedParameterProcessor> toAnnotatedArgumentProcessorMap(
List<AnnotatedParameterProcessor> processors) {
Map<Class<? extends Annotation>, AnnotatedParameterProcessor> result = new HashMap<>();
@@ -465,4 +485,40 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
}
private static class NameValueResolver {
private final String name;
private final String value;
private final boolean isNegated;
NameValueResolver(String expression) {
int separator = expression.indexOf('=');
if (separator == -1) {
isNegated = expression.startsWith("!");
name = (isNegated ? expression.substring(1) : expression);
value = null;
}
else {
isNegated = (separator > 0) && (expression.charAt(separator - 1) == '!');
name = (isNegated ? expression.substring(0, separator - 1) : expression.substring(0, separator));
value = expression.substring(separator + 1);
}
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public boolean isNegated() {
return isNegated;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* Copyright 2013-2024 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.
@@ -82,6 +82,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
* @author Szymon Linowski
* @author Sam Kruglov
* @author Bhavya Agrawal
* @author Tang Xiong
**/
class SpringMvcContractTests {
@@ -462,6 +463,68 @@ class SpringMvcContractTests {
assertThat(data.queryMapIndex().intValue()).isEqualTo(0);
}
@Test
void testProcessAnnotations_ParseParams_SingleParam() throws Exception {
Method method = TestTemplate_ParseParams.class.getDeclaredMethod("singleParam");
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/test?p1=1");
assertThat(data.template().method()).isEqualTo("GET");
}
@Test
void testProcessAnnotations_ParseParams_MultipleParams() throws Exception {
Method method = TestTemplate_ParseParams.class.getDeclaredMethod("multipleParams");
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/test?p1=1&p2=2");
assertThat(data.template().method()).isEqualTo("GET");
}
@Test
void testProcessAnnotations_ParseParams_MixParams() throws Exception {
Method method = TestTemplate_ParseParams.class.getDeclaredMethod("mixParams");
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/test?p1=1&p2");
assertThat(data.template().method()).isEqualTo("GET");
}
@Test
void testProcessAnnotations_ParseParams_SingleParamWithoutValue() throws Exception {
Method method = TestTemplate_ParseParams.class.getDeclaredMethod("singleParamWithoutValue");
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/test?p1");
assertThat(data.template().method()).isEqualTo("GET");
}
@Test
void testProcessAnnotations_ParseParams_MultipleParamsWithoutValue() throws Exception {
Method method = TestTemplate_ParseParams.class.getDeclaredMethod("multipleParamsWithoutValue");
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/test?p1&p2");
assertThat(data.template().method()).isEqualTo("GET");
}
@Test
void testProcessAnnotations_ParseParams_NotEqualParams() throws Exception {
assertThatIllegalArgumentException().isThrownBy(() -> {
Method method = TestTemplate_ParseParams.class.getDeclaredMethod("notEqualParams");
contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
});
}
@Test
void testProcessAnnotations_ParseParams_ParamsAndRequestParam() throws Exception {
Method method = TestTemplate_ParseParams.class.getDeclaredMethod("paramsAndRequestParam", String.class);
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/test?p1=1&p2={p2}");
assertThat(data.template().method()).isEqualTo("GET");
}
@Test
void testProcessHeaders() throws Exception {
Method method = TestTemplate_Headers.class.getDeclaredMethod("getTest", String.class);
@@ -750,6 +813,31 @@ class SpringMvcContractTests {
}
public interface TestTemplate_ParseParams {
@GetMapping(value = "test", params = "p1=1")
ResponseEntity<TestObject> singleParam();
@GetMapping(value = "test", params = { "p1=1", "p2=2" })
ResponseEntity<TestObject> multipleParams();
@GetMapping(value = "test", params = { "p1" })
ResponseEntity<TestObject> singleParamWithoutValue();
@GetMapping(value = "test", params = { "p1", "p2" })
ResponseEntity<TestObject> multipleParamsWithoutValue();
@GetMapping(value = "test", params = { "p1=1", "p2" })
ResponseEntity<TestObject> mixParams();
@GetMapping(value = "test", params = { "p1!=1" })
ResponseEntity<TestObject> notEqualParams();
@GetMapping(value = "test", params = { "p1=1" })
ResponseEntity<TestObject> paramsAndRequestParam(@RequestParam("p2") String p2);
}
public interface TestTemplate_HeaderMap {
@GetMapping("/headerMap")