diff --git a/apt-generator/docker/Dockerfile b/apt-generator/docker/Dockerfile new file mode 100644 index 0000000000..e2c491e0fe --- /dev/null +++ b/apt-generator/docker/Dockerfile @@ -0,0 +1,49 @@ +# +# Copyright 2012-2019 The Feign 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. +# + +# build native image +FROM oracle/graalvm-ce:19.0.0 +LABEL vendor="The Last Pickle" + +RUN gu install native-image + +COPY reflectconfig.json . +COPY ${project.artifactId}-${project.version}.jar . + +RUN native-image --verbose \ + --no-fallback \ + -H:ReflectionConfigurationFiles=reflectconfig.json \ + -H:EnableURLProtocols=https \ + -jar ${project.artifactId}-${project.version}.jar \ + -cp ${project.artifactId}-${project.version}.jar + + + +# test the native image +FROM ubuntu + +COPY --from=0 /opt/graalvm-ce-19.0.0/jre/lib/amd64/libsunec.so / +COPY --from=0 /${project.artifactId}-${project.version} / + +RUN /${project.artifactId}-${project.version} + + + +# create docker image with native +FROM ubuntu + +COPY --from=0 /opt/graalvm-ce-19.0.0/jre/lib/amd64/libsunec.so / +COPY --from=0 /${project.artifactId}-${project.version} / + +CMD /${project.artifactId}-${project.version} diff --git a/apt-generator/docker/reflectconfig.json b/apt-generator/docker/reflectconfig.json new file mode 100644 index 0000000000..6e35256716 --- /dev/null +++ b/apt-generator/docker/reflectconfig.json @@ -0,0 +1,41 @@ +[ + { + "name" : "example.github.Repository", + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true + }, + { + "name" : "example.github.Repository", + "fields" : [ + { "name" : "name" } + ] + }, + { + "name" : "example.github.Contributor", + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true + }, + { + "name" : "example.github.Contributor", + "fields" : [ + { "name" : "login" } + ] + }, + { + "name" : "example.github.GitHubClientError", + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true + }, + { + "name" : "example.github.GitHubClientError", + "fields" : [ + { "name" : "message" } + ] + } +] diff --git a/apt-generator/pom.xml b/apt-generator/pom.xml new file mode 100644 index 0000000000..29f3e0402b --- /dev/null +++ b/apt-generator/pom.xml @@ -0,0 +1,224 @@ + + + + 4.0.0 + + + io.github.openfeign + parent + 10.4.1-SNAPSHOT + + + io.github.openfeign.experimental + feign-apt-generator + + + ${project.basedir}/.. + + + + + + io.github.openfeign + feign-bom + ${project.version} + pom + import + + + + + + + io.github.openfeign + feign-core + + + io.github.openfeign + feign-gson + + + io.github.openfeign + feign-example-github + ${project.version} + + + org.apache.commons + commons-text + 1.3 + + + + org.apache.commons + commons-exec + 1.3 + test + + + com.google.testing.compile + compile-testing + 0.18 + test + + + com.google.guava + guava + 28.0-jre + + + com.google.auto.service + auto-service + 1.0-rc5 + provided + + + + org.hamcrest + hamcrest + 2.1 + test + + + + org.codehaus.groovy + groovy-all + 2.4.8 + test + + + + org.ccci + atlassian-hamcrest + 1.1-SNAPSHOT + test + + + io.github.openfeign + feign-core + ${project.version} + test-jar + test + + + + + + + docker + true + + ${project.basedir}/docker + + + + ${basedir}/src/main/resources + + + src/main/java + + **/*.java + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.3 + + + package + + shade + + + + + feign.aptgenerator.github.GitHubFactoryExample + + + false + + + + + + org.skife.maven + really-executable-jar-maven-plugin + 1.5.0 + + github + + + + package + + really-executable-jar + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-surefire-plugin.version} + + + + integration-test + verify + + + + + + + + com.spotify + docker-maven-plugin + + + ${project.build.directory}/classes/docker/ + + + true + + docker-hub + https://index.docker.io/v1/ + feign-apt-generator/test + + + / + ${project.build.directory} + ${project.artifactId}-${project.version}.jar + + + + + + + post-integration-test + + build + + + + + + + diff --git a/apt-generator/src/main/java/feign/apt/runtime/ParameterizedType.java b/apt-generator/src/main/java/feign/apt/runtime/ParameterizedType.java new file mode 100644 index 0000000000..d2eedce592 --- /dev/null +++ b/apt-generator/src/main/java/feign/apt/runtime/ParameterizedType.java @@ -0,0 +1,89 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.apt.runtime; + +import java.lang.reflect.Type; +import java.util.Arrays; + +public class ParameterizedType implements java.lang.reflect.ParameterizedType { + + private final Type ownerType; + private final Type rawType; + private final Type[] typedArgs; + + public ParameterizedType(Type rawType, Type ownerType, Type... typedArgs) { + super(); + this.ownerType = ownerType; + this.rawType = rawType; + this.typedArgs = typedArgs; + } + + @Override + public Type[] getActualTypeArguments() { + return this.typedArgs; + } + + @Override + public Type getRawType() { + return this.rawType; + } + + @Override + public Type getOwnerType() { + return this.ownerType; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((getOwnerType() == null) ? 0 : getOwnerType().hashCode()); + result = prime * result + ((getRawType() == null) ? 0 : getRawType().hashCode()); + result = prime * result + Arrays.hashCode(getActualTypeArguments()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof java.lang.reflect.ParameterizedType)) { + return false; + } + final java.lang.reflect.ParameterizedType other = (java.lang.reflect.ParameterizedType) obj; + if (getOwnerType() == null) { + if (other.getOwnerType() != null) { + return false; + } + } else if (!getOwnerType().equals(other.getOwnerType())) { + return false; + } + if (getRawType() == null) { + if (other.getRawType() != null) { + return false; + } + } else if (!getRawType().equals(other.getRawType())) { + return false; + } + if (!Arrays.equals(getActualTypeArguments(), other.getActualTypeArguments())) { + return false; + } + return true; + } + +} diff --git a/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java b/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java new file mode 100644 index 0000000000..03c44a0843 --- /dev/null +++ b/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java @@ -0,0 +1,244 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator; + +import static feign.Util.checkState; +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.*; +import java.util.stream.Collectors; +import javax.lang.model.element.*; +import feign.DeclarativeContract; +import feign.DeclarativeContract.AnnotationProcessor; +import feign.DeclarativeContract.ParameterAnnotationProcessor; +import feign.MethodMetadata; + +/** + * Defines what annotations and values are valid on interfaces. + */ +public class APTContractVisitor { + + public List parseAndValidateMetadata(TypeElement targetType, + DeclarativeContract processorsSource) { + checkState(targetType.getTypeParameters().size() == 0, "Parameterized types unsupported: %s", + targetType.getSimpleName()); + checkState(targetType.getInterfaces().size() <= 1, "Only single inheritance supported: %s", + targetType.getSimpleName()); + + final Map result = new LinkedHashMap(); + for (final Element element : targetType.getEnclosedElements()) { + if (element instanceof ExecutableElement) { + final ExecutableElement method = (ExecutableElement) element; + if (method.getModifiers().contains(Modifier.STATIC) || + method.isDefault()) { + continue; + } + final MethodMetadata metadata = + parseAndValidateMetadata(targetType, method, processorsSource); + checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s", + metadata.configKey()); + result.put(metadata.configKey(), metadata); + } + } + return new ArrayList<>(result.values()); + } + + + /** + * Called indirectly by {@link #parseAndValidateMetadata(Class)}. + */ + protected MethodMetadata parseAndValidateMetadata(TypeElement targetType, + ExecutableElement method, + DeclarativeContract processorsSource) { + final MethodMetadata data = new MethodMetadata(); + // TODO create warping type data.returnType(method.getReturnType()); + data.configKey(configKey(targetType, method)); + + // TODO if (targetType.getInterfaces().size() == 1) { + // processAnnotationOnClass(data, targetType.getInterfaces().get(0), processorsSource); + // } + processAnnotationOnClass(data, targetType, processorsSource); + + processAnnotationOnMethod(data, method, processorsSource); + checkState(data.template().method() != null, + "Method %s not annotated with HTTP method type (ex. GET, POST)", + data.configKey()); + final List parameterTypes = method.getParameters(); + + final int count = parameterTypes.size(); + for (int i = 0; i < count; i++) { + boolean isHttpAnnotation = false; + if (!parameterTypes.get(i).getAnnotationMirrors().isEmpty()) { + isHttpAnnotation = + processAnnotationsOnParameter(data, parameterTypes.get(i), i, processorsSource); + } + if ( + // TODO this probably do not work as intended + parameterTypes.get(i).asType().toString() == URI.class.getName()) { + data.urlIndex(i); + } else if (!isHttpAnnotation + // TODO && parameterTypes[i] != Request.Options.class + ) { + checkState(data.formParams().isEmpty(), + "Body parameters cannot be used with form parameters."); + checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method); + data.bodyIndex(i); + // data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i])); + } + } + + if (data.headerMapIndex() != null) { + // TODO how to check if one param is a Map on APT? + // checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()], + // genericParameterTypes[data.headerMapIndex()]); + } + + if (data.queryMapIndex() != null) { + // TODO how to check if one param is a Map on APT? + // if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) { + // checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]); + // } + } + + return data; + } + + private String configKey(TypeElement targetType, ExecutableElement method) { + final StringBuilder builder = new StringBuilder(); + builder.append(targetType.getSimpleName()); + builder.append('#').append(method.getSimpleName()).append('('); + for (final TypeParameterElement param : method.getTypeParameters()) { + builder.append(param.getSimpleName()).append(','); + } + if (method.getTypeParameters().size() > 0) { + builder.deleteCharAt(builder.length() - 1); + } + return builder.append(')').toString(); + } + + private static void checkMapString(String name, Class type, Type genericType) { + checkState(Map.class.isAssignableFrom(type), + "%s parameter must be a Map: %s", name, type); + checkMapKeys(name, genericType); + } + + private static void checkMapKeys(String name, Type genericType) { + Class keyClass = null; + + // assume our type parameterized + if (ParameterizedType.class.isAssignableFrom(genericType.getClass())) { + final Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments(); + keyClass = (Class) parameterTypes[0]; + } else if (genericType instanceof Class) { + // raw class, type parameters cannot be inferred directly, but we can scan any extended + // interfaces looking for any explict types + final Type[] interfaces = ((Class) genericType).getGenericInterfaces(); + if (interfaces != null) { + for (final Type extended : interfaces) { + if (ParameterizedType.class.isAssignableFrom(extended.getClass())) { + // use the first extended interface we find. + final Type[] parameterTypes = ((ParameterizedType) extended).getActualTypeArguments(); + keyClass = (Class) parameterTypes[0]; + break; + } + } + } + } + + if (keyClass != null) { + checkState(String.class.equals(keyClass), + "%s key must be a String: %s", name, keyClass.getSimpleName()); + } + } + + /** + * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target type + * (unless they are the same). + * + * @param data metadata collected so far relating to the current java method. + * @param processorsSource + * @param clz the class to process + */ + protected void processAnnotationOnClass(MethodMetadata data, + TypeElement targetType, + DeclarativeContract processorsSource) { + final List annotations = + processorsSource.getClassAnnotationProcessors(); + annotations.forEach((type, processor) -> { + final Annotation annotation = targetType.getAnnotation(type); + if (annotation != null) { + processor.process(annotation, data); + } + }); + } + + /** + * @param data metadata collected so far relating to the current java method. + * @param method method currently being processed. + * @param processorsSource + */ + private void processAnnotationOnMethod(MethodMetadata data, + ExecutableElement method, + DeclarativeContract processorsSource) { + final Map, AnnotationProcessor> annotations = + processorsSource.getMethodAnnotationProcessors(); + annotations.forEach((type, processor) -> { + final Annotation annotation = method.getAnnotation(type); + if (annotation != null) { + processor.process(annotation, data); + } + }); + } + + /** + * @param data metadata collected so far relating to the current java method. + * @param annotations annotations present on the current parameter annotation. + * @param paramIndex if you find a name in {@code annotations}, call + * {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter. + * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an + * http-relevant annotation. + */ + private boolean processAnnotationsOnParameter(MethodMetadata data, + VariableElement parameter, + int paramIndex, + DeclarativeContract processorsSource) { + final Map, ParameterAnnotationProcessor> annotations = + processorsSource.getParameterAnnotationProcessors(); + + return annotations.entrySet() + .stream() + .map(entry -> { + final Annotation annotation = parameter.getAnnotation(entry.getKey()); + if (annotation != null) { + return entry.getValue().process(annotation, data, paramIndex); + } + return false; + }) + .collect(Collectors.reducing(Boolean::logicalOr)) + .orElse(false); + } + + /** + * links a parameter name to its index in the method signature. + */ + protected void nameParam(MethodMetadata data, String name, int i) { + final Collection names = + data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList(); + names.add(name); + data.indexToName().put(i, names); + } + +} diff --git a/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java b/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java new file mode 100644 index 0000000000..7a793386d3 --- /dev/null +++ b/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java @@ -0,0 +1,174 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator; + +import static feign.Util.checkState; +import static feign.Util.emptyToNull; +import com.google.auto.service.AutoService; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import java.lang.reflect.Type; +import java.util.*; +import java.util.stream.Collectors; +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.WildcardType; +import javax.tools.Diagnostic.Kind; +import javax.tools.JavaFileObject; +import feign.Contract; +import feign.MethodMetadata; +import feign.Param; + +@SupportedAnnotationTypes({ + "feign.RequestLine" +}) +@SupportedSourceVersion(SourceVersion.RELEASE_8) +@AutoService(Processor.class) +public class ContractAPT extends AbstractProcessor { + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + System.out.println(annotations); + System.out.println(roundEnv); + + final Map> clientsToGenerate = annotations.stream() + .map(roundEnv::getElementsAnnotatedWith) + .flatMap(Set::stream) + .map(ExecutableElement.class::cast) + .collect(Collectors.toMap( + annotatedMethod -> TypeElement.class.cast(annotatedMethod.getEnclosingElement()), + ImmutableList::of, + (list1, list2) -> ImmutableList.builder() + .addAll(list1) + .addAll(list2) + .build())); + + System.out.println("Count: " + clientsToGenerate.size()); + System.out.println("clientsToGenerate: " + clientsToGenerate); + + final Contract.Default contract = new Contract.Default(); + contract.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> { + final String name = paramAnnotation.value(); + checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", + paramIndex); + contract.nameParam(data, name, paramIndex); + // FIXME exception "Attempt to access Class object for TypeMirror" + // Class expander = paramAnnotation.expander(); + // if (expander != Param.ToStringExpander.class) { + // data.indexToExpanderClass().put(paramIndex, expander); + // } + data.indexToEncoded().put(paramIndex, paramAnnotation.encoded()); + if (!data.template().hasRequestVariable(name)) { + data.formParams().add(name); + } + return true; + }); + + clientsToGenerate.forEach((type, methods) -> { + try { + final String jPackage = readPackage(type); + final JavaFileObject builderFile = processingEnv.getFiler() + .createSourceFile(type.getSimpleName() + "Factory"); + final StringBuilder writer = new StringBuilder(); + writer.append("package " + jPackage + ";").append("\n"); + writer.append("import feign.*;").append("\n"); + writer.append("public class " + type.getSimpleName() + "Factory").append("\n"); + writer.append("{").append("\n"); + + final List methodMetadatas = + new APTContractVisitor().parseAndValidateMetadata(type, contract); + + methodMetadatas.stream() + .peek( + method -> System.out.println("Generating metadata for method" + method.configKey())) + .forEach(method -> { + final String metadataFieldName = + "__" + method.configKey().replaceAll("\\W+", "_") + "_metadata"; + writer + .append( + "private static final MethodMetadata " + metadataFieldName + ";") + .append("\n"); + writer + .append("static { ").append("\n") + .append(MethodMetadataSerializer.toJavaCode(method)).append("\n") + .append(metadataFieldName + " = md;").append("\n") + .append("}").append("\n"); + }); + + writer.append("}").append("\n"); + + System.out.println(writer); + + builderFile.openWriter().append(writer).close(); + } catch (final Exception e) { + e.printStackTrace(); + processingEnv.getMessager().printMessage(Kind.ERROR, + "Unable to generate factory for " + type); + } + }); + + if (!clientsToGenerate.isEmpty()) { + try { + final JavaFileObject builderFile = processingEnv.getFiler() + .createSourceFile("feign.apt.runtime.ParameterizedType"); + ByteStreams.copy( + getClass().getResourceAsStream("/feign/apt/runtime/ParameterizedType.java"), + builderFile.openOutputStream()); + } catch (final Exception e) { + e.printStackTrace(); + processingEnv.getMessager().printMessage(Kind.ERROR, + "Unable to write ParameterizedType.java"); + } + } + + return true; + } + + + + private Type toJavaType(TypeMirror type) { + outType(type.getClass()); + if (type instanceof WildcardType) { + + } + return Object.class; + } + + private void outType(Class class1) { + if (Object.class.equals(class1) || class1 == null) { + return; + } + System.out.println(class1); + outType(class1.getSuperclass()); + Arrays.stream(class1.getInterfaces()).forEach(this::outType); + } + + + + private String readPackage(Element type) { + if (type.getKind() == ElementKind.PACKAGE) { + return type.getSimpleName().toString(); + } + + if (type.getKind() == ElementKind.CLASS + || type.getKind() == ElementKind.INTERFACE) { + return readPackage(type.getEnclosingElement()); + } + + return null; + } + +} diff --git a/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java b/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java new file mode 100644 index 0000000000..f5961d6974 --- /dev/null +++ b/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java @@ -0,0 +1,108 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator; + +import org.apache.commons.text.StringEscapeUtils; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.stream.Collectors; +import feign.MethodMetadata; + +public class MethodMetadataSerializer { + + public static StringBuilder toJavaCode(MethodMetadata method) { + final StringBuilder sb = new StringBuilder(); + + sb.append(" final MethodMetadata md = new MethodMetadata();").append("\n"); + sb.append(" md.returnType(" + toJavaCode(method.returnType()) + ");").append("\n"); + sb.append(" md.configKey(\"" + method.configKey() + "\");").append("\n"); + if (method.bodyIndex() != null) { + sb.append(" md.bodyIndex(" + method.bodyIndex() + ");").append("\n"); + } + sb.append("\n"); + sb.append( + " md.template().method(feign.Request.HttpMethod." + method.template().method() + ");") + .append("\n"); + sb.append(" md.template().uri(\"" + method.template().uri() + "\", "+method.template().uriAppend()+");") + .append("\n"); + sb.append(" md.template().decodeSlash(" + method.template().decodeSlash() + ");") + .append("\n"); + sb.append(" md.template().collectionFormat(CollectionFormat." + + method.template().collectionFormat().name() + ");") + .append("\n"); + method.template().headers().forEach((headerName, value) -> sb + .append(" md.template().header(\"" + headerName + "\", " + + value.stream().collect(Collectors.joining("\",\"", "\"", "\"")) + ");")); + if (method.template().requestBody().asBytes() != null) { + sb.append(" md.template().body(\"" + + method.template().requestBody().asString() + + "\");") + .append("\n"); + } + if (method.template().requestBody().bodyTemplate() != null) { + sb.append(" md.template().body(feign.Request.Body.bodyTemplate(\"" + + StringEscapeUtils.escapeJava(method.template().requestBody().bodyTemplate()) + + "\", java.nio.charset.Charset.forName(\"" + + method.template().requestBody().encoding().name() + "\")" + + "));") + .append("\n"); + } + + + sb.append("\n"); + method.formParams().forEach(formParam -> sb + .append(" md.formParams().add(\"" + formParam + "\");")); + + method.indexToName().forEach((index, valueList) -> sb + .append(" md.indexToName().put(" + index + ", java.util.Arrays.asList(" + + valueList.stream().collect(Collectors.joining("\",\"", "\"", "\"")) + "));") + .append("\n")); + if (method.indexToExpander() != null) { + method.indexToExpander().forEach((index, expander) -> sb + .append(" md.indexToExpander().put(" + index + ", new " + expander.getClass().getName() + "());") + .append("\n")); + } + method.indexToEncoded().forEach((index, encoded) -> sb + .append(" md.indexToEncoded().put(" + index + ", " + encoded + ");") + .append("\n")); + if (method.bodyType() != null) { + sb.append(" md.bodyType(" + toJavaCode(method.bodyType()) + ");").append("\n"); + } + + return sb; + } + + private static String toJavaCode(Type type) { + if (type == null) { + return null; + } + + if (type instanceof Class) { + return ((Class) type).getName() + ".class"; + } + + if (type instanceof ParameterizedType) { + return "new feign.apt.runtime.ParameterizedType(" + + toJavaCode(((ParameterizedType) type).getRawType()) + ", " + + toJavaCode(((ParameterizedType) type).getOwnerType()) + ", " + + Arrays.stream(((ParameterizedType) type).getActualTypeArguments()) + .map(MethodMetadataSerializer::toJavaCode) + .collect(Collectors.joining(",")) + + ")"; + } + throw new RuntimeException("not implemented " + type.getClass()); + } + +} diff --git a/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java new file mode 100644 index 0000000000..eb44b27ecc --- /dev/null +++ b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java @@ -0,0 +1,161 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator.github; + +import java.lang.reflect.Type; +import java.util.List; +import example.github.Contributor; +import example.github.GitHubExample.GitHub; +import example.github.Issue; +import example.github.Repository; +import feign.*; +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Request.HttpMethod; +import feign.Target.HardCodedTarget; +import feign.apt.runtime.ParameterizedType; + +public class GitHubFactory implements GitHub { + + private static abstract class TypeResolver { + private final Type type; + + private TypeResolver() { + this.type = + ((java.lang.reflect.ParameterizedType) getClass().getGenericSuperclass()) + .getActualTypeArguments()[0]; + } + + public Type resolve() { + return type; + } + + } + + private static final MethodMetadata __GitHub_repos__metadata; + static { + final MethodMetadata md = new MethodMetadata(); + md.returnType(new ParameterizedType(List.class, null, Repository.class)); + md.configKey("GitHub#repos(String)"); + + md.template().method(HttpMethod.GET); + md.template().uri("/users/{username}/repos?sort=full_name"); + md.template().decodeSlash(true); + md.template().collectionFormat(CollectionFormat.EXPLODED); + + md.indexToName().put(0, java.util.Arrays.asList("username")); + md.indexToEncoded().put(0, false); + __GitHub_repos__metadata = md; + } + + private static final MethodMetadata __GitHub_contributors__metadata; + static { + final MethodMetadata md = new MethodMetadata(); + __GitHub_contributors__metadata = md; + md.returnType(new ParameterizedType(List.class, null, Contributor.class)); + md.configKey("GitHub#contributors(String,String)"); + + md.template().method(HttpMethod.GET); + md.template().uri("/repos/{owner}/{repo}/contributors"); + md.template().decodeSlash(true); + md.template().collectionFormat(CollectionFormat.EXPLODED); + + md.indexToName().put(0, java.util.Arrays.asList("owner")); + md.indexToEncoded().put(0, false); + + md.indexToName().put(1, java.util.Arrays.asList("repo")); + md.indexToEncoded().put(1, false); + } + + private static final MethodMetadata __GitHub_createIssue__metadata; + static { + final MethodMetadata md = new MethodMetadata(); + __GitHub_createIssue__metadata = md; + md.returnType(void.class); + md.configKey("GitHub#createIssue(Issue,String,String)"); + + md.template().method(HttpMethod.POST); + md.template().uri("/repos/{owner}/{repo}/issues"); + md.template().decodeSlash(true); + md.template().collectionFormat(CollectionFormat.EXPLODED); + + md.indexToName().put(0, java.util.Arrays.asList("owner")); + md.indexToEncoded().put(0, false); + + md.indexToName().put(1, java.util.Arrays.asList("repo")); + md.indexToEncoded().put(1, false); + } + + private final MethodHandler __repos_handler; + private final MethodHandler __contributors_handler; + private final SynchronousMethodHandler __createIssue_handler; + + public GitHubFactory(FeignConfig feignConfig) { + final HardCodedTarget target = + new HardCodedTarget(GitHub.class, feignConfig.url); + + __repos_handler = new SynchronousMethodHandler( + target, + feignConfig, + __GitHub_repos__metadata, + ReflectiveFeign.from(__GitHub_repos__metadata, feignConfig)); + + __contributors_handler = new SynchronousMethodHandler( + target, + feignConfig, + __GitHub_contributors__metadata, + ReflectiveFeign.from(__GitHub_contributors__metadata, feignConfig)); + + __createIssue_handler = new SynchronousMethodHandler( + target, + feignConfig, + __GitHub_contributors__metadata, + ReflectiveFeign.from(__GitHub_contributors__metadata, feignConfig)); + + } + + @Override + public List repos(String owner) { + try { + return __repos_handler.invoke(owner); + } catch (final FeignException e) { + throw e; + } catch (final Throwable e) { + throw new FeignException(-1, "", e) {}; + } + } + + @Override + public List contributors(String owner, String repo) { + try { + return __contributors_handler.invoke(owner, repo); + } catch (final RuntimeException e) { + throw e; + } catch (final Throwable e) { + throw new FeignException(-1, "", e) {}; + } + } + + @Override + public void createIssue(Issue issue, String owner, String repo) { + try { + __createIssue_handler.invoke(issue, owner, repo); + } catch (final RuntimeException e) { + throw e; + } catch (final Throwable e) { + throw new FeignException(-1, "", e) {}; + } + + } + +} diff --git a/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactoryExample.java b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactoryExample.java new file mode 100644 index 0000000000..2191a4b3c1 --- /dev/null +++ b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactoryExample.java @@ -0,0 +1,60 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator.github; + +import com.google.gson.GsonBuilder; +import java.util.List; +import example.github.GitHubClientError; +import example.github.GitHubExample.GitHub; +import example.github.GitHubExample.GitHubErrorDecoder; +import feign.FeignConfig; +import feign.Logger; +import feign.codec.Decoder; +import feign.gson.GsonDecoder; + +/** + * Inspired by {@code com.example.retrofit.GitHubClient} + */ +public class GitHubFactoryExample { + + static FeignConfig config() { + final Decoder decoder = new GsonDecoder(new GsonBuilder() + .create()); + return FeignConfig.builder() + .decoder(decoder) + .errorDecoder(new GitHubErrorDecoder(decoder)) + .logger(new Logger.ErrorLogger()) + .logLevel(Logger.Level.FULL) + .url("https://api.github.com") + .build(); + } + + public static void main(String[] args) { + final GitHub github = new GitHubFactory(config()); + + System.out.println("Let's fetch and print a list of the contributors to this org."); + final List contributors = github.contributors("openfeign"); + for (final String contributor : contributors) { + System.out.println(contributor); + } + + System.out.println("Now, let's cause an error."); + try { + github.contributors("openfeign", "some-unknown-project"); + } catch (final GitHubClientError e) { + System.out.println(e.getMessage()); + } + } + +} diff --git a/apt-generator/src/test/java/feign/aptgenerator/ContractAPTTest.java b/apt-generator/src/test/java/feign/aptgenerator/ContractAPTTest.java new file mode 100644 index 0000000000..fa2c91b571 --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/ContractAPTTest.java @@ -0,0 +1,55 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import org.junit.Test; +import java.io.File; + +/** + * Test for {@link ContractAPT} + */ +public class ContractAPTTest { + + private final File main = new File("../example-github/src/main/java/").getAbsoluteFile(); + + @Test + public void test() throws Exception { + final Compilation compilation = + javac() + .withProcessors(new ContractAPT()) + .compile(JavaFileObjects.forResource( + new File(main, "example/github/GitHubExample.java") + .toURI() + .toURL())); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedSourceFile("feign.apt.runtime.ParameterizedType") + .hasSourceEquivalentTo(JavaFileObjects.forResource( + new File(main, "feign/apt/runtime/ParameterizedType.java") + .toURI() + .toURL())); + assertThat(compilation) + .generatedSourceFile("GitHubFactory") + .hasSourceEquivalentTo(JavaFileObjects.forResource( + new File("src/main/java/feign/aptgenerator/github/GitHubFactory.java") + .toURI() + .toURL() + )); + } + +} diff --git a/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerRepeatTest.java b/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerRepeatTest.java new file mode 100644 index 0000000000..5f129d020c --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerRepeatTest.java @@ -0,0 +1,95 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; +import com.atlassian.hamcrest.DeepIsEqual; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import example.github.GitHubExample.GitHub; +import feign.Contract; +import feign.DefaultContractTest; +import feign.MethodMetadata; +import groovy.lang.Binding; +import groovy.lang.GroovyShell; + + +/** + * Check if the generated java code generates a new {@link MethodMetadata} that is equal to source + * {@link MethodMetadata} + */ +@RunWith(Parameterized.class) +public class MethodMetadataSerializerRepeatTest { + + private static final GroovyShell shell = new GroovyShell(new Binding()); + + @Parameters(name = "{0}") + public static List methods() { + return Arrays.asList( + GitHub.class, + // TODO ideally we need to test all interfaces from DefaultContractTest + DefaultContractTest.BodyWithoutParameters.class, + // expanders not supported + // DefaultContractTest.CustomExpander.class, + DefaultContractTest.CustomMethod.class, + DefaultContractTest.DefaultMethodOnInterface.class, + DefaultContractTest.FormParams.class, + DefaultContractTest.HeaderParams.class, + DefaultContractTest.HeaderParamsNotAtStart.class, + DefaultContractTest.HeadersContainsWhitespaces.class, + DefaultContractTest.HeadersOnType.class, + DefaultContractTest.Methods.class + ) + .stream() + // create methodmetadata using default reflective code + .map(new Contract.Default()::parseAndValidatateMetadata) + .flatMap(List::stream) + .map(md -> new Object[] {md.configKey(), md}) + .collect(Collectors.toList()); + } + + private final MethodMetadata md; + + public MethodMetadataSerializerRepeatTest(String configKey, MethodMetadata md) { + this.md = md; + } + + @Test + public void serializedSameAsReflective() { + + final StringBuilder result = MethodMetadataSerializer.toJavaCode(md); + + final MethodMetadata resultMetadata = (MethodMetadata) shell.evaluate("" + + "import feign.*;\n" + + result.toString() + + "\nreturn md;"); + + assertThat(resultMetadata, not(sameInstance(md))); + assertThat(resultMetadata, DeepIsEqual.deeplyEqualTo(md)); + // compare transient fields too + assertThat(resultMetadata.indexToExpander(), equalTo(md.indexToExpander())); + assertThat(resultMetadata.returnType(), equalTo(md.returnType())); + assertThat(resultMetadata.bodyType(), equalTo(md.bodyType())); + + } + +} diff --git a/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerTest.java b/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerTest.java new file mode 100644 index 0000000000..5b85fe8ed5 --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerTest.java @@ -0,0 +1,74 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import com.atlassian.hamcrest.DeepIsEqual; +import org.junit.Test; +import java.util.Arrays; +import java.util.List; +import example.github.Repository; +import feign.CollectionFormat; +import feign.MethodMetadata; +import feign.Request.HttpMethod; +import feign.apt.runtime.ParameterizedType; +import groovy.lang.Binding; +import groovy.lang.GroovyShell; + + +public class MethodMetadataSerializerTest { + + @Test + public void githubGetRepos() { + final MethodMetadata md = new MethodMetadata(); + md.returnType(new ParameterizedType(List.class, null, Repository.class)); + md.configKey("GitHub#repos(String)"); + + md.template().method(HttpMethod.GET); + md.template().uri("/users/{username}/repos?sort=full_name"); + md.template().decodeSlash(true); + md.template().collectionFormat(CollectionFormat.EXPLODED); + + md.indexToName().put(0, Arrays.asList("username")); + md.indexToEncoded().put(0, false); + + final StringBuilder result = MethodMetadataSerializer.toJavaCode(md); + assertEquals(result.toString(), + " final MethodMetadata md = new MethodMetadata();\n" + + " md.returnType(new feign.apt.runtime.ParameterizedType(java.util.List.class, null, example.github.Repository.class));\n" + + + " md.configKey(\"GitHub#repos(String)\");\n" + + "\n" + + " md.template().method(feign.Request.HttpMethod.GET);\n" + + " md.template().uri(\"/users/{username}/repos?sort=full_name\", false);\n" + + " md.template().decodeSlash(true);\n" + + " md.template().collectionFormat(CollectionFormat.EXPLODED);\n" + + "\n" + + " md.indexToName().put(0, Arrays.asList(\"username\"));\n" + + " md.indexToEncoded().put(0, false);\n"); + + final Binding binding = new Binding(); + binding.setVariable("foo", new Integer(2)); + final GroovyShell shell = new GroovyShell(binding); + + final MethodMetadata resultMetadata = (MethodMetadata) shell.evaluate("" + + "import feign.*;\n" + + result.toString() + + "\nreturn md;"); + + assertThat(resultMetadata, DeepIsEqual.deeplyEqualTo(md)); + } + +} diff --git a/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExampleIT.java b/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExampleIT.java new file mode 100644 index 0000000000..336cd6e094 --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExampleIT.java @@ -0,0 +1,45 @@ +/** + * Copyright 2012-2019 The Feign 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 feign.aptgenerator.github; + +import static org.junit.Assert.assertThat; +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.hamcrest.CoreMatchers; +import org.junit.Test; +import java.io.File; +import java.util.Arrays; + +/** + * Run main for {@link GitHubFactoryExample} + */ +public class GitHubFactoryExampleIT { + + @Test + public void runMain() throws Exception { + final String jar = Arrays.stream(new File("target").listFiles()) + .filter(file -> file.getName().startsWith("feign-apt-generator") + && file.getName().endsWith(".jar")) + .findFirst() + .map(File::getAbsolutePath) + .get(); + + final String line = "java -jar " + jar; + final CommandLine cmdLine = CommandLine.parse(line); + final int exitValue = new DefaultExecutor().execute(cmdLine); + + assertThat(exitValue, CoreMatchers.equalTo(0)); + } + +} diff --git a/core/pom.xml b/core/pom.xml index 4438468c87..58fcc1f16f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -32,6 +32,11 @@ + + org.projectlombok + lombok + + org.jvnet animal-sniffer-annotation diff --git a/core/src/main/java/feign/DeclarativeContract.java b/core/src/main/java/feign/DeclarativeContract.java index 8fc017597d..dab7f25edf 100644 --- a/core/src/main/java/feign/DeclarativeContract.java +++ b/core/src/main/java/feign/DeclarativeContract.java @@ -25,8 +25,8 @@ */ public abstract class DeclarativeContract extends BaseContract { - private List classAnnotationProcessors = new ArrayList<>(); - private List methodAnnotationProcessors = new ArrayList<>(); + private final List classAnnotationProcessors = new ArrayList<>(); + private final List methodAnnotationProcessors = new ArrayList<>(); Map, DeclarativeContract.ParameterAnnotationProcessor> parameterAnnotationProcessors = new HashMap<>(); @@ -168,8 +168,6 @@ public interface ParameterAnnotationProcessor { * @param metadata metadata collected so far relating to the current java method. * @param paramIndex if you find a name in {@code annotations}, call * {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter. - * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an - * http-relevant annotation. */ void process(E annotation, MethodMetadata metadata, int paramIndex); } @@ -177,8 +175,8 @@ public interface ParameterAnnotationProcessor { private class GuardedAnnotationProcessor implements Predicate, DeclarativeContract.AnnotationProcessor { - private Predicate predicate; - private DeclarativeContract.AnnotationProcessor processor; + private final Predicate predicate; + private final DeclarativeContract.AnnotationProcessor processor; @SuppressWarnings({"rawtypes", "unchecked"}) private GuardedAnnotationProcessor(Predicate predicate, @@ -193,10 +191,14 @@ public void process(Annotation annotation, MethodMetadata metadata) { } @Override - public boolean test(Annotation t) { - return predicate.test(t); + public boolean test(Annotation annotation) { + return predicate.test(annotation); } } + public List getClassAnnotationProcessors() { + return this.classAnnotationProcessors; + } + } diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index bd6cc0b9e1..6047cd4fc3 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -18,14 +18,13 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; -import feign.Logger.NoOpLogger; +import feign.FeignConfig.FeignConfigBuilder; import feign.ReflectiveFeign.ParseHandlersByName; import feign.Request.Options; import feign.Target.HardCodedTarget; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; -import static feign.ExceptionPropagationPolicy.NONE; /** * Feign's purpose is to ease development against http apis that feign restfulness.
@@ -35,7 +34,7 @@ public abstract class Feign { public static Builder builder() { - return new Builder(); + return new Builder(FeignConfig.builder()); } /** @@ -65,7 +64,7 @@ public static Builder builder() { * @see MethodMetadata#configKey() */ public static String configKey(Class targetType, Method method) { - StringBuilder builder = new StringBuilder(); + final StringBuilder builder = new StringBuilder(); builder.append(targetType.getSimpleName()); builder.append('#').append(method.getName()).append('('); for (Type param : method.getGenericParameterTypes()) { @@ -96,24 +95,17 @@ public static class Builder { private final List requestInterceptors = new ArrayList(); - private Logger.Level logLevel = Logger.Level.NONE; private Contract contract = new Contract.Default(); - private Client client = new Client.Default(null, null); - private Retryer retryer = new Retryer.Default(); - private Logger logger = new NoOpLogger(); - private Encoder encoder = new Encoder.Default(); - private Decoder decoder = new Decoder.Default(); - private QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default(); - private ErrorDecoder errorDecoder = new ErrorDecoder.Default(); - private Options options = new Options(); private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default(); - private boolean decode404; - private boolean closeAfterDecode = true; - private ExceptionPropagationPolicy propagationPolicy = NONE; + private final FeignConfigBuilder feignConfigBuilder; + + protected Builder(FeignConfigBuilder feignConfigBuilder) { + this.feignConfigBuilder = feignConfigBuilder; + } public Builder logLevel(Logger.Level logLevel) { - this.logLevel = logLevel; + feignConfigBuilder.logLevel(logLevel); return this; } @@ -123,32 +115,32 @@ public Builder contract(Contract contract) { } public Builder client(Client client) { - this.client = client; + feignConfigBuilder.client(client); return this; } public Builder retryer(Retryer retryer) { - this.retryer = retryer; + feignConfigBuilder.retryer(retryer); return this; } public Builder logger(Logger logger) { - this.logger = logger; + feignConfigBuilder.logger(logger); return this; } public Builder encoder(Encoder encoder) { - this.encoder = encoder; + feignConfigBuilder.encoder(encoder); return this; } public Builder decoder(Decoder decoder) { - this.decoder = decoder; + feignConfigBuilder.decoder(decoder); return this; } public Builder queryMapEncoder(QueryMapEncoder queryMapEncoder) { - this.queryMapEncoder = queryMapEncoder; + feignConfigBuilder.queryMapEncoder(queryMapEncoder); return this; } @@ -156,7 +148,7 @@ public Builder queryMapEncoder(QueryMapEncoder queryMapEncoder) { * Allows to map the response before passing it to the decoder. */ public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) { - this.decoder = new ResponseMappingDecoder(mapper, decoder); + feignConfigBuilder.decoder(new ResponseMappingDecoder(mapper, decoder)); return this; } @@ -178,17 +170,17 @@ public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) { * @since 8.12 */ public Builder decode404() { - this.decode404 = true; + feignConfigBuilder.decode404(true); return this; } public Builder errorDecoder(ErrorDecoder errorDecoder) { - this.errorDecoder = errorDecoder; + feignConfigBuilder.errorDecoder(errorDecoder); return this; } public Builder options(Options options) { - this.options = options; + feignConfigBuilder.options(options); return this; } @@ -196,7 +188,8 @@ public Builder options(Options options) { * Adds a single request interceptor to the builder. */ public Builder requestInterceptor(RequestInterceptor requestInterceptor) { - this.requestInterceptors.add(requestInterceptor); + requestInterceptors.add(requestInterceptor); + feignConfigBuilder.requestInterceptors(requestInterceptors); return this; } @@ -206,9 +199,10 @@ public Builder requestInterceptor(RequestInterceptor requestInterceptor) { */ public Builder requestInterceptors(Iterable requestInterceptors) { this.requestInterceptors.clear(); - for (RequestInterceptor requestInterceptor : requestInterceptors) { + for (final RequestInterceptor requestInterceptor : requestInterceptors) { this.requestInterceptors.add(requestInterceptor); } + feignConfigBuilder.requestInterceptors(this.requestInterceptors); return this; } @@ -234,12 +228,12 @@ public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandl * */ public Builder doNotCloseAfterDecode() { - this.closeAfterDecode = false; + feignConfigBuilder.closeAfterDecode(false); return this; } public Builder exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) { - this.propagationPolicy = propagationPolicy; + feignConfigBuilder.propagationPolicy(propagationPolicy); return this; } @@ -252,13 +246,13 @@ public T target(Target target) { } public Feign build() { - SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = - new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, - logLevel, decode404, closeAfterDecode, propagationPolicy); - ParseHandlersByName handlersByName = - new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, - errorDecoder, synchronousMethodHandlerFactory); - return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); + final FeignConfig feignConfig = feignConfigBuilder.build(); + final SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = + new SynchronousMethodHandler.Factory(feignConfig); + final ParseHandlersByName handlersByName = + new ParseHandlersByName(contract, feignConfig, synchronousMethodHandlerFactory); + return new ReflectiveFeign(handlersByName, invocationHandlerFactory, + feignConfig.queryMapEncoder); } } diff --git a/core/src/main/java/feign/FeignConfig.java b/core/src/main/java/feign/FeignConfig.java new file mode 100644 index 0000000000..52d410e498 --- /dev/null +++ b/core/src/main/java/feign/FeignConfig.java @@ -0,0 +1,64 @@ +/** + * Copyright 2012-2019 The Feign 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 feign; + +import static feign.ExceptionPropagationPolicy.NONE; +import java.util.ArrayList; +import java.util.List; +import feign.Logger.NoOpLogger; +import feign.Request.Options; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class FeignConfig { + + @Builder.Default + public final List requestInterceptors = new ArrayList(); + @Builder.Default + public final Logger.Level logLevel = Logger.Level.NONE; + @Builder.Default + public final Contract contract = new Contract.Default(); + @Builder.Default + public final Client client = new Client.Default(null, null); + @Builder.Default + public final Retryer retryer = new Retryer.Default(); + @Builder.Default + public final Logger logger = new NoOpLogger(); + @Builder.Default + public final Encoder encoder = new Encoder.Default(); + @Builder.Default + public final Decoder decoder = new Decoder.Default(); + @Builder.Default + public final QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default(); + @Builder.Default + public final ErrorDecoder errorDecoder = new ErrorDecoder.Default(); + @Builder.Default + public final Options options = new Options(); + @Builder.Default + public final InvocationHandlerFactory invocationHandlerFactory = + new InvocationHandlerFactory.Default(); + public final boolean decode404; + @Builder.Default + public final boolean closeAfterDecode = true; + @Builder.Default + public final ExceptionPropagationPolicy propagationPolicy = NONE; + public final String url; + +} diff --git a/core/src/main/java/feign/InvocationHandlerFactory.java b/core/src/main/java/feign/InvocationHandlerFactory.java index 73f4a84e2a..9b35542536 100644 --- a/core/src/main/java/feign/InvocationHandlerFactory.java +++ b/core/src/main/java/feign/InvocationHandlerFactory.java @@ -30,7 +30,7 @@ public interface InvocationHandlerFactory { */ interface MethodHandler { - Object invoke(Object[] argv) throws Throwable; + E invoke(Object... argv) throws Throwable; } static final class Default implements InvocationHandlerFactory { diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java index fef54018bf..c1e884e1d9 100644 --- a/core/src/main/java/feign/MethodMetadata.java +++ b/core/src/main/java/feign/MethodMetadata.java @@ -29,17 +29,17 @@ public final class MethodMetadata implements Serializable { private Integer queryMapIndex; private boolean queryMapEncoded; private transient Type bodyType; - private RequestTemplate template = new RequestTemplate(); - private List formParams = new ArrayList(); - private Map> indexToName = + private final RequestTemplate template = new RequestTemplate(); + private final List formParams = new ArrayList(); + private final Map> indexToName = new LinkedHashMap>(); - private Map> indexToExpanderClass = + private final Map> indexToExpanderClass = new LinkedHashMap>(); - private Map indexToEncoded = new LinkedHashMap(); + private final Map indexToEncoded = new LinkedHashMap(); private transient Map indexToExpander; private BitSet parameterToIgnore = new BitSet(); - MethodMetadata() {} + public MethodMetadata() {} /** * Used as a reference to this method. For example, {@link Logger#log(String, String, Object...) diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index a71bed0e68..b8a937baec 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -13,7 +13,8 @@ */ package feign; -import feign.template.UriUtils; +import static feign.Util.checkArgument; +import static feign.Util.checkNotNull; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -21,13 +22,9 @@ import java.util.Map.Entry; import feign.InvocationHandlerFactory.MethodHandler; import feign.Param.Expander; -import feign.Request.Options; -import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; -import feign.codec.ErrorDecoder; -import static feign.Util.checkArgument; -import static feign.Util.checkNotNull; +import feign.template.UriUtils; public class ReflectiveFeign extends Feign { @@ -49,26 +46,26 @@ public class ReflectiveFeign extends Feign { @SuppressWarnings("unchecked") @Override public T newInstance(Target target) { - Map nameToHandler = targetToHandlersByName.apply(target); - Map methodToHandler = new LinkedHashMap(); - List defaultMethodHandlers = new LinkedList(); + final Map nameToHandler = targetToHandlersByName.apply(target); + final Map methodToHandler = new LinkedHashMap(); + final List defaultMethodHandlers = new LinkedList(); - for (Method method : target.type().getMethods()) { + for (final Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if (Util.isDefault(method)) { - DefaultMethodHandler handler = new DefaultMethodHandler(method); + final DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } - InvocationHandler handler = factory.create(target, methodToHandler); - T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), + final InvocationHandler handler = factory.create(target, methodToHandler); + final T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[] {target.type()}, handler); - for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { + for (final DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; @@ -88,10 +85,10 @@ static class FeignInvocationHandler implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { - Object otherHandler = + final Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); - } catch (IllegalArgumentException e) { + } catch (final IllegalArgumentException e) { return false; } } else if ("hashCode".equals(method.getName())) { @@ -106,7 +103,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl @Override public boolean equals(Object obj) { if (obj instanceof FeignInvocationHandler) { - FeignInvocationHandler other = (FeignInvocationHandler) obj; + final FeignInvocationHandler other = (FeignInvocationHandler) obj; return target.equals(other.target); } return false; @@ -126,47 +123,43 @@ public String toString() { static final class ParseHandlersByName { private final Contract contract; - private final Options options; - private final Encoder encoder; - private final Decoder decoder; - private final ErrorDecoder errorDecoder; - private final QueryMapEncoder queryMapEncoder; + private final FeignConfig feignConfig; private final SynchronousMethodHandler.Factory factory; ParseHandlersByName( Contract contract, - Options options, - Encoder encoder, - Decoder decoder, - QueryMapEncoder queryMapEncoder, - ErrorDecoder errorDecoder, + FeignConfig feignConfig, SynchronousMethodHandler.Factory factory) { - this.contract = contract; - this.options = options; - this.factory = factory; - this.errorDecoder = errorDecoder; - this.queryMapEncoder = queryMapEncoder; - this.encoder = checkNotNull(encoder, "encoder"); - this.decoder = checkNotNull(decoder, "decoder"); + this.contract = checkNotNull(contract, "contract"); + this.feignConfig = checkNotNull(feignConfig, "feignConfig"); + this.factory = checkNotNull(factory, "factory"); } public Map apply(Target key) { - List metadata = contract.parseAndValidatateMetadata(key.type()); - Map result = new LinkedHashMap(); - for (MethodMetadata md : metadata) { - BuildTemplateByResolvingArgs buildTemplate; - if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { - buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder); - } else if (md.bodyIndex() != null) { - buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder); - } else { - buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder); - } + final List metadata = contract.parseAndValidatateMetadata(key.type()); + final Map result = new LinkedHashMap(); + for (final MethodMetadata md : metadata) { + final RequestTemplate.Factory buildTemplate = from(md, feignConfig); result.put(md.configKey(), - factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); + factory.create(key, md, buildTemplate, feignConfig.options, feignConfig.decoder, + feignConfig.errorDecoder)); } return result; } + + } + + public static RequestTemplate.Factory from(MethodMetadata md, FeignConfig feignConfig) { + + if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { + return new BuildFormEncodedTemplateFromArgs(md, feignConfig.encoder, + feignConfig.queryMapEncoder); + } else if (md.bodyIndex() != null) { + return new BuildEncodedTemplateFromArgs(md, feignConfig.encoder, + feignConfig.queryMapEncoder); + } else { + return new BuildTemplateByResolvingArgs(md, feignConfig.queryMapEncoder); + } } private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory { @@ -186,14 +179,14 @@ private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder qu if (metadata.indexToExpanderClass().isEmpty()) { return; } - for (Entry> indexToExpanderClass : metadata + for (final Entry> indexToExpanderClass : metadata .indexToExpanderClass().entrySet()) { try { indexToExpander .put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance()); - } catch (InstantiationException e) { + } catch (final InstantiationException e) { throw new IllegalStateException(e); - } catch (IllegalAccessException e) { + } catch (final IllegalAccessException e) { throw new IllegalStateException(e); } } @@ -201,21 +194,21 @@ private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder qu @Override public RequestTemplate create(Object[] argv) { - RequestTemplate mutable = RequestTemplate.from(metadata.template()); + final RequestTemplate mutable = RequestTemplate.from(metadata.template()); if (metadata.urlIndex() != null) { - int urlIndex = metadata.urlIndex(); + final int urlIndex = metadata.urlIndex(); checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex); mutable.target(String.valueOf(argv[urlIndex])); } - Map varBuilder = new LinkedHashMap(); - for (Entry> entry : metadata.indexToName().entrySet()) { - int i = entry.getKey(); + final Map varBuilder = new LinkedHashMap(); + for (final Entry> entry : metadata.indexToName().entrySet()) { + final int i = entry.getKey(); Object value = argv[entry.getKey()]; if (value != null) { // Null values are skipped. if (indexToExpander.containsKey(i)) { value = expandElements(indexToExpander.get(i), value); } - for (String name : entry.getValue()) { + for (final String name : entry.getValue()) { varBuilder.put(name, value); } } @@ -225,8 +218,8 @@ public RequestTemplate create(Object[] argv) { if (metadata.queryMapIndex() != null) { // add query map parameters after initial resolve so that they take // precedence over any predefined values - Object value = argv[metadata.queryMapIndex()]; - Map queryMap = toQueryMap(value); + final Object value = argv[metadata.queryMapIndex()]; + final Map queryMap = toQueryMap(value); template = addQueryMapQueryParameters(queryMap, template); } @@ -244,7 +237,7 @@ private Map toQueryMap(Object value) { } try { return queryMapEncoder.encode(value); - } catch (EncodeException e) { + } catch (final EncodeException e) { throw new IllegalStateException(e); } } @@ -257,8 +250,8 @@ private Object expandElements(Expander expander, Object value) { } private List expandIterable(Expander expander, Iterable value) { - List values = new ArrayList(); - for (Object element : value) { + final List values = new ArrayList(); + for (final Object element : value) { if (element != null) { values.add(expander.expand(element)); } @@ -269,14 +262,14 @@ private List expandIterable(Expander expander, Iterable value) { @SuppressWarnings("unchecked") private RequestTemplate addHeaderMapHeaders(Map headerMap, RequestTemplate mutable) { - for (Entry currEntry : headerMap.entrySet()) { - Collection values = new ArrayList(); + for (final Entry currEntry : headerMap.entrySet()) { + final Collection values = new ArrayList(); - Object currValue = currEntry.getValue(); + final Object currValue = currEntry.getValue(); if (currValue instanceof Iterable) { - Iterator iter = ((Iterable) currValue).iterator(); + final Iterator iter = ((Iterable) currValue).iterator(); while (iter.hasNext()) { - Object nextObject = iter.next(); + final Object nextObject = iter.next(); values.add(nextObject == null ? null : nextObject.toString()); } } else { @@ -291,15 +284,15 @@ private RequestTemplate addHeaderMapHeaders(Map headerMap, @SuppressWarnings("unchecked") private RequestTemplate addQueryMapQueryParameters(Map queryMap, RequestTemplate mutable) { - for (Entry currEntry : queryMap.entrySet()) { - Collection values = new ArrayList(); + for (final Entry currEntry : queryMap.entrySet()) { + final Collection values = new ArrayList(); - boolean encoded = metadata.queryMapEncoded(); - Object currValue = currEntry.getValue(); + final boolean encoded = metadata.queryMapEncoded(); + final Object currValue = currEntry.getValue(); if (currValue instanceof Iterable) { - Iterator iter = ((Iterable) currValue).iterator(); + final Iterator iter = ((Iterable) currValue).iterator(); while (iter.hasNext()) { - Object nextObject = iter.next(); + final Object nextObject = iter.next(); values.add(nextObject == null ? null : encoded ? nextObject.toString() : UriUtils.encode(nextObject.toString())); @@ -335,17 +328,17 @@ private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encode protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { - Map formVariables = new LinkedHashMap(); - for (Entry entry : variables.entrySet()) { + final Map formVariables = new LinkedHashMap(); + for (final Entry entry : variables.entrySet()) { if (metadata.formParams().contains(entry.getKey())) { formVariables.put(entry.getKey(), entry.getValue()); } } try { encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable); - } catch (EncodeException e) { + } catch (final EncodeException e) { throw e; - } catch (RuntimeException e) { + } catch (final RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); @@ -366,13 +359,13 @@ private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { - Object body = argv[metadata.bodyIndex()]; + final Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); try { encoder.encode(body, metadata.bodyType(), mutable); - } catch (EncodeException e) { + } catch (final EncodeException e) { throw e; - } catch (RuntimeException e) { + } catch (final RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 164b95ebee..a1a8e8aa30 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -89,6 +89,10 @@ public boolean isBinary() { return encoding == null || data == null; } + public Charset encoding() { + return encoding; + } + } public enum HttpMethod { @@ -243,6 +247,12 @@ public static class Options { private final TimeUnit readTimeoutUnit; private final boolean followRedirects; + @Override + public String toString() { + return "Options [connectTimeout=" + connectTimeout + ", connectTimeoutUnit=" + + connectTimeoutUnit + ", readTimeout=" + readTimeout + ", readTimeoutUnit=" + + readTimeoutUnit + ", followRedirects=" + followRedirects + "]"; + } public Options(long connectTimeout, TimeUnit connectTimeoutUnit, long readTimeout, TimeUnit readTimeoutUnit, diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index 88215c7dd8..03891135d0 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -51,6 +51,8 @@ public final class RequestTemplate implements Serializable { private Request.Body body = Request.Body.empty(); private boolean decodeSlash = true; private CollectionFormat collectionFormat = CollectionFormat.EXPLODED; + private String uri; + private boolean uriAppend; /** * Create a new Request Template. @@ -402,6 +404,8 @@ public RequestTemplate uri(String uri, boolean append) { if (UriUtils.isAbsolute(uri)) { throw new IllegalArgumentException("url values must be not be absolute."); } + this.uri = uri; + this.uriAppend = append; if (uri == null) { uri = "/"; @@ -931,4 +935,12 @@ interface Factory { RequestTemplate create(Object[] argv); } + public String uri() { + return uri; + } + + public boolean uriAppend() { + return uriAppend; + } + } diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java index ad99b35d0d..1653e218cb 100644 --- a/core/src/main/java/feign/SynchronousMethodHandler.java +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -13,8 +13,12 @@ */ package feign; +import static feign.ExceptionPropagationPolicy.UNWRAP; +import static feign.FeignException.errorExecuting; +import static feign.FeignException.errorReading; +import static feign.Util.checkNotNull; +import static feign.Util.ensureClosed; import java.io.IOException; -import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import feign.InvocationHandlerFactory.MethodHandler; @@ -22,59 +26,33 @@ import feign.codec.DecodeException; import feign.codec.Decoder; import feign.codec.ErrorDecoder; -import static feign.ExceptionPropagationPolicy.UNWRAP; -import static feign.FeignException.errorExecuting; -import static feign.FeignException.errorReading; -import static feign.Util.checkNotNull; -import static feign.Util.ensureClosed; -final class SynchronousMethodHandler implements MethodHandler { +public final class SynchronousMethodHandler implements MethodHandler { private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L; private final MethodMetadata metadata; private final Target target; - private final Client client; - private final Retryer retryer; - private final List requestInterceptors; - private final Logger logger; - private final Logger.Level logLevel; private final RequestTemplate.Factory buildTemplateFromArgs; - private final Options options; - private final Decoder decoder; - private final ErrorDecoder errorDecoder; - private final boolean decode404; - private final boolean closeAfterDecode; - private final ExceptionPropagationPolicy propagationPolicy; - - private SynchronousMethodHandler(Target target, Client client, Retryer retryer, - List requestInterceptors, Logger logger, - Logger.Level logLevel, MethodMetadata metadata, - RequestTemplate.Factory buildTemplateFromArgs, Options options, - Decoder decoder, ErrorDecoder errorDecoder, boolean decode404, - boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy) { + + private final FeignConfig feignConfig; + + public SynchronousMethodHandler( + Target target, + FeignConfig feignConfig, + MethodMetadata metadata, + RequestTemplate.Factory buildTemplateFromArgs) { this.target = checkNotNull(target, "target"); - this.client = checkNotNull(client, "client for %s", target); - this.retryer = checkNotNull(retryer, "retryer for %s", target); - this.requestInterceptors = - checkNotNull(requestInterceptors, "requestInterceptors for %s", target); - this.logger = checkNotNull(logger, "logger for %s", target); - this.logLevel = checkNotNull(logLevel, "logLevel for %s", target); + this.feignConfig = checkNotNull(feignConfig, "feignConfig for %s", target); this.metadata = checkNotNull(metadata, "metadata for %s", target); this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target); - this.options = checkNotNull(options, "options for %s", target); - this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target); - this.decoder = checkNotNull(decoder, "decoder for %s", target); - this.decode404 = decode404; - this.closeAfterDecode = closeAfterDecode; - this.propagationPolicy = propagationPolicy; } @Override - public Object invoke(Object[] argv) throws Throwable { + public Object invoke(Object... argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Options options = findOptions(argv); - Retryer retryer = this.retryer.clone(); + Retryer retryer = feignConfig.retryer.clone(); while (true) { try { return executeAndDecode(template, options); @@ -83,14 +61,14 @@ public Object invoke(Object[] argv) throws Throwable { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); - if (propagationPolicy == UNWRAP && cause != null) { + if (feignConfig.propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } - if (logLevel != Logger.Level.NONE) { - logger.logRetry(metadata.configKey(), logLevel); + if (feignConfig.logLevel != Logger.Level.NONE) { + feignConfig.logger.logRetry(metadata.configKey(), feignConfig.logLevel); } continue; } @@ -98,19 +76,20 @@ public Object invoke(Object[] argv) throws Throwable { } Object executeAndDecode(RequestTemplate template, Options options) throws Throwable { - Request request = targetRequest(template); + final Request request = targetRequest(template); - if (logLevel != Logger.Level.NONE) { - logger.logRequest(metadata.configKey(), logLevel, request); + if (feignConfig.logLevel != Logger.Level.NONE) { + feignConfig.logger.logRequest(metadata.configKey(), feignConfig.logLevel, request); } Response response; long start = System.nanoTime(); try { - response = client.execute(request, options); + response = feignConfig.client.execute(request, options); } catch (IOException e) { - if (logLevel != Logger.Level.NONE) { - logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); + if (feignConfig.logLevel != Logger.Level.NONE) { + feignConfig.logger.logIOException(metadata.configKey(), feignConfig.logLevel, e, + elapsedTime(start)); } throw errorExecuting(request, e); } @@ -118,9 +97,10 @@ Object executeAndDecode(RequestTemplate template, Options options) throws Throwa boolean shouldClose = true; try { - if (logLevel != Logger.Level.NONE) { + if (feignConfig.logLevel != Logger.Level.NONE) { response = - logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); + feignConfig.logger.logAndRebufferResponse(metadata.configKey(), feignConfig.logLevel, + response, elapsedTime); } if (Response.class == metadata.returnType()) { if (response.body() == null) { @@ -140,19 +120,21 @@ Object executeAndDecode(RequestTemplate template, Options options) throws Throwa return null; } else { Object result = decode(response); - shouldClose = closeAfterDecode; + shouldClose = feignConfig.closeAfterDecode; return result; } - } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) { + } else if (feignConfig.decode404 && response.status() == 404 + && void.class != metadata.returnType()) { Object result = decode(response); - shouldClose = closeAfterDecode; + shouldClose = feignConfig.closeAfterDecode; return result; } else { - throw errorDecoder.decode(metadata.configKey(), response); + throw feignConfig.errorDecoder.decode(metadata.configKey(), response); } } catch (IOException e) { - if (logLevel != Logger.Level.NONE) { - logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); + if (feignConfig.logLevel != Logger.Level.NONE) { + feignConfig.logger.logIOException(metadata.configKey(), feignConfig.logLevel, e, + elapsedTime); } throw errorReading(request, response, e); } finally { @@ -167,7 +149,7 @@ long elapsedTime(long start) { } Request targetRequest(RequestTemplate template) { - for (RequestInterceptor interceptor : requestInterceptors) { + for (RequestInterceptor interceptor : feignConfig.requestInterceptors) { interceptor.apply(template); } return target.apply(template); @@ -175,7 +157,7 @@ Request targetRequest(RequestTemplate template) { Object decode(Response response) throws Throwable { try { - return decoder.decode(response, metadata.returnType()); + return feignConfig.decoder.decode(response, metadata.returnType()); } catch (FeignException e) { throw e; } catch (RuntimeException e) { @@ -185,37 +167,21 @@ Object decode(Response response) throws Throwable { Options findOptions(Object[] argv) { if (argv == null || argv.length == 0) { - return this.options; + return feignConfig.options; } return Stream.of(argv) .filter(Options.class::isInstance) .map(Options.class::cast) .findFirst() - .orElse(this.options); + .orElse(feignConfig.options); } static class Factory { - private final Client client; - private final Retryer retryer; - private final List requestInterceptors; - private final Logger logger; - private final Logger.Level logLevel; - private final boolean decode404; - private final boolean closeAfterDecode; - private final ExceptionPropagationPolicy propagationPolicy; - - Factory(Client client, Retryer retryer, List requestInterceptors, - Logger logger, Logger.Level logLevel, boolean decode404, boolean closeAfterDecode, - ExceptionPropagationPolicy propagationPolicy) { - this.client = checkNotNull(client, "client"); - this.retryer = checkNotNull(retryer, "retryer"); - this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); - this.logger = checkNotNull(logger, "logger"); - this.logLevel = checkNotNull(logLevel, "logLevel"); - this.decode404 = decode404; - this.closeAfterDecode = closeAfterDecode; - this.propagationPolicy = propagationPolicy; + private final FeignConfig feignConfig; + + Factory(FeignConfig feignConfig) { + this.feignConfig = checkNotNull(feignConfig, "feignConfig"); } public MethodHandler create(Target target, @@ -224,9 +190,7 @@ public MethodHandler create(Target target, Options options, Decoder decoder, ErrorDecoder errorDecoder) { - return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, - logLevel, md, buildTemplateFromArgs, options, decoder, - errorDecoder, decode404, closeAfterDecode, propagationPolicy); + return new SynchronousMethodHandler(target, feignConfig, md, buildTemplateFromArgs); } } } diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index e144f90f5f..00e0552e92 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -31,7 +31,7 @@ import static org.assertj.core.data.MapEntry.entry; /** - * Tests interfaces defined per {@link Contract.Default} are interpreted into expected + * Tests public interface s defined per {@link Contract.Default} are interpreted into expected * {@link feign .RequestTemplate template} instances. */ public class DefaultContractTest { @@ -387,7 +387,7 @@ public void headerMapSubclass() throws Exception { assertThat(md.headerMapIndex()).isEqualTo(0); } - interface Methods { + public interface Methods { @RequestLine("POST /") void post(); @@ -402,7 +402,7 @@ interface Methods { void delete(); } - interface BodyParams { + public interface BodyParams { @RequestLine("POST") Response post(List body); @@ -414,13 +414,13 @@ interface BodyParams { Response tooMany(List body, List body2); } - interface CustomMethod { + public interface CustomMethod { @RequestLine("PATCH") Response patch(); } - interface WithQueryParamsInPath { + public interface WithQueryParamsInPath { @RequestLine("GET /") Response none(); @@ -444,7 +444,7 @@ interface WithQueryParamsInPath { Response twoEmpty(); } - interface BodyWithoutParameters { + public interface BodyWithoutParameters { @RequestLine("POST /") @Headers("Content-Type: application/xml") @@ -453,7 +453,7 @@ interface BodyWithoutParameters { } @Headers("Content-Type: application/xml") - interface HeadersOnType { + public interface HeadersOnType { @RequestLine("POST /") @Body("") @@ -461,20 +461,20 @@ interface HeadersOnType { } @Headers("Content-Type: application/xml ") - interface HeadersContainsWhitespaces { + public interface HeadersContainsWhitespaces { @RequestLine("POST /") @Body("") Response post(); } - interface WithURIParam { + public interface WithURIParam { @RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); } - interface WithPathAndQueryParams { + public interface WithPathAndQueryParams { @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}") Response recordsByNameAndType(@Param("domainId") int id, @@ -482,7 +482,7 @@ Response recordsByNameAndType(@Param("domainId") int id, @Param("type") String typeFilter); } - interface FormParams { + public interface FormParams { @RequestLine("POST /") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") @@ -492,7 +492,7 @@ void login( @Param("password") String password); } - interface HeaderMapInterface { + public interface HeaderMapInterface { @RequestLine("POST /") void multipleHeaderMap(@HeaderMap Map headers, @@ -502,21 +502,21 @@ void multipleHeaderMap(@HeaderMap Map headers, void headerMapSubClass(@HeaderMap SubClassHeaders httpHeaders); } - interface HeaderParams { + public interface HeaderParams { @RequestLine("POST /") @Headers({"Auth-Token: {authToken}", "Auth-Token: Foo"}) void logout(@Param("authToken") String token); } - interface HeaderParamsNotAtStart { + public interface HeaderParamsNotAtStart { @RequestLine("POST /") @Headers({"Authorization: Bearer {authToken}", "Authorization: Foo"}) void logout(@Param("authToken") String token); } - interface CustomExpander { + public interface CustomExpander { @RequestLine("POST /?date={date}") void date(@Param(value = "date", expander = DateToMillis.class) Date date); @@ -530,7 +530,7 @@ public String expand(Object value) { } } - interface QueryMapTestInterface { + public interface QueryMapTestInterface { @RequestLine("POST /") void queryMap(@QueryMap Map queryMap); @@ -563,7 +563,7 @@ void multipleQueryMap(@QueryMap Map mapOne, void nonStringKeyQueryMap(@QueryMap Map queryMap); } - interface SlashNeedToBeEncoded { + public interface SlashNeedToBeEncoded { @RequestLine(value = "GET /api/queues/{vhost}", decodeSlash = false) String getQueues(@Param("vhost") String vhost); @@ -572,13 +572,13 @@ interface SlashNeedToBeEncoded { } @Headers("Foo: Bar") - interface SimpleParameterizedBaseApi { + public interface SimpleParameterizedBaseApi { @RequestLine("GET /api/{zoneId}") M get(@Param("key") String key); } - interface SimpleParameterizedApi extends SimpleParameterizedBaseApi { + public interface SimpleParameterizedApi extends SimpleParameterizedBaseApi { } @@ -603,7 +603,7 @@ public void parameterizedApiUnsupported() throws Exception { contract.parseAndValidatateMetadata(SimpleParameterizedBaseApi.class); } - interface OverrideParameterizedApi extends SimpleParameterizedBaseApi { + public interface OverrideParameterizedApi extends SimpleParameterizedBaseApi { @Override @RequestLine("GET /api/{zoneId}") @@ -617,11 +617,11 @@ public void overrideBaseApiUnsupported() throws Exception { contract.parseAndValidatateMetadata(OverrideParameterizedApi.class); } - interface Child extends SimpleParameterizedBaseApi> { + public interface Child extends SimpleParameterizedBaseApi> { } - interface GrandChild extends Child { + public interface GrandChild extends Child { } @@ -633,7 +633,7 @@ public void onlySingleLevelInheritanceSupported() throws Exception { } @Headers("Foo: Bar") - interface ParameterizedBaseApi { + public interface ParameterizedBaseApi { @RequestLine("GET /api/{key}") Entity get(@Param("key") K key); @@ -659,13 +659,13 @@ static class Entities { } - interface SubClassHeaders extends Map { + public interface SubClassHeaders extends Map { } @Headers("Version: 1") - interface ParameterizedApi extends ParameterizedBaseApi { + public interface ParameterizedApi extends ParameterizedBaseApi { } @@ -697,7 +697,7 @@ public void parameterizedBaseApi() throws Exception { } @Headers("Authorization: {authHdr}") - interface ParameterizedHeaderExpandApi { + public interface ParameterizedHeaderExpandApi { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); @@ -743,17 +743,17 @@ public void parameterizedHeaderNotStartingWithCurlyBraceExpandApi() throws Excep } @Headers("Authorization: Bearer {authHdr}") - interface ParameterizedHeaderNotStartingWithCurlyBraceExpandApi { + public interface ParameterizedHeaderNotStartingWithCurlyBraceExpandApi { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); } @Headers("Authorization: {authHdr}") - interface ParameterizedHeaderBase { + public interface ParameterizedHeaderBase { } - interface ParameterizedHeaderExpandInheritedApi extends ParameterizedHeaderBase { + public interface ParameterizedHeaderExpandInheritedApi extends ParameterizedHeaderBase { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZoneAccept(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); @@ -804,7 +804,7 @@ private MethodMetadata parseAndValidateMetadata(Class targetType, targetType.getMethod(method, parameterTypes)); } - interface MissingMethod { + public interface MissingMethod { @RequestLine("/path?queryParam={queryParam}") Response updateSharing(@Param("queryParam") long queryParam, String bodyParam); } @@ -819,7 +819,7 @@ public void missingMethod() throws Exception { contract.parseAndValidatateMetadata(MissingMethod.class); } - interface StaticMethodOnInterface { + public interface StaticMethodOnInterface { @RequestLine("GET /api/{key}") String get(@Param("key") String key); @@ -836,7 +836,7 @@ public void staticMethodsOnInterfaceIgnored() throws Exception { assertThat(md.configKey()).isEqualTo("StaticMethodOnInterface#get(String)"); } - interface DefaultMethodOnInterface { + public interface DefaultMethodOnInterface { @RequestLine("GET /api/{key}") String get(@Param("key") String key); @@ -853,7 +853,7 @@ public void defaultMethodsOnInterfaceIgnored() throws Exception { assertThat(md.configKey()).isEqualTo("DefaultMethodOnInterface#get(String)"); } - interface SubstringQuery { + public interface SubstringQuery { @RequestLine("GET /_search?q=body:{body}") String paramIsASubstringOfAQuery(@Param("body") String body); } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 317e43b5ee..f7056c7856 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -832,7 +832,7 @@ private Response responseWithText(String text) { public void mapAndDecodeExecutesMapFunction() throws Exception { server.enqueue(new MockResponse().setBody("response!")); - TestInterface api = new Feign.Builder() + TestInterface api = Feign.builder() .mapAndDecode(upperCaseResponseMapper(), new StringDecoder()) .target(TestInterface.class, "http://localhost:" + server.getPort()); @@ -1056,7 +1056,7 @@ public Exception decode(String methodKey, Response response) { static final class TestInterfaceBuilder { - private final Feign.Builder delegate = new Feign.Builder() + private final Feign.Builder delegate = Feign.builder() .decoder(new Decoder.Default()) .encoder(new Encoder() { @Override diff --git a/example-github/src/main/java/example/github/Contributor.java b/example-github/src/main/java/example/github/Contributor.java new file mode 100644 index 0000000000..6f1c39a25b --- /dev/null +++ b/example-github/src/main/java/example/github/Contributor.java @@ -0,0 +1,18 @@ +/** + * Copyright 2012-2019 The Feign 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 example.github; + +public class Contributor { + public String login; +} diff --git a/example-github/src/main/java/example/github/GitHubClientError.java b/example-github/src/main/java/example/github/GitHubClientError.java new file mode 100644 index 0000000000..f0bb4533c7 --- /dev/null +++ b/example-github/src/main/java/example/github/GitHubClientError.java @@ -0,0 +1,23 @@ +/** + * Copyright 2012-2019 The Feign 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 example.github; + +public class GitHubClientError extends RuntimeException { + private String message; // parsed from json + + @Override + public String getMessage() { + return message; + } +} diff --git a/example-github/src/main/java/example/github/GitHubExample.java b/example-github/src/main/java/example/github/GitHubExample.java index 4a7d3bc45d..9047c75143 100644 --- a/example-github/src/main/java/example/github/GitHubExample.java +++ b/example-github/src/main/java/example/github/GitHubExample.java @@ -13,15 +13,15 @@ */ package example.github; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; import feign.*; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.gson.GsonDecoder; import feign.gson.GsonEncoder; -import java.io.IOException; -import java.util.List; -import java.util.stream.Collectors; /** * Inspired by {@code com.example.retrofit.GitHubClient} @@ -30,28 +30,7 @@ public class GitHubExample { private static final String GITHUB_TOKEN = "GITHUB_TOKEN"; - interface GitHub { - - class Repository { - String name; - } - - class Contributor { - String login; - } - - class Issue { - - Issue() { - - } - - String title; - String body; - List assignees; - int milestone; - List labels; - } + public interface GitHub { @RequestLine("GET /users/{username}/repos?sort=full_name") List repos(@Param("username") String owner); @@ -64,7 +43,9 @@ class Issue { /** Lists all contributors for all repos owned by a user. */ default List contributors(String owner) { - return repos(owner).stream() + return repos(owner) + .stream() + .peek(System.out::println) .flatMap(repo -> contributors(owner, repo.name).stream()) .map(c -> c.login) .distinct() @@ -72,8 +53,8 @@ default List contributors(String owner) { } static GitHub connect() { - Decoder decoder = new GsonDecoder(); - Encoder encoder = new GsonEncoder(); + final Decoder decoder = new GsonDecoder(); + final Encoder encoder = new GsonEncoder(); return Feign.builder() .encoder(encoder) .decoder(decoder) @@ -93,48 +74,39 @@ static GitHub connect() { } - static class GitHubClientError extends RuntimeException { - private String message; // parsed from json - - @Override - public String getMessage() { - return message; - } - } - public static void main(String... args) { - GitHub github = GitHub.connect(); + final GitHub github = GitHub.connect(); System.out.println("Let's fetch and print a list of the contributors to this org."); - List contributors = github.contributors("openfeign"); - for (String contributor : contributors) { + final List contributors = github.contributors("openfeign"); + for (final String contributor : contributors) { System.out.println(contributor); } System.out.println("Now, let's cause an error."); try { github.contributors("openfeign", "some-unknown-project"); - } catch (GitHubClientError e) { + } catch (final GitHubClientError e) { System.out.println(e.getMessage()); } System.out.println("Now, try to create an issue - which will also cause an error."); try { - GitHub.Issue issue = new GitHub.Issue(); + final Issue issue = new Issue(); issue.title = "The title"; issue.body = "Some Text"; github.createIssue(issue, "OpenFeign", "SomeRepo"); - } catch (GitHubClientError e) { + } catch (final GitHubClientError e) { System.out.println(e.getMessage()); } } - static class GitHubErrorDecoder implements ErrorDecoder { + public static class GitHubErrorDecoder implements ErrorDecoder { final Decoder decoder; final ErrorDecoder defaultDecoder = new ErrorDecoder.Default(); - GitHubErrorDecoder(Decoder decoder) { + public GitHubErrorDecoder(Decoder decoder) { this.decoder = decoder; } @@ -143,8 +115,10 @@ public Exception decode(String methodKey, Response response) { try { // must replace status by 200 other GSONDecoder returns null response = response.toBuilder().status(200).build(); - return (Exception) decoder.decode(response, GitHubClientError.class); - } catch (IOException fallbackToDefault) { + return (Exception) decoder.decode( + response.toBuilder().status(200).build(), + GitHubClientError.class); + } catch (final IOException fallbackToDefault) { return defaultDecoder.decode(methodKey, response); } } diff --git a/example-github/src/main/java/example/github/Issue.java b/example-github/src/main/java/example/github/Issue.java new file mode 100644 index 0000000000..7b80e23942 --- /dev/null +++ b/example-github/src/main/java/example/github/Issue.java @@ -0,0 +1,24 @@ +/** + * Copyright 2012-2019 The Feign 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 example.github; + +import java.util.List; + +public class Issue { + public String title; + public String body; + public List assignees; + public int milestone; + public List labels; +} diff --git a/example-github/src/main/java/example/github/Repository.java b/example-github/src/main/java/example/github/Repository.java new file mode 100644 index 0000000000..22f66d82a7 --- /dev/null +++ b/example-github/src/main/java/example/github/Repository.java @@ -0,0 +1,18 @@ +/** + * Copyright 2012-2019 The Feign 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 example.github; + +public class Repository { + public String name; +} diff --git a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java index cbd13d46f2..8c0ad36281 100644 --- a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java +++ b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java @@ -14,19 +14,8 @@ package feign.hystrix; import com.netflix.hystrix.HystrixCommand; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.util.Map; -import feign.Client; -import feign.Contract; -import feign.Feign; -import feign.InvocationHandlerFactory; -import feign.Logger; -import feign.Request; -import feign.RequestInterceptor; -import feign.ResponseMapper; -import feign.Retryer; -import feign.Target; +import feign.*; +import feign.FeignConfig.FeignConfigBuilder; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; @@ -39,7 +28,7 @@ public final class HystrixFeign { public static Builder builder() { - return new Builder(); + return new Builder(FeignConfig.builder()); } public static final class Builder extends Feign.Builder { @@ -47,6 +36,10 @@ public static final class Builder extends Feign.Builder { private Contract contract = new Contract.Default(); private SetterFactory setterFactory = new SetterFactory.Default(); + public Builder(FeignConfigBuilder feignConfigBuilder) { + super(feignConfigBuilder); + } + /** * Allows you to override hystrix properties such as thread pools and command keys. */ @@ -80,7 +73,7 @@ public T target(Target target, FallbackFactory fallbackFacto * use this feature, pass a safe implementation of your target interface as the last parameter. * * Here's an example: - * + * *
      * {@code
      *
@@ -139,14 +132,9 @@ public Feign build() {
 
     /** Configures components needed for hystrix integration. */
     Feign build(final FallbackFactory nullableFallbackFactory) {
-      super.invocationHandlerFactory(new InvocationHandlerFactory() {
-        @Override
-        public InvocationHandler create(Target target,
-                                        Map dispatch) {
-          return new HystrixInvocationHandler(target, dispatch, setterFactory,
-              nullableFallbackFactory);
-        }
-      });
+      super.invocationHandlerFactory(
+          (target, dispatch) -> new HystrixInvocationHandler(target, dispatch, setterFactory,
+              nullableFallbackFactory));
       super.contract(new HystrixDelegatingContract(contract));
       return super.build();
     }
diff --git a/pom.xml b/pom.xml
index 81c20e278f..c3c9a06efc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,7 @@
     slf4j
     soap
     reactive
+    apt-generator
     example-github
     example-wikipedia
     
@@ -62,10 +63,6 @@
     1.8
     java18
 
-    
-    1.8
-    1.8
-
     ${project.basedir}
 
     3.6.0
@@ -331,6 +328,13 @@
         ${slf4j.version}
       
 
+      
+      
+        org.projectlombok
+        lombok
+        1.18.6
+        provided
+      
     
   
 
@@ -398,6 +402,11 @@
       
         maven-compiler-plugin
         true
+        
+          ${main.java.version}
+          ${main.java.version}
+          ${main.java.version}
+        
         
           
           
@@ -406,10 +415,6 @@
             
               compile
             
-            
-              ${main.java.version}
-              ${main.java.version}
-            
           
         
       
diff --git a/reactive/src/main/java/feign/reactive/ReactiveFeign.java b/reactive/src/main/java/feign/reactive/ReactiveFeign.java
index 96535c0fc1..47323f4277 100644
--- a/reactive/src/main/java/feign/reactive/ReactiveFeign.java
+++ b/reactive/src/main/java/feign/reactive/ReactiveFeign.java
@@ -15,7 +15,7 @@
 
 import feign.Contract;
 import feign.Feign;
-import feign.InvocationHandlerFactory;
+import feign.FeignConfig.FeignConfigBuilder;
 
 abstract class ReactiveFeign {
 
@@ -23,6 +23,10 @@ abstract class ReactiveFeign {
 
   public static class Builder extends Feign.Builder {
 
+    protected Builder(FeignConfigBuilder feignConfigBuilder) {
+      super(feignConfigBuilder);
+    }
+
     private Contract contract = new Contract.Default();
 
     /**
diff --git a/reactive/src/main/java/feign/reactive/ReactorFeign.java b/reactive/src/main/java/feign/reactive/ReactorFeign.java
index 9fb33f408b..4f565efb5a 100644
--- a/reactive/src/main/java/feign/reactive/ReactorFeign.java
+++ b/reactive/src/main/java/feign/reactive/ReactorFeign.java
@@ -13,23 +13,26 @@
  */
 package feign.reactive;
 
-import feign.Feign;
-import reactor.core.scheduler.Scheduler;
-import reactor.core.scheduler.Schedulers;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.util.Map;
-import feign.InvocationHandlerFactory;
-import feign.Target;
+import feign.*;
+import feign.FeignConfig.FeignConfigBuilder;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
 
 public class ReactorFeign extends ReactiveFeign {
 
   public static Builder builder() {
-    return new Builder();
+    return new Builder(FeignConfig.builder());
   }
 
   public static class Builder extends ReactiveFeign.Builder {
 
+    protected Builder(FeignConfigBuilder feignConfigBuilder) {
+      super(feignConfigBuilder);
+    }
+
     private Scheduler scheduler = Schedulers.elastic();
 
     @Override
diff --git a/reactive/src/main/java/feign/reactive/RxJavaFeign.java b/reactive/src/main/java/feign/reactive/RxJavaFeign.java
index 8554a3e552..3c10a076b1 100644
--- a/reactive/src/main/java/feign/reactive/RxJavaFeign.java
+++ b/reactive/src/main/java/feign/reactive/RxJavaFeign.java
@@ -16,20 +16,23 @@
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.util.Map;
-import feign.Feign;
-import feign.InvocationHandlerFactory;
-import feign.Target;
+import feign.*;
+import feign.FeignConfig.FeignConfigBuilder;
 import io.reactivex.Scheduler;
 import io.reactivex.schedulers.Schedulers;
 
 public class RxJavaFeign extends ReactiveFeign {
 
   public static Builder builder() {
-    return new Builder();
+    return new Builder(FeignConfig.builder());
   }
 
   public static class Builder extends ReactiveFeign.Builder {
 
+    protected Builder(FeignConfigBuilder feignConfigBuilder) {
+      super(feignConfigBuilder);
+    }
+
     private Scheduler scheduler = Schedulers.trampoline();
 
     @Override