Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @QueryMap mapEncoder attribute #1013

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion docs/modules/ROOT/pages/spring-cloud-openfeign.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -729,10 +729,32 @@ public interface DemoTemplate {

@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);

@GetMapping(path = "/custom")
String customEndpoint(@SpringQueryMap(mapEncoder = CustomQueryMapEncoder.class) Params params);
}
----

If you need more control over the generated query parameter map, you can implement a custom `QueryMapEncoder` bean.
If you need more control over the generated global query parameter map, you can implement a custom `QueryMapEncoder` bean and mark it with `@BaseQueryMapEncoder`.


[source,java,indent=0]
----
@Configuration
public class CustomConfig {

@Bean
@BaseQueryMapEncoder
public QueryMapEncoder globalQueryMapEncoder() {
return new BeanQueryMapEncoder();
}

@Bean
public QueryMapEncoder customQueryMapEncoder() {
return new CustomQueryMapEncoder();
}
}
----

[[hateoas-support]]
=== HATEOAS support
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 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.
Expand All @@ -16,7 +16,10 @@

package org.springframework.cloud.openfeign;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.BeansException;
Expand All @@ -35,6 +38,7 @@
* @author Matt King
* @author Jasbir Singh
* @author Olga Maciaszek-Sharma
* @author changjin wei(魏昌进)
*/
public class FeignClientFactory extends NamedContextFactory<FeignClientSpecification> {

Expand All @@ -58,6 +62,24 @@ public <T> T getInstanceWithoutAncestors(String name, Class<T> type) {
}
}

@Nullable
public <T> T getInstanceWithoutAncestorsForAnnotation(String name, Class<T> type,
Class<? extends Annotation> annotationType) {
GenericApplicationContext context = getContext(name);
String[] beanNames = context.getBeanNamesForAnnotation(annotationType);
List<T> beans = new ArrayList<>();
for (String beanName : beanNames) {
if (context.isTypeMatch(beanName, type)) {
beans.add((T) context.getBean(beanName));
}
}
if (beans.size() > 1) {
throw new IllegalStateException("Only one annotated bean for type expected.");
}
return beans.isEmpty() ? null : beans.get(0);

}

