Skip to content

Commit

Permalink
[GH-57] Introduce Annotation Processors (#70)
Browse files Browse the repository at this point in the history
This change introduces the concept of an `AnnotationProcessor`.
These components are used when processing an `AnnotationDrivenContract`

Contracts can now be modularized whereas they are now collections of
annotation processors instead of full fledged parsing components.

By doing so, `AnnotationProcessors` can be shared between our current
reflection based contract parsers and the upcoming compile time annotation
processors.
  • Loading branch information
kdavisk6 committed Jul 23, 2021
1 parent 9aa37eb commit 6268d88
Show file tree
Hide file tree
Showing 18 changed files with 554 additions and 262 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@
import feign.FeignConfiguration;
import feign.contract.TargetDefinition.TargetDefinitionBuilder;
import feign.impl.type.TypeDefinitionFactory;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -34,7 +39,10 @@ public abstract class AbstractAnnotationDrivenContract implements Contract {
private static final Logger logger =
LoggerFactory.getLogger(AbstractAnnotationDrivenContract.class);

protected final TypeDefinitionFactory typeDefinitionFactory = new TypeDefinitionFactory();
private final Map<Class<? extends Annotation>, AnnotationProcessor<Annotation>>
annotationProcessors = new LinkedHashMap<>();
private final Map<Class<? extends Annotation>, ParameterAnnotationProcessor<Annotation>>
parameterAnnotationProcessors = new LinkedHashMap<>();

/**
* Processes the {@link FeignConfiguration} and generate a new {@link TargetDefinition} instance.
Expand Down Expand Up @@ -109,35 +117,92 @@ public TargetDefinition apply(Class<?> targetType, FeignConfiguration configurat
return builder.build();
}

protected abstract Collection<Class<? extends Annotation>> getSupportedClassAnnotations();

protected abstract Collection<Class<? extends Annotation>> getSupportedMethodAnnotations();

protected abstract Collection<Class<? extends Annotation>> getSupportedParameterAnnotations();

@SuppressWarnings("unchecked")
protected <A extends Annotation> void registerAnnotationProcessor(
Class<A> annotation, AnnotationProcessor<A> processor) {
this.annotationProcessors
.computeIfAbsent(annotation, annotationType -> (AnnotationProcessor<Annotation>) processor);
}

@SuppressWarnings("unchecked")
protected <A extends Annotation> void registerParameterAnnotationProcessor(
Class<A> annotation, ParameterAnnotationProcessor<A> processor) {
this.parameterAnnotationProcessors
.computeIfAbsent(annotation,
annotationType -> (ParameterAnnotationProcessor<Annotation>) processor);
}

/**
* Apply any Annotations located at the Type level. Any definitions applied at this level will be
* used as defaults for all methods on the target, unless redefined at the method or parameter
* level.
*
* @param targetType to inspect.
* @param targetMethodDefinitionBuilder to store the applied configuration.
* @param type to inspect.
* @param builder to store the applied configuration.
*/
protected abstract void processAnnotationsOnType(Class<?> targetType,
TargetMethodDefinition.Builder targetMethodDefinitionBuilder);
protected void processAnnotationsOnType(Class<?> type, TargetMethodDefinition.Builder builder) {
/* get the list of annotations supported */
this.processAnnotations(type.getAnnotations(), this.getSupportedClassAnnotations(), builder);
}

/**
* Apply any Annotations located at the Method level.
*
* @param targetType to the method belongs to.
* @param method to inspect
* @param targetMethodDefinitionBuilder to store the applied configuration.
* @param method to inspect
* @param builder to store the applied configuration.
*/
protected abstract void processAnnotationsOnMethod(Class<?> targetType, Method method,
TargetMethodDefinition.Builder targetMethodDefinitionBuilder);
protected void processAnnotationsOnMethod(Class<?> type, Method method,
TargetMethodDefinition.Builder builder) {
/* set the common method information */
builder.name(method.getName());
builder.returnTypeDefinition(
TypeDefinitionFactory.getInstance().create(method.getGenericReturnType(), type));
this.processAnnotations(method.getAnnotations(), this.getSupportedMethodAnnotations(), builder);
}

/**
* Apply any Annotations located at the Parameter level.
*
* @param parameter to inspect.
* @param parameterIndex of the parameter in the method definition.
* @param targetMethodDefinitionBuilder to store the applied configuration.
* @param index of the parameter in the method definition.
* @param builder to store the applied configuration.
*/
protected abstract void processAnnotationsOnParameter(Parameter parameter, Integer parameterIndex,
TargetMethodDefinition.Builder targetMethodDefinitionBuilder);
protected void processAnnotationsOnParameter(Parameter parameter, Integer index,
TargetMethodDefinition.Builder builder) {

Annotation[] annotations = parameter.getAnnotations();
Collection<Class<? extends Annotation>> supportedAnnotations = this
.getSupportedParameterAnnotations();

Arrays.stream(annotations)
.filter(annotation -> supportedAnnotations.contains(annotation.annotationType()))
.filter(annotation -> this.parameterAnnotationProcessors
.containsKey(annotation.annotationType()))
.forEach(annotation -> {
ParameterAnnotationProcessor<Annotation> processor =
this.parameterAnnotationProcessors.get(annotation.annotationType());
processor.process(annotation, parameter.getName(), index, parameter.getType().getName(),
builder);
});
}

private void processAnnotations(Annotation[] annotations,
Collection<Class<? extends Annotation>> supportedAnnotations,
TargetMethodDefinition.Builder builder) {
Arrays.stream(annotations)
.filter(annotation -> supportedAnnotations.contains(annotation.annotationType()))
.filter(annotation -> this.annotationProcessors.containsKey(annotation.annotationType()))
.forEach(annotation -> {
AnnotationProcessor<Annotation> processor = this.annotationProcessors
.get(annotation.annotationType());
processor.process(annotation, builder);
});
}

}
38 changes: 38 additions & 0 deletions core/src/main/java/feign/contract/AnnotationProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2019-2021 OpenFeign Contributors
*
* 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 feign.contract;

import java.lang.annotation.Annotation;

/**
* Annotation Processor interface. Used by Annotation Driven Contracts to process annotated methods
* and classes. These processors are designed to be used in both reflective and compile-time modes.
*
* @param <T> Supported annotation type.
*/
public interface AnnotationProcessor<T extends Annotation> {

/**
* Evaluate and process the provided annotation, updating the provided builder. Implementations
* are expected to update builder via side-effects.
*
* @param annotation to evaluate.
* @param builder with the current method context.
*/
void process(T annotation, TargetMethodDefinition.Builder builder);

}
184 changes: 23 additions & 161 deletions core/src/main/java/feign/contract/FeignContract.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,13 @@

package feign.contract;

import feign.http.HttpHeader;
import feign.http.HttpMethod;
import feign.impl.type.TypeDefinition;
import feign.support.StringUtils;
import feign.template.ExpressionExpander;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import feign.contract.impl.BodyAnnotationProcessor;
import feign.contract.impl.HeadersAnnotationProcessor;
import feign.contract.impl.ParamAnnotationProcessor;
import feign.contract.impl.RequestAnnotationProcessor;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Set;

/**
* Contract that uses Feign annotations.
Expand All @@ -38,173 +34,39 @@ public class FeignContract extends AbstractAnnotationDrivenContract {
*/
public FeignContract() {
super();
this.registerAnnotationProcessor(Request.class, new RequestAnnotationProcessor());
this.registerAnnotationProcessor(Headers.class, new HeadersAnnotationProcessor());
this.registerParameterAnnotationProcessor(Param.class, new ParamAnnotationProcessor());
this.registerParameterAnnotationProcessor(Body.class, new BodyAnnotationProcessor());
}

/**
* Process any Annotations present on the Target Type. Any values determined here should be
* considered common for all methods in the Target.
* Support {@link Request} and {@link Headers} at the class level.
*
* @param targetType to inspect.
* @param targetMethodDefinition to store the resulting configuration.
* @return set of supported annotations at the class level.
*/
@Override
protected void processAnnotationsOnType(Class<?> targetType,
TargetMethodDefinition.Builder targetMethodDefinition) {
if (targetType.isAnnotationPresent(Request.class)) {
this.processRequest(targetType.getAnnotation(Request.class), targetMethodDefinition);
}
if (targetType.isAnnotationPresent(Headers.class)) {
this.processHeaders(targetType.getAnnotation(Headers.class), targetMethodDefinition);
}
protected Collection<Class<? extends Annotation>> getSupportedClassAnnotations() {
return Set.of(Request.class, Headers.class);
}

/**
* Process any Annotations present on the Method.
* Support the same items at the class level at the method level.
*
* @param targetType containing the method.
* @param method to inspect.
* @param targetMethodDefinition to store the resulting configuration.
* @return a set of supported annotations at the method level.
*/
@Override
protected void processAnnotationsOnMethod(Class<?> targetType, Method method,
TargetMethodDefinition.Builder targetMethodDefinition) {
if (method.isAnnotationPresent(Request.class)) {
targetMethodDefinition
.name(method.getName())
.tag(this.getMethodTag(targetType, method))
.returnType(this.getMethodReturnType(method));
this.processRequest(method.getAnnotation(Request.class), targetMethodDefinition);
}
if (method.isAnnotationPresent(Headers.class)) {
this.processHeaders(method.getAnnotation(Headers.class), targetMethodDefinition);
}
protected Collection<Class<? extends Annotation>> getSupportedMethodAnnotations() {
return this.getSupportedClassAnnotations();
}

/**
* Process any Annotations present on the method Parameter.
* Support the {@link Param} and {@link Body} annotations at the parameter level.
*
* @param parameter to inspect.
* @param parameterIndex of the parameter in the method signature.
* @param targetMethodDefinition to store the resulting configuration.
* @return the set of supported annotations at the parameter level.
*/
@Override
protected void processAnnotationsOnParameter(Parameter parameter, Integer parameterIndex,
TargetMethodDefinition.Builder targetMethodDefinition) {
if (parameter.isAnnotationPresent(Param.class)) {
this.processParameter(
parameter.getAnnotation(Param.class),
parameterIndex,
parameter.getType(),
targetMethodDefinition);
}
if (parameter.isAnnotationPresent(Body.class)) {
targetMethodDefinition.body(parameterIndex);
}
protected Collection<Class<? extends Annotation>> getSupportedParameterAnnotations() {
return Set.of(Param.class, Body.class);
}

/**
* Process the HttpRequest annotation.
*
* @param request annotation to process.
* @param targetMethodDefinition for the request.
*/
private void processRequest(Request request,
TargetMethodDefinition.Builder targetMethodDefinition) {
String uri = (StringUtils.isNotEmpty(request.uri())) ? request.uri() : request.value();
HttpMethod httpMethod = request.method();
boolean followRedirects = request.followRedirects();

targetMethodDefinition.uri(uri)
.method(httpMethod)
.followRedirects(followRedirects)
.connectTimeout(request.connectTimeout())
.readTimeout(request.readTimeout());
}

/**
* Process the Headers annotation.
*
* @param headers annotation to process.
* @param targetMethodDefinition for the request.
*/
private void processHeaders(Headers headers,
TargetMethodDefinition.Builder targetMethodDefinition) {
if (headers.value().length != 0) {
Header[] header = headers.value();
for (Header value : header) {
this.processHeader(value, targetMethodDefinition);
}
}
}

/**
* Process the Header annotation.
*
* @param header annotation to process.
* @param targetMethodDefinition for the header.
*/
private void processHeader(Header header, TargetMethodDefinition.Builder targetMethodDefinition) {
HttpHeader httpHeader = new HttpHeader(header.name());
httpHeader.value(header.value());
targetMethodDefinition.header(httpHeader);
}

/**
* Process the Param annotation.
*
* @param parameter annotation to process.
* @param index of the parameter in the method signature.
* @param type of the parameter.
* @param targetMethodDefinition for the parameter.
*/
private void processParameter(Param parameter, Integer index, Class<?> type,
TargetMethodDefinition.Builder targetMethodDefinition) {

String name = parameter.value();
String typeClass = type.getCanonicalName();

Class<? extends ExpressionExpander> expanderClass = parameter.expander();
String expanderClassName = expanderClass.getName();

targetMethodDefinition.parameterDefinition(
index, TargetMethodParameterDefinition.builder()
.name(name)
.index(index)
.type(typeClass)
.expanderClassName(expanderClassName)
.build());
}

/**
* Constructs a name for a Method that is formatted as a javadoc reference.
*
* @param targetType containing the method.
* @param method to inspect.
* @return a See Tag inspired name for the method.
*/
private String getMethodTag(Class<?> targetType, Method method) {
StringBuilder sb = new StringBuilder()
.append(targetType.getSimpleName())
.append("#")
.append(method.getName())
.append("(");
List<Type> parameters = Arrays.asList(method.getGenericParameterTypes());
Iterator<Type> iterator = parameters.iterator();
while (iterator.hasNext()) {
Type parameter = iterator.next();
sb.append(parameter.getTypeName());
if (iterator.hasNext()) {
sb.append(",");
}
}
sb.append(")");
return sb.toString();
}

private TypeDefinition getMethodReturnType(Method method) {
return this.typeDefinitionFactory
.create(method.getGenericReturnType(), method.getDeclaringClass());
}


}
Loading

0 comments on commit 6268d88

Please sign in to comment.