Feign annotated parameter processors
This commit is contained in:
committed by
Spencer Gibb
parent
50efa8536d
commit
31d2f65528
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2013-2015 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.netflix.feign;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
|
||||
import feign.MethodMetadata;
|
||||
|
||||
/**
|
||||
* Feign contract method parameter processor.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
*/
|
||||
public interface AnnotatedParameterProcessor {
|
||||
|
||||
/**
|
||||
* Retrieves the processor supported annotation type.
|
||||
*
|
||||
* @return the annotation type
|
||||
*/
|
||||
Class<? extends Annotation> getAnnotationType();
|
||||
|
||||
/**
|
||||
* Process the annotated parameter.
|
||||
*
|
||||
* @param context the parameter context
|
||||
* @param annotation the annotation instance
|
||||
* @return whether the parameter is http
|
||||
*/
|
||||
boolean processArgument(AnnotatedParameterContext context, Annotation annotation);
|
||||
|
||||
/**
|
||||
* Specifies the parameter context.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
*/
|
||||
interface AnnotatedParameterContext {
|
||||
|
||||
/**
|
||||
* Retrieves the method metadata.
|
||||
*
|
||||
* @return the method metadata
|
||||
*/
|
||||
MethodMetadata getMethodMetadata();
|
||||
|
||||
/**
|
||||
* Retrieves the index of the parameter.
|
||||
*
|
||||
* @return the parameter index
|
||||
*/
|
||||
int getParameterIndex();
|
||||
|
||||
/**
|
||||
* Sets the parameter name.
|
||||
*
|
||||
* @param name the name of the parameter
|
||||
*/
|
||||
void setParameterName(String name);
|
||||
|
||||
/**
|
||||
* Sets the template parameter.
|
||||
*
|
||||
* @param name the template parameter
|
||||
* @param rest the existing parameter values
|
||||
* @return parameters
|
||||
*/
|
||||
Collection<String> setTemplateParameter(String name, Collection<String> rest);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.cloud.netflix.feign;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -44,6 +47,9 @@ public class FeignClientsConfiguration {
|
||||
@Autowired
|
||||
private ObjectFactory<HttpMessageConverters> messageConverters;
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Decoder feignDecoder() {
|
||||
@@ -59,7 +65,7 @@ public class FeignClientsConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Contract feignContract() {
|
||||
return new SpringMvcContract();
|
||||
return new SpringMvcContract(parameterProcessors);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2013-2015 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.netflix.feign.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
import feign.MethodMetadata;
|
||||
|
||||
import static feign.Util.checkState;
|
||||
import static feign.Util.emptyToNull;
|
||||
|
||||
/**
|
||||
* {@link PathVariable} parameter processor.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
* @see AnnotatedParameterProcessor
|
||||
*/
|
||||
public class PathVariableParameterProcessor implements AnnotatedParameterProcessor {
|
||||
|
||||
private static final Class<PathVariable> ANNOTATION = PathVariable.class;
|
||||
|
||||
@Override
|
||||
public Class<? extends Annotation> getAnnotationType() {
|
||||
return ANNOTATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
|
||||
String name = ANNOTATION.cast(annotation).value();
|
||||
checkState(emptyToNull(name) != null,
|
||||
"PathVariable annotation was empty on param %s.", context.getParameterIndex());
|
||||
context.setParameterName(name);
|
||||
|
||||
MethodMetadata data = context.getMethodMetadata();
|
||||
String varName = '{' + name + '}';
|
||||
if (!data.template().url().contains(varName)
|
||||
&& !searchMapValues(data.template().queries(), varName)
|
||||
&& !searchMapValues(data.template().headers(), varName)) {
|
||||
data.formParams().add(name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private <K, V> boolean searchMapValues(Map<K, Collection<V>> map, V search) {
|
||||
Collection<Collection<V>> values = map.values();
|
||||
if (values == null) {
|
||||
return false;
|
||||
}
|
||||
for (Collection<V> entry : values) {
|
||||
if (entry.contains(search)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2013-2015 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.netflix.feign.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import feign.MethodMetadata;
|
||||
|
||||
import static feign.Util.checkState;
|
||||
import static feign.Util.emptyToNull;
|
||||
|
||||
/**
|
||||
* {@link RequestHeader} parameter processor.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
* @see AnnotatedParameterProcessor
|
||||
*/
|
||||
public class RequestHeaderParameterProcessor implements AnnotatedParameterProcessor {
|
||||
|
||||
private static final Class<RequestHeader> ANNOTATION = RequestHeader.class;
|
||||
|
||||
@Override
|
||||
public Class<? extends Annotation> getAnnotationType() {
|
||||
return ANNOTATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
|
||||
String name = ANNOTATION.cast(annotation).value();
|
||||
checkState(emptyToNull(name) != null,
|
||||
"RequestHeader.value() was empty on parameter %s", context.getParameterIndex());
|
||||
context.setParameterName(name);
|
||||
|
||||
MethodMetadata data = context.getMethodMetadata();
|
||||
Collection<String> header = context.setTemplateParameter(name, data.template().headers().get(name));
|
||||
data.template().header(name, header);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2013-2015 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.netflix.feign.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import feign.MethodMetadata;
|
||||
|
||||
import static feign.Util.checkState;
|
||||
import static feign.Util.emptyToNull;
|
||||
|
||||
/**
|
||||
* {@link RequestParam} parameter processor.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
* @see AnnotatedParameterProcessor
|
||||
*/
|
||||
public class RequestParamParameterProcessor implements AnnotatedParameterProcessor {
|
||||
|
||||
private static final Class<RequestParam> ANNOTATION = RequestParam.class;
|
||||
|
||||
@Override
|
||||
public Class<? extends Annotation> getAnnotationType() {
|
||||
return ANNOTATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
|
||||
String name = ANNOTATION.cast(annotation).value();
|
||||
checkState(emptyToNull(name) != null,
|
||||
"RequestParam.value() was empty on parameter %s", context.getParameterIndex());
|
||||
context.setParameterName(name);
|
||||
|
||||
MethodMetadata data = context.getMethodMetadata();
|
||||
Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
|
||||
data.template().query(name, query);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,26 @@
|
||||
|
||||
package org.springframework.cloud.netflix.feign.support;
|
||||
|
||||
import feign.Contract;
|
||||
import feign.MethodMetadata;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
|
||||
import org.springframework.cloud.netflix.feign.annotation.PathVariableParameterProcessor;
|
||||
import org.springframework.cloud.netflix.feign.annotation.RequestHeaderParameterProcessor;
|
||||
import org.springframework.cloud.netflix.feign.annotation.RequestParamParameterProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import feign.Contract;
|
||||
import feign.MethodMetadata;
|
||||
|
||||
import static feign.Util.checkState;
|
||||
import static feign.Util.emptyToNull;
|
||||
|
||||
@@ -41,6 +48,24 @@ public class SpringMvcContract extends Contract.BaseContract {
|
||||
|
||||
private static final String CONTENT_TYPE = "Content-Type";
|
||||
|
||||
private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;
|
||||
|
||||
public SpringMvcContract() {
|
||||
this(Collections.<AnnotatedParameterProcessor>emptyList());
|
||||
}
|
||||
|
||||
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors) {
|
||||
Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null.");
|
||||
|
||||
List<AnnotatedParameterProcessor> processors;
|
||||
if(!annotatedParameterProcessors.isEmpty()) {
|
||||
processors = new ArrayList<>(annotatedParameterProcessors);
|
||||
} else {
|
||||
processors = getDefaultAnnotatedArgumentsProcessors();
|
||||
}
|
||||
this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
|
||||
MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
|
||||
@@ -109,6 +134,7 @@ public class SpringMvcContract extends Contract.BaseContract {
|
||||
parseHeaders(data, method, methodMapping);
|
||||
}
|
||||
|
||||
|
||||
private void checkAtMostOne(Method method, Object[] values, String fieldName) {
|
||||
checkState(values != null && (values.length == 0 || values.length == 1),
|
||||
"Method %s can only contain at most 1 %s field. Found: %s",
|
||||
@@ -123,72 +149,19 @@ public class SpringMvcContract extends Contract.BaseContract {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean processAnnotationsOnParameter(MethodMetadata data,
|
||||
Annotation[] annotations, int paramIndex) {
|
||||
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
|
||||
boolean isHttpAnnotation = false;
|
||||
// TODO: support spring parameter annotations?
|
||||
|
||||
AnnotatedParameterProcessor.AnnotatedParameterContext context =
|
||||
new SimpleAnnotatedParameterContext(data, paramIndex);
|
||||
for (Annotation parameterAnnotation : annotations) {
|
||||
if (parameterAnnotation instanceof PathVariable) {
|
||||
String name = PathVariable.class.cast(parameterAnnotation).value();
|
||||
checkState(emptyToNull(name) != null,
|
||||
"PathVariable annotation was empty on param %s.", paramIndex);
|
||||
nameParam(data, name, paramIndex);
|
||||
isHttpAnnotation = true;
|
||||
String varName = '{' + name + '}';
|
||||
if (data.template().url().indexOf(varName) == -1
|
||||
&& !searchMapValues(data.template().queries(), varName)
|
||||
&& !searchMapValues(data.template().headers(), varName)) {
|
||||
data.formParams().add(name);
|
||||
}
|
||||
}
|
||||
else if (parameterAnnotation instanceof RequestParam) {
|
||||
String name = RequestParam.class.cast(parameterAnnotation).value();
|
||||
checkState(emptyToNull(name) != null,
|
||||
"QueryParam.value() was empty on parameter %s", paramIndex);
|
||||
Collection<String> query = addTemplatedParam(data.template().queries()
|
||||
.get(name), name);
|
||||
data.template().query(name, query);
|
||||
nameParam(data, name, paramIndex);
|
||||
isHttpAnnotation = true;
|
||||
}
|
||||
else if (parameterAnnotation instanceof RequestHeader) {
|
||||
String name = RequestHeader.class.cast(parameterAnnotation).value();
|
||||
checkState(emptyToNull(name) != null,
|
||||
"HeaderParam.value() was empty on parameter %s", paramIndex);
|
||||
Collection<String> header = addTemplatedParam(data.template().headers()
|
||||
.get(name), name);
|
||||
data.template().header(name, header);
|
||||
nameParam(data, name, paramIndex);
|
||||
isHttpAnnotation = true;
|
||||
}
|
||||
|
||||
// TODO
|
||||
/*
|
||||
* else if (annotationType == FormParam.class) { String name =
|
||||
* FormParam.class.cast(parameterAnnotation).value();
|
||||
* checkState(emptyToNull(name) != null,
|
||||
* "FormParam.value() was empty on parameter %s", paramIndex);
|
||||
* data.formParams().add(name); nameParam(data, name, paramIndex);
|
||||
* isHttpAnnotation = true; }
|
||||
*/
|
||||
|
||||
AnnotatedParameterProcessor processor =
|
||||
annotatedArgumentProcessors.get(parameterAnnotation.annotationType());
|
||||
isHttpAnnotation |= processor.processArgument(context, parameterAnnotation);
|
||||
}
|
||||
return isHttpAnnotation;
|
||||
}
|
||||
|
||||
private <K, V> boolean searchMapValues(Map<K, Collection<V>> map, V search) {
|
||||
Collection<Collection<V>> values = map.values();
|
||||
if (values == null) {
|
||||
return false;
|
||||
}
|
||||
for (Collection<V> entry : values) {
|
||||
if (entry.contains(search)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void parseProduces(MethodMetadata md, Method method, RequestMapping annotation) {
|
||||
checkAtMostOne(method, annotation.produces(), "produces");
|
||||
String[] serverProduces = annotation.produces();
|
||||
@@ -220,4 +193,55 @@ public class SpringMvcContract extends Contract.BaseContract {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Class<? extends Annotation>, AnnotatedParameterProcessor> toAnnotatedArgumentProcessorMap(List<AnnotatedParameterProcessor> processors) {
|
||||
Map<Class<? extends Annotation>, AnnotatedParameterProcessor> result = new HashMap<>();
|
||||
for(AnnotatedParameterProcessor processor : processors) {
|
||||
result.put(processor.getAnnotationType(), processor);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {
|
||||
|
||||
List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
|
||||
|
||||
annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
|
||||
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
|
||||
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
|
||||
|
||||
return annotatedArgumentResolvers;
|
||||
}
|
||||
|
||||
private class SimpleAnnotatedParameterContext implements AnnotatedParameterProcessor.AnnotatedParameterContext {
|
||||
|
||||
private final MethodMetadata methodMetadata;
|
||||
|
||||
private final int parameterIndex;
|
||||
|
||||
public SimpleAnnotatedParameterContext(MethodMetadata methodMetadata, int parameterIndex) {
|
||||
this.methodMetadata = methodMetadata;
|
||||
this.parameterIndex = parameterIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodMetadata getMethodMetadata() {
|
||||
return methodMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getParameterIndex() {
|
||||
return parameterIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParameterName(String name) {
|
||||
nameParam(methodMetadata, name, parameterIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> setTemplateParameter(String name, Collection<String> rest) {
|
||||
return addTemplatedParam(rest, name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
package org.springframework.cloud.netflix.feign.support;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -22,6 +16,11 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
|
||||
import feign.MethodMetadata;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* @author chadjaros
|
||||
|
||||
Reference in New Issue
Block a user