@Nullable
public <T> Map<String, T> getInstancesWithoutAncestors(String name, Class<T> type) {
return getContext(name).getBeansOfType(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.cloud.openfeign;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
Expand Down Expand Up @@ -52,8 +53,10 @@
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient;
import org.springframework.cloud.openfeign.support.BaseQueryMapEncoder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
Expand All @@ -74,6 +77,7 @@
* @author Felix Dittrich
* @author Dominique Villard
* @author Can Bezmen
* @author changjin wei(魏昌进)
*/
public class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
Expand Down Expand Up @@ -229,7 +233,11 @@ protected void configureUsingConfiguration(FeignClientFactory context, Feign.Bui
if (responseInterceptor != null) {
builder.responseInterceptor(responseInterceptor);
}
QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context, QueryMapEncoder.class);
QueryMapEncoder queryMapEncoder = getInheritedAwareAnnotated(context, QueryMapEncoder.class,
BaseQueryMapEncoder.class);
if (queryMapEncoder == null) {
queryMapEncoder = getInheritedAwareOptional(context, QueryMapEncoder.class);
}
if (queryMapEncoder != null) {
builder.queryMapEncoder(queryMapEncoder);
}
Expand Down Expand Up @@ -420,6 +428,16 @@ protected <T> Map<String, T> getInheritedAwareInstances(FeignClientFactory conte
}
}

protected <T> T getInheritedAwareAnnotated(FeignClientFactory context, Class<T> type,
Class<? extends Annotation> annotationType) {
if (inheritParentContext) {
return context.getAnnotatedInstance(contextId, ResolvableType.forType(type), annotationType);
}
else {
return context.getInstanceWithoutAncestorsForAnnotation(contextId, type, annotationType);
}
}

protected <T> T loadBalance(Feign.Builder builder, FeignClientFactory context, HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
if (client != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 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.
Expand Down Expand Up @@ -74,6 +74,7 @@
* @author Olga Maciaszek-Sharma
* @author Hyeonmin Park
* @author Yanming Zhou
* @author changjin wei(魏昌进)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the year in license comments of all the modified files to -2024.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

*/
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
Expand Down Expand Up @@ -144,9 +145,9 @@ public QueryMapEncoder feignQueryMapEncoderPageable() {

@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
public Contract feignContract(ConversionService feignConversionService, List<QueryMapEncoder> springMapEncoders) {
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash);
return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash, springMapEncoders);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 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.
Expand All @@ -21,15 +21,25 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import feign.QueryMapEncoder;

/**
* Spring MVC equivalent of OpenFeign's {@link feign.QueryMap} parameter annotation.
*
* @author Aram Peres
* @author changjin wei(魏昌进)
* @see feign.QueryMap
* @see org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER })
public @interface SpringQueryMap {

/**
* Specifies the {@link feign.QueryMapEncoder} implementation to use to transform DTO
* into query map. The {@link feign.QueryMapEncoder} must be a valid spring bean.
* @return the {@link feign.QueryMapEncoder} instance.
*/
Class<? extends QueryMapEncoder> mapEncoder() default QueryMapEncoder.class;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2022 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.
Expand All @@ -18,8 +18,10 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;

import feign.MethodMetadata;
import feign.QueryMapEncoder;

import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
import org.springframework.cloud.openfeign.SpringQueryMap;
Expand All @@ -29,12 +31,23 @@
*
* @author Aram Peres
* @author Olga Maciaszek-Sharma
* @author changjin wei(魏昌进)
* @see AnnotatedParameterProcessor
*/
public class QueryMapParameterProcessor implements AnnotatedParameterProcessor {

private static final Class<SpringQueryMap> ANNOTATION = SpringQueryMap.class;

private final Map<Class<? extends QueryMapEncoder>, QueryMapEncoder> encoders;

public QueryMapParameterProcessor() {
this.encoders = Map.of();
}

public QueryMapParameterProcessor(Map<Class<? extends QueryMapEncoder>, QueryMapEncoder> encoders) {
this.encoders = encoders;
}

@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
Expand All @@ -46,8 +59,14 @@ public boolean processArgument(AnnotatedParameterContext context, Annotation ann
MethodMetadata metadata = context.getMethodMetadata();
if (metadata.queryMapIndex() == null) {
metadata.queryMapIndex(paramIndex);
metadata.queryMapEncoder(getQueryMapEncoder(annotation));
}
return true;
}

protected QueryMapEncoder getQueryMapEncoder(Annotation annotation) {
SpringQueryMap springQueryMap = (SpringQueryMap) annotation;
return encoders.get(springQueryMap.mapEncoder());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2016-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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.openfeign.support;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import feign.QueryMapEncoder;

/**
* Globally specifies the {@link QueryMapEncoder} implementation to use to transform DTO
* into query map.
* @author changjin wei(魏昌进)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface BaseQueryMapEncoder {

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -35,6 +35,7 @@
import feign.MethodMetadata;
import feign.Param;
import feign.QueryMap;
import feign.QueryMapEncoder;
import feign.Request;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand Down Expand Up @@ -87,6 +88,7 @@
* @author Darren Foong
* @author Ram Anaswara
* @author Sam Kruglov
* @author changjin wei(魏昌进)
*/
public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {

Expand Down Expand Up @@ -129,10 +131,16 @@ public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterPro

public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
ConversionService conversionService, boolean decodeSlash) {
this(annotatedParameterProcessors, conversionService, decodeSlash, List.of());
}

public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
ConversionService conversionService, boolean decodeSlash, List<QueryMapEncoder> springMapEncoders) {
Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null.");
Assert.notNull(conversionService, "ConversionService can not be null.");

List<AnnotatedParameterProcessor> processors = getDefaultAnnotatedArgumentsProcessors();
Map<Class<? extends QueryMapEncoder>, QueryMapEncoder> encoders = toSpringMapEncodersMap(springMapEncoders);
List<AnnotatedParameterProcessor> processors = getDefaultAnnotatedArgumentsProcessors(encoders);
processors.addAll(annotatedParameterProcessors);

annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
Expand Down Expand Up @@ -363,15 +371,16 @@ private Map<Class<? extends Annotation>, AnnotatedParameterProcessor> toAnnotate
return result;
}

private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {
private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors(
Map<Class<? extends QueryMapEncoder>, QueryMapEncoder> encoders) {

List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();

annotatedArgumentResolvers.add(new MatrixVariableParameterProcessor());
annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
annotatedArgumentResolvers.add(new QueryMapParameterProcessor());
annotatedArgumentResolvers.add(new QueryMapParameterProcessor(encoders));
annotatedArgumentResolvers.add(new RequestPartParameterProcessor());
annotatedArgumentResolvers.add(new CookieValueParameterProcessor());

Expand Down Expand Up @@ -415,6 +424,15 @@ private boolean isMultipartFormData(MethodMetadata data) {
return false;
}

public Map<Class<? extends QueryMapEncoder>, QueryMapEncoder> toSpringMapEncodersMap(
List<QueryMapEncoder> springMapEncoders) {
Map<Class<? extends QueryMapEncoder>, QueryMapEncoder> result = new HashMap<>();
for (QueryMapEncoder encoder : springMapEncoders) {
result.put(encoder.getClass(), encoder);
}
return result;
}

private static class ConvertingExpanderFactory {

private final ConversionService conversionService;
Expand Down
Loading