Skip to content

Commit

Permalink
[OpenFeignGH-57] Introduce Annotation Processors
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 88d0ce2 commit 8c63e8e
Show file tree
Hide file tree
Showing 18 changed files with 405 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -42,7 +39,6 @@ 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>>
Expand Down Expand Up @@ -121,16 +117,27 @@ public TargetDefinition apply(Class<?> targetType, FeignConfiguration configurat
return builder.build();
}

private void registerAnnotationProcessor(Annotation annotation, AnnotationProcessor<?> processor) {
this.annotationProcessors.put(annotation.annotationType(), processor);
}

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
Expand All @@ -147,15 +154,15 @@ protected void processAnnotationsOnType(Class<?> type, TargetMethodDefinition.Bu
/**
* Apply any Annotations located at the Method level.
*
* @param type to the method belongs to.
* @param method to inspect
* @param builder to store the applied configuration.
*/
protected void processAnnotationsOnMethod(Class<?> type, Method method,
TargetMethodDefinition.Builder builder) {
/* set the common method information */
builder.name(method.getName());
builder.returnType(method.getGenericReturnType().getTypeName());
builder.returnTypeDefinition(
TypeDefinitionFactory.getInstance().create(method.getGenericReturnType(), type));
this.processAnnotations(method.getAnnotations(), this.getSupportedMethodAnnotations(), builder);
}

Expand Down
16 changes: 16 additions & 0 deletions core/src/main/java/feign/contract/AnnotationProcessor.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* 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;
Expand Down
110 changes: 26 additions & 84 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,93 +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 the Headers annotation.
* Support {@link Request} and {@link Headers} at the class level.
*
* @param headers annotation to process.
* @param targetMethodDefinition for the request.
* @return set of supported annotations at the class level.
*/
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);
}
}
@Override
protected Collection<Class<? extends Annotation>> getSupportedClassAnnotations() {
return Set.of(Request.class, Headers.class);
}

/**
* Process the Header annotation.
* Support the same items at the class level at the method level.
*
* @param header annotation to process.
* @param targetMethodDefinition for the header.
* @return a set of supported annotations at the method level.
*/
private void processHeader(Header header, TargetMethodDefinition.Builder targetMethodDefinition) {
HttpHeader httpHeader = new HttpHeader(header.name());
httpHeader.value(header.value());
targetMethodDefinition.header(httpHeader);
@Override
protected Collection<Class<? extends Annotation>> getSupportedMethodAnnotations() {
return this.getSupportedClassAnnotations();
}

/**
* Process the Param annotation.
* Support the {@link Param} and {@link Body} annotations at the parameter level.
*
* @param parameter annotation to process.
* @param index of the parameter in the method signature.
* @param type of the parameter.
* @param targetMethodDefinition for the parameter.
* @return the set of supported annotations at the parameter level.
*/
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());
@Override
protected Collection<Class<? extends Annotation>> getSupportedParameterAnnotations() {
return Set.of(Param.class, Body.class);
}

/**
* 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());
}


}
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* 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;
Expand Down
62 changes: 42 additions & 20 deletions core/src/main/java/feign/contract/TargetMethodDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import feign.http.RequestSpecification;
import feign.impl.type.TypeDefinition;
import feign.impl.type.TypeDefinitionFactory;
import feign.impl.type.TypeUtils;
import feign.support.Assert;
import feign.support.StringUtils;
import feign.template.TemplateParameter;
Expand Down Expand Up @@ -52,7 +53,7 @@ public final class TargetMethodDefinition {
private final String targetType;
private final String name;
private String returnTypeFullyQualifiedClassName;
private transient TypeDefinition returnType;
private transient TypeDefinition returnTypeDefinition;
private final Consumer<RequestSpecification> target;
private final String tag;
private final HttpMethod method;
Expand Down Expand Up @@ -87,9 +88,13 @@ public static Builder from(TargetMethodDefinition targetMethodDefinition) {
.readTimeout(targetMethodDefinition.readTimeout)
.target(targetMethodDefinition.target);

if (targetMethodDefinition.returnType != null) {
builder.returnType(targetMethodDefinition.returnTypeFullyQualifiedClassName);
if (targetMethodDefinition.returnTypeDefinition != null) {
builder.returnTypeDefinition(targetMethodDefinition.returnTypeDefinition);
} else if (targetMethodDefinition.returnTypeFullyQualifiedClassName != null) {
builder.returnTypeFullyQualifiedClassName(
targetMethodDefinition.returnTypeFullyQualifiedClassName);
}

if (targetMethodDefinition.template != null) {
builder.uri(targetMethodDefinition.template.toString());
}
Expand Down Expand Up @@ -138,10 +143,12 @@ private TargetMethodDefinition(TargetMethodDefinition.Builder builder) {
.collect(Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue));
}

if (builder.returnType != null) {
this.returnTypeFullyQualifiedClassName = builder.returnType;
if (builder.returnTypeDefinition != null) {
this.returnTypeDefinition = builder.returnTypeDefinition;
} else if (StringUtils.isNotEmpty(builder.returnTypeFullyQualifiedClassName)) {
this.returnTypeFullyQualifiedClassName = builder.returnTypeFullyQualifiedClassName;
} else {
this.returnType = null;
this.returnTypeDefinition = null;
}
}

Expand All @@ -163,18 +170,21 @@ public String getReturnTypeFullyQualifiedClassName() {
*
* @return return Type.
*/
public synchronized TypeDefinition getReturnType() {
if (this.returnType == null && StringUtils.isNotEmpty(this.returnTypeFullyQualifiedClassName)) {
try {
return TypeDefinitionFactory.getInstance()
.create(
Class.forName(this.returnTypeFullyQualifiedClassName),
Class.forName(this.targetType));
} catch (ClassNotFoundException cnfe) {
throw new IllegalStateException("Error obtaining return type definition.", cnfe);
public TypeDefinition getReturnTypeDefinition() {
if (this.returnTypeDefinition == null
&& StringUtils.isNotEmpty(this.returnTypeFullyQualifiedClassName)) {
synchronized (this) {
try {
Class<?> type = TypeUtils.getInstance(this.returnTypeFullyQualifiedClassName);
Class<?> context = TypeUtils.getInstance(this.targetType);
return TypeDefinitionFactory.getInstance()
.create(type, context);
} catch (Exception ex) {
throw new IllegalStateException("Error obtaining return type definition.", ex);
}
}
}
return returnType;
return returnTypeDefinition;
}

/**
Expand Down Expand Up @@ -335,7 +345,7 @@ public String toString() {
.add("target=" + targetType)
.add("name='" + name + "'")
.add("tag='" + tag + "'")
.add("returnType=" + returnType)
.add("returnType=" + returnTypeDefinition)
.add("template=" + template)
.add("method=" + method)
.add("followRedirects=" + followRedirects)
Expand All @@ -352,7 +362,8 @@ public static class Builder {
private final String targetType;
private String name;
private String tag;
private String returnType;
private String returnTypeFullyQualifiedClassName;
private transient TypeDefinition returnTypeDefinition;
private Consumer<RequestSpecification> target = RequestSpecification::uri;
private UriTemplate template;
private HttpMethod method = HttpMethod.GET;
Expand Down Expand Up @@ -395,14 +406,25 @@ public Builder tag(String tag) {
return this;
}

/**
* The {@link TypeDefinition} of the method's return type.
*
* @param typeDefinition of the method's return type.
* @return the reference chain.
*/
public Builder returnTypeDefinition(TypeDefinition typeDefinition) {
this.returnTypeDefinition = typeDefinition;
return this;
}

/**
* Method Return Type.
*
* @param returnTypeFullyQualifiedClassName of the method.
* @return the reference chain.
*/
public Builder returnType(String returnTypeFullyQualifiedClassName) {
this.returnType = returnTypeFullyQualifiedClassName;
public Builder returnTypeFullyQualifiedClassName(String returnTypeFullyQualifiedClassName) {
this.returnTypeFullyQualifiedClassName = returnTypeFullyQualifiedClassName;
return this;
}

Expand Down
Loading

0 comments on commit 8c63e8e

Please sign in to comment.